diff options
1171 files changed, 19913 insertions, 12814 deletions
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ab30e1789d..104f1cfeed 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,10 +4,10 @@ "features": { // installs nodejs into container "ghcr.io/devcontainers/features/node:1": { - "version": "20" + "version": "lts" }, "ghcr.io/devcontainers/features/git-lfs:1.2.2": {}, - "ghcr.io/devcontainers-contrib/features/poetry:2": {}, + "ghcr.io/devcontainers-extra/features/poetry:2": {}, "ghcr.io/devcontainers/features/python:1": { "version": "3.12" }, diff --git a/.dockerignore b/.dockerignore index 94aca6b8d3..843f12a7be 100644 --- a/.dockerignore +++ b/.dockerignore @@ -36,15 +36,6 @@ _testmain.go coverage.all cpu.out -/modules/migration/bindata.go -/modules/migration/bindata.go.hash -/modules/options/bindata.go -/modules/options/bindata.go.hash -/modules/public/bindata.go -/modules/public/bindata.go.hash -/modules/templates/bindata.go -/modules/templates/bindata.go.hash - *.db *.log diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f9e1050240..57c6b19600 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -91,6 +91,7 @@ module.exports = { plugins: ['@vitest/eslint-plugin'], globals: vitestPlugin.environments.env.globals, rules: { + 'github/unescaped-html-literal': [0], '@vitest/consistent-test-filename': [0], '@vitest/consistent-test-it': [0], '@vitest/expect-expect': [0], @@ -423,7 +424,7 @@ module.exports = { 'github/no-useless-passive': [2], 'github/prefer-observers': [2], 'github/require-passive-events': [2], - 'github/unescaped-html-literal': [0], + 'github/unescaped-html-literal': [2], 'grouped-accessor-pairs': [2], 'guard-for-in': [0], 'id-blacklist': [0], diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 64090d6490..f6720bf2f6 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -37,7 +37,7 @@ jobs: python-version: "3.12" - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: pip install poetry @@ -66,7 +66,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend @@ -137,7 +137,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend @@ -186,7 +186,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index a3fd8ca621..55c2d2bf5e 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest services: pgsql: - image: postgres:12 + image: postgres:14 env: POSTGRES_DB: test POSTGRES_PASSWORD: postgres diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml index 87e931117c..cc3fbd9c34 100644 --- a/.github/workflows/pull-e2e-tests.yml +++ b/.github/workflows/pull-e2e-tests.yml @@ -25,7 +25,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend frontend deps-backend diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 2558a16a71..c2cc14f771 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -22,7 +22,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend deps-backend diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index 37b3ff57d2..c9c15c31a0 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -23,7 +23,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend deps-backend diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index 4250623da0..ae717c7cec 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -27,7 +27,7 @@ jobs: check-latest: true - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 cache: npm cache-dependency-path: package-lock.json - run: make deps-frontend deps-backend diff --git a/.gitignore b/.gitignore index 272ea2b5ed..0791a17c71 100644 --- a/.gitignore +++ b/.gitignore @@ -42,14 +42,10 @@ _testmain.go coverage.all cpu.out -/modules/migration/bindata.go -/modules/migration/bindata.go.hash -/modules/options/bindata.go -/modules/options/bindata.go.hash -/modules/public/bindata.go -/modules/public/bindata.go.hash -/modules/templates/bindata.go -/modules/templates/bindata.go.hash +/modules/migration/bindata.* +/modules/options/bindata.* +/modules/public/bindata.* +/modules/templates/bindata.* *.db *.log diff --git a/.golangci.yml b/.golangci.yml index c176d2115c..70efd288ff 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -45,6 +45,10 @@ linters: desc: do not use the ini package, use gitea's config system instead - pkg: gitea.com/go-chi/cache desc: do not use the go-chi cache package, use gitea's cache system + nolintlint: + allow-unused: false + require-explanation: true + require-specific: true gocritic: disabled-checks: - ifElseChain @@ -83,6 +87,10 @@ linters: - name: unreachable-code - name: var-declaration - name: var-naming + arguments: + - [] # AllowList - do not remove as args for the rule are positional and won't work without lists first + - [] # DenyList + - - skip-package-name-checks: true # supress errors from underscore in migration packages staticcheck: checks: - all @@ -1,9 +1,6 @@ *.min.css *.min.js /assets/*.json -/modules/options/bindata.go -/modules/public/bindata.go -/modules/templates/bindata.go /options/gitignore /options/license /public/assets diff --git a/CHANGELOG.md b/CHANGELOG.md index ca2e67929c..b72ac4849a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,429 @@ This changelog goes through the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.com). +## [1.24.0](https://github.com/go-gitea/gitea/releases/tag/1.24.0) - 2025-05-26 + +* BREAKING + * Make Gitea always use its internal config, ignore `/etc/gitconfig` (#33076) + * Improve log format (#33814) + * Fix markdown render behaviors (#34122) + * Add package version api endpoints (#34173) + +* FEATURES + * Enforce two-factor auth (2FA: TOTP or WebAuthn) (#34187) + * Add fullscreen mode as a more efficient operation way to view projects (#34081) + * Add anonymous access support for private/unlisted repositories (#34051) + * Support public code/issue access for private repositories (#33127) + * Add middleware for request prioritization (#33951) + * Add cli flags LDAP group configuration (#33933) + * Add file tree to file view page (#32721) + * Add material icons for file list (#33837) + * Artifacts download api for artifact actions v4 (#33510) + * Support choose email when creating a commit via web UI (#33432) + * Add basic auth support to rss/atom feeds (#33371) + * Add sorting by exclusive labels (issue priority) (#33206) + * Add sub issue list support (#32940) + * Private README.md for organization (#32872) + * Email option to embed images as base64 instead of link (#32061) + * Option to delay conflict checking of old pull requests until page view (#27779) + * Worktime tracking for the organization level (#19808) + +* PERFORMANCE + * Add cache for common package queries (#22491) + * Move issue pin to an standalone table for querying performance (#33452) + * Improve commits list performance to reduce unnecessary database queries (#33528) + * Optimize total count of feed when loading activities in user dashboard. (#33841) + * Optimize heatmap query (#33853) + * Only use prev and next buttons for pagination on user dashboard (#33981) + * Improve pull request list API performance (#34052) + * Cache GPG keys, emails and users when list commits (#34086) + * Refactor Git Attribute & performance optimization (#34154) + * Performance optimization for tags synchronization (#34355) #34522 + +* ENHANCEMENTS + * Code + * Display when a release attachment was uploaded (#34261) + * Support creating relative link to raw path in markdown (#34105) + * Improve code block readability and isolate copy button (#34009) + * Improve repository commit view (#33877) + * Full-file syntax highlighting for diff pages (#33766) + * Clone repository with Tea CLI (#33725) + * Improve sync fork behavior (#33319) + * Make git clone URL could use current signed-in user (#33091) + * Add submodule diff links (#33097) + * Link to tree views of submodules if possible (#33424) + * Only keep popular licenses (#33832) + * De-emphasize signed commits (#31160) + + * Actions + * Add flat-square action badge style (#34062) + * Update action status badge layout (#34018) + * Download actions job logs from API (#33858) + * Always show the "rerun" button for action jobs (#33692) + * Add auto-expanding running actions step (#30058) + * Update status check for all supported on.pull_request.types in Gitea (#33117) + * Workflow_dispatch use workflow from trigger branch (#33098) + * Add action auto-scroll (#30057) + * Add workflow_job webhook (#33694) + * Add a button editing action secret (#34462) + + * Pull Request + * Auto expand "New PR" form (#33971) + * Mark parent directory as viewed when all files are viewed (#33958) + * Show info about maintainers are allowed to edit a PR (#33738) + * Automerge supports deleting branch automatically after merging (#32343) + * Add additional command hints for PowerShell & CMD (#33548) + + * Issues + * Allow filtering issues by any assignee (#33343) + * Show warning on navigation if currently editing comment or title (#32920) + * Make tracked time representation display as hours (#33315) + * Add No Results Prompt Message on Issue List Page (#33699) + * Add sort option recentclose for issues and pulls (#34525) #34539 + + * Packages + * Link to nuget dependencies (#26554) + * Add composor source field (#33502) + + * Administration + * Improve navbar: add "admin" tip, add "active" style (#32927) + * Add a option "--user-type bot" to admin user create, improve role display (#27885) + * Improve admin user view page (#33735) + * Support performance trace (#32973) + * Change pprof labels to be prometheus compatible (#32865) + * Allow admins and org owners to change org member public status (#28294) + * Optimize the installation page (#32994) + * Make public URL generation configurable (#34250) + * Add a --fullname arg to gitea admin user create. (#34241) + + * Others + * Improve oauth2 error handling (#33969) + * Fail mirroring more gracefully (#34002) + * Align User Details Page Header Layout with Design Specifications (#34192) + * Webhook add X-Gitea-Hook-Installation-Target-Type Header (#33752) + * Optimize the dashboard (#32990) + * Improve button layout on small screens (#33633) + * Add cropping support when modifying the user/org/repo avatar (#33498) + * Make ROOT_URL support using request Host header (#32564) + * Add `show more` organizations icon in user's profile (#32986) + * Introduce `--page-space-bottom` at 64px (#30692) + * Improve theme display (#30671) + * Add alphabetical project sorting (#33504) + * Add global lock for migrations to make upgrade more safe with multiple replications (#33706) + * Add descriptions for private repo public access settings and improve the UI (#34057) + +* API + * Actions Runner rest api (#33873) + * Inclusion of rename organization api (#33303) + * Add API to support link package to repository and unlink it (#33481) + * Add API endpoint to request contents of multiple files simultaniously (#34139) + * Actions artifacts API list/download check status upload confirmed (#34273) + * Add API routes to lock and unlock issues (#34165) + * Fix some user name usages (#33689) + * Allow filtering /repos/{owner}/{repo}/pulls by target base branch queryparam (#33684) + * Improve swagger generation (#33664) + * Support Ephemeral action runners (#33570) + * Support workflow event dispatch via API (#33545) + * Support workflow event dispatch via API (#32059) + * Added Description Field for Secrets and Variables (#33526) + * Reject star-related requests if stars are disabled (#33208) + * Let API create and edit system webhooks, attempt 2 (#33180) + * Use `Project-URL` metadata field to get a PyPI package's homepage URL (#33089) + * Add `last_committer_date` and `last_author_date` for file contents API (#32921) + +* REFACTORS + * Remove context from git struct (#33793) + * Refactor admin/common.ts (#33788) + * Refactor repo-settings.ts (#33785) + * Refactor repo-issue.ts (#33784) + * Small refactor to reduce unnecessary database queries and remove duplicated functions (#33779) + * Refactor initRepoBranchTagSelector to use new init framework (#33776) + * Refactor buttons to use new init framework (#33774) + * Refactor markup and pdf-viewer to use new init framework (#33772) + * Refactor error system (#33771) + * Refactor mail code (#33768) + * Update TypeScript types (#33799) + * Refactor older tests to use testify (#33140) + * Move notifywatch to service layer (#33825) + * Decouple context from repository related structs (#33823) + * Remove context from mail struct (#33811) + * Refactor dropdown ellipsis (#34123) + * Refactor functions to reduce repopath expose (#33892) + * Refactor repo-diff.ts (#33746) + * Refactor web route handler (#33488) + * Refactor user & avatar (#33433) + * Refactor user package (#33423) + * Refactor decouple context from migration structs (#33399) + * Refactor context flash msg and global variables (#33375) + * Refactor response writer & access logger (#33323) + * Refactor ref type (#33242) + * Refactor context repository (#33202) + * Refactor legacy JS (#33115) + * Refactor legacy line-number and scroll code (#33094) + * Refactor env var related code (#33075) + * Move SetMerged to service layer (#33045) + * Merge updatecommentattachment functions (#33044) + * Refactor pull-request compare&create page (#33071) + * Refactor repo-new.ts (#33070) + * Refactor pagination (#33037) + * Refactor tests (#33021) + * Refactor markup render to fix various path problems (#34114) + * Refactor Branch struct in package modules/git (#33980) + * Don't create duplicated functions for code repositories and wiki repositories (#33924) + * Move git references checking to gitrepo packages to reduce expose of repository path (#33891) + * Refactor cache-control (#33861) + * Decouple diff stats query from actual diffing (#33810) + * Move part of updating protected branch logic to service layer (#33742) + * Decouple Batch from git.Repository to simplify usage without requiring the creation of a Repository struct. (#34001) + * Refactor tmpl and blob_excerpt (#32967) + * Refactor template & test related code (#32938) + * Refactor db package and remove unnecessary `DumpTables` (#32930) + * Refactor pprof labels and process desc (#32909) + * Refactor repo-projects.ts (#32892) + * Refactor getpatch/getdiff functions and remove unnecessary fallback (#32817) + * Uniform all temporary directories and allow customizing temp path (#32352) + * Remove context from retry downloader (#33871) + * Refactor global init code and add more comments (#33755) + * Remove some unnecessary template helpers (#33069) + * Move and rename UpdateRepository (#34136) + * Move hooks function to gitrepo and reduce expose repopath (#33890) + * Add abstraction layer to delete repository from disk (#33879) + * Add abstraction layer to check if the repository exists on disk (#33874) + * Move ParseCommitWithSSHSignature to service layer (#34087) + * Move duplicated functions (#33977) + * Extract code to their own functions for push update (#33944) + * Move gitgraph from modules to services layer (#33527) + * Move commits signature and verify functions to service layers (#33605) + * Use `CloseIssue` and `ReopenIssue` instead of `ChangeStatus` (#32467) + * Refactor arch route handlers (#32993) + * Refactor "string truncate" (#32984) + * Refactor arch route handlers (#32972) + * Clarify path param naming (#32969) + * Refactor request context (#32956) + * Move some errors to their own sub packages (#32880) + * Move RepoTransfer from models to models/repo sub package (#32506) + * Move delete deploy keys into service layer (#32201) + * Refactor webhook events (#33337) + * Move some Actions related functions from `routers` to `services` (#33280) + * Refactor RefName (#33234) + * Refactor context RefName and RepoAssignment (#33226) + * Refactor repository transfer (#33211) + * Refactor error system (#33626) + * Refactor error system (#33610) + * Refactor package (routes and error handling, npm peer dependency) (#33111) + * Use test context in tests and new loop system in benchmarks (#33648) + * Some small refactors (#33144) + * Simplify context ref name (#33267) + +* BUGFIXES + * Fix some dropdown problems on the issue sidebar (#34308) #34327 + * Do not return archive download URLs in API if downloads are disabled (#34324) #34338 + * Fix LFS files being editable in web UI (#34356) #34362 + * Fix only text/* being viewable in web UI (#34374) #34378 + * Fix LFS file not stored in LFS when uploaded/edited via API or web UI (#34367) + * Grey out expired artifact on Artifacts list (#34314) #34404 + * Fix incorrect divergence cache after switching default branch (#34370) #34406 + * Refactor commit message rendering and fix bugs (#34412) #34414 + * Merge and tweak markup editor expander CSS (#34409) #34415 + * Fix GetUsersByEmails (#34423) #34425 + * Only git operations should update last changed of a repository (#34388) #34427 + * Fix comment textarea scroll issue in Firefox (#34438) #34446 + * Fix repo broken check (#34444) #34452 + * Fix remove org user failure on mssql (#34449) #34453 + * Fix Workflow run Not Found page (#34459) #34466 + * When updating comment, if the content is the same, just return and not update the database (#34422) #34464 + * Fix project board view (#34470) #34475 + * Fix get / delete runner to use consistent http 404 and 500 status (#34480) #34488 + * Fix url validation in webhook add/edit API (#34492) #34496 + * Fix edithook api can not update package, status and workflow_job events (#34495) #34499 + * Fix ephemeral runner deletion (#34447) #34513 + * Don't display error log when .git-blame-ignore-revs doesn't exist (#34457) + * Only allow admins to rename default/protected branches (#33276) + * Improve "lock conversation" UI (#34207) + * Fix incorrect file links (#34189) + * Optimize Overflow Menu (#34183) + * Check user/org repo limit instead of doer (#34147) + * Make markdown render match GitHub's behavior (#34129) + * Fix team permission (#34128) + * Correctly handle submodule view and avoid throwing 500 error (#34121) + * Fix users being able bypass limits with repo transfers (#34031) + * Avoid creating unnecessary temporary cat file sub process (#33942) + * Refactor organization menu (#33928) + * Fix various Fomantic UI and htmx problems (#33851) + * Fix 500 error when error occurred in migration page (#33256) + * Validate that the tag doesn't exist when creating a tag via the web (#33241) + * Add missed transaction on setmerged (#33079) + * Rework create/fork/adopt/generate repository to make sure resources will be cleanup once failed (#31035) + * Valid email address should only start with alphanumeric (#28174) + * Fix webhook url (#34186) + * Fix "toAbsoluteLocaleDate" test when system locale is not en-US (#33939) + * Fix file name could not be searched if the file was not a text file when using the Bleve indexer (#33959) + * Fix cannot delete runners via the modal dialog (#33895) + * Fix unpin hint on the pinned pull requests (#33207) + * Fix parentCommit invalid memory address or nil pointer dereference. (#33204) + * Fix comment header padding (#33377) + * Fix some migration and repo name problems (#33986) + * Fix various trivial frontend problems (#34263) + * Fix Set Email Preference dropdown and button placement (#34255) + * Fix quoted replies incorrectly render user input as part of the quote (#34216) + * Fix button alignments and remove unnecessary styles (#34206) + * Restore form inputs on organization create error (#34201) + * Try to fix ACME (3rd) (#33807) + * Fix incorrect ref "blob" (#33240) + * Fix dynamic content loading init problem (#33748) + * Fix git empty check and HEAD request (#33690) + * Fix Untranslated Text on Actions Page (#33635) + * Fix issue label delete incorrect labels webhook payload (#34575) + * Fix incorrect page navigation with up and down arrow on last item of dashboard repos (#34570) + * Fix/improve avatar sync from LDAP (#34573) + * Fix some trivial problems (#34579) + * Retain issue sort type when a keyword search is introduced (#34559) + * Always use an empty line to separate the commit message and trailer (#34512) + * Fix line-button issue after file selection in file tree (#34574) + * Fix doctor deleting orphaned issues attachments (#34142) + * Add webhook assigning test and fix possible bug (#34420) + * Fix possible nil description of pull request when migrating from CodeCommit (#34541) + * Refactor commit reader (#34542) + * Fix possible pull request broken when leave the page immediately after clicking the update button #34509 + * Ignore "Close" error when uploading container blob (#34620) + * Fix missed merge commit sha and time when migrating from codecommit (#34645) + * Fix GetUsersByEmails (#34643) + * Misc CSS fixes (#34638) + * Add codecommit to supported services in api docs (#34626) + * Validate hex colors when creating/editing labels (#34623) + * Fix possible pull request broken when leave the page immediately after clicking the update button (#34509) + * Fix margin issue in markup paragraph rendering (#34599) + * Fix migration pull request title too long (#34577) + * Fix footnote jump behavior on the issue page. (#34621) + * Fix "oras" OCI client compatibility (#34666) + * Fix last admin check when syncing users (#34649) + * Fix skip paths check on tag push events in workflows (#34602) #34670 + +* MISC + + * Bump to alpine 3.22 (#34613) + * Make pull request and issue history more compact (#34588) + * Run integration tests against postgres 14 (#34514) #34536 + * Enable addtional linters (#34085) + * Enable testifylint rules (#34075) + * Enable staticcheck QFxxxx rules (#34064) + * Improve Actions test (#32883) + * Drop fomantic build (#33845) + * Go1.24 (#33562) + * Run yamllint with strict mode, fix issue (#33551) + * Disable cron task to update license (#33486) + * Optimize makefile help information generation (#33390) + * Convert github.com/xanzy/go-gitlab into gitlab.com/gitlab-org/api/client-go (#33126) + * Add missed changelogs (#33649) + * Update .changelog file to add performance label group (#33472) + * Add missing POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES in app.example.ini (#33363) + * Update README screenshots (#33347) + * Update unrs-resolver (#34279) + * Update go&js dependencies (#34262) + * Optimize the calling code of queryElems (#34235) + * Update protected_branch.tmpl (#34193) + * Feat/optimize span svg layout (#34185) + * Set MERMAID_MAX_SOURCE_CHARACTERS to 50000 (#34152) + * Update JS and PY deps (#34143) + * Add Chinese translations for README files (#34132) + * Use `overflow-wrap: anywhere` to replace `word-break: break-all` (#34126) + * Clarify ownership in password change error messages (#34092) + * Add toggleClass function in dom.ts (#34063) + * Update to golangci-lint v2 (#34054) + * Update Makefile test comments (#34013) + * Update go mod dependencies (#33988) + * Use filepath.Join instead of path.Join for file system file operations (#33978) + * Prepare common tmpl functions in a middleware (#33957) + * Remove unused or abused styles (#33918) + * Update JS and PY deps, misc tweaks (#33903) + * Try to figure out attribute checker problem (#33901) + * Add lock for a repository pull mirror (#33876) + * Fine tune push mirror UI (#33866) + * Improve issue & code search (#33860) + * Use pullrequestlist instead of []*pullrequest (#33765) + * Upgrade act to 0.261.4 and actions-proto-go to v0.4.1 (#33760) + * Align sidebar gears to the right (#33721) + * Update Go dependencies (skip blevesearch, meilisearch) (#33655) + * Add migrations and doctor fixes (#33556) + * Remove "class-name" from svg icon (#33540) + * Update MAINTAINERS (#33529) + * Add "No data available" display when list is empty (#33517) + * Use `git diff-tree` for `DiffFileTree` on diff pages (#33514) + * Give organisation members access to organisation feeds (#33508) + * Update feishu icon (#33470) + * Hide/disable unusable UI elements when a repository is archived (#33459) + * Update `@github/text-expander-element` to 2.9.0 (#33435) + * Do not access GitRepo when a repo is being created (#33380) + * Fix incorrect ref usages (#33301) + * Prepare for support performance trace (#33286) + * Enable Typescript `noImplicitThis` (#33250) + * Remove unused CSS styles and move some styles to proper files (#33217) + * Add .run to gitignore (#33175) + * Fix typo in gitea downloader test and add missing codebase in `ToGitServiceType` (#33146) + * Remove extended glob pattern from branch protection UI (#33125) + * Clean up legacy form CSS styles (#33081) + * Unset XDG_HOME_CONFIG as gitea manages configuration locations (#33067) + * Add IntelliJ Gateway's .uuid to gitignore (#33052) + * User facing messages for AGit errors (#33012) + * Always show assignees on right (#33006) + * Fix eslint (#33002) + * Update JS dependencies (#32914) + * Bump x/net (#32896) (#32900) + * Only activity tab needs heatmap data loading (#34652) + +## [1.23.8](https://github.com/go-gitea/gitea/releases/tag/1.23.8) - 2025-05-11 + +* SECURITY + * Fix a bug when uploading file via lfs ssh command (#34408) (#34411) + * Update net package (#34228) (#34232) +* BUGFIXES + * Fix releases sidebar navigation link (#34436) #34439 + * Fix bug webhook milestone is not right. (#34419) #34429 + * Fix two missed null value checks on the wiki page. (#34205) (#34215) + * Swift files can be passed either as file or as form value (#34068) (#34236) + * Fix bug when API get pull changed files for deleted head repository (#34333) (#34368) + * Upgrade github v61 -> v71 to fix migrating bug (#34389) + * Fix bug when visiting comparation page (#34334) (#34364) + * Fix wrong review requests when updating the pull request (#34286) (#34304) + * Fix github migration error when using multiple tokens (#34144) (#34302) + * Explicitly not update indexes when sync database schemas (#34281) (#34295) + * Fix panic when comment is nil (#34257) (#34277) + * Fix project board links to related Pull Requests (#34213) (#34222) + * Don't assume the default wiki branch is master in the wiki API (#34244) (#34245) +* DOCUMENTATION + * Update token creation API swagger documentation (#34288) (#34296) +* MISC + * Fix CI Build (#34315) + * Add riscv64 support (#34199) (#34204) + * Bump go version in go.mod (#34160) + * remove hardcoded 'code' string in clone_panel.tmpl (#34153) (#34158) + +## [1.23.7](https://github.com/go-gitea/gitea/releases/tag/1.23.7) - 2025-04-07 + +* Enhancements + * Add a config option to block "expensive" pages for anonymous users (#34024) (#34071) + * Also check default ssh-cert location for host (#34099) (#34100) (#34116) +* BUGFIXES + * Fix discord webhook 400 status code when description limit is exceeded (#34084) (#34124) + * Get changed files based on merge base when checking `pull_request` actions trigger (#34106) (#34120) + * Fix invalid version in RPM package path (#34112) (#34115) + * Return default avatar url when user id is zero rather than updating database (#34094) (#34095) + * Add additional ReplaceAll in pathsep to cater for different pathsep (#34061) (#34070) + * Try to fix check-attr bug (#34029) (#34033) + * Git client will follow 301 but 307 (#34005) (#34010) + * Fix block expensive for 1.23 (#34127) + * Fix markdown frontmatter rendering (#34102) (#34107) + * Add new CLI flags to set name and scopes when creating a user with access token (#34080) (#34103) + * Do not show 500 error when default branch doesn't exist (#34096) (#34097) + * Hide activity contributors, recent commits and code frequrency left tabs if there is no code permission (#34053) (#34065) + * Simplify emoji rendering (#34048) (#34049) + * Adjust the layout of the toolbar on the Issues/Projects page (#33667) (#34047) + * Pull request updates will also trigger code owners review requests (#33744) (#34045) + * Fix org repo creation being limited by user limits (#34030) (#34044) + * Fix git client accessing renamed repo (#34034) (#34043) + * Fix the issue with error message logging for the `check-attr` command on Windows OS. (#34035) (#34036) + * Polyfill WeakRef (#34025) (#34028) + ## [1.23.6](https://github.com/go-gitea/gitea/releases/tag/v1.23.6) - 2025-03-24 * SECURITY diff --git a/Dockerfile b/Dockerfile index fa2ae9913c..c9e6a2d3db 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM docker.io/library/golang:1.24-alpine3.21 AS build-env +FROM docker.io/library/golang:1.24-alpine3.22 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-direct} @@ -41,7 +41,7 @@ RUN chmod 755 /tmp/local/usr/bin/entrypoint \ /go/src/code.gitea.io/gitea/environment-to-ini RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete -FROM docker.io/library/alpine:3.21 +FROM docker.io/library/alpine:3.22 LABEL maintainer="maintainers@gitea.io" EXPOSE 22 3000 diff --git a/Dockerfile.rootless b/Dockerfile.rootless index b74dfa58e0..558e6cf73b 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,5 +1,5 @@ # Build stage -FROM docker.io/library/golang:1.24-alpine3.21 AS build-env +FROM docker.io/library/golang:1.24-alpine3.22 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-direct} @@ -39,7 +39,7 @@ RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \ /go/src/code.gitea.io/gitea/environment-to-ini RUN chmod 644 /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_autocomplete -FROM docker.io/library/alpine:3.21 +FROM docker.io/library/alpine:3.22 LABEL maintainer="maintainers@gitea.io" EXPOSE 2222 3000 @@ -52,6 +52,7 @@ RUN apk --no-cache add \ git \ curl \ gnupg \ + openssh-keygen \ && rm -rf /var/cache/apk/* RUN addgroup \ @@ -26,17 +26,18 @@ COMMA := , XGO_VERSION := go-1.24.x AIR_PACKAGE ?= github.com/air-verse/air@v1 -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.2.1 -GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.2 +EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 +GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.8.0 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.6 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.12 -MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.6.0 -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.32.3 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1 -GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.1 +GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.19.1 +GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.19.1 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest @@ -80,7 +81,6 @@ ifeq ($(RACE_ENABLED),true) endif STORED_VERSION_FILE := VERSION -HUGO_VERSION ?= 0.111.3 GITHUB_REF_TYPE ?= branch GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) @@ -120,8 +120,7 @@ WEBPACK_CONFIGS := webpack.config.js tailwind.config.js WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts -BINDATA_DEST := modules/public/bindata.go modules/options/bindata.go modules/templates/bindata.go -BINDATA_HASH := $(addsuffix .hash,$(BINDATA_DEST)) +BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.* GENERATED_GO_DEST := modules/charset/invisible_gen.go modules/charset/ambiguous_gen.go @@ -149,14 +148,8 @@ SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini GO_SOURCES := $(wildcard *.go) -GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go" ! -path modules/options/bindata.go ! -path modules/public/bindata.go ! -path modules/templates/bindata.go) +GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go") GO_SOURCES += $(GENERATED_GO_DEST) -GO_SOURCES_NO_BINDATA := $(GO_SOURCES) - -ifeq ($(filter $(TAGS_SPLIT),bindata),bindata) - GO_SOURCES += $(BINDATA_DEST) - GENERATED_GO_DEST += $(BINDATA_DEST) -endif # Force installation of playwright dependencies by setting this flag ifdef DEPS_PLAYWRIGHT @@ -226,7 +219,7 @@ clean-all: clean ## delete backend, frontend and integration files .PHONY: clean clean: ## delete backend and integration files - rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST) $(BINDATA_HASH) \ + rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \ integrations*.test \ e2e*.test \ tests/integration/gitea-integration-* \ @@ -237,7 +230,7 @@ clean: ## delete backend and integration files tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/ .PHONY: fmt -fmt: ## format the Go code +fmt: ## format the Go and template code @GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' $(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl')) @# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only @@ -256,6 +249,19 @@ fmt-check: fmt exit 1; \ fi +.PHONY: fix +fix: ## apply automated fixes to Go code + $(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./... + +.PHONY: fix-check +fix-check: fix + @diff=$$(git diff --color=always $(GO_SOURCES)); \ + if [ -n "$$diff" ]; then \ + echo "Please run 'make fix' and commit the result:"; \ + printf "%s" "$${diff}"; \ + exit 1; \ + fi + .PHONY: $(TAGS_EVIDENCE) $(TAGS_EVIDENCE): @mkdir -p $(MAKE_EVIDENCE_DIR) @@ -268,7 +274,7 @@ endif .PHONY: generate-swagger generate-swagger: $(SWAGGER_SPEC) ## generate the swagger spec from code comments -$(SWAGGER_SPEC): $(GO_SOURCES_NO_BINDATA) $(SWAGGER_SPEC_INPUT) +$(SWAGGER_SPEC): $(GO_SOURCES) $(SWAGGER_SPEC_INPUT) $(GO) run $(SWAGGER_PACKAGE) generate spec --exclude "$(SWAGGER_EXCLUDE)" --input "$(SWAGGER_SPEC_INPUT)" --output './$(SWAGGER_SPEC)' .PHONY: swagger-check @@ -295,7 +301,7 @@ checks: checks-frontend checks-backend ## run various consistency checks checks-frontend: lockfile-check svg-check ## check frontend files .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files +checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files .PHONY: lint lint: lint-frontend lint-backend lint-spell ## lint everything @@ -373,7 +379,7 @@ lint-go-gitea-vet: ## lint go files with gitea-vet .PHONY: lint-go-gopls lint-go-gopls: ## lint go files with gopls @echo "Running gopls check..." - @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES_NO_BINDATA) + @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES) .PHONY: lint-editorconfig lint-editorconfig: @@ -816,6 +822,7 @@ deps-tools: ## install tool dependencies $(GO) install $(GOVULNCHECK_PACKAGE) & \ $(GO) install $(ACTIONLINT_PACKAGE) & \ $(GO) install $(GOPLS_PACKAGE) & \ + $(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \ wait node_modules: package-lock.json diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 3827a092f1..d961444239 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -1080,9 +1080,14 @@ "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." }, { - "name": "github.com/urfave/cli/v2", - "path": "github.com/urfave/cli/v2/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2022 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + "name": "github.com/urfave/cli-docs/v3", + "path": "github.com/urfave/cli-docs/v3/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, + { + "name": "github.com/urfave/cli/v3", + "path": "github.com/urfave/cli/v3/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, { "name": "github.com/valyala/fastjson", @@ -1110,11 +1115,6 @@ "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n" }, { - "name": "github.com/xrash/smetrics", - "path": "github.com/xrash/smetrics/LICENSE", - "licenseText": "Copyright (C) 2016 Felipe da Cunha Gonçalves\nAll Rights Reserved.\n\nMIT LICENSE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" - }, - { "name": "github.com/yohcop/openid-go", "path": "github.com/yohcop/openid-go/LICENSE", "licenseText": "Copyright 2015 Yohann Coppel\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n" @@ -5,19 +5,10 @@ package main -// Libraries that are included to vendor utilities used during build. +// Libraries that are included to vendor utilities used during Makefile build. // These libraries will not be included in a normal compilation. import ( - // for embed - _ "github.com/shurcooL/vfsgen" - - // for cover merge - _ "golang.org/x/tools/cover" - // for vet _ "code.gitea.io/gitea-vet" - - // for swagger - _ "github.com/go-swagger/go-swagger/cmd/swagger" ) diff --git a/build/generate-bindata.go b/build/generate-bindata.go index 2fcb7c2f2a..2553770762 100644 --- a/build/generate-bindata.go +++ b/build/generate-bindata.go @@ -6,87 +6,22 @@ package main import ( - "bytes" - "crypto/sha1" "fmt" - "log" - "net/http" "os" - "path/filepath" - "strconv" - "github.com/shurcooL/vfsgen" + "code.gitea.io/gitea/modules/assetfs" ) -func needsUpdate(dir, filename string) (bool, []byte) { - needRegen := false - _, err := os.Stat(filename) - if err != nil { - needRegen = true - } - - oldHash, err := os.ReadFile(filename + ".hash") - if err != nil { - oldHash = []byte{} - } - - hasher := sha1.New() - - err = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - info, err := d.Info() - if err != nil { - return err - } - _, _ = hasher.Write([]byte(d.Name())) - _, _ = hasher.Write([]byte(info.ModTime().String())) - _, _ = hasher.Write([]byte(strconv.FormatInt(info.Size(), 16))) - return nil - }) - if err != nil { - return true, oldHash - } - - newHash := hasher.Sum([]byte{}) - - if bytes.Compare(oldHash, newHash) != 0 { - return true, newHash - } - - return needRegen, newHash -} - func main() { - if len(os.Args) < 4 { - log.Fatal("Insufficient number of arguments. Need: directory packageName filename") - } - - dir, packageName, filename := os.Args[1], os.Args[2], os.Args[3] - var useGlobalModTime bool - if len(os.Args) == 5 { - useGlobalModTime, _ = strconv.ParseBool(os.Args[4]) - } - - update, newHash := needsUpdate(dir, filename) - - if !update { - fmt.Printf("bindata for %s already up-to-date\n", packageName) - return + if len(os.Args) != 3 { + fmt.Println("usage: ./generate-bindata {local-directory} {bindata-filename}") + os.Exit(1) } - fmt.Printf("generating bindata for %s\n", packageName) - var fsTemplates http.FileSystem = http.Dir(dir) - err := vfsgen.Generate(fsTemplates, vfsgen.Options{ - PackageName: packageName, - BuildTags: "bindata", - VariableName: "Assets", - Filename: filename, - UseGlobalModTime: useGlobalModTime, - }) - if err != nil { - log.Fatalf("%v\n", err) + dir, filename := os.Args[1], os.Args[2] + fmt.Printf("generating bindata for %s to %s\n", dir, filename) + if err := assetfs.GenerateEmbedBindata(dir, filename); err != nil { + fmt.Printf("failed: %s\n", err.Error()) + os.Exit(1) } - _ = os.WriteFile(filename+".hash", newHash, 0o666) } diff --git a/cmd/actions.go b/cmd/actions.go index f582c16c81..2c51c6a1bc 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -4,12 +4,13 @@ package cmd import ( + "context" "fmt" "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -17,7 +18,7 @@ var ( CmdActions = &cli.Command{ Name: "actions", Usage: "Manage Gitea Actions", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdActionsGenRunnerToken, }, } @@ -38,10 +39,7 @@ var ( } ) -func runGenerateActionsRunnerToken(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error { setting.MustInstalled() scope := c.String("scope") diff --git a/cmd/admin.go b/cmd/admin.go index 6c9480e76e..559544edd3 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -15,7 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -23,7 +23,7 @@ var ( CmdAdmin = &cli.Command{ Name: "admin", Usage: "Perform common administrative operations", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdUser, subcmdRepoSyncReleases, subcmdRegenerate, @@ -41,7 +41,7 @@ var ( subcmdRegenerate = &cli.Command{ Name: "regenerate", Usage: "Regenerate specific files", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ microcmdRegenHooks, microcmdRegenKeys, }, @@ -50,15 +50,15 @@ var ( subcmdAuth = &cli.Command{ Name: "auth", Usage: "Modify external auth providers", - Subcommands: []*cli.Command{ - microcmdAuthAddOauth, - microcmdAuthUpdateOauth, - microcmdAuthAddLdapBindDn, - microcmdAuthUpdateLdapBindDn, - microcmdAuthAddLdapSimpleAuth, - microcmdAuthUpdateLdapSimpleAuth, - microcmdAuthAddSMTP, - microcmdAuthUpdateSMTP, + Commands: []*cli.Command{ + microcmdAuthAddOauth(), + microcmdAuthUpdateOauth(), + microcmdAuthAddLdapBindDn(), + microcmdAuthUpdateLdapBindDn(), + microcmdAuthAddLdapSimpleAuth(), + microcmdAuthUpdateLdapSimpleAuth(), + microcmdAuthAddSMTP(), + microcmdAuthUpdateSMTP(), microcmdAuthList, microcmdAuthDelete, }, @@ -70,9 +70,9 @@ var ( Action: runSendMail, Flags: []cli.Flag{ &cli.StringFlag{ - Name: "title", - Usage: `a title of a message`, - Value: "", + Name: "title", + Usage: "a title of a message", + Required: true, }, &cli.StringFlag{ Name: "content", @@ -86,17 +86,16 @@ var ( }, }, } +) - idFlag = &cli.Int64Flag{ +func idFlag() *cli.Int64Flag { + return &cli.Int64Flag{ Name: "id", Usage: "ID of authentication source", } -) - -func runRepoSyncReleases(_ *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() +} +func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { return err } @@ -107,7 +106,7 @@ func runRepoSyncReleases(_ *cli.Context) error { log.Trace("Synchronizing repository releases (this may take a while)") for page := 1; ; page++ { - repos, count, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{ + repos, count, err := repo_model.SearchRepositoryByName(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: repo_model.RepositoryListDefaultPageSize, Page: page, diff --git a/cmd/admin_auth.go b/cmd/admin_auth.go index 4777a92908..1a09366722 100644 --- a/cmd/admin_auth.go +++ b/cmd/admin_auth.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -13,14 +14,14 @@ import ( "code.gitea.io/gitea/models/db" auth_service "code.gitea.io/gitea/services/auth" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( microcmdAuthDelete = &cli.Command{ Name: "delete", Usage: "Delete specific auth source", - Flags: []cli.Flag{idFlag}, + Flags: []cli.Flag{idFlag()}, Action: runDeleteAuth, } microcmdAuthList = &cli.Command{ @@ -56,10 +57,7 @@ var ( } ) -func runListAuth(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runListAuth(ctx context.Context, c *cli.Command) error { if err := initDB(ctx); err != nil { return err } @@ -90,14 +88,11 @@ func runListAuth(c *cli.Context) error { return nil } -func runDeleteAuth(c *cli.Context) error { +func runDeleteAuth(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") { return errors.New("--id flag is missing") } - ctx, cancel := installSignals() - defer cancel() - if err := initDB(ctx); err != nil { return err } diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go index d2eeb7c0d6..069ad6600c 100644 --- a/cmd/admin_auth_ldap.go +++ b/cmd/admin_auth_ldap.go @@ -12,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth/source/ldap" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) type ( @@ -24,8 +24,8 @@ type ( } ) -var ( - commonLdapCLIFlags = []cli.Flag{ +func commonLdapCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Usage: "Authentication name.", @@ -103,8 +103,10 @@ var ( Usage: "The attribute of the user’s LDAP record containing the user’s avatar.", }, } +} - ldapBindDnCLIFlags = append(commonLdapCLIFlags, +func ldapBindDnCLIFlags() []cli.Flag { + return append(commonLdapCLIFlags(), &cli.StringFlag{ Name: "bind-dn", Usage: "The DN to bind to the LDAP server with when searching for the user.", @@ -157,49 +159,59 @@ var ( Name: "group-team-map-removal", Usage: "Remove users from synchronized teams if user does not belong to corresponding LDAP group", }) +} - ldapSimpleAuthCLIFlags = append(commonLdapCLIFlags, +func ldapSimpleAuthCLIFlags() []cli.Flag { + return append(commonLdapCLIFlags(), &cli.StringFlag{ Name: "user-dn", Usage: "The user's DN.", }) +} - microcmdAuthAddLdapBindDn = &cli.Command{ +func microcmdAuthAddLdapBindDn() *cli.Command { + return &cli.Command{ Name: "add-ldap", Usage: "Add new LDAP (via Bind DN) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().addLdapBindDn(c) + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().addLdapBindDn(ctx, cmd) }, - Flags: ldapBindDnCLIFlags, + Flags: ldapBindDnCLIFlags(), } +} - microcmdAuthUpdateLdapBindDn = &cli.Command{ +func microcmdAuthUpdateLdapBindDn() *cli.Command { + return &cli.Command{ Name: "update-ldap", Usage: "Update existing LDAP (via Bind DN) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().updateLdapBindDn(c) + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().updateLdapBindDn(ctx, cmd) }, - Flags: append([]cli.Flag{idFlag}, ldapBindDnCLIFlags...), + Flags: append([]cli.Flag{idFlag()}, ldapBindDnCLIFlags()...), } +} - microcmdAuthAddLdapSimpleAuth = &cli.Command{ +func microcmdAuthAddLdapSimpleAuth() *cli.Command { + return &cli.Command{ Name: "add-ldap-simple", Usage: "Add new LDAP (simple auth) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().addLdapSimpleAuth(c) + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().addLdapSimpleAuth(ctx, cmd) }, - Flags: ldapSimpleAuthCLIFlags, + Flags: ldapSimpleAuthCLIFlags(), } +} - microcmdAuthUpdateLdapSimpleAuth = &cli.Command{ +func microcmdAuthUpdateLdapSimpleAuth() *cli.Command { + return &cli.Command{ Name: "update-ldap-simple", Usage: "Update existing LDAP (simple auth) authentication source", - Action: func(c *cli.Context) error { - return newAuthService().updateLdapSimpleAuth(c) + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().updateLdapSimpleAuth(ctx, cmd) }, - Flags: append([]cli.Flag{idFlag}, ldapSimpleAuthCLIFlags...), + Flags: append([]cli.Flag{idFlag()}, ldapSimpleAuthCLIFlags()...), } -) +} // newAuthService creates a service with default functions. func newAuthService() *authService { @@ -212,7 +224,7 @@ func newAuthService() *authService { } // parseAuthSourceLdap assigns values on authSource according to command line flags. -func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) { +func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) { if c.IsSet("name") { authSource.Name = c.String("name") } @@ -232,7 +244,7 @@ func parseAuthSourceLdap(c *cli.Context, authSource *auth.Source) { } // parseLdapConfig assigns values on config according to command line flags. -func parseLdapConfig(c *cli.Context, config *ldap.Source) error { +func parseLdapConfig(c *cli.Command, config *ldap.Source) error { if c.IsSet("name") { config.Name = c.String("name") } @@ -245,7 +257,7 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error { if c.IsSet("security-protocol") { p, ok := findLdapSecurityProtocolByName(c.String("security-protocol")) if !ok { - return fmt.Errorf("Unknown security protocol name: %s", c.String("security-protocol")) + return fmt.Errorf("unknown security protocol name: %s", c.String("security-protocol")) } config.SecurityProtocol = p } @@ -337,32 +349,27 @@ func findLdapSecurityProtocolByName(name string) (ldap.SecurityProtocol, bool) { // getAuthSource gets the login source by its id defined in the command line flags. // It returns an error if the id is not set, does not match any source or if the source is not of expected type. -func (a *authService) getAuthSource(ctx context.Context, c *cli.Context, authType auth.Type) (*auth.Source, error) { +func (a *authService) getAuthSource(ctx context.Context, c *cli.Command, authType auth.Type) (*auth.Source, error) { if err := argsSet(c, "id"); err != nil { return nil, err } - authSource, err := a.getAuthSourceByID(ctx, c.Int64("id")) if err != nil { return nil, err } if authSource.Type != authType { - return nil, fmt.Errorf("Invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String()) + return nil, fmt.Errorf("invalid authentication type. expected: %s, actual: %s", authType.String(), authSource.Type.String()) } return authSource, nil } // addLdapBindDn adds a new LDAP via Bind DN authentication source. -func (a *authService) addLdapBindDn(c *cli.Context) error { +func (a *authService) addLdapBindDn(ctx context.Context, c *cli.Command) error { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-search-base", "user-filter", "email-attribute"); err != nil { return err } - - ctx, cancel := installSignals() - defer cancel() - if err := a.initDB(ctx); err != nil { return err } @@ -384,10 +391,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error { } // updateLdapBindDn updates a new LDAP via Bind DN authentication source. -func (a *authService) updateLdapBindDn(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func (a *authService) updateLdapBindDn(ctx context.Context, c *cli.Command) error { if err := a.initDB(ctx); err != nil { return err } @@ -406,14 +410,11 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error { } // addLdapSimpleAuth adds a new LDAP (simple auth) authentication source. -func (a *authService) addLdapSimpleAuth(c *cli.Context) error { +func (a *authService) addLdapSimpleAuth(ctx context.Context, c *cli.Command) error { if err := argsSet(c, "name", "security-protocol", "host", "port", "user-dn", "user-filter", "email-attribute"); err != nil { return err } - ctx, cancel := installSignals() - defer cancel() - if err := a.initDB(ctx); err != nil { return err } @@ -435,10 +436,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error { } // updateLdapSimpleAuth updates a new LDAP (simple auth) authentication source. -func (a *authService) updateLdapSimpleAuth(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func (a *authService) updateLdapSimpleAuth(ctx context.Context, c *cli.Command) error { if err := a.initDB(ctx); err != nil { return err } diff --git a/cmd/admin_auth_ldap_test.go b/cmd/admin_auth_ldap_test.go index ea9a83ef76..2da7ebc573 100644 --- a/cmd/admin_auth_ldap_test.go +++ b/cmd/admin_auth_ldap_test.go @@ -8,17 +8,16 @@ import ( "testing" "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/services/auth/source/ldap" "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestAddLdapBindDn(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -135,7 +134,7 @@ func TestAddLdapBindDn(t *testing.T) { "--user-filter", "(memberOf=cn=user-group,ou=example,dc=domain,dc=org)", "--email-attribute", "mail", }, - errMsg: "Unknown security protocol name: zzzzz", + errMsg: "unknown security protocol name: zzzzz", }, // case 3 { @@ -239,12 +238,13 @@ func TestAddLdapBindDn(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthAddLdapBindDn.Flags - app.Action = service.addLdapBindDn + app := cli.Command{ + Flags: microcmdAuthAddLdapBindDn().Flags, + Action: service.addLdapBindDn, + } // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { @@ -256,9 +256,7 @@ func TestAddLdapBindDn(t *testing.T) { func TestAddLdapSimpleAuth(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -348,12 +346,12 @@ func TestAddLdapSimpleAuth(t *testing.T) { "--name", "ldap (simple auth) source", "--security-protocol", "zzzzz", "--host", "ldap-server", - "--port", "123", + "--port", "1234", "--user-filter", "(&(objectClass=posixAccount)(cn=%s))", "--email-attribute", "mail", "--user-dn", "cn=%s,ou=Users,dc=domain,dc=org", }, - errMsg: "Unknown security protocol name: zzzzz", + errMsg: "unknown security protocol name: zzzzz", }, // case 3 { @@ -470,12 +468,13 @@ func TestAddLdapSimpleAuth(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthAddLdapSimpleAuth.Flags - app.Action = service.addLdapSimpleAuth + app := &cli.Command{ + Flags: microcmdAuthAddLdapSimpleAuth().Flags, + Action: service.addLdapSimpleAuth, + } // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { @@ -487,9 +486,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { func TestUpdateLdapBindDn(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -864,7 +861,7 @@ func TestUpdateLdapBindDn(t *testing.T) { "--id", "1", "--security-protocol", "xxxxx", }, - errMsg: "Unknown security protocol name: xxxxx", + errMsg: "unknown security protocol name: xxxxx", }, // case 22 { @@ -883,7 +880,7 @@ func TestUpdateLdapBindDn(t *testing.T) { Type: auth.OAuth2, Cfg: &ldap.Source{}, }, - errMsg: "Invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2", + errMsg: "invalid authentication type. expected: LDAP (via BindDN), actual: OAuth2", }, // case 24 { @@ -947,12 +944,12 @@ func TestUpdateLdapBindDn(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthUpdateLdapBindDn.Flags - app.Action = service.updateLdapBindDn - + app := cli.Command{ + Flags: microcmdAuthUpdateLdapBindDn().Flags, + Action: service.updateLdapBindDn, + } // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { @@ -964,9 +961,7 @@ func TestUpdateLdapBindDn(t *testing.T) { func TestUpdateLdapSimpleAuth(t *testing.T) { // Mock cli functions to do not exit on error - osExiter := cli.OsExiter - defer func() { cli.OsExiter = osExiter }() - cli.OsExiter = func(code int) {} + defer test.MockVariableValue(&cli.OsExiter, func(code int) {})() // Test cases cases := []struct { @@ -1257,7 +1252,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { "--id", "1", "--security-protocol", "xxxxx", }, - errMsg: "Unknown security protocol name: xxxxx", + errMsg: "unknown security protocol name: xxxxx", }, // case 18 { @@ -1276,7 +1271,7 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { Type: auth.PAM, Cfg: &ldap.Source{}, }, - errMsg: "Invalid authentication type. expected: LDAP (simple auth), actual: PAM", + errMsg: "invalid authentication type. expected: LDAP (simple auth), actual: PAM", }, // case 20 { @@ -1337,12 +1332,12 @@ func TestUpdateLdapSimpleAuth(t *testing.T) { } // Create a copy of command to test - app := cli.NewApp() - app.Flags = microcmdAuthUpdateLdapSimpleAuth.Flags - app.Action = service.updateLdapSimpleAuth - + app := cli.Command{ + Flags: microcmdAuthUpdateLdapSimpleAuth().Flags, + Action: service.updateLdapSimpleAuth, + } // Run it - err := app.Run(c.args) + err := app.Run(t.Context(), c.args) if c.errMsg != "" { assert.EqualError(t, err, c.errMsg, "case %d: error should match", n) } else { diff --git a/cmd/admin_auth_oauth.go b/cmd/admin_auth_oauth.go index be5345d992..d1aa753500 100644 --- a/cmd/admin_auth_oauth.go +++ b/cmd/admin_auth_oauth.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "net/url" @@ -12,11 +13,11 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth/source/oauth2" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - oauthCLIFlags = []cli.Flag{ +func oauthCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Value: "", @@ -121,23 +122,34 @@ var ( Usage: "Activate automatic team membership removal depending on groups", }, } +} - microcmdAuthAddOauth = &cli.Command{ - Name: "add-oauth", - Usage: "Add new Oauth authentication source", - Action: runAddOauth, - Flags: oauthCLIFlags, +func microcmdAuthAddOauth() *cli.Command { + return &cli.Command{ + Name: "add-oauth", + Usage: "Add new Oauth authentication source", + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().runAddOauth(ctx, cmd) + }, + Flags: oauthCLIFlags(), } +} - microcmdAuthUpdateOauth = &cli.Command{ - Name: "update-oauth", - Usage: "Update existing Oauth authentication source", - Action: runUpdateOauth, - Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...), +func microcmdAuthUpdateOauth() *cli.Command { + return &cli.Command{ + Name: "update-oauth", + Usage: "Update existing Oauth authentication source", + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().runUpdateOauth(ctx, cmd) + }, + Flags: append(oauthCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{ + Name: "id", + Usage: "ID of authentication source", + }}, oauthCLIFlags()[1:]...)...), } -) +} -func parseOAuth2Config(c *cli.Context) *oauth2.Source { +func parseOAuth2Config(c *cli.Command) *oauth2.Source { var customURLMapping *oauth2.CustomURLMapping if c.IsSet("use-custom-urls") { customURLMapping = &oauth2.CustomURLMapping{ @@ -168,11 +180,8 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source { } } -func runAddOauth(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { +func (a *authService) runAddOauth(ctx context.Context, c *cli.Command) error { + if err := a.initDB(ctx); err != nil { return err } @@ -184,7 +193,7 @@ func runAddOauth(c *cli.Context) error { } } - return auth_model.CreateSource(ctx, &auth_model.Source{ + return a.createAuthSource(ctx, &auth_model.Source{ Type: auth_model.OAuth2, Name: c.String("name"), IsActive: true, @@ -193,19 +202,16 @@ func runAddOauth(c *cli.Context) error { }) } -func runUpdateOauth(c *cli.Context) error { +func (a *authService) runUpdateOauth(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") { return errors.New("--id flag is missing") } - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { + if err := a.initDB(ctx); err != nil { return err } - source, err := auth_model.GetSourceByID(ctx, c.Int64("id")) + source, err := a.getAuthSourceByID(ctx, c.Int64("id")) if err != nil { return err } @@ -296,5 +302,5 @@ func runUpdateOauth(c *cli.Context) error { oAuth2Config.CustomURLMapping = customURLMapping source.Cfg = oAuth2Config source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") - return auth_model.UpdateSource(ctx, source) + return a.updateAuthSource(ctx, source) } diff --git a/cmd/admin_auth_oauth_test.go b/cmd/admin_auth_oauth_test.go new file mode 100644 index 0000000000..df1bd9c1a6 --- /dev/null +++ b/cmd/admin_auth_oauth_test.go @@ -0,0 +1,333 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/services/auth/source/oauth2" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestAddOauth(t *testing.T) { + testCases := []struct { + name string + args []string + source *auth_model.Source + errMsg string + }{ + { + name: "valid config", + args: []string{ + "--name", "test", + "--provider", "github", + "--key", "some_key", + "--secret", "some_secret", + }, + source: &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Scopes: []string{}, + Provider: "github", + ClientID: "some_key", + ClientSecret: "some_secret", + }, + TwoFactorPolicy: "", + }, + }, + { + name: "valid config with openid connect", + args: []string{ + "--name", "test", + "--provider", "openidConnect", + "--key", "some_key", + "--secret", "some_secret", + "--auto-discover-url", "https://example.com", + }, + source: &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Scopes: []string{}, + Provider: "openidConnect", + ClientID: "some_key", + ClientSecret: "some_secret", + OpenIDConnectAutoDiscoveryURL: "https://example.com", + }, + TwoFactorPolicy: "", + }, + }, + { + name: "valid config with options", + args: []string{ + "--name", "test", + "--provider", "gitlab", + "--key", "some_key", + "--secret", "some_secret", + "--use-custom-urls", "true", + "--custom-token-url", "https://example.com/token", + "--custom-auth-url", "https://example.com/auth", + "--custom-profile-url", "https://example.com/profile", + "--custom-email-url", "https://example.com/email", + "--custom-tenant-id", "some_tenant", + "--icon-url", "https://example.com/icon", + "--scopes", "scope1,scope2", + "--skip-local-2fa", "true", + "--required-claim-name", "claim_name", + "--required-claim-value", "claim_value", + "--group-claim-name", "group_name", + "--admin-group", "admin", + "--restricted-group", "restricted", + "--group-team-map", `{"group1": [1,2]}`, + "--group-team-map-removal=true", + }, + source: &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "gitlab", + ClientID: "some_key", + ClientSecret: "some_secret", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: "https://example.com/token", + AuthURL: "https://example.com/auth", + ProfileURL: "https://example.com/profile", + EmailURL: "https://example.com/email", + Tenant: "some_tenant", + }, + IconURL: "https://example.com/icon", + Scopes: []string{"scope1", "scope2"}, + RequiredClaimName: "claim_name", + RequiredClaimValue: "claim_value", + GroupClaimName: "group_name", + AdminGroup: "admin", + RestrictedGroup: "restricted", + GroupTeamMap: `{"group1": [1,2]}`, + GroupTeamMapRemoval: true, + }, + TwoFactorPolicy: "skip", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var createdSource *auth_model.Source + a := &authService{ + initDB: func(ctx context.Context) error { + return nil + }, + createAuthSource: func(ctx context.Context, source *auth_model.Source) error { + createdSource = source + return nil + }, + } + + app := &cli.Command{ + Flags: microcmdAuthAddOauth().Flags, + Action: a.runAddOauth, + } + + args := []string{"oauth-test"} + args = append(args, tc.args...) + + err := app.Run(t.Context(), args) + + if tc.errMsg != "" { + assert.EqualError(t, err, tc.errMsg) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.source, createdSource) + } + }) + } +} + +func TestUpdateOauth(t *testing.T) { + testCases := []struct { + name string + args []string + id int64 + existingAuthSource *auth_model.Source + authSource *auth_model.Source + errMsg string + }{ + { + name: "missing id", + args: []string{ + "--name", "test", + }, + errMsg: "--id flag is missing", + }, + { + name: "valid config", + id: 1, + existingAuthSource: &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "old name", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "github", + ClientID: "old_key", + ClientSecret: "old_secret", + }, + TwoFactorPolicy: "", + }, + args: []string{ + "--id", "1", + "--name", "test", + "--provider", "gitlab", + "--key", "new_key", + "--secret", "new_secret", + }, + authSource: &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "gitlab", + ClientID: "new_key", + ClientSecret: "new_secret", + CustomURLMapping: &oauth2.CustomURLMapping{}, + }, + TwoFactorPolicy: "", + }, + }, + { + name: "valid config with options", + id: 1, + existingAuthSource: &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "old name", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "gitlab", + ClientID: "old_key", + ClientSecret: "old_secret", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: "https://old.example.com/token", + AuthURL: "https://old.example.com/auth", + ProfileURL: "https://old.example.com/profile", + EmailURL: "https://old.example.com/email", + Tenant: "old_tenant", + }, + IconURL: "https://old.example.com/icon", + Scopes: []string{"old_scope1", "old_scope2"}, + RequiredClaimName: "old_claim_name", + RequiredClaimValue: "old_claim_value", + GroupClaimName: "old_group_name", + AdminGroup: "old_admin", + RestrictedGroup: "old_restricted", + GroupTeamMap: `{"old_group1": [1,2]}`, + GroupTeamMapRemoval: true, + }, + TwoFactorPolicy: "", + }, + args: []string{ + "--id", "1", + "--name", "test", + "--provider", "github", + "--key", "new_key", + "--secret", "new_secret", + "--use-custom-urls", "true", + "--custom-token-url", "https://example.com/token", + "--custom-auth-url", "https://example.com/auth", + "--custom-profile-url", "https://example.com/profile", + "--custom-email-url", "https://example.com/email", + "--custom-tenant-id", "new_tenant", + "--icon-url", "https://example.com/icon", + "--scopes", "scope1,scope2", + "--skip-local-2fa=true", + "--required-claim-name", "claim_name", + "--required-claim-value", "claim_value", + "--group-claim-name", "group_name", + "--admin-group", "admin", + "--restricted-group", "restricted", + "--group-team-map", `{"group1": [1,2]}`, + "--group-team-map-removal=false", + }, + authSource: &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + Provider: "github", + ClientID: "new_key", + ClientSecret: "new_secret", + CustomURLMapping: &oauth2.CustomURLMapping{ + TokenURL: "https://example.com/token", + AuthURL: "https://example.com/auth", + ProfileURL: "https://example.com/profile", + EmailURL: "https://example.com/email", + Tenant: "new_tenant", + }, + IconURL: "https://example.com/icon", + Scopes: []string{"scope1", "scope2"}, + RequiredClaimName: "claim_name", + RequiredClaimValue: "claim_value", + GroupClaimName: "group_name", + AdminGroup: "admin", + RestrictedGroup: "restricted", + GroupTeamMap: `{"group1": [1,2]}`, + GroupTeamMapRemoval: false, + }, + TwoFactorPolicy: "skip", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + a := &authService{ + initDB: func(ctx context.Context) error { + return nil + }, + getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) { + return &auth_model.Source{ + ID: 1, + Type: auth_model.OAuth2, + Name: "test", + IsActive: true, + Cfg: &oauth2.Source{ + CustomURLMapping: &oauth2.CustomURLMapping{}, + }, + TwoFactorPolicy: "skip", + }, nil + }, + updateAuthSource: func(ctx context.Context, source *auth_model.Source) error { + assert.Equal(t, tc.authSource, source) + return nil + }, + } + + app := &cli.Command{ + Flags: microcmdAuthUpdateOauth().Flags, + Action: a.runUpdateOauth, + } + + args := []string{"oauth-test"} + args = append(args, tc.args...) + + err := app.Run(t.Context(), args) + + if tc.errMsg != "" { + assert.EqualError(t, err, tc.errMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/admin_auth_stmp.go b/cmd/admin_auth_smtp.go index babcf78cea..93e0587fc3 100644 --- a/cmd/admin_auth_stmp.go +++ b/cmd/admin_auth_smtp.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "strings" @@ -11,11 +12,11 @@ import ( "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/auth/source/smtp" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var ( - smtpCLIFlags = []cli.Flag{ +func smtpCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Value: "", @@ -38,12 +39,10 @@ var ( &cli.BoolFlag{ Name: "force-smtps", Usage: "SMTPS is always used on port 465. Set this to force SMTPS on other ports.", - Value: true, }, &cli.BoolFlag{ Name: "skip-verify", Usage: "Skip TLS verify.", - Value: true, }, &cli.StringFlag{ Name: "helo-hostname", @@ -53,7 +52,6 @@ var ( &cli.BoolFlag{ Name: "disable-helo", Usage: "Disable SMTP helo.", - Value: true, }, &cli.StringFlag{ Name: "allowed-domains", @@ -63,7 +61,6 @@ var ( &cli.BoolFlag{ Name: "skip-local-2fa", Usage: "Skip 2FA to log on.", - Value: true, }, &cli.BoolFlag{ Name: "active", @@ -71,23 +68,34 @@ var ( Value: true, }, } +} - microcmdAuthAddSMTP = &cli.Command{ - Name: "add-smtp", - Usage: "Add new SMTP authentication source", - Action: runAddSMTP, - Flags: smtpCLIFlags, +func microcmdAuthUpdateSMTP() *cli.Command { + return &cli.Command{ + Name: "update-smtp", + Usage: "Update existing SMTP authentication source", + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().runUpdateSMTP(ctx, cmd) + }, + Flags: append(smtpCLIFlags()[:1], append([]cli.Flag{&cli.Int64Flag{ + Name: "id", + Usage: "ID of authentication source", + }}, smtpCLIFlags()[1:]...)...), } +} - microcmdAuthUpdateSMTP = &cli.Command{ - Name: "update-smtp", - Usage: "Update existing SMTP authentication source", - Action: runUpdateSMTP, - Flags: append(smtpCLIFlags[:1], append([]cli.Flag{idFlag}, smtpCLIFlags[1:]...)...), +func microcmdAuthAddSMTP() *cli.Command { + return &cli.Command{ + Name: "add-smtp", + Usage: "Add new SMTP authentication source", + Action: func(ctx context.Context, cmd *cli.Command) error { + return newAuthService().runAddSMTP(ctx, cmd) + }, + Flags: smtpCLIFlags(), } -) +} -func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { +func parseSMTPConfig(c *cli.Command, conf *smtp.Source) error { if c.IsSet("auth-type") { conf.Auth = c.String("auth-type") validAuthTypes := []string{"PLAIN", "LOGIN", "CRAM-MD5"} @@ -120,11 +128,8 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { return nil } -func runAddSMTP(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { +func (a *authService) runAddSMTP(ctx context.Context, c *cli.Command) error { + if err := a.initDB(ctx); err != nil { return err } @@ -152,7 +157,7 @@ func runAddSMTP(c *cli.Context) error { smtpConfig.Auth = "PLAIN" } - return auth_model.CreateSource(ctx, &auth_model.Source{ + return a.createAuthSource(ctx, &auth_model.Source{ Type: auth_model.SMTP, Name: c.String("name"), IsActive: active, @@ -161,19 +166,16 @@ func runAddSMTP(c *cli.Context) error { }) } -func runUpdateSMTP(c *cli.Context) error { +func (a *authService) runUpdateSMTP(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") { return errors.New("--id flag is missing") } - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { + if err := a.initDB(ctx); err != nil { return err } - source, err := auth_model.GetSourceByID(ctx, c.Int64("id")) + source, err := a.getAuthSourceByID(ctx, c.Int64("id")) if err != nil { return err } @@ -194,5 +196,5 @@ func runUpdateSMTP(c *cli.Context) error { source.Cfg = smtpConfig source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") - return auth_model.UpdateSource(ctx, source) + return a.updateAuthSource(ctx, source) } diff --git a/cmd/admin_auth_smtp_test.go b/cmd/admin_auth_smtp_test.go new file mode 100644 index 0000000000..e54e01830c --- /dev/null +++ b/cmd/admin_auth_smtp_test.go @@ -0,0 +1,271 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/services/auth/source/smtp" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestAddSMTP(t *testing.T) { + testCases := []struct { + name string + args []string + source *auth_model.Source + errMsg string + }{ + { + name: "missing name", + args: []string{ + "--host", "localhost", + "--port", "25", + }, + errMsg: "name must be set", + }, + { + name: "missing host", + args: []string{ + "--name", "test", + "--port", "25", + }, + errMsg: "host must be set", + }, + { + name: "missing port", + args: []string{ + "--name", "test", + "--host", "localhost", + }, + errMsg: "port must be set", + }, + { + name: "valid config", + args: []string{ + "--name", "test", + "--host", "localhost", + "--port", "25", + }, + source: &auth_model.Source{ + Type: auth_model.SMTP, + Name: "test", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + Host: "localhost", + Port: 25, + }, + TwoFactorPolicy: "", + }, + }, + { + name: "valid config with options", + args: []string{ + "--name", "test", + "--host", "localhost", + "--port", "25", + "--auth-type", "LOGIN", + "--force-smtps", + "--skip-verify", + "--helo-hostname", "example.com", + "--disable-helo=true", + "--allowed-domains", "example.com,example.org", + "--skip-local-2fa", + "--active=false", + }, + source: &auth_model.Source{ + Type: auth_model.SMTP, + Name: "test", + IsActive: false, + Cfg: &smtp.Source{ + Auth: "LOGIN", + Host: "localhost", + Port: 25, + ForceSMTPS: true, + SkipVerify: true, + HeloHostname: "example.com", + DisableHelo: true, + AllowedDomains: "example.com,example.org", + }, + TwoFactorPolicy: "skip", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + a := &authService{ + initDB: func(ctx context.Context) error { + return nil + }, + createAuthSource: func(ctx context.Context, source *auth_model.Source) error { + assert.Equal(t, tc.source, source) + return nil + }, + } + + cmd := &cli.Command{ + Flags: microcmdAuthAddSMTP().Flags, + Action: a.runAddSMTP, + } + + args := []string{"smtp-test"} + args = append(args, tc.args...) + + t.Log(args) + err := cmd.Run(t.Context(), args) + + if tc.errMsg != "" { + assert.EqualError(t, err, tc.errMsg) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestUpdateSMTP(t *testing.T) { + testCases := []struct { + name string + args []string + existingAuthSource *auth_model.Source + authSource *auth_model.Source + errMsg string + }{ + { + name: "missing id", + args: []string{ + "--name", "test", + "--host", "localhost", + "--port", "25", + }, + errMsg: "--id flag is missing", + }, + { + name: "valid config", + existingAuthSource: &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "old name", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + Host: "old host", + Port: 26, + }, + }, + args: []string{ + "--id", "1", + "--name", "test", + "--host", "localhost", + "--port", "25", + }, + authSource: &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "test", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + Host: "localhost", + Port: 25, + }, + }, + }, + { + name: "valid config with options", + existingAuthSource: &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "old name", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + Host: "old host", + Port: 26, + HeloHostname: "old.example.com", + AllowedDomains: "old.example.com", + }, + TwoFactorPolicy: "", + }, + args: []string{ + "--id", "1", + "--name", "test", + "--host", "localhost", + "--port", "25", + "--auth-type", "LOGIN", + "--force-smtps", + "--skip-verify", + "--helo-hostname", "example.com", + "--disable-helo", + "--allowed-domains", "example.com,example.org", + "--skip-local-2fa", + "--active=false", + }, + authSource: &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "test", + IsActive: false, + Cfg: &smtp.Source{ + Auth: "LOGIN", + Host: "localhost", + Port: 25, + ForceSMTPS: true, + SkipVerify: true, + HeloHostname: "example.com", + DisableHelo: true, + AllowedDomains: "example.com,example.org", + }, + TwoFactorPolicy: "skip", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + a := &authService{ + initDB: func(ctx context.Context) error { + return nil + }, + getAuthSourceByID: func(ctx context.Context, id int64) (*auth_model.Source, error) { + return &auth_model.Source{ + ID: 1, + Type: auth_model.SMTP, + Name: "test", + IsActive: true, + Cfg: &smtp.Source{ + Auth: "PLAIN", + }, + }, nil + }, + + updateAuthSource: func(ctx context.Context, source *auth_model.Source) error { + assert.Equal(t, tc.authSource, source) + return nil + }, + } + + app := &cli.Command{ + Flags: microcmdAuthUpdateSMTP().Flags, + Action: a.runUpdateSMTP, + } + args := []string{"smtp-tests"} + args = append(args, tc.args...) + + err := app.Run(t.Context(), args) + + if tc.errMsg != "" { + assert.EqualError(t, err, tc.errMsg) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index ab769f6d0c..a5f1bd5105 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -4,11 +4,13 @@ package cmd import ( + "context" + "code.gitea.io/gitea/modules/graceful" asymkey_service "code.gitea.io/gitea/services/asymkey" repo_service "code.gitea.io/gitea/services/repository" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -25,20 +27,14 @@ var ( } ) -func runRegenerateHooks(_ *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRegenerateHooks(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { return err } return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext()) } -func runRegenerateKeys(_ *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRegenerateKeys(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { return err } diff --git a/cmd/admin_user.go b/cmd/admin_user.go index 967a6ed88a..3a24c3e56f 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -4,18 +4,18 @@ package cmd import ( - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var subcmdUser = &cli.Command{ Name: "user", Usage: "Modify users", - Subcommands: []*cli.Command{ - microcmdUserCreate, + Commands: []*cli.Command{ + microcmdUserCreate(), microcmdUserList, - microcmdUserChangePassword, - microcmdUserDelete, + microcmdUserChangePassword(), + microcmdUserDelete(), microcmdUserGenerateAccessToken, - microcmdUserMustChangePassword, + microcmdUserMustChangePassword(), }, } diff --git a/cmd/admin_user_change_password.go b/cmd/admin_user_change_password.go index f1ed46e70b..c27905b4db 100644 --- a/cmd/admin_user_change_password.go +++ b/cmd/admin_user_change_password.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" @@ -13,44 +14,41 @@ import ( "code.gitea.io/gitea/modules/setting" user_service "code.gitea.io/gitea/services/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserChangePassword = &cli.Command{ - Name: "change-password", - Usage: "Change a user's password", - Action: runChangePassword, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Value: "", - Usage: "The user to change password for", +func microcmdUserChangePassword() *cli.Command { + return &cli.Command{ + Name: "change-password", + Usage: "Change a user's password", + Action: runChangePassword, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "The user to change password for", + Required: true, + }, + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "New password to set for user", + Required: true, + }, + &cli.BoolFlag{ + Name: "must-change-password", + Usage: "User must change password (can be disabled by --must-change-password=false)", + Value: true, + }, }, - &cli.StringFlag{ - Name: "password", - Aliases: []string{"p"}, - Value: "", - Usage: "New password to set for user", - }, - &cli.BoolFlag{ - Name: "must-change-password", - Usage: "User must change password (can be disabled by --must-change-password=false)", - Value: true, - }, - }, -} - -func runChangePassword(c *cli.Context) error { - if err := argsSet(c, "username", "password"); err != nil { - return err } +} - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err +func runChangePassword(ctx context.Context, c *cli.Command) error { + if !setting.IsInTesting { + if err := initDB(ctx); err != nil { + return err + } } user, err := user_model.GetUserByName(ctx, c.String("username")) diff --git a/cmd/admin_user_change_password_test.go b/cmd/admin_user_change_password_test.go new file mode 100644 index 0000000000..17d0382af7 --- /dev/null +++ b/cmd/admin_user_change_password_test.go @@ -0,0 +1,91 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestChangePasswordCommand(t *testing.T) { + ctx := t.Context() + + defer func() { + require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) + }() + + t.Run("change password successfully", func(t *testing.T) { + // defer func() { + // require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) + // }() + // Prepare test user + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserCreate().Run(ctx, []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"}) + require.NoError(t, err) + + // load test user + userBase := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + + // Change the password + err = microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "newpassword"}) + require.NoError(t, err) + + // Verify the password has been changed + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.NotEqual(t, userBase.Passwd, user.Passwd) + assert.NotEqual(t, userBase.Salt, user.Salt) + + // Additional check for must-change-password flag + require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "anotherpassword", "--must-change-password=false"})) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.False(t, user.MustChangePassword) + + require.NoError(t, microcmdUserChangePassword().Run(ctx, []string{"change-password", "--username", "testuser", "--password", "yetanotherpassword", "--must-change-password"})) + user = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.True(t, user.MustChangePassword) + }) + + t.Run("failure cases", func(t *testing.T) { + testCases := []struct { + name string + args []string + expectedErr string + }{ + { + name: "user does not exist", + args: []string{"change-password", "--username", "nonexistentuser", "--password", "newpassword"}, + expectedErr: "user does not exist", + }, + { + name: "missing username", + args: []string{"change-password", "--password", "newpassword"}, + expectedErr: `"username" not set`, + }, + { + name: "missing password", + args: []string{"change-password", "--username", "testuser"}, + expectedErr: `"password" not set`, + }, + { + name: "too short password", + args: []string{"change-password", "--username", "testuser", "--password", "1"}, + expectedErr: "password is not long enough", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := microcmdUserChangePassword().Run(ctx, tc.args) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + }) + } + }) +} diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index 97f9bb7f06..cbdb5f90e2 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -16,87 +16,95 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserCreate = &cli.Command{ - Name: "create", - Usage: "Create a new user in database", - Action: runCreateUser, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "name", - Usage: "Username. DEPRECATED: use username instead", +func microcmdUserCreate() *cli.Command { + return &cli.Command{ + Name: "create", + Usage: "Create a new user in database", + Action: runCreateUser, + MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{ + { + Flags: [][]cli.Flag{ + { + &cli.StringFlag{ + Name: "name", + Usage: "Username. DEPRECATED: use username instead", + }, + &cli.StringFlag{ + Name: "username", + Usage: "Username", + }, + }, + }, + Required: true, + }, }, - &cli.StringFlag{ - Name: "username", - Usage: "Username", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "user-type", + Usage: "Set user's type: individual or bot", + Value: "individual", + }, + &cli.StringFlag{ + Name: "password", + Usage: "User password", + }, + &cli.StringFlag{ + Name: "email", + Usage: "User email address", + Required: true, + }, + &cli.BoolFlag{ + Name: "admin", + Usage: "User is an admin", + }, + &cli.BoolFlag{ + Name: "random-password", + Usage: "Generate a random password for the user", + }, + &cli.BoolFlag{ + Name: "must-change-password", + Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)", + HideDefault: true, + }, + &cli.IntFlag{ + Name: "random-password-length", + Usage: "Length of the random password to be generated", + Value: 12, + }, + &cli.BoolFlag{ + Name: "access-token", + Usage: "Generate access token for the user", + }, + &cli.StringFlag{ + Name: "access-token-name", + Usage: `Name of the generated access token`, + Value: "gitea-admin", + }, + &cli.StringFlag{ + Name: "access-token-scopes", + Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`, + Value: "all", + }, + &cli.BoolFlag{ + Name: "restricted", + Usage: "Make a restricted user account", + }, + &cli.StringFlag{ + Name: "fullname", + Usage: `The full, human-readable name of the user`, + }, }, - &cli.StringFlag{ - Name: "user-type", - Usage: "Set user's type: individual or bot", - Value: "individual", - }, - &cli.StringFlag{ - Name: "password", - Usage: "User password", - }, - &cli.StringFlag{ - Name: "email", - Usage: "User email address", - }, - &cli.BoolFlag{ - Name: "admin", - Usage: "User is an admin", - }, - &cli.BoolFlag{ - Name: "random-password", - Usage: "Generate a random password for the user", - }, - &cli.BoolFlag{ - Name: "must-change-password", - Usage: "User must change password after initial login, defaults to true for all users except the first one (can be disabled by --must-change-password=false)", - DisableDefaultText: true, - }, - &cli.IntFlag{ - Name: "random-password-length", - Usage: "Length of the random password to be generated", - Value: 12, - }, - &cli.BoolFlag{ - Name: "access-token", - Usage: "Generate access token for the user", - }, - &cli.StringFlag{ - Name: "access-token-name", - Usage: `Name of the generated access token`, - Value: "gitea-admin", - }, - &cli.StringFlag{ - Name: "access-token-scopes", - Usage: `Scopes of the generated access token, comma separated. Examples: "all", "public-only,read:issue", "write:repository,write:user"`, - Value: "all", - }, - &cli.BoolFlag{ - Name: "restricted", - Usage: "Make a restricted user account", - }, - &cli.StringFlag{ - Name: "fullname", - Usage: `The full, human-readable name of the user`, - }, - }, + } } -func runCreateUser(c *cli.Context) error { +func runCreateUser(ctx context.Context, c *cli.Command) error { // this command highly depends on the many setting options (create org, visibility, etc.), so it must have a full setting load first // duplicate setting loading should be safe at the moment, but it should be refactored & improved in the future. setting.LoadSettings() - if err := argsSet(c, "email"); err != nil { - return err - } - userTypes := map[string]user_model.UserType{ "individual": user_model.UserTypeIndividual, "bot": user_model.UserTypeBot, @@ -113,12 +121,6 @@ func runCreateUser(c *cli.Context) error { return errors.New("password can only be set for individual users") } } - if c.IsSet("name") && c.IsSet("username") { - return errors.New("cannot set both --name and --username flags") - } - if !c.IsSet("name") && !c.IsSet("username") { - return errors.New("one of --name or --username flags must be set") - } if c.IsSet("password") && c.IsSet("random-password") { return errors.New("cannot set both -random-password and -password flags") @@ -129,16 +131,12 @@ func runCreateUser(c *cli.Context) error { username = c.String("username") } else { username = c.String("name") - _, _ = fmt.Fprintf(c.App.ErrWriter, "--name flag is deprecated. Use --username instead.\n") + _, _ = fmt.Fprintf(c.ErrWriter, "--name flag is deprecated. Use --username instead.\n") } - ctx := c.Context if !setting.IsInTesting { - // FIXME: need to refactor the "installSignals/initDB" related code later + // FIXME: need to refactor the "initDB" related code later // it doesn't make sense to call it in (almost) every command action function - var cancel context.CancelFunc - ctx, cancel = installSignals() - defer cancel() if err := initDB(ctx); err != nil { return err } diff --git a/cmd/admin_user_create_test.go b/cmd/admin_user_create_test.go index d5952412c3..437e07d9a2 100644 --- a/cmd/admin_user_create_test.go +++ b/cmd/admin_user_create_test.go @@ -18,8 +18,6 @@ import ( ) func TestAdminUserCreate(t *testing.T) { - app := NewMainApp(AppVersion{}) - reset := func() { require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) @@ -31,8 +29,9 @@ func TestAdminUserCreate(t *testing.T) { IsAdmin bool MustChangePassword bool } + createCheck := func(name, args string) check { - require.NoError(t, app.Run(strings.Fields(fmt.Sprintf("./gitea admin user create --username %s --email %s@gitea.local %s --password foobar", name, name, args)))) + require.NoError(t, microcmdUserCreate().Run(t.Context(), strings.Fields(fmt.Sprintf("create --username %s --email %s@gitea.local %s --password foobar", name, name, args)))) u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: name}) return check{IsAdmin: u.IsAdmin, MustChangePassword: u.MustChangePassword} } @@ -51,7 +50,7 @@ func TestAdminUserCreate(t *testing.T) { }) createUser := func(name string, args ...string) error { - return app.Run(append([]string{"./gitea", "admin", "user", "create", "--username", name, "--email", name + "@gitea.local"}, args...)) + return microcmdUserCreate().Run(t.Context(), append([]string{"create", "--username", name, "--email", name + "@gitea.local"}, args...)) } t.Run("UserType", func(t *testing.T) { diff --git a/cmd/admin_user_delete.go b/cmd/admin_user_delete.go index 520557554a..f91041577c 100644 --- a/cmd/admin_user_delete.go +++ b/cmd/admin_user_delete.go @@ -4,53 +4,56 @@ package cmd import ( + "context" "errors" "fmt" "strings" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" user_service "code.gitea.io/gitea/services/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserDelete = &cli.Command{ - Name: "delete", - Usage: "Delete specific user by id, name or email", - Flags: []cli.Flag{ - &cli.Int64Flag{ - Name: "id", - Usage: "ID of user of the user to delete", +func microcmdUserDelete() *cli.Command { + return &cli.Command{ + Name: "delete", + Usage: "Delete specific user by id, name or email", + Flags: []cli.Flag{ + &cli.Int64Flag{ + Name: "id", + Usage: "ID of user of the user to delete", + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username of the user to delete", + }, + &cli.StringFlag{ + Name: "email", + Aliases: []string{"e"}, + Usage: "Email of the user to delete", + }, + &cli.BoolFlag{ + Name: "purge", + Usage: "Purge user, all their repositories, organizations and comments", + }, }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username of the user to delete", - }, - &cli.StringFlag{ - Name: "email", - Aliases: []string{"e"}, - Usage: "Email of the user to delete", - }, - &cli.BoolFlag{ - Name: "purge", - Usage: "Purge user, all their repositories, organizations and comments", - }, - }, - Action: runDeleteUser, + Action: runDeleteUser, + } } -func runDeleteUser(c *cli.Context) error { +func runDeleteUser(ctx context.Context, c *cli.Command) error { if !c.IsSet("id") && !c.IsSet("username") && !c.IsSet("email") { return errors.New("You must provide the id, username or email of a user to delete") } - ctx, cancel := installSignals() - defer cancel() - - if err := initDB(ctx); err != nil { - return err + if !setting.IsInTesting { + if err := initDB(ctx); err != nil { + return err + } } if err := storage.Init(); err != nil { @@ -70,11 +73,11 @@ func runDeleteUser(c *cli.Context) error { return err } if c.IsSet("username") && user.LowerName != strings.ToLower(strings.TrimSpace(c.String("username"))) { - return fmt.Errorf("The user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) + return fmt.Errorf("the user %s who has email %s does not match the provided username %s", user.Name, c.String("email"), c.String("username")) } if c.IsSet("id") && user.ID != c.Int64("id") { - return fmt.Errorf("The user %s does not match the provided id %d", user.Name, c.Int64("id")) + return fmt.Errorf("the user %s does not match the provided id %d", user.Name, c.Int64("id")) } return user_service.DeleteUser(ctx, user, c.Bool("purge")) diff --git a/cmd/admin_user_delete_test.go b/cmd/admin_user_delete_test.go new file mode 100644 index 0000000000..d0330582d7 --- /dev/null +++ b/cmd/admin_user_delete_test.go @@ -0,0 +1,111 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "strconv" + "strings" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/require" +) + +func TestAdminUserDelete(t *testing.T) { + ctx := t.Context() + defer func() { + require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) + require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) + require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{})) + }() + + setupTestUser := func(t *testing.T) { + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"}) + require.NoError(t, err) + } + + t.Run("delete user by id", func(t *testing.T) { + setupTestUser(t) + + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--id", strconv.FormatInt(u.ID, 10)}) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + }) + t.Run("delete user by username", func(t *testing.T) { + setupTestUser(t) + + err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--username", "testuser"}) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + }) + t.Run("delete user by email", func(t *testing.T) { + setupTestUser(t) + + err := microcmdUserDelete().Run(ctx, []string{"delete-test", "--email", "testuser@gitea.local"}) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + }) + t.Run("delete user by all 3 attributes", func(t *testing.T) { + setupTestUser(t) + + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserDelete().Run(ctx, []string{"delete", "--id", strconv.FormatInt(u.ID, 10), "--username", "testuser", "--email", "testuser@gitea.local"}) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + }) +} + +func TestAdminUserDeleteFailure(t *testing.T) { + testCases := []struct { + name string + args []string + expectedErr string + }{ + { + name: "no user to delete", + args: []string{"delete", "--username", "nonexistentuser"}, + expectedErr: "user does not exist", + }, + { + name: "user exists but provided username does not match", + args: []string{"delete", "--email", "testuser@gitea.local", "--username", "wrongusername"}, + expectedErr: "the user testuser who has email testuser@gitea.local does not match the provided username wrongusername", + }, + { + name: "user exists but provided id does not match", + args: []string{"delete", "--username", "testuser", "--id", "999"}, + expectedErr: "the user testuser does not match the provided id 999", + }, + { + name: "no required flags are provided", + args: []string{"delete"}, + expectedErr: "You must provide the id, username or email of a user to delete", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := t.Context() + if strings.Contains(tc.name, "user exists") { + unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "testuser"}) + err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"}) + require.NoError(t, err) + } + + err := microcmdUserDelete().Run(ctx, tc.args) + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + }) + + require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) + require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.EmailAddress{})) + require.NoError(t, db.TruncateBeans(db.DefaultContext, &auth_model.AccessToken{})) + } +} diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index f6db7a74bd..61064fdef4 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -4,13 +4,14 @@ package cmd import ( + "context" "errors" "fmt" auth_model "code.gitea.io/gitea/models/auth" user_model "code.gitea.io/gitea/models/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var microcmdUserGenerateAccessToken = &cli.Command{ @@ -41,14 +42,11 @@ var microcmdUserGenerateAccessToken = &cli.Command{ Action: runGenerateAccessToken, } -func runGenerateAccessToken(c *cli.Context) error { +func runGenerateAccessToken(ctx context.Context, c *cli.Command) error { if !c.IsSet("username") { return errors.New("you must provide a username to generate a token for") } - ctx, cancel := installSignals() - defer cancel() - if err := initDB(ctx); err != nil { return err } diff --git a/cmd/admin_user_list.go b/cmd/admin_user_list.go index 4c2b26d1df..e3d345e2f2 100644 --- a/cmd/admin_user_list.go +++ b/cmd/admin_user_list.go @@ -4,13 +4,14 @@ package cmd import ( + "context" "fmt" "os" "text/tabwriter" user_model "code.gitea.io/gitea/models/user" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var microcmdUserList = &cli.Command{ @@ -25,10 +26,7 @@ var microcmdUserList = &cli.Command{ }, } -func runListUsers(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runListUsers(ctx context.Context, c *cli.Command) error { if err := initDB(ctx); err != nil { return err } diff --git a/cmd/admin_user_must_change_password.go b/cmd/admin_user_must_change_password.go index 2794414259..8521853dc1 100644 --- a/cmd/admin_user_must_change_password.go +++ b/cmd/admin_user_must_change_password.go @@ -4,40 +4,41 @@ package cmd import ( + "context" "errors" "fmt" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -var microcmdUserMustChangePassword = &cli.Command{ - Name: "must-change-password", - Usage: "Set the must change password flag for the provided users or all users", - Action: runMustChangePassword, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "all", - Aliases: []string{"A"}, - Usage: "All users must change password, except those explicitly excluded with --exclude", +func microcmdUserMustChangePassword() *cli.Command { + return &cli.Command{ + Name: "must-change-password", + Usage: "Set the must change password flag for the provided users or all users", + Action: runMustChangePassword, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Aliases: []string{"A"}, + Usage: "All users must change password, except those explicitly excluded with --exclude", + }, + &cli.StringSliceFlag{ + Name: "exclude", + Aliases: []string{"e"}, + Usage: "Do not change the must-change-password flag for these users", + }, + &cli.BoolFlag{ + Name: "unset", + Usage: "Instead of setting the must-change-password flag, unset it", + }, }, - &cli.StringSliceFlag{ - Name: "exclude", - Aliases: []string{"e"}, - Usage: "Do not change the must-change-password flag for these users", - }, - &cli.BoolFlag{ - Name: "unset", - Usage: "Instead of setting the must-change-password flag, unset it", - }, - }, + } } -func runMustChangePassword(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runMustChangePassword(ctx context.Context, c *cli.Command) error { if c.NArg() == 0 && !c.IsSet("all") { return errors.New("either usernames or --all must be provided") } @@ -46,8 +47,10 @@ func runMustChangePassword(c *cli.Context) error { all := c.Bool("all") exclude := c.StringSlice("exclude") - if err := initDB(ctx); err != nil { - return err + if !setting.IsInTesting { + if err := initDB(ctx); err != nil { + return err + } } n, err := user_model.SetMustChangePassword(ctx, all, mustChangePassword, c.Args().Slice(), exclude) diff --git a/cmd/admin_user_must_change_password_test.go b/cmd/admin_user_must_change_password_test.go new file mode 100644 index 0000000000..a6611fdc04 --- /dev/null +++ b/cmd/admin_user_must_change_password_test.go @@ -0,0 +1,78 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMustChangePassword(t *testing.T) { + defer func() { + require.NoError(t, db.TruncateBeans(db.DefaultContext, &user_model.User{})) + }() + err := microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuser", "--email", "testuser@gitea.local", "--random-password"}) + require.NoError(t, err) + err = microcmdUserCreate().Run(t.Context(), []string{"create", "--username", "testuserexclude", "--email", "testuserexclude@gitea.local", "--random-password"}) + require.NoError(t, err) + // Reset password change flag + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset"}) + require.NoError(t, err) + + testUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.False(t, testUser.MustChangePassword) + testUserExclude := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.False(t, testUserExclude.MustChangePassword) + + // Make all users change password + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.True(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.True(t, testUserExclude.MustChangePassword) + + // Reset password change flag but exclude all tested users + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--unset", "--exclude", "testuser,testuserexclude"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.True(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.True(t, testUserExclude.MustChangePassword) + + // Reset password change flag by listing multiple users + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser", "testuserexclude"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.False(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.False(t, testUserExclude.MustChangePassword) + + // Exclude a user from all user + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--all", "--exclude", "testuserexclude"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.True(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.False(t, testUserExclude.MustChangePassword) + + // Unset a flag for single user + err = microcmdUserMustChangePassword().Run(t.Context(), []string{"change-test", "--unset", "testuser"}) + require.NoError(t, err) + + testUser = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuser"}) + assert.False(t, testUser.MustChangePassword) + testUserExclude = unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: "testuserexclude"}) + assert.False(t, testUserExclude.MustChangePassword) +} diff --git a/cmd/cert.go b/cmd/cert.go index 38241d71a3..53b4f9dcb4 100644 --- a/cmd/cert.go +++ b/cmd/cert.go @@ -6,6 +6,7 @@ package cmd import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -13,6 +14,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "fmt" "log" "math/big" "net" @@ -20,47 +22,59 @@ import ( "strings" "time" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -// CmdCert represents the available cert sub-command. -var CmdCert = &cli.Command{ - Name: "cert", - Usage: "Generate self-signed certificate", - Description: `Generate a self-signed X.509 certificate for a TLS server. +// cmdCert represents the available cert sub-command. +func cmdCert() *cli.Command { + return &cli.Command{ + Name: "cert", + Usage: "Generate self-signed certificate", + Description: `Generate a self-signed X.509 certificate for a TLS server. Outputs to 'cert.pem' and 'key.pem' and will overwrite existing files.`, - Action: runCert, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "host", - Value: "", - Usage: "Comma-separated hostnames and IPs to generate a certificate for", + Action: runCert, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "host", + Usage: "Comma-separated hostnames and IPs to generate a certificate for", + Required: true, + }, + &cli.StringFlag{ + Name: "ecdsa-curve", + Value: "", + Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521", + }, + &cli.IntFlag{ + Name: "rsa-bits", + Value: 3072, + Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set", + }, + &cli.StringFlag{ + Name: "start-date", + Value: "", + Usage: "Creation date formatted as Jan 1 15:04:05 2011", + }, + &cli.DurationFlag{ + Name: "duration", + Value: 365 * 24 * time.Hour, + Usage: "Duration that certificate is valid for", + }, + &cli.BoolFlag{ + Name: "ca", + Usage: "whether this cert should be its own Certificate Authority", + }, + &cli.StringFlag{ + Name: "out", + Value: "cert.pem", + Usage: "Path to the file where there certificate will be saved", + }, + &cli.StringFlag{ + Name: "keyout", + Value: "key.pem", + Usage: "Path to the file where there certificate key will be saved", + }, }, - &cli.StringFlag{ - Name: "ecdsa-curve", - Value: "", - Usage: "ECDSA curve to use to generate a key. Valid values are P224, P256, P384, P521", - }, - &cli.IntFlag{ - Name: "rsa-bits", - Value: 3072, - Usage: "Size of RSA key to generate. Ignored if --ecdsa-curve is set", - }, - &cli.StringFlag{ - Name: "start-date", - Value: "", - Usage: "Creation date formatted as Jan 1 15:04:05 2011", - }, - &cli.DurationFlag{ - Name: "duration", - Value: 365 * 24 * time.Hour, - Usage: "Duration that certificate is valid for", - }, - &cli.BoolFlag{ - Name: "ca", - Usage: "whether this cert should be its own Certificate Authority", - }, - }, + } } func publicKey(priv any) any { @@ -89,11 +103,7 @@ func pemBlockForKey(priv any) *pem.Block { } } -func runCert(c *cli.Context) error { - if err := argsSet(c, "host"); err != nil { - return err - } - +func runCert(_ context.Context, c *cli.Command) error { var priv any var err error switch c.String("ecdsa-curve") { @@ -108,17 +118,17 @@ func runCert(c *cli.Context) error { case "P521": priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) default: - log.Fatalf("Unrecognized elliptic curve: %q", c.String("ecdsa-curve")) + err = fmt.Errorf("unrecognized elliptic curve: %q", c.String("ecdsa-curve")) } if err != nil { - log.Fatalf("Failed to generate private key: %v", err) + return fmt.Errorf("failed to generate private key: %w", err) } var notBefore time.Time if startDate := c.String("start-date"); startDate != "" { notBefore, err = time.Parse("Jan 2 15:04:05 2006", startDate) if err != nil { - log.Fatalf("Failed to parse creation date: %v", err) + return fmt.Errorf("failed to parse creation date %w", err) } } else { notBefore = time.Now() @@ -129,7 +139,7 @@ func runCert(c *cli.Context) error { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { - log.Fatalf("Failed to generate serial number: %v", err) + return fmt.Errorf("failed to generate serial number: %w", err) } template := x509.Certificate{ @@ -146,8 +156,8 @@ func runCert(c *cli.Context) error { BasicConstraintsValid: true, } - hosts := strings.Split(c.String("host"), ",") - for _, h := range hosts { + hosts := strings.SplitSeq(c.String("host"), ",") + for h := range hosts { if ip := net.ParseIP(h); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { @@ -162,35 +172,35 @@ func runCert(c *cli.Context) error { derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) if err != nil { - log.Fatalf("Failed to create certificate: %v", err) + return fmt.Errorf("failed to create certificate: %w", err) } - certOut, err := os.Create("cert.pem") + certOut, err := os.Create(c.String("out")) if err != nil { - log.Fatalf("Failed to open cert.pem for writing: %v", err) + return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err) } err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) if err != nil { - log.Fatalf("Failed to encode certificate: %v", err) + return fmt.Errorf("failed to encode certificate: %w", err) } err = certOut.Close() if err != nil { - log.Fatalf("Failed to write cert: %v", err) + return fmt.Errorf("failed to write cert: %w", err) } - log.Println("Written cert.pem") + fmt.Fprintf(c.Writer, "Written cert to %s\n", c.String("out")) - keyOut, err := os.OpenFile("key.pem", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + keyOut, err := os.OpenFile(c.String("keyout"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { - log.Fatalf("Failed to open key.pem for writing: %v", err) + return fmt.Errorf("failed to open %s for writing: %w", c.String("keyout"), err) } err = pem.Encode(keyOut, pemBlockForKey(priv)) if err != nil { - log.Fatalf("Failed to encode key: %v", err) + return fmt.Errorf("failed to encode key: %w", err) } err = keyOut.Close() if err != nil { - log.Fatalf("Failed to write key: %v", err) + return fmt.Errorf("failed to write key: %w", err) } - log.Println("Written key.pem") + fmt.Fprintf(c.Writer, "Written key to %s\n", c.String("keyout")) return nil } diff --git a/cmd/cert_test.go b/cmd/cert_test.go new file mode 100644 index 0000000000..4242d8915b --- /dev/null +++ b/cmd/cert_test.go @@ -0,0 +1,123 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCertCommand(t *testing.T) { + cases := []struct { + name string + args []string + }{ + { + name: "RSA cert generation", + args: []string{ + "cert-test", + "--host", "localhost", + "--rsa-bits", "2048", + "--duration", "1h", + "--start-date", "Jan 1 00:00:00 2024", + }, + }, + { + name: "ECDSA cert generation", + args: []string{ + "cert-test", + "--host", "localhost", + "--ecdsa-curve", "P256", + "--duration", "1h", + "--start-date", "Jan 1 00:00:00 2024", + }, + }, + { + name: "mixed host, certificate authority", + args: []string{ + "cert-test", + "--host", "localhost,127.0.0.1", + "--duration", "1h", + "--start-date", "Jan 1 00:00:00 2024", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + app := cmdCert() + tempDir := t.TempDir() + + certFile := filepath.Join(tempDir, "cert.pem") + keyFile := filepath.Join(tempDir, "key.pem") + + err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile)) + require.NoError(t, err) + + assert.FileExists(t, certFile) + assert.FileExists(t, keyFile) + }) + } +} + +func TestCertCommandFailures(t *testing.T) { + cases := []struct { + name string + args []string + errMsg string + }{ + { + name: "Start Date Parsing failure", + args: []string{ + "cert-test", + "--host", "localhost", + "--start-date", "invalid-date", + }, + errMsg: "parsing time", + }, + { + name: "Unknown curve", + args: []string{ + "cert-test", + "--host", "localhost", + "--ecdsa-curve", "invalid-curve", + }, + errMsg: "unrecognized elliptic curve", + }, + { + name: "Key generation failure", + args: []string{ + "cert-test", + "--host", "localhost", + "--rsa-bits", "invalid-bits", + }, + }, + { + name: "Missing parameters", + args: []string{ + "cert-test", + }, + errMsg: `"host" not set`, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + app := cmdCert() + tempDir := t.TempDir() + + certFile := filepath.Join(tempDir, "cert.pem") + keyFile := filepath.Join(tempDir, "key.pem") + err := app.Run(t.Context(), append(c.args, "--out", certFile, "--keyout", keyFile)) + require.Error(t, err) + if c.errMsg != "" { + assert.ErrorContains(t, err, c.errMsg) + } + assert.NoFileExists(t, certFile) + assert.NoFileExists(t, keyFile) + }) + } +} diff --git a/cmd/cmd.go b/cmd/cmd.go index 423dce2674..5b96bcbf9a 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -18,20 +18,19 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // argsSet checks that all the required arguments are set. args is a list of // arguments that must be set in the passed Context. -func argsSet(c *cli.Context, args ...string) error { +func argsSet(c *cli.Command, args ...string) error { for _, a := range args { if !c.IsSet(a) { return errors.New(a + " is not set") } - if util.IsEmptyString(c.String(a)) { + if c.Value(a) == nil { return errors.New(a + " is required") } } @@ -109,7 +108,7 @@ func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) { log.GetManager().GetLogger(log.DEFAULT).ReplaceAllWriters(writer) } -func globalBool(c *cli.Context, name string) bool { +func globalBool(c *cli.Command, name string) bool { for _, ctx := range c.Lineage() { if ctx.Bool(name) { return true @@ -120,8 +119,8 @@ func globalBool(c *cli.Context, name string) bool { // PrepareConsoleLoggerLevel by default, use INFO level for console logger, but some sub-commands (for git/ssh protocol) shouldn't output any log to stdout. // Any log appears in git stdout pipe will break the git protocol, eg: client can't push and hangs forever. -func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error { - return func(c *cli.Context) error { +func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(context.Context, *cli.Command) (context.Context, error) { + return func(ctx context.Context, c *cli.Command) (context.Context, error) { level := defaultLevel if globalBool(c, "quiet") { level = log.FATAL @@ -130,6 +129,16 @@ func PrepareConsoleLoggerLevel(defaultLevel log.Level) func(*cli.Context) error level = log.TRACE } log.SetConsoleLogger(log.DEFAULT, "console-default", level) - return nil + return ctx, nil } } + +func isValidDefaultSubCommand(cmd *cli.Command) (string, bool) { + // Dirty patch for urfave/cli's strange design. + // "./gitea bad-cmd" should not start the web server. + rootArgs := cmd.Root().Args().Slice() + if len(rootArgs) != 0 && rootArgs[0] != cmd.Name { + return rootArgs[0], false + } + return "", true +} diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 0000000000..a36d05c76e --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urfave/cli/v3" +) + +func TestDefaultCommand(t *testing.T) { + test := func(t *testing.T, args []string, expectedRetName string, expectedRetValid bool) { + called := false + cmd := &cli.Command{ + DefaultCommand: "test", + Commands: []*cli.Command{ + { + Name: "test", + Action: func(ctx context.Context, command *cli.Command) error { + retName, retValid := isValidDefaultSubCommand(command) + assert.Equal(t, expectedRetName, retName) + assert.Equal(t, expectedRetValid, retValid) + called = true + return nil + }, + }, + }, + } + assert.NoError(t, cmd.Run(t.Context(), args)) + assert.True(t, called) + } + test(t, []string{"./gitea"}, "", true) + test(t, []string{"./gitea", "test"}, "", true) + test(t, []string{"./gitea", "other"}, "other", false) +} diff --git a/cmd/docs.go b/cmd/docs.go index 605d02e3ef..098c0e9a8a 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -4,11 +4,13 @@ package cmd import ( + "context" "fmt" "os" "strings" - "github.com/urfave/cli/v2" + cli_docs "github.com/urfave/cli-docs/v3" + "github.com/urfave/cli/v3" ) // CmdDocs represents the available docs sub-command. @@ -30,16 +32,16 @@ var CmdDocs = &cli.Command{ }, } -func runDocs(ctx *cli.Context) error { - docs, err := ctx.App.ToMarkdown() - if ctx.Bool("man") { - docs, err = ctx.App.ToMan() +func runDocs(_ context.Context, cmd *cli.Command) error { + docs, err := cli_docs.ToMarkdown(cmd.Root()) + if cmd.Bool("man") { + docs, err = cli_docs.ToMan(cmd.Root()) } if err != nil { return err } - if !ctx.Bool("man") { + if !cmd.Bool("man") { // Clean up markdown. The following bug was fixed in v2, but is present in v1. // It affects markdown output (even though the issue is referring to man pages) // https://github.com/urfave/cli/issues/1040 @@ -51,8 +53,8 @@ func runDocs(ctx *cli.Context) error { } out := os.Stdout - if ctx.String("output") != "" { - fi, err := os.Create(ctx.String("output")) + if cmd.String("output") != "" { + fi, err := os.Create(cmd.String("output")) if err != nil { return err } diff --git a/cmd/doctor.go b/cmd/doctor.go index 4a12b957f5..9e0fcbf877 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -20,7 +20,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/doctor" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "xorm.io/xorm" ) @@ -30,7 +30,7 @@ var CmdDoctor = &cli.Command{ Usage: "Diagnose and optionally fix problems, convert or re-create database tables", Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ cmdDoctorCheck, cmdRecreateTable, cmdDoctorConvert, @@ -93,16 +93,13 @@ You should back-up your database before doing this and ensure that your database Action: runRecreateTable, } -func runRecreateTable(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - +func runRecreateTable(ctx context.Context, cmd *cli.Command) error { // Redirect the default golog to here golog.SetFlags(0) golog.SetPrefix("") golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) - debug := ctx.Bool("debug") + debug := cmd.Bool("debug") setting.MustInstalled() setting.LoadDBSetting() @@ -113,15 +110,15 @@ func runRecreateTable(ctx *cli.Context) error { } setting.Database.LogSQL = debug - if err := db.InitEngine(stdCtx); err != nil { + if err := db.InitEngine(ctx); err != nil { fmt.Println(err) fmt.Println("Check if you are using the right config file. You can use a --config directive to specify one.") return nil } - args := ctx.Args() - names := make([]string, 0, ctx.NArg()) - for i := 0; i < ctx.NArg(); i++ { + args := cmd.Args() + names := make([]string, 0, cmd.NArg()) + for i := 0; i < cmd.NArg(); i++ { names = append(names, args.Get(i)) } @@ -131,7 +128,7 @@ func runRecreateTable(ctx *cli.Context) error { } recreateTables := migrate_base.RecreateTables(beans...) - return db.InitEngineWithMigration(stdCtx, func(ctx context.Context, x *xorm.Engine) error { + return db.InitEngineWithMigration(ctx, func(ctx context.Context, x *xorm.Engine) error { if err := migrations.EnsureUpToDate(ctx, x); err != nil { return err } @@ -139,11 +136,11 @@ func runRecreateTable(ctx *cli.Context) error { }) } -func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { +func setupDoctorDefaultLogger(cmd *cli.Command, colorize bool) { // Silence the default loggers setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr) - logFile := ctx.String("log-file") + logFile := cmd.String("log-file") switch logFile { case "": return // if no doctor log-file is set, do not show any log from default logger @@ -161,23 +158,20 @@ func setupDoctorDefaultLogger(ctx *cli.Context, colorize bool) { } } -func runDoctorCheck(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - +func runDoctorCheck(ctx context.Context, cmd *cli.Command) error { colorize := log.CanColorStdout - if ctx.IsSet("color") { - colorize = ctx.Bool("color") + if cmd.IsSet("color") { + colorize = cmd.Bool("color") } - setupDoctorDefaultLogger(ctx, colorize) + setupDoctorDefaultLogger(cmd, colorize) // Finally redirect the default golang's log to here golog.SetFlags(0) golog.SetPrefix("") golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) - if ctx.IsSet("list") { + if cmd.IsSet("list") { w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) _, _ = w.Write([]byte("Default\tName\tTitle\n")) doctor.SortChecks(doctor.Checks) @@ -195,12 +189,12 @@ func runDoctorCheck(ctx *cli.Context) error { } var checks []*doctor.Check - if ctx.Bool("all") { + if cmd.Bool("all") { checks = make([]*doctor.Check, len(doctor.Checks)) copy(checks, doctor.Checks) - } else if ctx.IsSet("run") { - addDefault := ctx.Bool("default") - runNamesSet := container.SetOf(ctx.StringSlice("run")...) + } else if cmd.IsSet("run") { + addDefault := cmd.Bool("default") + runNamesSet := container.SetOf(cmd.StringSlice("run")...) for _, check := range doctor.Checks { if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) { checks = append(checks, check) @@ -217,5 +211,5 @@ func runDoctorCheck(ctx *cli.Context) error { } } } - return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks) + return doctor.RunChecks(ctx, colorize, cmd.Bool("fix"), checks) } diff --git a/cmd/doctor_convert.go b/cmd/doctor_convert.go index 48c835ad0e..8cb718d383 100644 --- a/cmd/doctor_convert.go +++ b/cmd/doctor_convert.go @@ -4,13 +4,14 @@ package cmd import ( + "context" "fmt" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // cmdDoctorConvert represents the available convert sub-command. @@ -21,11 +22,8 @@ var cmdDoctorConvert = &cli.Command{ Action: runDoctorConvert, } -func runDoctorConvert(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - - if err := initDB(stdCtx); err != nil { +func runDoctorConvert(ctx context.Context, cmd *cli.Command) error { + if err := initDB(ctx); err != nil { return err } diff --git a/cmd/doctor_test.go b/cmd/doctor_test.go index 3e1ff299c5..da942b38b6 100644 --- a/cmd/doctor_test.go +++ b/cmd/doctor_test.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/services/doctor" "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestDoctorRun(t *testing.T) { @@ -22,12 +22,13 @@ func TestDoctorRun(t *testing.T) { SkipDatabaseInitialization: true, }) - app := cli.NewApp() - app.Commands = []*cli.Command{cmdDoctorCheck} - err := app.Run([]string{"./gitea", "check", "--run", "test-check"}) + app := &cli.Command{ + Commands: []*cli.Command{cmdDoctorCheck}, + } + err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"}) assert.NoError(t, err) - err = app.Run([]string{"./gitea", "check", "--run", "no-such"}) + err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "no-such"}) assert.ErrorContains(t, err, `unknown checks: "no-such"`) - err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"}) + err = app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check,no-such"}) assert.ErrorContains(t, err, `unknown checks: "no-such"`) } diff --git a/cmd/dump.go b/cmd/dump.go index 7d640b78fd..ed19e3d4bf 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -5,6 +5,7 @@ package cmd import ( + "context" "os" "path" "path/filepath" @@ -20,7 +21,7 @@ import ( "gitea.com/go-chi/session" "github.com/mholt/archiver/v3" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdDump represents the available dump sub-command. @@ -101,17 +102,17 @@ func fatal(format string, args ...any) { log.Fatal(format, args...) } -func runDump(ctx *cli.Context) error { +func runDump(ctx context.Context, cmd *cli.Command) error { setting.MustInstalled() - quite := ctx.Bool("quiet") - verbose := ctx.Bool("verbose") + quite := cmd.Bool("quiet") + verbose := cmd.Bool("verbose") if verbose && quite { fatal("Option --quiet and --verbose cannot both be set") } // outFileName is either "-" or a file name (will be made absolute) - outFileName, outType := dump.PrepareFileNameAndType(ctx.String("file"), ctx.String("type")) + outFileName, outType := dump.PrepareFileNameAndType(cmd.String("file"), cmd.String("type")) if outType == "" { fatal("Invalid output type") } @@ -136,10 +137,7 @@ func runDump(ctx *cli.Context) error { setting.DisableLoggerInit() setting.LoadSettings() // cannot access session settings otherwise - stdCtx, cancel := installSignals() - defer cancel() - - err := db.InitEngine(stdCtx) + err := db.InitEngine(ctx) if err != nil { return err } @@ -165,7 +163,7 @@ func runDump(ctx *cli.Context) error { } dumper.GlobalExcludeAbsPath(outFileName) - if ctx.IsSet("skip-repository") && ctx.Bool("skip-repository") { + if cmd.IsSet("skip-repository") && cmd.Bool("skip-repository") { log.Info("Skip dumping local repositories") } else { log.Info("Dumping local repositories... %s", setting.RepoRootPath) @@ -173,7 +171,7 @@ func runDump(ctx *cli.Context) error { fatal("Failed to include repositories: %v", err) } - if ctx.IsSet("skip-lfs-data") && ctx.Bool("skip-lfs-data") { + if cmd.IsSet("skip-lfs-data") && cmd.Bool("skip-lfs-data") { log.Info("Skip dumping LFS data") } else if !setting.LFS.StartServer { log.Info("LFS isn't enabled. Skip dumping LFS data") @@ -188,12 +186,12 @@ func runDump(ctx *cli.Context) error { } } - if ctx.Bool("skip-db") { + if cmd.Bool("skip-db") { // Ensure that we don't dump the database file that may reside in setting.AppDataPath or elsewhere. dumper.GlobalExcludeAbsPath(setting.Database.Path) log.Info("Skipping database") } else { - tmpDir := ctx.String("tempdir") + tmpDir := cmd.String("tempdir") if _, err := os.Stat(tmpDir); os.IsNotExist(err) { fatal("Path does not exist: %s", tmpDir) } @@ -209,7 +207,7 @@ func runDump(ctx *cli.Context) error { } }() - targetDBType := ctx.String("database") + targetDBType := cmd.String("database") if len(targetDBType) > 0 && targetDBType != setting.Database.Type.String() { log.Info("Dumping database %s => %s...", setting.Database.Type, targetDBType) } else { @@ -230,7 +228,7 @@ func runDump(ctx *cli.Context) error { fatal("Failed to include specified app.ini: %v", err) } - if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") { + if cmd.IsSet("skip-custom-dir") && cmd.Bool("skip-custom-dir") { log.Info("Skipping custom directory") } else { customDir, err := os.Stat(setting.CustomPath) @@ -263,7 +261,7 @@ func runDump(ctx *cli.Context) error { excludes = append(excludes, opts.ProviderConfig) } - if ctx.IsSet("skip-index") && ctx.Bool("skip-index") { + if cmd.IsSet("skip-index") && cmd.Bool("skip-index") { excludes = append(excludes, setting.Indexer.RepoPath) excludes = append(excludes, setting.Indexer.IssuePath) } @@ -278,7 +276,7 @@ func runDump(ctx *cli.Context) error { } } - if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") { + if cmd.IsSet("skip-attachment-data") && cmd.Bool("skip-attachment-data") { log.Info("Skip dumping attachment data") } else if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error { info, err := object.Stat() @@ -290,7 +288,7 @@ func runDump(ctx *cli.Context) error { fatal("Failed to dump attachments: %v", err) } - if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") { + if cmd.IsSet("skip-package-data") && cmd.Bool("skip-package-data") { log.Info("Skip dumping package data") } else if !setting.Packages.Enabled { log.Info("Packages isn't enabled. Skip dumping package data") @@ -307,7 +305,7 @@ func runDump(ctx *cli.Context) error { // Doesn't check if LogRootPath exists before processing --skip-log intentionally, // ensuring that it's clear the dump is skipped whether the directory's initialized // yet or not. - if ctx.IsSet("skip-log") && ctx.Bool("skip-log") { + if cmd.IsSet("skip-log") && cmd.Bool("skip-log") { log.Info("Skip dumping log files") } else { isExist, err := util.IsExist(setting.Log.RootPath) diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index 11d0270404..a75b2d1b94 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -19,7 +19,7 @@ import ( "code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/migrations" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdDumpRepository represents the available dump repository sub-command. @@ -79,16 +79,13 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme }, } -func runDumpRepository(ctx *cli.Context) error { +func runDumpRepository(ctx context.Context, cmd *cli.Command) error { setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr) setting.DisableLoggerInit() setting.LoadSettings() // cannot access skip_tls_verify settings otherwise - stdCtx, cancel := installSignals() - defer cancel() - - if err := initDB(stdCtx); err != nil { + if err := initDB(ctx); err != nil { return err } @@ -105,8 +102,8 @@ func runDumpRepository(ctx *cli.Context) error { var ( serviceType structs.GitServiceType - cloneAddr = ctx.String("clone_addr") - serviceStr = ctx.String("git_service") + cloneAddr = cmd.String("clone_addr") + serviceStr = cmd.String("git_service") ) if strings.HasPrefix(strings.ToLower(cloneAddr), "https://github.com/") { @@ -124,13 +121,13 @@ func runDumpRepository(ctx *cli.Context) error { opts := base.MigrateOptions{ GitServiceType: serviceType, CloneAddr: cloneAddr, - AuthUsername: ctx.String("auth_username"), - AuthPassword: ctx.String("auth_password"), - AuthToken: ctx.String("auth_token"), - RepoName: ctx.String("repo_name"), + AuthUsername: cmd.String("auth_username"), + AuthPassword: cmd.String("auth_password"), + AuthToken: cmd.String("auth_token"), + RepoName: cmd.String("repo_name"), } - if len(ctx.String("units")) == 0 { + if len(cmd.String("units")) == 0 { opts.Wiki = true opts.Issues = true opts.Milestones = true @@ -140,8 +137,8 @@ func runDumpRepository(ctx *cli.Context) error { opts.PullRequests = true opts.ReleaseAssets = true } else { - units := strings.Split(ctx.String("units"), ",") - for _, unit := range units { + units := strings.SplitSeq(cmd.String("units"), ",") + for unit := range units { switch strings.ToLower(strings.TrimSpace(unit)) { case "": continue @@ -169,7 +166,7 @@ func runDumpRepository(ctx *cli.Context) error { // the repo_dir will be removed if error occurs in DumpRepository // make sure the directory doesn't exist or is empty, prevent from deleting user files - repoDir := ctx.String("repo_dir") + repoDir := cmd.String("repo_dir") if exists, err := util.IsExist(repoDir); err != nil { return fmt.Errorf("unable to stat repo_dir %q: %w", repoDir, err) } else if exists { @@ -184,7 +181,7 @@ func runDumpRepository(ctx *cli.Context) error { if err := migrations.DumpRepository( context.Background(), repoDir, - ctx.String("owner_name"), + cmd.String("owner_name"), opts, ); err != nil { log.Fatal("Failed to dump repository: %v", err) diff --git a/cmd/embedded.go b/cmd/embedded.go index 9f03f7be7c..1908352453 100644 --- a/cmd/embedded.go +++ b/cmd/embedded.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -19,7 +20,7 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/gobwas/glob" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdEmbedded represents the available extract sub-command. @@ -28,7 +29,7 @@ var ( Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdList, subcmdView, subcmdExtract, @@ -100,7 +101,7 @@ type assetFile struct { path string } -func initEmbeddedExtractor(c *cli.Context) error { +func initEmbeddedExtractor(c *cli.Command) error { setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr) patterns, err := compileCollectPatterns(c.Args().Slice()) @@ -115,31 +116,31 @@ func initEmbeddedExtractor(c *cli.Context) error { return nil } -func runList(c *cli.Context) error { +func runList(_ context.Context, c *cli.Command) error { if err := runListDo(c); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) return err } return nil } -func runView(c *cli.Context) error { +func runView(_ context.Context, c *cli.Command) error { if err := runViewDo(c); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) return err } return nil } -func runExtract(c *cli.Context) error { +func runExtract(_ context.Context, c *cli.Command) error { if err := runExtractDo(c); err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) return err } return nil } -func runListDo(c *cli.Context) error { +func runListDo(c *cli.Command) error { if err := initEmbeddedExtractor(c); err != nil { return err } @@ -151,7 +152,7 @@ func runListDo(c *cli.Context) error { return nil } -func runViewDo(c *cli.Context) error { +func runViewDo(c *cli.Command) error { if err := initEmbeddedExtractor(c); err != nil { return err } @@ -174,7 +175,7 @@ func runViewDo(c *cli.Context) error { return nil } -func runExtractDo(c *cli.Context) error { +func runExtractDo(c *cli.Command) error { if err := initEmbeddedExtractor(c); err != nil { return err } @@ -216,7 +217,7 @@ func runExtractDo(c *cli.Context) error { for _, a := range matchedAssetFiles { if err := extractAsset(destdir, a, overwrite, rename); err != nil { // Non-fatal error - fmt.Fprintf(os.Stderr, "%s: %v", a.path, err) + _, _ = fmt.Fprintf(os.Stderr, "%s: %v\n", a.path, err) } } @@ -271,7 +272,7 @@ func extractAsset(d string, a assetFile, overwrite, rename bool) error { return nil } -func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, layer *assetfs.Layer) { +func collectAssetFilesByPattern(c *cli.Command, globs []glob.Glob, path string, layer *assetfs.Layer) { fs := assetfs.Layered(layer) files, err := fs.ListAllFiles(".", true) if err != nil { @@ -294,16 +295,14 @@ func collectAssetFilesByPattern(c *cli.Context, globs []glob.Glob, path string, } } -func compileCollectPatterns(args []string) ([]glob.Glob, error) { +func compileCollectPatterns(args []string) (_ []glob.Glob, err error) { if len(args) == 0 { args = []string{"**"} } pat := make([]glob.Glob, len(args)) for i := range args { - if g, err := glob.Compile(args[i], '/'); err != nil { - return nil, fmt.Errorf("'%s': Invalid glob pattern: %w", args[i], err) - } else { //nolint:revive - pat[i] = g + if pat[i], err = glob.Compile(args[i], '/'); err != nil { + return nil, fmt.Errorf("invalid glob patterh %q: %w", args[i], err) } } return pat, nil diff --git a/cmd/generate.go b/cmd/generate.go index 90b32ecaf0..cf491604ef 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -5,13 +5,14 @@ package cmd import ( + "context" "fmt" "os" "code.gitea.io/gitea/modules/generate" "github.com/mattn/go-isatty" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -19,7 +20,7 @@ var ( CmdGenerate = &cli.Command{ Name: "generate", Usage: "Generate Gitea's secrets/keys/tokens", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdSecret, }, } @@ -27,7 +28,7 @@ var ( subcmdSecret = &cli.Command{ Name: "secret", Usage: "Generate a secret token", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ microcmdGenerateInternalToken, microcmdGenerateLfsJwtSecret, microcmdGenerateSecretKey, @@ -54,7 +55,7 @@ var ( } ) -func runGenerateInternalToken(c *cli.Context) error { +func runGenerateInternalToken(_ context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() if err != nil { return err @@ -69,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error { return nil } -func runGenerateLfsJwtSecret(c *cli.Context) error { +func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error { _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64() if err != nil { return err @@ -84,7 +85,7 @@ func runGenerateLfsJwtSecret(c *cli.Context) error { return nil } -func runGenerateSecretKey(c *cli.Context) error { +func runGenerateSecretKey(_ context.Context, c *cli.Command) error { secretKey, err := generate.NewSecretKey() if err != nil { return err diff --git a/cmd/hook.go b/cmd/hook.go index 41e3c3ce34..2ce272b411 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -20,11 +20,11 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) const ( - hookBatchSize = 30 + hookBatchSize = 500 ) var ( @@ -34,7 +34,7 @@ var ( Usage: "(internal) Should only be called by Git", Description: "Delegate commands to corresponding Git hooks", Before: PrepareConsoleLoggerLevel(log.FATAL), - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdHookPreReceive, subcmdHookUpdate, subcmdHookPostReceive, @@ -161,12 +161,10 @@ func (n *nilWriter) WriteString(s string) (int, error) { return len(s), nil } -func runHookPreReceive(c *cli.Context) error { +func runHookPreReceive(ctx context.Context, c *cli.Command) error { if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { return nil } - ctx, cancel := installSignals() - defer cancel() setup(ctx, c.Bool("debug")) @@ -292,7 +290,7 @@ Gitea or set your environment appropriately.`, "") // runHookUpdate avoid to do heavy operations on update hook because it will be // invoked for every ref update which does not like pre-receive and post-receive -func runHookUpdate(c *cli.Context) error { +func runHookUpdate(_ context.Context, c *cli.Command) error { if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { return nil } @@ -309,15 +307,12 @@ func runHookUpdate(c *cli.Context) error { return nil } -func runHookPostReceive(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runHookPostReceive(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) // First of all run update-server-info no matter what if _, _, err := git.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil { - return fmt.Errorf("Failed to call 'git update-server-info': %w", err) + return fmt.Errorf("failed to call 'git update-server-info': %w", err) } // Now if we're an internal don't do anything else @@ -485,7 +480,7 @@ func hookPrintResult(output, isCreate bool, branch, url string) { func pushOptions() map[string]string { opts := make(map[string]string) if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil { - for idx := 0; idx < pushCount; idx++ { + for idx := range pushCount { opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx)) kv := strings.SplitN(opt, "=", 2) if len(kv) == 2 { @@ -496,10 +491,7 @@ func pushOptions() map[string]string { return opts } -func runHookProcReceive(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runHookProcReceive(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { @@ -740,7 +732,7 @@ func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) // read prefix lengthBytes := make([]byte, 4) - for i := 0; i < 4; i++ { + for i := range 4 { lengthBytes[i], err = in.ReadByte() if err != nil { return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err) diff --git a/cmd/keys.go b/cmd/keys.go index 7fdbe16119..8710756a81 100644 --- a/cmd/keys.go +++ b/cmd/keys.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "strings" @@ -11,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdKeys represents the available keys sub-command @@ -49,7 +50,7 @@ var CmdKeys = &cli.Command{ }, } -func runKeys(c *cli.Context) error { +func runKeys(ctx context.Context, c *cli.Command) error { if !c.IsSet("username") { return errors.New("No username provided") } @@ -68,9 +69,6 @@ func runKeys(c *cli.Context) error { return errors.New("No key type and content provided") } - ctx, cancel := installSignals() - defer cancel() - setup(ctx, c.Bool("debug")) authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content) @@ -78,6 +76,6 @@ func runKeys(c *cli.Context) error { if extra.Error != nil { return extra.Error } - _, _ = fmt.Fprintln(c.App.Writer, strings.TrimSpace(authorizedString.Text)) + _, _ = fmt.Fprintln(c.Root().Writer, strings.TrimSpace(authorizedString.Text)) return nil } diff --git a/cmd/mailer.go b/cmd/mailer.go index 0c5f2c8c8d..72bd8e5601 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -4,24 +4,18 @@ package cmd import ( + "context" "fmt" "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) -func runSendMail(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runSendMail(ctx context.Context, c *cli.Command) error { setting.MustInstalled() - if err := argsSet(c, "title"); err != nil { - return err - } - subject := c.String("title") confirmSkiped := c.Bool("force") body := c.String("content") diff --git a/cmd/main.go b/cmd/main.go index 7251bd09a3..3b8a8a9311 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "fmt" "os" "strings" @@ -11,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // cmdHelp is our own help subcommand with more information @@ -22,18 +23,18 @@ func cmdHelp() *cli.Command { Aliases: []string{"h"}, Usage: "Shows a list of commands or help for one command", ArgsUsage: "[command]", - Action: func(c *cli.Context) (err error) { - lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea, {Command:nil} + Action: func(ctx context.Context, c *cli.Command) (err error) { + lineage := c.Lineage() // The order is from child to parent: help, doctor, Gitea targetCmdIdx := 0 - if c.Command.Name == "help" { + if c.Name == "help" { targetCmdIdx = 1 } - if lineage[targetCmdIdx+1].Command != nil { - err = cli.ShowCommandHelp(lineage[targetCmdIdx+1], lineage[targetCmdIdx].Command.Name) + if lineage[targetCmdIdx] != lineage[targetCmdIdx].Root() { + err = cli.ShowCommandHelp(ctx, lineage[targetCmdIdx+1] /* parent cmd */, lineage[targetCmdIdx].Name /* sub cmd */) } else { err = cli.ShowAppHelp(c) } - _, _ = fmt.Fprintf(c.App.Writer, ` + _, _ = fmt.Fprintf(c.Root().Writer, ` DEFAULT CONFIGURATION: AppPath: %s WorkPath: %s @@ -74,25 +75,25 @@ func appGlobalFlags() []cli.Flag { } } -func prepareSubcommandWithConfig(command *cli.Command, globalFlags []cli.Flag) { - command.Flags = append(append([]cli.Flag{}, globalFlags...), command.Flags...) +func prepareSubcommandWithGlobalFlags(command *cli.Command) { + command.Flags = append(append([]cli.Flag{}, appGlobalFlags()...), command.Flags...) command.Action = prepareWorkPathAndCustomConf(command.Action) command.HideHelp = true if command.Name != "help" { - command.Subcommands = append(command.Subcommands, cmdHelp()) + command.Commands = append(command.Commands, cmdHelp()) } - for i := range command.Subcommands { - prepareSubcommandWithConfig(command.Subcommands[i], globalFlags) + for i := range command.Commands { + prepareSubcommandWithGlobalFlags(command.Commands[i]) } } // prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config // It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times -func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) error { - return func(ctx *cli.Context) error { +func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(context.Context, *cli.Command) error { + return func(ctx context.Context, cmd *cli.Command) error { var args setting.ArgWorkPathAndCustomConf // from children to parent, check the global flags - for _, curCtx := range ctx.Lineage() { + for _, curCtx := range cmd.Lineage() { if curCtx.IsSet("work-path") && args.WorkPath == "" { args.WorkPath = curCtx.String("work-path") } @@ -104,11 +105,11 @@ func prepareWorkPathAndCustomConf(action cli.ActionFunc) func(ctx *cli.Context) } } setting.InitWorkPathAndCommonConfig(os.Getenv, args) - if ctx.Bool("help") || action == nil { + if cmd.Bool("help") || action == nil { // the default behavior of "urfave/cli": "nil action" means "show help" - return cmdHelp().Action(ctx) + return cmdHelp().Action(ctx, cmd) } - return action(ctx) + return action(ctx, cmd) } } @@ -117,14 +118,13 @@ type AppVersion struct { Extra string } -func NewMainApp(appVer AppVersion) *cli.App { - app := cli.NewApp() - app.Name = "Gitea" - app.HelpName = "gitea" +func NewMainApp(appVer AppVersion) *cli.Command { + app := &cli.Command{} + app.Name = "gitea" // must be lower-cased because it appears in the "USAGE" section like "gitea doctor [command [command options]]" app.Usage = "A painless self-hosted Git service" app.Description = `Gitea program contains "web" and other subcommands. If no subcommand is given, it starts the web server by default. Use "web" subcommand for more web server arguments, use other subcommands for other purposes.` app.Version = appVer.Version + appVer.Extra - app.EnableBashCompletion = true + app.EnableShellCompletion = true // these sub-commands need to use config file subCmdWithConfig := []*cli.Command{ @@ -147,20 +147,21 @@ func NewMainApp(appVer AppVersion) *cli.App { // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ - CmdCert, + cmdCert(), CmdGenerate, CmdDocs, } + // TODO: we should eventually drop the default command, + // but not sure whether it would break Windows users who used to double-click the EXE to run. app.DefaultCommand = CmdWeb.Name - globalFlags := appGlobalFlags() app.Flags = append(app.Flags, cli.VersionFlag) - app.Flags = append(app.Flags, globalFlags...) + app.Flags = append(app.Flags, appGlobalFlags()...) app.HideHelp = true // use our own help action to show helps (with more information like default config) app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { - prepareSubcommandWithConfig(subCmdWithConfig[i], globalFlags) + prepareSubcommandWithGlobalFlags(subCmdWithConfig[i]) } app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) @@ -169,8 +170,10 @@ func NewMainApp(appVer AppVersion) *cli.App { return app } -func RunMainApp(app *cli.App, args ...string) error { - err := app.Run(args) +func RunMainApp(app *cli.Command, args ...string) error { + ctx, cancel := installSignals() + defer cancel() + err := app.Run(ctx, args) if err == nil { return nil } diff --git a/cmd/main_test.go b/cmd/main_test.go index 9573cacbd4..7dfa87a0ef 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "io" @@ -16,7 +17,7 @@ import ( "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func TestMain(m *testing.M) { @@ -27,10 +28,10 @@ func makePathOutput(workPath, customPath, customConf string) string { return fmt.Sprintf("WorkPath=%s\nCustomPath=%s\nCustomConf=%s", workPath, customPath, customConf) } -func newTestApp(testCmdAction func(ctx *cli.Context) error) *cli.App { +func newTestApp(testCmdAction cli.ActionFunc) *cli.Command { app := NewMainApp(AppVersion{}) testCmd := &cli.Command{Name: "test-cmd", Action: testCmdAction} - prepareSubcommandWithConfig(testCmd, appGlobalFlags()) + prepareSubcommandWithGlobalFlags(testCmd) app.Commands = append(app.Commands, testCmd) app.DefaultCommand = testCmd.Name return app @@ -42,7 +43,7 @@ type runResult struct { ExitCode int } -func runTestApp(app *cli.App, args ...string) (runResult, error) { +func runTestApp(app *cli.Command, args ...string) (runResult, error) { outBuf := new(strings.Builder) errBuf := new(strings.Builder) app.Writer = outBuf @@ -65,7 +66,7 @@ func TestCliCmd(t *testing.T) { defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini") cli.CommandHelpTemplate = "(command help template)" - cli.AppHelpTemplate = "(app help template)" + cli.RootCommandHelpTemplate = "(app help template)" cli.SubcommandHelpTemplate = "(subcommand help template)" cases := []struct { @@ -109,12 +110,12 @@ func TestCliCmd(t *testing.T) { }, } - app := newTestApp(func(ctx *cli.Context) error { - _, _ = fmt.Fprint(ctx.App.Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) - return nil - }) for _, c := range cases { t.Run(c.cmd, func(t *testing.T) { + app := newTestApp(func(ctx context.Context, cmd *cli.Command) error { + _, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) + return nil + }) for k, v := range c.env { t.Setenv(k, v) } @@ -128,28 +129,28 @@ func TestCliCmd(t *testing.T) { } func TestCliCmdError(t *testing.T) { - app := newTestApp(func(ctx *cli.Context) error { return errors.New("normal error") }) + app := newTestApp(func(ctx context.Context, cmd *cli.Command) error { return errors.New("normal error") }) r, err := runTestApp(app, "./gitea", "test-cmd") assert.Error(t, err) assert.Equal(t, 1, r.ExitCode) assert.Empty(t, r.Stdout) assert.Equal(t, "Command error: normal error\n", r.Stderr) - app = newTestApp(func(ctx *cli.Context) error { return cli.Exit("exit error", 2) }) + app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return cli.Exit("exit error", 2) }) r, err = runTestApp(app, "./gitea", "test-cmd") assert.Error(t, err) assert.Equal(t, 2, r.ExitCode) assert.Empty(t, r.Stdout) assert.Equal(t, "exit error\n", r.Stderr) - app = newTestApp(func(ctx *cli.Context) error { return nil }) + app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil }) r, err = runTestApp(app, "./gitea", "test-cmd", "--no-such") assert.Error(t, err) assert.Equal(t, 1, r.ExitCode) - assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stdout) - assert.Empty(t, r.Stderr) // the cli package's strange behavior, the error message is not in stderr .... + assert.Empty(t, r.Stdout) + assert.Equal(t, "Incorrect Usage: flag provided but not defined: -no-such\n\n", r.Stderr) - app = newTestApp(func(ctx *cli.Context) error { return nil }) + app = newTestApp(func(ctx context.Context, cmd *cli.Command) error { return nil }) r, err = runTestApp(app, "./gitea", "test-cmd") assert.NoError(t, err) assert.Equal(t, -1, r.ExitCode) // the cli.OsExiter is not called diff --git a/cmd/manager.go b/cmd/manager.go index bd2da8edc7..f0935ea065 100644 --- a/cmd/manager.go +++ b/cmd/manager.go @@ -4,12 +4,13 @@ package cmd import ( + "context" "os" "time" "code.gitea.io/gitea/modules/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -18,7 +19,7 @@ var ( Name: "manager", Usage: "Manage the running gitea process", Description: "This is a command for managing the running gitea process", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ subcmdShutdown, subcmdRestart, subcmdReloadTemplates, @@ -108,46 +109,31 @@ var ( } ) -func runShutdown(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runShutdown(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.Shutdown(ctx) return handleCliResponseExtra(extra) } -func runRestart(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRestart(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.Restart(ctx) return handleCliResponseExtra(extra) } -func runReloadTemplates(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runReloadTemplates(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.ReloadTemplates(ctx) return handleCliResponseExtra(extra) } -func runFlushQueues(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runFlushQueues(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking")) return handleCliResponseExtra(extra) } -func runProcesses(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runProcesses(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel")) return handleCliResponseExtra(extra) diff --git a/cmd/manager_logging.go b/cmd/manager_logging.go index c2ae25ec57..ac29e7d3e5 100644 --- a/cmd/manager_logging.go +++ b/cmd/manager_logging.go @@ -4,6 +4,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -11,7 +12,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/private" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) var ( @@ -60,7 +61,7 @@ var ( subcmdLogging = &cli.Command{ Name: "logging", Usage: "Adjust logging commands", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "pause", Usage: "Pause logging (Gitea will buffer logs up to a certain point and will drop them after that point)", @@ -104,7 +105,7 @@ var ( }, { Name: "add", Usage: "Add a logger", - Subcommands: []*cli.Command{ + Commands: []*cli.Command{ { Name: "file", Usage: "Add a file logger", @@ -118,7 +119,6 @@ var ( Name: "rotate", Aliases: []string{"r"}, Usage: "Rotate logs", - Value: true, }, &cli.Int64Flag{ Name: "max-size", @@ -129,7 +129,6 @@ var ( Name: "daily", Aliases: []string{"d"}, Usage: "Rotate logs daily", - Value: true, }, &cli.IntFlag{ Name: "max-days", @@ -140,7 +139,6 @@ var ( Name: "compress", Aliases: []string{"z"}, Usage: "Compress rotated logs", - Value: true, }, &cli.IntFlag{ Name: "compression-level", @@ -195,10 +193,7 @@ var ( } ) -func runRemoveLogger(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRemoveLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) logger := c.String("logger") if len(logger) == 0 { @@ -210,10 +205,7 @@ func runRemoveLogger(c *cli.Context) error { return handleCliResponseExtra(extra) } -func runAddConnLogger(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runAddConnLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) vals := map[string]any{} mode := "conn" @@ -237,13 +229,10 @@ func runAddConnLogger(c *cli.Context) error { if c.IsSet("reconnect-on-message") { vals["reconnectOnMsg"] = c.Bool("reconnect-on-message") } - return commonAddLogger(c, mode, vals) + return commonAddLogger(ctx, c, mode, vals) } -func runAddFileLogger(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runAddFileLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) vals := map[string]any{} mode := "file" @@ -270,10 +259,10 @@ func runAddFileLogger(c *cli.Context) error { if c.IsSet("compression-level") { vals["compressionLevel"] = c.Int("compression-level") } - return commonAddLogger(c, mode, vals) + return commonAddLogger(ctx, c, mode, vals) } -func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error { +func commonAddLogger(ctx context.Context, c *cli.Command, mode string, vals map[string]any) error { if len(c.String("level")) > 0 { vals["level"] = log.LevelFromString(c.String("level")).String() } @@ -300,46 +289,33 @@ func commonAddLogger(c *cli.Context, mode string, vals map[string]any) error { if c.IsSet("writer") { writer = c.String("writer") } - ctx, cancel := installSignals() - defer cancel() extra := private.AddLogger(ctx, logger, writer, mode, vals) return handleCliResponseExtra(extra) } -func runPauseLogging(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runPauseLogging(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) userMsg := private.PauseLogging(ctx) _, _ = fmt.Fprintln(os.Stdout, userMsg) return nil } -func runResumeLogging(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runResumeLogging(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) userMsg := private.ResumeLogging(ctx) _, _ = fmt.Fprintln(os.Stdout, userMsg) return nil } -func runReleaseReopenLogging(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runReleaseReopenLogging(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) userMsg := private.ReleaseReopenLogging(ctx) _, _ = fmt.Fprintln(os.Stdout, userMsg) return nil } -func runSetLogSQL(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() +func runSetLogSQL(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) extra := private.SetLogSQL(ctx, !c.Bool("off")) diff --git a/cmd/migrate.go b/cmd/migrate.go index 25d8b50c45..e24dc9e572 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/versioned_migration" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdMigrate represents the available migrate sub-command. @@ -22,11 +22,8 @@ var CmdMigrate = &cli.Command{ Action: runMigrate, } -func runMigrate(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - - if err := initDB(stdCtx); err != nil { +func runMigrate(ctx context.Context, c *cli.Command) error { + if err := initDB(ctx); err != nil { return err } diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index f9ed140395..2c63e15f50 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -22,7 +22,7 @@ import ( "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/services/versioned_migration" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdMigrateStorage represents the available migrate storage sub-command. @@ -213,11 +213,8 @@ func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStora }) } -func runMigrateStorage(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() - - if err := initDB(stdCtx); err != nil { +func runMigrateStorage(ctx context.Context, cmd *cli.Command) error { + if err := initDB(ctx); err != nil { return err } @@ -238,51 +235,51 @@ func runMigrateStorage(ctx *cli.Context) error { var dstStorage storage.ObjectStorage var err error - switch strings.ToLower(ctx.String("storage")) { + switch strings.ToLower(cmd.String("storage")) { case "": fallthrough case string(setting.LocalStorageType): - p := ctx.String("path") + p := cmd.String("path") if p == "" { log.Fatal("Path must be given when storage is local") return nil } dstStorage, err = storage.NewLocalStorage( - stdCtx, + ctx, &setting.Storage{ Path: p, }) case string(setting.MinioStorageType): dstStorage, err = storage.NewMinioStorage( - stdCtx, + ctx, &setting.Storage{ MinioConfig: setting.MinioStorageConfig{ - Endpoint: ctx.String("minio-endpoint"), - AccessKeyID: ctx.String("minio-access-key-id"), - SecretAccessKey: ctx.String("minio-secret-access-key"), - Bucket: ctx.String("minio-bucket"), - Location: ctx.String("minio-location"), - BasePath: ctx.String("minio-base-path"), - UseSSL: ctx.Bool("minio-use-ssl"), - InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"), - ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"), - BucketLookUpType: ctx.String("minio-bucket-lookup-type"), + Endpoint: cmd.String("minio-endpoint"), + AccessKeyID: cmd.String("minio-access-key-id"), + SecretAccessKey: cmd.String("minio-secret-access-key"), + Bucket: cmd.String("minio-bucket"), + Location: cmd.String("minio-location"), + BasePath: cmd.String("minio-base-path"), + UseSSL: cmd.Bool("minio-use-ssl"), + InsecureSkipVerify: cmd.Bool("minio-insecure-skip-verify"), + ChecksumAlgorithm: cmd.String("minio-checksum-algorithm"), + BucketLookUpType: cmd.String("minio-bucket-lookup-type"), }, }) case string(setting.AzureBlobStorageType): dstStorage, err = storage.NewAzureBlobStorage( - stdCtx, + ctx, &setting.Storage{ AzureBlobConfig: setting.AzureBlobStorageConfig{ - Endpoint: ctx.String("azureblob-endpoint"), - AccountName: ctx.String("azureblob-account-name"), - AccountKey: ctx.String("azureblob-account-key"), - Container: ctx.String("azureblob-container"), - BasePath: ctx.String("azureblob-base-path"), + Endpoint: cmd.String("azureblob-endpoint"), + AccountName: cmd.String("azureblob-account-name"), + AccountKey: cmd.String("azureblob-account-key"), + Container: cmd.String("azureblob-container"), + BasePath: cmd.String("azureblob-base-path"), }, }) default: - return fmt.Errorf("unsupported storage type: %s", ctx.String("storage")) + return fmt.Errorf("unsupported storage type: %s", cmd.String("storage")) } if err != nil { return err @@ -299,14 +296,14 @@ func runMigrateStorage(ctx *cli.Context) error { "actions-artifacts": migrateActionsArtifacts, } - tp := strings.ToLower(ctx.String("type")) + tp := strings.ToLower(cmd.String("type")) if m, ok := migratedMethods[tp]; ok { - if err := m(stdCtx, dstStorage); err != nil { + if err := m(ctx, dstStorage); err != nil { return err } log.Info("%s files have successfully been copied to the new storage.", tp) return nil } - return fmt.Errorf("unsupported storage: %s", ctx.String("type")) + return fmt.Errorf("unsupported storage: %s", cmd.String("type")) } diff --git a/cmd/restore_repo.go b/cmd/restore_repo.go index 37b32aa304..c61f5a582e 100644 --- a/cmd/restore_repo.go +++ b/cmd/restore_repo.go @@ -4,12 +4,13 @@ package cmd import ( + "context" "strings" "code.gitea.io/gitea/modules/private" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdRestoreRepository represents the available restore a repository sub-command. @@ -48,10 +49,7 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme }, } -func runRestoreRepository(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runRestoreRepository(ctx context.Context, c *cli.Command) error { setting.MustInstalled() var units []string if s := c.String("units"); s != "" { diff --git a/cmd/serv.go b/cmd/serv.go index 26a3af50f3..8c6001e727 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -33,7 +33,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/kballard/go-shellquote" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // CmdServ represents the available serv sub-command. @@ -152,10 +152,7 @@ func getLFSAuthToken(ctx context.Context, lfsVerb string, results *private.ServC return "Bearer " + tokenString, nil } -func runServ(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runServ(ctx context.Context, c *cli.Command) error { // FIXME: This needs to internationalised setup(ctx, c.Bool("debug")) @@ -215,7 +212,7 @@ func runServ(c *cli.Context) error { if git.DefaultFeatures().SupportProcReceive { // for AGit Flow if cmd == "ssh_info" { - fmt.Print(`{"type":"gitea","version":1}`) + fmt.Print(`{"type":"agit","version":1}`) return nil } } diff --git a/cmd/web.go b/cmd/web.go index e47b171455..61ee3cbc20 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -28,7 +28,7 @@ import ( "code.gitea.io/gitea/routers/install" "github.com/felixge/fgprof" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // PIDFile could be set from build tag @@ -130,19 +130,19 @@ func showWebStartupMessage(msg string) { } } -func serveInstall(ctx *cli.Context) error { +func serveInstall(cmd *cli.Command) error { showWebStartupMessage("Prepare to run install page") routers.InitWebInstallPage(graceful.GetManager().HammerContext()) // Flag for port number in case first time run conflict - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { + if cmd.IsSet("port") { + if err := setPort(cmd.String("port")); err != nil { return err } } - if ctx.IsSet("install-port") { - if err := setPort(ctx.String("install-port")); err != nil { + if cmd.IsSet("install-port") { + if err := setPort(cmd.String("install-port")); err != nil { return err } } @@ -163,7 +163,7 @@ func serveInstall(ctx *cli.Context) error { return nil } -func serveInstalled(ctx *cli.Context) error { +func serveInstalled(c *cli.Command) error { setting.InitCfgProvider(setting.CustomConf) setting.LoadCommonSettings() setting.MustInstalled() @@ -218,8 +218,8 @@ func serveInstalled(ctx *cli.Context) error { setting.AppDataTempDir("").RemoveOutdated(3 * 24 * time.Hour) // Override the provided port number within the configuration - if ctx.IsSet("port") { - if err := setPort(ctx.String("port")); err != nil { + if c.IsSet("port") { + if err := setPort(c.String("port")); err != nil { return err } } @@ -244,13 +244,17 @@ func servePprof() { finished() } -func runWeb(ctx *cli.Context) error { +func runWeb(_ context.Context, cmd *cli.Command) error { defer func() { if panicked := recover(); panicked != nil { log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2)) } }() + if subCmdName, valid := isValidDefaultSubCommand(cmd); !valid { + return fmt.Errorf("unknown command: %s", subCmdName) + } + managerCtx, cancel := context.WithCancel(context.Background()) graceful.InitManager(managerCtx) defer cancel() @@ -262,12 +266,12 @@ func runWeb(ctx *cli.Context) error { } // Set pid file setting - if ctx.IsSet("pid") { - createPIDFile(ctx.String("pid")) + if cmd.IsSet("pid") { + createPIDFile(cmd.String("pid")) } if !setting.InstallLock { - if err := serveInstall(ctx); err != nil { + if err := serveInstall(cmd); err != nil { return err } } else { @@ -278,7 +282,7 @@ func runWeb(ctx *cli.Context) error { go servePprof() } - return serveInstalled(ctx) + return serveInstalled(cmd) } func setPort(port string) error { diff --git a/cmd/web_graceful.go b/cmd/web_graceful.go index 996537be3b..5e06d2c216 100644 --- a/cmd/web_graceful.go +++ b/cmd/web_graceful.go @@ -23,12 +23,6 @@ func NoHTTPRedirector() { graceful.GetManager().InformCleanup() } -// NoMainListener tells our cleanup routine that we will not be using a possibly provided listener -// for our main HTTP/HTTPS service -func NoMainListener() { - graceful.GetManager().InformCleanup() -} - // NoInstallListener tells our cleanup routine that we will not be using a possibly provided listener // for our install HTTP/HTTPS service func NoInstallListener() { diff --git a/contrib/backport/backport.go b/contrib/backport/backport.go index 44e4eacf90..2052295fb1 100644 --- a/contrib/backport/backport.go +++ b/contrib/backport/backport.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//nolint:forbidigo +//nolint:forbidigo // use of print functions is allowed in cli package main import ( @@ -12,21 +12,19 @@ import ( "net/http" "os" "os/exec" - "os/signal" "path" "strconv" "strings" - "syscall" "github.com/google/go-github/v71/github" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" "gopkg.in/yaml.v3" ) const defaultVersion = "v1.18" // to backport to func main() { - app := cli.NewApp() + app := &cli.Command{} app.Name = "backport" app.Usage = "Backport provided PR-number on to the current or previous released version" app.Description = `Backport will look-up the PR in Gitea's git log and attempt to cherry-pick it on the current version` @@ -91,7 +89,7 @@ func main() { Usage: "Set this flag to continue from a git cherry-pick that has broken", }, } - cli.AppHelpTemplate = `NAME: + cli.RootCommandHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: {{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} @@ -105,16 +103,12 @@ OPTIONS: ` app.Action = runBackport - - if err := app.Run(os.Args); err != nil { + if err := app.Run(context.Background(), os.Args); err != nil { fmt.Fprintf(os.Stderr, "Unable to backport: %v\n", err) } } -func runBackport(c *cli.Context) error { - ctx, cancel := installSignals() - defer cancel() - +func runBackport(ctx context.Context, c *cli.Command) error { continuing := c.Bool("continue") var pr string @@ -343,8 +337,8 @@ func determineRemote(ctx context.Context, forkUser string) (string, string, erro fmt.Fprintf(os.Stderr, "Unable to list git remotes:\n%s\n", string(out)) return "", "", fmt.Errorf("unable to determine forked remote: %w", err) } - lines := strings.Split(string(out), "\n") - for _, line := range lines { + lines := strings.SplitSeq(string(out), "\n") + for line := range lines { fields := strings.Split(line, "\t") name, remote := fields[0], fields[1] // only look at pushers @@ -362,12 +356,12 @@ func determineRemote(ctx context.Context, forkUser string) (string, string, erro if !strings.Contains(remote, forkUser) { continue } - if strings.HasPrefix(remote, "git@github.com:") { - forkUser = strings.TrimPrefix(remote, "git@github.com:") - } else if strings.HasPrefix(remote, "https://github.com/") { - forkUser = strings.TrimPrefix(remote, "https://github.com/") - } else if strings.HasPrefix(remote, "https://www.github.com/") { - forkUser = strings.TrimPrefix(remote, "https://www.github.com/") + if after, ok := strings.CutPrefix(remote, "git@github.com:"); ok { + forkUser = after + } else if after, ok := strings.CutPrefix(remote, "https://github.com/"); ok { + forkUser = after + } else if after, ok := strings.CutPrefix(remote, "https://www.github.com/"); ok { + forkUser = after } else if forkUser == "" { return "", "", fmt.Errorf("unable to extract forkUser from remote %s: %s", name, remote) } @@ -460,25 +454,3 @@ func determineSHAforPR(ctx context.Context, prStr, accessToken string) (string, return "", nil } - -func installSignals() (context.Context, context.CancelFunc) { - ctx, cancel := context.WithCancel(context.Background()) - go func() { - // install notify - signalChannel := make(chan os.Signal, 1) - - signal.Notify( - signalChannel, - syscall.SIGINT, - syscall.SIGTERM, - ) - select { - case <-signalChannel: - case <-ctx.Done(): - } - cancel() - signal.Reset() - }() - - return ctx, cancel -} diff --git a/contrib/environment-to-ini/environment-to-ini.go b/contrib/environment-to-ini/environment-to-ini.go index a7d7a6d293..5eb576c6fe 100644 --- a/contrib/environment-to-ini/environment-to-ini.go +++ b/contrib/environment-to-ini/environment-to-ini.go @@ -4,16 +4,17 @@ package main import ( + "context" "os" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func main() { - app := cli.NewApp() + app := cli.Command{} app.Name = "environment-to-ini" app.Usage = "Use provided environment to update configuration ini" app.Description = `As a helper to allow docker users to update the gitea configuration @@ -72,13 +73,13 @@ func main() { }, } app.Action = runEnvironmentToIni - err := app.Run(os.Args) + err := app.Run(context.Background(), os.Args) if err != nil { log.Fatal("Failed to run app with %s: %v", os.Args, err) } } -func runEnvironmentToIni(c *cli.Context) error { +func runEnvironmentToIni(_ context.Context, c *cli.Command) error { // the config system may change the environment variables, so get a copy first, to be used later env := append([]string{}, os.Environ()...) setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{ diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index a7476ad1be..aa2fcee765 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -186,17 +186,13 @@ RUN_USER = ; git ;; If you intend to use the AuthorizedPrincipalsCommand functionality then you should turn this off. ;SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE = true ;; -;; For the built-in SSH server, choose the ciphers to support for SSH connections, -;; for system SSH this setting has no effect -;SSH_SERVER_CIPHERS = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com -;; -;; For the built-in SSH server, choose the key exchange algorithms to support for SSH connections, -;; for system SSH this setting has no effect -;SSH_SERVER_KEY_EXCHANGES = curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1 -;; -;; For the built-in SSH server, choose the MACs to support for SSH connections, -;; for system SSH this setting has no effect -;SSH_SERVER_MACS = hmac-sha2-256-etm@openssh.com, hmac-sha2-256, hmac-sha1 +;; For the builtin SSH server, choose the supported ciphers/key-exchange-algorithms/MACs for SSH connections. +;; The supported names are listed in https://github.com/golang/crypto/blob/master/ssh/common.go. +;; Leave them empty to use the Golang crypto's recommended default values. +;; For system SSH (non-builtin SSH server), this setting has no effect. +;SSH_SERVER_CIPHERS = +;SSH_SERVER_KEY_EXCHANGES = +;SSH_SERVER_MACS = ;; ;; For the built-in SSH server, choose the keypair to offer as the host key ;; The private key should be at SSH_SERVER_HOST_KEY and the public SSH_SERVER_HOST_KEY.pub @@ -1190,17 +1186,24 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey +;; GPG or SSH key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey +;; Depending on the value of SIGNING_FORMAT this is either: +;; - openpgp: the GPG key ID +;; - ssh: the path to the ssh public key "/path/to/key.pub": where "/path/to/key" is the private key, use ssh-keygen -t ed25519 to generate a new key pair without password ;; run in the context of the RUN_USER ;; Switch to none to stop signing completely ;SIGNING_KEY = default ;; -;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer. +;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer and the signing format. ;; These should match a publicized name and email address for the key. (When SIGNING_KEY is default these are set to -;; the results of git config --get user.name and git config --get user.email respectively and can only be overridden +;; the results of git config --get user.name, git config --get user.email and git config --default openpgp --get gpg.format respectively and can only be overridden ;; by setting the SIGNING_KEY ID to the correct ID.) ;SIGNING_NAME = ;SIGNING_EMAIL = +;; SIGNING_FORMAT can be one of: +;; - openpgp (default): use GPG to sign commits +;; - ssh: use SSH to sign commits +;SIGNING_FORMAT = openpgp ;; ;; Sets the default trust model for repositories. Options are: collaborator, committer, collaboratorcommitter ;DEFAULT_TRUST_MODEL = collaborator @@ -1227,6 +1230,13 @@ LEVEL = Info ;; - commitssigned: require that all the commits in the head branch are signed. ;; - approved: only sign when merging an approved pr to a protected branch ;MERGES = pubkey, twofa, basesigned, commitssigned +;; +;; Determines which additional ssh keys are trusted for all signed commits regardless of the user +;; This is useful for ssh signing key rotation. +;; Exposes the provided SIGNING_NAME and SIGNING_EMAIL as the signer, regardless of the SIGNING_FORMAT value. +;; Multiple keys should be comma separated. +;; E.g."ssh-<algorithm> <key>". or "ssh-<algorithm> <key1>, ssh-<algorithm> <key2>". +;TRUSTED_SSH_KEYS = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1,6 +1,6 @@ module code.gitea.io/gitea -go 1.24.2 +go 1.24.4 // 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: @@ -51,7 +51,7 @@ require ( github.com/gliderlabs/ssh v0.3.8 github.com/go-ap/activitypub v0.0.0-20250409143848-7113328b1f3d github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 - github.com/go-chi/chi/v5 v5.2.1 + github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/cors v1.2.1 github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.2 @@ -60,7 +60,6 @@ require ( github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-redsync/redsync/v4 v4.13.0 github.com/go-sql-driver/mysql v1.9.2 - github.com/go-swagger/go-swagger v0.31.0 github.com/go-webauthn/webauthn v0.12.3 github.com/gobwas/glob v0.2.3 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f @@ -92,7 +91,7 @@ require ( github.com/minio/minio-go/v7 v7.0.91 github.com/msteinert/pam v1.2.0 github.com/nektos/act v0.2.63 - github.com/niklasfasching/go-org v1.7.0 + github.com/niklasfasching/go-org v1.8.0 github.com/olivere/elastic/v7 v7.0.32 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 @@ -105,12 +104,12 @@ require ( github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/sassoftware/go-rpmutils v0.4.0 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 - github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 github.com/stretchr/testify v1.10.0 github.com/syndtr/goleveldb v1.0.0 github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.12 - github.com/urfave/cli/v2 v2.27.6 + github.com/urfave/cli-docs/v3 v3.0.0-alpha6 + github.com/urfave/cli/v3 v3.3.3 github.com/wneessen/go-mail v0.6.2 github.com/xeipuuv/gojsonschema v1.2.0 github.com/yohcop/openid-go v1.0.1 @@ -118,14 +117,13 @@ require ( github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 gitlab.com/gitlab-org/api/client-go v0.127.0 - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.39.0 golang.org/x/image v0.26.0 - golang.org/x/net v0.39.0 + golang.org/x/net v0.40.0 golang.org/x/oauth2 v0.29.0 - golang.org/x/sync v0.13.0 - golang.org/x/sys v0.32.0 - golang.org/x/text v0.24.0 - golang.org/x/tools v0.32.0 + golang.org/x/sync v0.15.0 + golang.org/x/sys v0.33.0 + golang.org/x/text v0.26.0 google.golang.org/grpc v1.72.0 google.golang.org/protobuf v1.36.6 gopkg.in/ini.v1 v1.67.0 @@ -143,15 +141,11 @@ require ( git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/DataDog/zstd v1.5.7 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect - github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect @@ -186,7 +180,7 @@ require ( github.com/couchbase/go-couchbase v0.1.1 // indirect github.com/couchbase/gomemcached v0.3.3 // indirect github.com/couchbase/goutils v0.1.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect @@ -194,7 +188,6 @@ require ( github.com/dlclark/regexp2 v1.11.5 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.8.0 // indirect github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect github.com/go-ap/errors v0.0.0-20250409143711-5686c11ae650 // indirect @@ -203,18 +196,6 @@ require ( github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-openapi/analysis v0.23.0 // indirect - github.com/go-openapi/errors v0.22.1 // indirect - github.com/go-openapi/inflect v0.21.2 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/runtime v0.28.0 // indirect - github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect - github.com/go-openapi/validate v0.24.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/go-webauthn/x v0.1.20 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -228,7 +209,6 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-tpm v0.9.3 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -236,12 +216,9 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jessevdk/go-flags v1.6.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect github.com/libdns/libdns v1.0.0-beta.1 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/markbates/going v1.0.3 // indirect @@ -252,19 +229,15 @@ require ( github.com/miekg/dns v1.1.65 // indirect github.com/minio/crc64nvme v1.0.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect - github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nwaples/rardecode v1.1.3 // indirect - github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -273,22 +246,11 @@ require ( github.com/prometheus/procfs v0.16.1 // indirect github.com/rhysd/actionlint v1.7.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.9.0 // indirect - github.com/shopspring/decimal v1.4.0 // indirect - github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.14.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/spf13/viper v1.20.1 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/toqueteos/webbrowser v1.2.0 // indirect github.com/unknwon/com v1.0.1 // indirect github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -296,18 +258,17 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect - github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect github.com/zeebo/assert v1.3.0 // indirect github.com/zeebo/blake3 v0.2.4 // indirect go.etcd.io/bbolt v1.4.0 // indirect - go.mongodb.org/mongo-driver v1.17.3 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/mod v0.24.0 // indirect + golang.org/x/mod v0.25.0 // indirect golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.33.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250422160041-2d3770c4ea7f // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -315,8 +276,6 @@ require ( replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 -replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 - replace github.com/nektos/act => gitea.com/gitea/act v0.261.6 // TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why @@ -62,12 +62,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= -github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= -github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= -github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= @@ -103,8 +97,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= @@ -223,8 +215,8 @@ github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9B github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= @@ -274,12 +266,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -301,8 +289,8 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= @@ -325,28 +313,6 @@ 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-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= -github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= -github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= -github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= -github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= -github.com/go-openapi/inflect v0.21.2 h1:0gClGlGcxifcJR56zwvhaOulnNgnhc4qTAkob5ObnSM= -github.com/go-openapi/inflect v0.21.2/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= -github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= -github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= -github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= -github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= -github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= -github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= -github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= @@ -357,13 +323,9 @@ github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkv github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ= github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/go-swagger/go-swagger v0.31.0 h1:H8eOYQnY2u7vNKWDNykv2xJP3pBhRG/R+SOCAmKrLlc= -github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/HoaZJ5edupq7po= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE= github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY= github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw= @@ -446,8 +408,6 @@ github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= -github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5oVvhMjDyLhmA1LG86oSo+IqY= @@ -497,8 +457,6 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= -github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw= github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -540,8 +498,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/libdns v1.0.0-beta.1 h1:KIf4wLfsrEpXpZ3vmc/poM8zCATXT2klbdPe6hyOBjQ= github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= -github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 h1:F/3FfGmKdiKFa8kL3YrpZ7pe9H4l4AzA1pbaOUnRvPI= -github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0/go.mod h1:JEfTc3+2DF9Z4PXhLLvXL42zexJyh8rIq3OzUj/0rAk= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -577,14 +533,10 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.91 h1:tWLZnEfo3OZl5PoXQwcwTAPNNrjyWwOh6cbZitW5JQc= github.com/minio/minio-go/v7 v7.0.91/go.mod h1:uvMUcGrpgeSAAI6+sD3818508nUyMULw94j2Nxku/Go= -github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -599,16 +551,14 @@ github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= -github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= +github.com/niklasfasching/go-org v1.8.0 h1:WyGLaajLLp8JbQzkmapZ1y0MOzKuKV47HkZRloi+HGY= +github.com/niklasfasching/go-org v1.8.0/go.mod h1:e2A9zJs7cdONrEGs3gvxCcaAEpwwPNPG7csDpXckMNg= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= @@ -629,8 +579,6 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= @@ -674,7 +622,6 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -682,8 +629,6 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= -github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= @@ -692,10 +637,6 @@ github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLS github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= -github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+YgDpBcK1ITf3o96N/K7/wsRXQnUTEs= -github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -707,22 +648,12 @@ github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 h1:WN9BUFbdyOsSH/XohnWpXOlq9NBD5sGAB2FciQMUEe8= github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= -github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stephens2424/writerset v1.0.2/go.mod h1:aS2JhsMn6eA7e82oNmW4rfsgAOp9COBTTl8mzkwADnc= @@ -745,13 +676,9 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= -github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ= github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -761,8 +688,10 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= -github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= -github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= +github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU= +github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= +github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= @@ -782,8 +711,6 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= -github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js= @@ -809,8 +736,6 @@ gitlab.com/gitlab-org/api/client-go v0.127.0/go.mod h1:bYC6fPORKSmtuPRyD9Z2rtbAj go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= -go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= -go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -835,8 +760,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= @@ -850,8 +775,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -869,8 +794,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -886,8 +811,8 @@ golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -920,8 +845,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -933,8 +858,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -946,8 +871,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -960,8 +885,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -21,7 +21,7 @@ import ( _ "code.gitea.io/gitea/modules/markup/markdown" _ "code.gitea.io/gitea/modules/markup/orgmode" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) // these flags will be set by the build flags diff --git a/models/actions/run.go b/models/actions/run.go index 498a73dc20..f0ab61b200 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -166,6 +166,17 @@ func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, err return nil, fmt.Errorf("event %s is not a pull request event", run.Event) } +func (run *ActionRun) GetWorkflowRunEventPayload() (*api.WorkflowRunPayload, error) { + if run.Event == webhook_module.HookEventWorkflowRun { + var payload api.WorkflowRunPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return &payload, nil + } + return nil, fmt.Errorf("event %s is not a workflow run event", run.Event) +} + func (run *ActionRun) IsSchedule() bool { return run.ScheduleID > 0 } diff --git a/models/actions/run_job.go b/models/actions/run_job.go index c0df19b020..bad895036d 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -185,10 +185,10 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status { return StatusSuccess case hasCancelled: return StatusCancelled - case hasFailure: - return StatusFailure case hasRunning: return StatusRunning + case hasFailure: + return StatusFailure case hasWaiting: return StatusWaiting case hasBlocked: diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index 1d50c9c8dd..5f7bb62878 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -80,22 +80,31 @@ type FindRunJobOptions struct { func (opts FindRunJobOptions) ToConds() builder.Cond { cond := builder.NewCond() if opts.RunID > 0 { - cond = cond.And(builder.Eq{"run_id": opts.RunID}) + cond = cond.And(builder.Eq{"`action_run_job`.run_id": opts.RunID}) } if opts.RepoID > 0 { - cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) - } - if opts.OwnerID > 0 { - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + cond = cond.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID}) } if opts.CommitSHA != "" { - cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA}) + cond = cond.And(builder.Eq{"`action_run_job`.commit_sha": opts.CommitSHA}) } if len(opts.Statuses) > 0 { - cond = cond.And(builder.In("status", opts.Statuses)) + cond = cond.And(builder.In("`action_run_job`.status", opts.Statuses)) } if opts.UpdatedBefore > 0 { - cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore}) + cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore}) } return cond } + +func (opts FindRunJobOptions) ToJoins() []db.JoinFunc { + if opts.OwnerID > 0 { + return []db.JoinFunc{ + func(sess db.Engine) error { + sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID) + return nil + }, + } + } + return nil +} diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go index 523d38327e..2a5eb00a6f 100644 --- a/models/actions/run_job_status_test.go +++ b/models/actions/run_job_status_test.go @@ -58,14 +58,14 @@ func TestAggregateJobStatus(t *testing.T) { {[]Status{StatusCancelled, StatusRunning}, StatusCancelled}, {[]Status{StatusCancelled, StatusBlocked}, StatusCancelled}, - // failure with other status, fail fast - // Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast. + // failure with other status, usually fail fast, but "running" wins to match GitHub's behavior + // another reason that we can't make "failure" wins over "running": it would cause a weird behavior that user cannot cancel a workflow or get current running workflows correctly by filter after a job fail. {[]Status{StatusFailure}, StatusFailure}, {[]Status{StatusFailure, StatusSuccess}, StatusFailure}, {[]Status{StatusFailure, StatusSkipped}, StatusFailure}, {[]Status{StatusFailure, StatusCancelled}, StatusCancelled}, {[]Status{StatusFailure, StatusWaiting}, StatusFailure}, - {[]Status{StatusFailure, StatusRunning}, StatusFailure}, + {[]Status{StatusFailure, StatusRunning}, StatusRunning}, {[]Status{StatusFailure, StatusBlocked}, StatusFailure}, // skipped with other status diff --git a/models/actions/run_list.go b/models/actions/run_list.go index b9b9324e07..12c55e538e 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -72,39 +72,50 @@ type FindRunOptions struct { TriggerEvent webhook_module.HookEventType Approved bool // not util.OptionalBool, it works only when it's true Status []Status + CommitSHA string } func (opts FindRunOptions) ToConds() builder.Cond { cond := builder.NewCond() if opts.RepoID > 0 { - cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) - } - if opts.OwnerID > 0 { - cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + cond = cond.And(builder.Eq{"`action_run`.repo_id": opts.RepoID}) } if opts.WorkflowID != "" { - cond = cond.And(builder.Eq{"workflow_id": opts.WorkflowID}) + cond = cond.And(builder.Eq{"`action_run`.workflow_id": opts.WorkflowID}) } if opts.TriggerUserID > 0 { - cond = cond.And(builder.Eq{"trigger_user_id": opts.TriggerUserID}) + cond = cond.And(builder.Eq{"`action_run`.trigger_user_id": opts.TriggerUserID}) } if opts.Approved { - cond = cond.And(builder.Gt{"approved_by": 0}) + cond = cond.And(builder.Gt{"`action_run`.approved_by": 0}) } if len(opts.Status) > 0 { - cond = cond.And(builder.In("status", opts.Status)) + cond = cond.And(builder.In("`action_run`.status", opts.Status)) } if opts.Ref != "" { - cond = cond.And(builder.Eq{"ref": opts.Ref}) + cond = cond.And(builder.Eq{"`action_run`.ref": opts.Ref}) } if opts.TriggerEvent != "" { - cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent}) + cond = cond.And(builder.Eq{"`action_run`.trigger_event": opts.TriggerEvent}) + } + if opts.CommitSHA != "" { + cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA}) } return cond } +func (opts FindRunOptions) ToJoins() []db.JoinFunc { + if opts.OwnerID > 0 { + return []db.JoinFunc{func(sess db.Engine) error { + sess.Join("INNER", "repository", "repository.id = repo_id AND repository.owner_id = ?", opts.OwnerID) + return nil + }} + } + return nil +} + func (opts FindRunOptions) ToOrders() string { - return "`id` DESC" + return "`action_run`.`id` DESC" } type StatusInfo struct { diff --git a/models/actions/status.go b/models/actions/status.go index eda2234137..2b1d70613c 100644 --- a/models/actions/status.go +++ b/models/actions/status.go @@ -4,6 +4,8 @@ package actions import ( + "slices" + "code.gitea.io/gitea/modules/translation" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" @@ -88,12 +90,7 @@ func (s Status) IsBlocked() bool { // In returns whether s is one of the given statuses func (s Status) In(statuses ...Status) bool { - for _, v := range statuses { - if s == v { - return true - } - } - return false + return slices.Contains(statuses, s) } func (s Status) AsResult() runnerv1.Result { diff --git a/models/actions/task.go b/models/actions/task.go index 63259582f6..e0756b10c2 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -278,14 +278,13 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask return nil, false, err } - var workflowJob *jobparser.Job - if gots, err := jobparser.Parse(job.WorkflowPayload); err != nil { + parsedWorkflows, err := jobparser.Parse(job.WorkflowPayload) + if err != nil { return nil, false, fmt.Errorf("parse workflow of job %d: %w", job.ID, err) - } else if len(gots) != 1 { + } else if len(parsedWorkflows) != 1 { return nil, false, fmt.Errorf("workflow of job %d: not single workflow", job.ID) - } else { //nolint:revive - _, workflowJob = gots[0].Job() } + _, workflowJob := parsedWorkflows[0].Job() if _, err := e.Insert(task); err != nil { return nil, false, err diff --git a/models/activities/action.go b/models/activities/action.go index 6f1837d9f6..1a0dfe6412 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -9,6 +9,7 @@ import ( "fmt" "net/url" "path" + "slices" "strconv" "strings" "time" @@ -125,12 +126,7 @@ func (at ActionType) String() string { } func (at ActionType) InActions(actions ...string) bool { - for _, action := range actions { - if action == at.String() { - return true - } - } - return false + return slices.Contains(actions, at.String()) } // Action represents user operation type and other information to @@ -191,7 +187,7 @@ func (a *Action) LoadActUser(ctx context.Context) { return } var err error - a.ActUser, err = user_model.GetUserByID(ctx, a.ActUserID) + a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID) if err == nil { return } else if user_model.IsErrUserNotExist(err) { diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go index 0cbb91df3c..b47f5dc404 100644 --- a/models/activities/notification_list.go +++ b/models/activities/notification_list.go @@ -208,10 +208,7 @@ func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.Repository repos := make(map[int64]*repo_model.Repository, len(repoIDs)) left := len(repoIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", repoIDs[:limit]). Rows(new(repo_model.Repository)) @@ -282,10 +279,7 @@ func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { issues := make(map[int64]*issues_model.Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", issueIDs[:limit]). Rows(new(issues_model.Issue)) @@ -377,10 +371,7 @@ func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { users := make(map[int64]*user_model.User, len(userIDs)) left := len(userIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", userIDs[:limit]). Rows(new(user_model.User)) @@ -428,10 +419,7 @@ func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { comments := make(map[int64]*issues_model.Comment, len(commentIDs)) left := len(commentIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", commentIDs[:limit]). Rows(new(issues_model.Comment)) diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index 3ccdbd47d3..aeaa452c9e 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -139,10 +139,7 @@ func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository return v[i].Commits > v[j].Commits }) - cnt := count - if cnt > len(v) { - cnt = len(v) - } + cnt := min(count, len(v)) return v[:cnt], nil } diff --git a/models/activities/statistic.go b/models/activities/statistic.go index 983a124550..940651d359 100644 --- a/models/activities/statistic.go +++ b/models/activities/statistic.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" ) // Statistic contains the database statistics @@ -68,7 +69,7 @@ func GetStatistic(ctx context.Context) (stats Statistic) { } stats.Counter.UsersNotActive = user_model.CountUsers(ctx, &usersNotActiveOpts) - stats.Counter.Org, _ = db.Count[organization.Organization](ctx, organization.FindOrgOptions{IncludePrivate: true}) + stats.Counter.Org, _ = db.Count[organization.Organization](ctx, organization.FindOrgOptions{IncludeVisibility: structs.VisibleTypePrivate}) stats.Counter.PublicKey, _ = e.Count(new(asymkey_model.PublicKey)) stats.Counter.Repo, _ = repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{}) stats.Counter.Watch, _ = e.Count(new(repo_model.Watch)) diff --git a/models/activities/user_heatmap.go b/models/activities/user_heatmap.go index 1f8f0f590e..ef67838be7 100644 --- a/models/activities/user_heatmap.go +++ b/models/activities/user_heatmap.go @@ -66,7 +66,7 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi Select(groupBy+" AS timestamp, count(user_id) as contributions"). Table("action"). Where(cond). - And("created_unix > ?", timeutil.TimeStampNow()-31536000). + And("created_unix > ?", timeutil.TimeStampNow()-(366+7)*86400). // (366+7) days to include the first week for the heatmap GroupBy(groupByName). OrderBy("timestamp"). Find(&hdata) diff --git a/models/asymkey/gpg_key_add.go b/models/asymkey/gpg_key_add.go index ec2031088a..1c7d2c1da2 100644 --- a/models/asymkey/gpg_key_add.go +++ b/models/asymkey/gpg_key_add.go @@ -91,7 +91,7 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil) } if err != nil { - log.Error("Unable to validate token signature. Error: %v", err) + log.Debug("AddGPGKey CheckArmoredDetachedSignature failed: %v", err) return nil, ErrGPGInvalidTokenSignature{ ID: ekeys[0].PrimaryKey.KeyIdString(), Wrapped: err, diff --git a/models/asymkey/gpg_key_verify.go b/models/asymkey/gpg_key_verify.go index 6eedb5b7ba..5ab2fd8081 100644 --- a/models/asymkey/gpg_key_verify.go +++ b/models/asymkey/gpg_key_verify.go @@ -85,7 +85,7 @@ func VerifyGPGKey(ctx context.Context, ownerID int64, keyID, token, signature st } if signer == nil { - log.Error("Unable to validate token signature. Error: %v", err) + log.Debug("VerifyGPGKey failed: no signer") return "", ErrGPGInvalidTokenSignature{ ID: key.KeyID, } diff --git a/models/asymkey/ssh_key_parse.go b/models/asymkey/ssh_key_parse.go index 46dcf4d894..fc39f28624 100644 --- a/models/asymkey/ssh_key_parse.go +++ b/models/asymkey/ssh_key_parse.go @@ -208,7 +208,7 @@ func SSHNativeParsePublicKey(keyLine string) (string, int, error) { // The ssh library can parse the key, so next we find out what key exactly we have. switch pkey.Type() { - case ssh.KeyAlgoDSA: + case ssh.KeyAlgoDSA: //nolint:staticcheck // it's deprecated rawPub := struct { Name string P, Q, G, Y *big.Int diff --git a/models/asymkey/ssh_key_verify.go b/models/asymkey/ssh_key_verify.go index 605ffe9096..0cf29ca9f1 100644 --- a/models/asymkey/ssh_key_verify.go +++ b/models/asymkey/ssh_key_verify.go @@ -35,7 +35,7 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat // edge case for Windows based shells that will add CR LF if piped to ssh-keygen command // see https://github.com/PowerShell/PowerShell/issues/5974 if sshsig.Verify(strings.NewReader(token+"\r\n"), []byte(signature), []byte(key.Content), "gitea") != nil { - log.Error("Unable to validate token signature. Error: %v", err) + log.Debug("VerifySSHKey sshsig.Verify failed: %v", err) return "", ErrSSHInvalidTokenSignature{ Fingerprint: key.Fingerprint, } diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index 2293fd89a0..3eae19b2a5 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -213,12 +213,7 @@ func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTok // ContainsCategory checks if a list of categories contains a specific category func ContainsCategory(categories []AccessTokenScopeCategory, category AccessTokenScopeCategory) bool { - for _, c := range categories { - if c == category { - return true - } - } - return false + return slices.Contains(categories, category) } // GetScopeLevelFromAccessMode converts permission access mode to scope level diff --git a/models/auth/auth_token.go b/models/auth/auth_token.go index 81f07d1a83..54ff5a0d75 100644 --- a/models/auth/auth_token.go +++ b/models/auth/auth_token.go @@ -15,7 +15,7 @@ import ( var ErrAuthTokenNotExist = util.NewNotExistErrorf("auth token does not exist") -type AuthToken struct { //nolint:revive +type AuthToken struct { //nolint:revive // export stutter ID string `xorm:"pk"` TokenHash string UserID int64 `xorm:"INDEX"` diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index c270e4856e..c2b6690116 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -12,6 +12,7 @@ import ( "fmt" "net" "net/url" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -511,12 +512,7 @@ func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error { // ScopeContains returns true if the grant scope contains the specified scope func (grant *OAuth2Grant) ScopeContains(scope string) bool { - for _, currentScope := range strings.Split(grant.Scope, " ") { - if scope == currentScope { - return true - } - } - return false + return slices.Contains(strings.Split(grant.Scope, " "), scope) } // SetNonce updates the current nonce value of a grant diff --git a/models/db/context.go b/models/db/context.go index 4b98796ef0..ad99ada8c8 100644 --- a/models/db/context.go +++ b/models/db/context.go @@ -67,7 +67,7 @@ func contextSafetyCheck(e Engine) { _ = e.SQL("SELECT 1").Iterate(&m{}, func(int, any) error { callers := make([]uintptr, 32) callerNum := runtime.Callers(1, callers) - for i := 0; i < callerNum; i++ { + for i := range callerNum { if funcName := runtime.FuncForPC(callers[i]).Name(); funcName == "xorm.io/xorm.(*Session).Iterate" { contextSafetyDeniedFuncPCs = append(contextSafetyDeniedFuncPCs, callers[i]) } @@ -82,7 +82,7 @@ func contextSafetyCheck(e Engine) { // it should be very fast: xxxx ns/op callers := make([]uintptr, 32) callerNum := runtime.Callers(3, callers) // skip 3: runtime.Callers, contextSafetyCheck, GetEngine - for i := 0; i < callerNum; i++ { + for i := range callerNum { if slices.Contains(contextSafetyDeniedFuncPCs, callers[i]) { panic(errors.New("using database context in an iterator would cause corrupted results")) } @@ -178,6 +178,15 @@ func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error return txWithNoCheck(parentCtx, f) } +// WithTx2 is similar to WithTx, but it has two return values: result and error. +func WithTx2[T any](parentCtx context.Context, f func(ctx context.Context) (T, error)) (ret T, errRet error) { + errRet = WithTx(parentCtx, func(ctx context.Context) (errInner error) { + ret, errInner = f(ctx) + return errInner + }) + return ret, errRet +} + func txWithNoCheck(parentCtx context.Context, f func(ctx context.Context) error) error { sess := xormEngine.NewSession() defer sess.Close() diff --git a/models/db/name.go b/models/db/name.go index 0e11c78372..48c7fdbce5 100644 --- a/models/db/name.go +++ b/models/db/name.go @@ -5,6 +5,7 @@ package db import ( "fmt" + "slices" "strings" "unicode/utf8" @@ -80,10 +81,8 @@ func IsUsableName(reservedNames, reservedPatterns []string, name string) error { return util.NewInvalidArgumentErrorf("name is empty") } - for i := range reservedNames { - if name == reservedNames[i] { - return ErrNameReserved{name} - } + if slices.Contains(reservedNames, name) { + return ErrNameReserved{name} } for _, pat := range reservedPatterns { diff --git a/models/db/sql_postgres_with_schema.go b/models/db/sql_postgres_with_schema.go index 64b61b2ef3..812fe4a6a6 100644 --- a/models/db/sql_postgres_with_schema.go +++ b/models/db/sql_postgres_with_schema.go @@ -39,7 +39,7 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) { // golangci lint is incorrect here - there is no benefit to using driver.ExecerContext here // and in any case pq does not implement it - if execer, ok := conn.(driver.Execer); ok { //nolint:staticcheck + if execer, ok := conn.(driver.Execer); ok { //nolint:staticcheck // see above _, err := execer.Exec(`SELECT set_config( 'search_path', $1 || ',' || current_setting('search_path'), @@ -64,7 +64,7 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) { // driver.String.ConvertValue will never return err for string // golangci lint is incorrect here - there is no benefit to using stmt.ExecWithContext here - _, err = stmt.Exec([]driver.Value{schemaValue}) //nolint:staticcheck + _, err = stmt.Exec([]driver.Value{schemaValue}) //nolint:staticcheck // see above if err != nil { _ = conn.Close() return nil, err diff --git a/models/dbfs/dbfile.go b/models/dbfs/dbfile.go index dd27b5c36b..eaf506fbe6 100644 --- a/models/dbfs/dbfile.go +++ b/models/dbfs/dbfile.go @@ -46,10 +46,7 @@ func (f *file) readAt(fileMeta *dbfsMeta, offset int64, p []byte) (n int, err er blobPos := int(offset % f.blockSize) blobOffset := offset - int64(blobPos) blobRemaining := int(f.blockSize) - blobPos - needRead := len(p) - if needRead > blobRemaining { - needRead = blobRemaining - } + needRead := min(len(p), blobRemaining) if blobOffset+int64(blobPos)+int64(needRead) > fileMeta.FileSize { needRead = int(fileMeta.FileSize - blobOffset - int64(blobPos)) } @@ -66,14 +63,8 @@ func (f *file) readAt(fileMeta *dbfsMeta, offset int64, p []byte) (n int, err er blobData = nil } - canCopy := len(blobData) - blobPos - if canCopy <= 0 { - canCopy = 0 - } - realRead := needRead - if realRead > canCopy { - realRead = canCopy - } + canCopy := max(len(blobData)-blobPos, 0) + realRead := min(needRead, canCopy) if realRead > 0 { copy(p[:realRead], fileData.BlobData[blobPos:blobPos+realRead]) } @@ -113,10 +104,7 @@ func (f *file) Write(p []byte) (n int, err error) { blobPos := int(f.offset % f.blockSize) blobOffset := f.offset - int64(blobPos) blobRemaining := int(f.blockSize) - blobPos - needWrite := len(p) - if needWrite > blobRemaining { - needWrite = blobRemaining - } + needWrite := min(len(p), blobRemaining) buf := make([]byte, f.blockSize) readBytes, err := f.readAt(fileMeta, blobOffset, buf) if err != nil && !errors.Is(err, io.EOF) { diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index ae7dc481ec..09dfa6cccb 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -9,6 +9,7 @@ ref: "refs/heads/master" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" + trigger_event: "push" is_fork_pull_request: 0 status: 1 started: 1683636528 @@ -28,6 +29,7 @@ ref: "refs/heads/master" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" + trigger_event: "push" is_fork_pull_request: 0 status: 1 started: 1683636528 @@ -47,6 +49,7 @@ ref: "refs/heads/master" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" + trigger_event: "push" is_fork_pull_request: 0 status: 6 # running started: 1683636528 @@ -66,6 +69,47 @@ ref: "refs/heads/test" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 +- + id: 802 + title: "workflow run list" + repo_id: 5 + owner_id: 3 + workflow_id: "test.yaml" + index: 191 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + trigger_event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 +- + id: 803 + title: "workflow run list for user" + repo_id: 2 + owner_id: 0 + workflow_id: "test.yaml" + index: 192 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + trigger_event: "push" is_fork_pull_request: 0 status: 1 started: 1683636528 @@ -86,6 +130,7 @@ ref: "refs/heads/test" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" + trigger_event: "push" is_fork_pull_request: 0 status: 2 started: 1683636528 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 72f8627224..6c06d94aa4 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -99,3 +99,33 @@ status: 2 started: 1683636528 stopped: 1683636626 +- + id: 203 + run_id: 802 + repo_id: 5 + owner_id: 0 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job2 + attempt: 1 + job_id: job2 + needs: '["job1"]' + task_id: 51 + status: 5 + started: 1683636528 + stopped: 1683636626 +- + id: 204 + run_id: 803 + repo_id: 2 + owner_id: 0 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job2 + attempt: 1 + job_id: job2 + needs: '["job1"]' + task_id: 51 + status: 5 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index 6536e1dda7..03e21d04b4 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -201,3 +201,15 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +- + id: 25 + repo_id: 54 + name: 'master' + commit_id: '73cf03db6ece34e12bf91e8853dc58f678f2f82d' + commit_message: 'Initial commit' + commit_time: 1671663402 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 diff --git a/models/fixtures/commit_status.yml b/models/fixtures/commit_status.yml index 20d57975ef..87c652e53a 100644 --- a/models/fixtures/commit_status.yml +++ b/models/fixtures/commit_status.yml @@ -7,6 +7,7 @@ target_url: https://example.com/builds/ description: My awesome CI-service context: ci/awesomeness + context_hash: c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7 creator_id: 2 - @@ -18,6 +19,7 @@ target_url: https://example.com/converage/ description: My awesome Coverage service context: cov/awesomeness + context_hash: 3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe creator_id: 2 - @@ -29,6 +31,7 @@ target_url: https://example.com/converage/ description: My awesome Coverage service context: cov/awesomeness + context_hash: 3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe creator_id: 2 - @@ -40,6 +43,7 @@ target_url: https://example.com/builds/ description: My awesome CI-service context: ci/awesomeness + context_hash: c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7 creator_id: 2 - @@ -51,4 +55,5 @@ target_url: https://example.com/builds/ description: My awesome deploy service context: deploy/awesomeness + context_hash: ae9547713a6665fc4261d0756904932085a41cf2 creator_id: 2 diff --git a/models/fixtures/email_address.yml b/models/fixtures/email_address.yml index b2a0432635..0f6bd9ee6d 100644 --- a/models/fixtures/email_address.yml +++ b/models/fixtures/email_address.yml @@ -81,7 +81,7 @@ - id: 11 uid: 4 - email: user4@example.com + email: User4@Example.Com lower_email: user4@example.com is_activated: true is_primary: true diff --git a/models/fixtures/hook_task.yml b/models/fixtures/hook_task.yml index d573406b36..6023719b1e 100644 --- a/models/fixtures/hook_task.yml +++ b/models/fixtures/hook_task.yml @@ -18,7 +18,7 @@ id: 2 hook_id: 1 uuid: uuid2 - is_delivered: false + is_delivered: true - id: 3 diff --git a/models/git/branch.go b/models/git/branch.go index beeb7c0689..07c94a8ba5 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -487,7 +487,7 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o ForkFrom: opts.BaseRepo.ID, Archived: optional.Some(false), } - repoCond := repo_model.SearchRepositoryCondition(&repoOpts).And(repo_model.AccessibleRepositoryCondition(doer, unit.TypeCode)) + repoCond := repo_model.SearchRepositoryCondition(repoOpts).And(repo_model.AccessibleRepositoryCondition(doer, unit.TypeCode)) if opts.Repo.ID == opts.BaseRepo.ID { // should also include the base repo's branches repoCond = repoCond.Or(builder.Eq{"id": opts.BaseRepo.ID}) diff --git a/models/git/commit_status.go b/models/git/commit_status.go index b978476c4b..f85e1b15e5 100644 --- a/models/git/commit_status.go +++ b/models/git/commit_status.go @@ -17,10 +17,10 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" @@ -30,17 +30,17 @@ import ( // CommitStatus holds a single Status of a single Commit type CommitStatus struct { - ID int64 `xorm:"pk autoincr"` - Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` - RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` - Repo *repo_model.Repository `xorm:"-"` - State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` - SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` - TargetURL string `xorm:"TEXT"` - Description string `xorm:"TEXT"` - ContextHash string `xorm:"VARCHAR(64) index"` - Context string `xorm:"TEXT"` - Creator *user_model.User `xorm:"-"` + ID int64 `xorm:"pk autoincr"` + Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` + Repo *repo_model.Repository `xorm:"-"` + State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` + SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` + TargetURL string `xorm:"TEXT"` + Description string `xorm:"TEXT"` + ContextHash string `xorm:"VARCHAR(64) index"` + Context string `xorm:"TEXT"` + Creator *user_model.User `xorm:"-"` CreatorID int64 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` @@ -230,22 +230,25 @@ func (status *CommitStatus) HideActionsURL(ctx context.Context) { // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc func CalcCommitStatus(statuses []*CommitStatus) *CommitStatus { - var lastStatus *CommitStatus - state := api.CommitStatusSuccess + if len(statuses) == 0 { + return nil + } + + states := make(commitstatus.CommitStatusStates, 0, len(statuses)) + targetURL := "" for _, status := range statuses { - if status.State.NoBetterThan(state) { - state = status.State - lastStatus = status + states = append(states, status.State) + if status.TargetURL != "" { + targetURL = status.TargetURL } } - if lastStatus == nil { - if len(statuses) > 0 { - lastStatus = statuses[0] - } else { - lastStatus = &CommitStatus{} - } + + return &CommitStatus{ + RepoID: statuses[0].RepoID, + SHA: statuses[0].SHA, + State: states.Combine(), + TargetURL: targetURL, } - return lastStatus } // CommitStatusOptions holds the options for query commit statuses @@ -298,27 +301,37 @@ type CommitStatusIndex struct { MaxIndex int64 `xorm:"index"` } +func makeRepoCommitQuery(ctx context.Context, repoID int64, sha string) *xorm.Session { + return db.GetEngine(ctx).Table(&CommitStatus{}). + Where("repo_id = ?", repoID).And("sha = ?", sha) +} + // GetLatestCommitStatus returns all statuses with a unique context for a given commit. -func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, int64, error) { - getBase := func() *xorm.Session { - return db.GetEngine(ctx).Table(&CommitStatus{}). - Where("repo_id = ?", repoID).And("sha = ?", sha) - } +func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOptions db.ListOptions) ([]*CommitStatus, error) { indices := make([]int64, 0, 10) - sess := getBase().Select("max( `index` ) as `index`"). - GroupBy("context_hash").OrderBy("max( `index` ) desc") + sess := makeRepoCommitQuery(ctx, repoID, sha). + Select("max( `index` ) as `index`"). + GroupBy("context_hash"). + OrderBy("max( `index` ) desc") if !listOptions.IsListAll() { sess = db.SetSessionPagination(sess, &listOptions) } - count, err := sess.FindAndCount(&indices) - if err != nil { - return nil, count, err + if err := sess.Find(&indices); err != nil { + return nil, err } statuses := make([]*CommitStatus, 0, len(indices)) if len(indices) == 0 { - return statuses, count, nil + return statuses, nil } - return statuses, count, getBase().And(builder.In("`index`", indices)).Find(&statuses) + err := makeRepoCommitQuery(ctx, repoID, sha).And(builder.In("`index`", indices)).Find(&statuses) + return statuses, err +} + +func CountLatestCommitStatus(ctx context.Context, repoID int64, sha string) (int64, error) { + return makeRepoCommitQuery(ctx, repoID, sha). + Select("count(context_hash)"). + GroupBy("context_hash"). + Count() } // GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs diff --git a/models/git/commit_status_summary.go b/models/git/commit_status_summary.go index 7603e7aa65..dd416fa015 100644 --- a/models/git/commit_status_summary.go +++ b/models/git/commit_status_summary.go @@ -7,19 +7,19 @@ import ( "context" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/setting" - api "code.gitea.io/gitea/modules/structs" "xorm.io/builder" ) // CommitStatusSummary holds the latest commit Status of a single Commit type CommitStatusSummary struct { - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"` - SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"` - State api.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` - TargetURL string `xorm:"TEXT"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_id_sha)"` + SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_id_sha)"` + State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` + TargetURL string `xorm:"TEXT"` } func init() { @@ -55,11 +55,15 @@ func GetLatestCommitStatusForRepoAndSHAs(ctx context.Context, repoSHAs []RepoSHA } func UpdateCommitStatusSummary(ctx context.Context, repoID int64, sha string) error { - commitStatuses, _, err := GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll) + commitStatuses, err := GetLatestCommitStatus(ctx, repoID, sha, db.ListOptionsAll) if err != nil { return err } - state := CalcCommitStatus(commitStatuses) + // it guarantees that commitStatuses is not empty because this function is always called after a commit status is created + if len(commitStatuses) == 0 { + setting.PanicInDevOrTesting("no commit statuses found for repo %d and sha %s", repoID, sha) + } + state := CalcCommitStatus(commitStatuses) // non-empty commitStatuses is guaranteed // mysql will return 0 when update a record which state hasn't been changed which behaviour is different from other database, // so we need to use insert in on duplicate if setting.Database.Type.IsMySQL() { diff --git a/models/git/commit_status_test.go b/models/git/commit_status_test.go index 37d785e938..4c0f5e891b 100644 --- a/models/git/commit_status_test.go +++ b/models/git/commit_status_test.go @@ -14,9 +14,9 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) @@ -26,7 +26,7 @@ func TestGetCommitStatuses(t *testing.T) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - sha1 := "1234123412341234123412341234123412341234" + sha1 := "1234123412341234123412341234123412341234" // the mocked commit ID in test fixtures statuses, maxResults, err := db.FindAndCount[git_model.CommitStatus](db.DefaultContext, &git_model.CommitStatusOptions{ ListOptions: db.ListOptions{Page: 1, PageSize: 50}, @@ -38,23 +38,23 @@ func TestGetCommitStatuses(t *testing.T) { assert.Len(t, statuses, 5) assert.Equal(t, "ci/awesomeness", statuses[0].Context) - assert.Equal(t, structs.CommitStatusPending, statuses[0].State) + assert.Equal(t, commitstatus.CommitStatusPending, statuses[0].State) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[0].APIURL(db.DefaultContext)) assert.Equal(t, "cov/awesomeness", statuses[1].Context) - assert.Equal(t, structs.CommitStatusWarning, statuses[1].State) + assert.Equal(t, commitstatus.CommitStatusWarning, statuses[1].State) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[1].APIURL(db.DefaultContext)) assert.Equal(t, "cov/awesomeness", statuses[2].Context) - assert.Equal(t, structs.CommitStatusSuccess, statuses[2].State) + assert.Equal(t, commitstatus.CommitStatusSuccess, statuses[2].State) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[2].APIURL(db.DefaultContext)) assert.Equal(t, "ci/awesomeness", statuses[3].Context) - assert.Equal(t, structs.CommitStatusFailure, statuses[3].State) + assert.Equal(t, commitstatus.CommitStatusFailure, statuses[3].State) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[3].APIURL(db.DefaultContext)) assert.Equal(t, "deploy/awesomeness", statuses[4].Context) - assert.Equal(t, structs.CommitStatusError, statuses[4].State) + assert.Equal(t, commitstatus.CommitStatusError, statuses[4].State) assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(db.DefaultContext)) statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](db.DefaultContext, &git_model.CommitStatusOptions{ @@ -75,110 +75,110 @@ func Test_CalcCommitStatus(t *testing.T) { { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusError, + State: commitstatus.CommitStatusError, }, { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusError, + State: commitstatus.CommitStatusFailure, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusWarning, + State: commitstatus.CommitStatusWarning, }, { - State: structs.CommitStatusPending, + State: commitstatus.CommitStatusPending, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusWarning, + State: commitstatus.CommitStatusPending, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, { - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, }, }, { statuses: []*git_model.CommitStatus{ { - State: structs.CommitStatusFailure, + State: commitstatus.CommitStatusFailure, }, { - State: structs.CommitStatusError, + State: commitstatus.CommitStatusError, }, { - State: structs.CommitStatusWarning, + State: commitstatus.CommitStatusWarning, }, }, expected: &git_model.CommitStatus{ - State: structs.CommitStatusError, + State: commitstatus.CommitStatusFailure, }, }, } for _, kase := range kases { - assert.Equal(t, kase.expected, git_model.CalcCommitStatus(kase.statuses)) + assert.Equal(t, kase.expected, git_model.CalcCommitStatus(kase.statuses), "statuses: %v", kase.statuses) } } @@ -208,7 +208,7 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) { Creator: user2, SHA: commit.ID, CommitStatus: &git_model.CommitStatus{ - State: structs.CommitStatusFailure, + State: commitstatus.CommitStatusFailure, TargetURL: "https://example.com/tests/", Context: "compliance/lint-backend", }, @@ -220,7 +220,7 @@ func TestFindRepoRecentCommitStatusContexts(t *testing.T) { Creator: user2, SHA: commit.ID, CommitStatus: &git_model.CommitStatus{ - State: structs.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, TargetURL: "https://example.com/tests/", Context: "compliance/lint-backend", }, @@ -256,3 +256,26 @@ func TestCommitStatusesHideActionsURL(t *testing.T) { assert.Empty(t, statuses[0].TargetURL) assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL) } + +func TestGetCountLatestCommitStatus(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + sha1 := "1234123412341234123412341234123412341234" // the mocked commit ID in test fixtures + + commitStatuses, err := git_model.GetLatestCommitStatus(db.DefaultContext, repo1.ID, sha1, db.ListOptions{ + Page: 1, + PageSize: 2, + }) + assert.NoError(t, err) + assert.Len(t, commitStatuses, 2) + assert.Equal(t, commitstatus.CommitStatusFailure, commitStatuses[0].State) + assert.Equal(t, "ci/awesomeness", commitStatuses[0].Context) + assert.Equal(t, commitstatus.CommitStatusError, commitStatuses[1].State) + assert.Equal(t, "deploy/awesomeness", commitStatuses[1].Context) + + count, err := git_model.CountLatestCommitStatus(db.DefaultContext, repo1.ID, sha1) + assert.NoError(t, err) + assert.EqualValues(t, 3, count) +} diff --git a/models/git/lfs.go b/models/git/lfs.go index bb6361050a..e4fa2b446a 100644 --- a/models/git/lfs.go +++ b/models/git/lfs.go @@ -112,7 +112,6 @@ type LFSMetaObject struct { ID int64 `xorm:"pk autoincr"` lfs.Pointer `xorm:"extends"` RepositoryID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` - Existing bool `xorm:"-"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` } @@ -146,7 +145,6 @@ func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMet if err != nil { return nil, err } else if exist { - m.Existing = true return m, committer.Commit() } diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index a3caed73c4..55bbe6938c 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -246,7 +246,7 @@ func (protectBranch *ProtectedBranch) GetUnprotectedFilePatterns() []glob.Glob { func getFilePatterns(filePatterns string) []glob.Glob { extarr := make([]glob.Glob, 0, 10) - for _, expr := range strings.Split(strings.ToLower(filePatterns), ";") { + for expr := range strings.SplitSeq(strings.ToLower(filePatterns), ";") { expr = strings.TrimSpace(expr) if expr != "" { if g, err := glob.Compile(expr, '.', '/'); err != nil { @@ -518,7 +518,7 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre return currentWhitelist, nil } - teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead) + teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests) if err != nil { return nil, fmt.Errorf("GetTeamsWithAccessToRepo [org_id: %d, repo_id: %d]: %v", repo.OwnerID, repo.ID, err) } diff --git a/models/issues/comment.go b/models/issues/comment.go index ab9b2042f3..9bef96d0dd 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -9,6 +9,7 @@ import ( "context" "fmt" "html/template" + "slices" "strconv" "unicode/utf8" @@ -196,12 +197,7 @@ func (t CommentType) HasMailReplySupport() bool { } func (t CommentType) CountedAsConversation() bool { - for _, ct := range ConversationCountedCommentType() { - if t == ct { - return true - } - } - return false + return slices.Contains(ConversationCountedCommentType(), t) } // ConversationCountedCommentType returns the comment types that are counted as a conversation @@ -614,7 +610,7 @@ func UpdateCommentAttachments(ctx context.Context, c *Comment, uuids []string) e if err != nil { return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) } - for i := 0; i < len(attachments); i++ { + for i := range attachments { attachments[i].IssueID = c.IssueID attachments[i].CommentID = c.ID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index b562aab500..55e67a1243 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -5,6 +5,7 @@ package issues import ( "context" + "strconv" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/renderhelper" @@ -114,7 +115,9 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu } var err error - rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo) + rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{ + FootnoteContextID: strconv.FormatInt(comment.ID, 10), + }) if comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content); err != nil { return nil, err } diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index c483ada75a..f6c485449f 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -57,10 +57,7 @@ func (comments CommentList) loadLabels(ctx context.Context) error { commentLabels := make(map[int64]*Label, len(labelIDs)) left := len(labelIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", labelIDs[:limit]). Rows(new(Label)) @@ -107,10 +104,7 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", milestoneIDs[:limit]). Find(&milestoneMaps) @@ -146,10 +140,7 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error { milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", milestoneIDs[:limit]). Find(&milestoneMaps) @@ -184,10 +175,7 @@ func (comments CommentList) loadAssignees(ctx context.Context) error { assignees := make(map[int64]*user_model.User, len(assigneeIDs)) left := len(assigneeIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", assigneeIDs[:limit]). Rows(new(user_model.User)) @@ -256,10 +244,7 @@ func (comments CommentList) LoadIssues(ctx context.Context) error { issues := make(map[int64]*Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("id", issueIDs[:limit]). Rows(new(Issue)) @@ -313,10 +298,7 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { issues := make(map[int64]*Issue, len(issueIDs)) left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := e. In("id", issueIDs[:limit]). Rows(new(Issue)) @@ -392,10 +374,7 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { commentsIDs := comments.getAttachmentCommentIDs() left := len(commentsIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("comment_id", commentsIDs[:limit]). Rows(new(repo_model.Attachment)) diff --git a/models/issues/issue_label.go b/models/issues/issue_label.go index 10fc821454..f082079e07 100644 --- a/models/issues/issue_label.go +++ b/models/issues/issue_label.go @@ -206,6 +206,7 @@ func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *use } issue.Labels = nil + issue.isLabelsLoaded = false return issue.LoadLabels(ctx) } diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 6c74b533b3..26b93189b8 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -42,10 +42,7 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs)) left := len(repoIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", repoIDs[:limit]). Find(&repoMaps) @@ -116,10 +113,7 @@ func (issues IssueList) LoadLabels(ctx context.Context) error { issueIDs := issues.getIssueIDs() left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx).Table("label"). Join("LEFT", "issue_label", "issue_label.label_id = label.id"). In("issue_label.issue_id", issueIDs[:limit]). @@ -171,10 +165,7 @@ func (issues IssueList) LoadMilestones(ctx context.Context) error { milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) left := len(milestoneIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", milestoneIDs[:limit]). Find(&milestoneMaps) @@ -203,10 +194,7 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { } for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) projects := make([]*projectWithIssueID, 0, limit) err := db.GetEngine(ctx). @@ -245,10 +233,7 @@ func (issues IssueList) LoadAssignees(ctx context.Context) error { issueIDs := issues.getIssueIDs() left := len(issueIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx).Table("issue_assignees"). Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id"). In("`issue_assignees`.issue_id", issueIDs[:limit]).OrderBy(user_model.GetOrderByName()). @@ -306,10 +291,7 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error { pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs)) left := len(issuesIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("issue_id", issuesIDs[:limit]). Rows(new(PullRequest)) @@ -354,10 +336,7 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) { issuesIDs := issues.getIssueIDs() left := len(issuesIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx). In("issue_id", issuesIDs[:limit]). Rows(new(repo_model.Attachment)) @@ -399,10 +378,7 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er issuesIDs := issues.getIssueIDs() left := len(issuesIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) rows, err := db.GetEngine(ctx).Table("comment"). Join("INNER", "issue", "issue.id = comment.issue_id"). In("issue.id", issuesIDs[:limit]). @@ -466,10 +442,7 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { left := len(ids) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) // select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id rows, err := db.GetEngine(ctx).Table("tracked_time"). diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index f9e1fbeb14..79bd6a19b0 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -24,7 +24,7 @@ import ( const ScopeSortPrefix = "scope-" // IssuesOptions represents options of an issue. -type IssuesOptions struct { //nolint +type IssuesOptions struct { //nolint:revive // export stutter Paginator *db.ListOptions RepoIDs []int64 // overwrites RepoCond if the length is not 0 AllPublic bool // include also all public repositories @@ -73,8 +73,8 @@ func (o *IssuesOptions) Copy(edit ...func(options *IssuesOptions)) *IssuesOption // sortType string func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) { // Since this sortType is dynamically created, it has to be treated specially. - if strings.HasPrefix(sortType, ScopeSortPrefix) { - scope := strings.TrimPrefix(sortType, ScopeSortPrefix) + if after, ok := strings.CutPrefix(sortType, ScopeSortPrefix); ok { + scope := after sess.Join("LEFT", "issue_label", "issue.id = issue_label.issue_id") // "exclusive_order=0" means "no order is set", so exclude it from the JOIN criteria and then "LEFT JOIN" result is also null sess.Join("LEFT", "label", "label.id = issue_label.label_id AND label.exclusive_order <> 0 AND label.name LIKE ?", scope+"/%") @@ -88,6 +88,8 @@ func applySorts(sess *xorm.Session, sortType string, priorityRepoID int64) { sess.Asc("issue.created_unix").Asc("issue.id") case "recentupdate": sess.Desc("issue.updated_unix").Desc("issue.created_unix").Desc("issue.id") + case "recentclose": + sess.Desc("issue.closed_unix").Desc("issue.created_unix").Desc("issue.id") case "leastupdate": sess.Asc("issue.updated_unix").Asc("issue.created_unix").Asc("issue.id") case "mostcomment": diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index 50409fbbd8..adedaa3d3a 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -94,10 +94,7 @@ func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error // ids in a temporary table and join from them. accum := &IssueStats{} for i := 0; i < len(opts.IssueIDs); { - chunk := i + MaxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } + chunk := min(i+MaxQueryParameters, len(opts.IssueIDs)) stats, err := getIssueStatsChunk(ctx, opts, opts.IssueIDs[i:chunk]) if err != nil { return nil, err diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 18571e3aaa..1c5db55bbc 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -5,6 +5,7 @@ package issues_test import ( "fmt" + "slices" "sort" "sync" "testing" @@ -270,7 +271,7 @@ func TestIssue_ResolveMentions(t *testing.T) { for i, user := range resolved { ids[i] = user.ID } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + slices.Sort(ids) assert.Equal(t, expected, ids) } @@ -292,7 +293,7 @@ func TestResourceIndex(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) var wg sync.WaitGroup - for i := 0; i < 100; i++ { + for i := range 100 { wg.Add(1) go func(i int) { testInsertIssue(t, fmt.Sprintf("issue %d", i+1), "my issue", 0) @@ -314,7 +315,7 @@ func TestCorrectIssueStats(t *testing.T) { issueAmount := issues_model.MaxQueryParameters + 10 var wg sync.WaitGroup - for i := 0; i < issueAmount; i++ { + for i := range issueAmount { wg.Add(1) go func(i int) { testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0) diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 7ddf7ee901..9b99787e3b 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -12,9 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" - system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -306,7 +304,7 @@ func UpdateIssueAttachments(ctx context.Context, issueID int64, uuids []string) if err != nil { return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err) } - for i := 0; i < len(attachments); i++ { + for i := range attachments { attachments[i].IssueID = issueID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) @@ -715,138 +713,13 @@ func UpdateReactionsMigrationsByType(ctx context.Context, gitServiceType api.Git return err } -// DeleteIssuesByRepoID deletes issues by repositories id -func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { - // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289 - // so here it uses "DELETE ... WHERE IN" with pre-queried IDs. - sess := db.GetEngine(ctx) - - for { - issueIDs := make([]int64, 0, db.DefaultMaxInSize) - - err := sess.Table(&Issue{}).Where("repo_id = ?", repoID).OrderBy("id").Limit(db.DefaultMaxInSize).Cols("id").Find(&issueIDs) - if err != nil { - return nil, err - } - - if len(issueIDs) == 0 { - break - } - - // Delete content histories - _, err = sess.In("issue_id", issueIDs).Delete(&ContentHistory{}) - if err != nil { - return nil, err - } - - // Delete comments and attachments - _, err = sess.In("issue_id", issueIDs).Delete(&Comment{}) - if err != nil { - return nil, err - } - - // Dependencies for issues in this repository - _, err = sess.In("issue_id", issueIDs).Delete(&IssueDependency{}) - if err != nil { - return nil, err - } - - // Delete dependencies for issues in other repositories - _, err = sess.In("dependency_id", issueIDs).Delete(&IssueDependency{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&IssueUser{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&Reaction{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&IssueWatch{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&Stopwatch{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&TrackedTime{}) - if err != nil { - return nil, err - } - - _, err = sess.In("issue_id", issueIDs).Delete(&project_model.ProjectIssue{}) - if err != nil { - return nil, err - } - - _, err = sess.In("dependent_issue_id", issueIDs).Delete(&Comment{}) - if err != nil { - return nil, err - } - - var attachments []*repo_model.Attachment - err = sess.In("issue_id", issueIDs).Find(&attachments) - if err != nil { - return nil, err - } - - for j := range attachments { - attachmentPaths = append(attachmentPaths, attachments[j].RelativePath()) - } - - _, err = sess.In("issue_id", issueIDs).Delete(&repo_model.Attachment{}) - if err != nil { - return nil, err - } - - _, err = sess.In("id", issueIDs).Delete(&Issue{}) - if err != nil { - return nil, err - } - } - - return attachmentPaths, err -} - -// DeleteOrphanedIssues delete issues without a repo -func DeleteOrphanedIssues(ctx context.Context) error { - var attachmentPaths []string - err := db.WithTx(ctx, func(ctx context.Context) error { - var ids []int64 - - if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id"). - Join("LEFT", "repository", "issue.repo_id=repository.id"). - Where(builder.IsNull{"repository.id"}).GroupBy("issue.repo_id"). - Find(&ids); err != nil { - return err - } - - for i := range ids { - paths, err := DeleteIssuesByRepoID(ctx, ids[i]) - if err != nil { - return err - } - attachmentPaths = append(attachmentPaths, paths...) - } - - return nil - }) - if err != nil { - return err - } - - // Remove issue attachment files. - for i := range attachmentPaths { - // FIXME: it's not right, because the attachment might not be on local filesystem - system_model.RemoveAllWithNotice(ctx, "Delete issue attachment", attachmentPaths[i]) +func GetOrphanedIssueRepoIDs(ctx context.Context) ([]int64, error) { + var repoIDs []int64 + if err := db.GetEngine(ctx).Table("issue").Distinct("issue.repo_id"). + Join("LEFT", "repository", "issue.repo_id=repository.id"). + Where(builder.IsNull{"repository.id"}). + Find(&repoIDs); err != nil { + return nil, err } - return nil + return repoIDs, nil } diff --git a/models/issues/pull.go b/models/issues/pull.go index e65b214dab..0ff32e2473 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -649,12 +649,6 @@ func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*P return pulls, err } -// Update updates all fields of pull request. -func (pr *PullRequest) Update(ctx context.Context) error { - _, err := db.GetEngine(ctx).ID(pr.ID).AllCols().Update(pr) - return err -} - // UpdateCols updates specific fields of pull request. func (pr *PullRequest) UpdateCols(ctx context.Context, cols ...string) error { _, err := db.GetEngine(ctx).ID(pr.ID).Cols(cols...).Update(pr) diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index b685175f8e..84f9f6166d 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -152,7 +152,8 @@ func PullRequests(ctx context.Context, baseRepoID int64, opts *PullRequestsOptio applySorts(findSession, opts.SortType, 0) findSession = db.SetSessionPagination(findSession, opts) prs := make([]*PullRequest, 0, opts.PageSize) - return prs, maxResults, findSession.Find(&prs) + found := findSession.Find(&prs) + return prs, maxResults, found } // PullRequestList defines a list of pull requests diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 8e09030215..39efaa5792 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestPullRequest_LoadAttributes(t *testing.T) { @@ -76,6 +77,47 @@ func TestPullRequestsNewest(t *testing.T) { } } +func TestPullRequests_Closed_RecentSortType(t *testing.T) { + // Issue ID | Closed At. | Updated At + // 2 | 1707270001 | 1707270001 + // 3 | 1707271000 | 1707279999 + // 11 | 1707279999 | 1707275555 + tests := []struct { + sortType string + expectedIssueIDOrder []int64 + }{ + {"recentupdate", []int64{3, 11, 2}}, + {"recentclose", []int64{11, 3, 2}}, + } + + assert.NoError(t, unittest.PrepareTestDatabase()) + _, err := db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2") + require.NoError(t, err) + _, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3") + require.NoError(t, err) + _, err = db.Exec(db.DefaultContext, "UPDATE issue SET closed_unix = 1707279999, updated_unix = 1707275555, is_closed = true WHERE id = 11") + require.NoError(t, err) + + for _, test := range tests { + t.Run(test.sortType, func(t *testing.T) { + prs, _, err := issues_model.PullRequests(db.DefaultContext, 1, &issues_model.PullRequestsOptions{ + ListOptions: db.ListOptions{ + Page: 1, + }, + State: "closed", + SortType: test.sortType, + }) + require.NoError(t, err) + + if assert.Len(t, prs, len(test.expectedIssueIDOrder)) { + for i := range test.expectedIssueIDOrder { + assert.Equal(t, test.expectedIssueIDOrder[i], prs[i].IssueID) + } + } + }) + } +} + func TestLoadRequestedReviewers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) @@ -206,19 +248,6 @@ func TestGetPullRequestByIssueID(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestPullRequest_Update(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) - pr.BaseBranch = "baseBranch" - pr.HeadBranch = "headBranch" - pr.Update(db.DefaultContext) - - pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) - assert.Equal(t, "baseBranch", pr.BaseBranch) - assert.Equal(t, "headBranch", pr.HeadBranch) - unittest.CheckConsistencyFor(t, pr) -} - func TestPullRequest_UpdateCols(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) pr := &issues_model.PullRequest{ diff --git a/models/issues/review_list.go b/models/issues/review_list.go index 928f24fb2d..bbb8c489fa 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -22,7 +22,7 @@ type ReviewList []*Review // LoadReviewers loads reviewers func (reviews ReviewList) LoadReviewers(ctx context.Context) error { reviewerIDs := make([]int64, len(reviews)) - for i := 0; i < len(reviews); i++ { + for i := range reviews { reviewerIDs[i] = reviews[i].ReviewerID } reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs) diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go index 7c05a3a883..761b8f91a0 100644 --- a/models/issues/stopwatch.go +++ b/models/issues/stopwatch.go @@ -5,7 +5,6 @@ package issues import ( "context" - "fmt" "time" "code.gitea.io/gitea/models/db" @@ -15,20 +14,6 @@ import ( "code.gitea.io/gitea/modules/util" ) -// ErrIssueStopwatchNotExist represents an error that stopwatch is not exist -type ErrIssueStopwatchNotExist struct { - UserID int64 - IssueID int64 -} - -func (err ErrIssueStopwatchNotExist) Error() string { - return fmt.Sprintf("issue stopwatch doesn't exist[uid: %d, issue_id: %d", err.UserID, err.IssueID) -} - -func (err ErrIssueStopwatchNotExist) Unwrap() error { - return util.ErrNotExist -} - // Stopwatch represents a stopwatch for time tracking. type Stopwatch struct { ID int64 `xorm:"pk autoincr"` @@ -55,13 +40,11 @@ func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, ex return sw, exists, err } -// UserIDCount is a simple coalition of UserID and Count type UserStopwatch struct { UserID int64 StopWatches []*Stopwatch } -// GetUIDsAndNotificationCounts between the two provided times func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) { sws := []*Stopwatch{} if err := db.GetEngine(ctx).Where("issue_id != 0").Find(&sws); err != nil { @@ -87,7 +70,7 @@ func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) { return res, nil } -// GetUserStopwatches return list of all stopwatches of a user +// GetUserStopwatches return list of the user's all stopwatches func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) { sws := make([]*Stopwatch, 0, 8) sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID) @@ -102,7 +85,7 @@ func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOp return sws, nil } -// CountUserStopwatches return count of all stopwatches of a user +// CountUserStopwatches return count of the user's all stopwatches func CountUserStopwatches(ctx context.Context, userID int64) (int64, error) { return db.GetEngine(ctx).Where("user_id = ?", userID).Count(&Stopwatch{}) } @@ -136,43 +119,21 @@ func HasUserStopwatch(ctx context.Context, userID int64) (exists bool, sw *Stopw return exists, sw, issue, err } -// FinishIssueStopwatchIfPossible if stopwatch exist then finish it otherwise ignore -func FinishIssueStopwatchIfPossible(ctx context.Context, user *user_model.User, issue *Issue) error { - _, exists, err := getStopwatch(ctx, user.ID, issue.ID) - if err != nil { - return err - } - if !exists { - return nil - } - return FinishIssueStopwatch(ctx, user, issue) -} - -// CreateOrStopIssueStopwatch create an issue stopwatch if it's not exist, otherwise finish it -func CreateOrStopIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - _, exists, err := getStopwatch(ctx, user.ID, issue.ID) - if err != nil { - return err - } - if exists { - return FinishIssueStopwatch(ctx, user, issue) - } - return CreateIssueStopwatch(ctx, user, issue) -} - -// FinishIssueStopwatch if stopwatch exist then finish it otherwise return an error -func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { +// FinishIssueStopwatch if stopwatch exists, then finish it. +func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) { sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) if err != nil { - return err + return false, err + } else if !exists { + return false, nil } - if !exists { - return ErrIssueStopwatchNotExist{ - UserID: user.ID, - IssueID: issue.ID, - } + if err = finishIssueStopwatch(ctx, user, issue, sw); err != nil { + return false, err } + return true, nil +} +func finishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue, sw *Stopwatch) error { // Create tracked time out of the time difference between start date and actual date timediff := time.Now().Unix() - int64(sw.CreatedUnix) @@ -184,14 +145,12 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss Time: timediff, } - if err := db.Insert(ctx, tt); err != nil { + if err := issue.LoadRepo(ctx); err != nil { return err } - - if err := issue.LoadRepo(ctx); err != nil { + if err := db.Insert(ctx, tt); err != nil { return err } - if _, err := CreateComment(ctx, &CreateCommentOptions{ Doer: user, Issue: issue, @@ -202,83 +161,65 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss }); err != nil { return err } - _, err = db.DeleteByBean(ctx, sw) + _, err := db.DeleteByBean(ctx, sw) return err } -// CreateIssueStopwatch creates a stopwatch if not exist, otherwise return an error -func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - // if another stopwatch is running: stop it - exists, _, otherIssue, err := HasUserStopwatch(ctx, user.ID) - if err != nil { - return err - } - if exists { - if err := FinishIssueStopwatch(ctx, user, otherIssue); err != nil { - return err +// CreateIssueStopwatch creates a stopwatch if the issue doesn't have the user's stopwatch. +// It also stops any other stopwatch that might be running for the user. +func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) { + { // if another issue's stopwatch is running: stop it; if this issue has a stopwatch: return an error. + exists, otherStopWatch, otherIssue, err := HasUserStopwatch(ctx, user.ID) + if err != nil { + return false, err + } + if exists { + if otherStopWatch.IssueID == issue.ID { + // don't allow starting stopwatch for the same issue + return false, nil + } + // stop the other issue's stopwatch + if err = finishIssueStopwatch(ctx, user, otherIssue, otherStopWatch); err != nil { + return false, err + } } } - // Create stopwatch - sw := &Stopwatch{ - UserID: user.ID, - IssueID: issue.ID, + if err = issue.LoadRepo(ctx); err != nil { + return false, err } - - if err := db.Insert(ctx, sw); err != nil { - return err + if err = db.Insert(ctx, &Stopwatch{UserID: user.ID, IssueID: issue.ID}); err != nil { + return false, err } - - if err := issue.LoadRepo(ctx); err != nil { - return err - } - - if _, err := CreateComment(ctx, &CreateCommentOptions{ + if _, err = CreateComment(ctx, &CreateCommentOptions{ Doer: user, Issue: issue, Repo: issue.Repo, Type: CommentTypeStartTracking, }); err != nil { - return err + return false, err } - - return nil + return true, nil } // CancelStopwatch removes the given stopwatch and logs it into issue's timeline. -func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - if err := cancelStopwatch(ctx, user, issue); err != nil { - return err - } - return committer.Commit() -} - -func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error { - e := db.GetEngine(ctx) - sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) - if err != nil { - return err - } - - if exists { - if _, err := e.Delete(sw); err != nil { +func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) { + err = db.WithTx(ctx, func(ctx context.Context) error { + e := db.GetEngine(ctx) + sw, exists, err := getStopwatch(ctx, user.ID, issue.ID) + if err != nil { return err + } else if !exists { + return nil } - if err := issue.LoadRepo(ctx); err != nil { + if err = issue.LoadRepo(ctx); err != nil { return err } - - if _, err := CreateComment(ctx, &CreateCommentOptions{ + if _, err = e.Delete(sw); err != nil { + return err + } + if _, err = CreateComment(ctx, &CreateCommentOptions{ Doer: user, Issue: issue, Repo: issue.Repo, @@ -286,6 +227,8 @@ func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) e }); err != nil { return err } - } - return nil + ok = true + return nil + }) + return ok, err } diff --git a/models/issues/stopwatch_test.go b/models/issues/stopwatch_test.go index a1bf9dc931..6333c10234 100644 --- a/models/issues/stopwatch_test.go +++ b/models/issues/stopwatch_test.go @@ -10,7 +10,6 @@ import ( issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" ) @@ -18,26 +17,22 @@ import ( func TestCancelStopwatch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - user1, err := user_model.GetUserByID(db.DefaultContext, 1) - assert.NoError(t, err) - - issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1) - assert.NoError(t, err) - issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2) - assert.NoError(t, err) + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - err = issues_model.CancelStopwatch(db.DefaultContext, user1, issue1) + ok, err := issues_model.CancelStopwatch(db.DefaultContext, user1, issue1) assert.NoError(t, err) + assert.True(t, ok) unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user1.ID, IssueID: issue1.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID}) - _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID}) - - assert.NoError(t, issues_model.CancelStopwatch(db.DefaultContext, user1, issue2)) + ok, err = issues_model.CancelStopwatch(db.DefaultContext, user1, issue1) + assert.NoError(t, err) + assert.False(t, ok) } func TestStopwatchExists(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - assert.True(t, issues_model.StopwatchExists(db.DefaultContext, 1, 1)) assert.False(t, issues_model.StopwatchExists(db.DefaultContext, 1, 2)) } @@ -58,21 +53,35 @@ func TestHasUserStopwatch(t *testing.T) { func TestCreateOrStopIssueStopwatch(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - user2, err := user_model.GetUserByID(db.DefaultContext, 2) + user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + issue1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + issue3 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}) + + // create a new stopwatch + ok, err := issues_model.CreateIssueStopwatch(db.DefaultContext, user4, issue1) assert.NoError(t, err) - org3, err := user_model.GetUserByID(db.DefaultContext, 3) + assert.True(t, ok) + unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: user4.ID, IssueID: issue1.ID}) + // should not create a second stopwatch for the same issue + ok, err = issues_model.CreateIssueStopwatch(db.DefaultContext, user4, issue1) assert.NoError(t, err) - - issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1) + assert.False(t, ok) + // on a different issue, it will finish the existing stopwatch and create a new one + ok, err = issues_model.CreateIssueStopwatch(db.DefaultContext, user4, issue3) assert.NoError(t, err) - issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2) + assert.True(t, ok) + unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user4.ID, IssueID: issue1.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: user4.ID, IssueID: issue3.ID}) + + // user2 already has a stopwatch in test fixture + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + issue2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) + ok, err = issues_model.FinishIssueStopwatch(db.DefaultContext, user2, issue2) assert.NoError(t, err) - - assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(db.DefaultContext, org3, issue1)) - sw := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: 3, IssueID: 1}) - assert.LessOrEqual(t, sw.CreatedUnix, timeutil.TimeStampNow()) - - assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(db.DefaultContext, user2, issue2)) - unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: 2, IssueID: 2}) - unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 2, IssueID: 2}) + assert.True(t, ok) + unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user2.ID, IssueID: issue2.ID}) + unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: user2.ID, IssueID: issue2.ID}) + ok, err = issues_model.FinishIssueStopwatch(db.DefaultContext, user2, issue2) + assert.NoError(t, err) + assert.False(t, ok) } diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index ea404d36cd..2afbe272ed 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -350,10 +350,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed // we get the statistics in smaller chunks and get accumulates var accum int64 for i := 0; i < len(opts.IssueIDs); { - chunk := i + MaxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } + chunk := min(i+MaxQueryParameters, len(opts.IssueIDs)) time, err := getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs[i:chunk]) if err != nil { return 0, err diff --git a/models/migrations/base/db.go b/models/migrations/base/db.go index 4ecc930f10..479a46379c 100644 --- a/models/migrations/base/db.go +++ b/models/migrations/base/db.go @@ -518,7 +518,7 @@ func ModifyColumn(x *xorm.Engine, tableName string, col *schemas.Column) error { func removeAllWithRetry(dir string) error { var err error - for i := 0; i < 20; i++ { + for range 20 { err = os.RemoveAll(dir) if err == nil { break diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 7da426fef0..33fd1df707 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -1,7 +1,6 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//nolint:forbidigo package base import ( @@ -106,7 +105,7 @@ func MainTest(m *testing.M) { giteaConf := os.Getenv("GITEA_CONF") if giteaConf == "" { giteaConf = filepath.Join(filepath.Dir(setting.AppPath), "tests/sqlite.ini") - fmt.Printf("Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) + _, _ = fmt.Fprintf(os.Stderr, "Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) } if !filepath.IsAbs(giteaConf) { @@ -134,7 +133,7 @@ func MainTest(m *testing.M) { exitStatus := m.Run() if err := removeAllWithRetry(setting.RepoRootPath); err != nil { - fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) } os.Exit(exitStatus) } diff --git a/models/migrations/v1_10/v100.go b/models/migrations/v1_10/v100.go index 5d2fd8e244..1742bea296 100644 --- a/models/migrations/v1_10/v100.go +++ b/models/migrations/v1_10/v100.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "net/url" diff --git a/models/migrations/v1_10/v101.go b/models/migrations/v1_10/v101.go index f023a2a0e7..6c8dfe2486 100644 --- a/models/migrations/v1_10/v101.go +++ b/models/migrations/v1_10/v101.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_10/v88.go b/models/migrations/v1_10/v88.go index 7e86ac364f..eb8e81c19e 100644 --- a/models/migrations/v1_10/v88.go +++ b/models/migrations/v1_10/v88.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "crypto/sha1" diff --git a/models/migrations/v1_10/v89.go b/models/migrations/v1_10/v89.go index d5f27ffdc6..0df2a6e17b 100644 --- a/models/migrations/v1_10/v89.go +++ b/models/migrations/v1_10/v89.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v90.go b/models/migrations/v1_10/v90.go index 295d4b1c1b..5521a97e32 100644 --- a/models/migrations/v1_10/v90.go +++ b/models/migrations/v1_10/v90.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v91.go b/models/migrations/v1_10/v91.go index 48cac2de70..08db6c2742 100644 --- a/models/migrations/v1_10/v91.go +++ b/models/migrations/v1_10/v91.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v92.go b/models/migrations/v1_10/v92.go index 9080108594..b6c04a9234 100644 --- a/models/migrations/v1_10/v92.go +++ b/models/migrations/v1_10/v92.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "xorm.io/builder" diff --git a/models/migrations/v1_10/v93.go b/models/migrations/v1_10/v93.go index ee59a8db39..c131be9a8d 100644 --- a/models/migrations/v1_10/v93.go +++ b/models/migrations/v1_10/v93.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v94.go b/models/migrations/v1_10/v94.go index c131af162b..13b7d7b303 100644 --- a/models/migrations/v1_10/v94.go +++ b/models/migrations/v1_10/v94.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v95.go b/models/migrations/v1_10/v95.go index 3b1f67fd9c..86b52026bf 100644 --- a/models/migrations/v1_10/v95.go +++ b/models/migrations/v1_10/v95.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v96.go b/models/migrations/v1_10/v96.go index 34c8240031..ca35a169c4 100644 --- a/models/migrations/v1_10/v96.go +++ b/models/migrations/v1_10/v96.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "path/filepath" diff --git a/models/migrations/v1_10/v97.go b/models/migrations/v1_10/v97.go index dee45b32e3..5872bb63e5 100644 --- a/models/migrations/v1_10/v97.go +++ b/models/migrations/v1_10/v97.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v98.go b/models/migrations/v1_10/v98.go index bdd9aed089..d21c326459 100644 --- a/models/migrations/v1_10/v98.go +++ b/models/migrations/v1_10/v98.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import "xorm.io/xorm" diff --git a/models/migrations/v1_10/v99.go b/models/migrations/v1_10/v99.go index ebe6597f7c..223c188057 100644 --- a/models/migrations/v1_10/v99.go +++ b/models/migrations/v1_10/v99.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_10 //nolint +package v1_10 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_11/v102.go b/models/migrations/v1_11/v102.go index 9358e4cef3..e52290afb0 100644 --- a/models/migrations/v1_11/v102.go +++ b/models/migrations/v1_11/v102.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_11/v103.go b/models/migrations/v1_11/v103.go index 53527dac58..a515710160 100644 --- a/models/migrations/v1_11/v103.go +++ b/models/migrations/v1_11/v103.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v104.go b/models/migrations/v1_11/v104.go index 3e8ee64bc1..3b0d3c64b2 100644 --- a/models/migrations/v1_11/v104.go +++ b/models/migrations/v1_11/v104.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_11/v105.go b/models/migrations/v1_11/v105.go index b91340c30a..d86973a0f6 100644 --- a/models/migrations/v1_11/v105.go +++ b/models/migrations/v1_11/v105.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v106.go b/models/migrations/v1_11/v106.go index ecb11cdd1e..edffe18683 100644 --- a/models/migrations/v1_11/v106.go +++ b/models/migrations/v1_11/v106.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v107.go b/models/migrations/v1_11/v107.go index f0bfe5862c..a158e3bb50 100644 --- a/models/migrations/v1_11/v107.go +++ b/models/migrations/v1_11/v107.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v108.go b/models/migrations/v1_11/v108.go index a85096234d..8f14504ceb 100644 --- a/models/migrations/v1_11/v108.go +++ b/models/migrations/v1_11/v108.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v109.go b/models/migrations/v1_11/v109.go index ea565ccda3..f7616aec7b 100644 --- a/models/migrations/v1_11/v109.go +++ b/models/migrations/v1_11/v109.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v110.go b/models/migrations/v1_11/v110.go index 81afa1331d..512f728c03 100644 --- a/models/migrations/v1_11/v110.go +++ b/models/migrations/v1_11/v110.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_11/v111.go b/models/migrations/v1_11/v111.go index ff108479a9..c27465f051 100644 --- a/models/migrations/v1_11/v111.go +++ b/models/migrations/v1_11/v111.go @@ -1,10 +1,11 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "fmt" + "slices" "xorm.io/xorm" ) @@ -344,10 +345,8 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { } return AccessModeWrite <= perm.UnitsMode[UnitTypeCode], nil } - for _, id := range protectedBranch.ApprovalsWhitelistUserIDs { - if id == reviewer.ID { - return true, nil - } + if slices.Contains(protectedBranch.ApprovalsWhitelistUserIDs, reviewer.ID) { + return true, nil } // isUserInTeams diff --git a/models/migrations/v1_11/v112.go b/models/migrations/v1_11/v112.go index 0857663119..fe45cf9222 100644 --- a/models/migrations/v1_11/v112.go +++ b/models/migrations/v1_11/v112.go @@ -1,12 +1,12 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( - "fmt" "path/filepath" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -31,7 +31,7 @@ func RemoveAttachmentMissedRepo(x *xorm.Engine) error { for i := 0; i < len(attachments); i++ { uuid := attachments[i].UUID if err = util.RemoveAll(filepath.Join(setting.Attachment.Storage.Path, uuid[0:1], uuid[1:2], uuid)); err != nil { - fmt.Printf("Error: %v", err) //nolint:forbidigo + log.Warn("Unable to remove attachment file by UUID %s: %v", uuid, err) } } diff --git a/models/migrations/v1_11/v113.go b/models/migrations/v1_11/v113.go index dea344a44f..a4d54f66fb 100644 --- a/models/migrations/v1_11/v113.go +++ b/models/migrations/v1_11/v113.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "fmt" diff --git a/models/migrations/v1_11/v114.go b/models/migrations/v1_11/v114.go index 95adcee989..9467a8a90c 100644 --- a/models/migrations/v1_11/v114.go +++ b/models/migrations/v1_11/v114.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "net/url" diff --git a/models/migrations/v1_11/v115.go b/models/migrations/v1_11/v115.go index 8c631cfd0b..5933c0520f 100644 --- a/models/migrations/v1_11/v115.go +++ b/models/migrations/v1_11/v115.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "crypto/md5" @@ -146,7 +146,7 @@ func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) return "", fmt.Errorf("io.ReadAll: %w", err) } - newAvatar := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", userID, md5.Sum(data))))) + newAvatar := fmt.Sprintf("%x", md5.Sum(fmt.Appendf(nil, "%d-%x", userID, md5.Sum(data)))) if newAvatar == oldAvatar { return newAvatar, nil } diff --git a/models/migrations/v1_11/v116.go b/models/migrations/v1_11/v116.go index 85aa76c1e0..729fbad18b 100644 --- a/models/migrations/v1_11/v116.go +++ b/models/migrations/v1_11/v116.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_11 //nolint +package v1_11 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v117.go b/models/migrations/v1_12/v117.go index 8eadcdef2b..73b58ca34b 100644 --- a/models/migrations/v1_12/v117.go +++ b/models/migrations/v1_12/v117.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v118.go b/models/migrations/v1_12/v118.go index eb022dc5e4..e8b4249743 100644 --- a/models/migrations/v1_12/v118.go +++ b/models/migrations/v1_12/v118.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v119.go b/models/migrations/v1_12/v119.go index 60bfe6a57d..b4bf29a935 100644 --- a/models/migrations/v1_12/v119.go +++ b/models/migrations/v1_12/v119.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v120.go b/models/migrations/v1_12/v120.go index 3f7ed8d373..14d515f5a7 100644 --- a/models/migrations/v1_12/v120.go +++ b/models/migrations/v1_12/v120.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v121.go b/models/migrations/v1_12/v121.go index 175ec9164d..a28ae4e1c9 100644 --- a/models/migrations/v1_12/v121.go +++ b/models/migrations/v1_12/v121.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import "xorm.io/xorm" diff --git a/models/migrations/v1_12/v122.go b/models/migrations/v1_12/v122.go index 6e31d863a1..bc1b175f6a 100644 --- a/models/migrations/v1_12/v122.go +++ b/models/migrations/v1_12/v122.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v123.go b/models/migrations/v1_12/v123.go index b0c3af07a3..52b10bb850 100644 --- a/models/migrations/v1_12/v123.go +++ b/models/migrations/v1_12/v123.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v124.go b/models/migrations/v1_12/v124.go index d2ba03ffe0..9a93f436d4 100644 --- a/models/migrations/v1_12/v124.go +++ b/models/migrations/v1_12/v124.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v125.go b/models/migrations/v1_12/v125.go index ec4ffaab25..7f582ecff5 100644 --- a/models/migrations/v1_12/v125.go +++ b/models/migrations/v1_12/v125.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v126.go b/models/migrations/v1_12/v126.go index ca9ec3aa3f..64fd7f7478 100644 --- a/models/migrations/v1_12/v126.go +++ b/models/migrations/v1_12/v126.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/builder" diff --git a/models/migrations/v1_12/v127.go b/models/migrations/v1_12/v127.go index 00e391dc87..9bd78db95e 100644 --- a/models/migrations/v1_12/v127.go +++ b/models/migrations/v1_12/v127.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v128.go b/models/migrations/v1_12/v128.go index cba64711d0..e7dbff3766 100644 --- a/models/migrations/v1_12/v128.go +++ b/models/migrations/v1_12/v128.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v129.go b/models/migrations/v1_12/v129.go index cf228242b9..3e4d3aca68 100644 --- a/models/migrations/v1_12/v129.go +++ b/models/migrations/v1_12/v129.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v130.go b/models/migrations/v1_12/v130.go index 391810c7ca..107bb756fd 100644 --- a/models/migrations/v1_12/v130.go +++ b/models/migrations/v1_12/v130.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "code.gitea.io/gitea/modules/json" diff --git a/models/migrations/v1_12/v131.go b/models/migrations/v1_12/v131.go index 5184bc3590..1266c2f185 100644 --- a/models/migrations/v1_12/v131.go +++ b/models/migrations/v1_12/v131.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v132.go b/models/migrations/v1_12/v132.go index 3b2b28f7ab..8b1ae6db93 100644 --- a/models/migrations/v1_12/v132.go +++ b/models/migrations/v1_12/v132.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v133.go b/models/migrations/v1_12/v133.go index c9087fc8c1..69e20597d8 100644 --- a/models/migrations/v1_12/v133.go +++ b/models/migrations/v1_12/v133.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import "xorm.io/xorm" diff --git a/models/migrations/v1_12/v134.go b/models/migrations/v1_12/v134.go index a918d38757..09d743964d 100644 --- a/models/migrations/v1_12/v134.go +++ b/models/migrations/v1_12/v134.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v135.go b/models/migrations/v1_12/v135.go index 8898011df5..5df0ad7fc4 100644 --- a/models/migrations/v1_12/v135.go +++ b/models/migrations/v1_12/v135.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v136.go b/models/migrations/v1_12/v136.go index d91ff92feb..0f53278b46 100644 --- a/models/migrations/v1_12/v136.go +++ b/models/migrations/v1_12/v136.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v137.go b/models/migrations/v1_12/v137.go index 0d86b72010..9d38483488 100644 --- a/models/migrations/v1_12/v137.go +++ b/models/migrations/v1_12/v137.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_12/v138.go b/models/migrations/v1_12/v138.go index 8c8d353f40..4485adeb2d 100644 --- a/models/migrations/v1_12/v138.go +++ b/models/migrations/v1_12/v138.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "fmt" diff --git a/models/migrations/v1_12/v139.go b/models/migrations/v1_12/v139.go index 279aa7df87..a3799841ac 100644 --- a/models/migrations/v1_12/v139.go +++ b/models/migrations/v1_12/v139.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_12 //nolint +package v1_12 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_13/v140.go b/models/migrations/v1_13/v140.go index f3719e16f6..a9a047bca9 100644 --- a/models/migrations/v1_13/v140.go +++ b/models/migrations/v1_13/v140.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "fmt" @@ -21,12 +21,7 @@ func FixLanguageStatsToSaveSize(x *xorm.Engine) error { // RepoIndexerType specifies the repository indexer type type RepoIndexerType int - const ( - // RepoIndexerTypeCode code indexer - 0 - RepoIndexerTypeCode RepoIndexerType = iota //nolint:unused - // RepoIndexerTypeStats repository stats indexer - 1 - RepoIndexerTypeStats - ) + const RepoIndexerTypeStats RepoIndexerType = 1 // RepoIndexerStatus see models/repo_indexer.go type RepoIndexerStatus struct { diff --git a/models/migrations/v1_13/v141.go b/models/migrations/v1_13/v141.go index ae211e0e44..b54bc1727c 100644 --- a/models/migrations/v1_13/v141.go +++ b/models/migrations/v1_13/v141.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "fmt" diff --git a/models/migrations/v1_13/v142.go b/models/migrations/v1_13/v142.go index 7c7c01ad47..d08a0ae0bf 100644 --- a/models/migrations/v1_13/v142.go +++ b/models/migrations/v1_13/v142.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_13/v143.go b/models/migrations/v1_13/v143.go index 885768dff3..b9a856ed0f 100644 --- a/models/migrations/v1_13/v143.go +++ b/models/migrations/v1_13/v143.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_13/v144.go b/models/migrations/v1_13/v144.go index f5a0bc5751..9352d78bc8 100644 --- a/models/migrations/v1_13/v144.go +++ b/models/migrations/v1_13/v144.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_13/v145.go b/models/migrations/v1_13/v145.go index bb1f40baa7..86ebb4f9d9 100644 --- a/models/migrations/v1_13/v145.go +++ b/models/migrations/v1_13/v145.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "fmt" diff --git a/models/migrations/v1_13/v146.go b/models/migrations/v1_13/v146.go index 7d9a878704..355c772c26 100644 --- a/models/migrations/v1_13/v146.go +++ b/models/migrations/v1_13/v146.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_13/v147.go b/models/migrations/v1_13/v147.go index 510ef39b28..0059c06220 100644 --- a/models/migrations/v1_13/v147.go +++ b/models/migrations/v1_13/v147.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_13/v148.go b/models/migrations/v1_13/v148.go index 7bb8ab700b..d276db3d61 100644 --- a/models/migrations/v1_13/v148.go +++ b/models/migrations/v1_13/v148.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_13/v149.go b/models/migrations/v1_13/v149.go index 2a1db04cbb..a96b8e5ac7 100644 --- a/models/migrations/v1_13/v149.go +++ b/models/migrations/v1_13/v149.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "fmt" diff --git a/models/migrations/v1_13/v150.go b/models/migrations/v1_13/v150.go index d5ba489566..590ea72903 100644 --- a/models/migrations/v1_13/v150.go +++ b/models/migrations/v1_13/v150.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_13/v151.go b/models/migrations/v1_13/v151.go index 1865d58f04..454929534f 100644 --- a/models/migrations/v1_13/v151.go +++ b/models/migrations/v1_13/v151.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "context" diff --git a/models/migrations/v1_13/v152.go b/models/migrations/v1_13/v152.go index 502c82a40d..648e26446f 100644 --- a/models/migrations/v1_13/v152.go +++ b/models/migrations/v1_13/v152.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import "xorm.io/xorm" diff --git a/models/migrations/v1_13/v153.go b/models/migrations/v1_13/v153.go index 0b2dd3eb62..e5462fc162 100644 --- a/models/migrations/v1_13/v153.go +++ b/models/migrations/v1_13/v153.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_13/v154.go b/models/migrations/v1_13/v154.go index 60cc56713e..5477d1b889 100644 --- a/models/migrations/v1_13/v154.go +++ b/models/migrations/v1_13/v154.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_13 //nolint +package v1_13 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_14/main_test.go b/models/migrations/v1_14/main_test.go index 7a091b9b9a..978f88577c 100644 --- a/models/migrations/v1_14/main_test.go +++ b/models/migrations/v1_14/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "testing" diff --git a/models/migrations/v1_14/v155.go b/models/migrations/v1_14/v155.go index e814f59938..505a9ae033 100644 --- a/models/migrations/v1_14/v155.go +++ b/models/migrations/v1_14/v155.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v156.go b/models/migrations/v1_14/v156.go index 2cf4954a15..2fa5819610 100644 --- a/models/migrations/v1_14/v156.go +++ b/models/migrations/v1_14/v156.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v157.go b/models/migrations/v1_14/v157.go index 7187278d29..2c5625ebbd 100644 --- a/models/migrations/v1_14/v157.go +++ b/models/migrations/v1_14/v157.go @@ -1,24 +1,13 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "xorm.io/xorm" ) func FixRepoTopics(x *xorm.Engine) error { - type Topic struct { //nolint:unused - ID int64 `xorm:"pk autoincr"` - Name string `xorm:"UNIQUE VARCHAR(25)"` - RepoCount int - } - - type RepoTopic struct { //nolint:unused - RepoID int64 `xorm:"pk"` - TopicID int64 `xorm:"pk"` - } - type Repository struct { ID int64 `xorm:"pk autoincr"` Topics []string `xorm:"TEXT JSON"` diff --git a/models/migrations/v1_14/v158.go b/models/migrations/v1_14/v158.go index a849ddf27e..3c57e8e3da 100644 --- a/models/migrations/v1_14/v158.go +++ b/models/migrations/v1_14/v158.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "errors" diff --git a/models/migrations/v1_14/v159.go b/models/migrations/v1_14/v159.go index 149ae0f6a8..e6f6f0f061 100644 --- a/models/migrations/v1_14/v159.go +++ b/models/migrations/v1_14/v159.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_14/v160.go b/models/migrations/v1_14/v160.go index 4dea91b514..73f3798954 100644 --- a/models/migrations/v1_14/v160.go +++ b/models/migrations/v1_14/v160.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_14/v161.go b/models/migrations/v1_14/v161.go index ac7e821a80..eb92dee77c 100644 --- a/models/migrations/v1_14/v161.go +++ b/models/migrations/v1_14/v161.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "context" diff --git a/models/migrations/v1_14/v162.go b/models/migrations/v1_14/v162.go index 2e4e0b8eb0..a0ddd36d55 100644 --- a/models/migrations/v1_14/v162.go +++ b/models/migrations/v1_14/v162.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_14/v163.go b/models/migrations/v1_14/v163.go index 0cd8ba68c8..84c35190b7 100644 --- a/models/migrations/v1_14/v163.go +++ b/models/migrations/v1_14/v163.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_14/v164.go b/models/migrations/v1_14/v164.go index 54f6951427..d2fd9b8464 100644 --- a/models/migrations/v1_14/v164.go +++ b/models/migrations/v1_14/v164.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v165.go b/models/migrations/v1_14/v165.go index 926350cdf7..6e1b34156b 100644 --- a/models/migrations/v1_14/v165.go +++ b/models/migrations/v1_14/v165.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/models/migrations/base" @@ -16,10 +16,7 @@ func ConvertHookTaskTypeToVarcharAndTrim(x *xorm.Engine) error { return nil } - type HookTask struct { //nolint:unused - Typ string `xorm:"VARCHAR(16) index"` - } - + // HookTask: Typ string `xorm:"VARCHAR(16) index"` if err := base.ModifyColumn(x, "hook_task", &schemas.Column{ Name: "typ", SQLType: schemas.SQLType{ @@ -42,10 +39,7 @@ func ConvertHookTaskTypeToVarcharAndTrim(x *xorm.Engine) error { return err } - type Webhook struct { //nolint:unused - Type string `xorm:"VARCHAR(16) index"` - } - + // Webhook: Type string `xorm:"VARCHAR(16) index"` if err := base.ModifyColumn(x, "webhook", &schemas.Column{ Name: "type", SQLType: schemas.SQLType{ diff --git a/models/migrations/v1_14/v166.go b/models/migrations/v1_14/v166.go index e5731582fd..4c106bd7da 100644 --- a/models/migrations/v1_14/v166.go +++ b/models/migrations/v1_14/v166.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "crypto/sha256" diff --git a/models/migrations/v1_14/v167.go b/models/migrations/v1_14/v167.go index 9d416f6a32..d77bbc401e 100644 --- a/models/migrations/v1_14/v167.go +++ b/models/migrations/v1_14/v167.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v168.go b/models/migrations/v1_14/v168.go index a30a8859f7..aa93eec19b 100644 --- a/models/migrations/v1_14/v168.go +++ b/models/migrations/v1_14/v168.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import "xorm.io/xorm" diff --git a/models/migrations/v1_14/v169.go b/models/migrations/v1_14/v169.go index 5b81bb58b1..4f9df0d96f 100644 --- a/models/migrations/v1_14/v169.go +++ b/models/migrations/v1_14/v169.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_14/v170.go b/models/migrations/v1_14/v170.go index 7b6498a3e9..a2ff4623e1 100644 --- a/models/migrations/v1_14/v170.go +++ b/models/migrations/v1_14/v170.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v171.go b/models/migrations/v1_14/v171.go index 51a35a02ad..7b200e960a 100644 --- a/models/migrations/v1_14/v171.go +++ b/models/migrations/v1_14/v171.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v172.go b/models/migrations/v1_14/v172.go index 0f9bef902a..bbd61d87b2 100644 --- a/models/migrations/v1_14/v172.go +++ b/models/migrations/v1_14/v172.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_14/v173.go b/models/migrations/v1_14/v173.go index 2d9eee9197..7752fbe966 100644 --- a/models/migrations/v1_14/v173.go +++ b/models/migrations/v1_14/v173.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v174.go b/models/migrations/v1_14/v174.go index c839e15db8..4049e43070 100644 --- a/models/migrations/v1_14/v174.go +++ b/models/migrations/v1_14/v174.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v175.go b/models/migrations/v1_14/v175.go index 70d72b2600..92ed130473 100644 --- a/models/migrations/v1_14/v175.go +++ b/models/migrations/v1_14/v175.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v176.go b/models/migrations/v1_14/v176.go index 1ed49f75fa..ef5dce9a02 100644 --- a/models/migrations/v1_14/v176.go +++ b/models/migrations/v1_14/v176.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_14/v176_test.go b/models/migrations/v1_14/v176_test.go index ea3e750d7f..5c1db4db71 100644 --- a/models/migrations/v1_14/v176_test.go +++ b/models/migrations/v1_14/v176_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "testing" diff --git a/models/migrations/v1_14/v177.go b/models/migrations/v1_14/v177.go index 6e1838f369..96676bf8d9 100644 --- a/models/migrations/v1_14/v177.go +++ b/models/migrations/v1_14/v177.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "fmt" diff --git a/models/migrations/v1_14/v177_test.go b/models/migrations/v1_14/v177_test.go index 5568a18fec..263f69f338 100644 --- a/models/migrations/v1_14/v177_test.go +++ b/models/migrations/v1_14/v177_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_14 //nolint +package v1_14 import ( "testing" diff --git a/models/migrations/v1_15/main_test.go b/models/migrations/v1_15/main_test.go index 366f19788e..d01585e997 100644 --- a/models/migrations/v1_15/main_test.go +++ b/models/migrations/v1_15/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "testing" diff --git a/models/migrations/v1_15/v178.go b/models/migrations/v1_15/v178.go index 6d236eb049..ca3a5c262e 100644 --- a/models/migrations/v1_15/v178.go +++ b/models/migrations/v1_15/v178.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_15/v179.go b/models/migrations/v1_15/v179.go index f6b142eb42..d6fb86ffec 100644 --- a/models/migrations/v1_15/v179.go +++ b/models/migrations/v1_15/v179.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_15/v180.go b/models/migrations/v1_15/v180.go index c71e771861..dd132f8330 100644 --- a/models/migrations/v1_15/v180.go +++ b/models/migrations/v1_15/v180.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "code.gitea.io/gitea/modules/json" diff --git a/models/migrations/v1_15/v181.go b/models/migrations/v1_15/v181.go index 2185ed0213..fb1d3d7a75 100644 --- a/models/migrations/v1_15/v181.go +++ b/models/migrations/v1_15/v181.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "strings" diff --git a/models/migrations/v1_15/v181_test.go b/models/migrations/v1_15/v181_test.go index 7295aa4180..73b5c1f3d6 100644 --- a/models/migrations/v1_15/v181_test.go +++ b/models/migrations/v1_15/v181_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "strings" diff --git a/models/migrations/v1_15/v182.go b/models/migrations/v1_15/v182.go index 9ca500c0f9..f53ff11df9 100644 --- a/models/migrations/v1_15/v182.go +++ b/models/migrations/v1_15/v182.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_15/v182_test.go b/models/migrations/v1_15/v182_test.go index 75ef8e1cd8..5fc6a0c467 100644 --- a/models/migrations/v1_15/v182_test.go +++ b/models/migrations/v1_15/v182_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "testing" diff --git a/models/migrations/v1_15/v183.go b/models/migrations/v1_15/v183.go index effad1b467..5d0582f03d 100644 --- a/models/migrations/v1_15/v183.go +++ b/models/migrations/v1_15/v183.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "fmt" diff --git a/models/migrations/v1_15/v184.go b/models/migrations/v1_15/v184.go index 4b3dd1467a..2823bc1f7a 100644 --- a/models/migrations/v1_15/v184.go +++ b/models/migrations/v1_15/v184.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "context" diff --git a/models/migrations/v1_15/v185.go b/models/migrations/v1_15/v185.go index e5878ec193..60af59edca 100644 --- a/models/migrations/v1_15/v185.go +++ b/models/migrations/v1_15/v185.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_15/v186.go b/models/migrations/v1_15/v186.go index 01aab3add5..67dc97d13d 100644 --- a/models/migrations/v1_15/v186.go +++ b/models/migrations/v1_15/v186.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_15/v187.go b/models/migrations/v1_15/v187.go index 21cd6772b7..5fd90c65fb 100644 --- a/models/migrations/v1_15/v187.go +++ b/models/migrations/v1_15/v187.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_15/v188.go b/models/migrations/v1_15/v188.go index 71e45cab0e..4494e6ff05 100644 --- a/models/migrations/v1_15/v188.go +++ b/models/migrations/v1_15/v188.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_15 //nolint +package v1_15 import "xorm.io/xorm" diff --git a/models/migrations/v1_16/main_test.go b/models/migrations/v1_16/main_test.go index 817a0c13a4..7f93d6e9e5 100644 --- a/models/migrations/v1_16/main_test.go +++ b/models/migrations/v1_16/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" diff --git a/models/migrations/v1_16/v189.go b/models/migrations/v1_16/v189.go index 5649645051..6bc99e58ab 100644 --- a/models/migrations/v1_16/v189.go +++ b/models/migrations/v1_16/v189.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "encoding/binary" diff --git a/models/migrations/v1_16/v189_test.go b/models/migrations/v1_16/v189_test.go index 2a73bfae03..fb56ac8e11 100644 --- a/models/migrations/v1_16/v189_test.go +++ b/models/migrations/v1_16/v189_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" diff --git a/models/migrations/v1_16/v190.go b/models/migrations/v1_16/v190.go index 5953802849..1eb6b6ddb4 100644 --- a/models/migrations/v1_16/v190.go +++ b/models/migrations/v1_16/v190.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v191.go b/models/migrations/v1_16/v191.go index c618783c08..957c82e484 100644 --- a/models/migrations/v1_16/v191.go +++ b/models/migrations/v1_16/v191.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_16/v192.go b/models/migrations/v1_16/v192.go index 2d5d158a09..9d03fbe3c8 100644 --- a/models/migrations/v1_16/v192.go +++ b/models/migrations/v1_16/v192.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_16/v193.go b/models/migrations/v1_16/v193.go index 8d3ce7a558..a5af2de380 100644 --- a/models/migrations/v1_16/v193.go +++ b/models/migrations/v1_16/v193.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go index 7f43846bc3..2e827f0550 100644 --- a/models/migrations/v1_16/v193_test.go +++ b/models/migrations/v1_16/v193_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" diff --git a/models/migrations/v1_16/v194.go b/models/migrations/v1_16/v194.go index 6aa13c50cf..2e4ed8340e 100644 --- a/models/migrations/v1_16/v194.go +++ b/models/migrations/v1_16/v194.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v195.go b/models/migrations/v1_16/v195.go index 6d7e94141e..4fd42b7bd2 100644 --- a/models/migrations/v1_16/v195.go +++ b/models/migrations/v1_16/v195.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v195_test.go b/models/migrations/v1_16/v195_test.go index 742397bf32..946e06e399 100644 --- a/models/migrations/v1_16/v195_test.go +++ b/models/migrations/v1_16/v195_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" diff --git a/models/migrations/v1_16/v196.go b/models/migrations/v1_16/v196.go index 7cbafc61e5..6c9caa100f 100644 --- a/models/migrations/v1_16/v196.go +++ b/models/migrations/v1_16/v196.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v197.go b/models/migrations/v1_16/v197.go index 97888b2847..862bdfdcbd 100644 --- a/models/migrations/v1_16/v197.go +++ b/models/migrations/v1_16/v197.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v198.go b/models/migrations/v1_16/v198.go index 115bb313a0..f35ede138a 100644 --- a/models/migrations/v1_16/v198.go +++ b/models/migrations/v1_16/v198.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v199.go b/models/migrations/v1_16/v199.go index 6adcf890af..4020352f2b 100644 --- a/models/migrations/v1_16/v199.go +++ b/models/migrations/v1_16/v199.go @@ -1,6 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 // We used to use a table `remote_version` to store information for updater, now we use `AppState`, so this migration task is a no-op now. diff --git a/models/migrations/v1_16/v200.go b/models/migrations/v1_16/v200.go index c08c20e51d..de57fad8fe 100644 --- a/models/migrations/v1_16/v200.go +++ b/models/migrations/v1_16/v200.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v201.go b/models/migrations/v1_16/v201.go index 35e0c9f2fb..2c43698b0c 100644 --- a/models/migrations/v1_16/v201.go +++ b/models/migrations/v1_16/v201.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v202.go b/models/migrations/v1_16/v202.go index 6ba36152f1..d8c8fdcadc 100644 --- a/models/migrations/v1_16/v202.go +++ b/models/migrations/v1_16/v202.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v203.go b/models/migrations/v1_16/v203.go index e8e6b52453..c3241cba57 100644 --- a/models/migrations/v1_16/v203.go +++ b/models/migrations/v1_16/v203.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v204.go b/models/migrations/v1_16/v204.go index ece03e1305..4d375307e7 100644 --- a/models/migrations/v1_16/v204.go +++ b/models/migrations/v1_16/v204.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import "xorm.io/xorm" diff --git a/models/migrations/v1_16/v205.go b/models/migrations/v1_16/v205.go index d6c577083c..78241bad5b 100644 --- a/models/migrations/v1_16/v205.go +++ b/models/migrations/v1_16/v205.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_16/v206.go b/models/migrations/v1_16/v206.go index 581a7d76e9..01a9c386eb 100644 --- a/models/migrations/v1_16/v206.go +++ b/models/migrations/v1_16/v206.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "fmt" diff --git a/models/migrations/v1_16/v207.go b/models/migrations/v1_16/v207.go index 91208f066c..19126ead1f 100644 --- a/models/migrations/v1_16/v207.go +++ b/models/migrations/v1_16/v207.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v208.go b/models/migrations/v1_16/v208.go index 1a11ef096a..fb643324f4 100644 --- a/models/migrations/v1_16/v208.go +++ b/models/migrations/v1_16/v208.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v209.go b/models/migrations/v1_16/v209.go index be3100e02a..230838647b 100644 --- a/models/migrations/v1_16/v209.go +++ b/models/migrations/v1_16/v209.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_16/v210.go b/models/migrations/v1_16/v210.go index 51b7d81e99..0b94baf8e3 100644 --- a/models/migrations/v1_16/v210.go +++ b/models/migrations/v1_16/v210.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "encoding/base32" diff --git a/models/migrations/v1_16/v210_test.go b/models/migrations/v1_16/v210_test.go index 7917301c98..3b4ac7aa4b 100644 --- a/models/migrations/v1_16/v210_test.go +++ b/models/migrations/v1_16/v210_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_16 //nolint +package v1_16 import ( "testing" diff --git a/models/migrations/v1_17/main_test.go b/models/migrations/v1_17/main_test.go index 79cb3fa078..571a4f55a3 100644 --- a/models/migrations/v1_17/main_test.go +++ b/models/migrations/v1_17/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "testing" diff --git a/models/migrations/v1_17/v211.go b/models/migrations/v1_17/v211.go index 9b72c8610b..517cf19388 100644 --- a/models/migrations/v1_17/v211.go +++ b/models/migrations/v1_17/v211.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_17/v212.go b/models/migrations/v1_17/v212.go index e3f9437121..788792211f 100644 --- a/models/migrations/v1_17/v212.go +++ b/models/migrations/v1_17/v212.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_17/v213.go b/models/migrations/v1_17/v213.go index bb3f466e52..b2bbdf7279 100644 --- a/models/migrations/v1_17/v213.go +++ b/models/migrations/v1_17/v213.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_17/v214.go b/models/migrations/v1_17/v214.go index 2268164919..1925324f0f 100644 --- a/models/migrations/v1_17/v214.go +++ b/models/migrations/v1_17/v214.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_17/v215.go b/models/migrations/v1_17/v215.go index b338f85417..748539225d 100644 --- a/models/migrations/v1_17/v215.go +++ b/models/migrations/v1_17/v215.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "code.gitea.io/gitea/models/pull" diff --git a/models/migrations/v1_17/v216.go b/models/migrations/v1_17/v216.go index 268f472a42..37aeacb6fc 100644 --- a/models/migrations/v1_17/v216.go +++ b/models/migrations/v1_17/v216.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 // This migration added non-ideal indices to the action table which on larger datasets slowed things down // it has been superseded by v218.go diff --git a/models/migrations/v1_17/v217.go b/models/migrations/v1_17/v217.go index 3f970b68a5..04626bcbc5 100644 --- a/models/migrations/v1_17/v217.go +++ b/models/migrations/v1_17/v217.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_17/v218.go b/models/migrations/v1_17/v218.go index 4c05a9b539..17d4cd89d4 100644 --- a/models/migrations/v1_17/v218.go +++ b/models/migrations/v1_17/v218.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_17/v219.go b/models/migrations/v1_17/v219.go index d266029fd9..6e335cb813 100644 --- a/models/migrations/v1_17/v219.go +++ b/models/migrations/v1_17/v219.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "time" diff --git a/models/migrations/v1_17/v220.go b/models/migrations/v1_17/v220.go index 904ddc5192..4ac8c58e1e 100644 --- a/models/migrations/v1_17/v220.go +++ b/models/migrations/v1_17/v220.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( packages_model "code.gitea.io/gitea/models/packages" diff --git a/models/migrations/v1_17/v221.go b/models/migrations/v1_17/v221.go index 9e159388bd..9e6a67eb18 100644 --- a/models/migrations/v1_17/v221.go +++ b/models/migrations/v1_17/v221.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "encoding/base32" diff --git a/models/migrations/v1_17/v221_test.go b/models/migrations/v1_17/v221_test.go index 9ca54142e2..a2dc0fae55 100644 --- a/models/migrations/v1_17/v221_test.go +++ b/models/migrations/v1_17/v221_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "encoding/base32" diff --git a/models/migrations/v1_17/v222.go b/models/migrations/v1_17/v222.go index 6c28f8102b..a5ea537d8a 100644 --- a/models/migrations/v1_17/v222.go +++ b/models/migrations/v1_17/v222.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "context" diff --git a/models/migrations/v1_17/v223.go b/models/migrations/v1_17/v223.go index 018451ee4c..b2bfb76551 100644 --- a/models/migrations/v1_17/v223.go +++ b/models/migrations/v1_17/v223.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_17 //nolint +package v1_17 import ( "context" diff --git a/models/migrations/v1_18/main_test.go b/models/migrations/v1_18/main_test.go index f71a21d1fb..ebcfb45a94 100644 --- a/models/migrations/v1_18/main_test.go +++ b/models/migrations/v1_18/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "testing" diff --git a/models/migrations/v1_18/v224.go b/models/migrations/v1_18/v224.go index f3d522b91a..6dc12020ea 100644 --- a/models/migrations/v1_18/v224.go +++ b/models/migrations/v1_18/v224.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_18/v225.go b/models/migrations/v1_18/v225.go index b0ac3777fc..bc6117e38f 100644 --- a/models/migrations/v1_18/v225.go +++ b/models/migrations/v1_18/v225.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_18/v226.go b/models/migrations/v1_18/v226.go index f87e24b11d..8ed9761476 100644 --- a/models/migrations/v1_18/v226.go +++ b/models/migrations/v1_18/v226.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "xorm.io/builder" diff --git a/models/migrations/v1_18/v227.go b/models/migrations/v1_18/v227.go index 5fe5dcd0c9..3aca686d59 100644 --- a/models/migrations/v1_18/v227.go +++ b/models/migrations/v1_18/v227.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_18/v228.go b/models/migrations/v1_18/v228.go index 3e7a36de15..b13f6461bd 100644 --- a/models/migrations/v1_18/v228.go +++ b/models/migrations/v1_18/v228.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_18/v229.go b/models/migrations/v1_18/v229.go index 10d9f35097..bc15e01390 100644 --- a/models/migrations/v1_18/v229.go +++ b/models/migrations/v1_18/v229.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "fmt" diff --git a/models/migrations/v1_18/v229_test.go b/models/migrations/v1_18/v229_test.go index d489328c00..5722dd3557 100644 --- a/models/migrations/v1_18/v229_test.go +++ b/models/migrations/v1_18/v229_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "testing" diff --git a/models/migrations/v1_18/v230.go b/models/migrations/v1_18/v230.go index ea5b4d02e1..078fce7643 100644 --- a/models/migrations/v1_18/v230.go +++ b/models/migrations/v1_18/v230.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_18/v230_test.go b/models/migrations/v1_18/v230_test.go index 40db4c2ffe..25b2f6525d 100644 --- a/models/migrations/v1_18/v230_test.go +++ b/models/migrations/v1_18/v230_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_18 //nolint +package v1_18 import ( "testing" diff --git a/models/migrations/v1_19/main_test.go b/models/migrations/v1_19/main_test.go index 59f42af111..87e807be6e 100644 --- a/models/migrations/v1_19/main_test.go +++ b/models/migrations/v1_19/main_test.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "testing" diff --git a/models/migrations/v1_19/v231.go b/models/migrations/v1_19/v231.go index 79e46132f0..8ef1e4e743 100644 --- a/models/migrations/v1_19/v231.go +++ b/models/migrations/v1_19/v231.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v232.go b/models/migrations/v1_19/v232.go index 9caf587c1e..493dbc6df8 100644 --- a/models/migrations/v1_19/v232.go +++ b/models/migrations/v1_19/v232.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_19/v233.go b/models/migrations/v1_19/v233.go index ba4cd8e20b..9eb6d40509 100644 --- a/models/migrations/v1_19/v233.go +++ b/models/migrations/v1_19/v233.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "fmt" diff --git a/models/migrations/v1_19/v233_test.go b/models/migrations/v1_19/v233_test.go index 5d445d5132..7436ff7483 100644 --- a/models/migrations/v1_19/v233_test.go +++ b/models/migrations/v1_19/v233_test.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "testing" diff --git a/models/migrations/v1_19/v234.go b/models/migrations/v1_19/v234.go index 728a580807..3475384d6f 100644 --- a/models/migrations/v1_19/v234.go +++ b/models/migrations/v1_19/v234.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_19/v235.go b/models/migrations/v1_19/v235.go index 3715de3920..297d90f65a 100644 --- a/models/migrations/v1_19/v235.go +++ b/models/migrations/v1_19/v235.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v236.go b/models/migrations/v1_19/v236.go index f172a85b1f..0ed4d97a27 100644 --- a/models/migrations/v1_19/v236.go +++ b/models/migrations/v1_19/v236.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_19/v237.go b/models/migrations/v1_19/v237.go index b23c765aa5..cf30226ccd 100644 --- a/models/migrations/v1_19/v237.go +++ b/models/migrations/v1_19/v237.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v238.go b/models/migrations/v1_19/v238.go index 266e6cea58..de681bfc7a 100644 --- a/models/migrations/v1_19/v238.go +++ b/models/migrations/v1_19/v238.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_19/v239.go b/models/migrations/v1_19/v239.go index 10076f2401..8f4a65be95 100644 --- a/models/migrations/v1_19/v239.go +++ b/models/migrations/v1_19/v239.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v240.go b/models/migrations/v1_19/v240.go index 4505f86299..7fdbaeb9dc 100644 --- a/models/migrations/v1_19/v240.go +++ b/models/migrations/v1_19/v240.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/models/db" diff --git a/models/migrations/v1_19/v241.go b/models/migrations/v1_19/v241.go index a617d6fd2f..e35801a057 100644 --- a/models/migrations/v1_19/v241.go +++ b/models/migrations/v1_19/v241.go @@ -1,7 +1,7 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_19/v242.go b/models/migrations/v1_19/v242.go index 4470835214..e9e759eaaa 100644 --- a/models/migrations/v1_19/v242.go +++ b/models/migrations/v1_19/v242.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_19/v243.go b/models/migrations/v1_19/v243.go index 55bbfafb2f..9c3f372594 100644 --- a/models/migrations/v1_19/v243.go +++ b/models/migrations/v1_19/v243.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_19 //nolint +package v1_19 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/main_test.go b/models/migrations/v1_20/main_test.go index 92a1a9f622..2fd63a7118 100644 --- a/models/migrations/v1_20/main_test.go +++ b/models/migrations/v1_20/main_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "testing" diff --git a/models/migrations/v1_20/v244.go b/models/migrations/v1_20/v244.go index 977566ad7d..76cdccaca5 100644 --- a/models/migrations/v1_20/v244.go +++ b/models/migrations/v1_20/v244.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v245.go b/models/migrations/v1_20/v245.go index 5a195d2ccd..4acb11416c 100644 --- a/models/migrations/v1_20/v245.go +++ b/models/migrations/v1_20/v245.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "context" diff --git a/models/migrations/v1_20/v246.go b/models/migrations/v1_20/v246.go index e6340ef079..22bf723404 100644 --- a/models/migrations/v1_20/v246.go +++ b/models/migrations/v1_20/v246.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v247.go b/models/migrations/v1_20/v247.go index 59fc5c46b5..4f82937e18 100644 --- a/models/migrations/v1_20/v247.go +++ b/models/migrations/v1_20/v247.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_20/v248.go b/models/migrations/v1_20/v248.go index 40555210e7..4f2091e4bc 100644 --- a/models/migrations/v1_20/v248.go +++ b/models/migrations/v1_20/v248.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import "xorm.io/xorm" diff --git a/models/migrations/v1_20/v249.go b/models/migrations/v1_20/v249.go index 02951a74d6..c6d3a177ca 100644 --- a/models/migrations/v1_20/v249.go +++ b/models/migrations/v1_20/v249.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_20/v250.go b/models/migrations/v1_20/v250.go index 86388ef0b8..ec45e6e5c3 100644 --- a/models/migrations/v1_20/v250.go +++ b/models/migrations/v1_20/v250.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "strings" diff --git a/models/migrations/v1_20/v251.go b/models/migrations/v1_20/v251.go index 7743248a3f..a274c22a73 100644 --- a/models/migrations/v1_20/v251.go +++ b/models/migrations/v1_20/v251.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_20/v252.go b/models/migrations/v1_20/v252.go index ab61cd9b8b..d6aa602753 100644 --- a/models/migrations/v1_20/v252.go +++ b/models/migrations/v1_20/v252.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_20/v253.go b/models/migrations/v1_20/v253.go index 96c494bd8d..c96454dbf9 100644 --- a/models/migrations/v1_20/v253.go +++ b/models/migrations/v1_20/v253.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/log" diff --git a/models/migrations/v1_20/v254.go b/models/migrations/v1_20/v254.go index 1e26979a5b..9cdbfb3916 100644 --- a/models/migrations/v1_20/v254.go +++ b/models/migrations/v1_20/v254.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v255.go b/models/migrations/v1_20/v255.go index 14b70f8f96..caf198700e 100644 --- a/models/migrations/v1_20/v255.go +++ b/models/migrations/v1_20/v255.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_20/v256.go b/models/migrations/v1_20/v256.go index 822153b93e..7b84c1e154 100644 --- a/models/migrations/v1_20/v256.go +++ b/models/migrations/v1_20/v256.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v257.go b/models/migrations/v1_20/v257.go index 6c6ca4c748..9d5f7c07df 100644 --- a/models/migrations/v1_20/v257.go +++ b/models/migrations/v1_20/v257.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_20/v258.go b/models/migrations/v1_20/v258.go index 47174ce805..1d3faffdae 100644 --- a/models/migrations/v1_20/v258.go +++ b/models/migrations/v1_20/v258.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_20/v259.go b/models/migrations/v1_20/v259.go index 5b8ced4ad7..9e0dc9b61d 100644 --- a/models/migrations/v1_20/v259.go +++ b/models/migrations/v1_20/v259.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "fmt" @@ -329,7 +329,7 @@ func ConvertScopedAccessTokens(x *xorm.Engine) error { for _, token := range tokens { var scopes []string allNewScopesMap := make(map[AccessTokenScope]bool) - for _, oldScope := range strings.Split(token.Scope, ",") { + for oldScope := range strings.SplitSeq(token.Scope, ",") { if newScopes, exists := accessTokenScopeMap[OldAccessTokenScope(oldScope)]; exists { for _, newScope := range newScopes { allNewScopesMap[newScope] = true diff --git a/models/migrations/v1_20/v259_test.go b/models/migrations/v1_20/v259_test.go index a1aeb53d5d..0bf63719e5 100644 --- a/models/migrations/v1_20/v259_test.go +++ b/models/migrations/v1_20/v259_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_20 //nolint +package v1_20 import ( "sort" diff --git a/models/migrations/v1_21/main_test.go b/models/migrations/v1_21/main_test.go index 9afdea1677..536a7ade08 100644 --- a/models/migrations/v1_21/main_test.go +++ b/models/migrations/v1_21/main_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "testing" diff --git a/models/migrations/v1_21/v260.go b/models/migrations/v1_21/v260.go index 6ca52c5998..8540c58ae8 100644 --- a/models/migrations/v1_21/v260.go +++ b/models/migrations/v1_21/v260.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_21/v261.go b/models/migrations/v1_21/v261.go index 4ec1160d0b..122b98eb93 100644 --- a/models/migrations/v1_21/v261.go +++ b/models/migrations/v1_21/v261.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_21/v262.go b/models/migrations/v1_21/v262.go index 23e900572a..6e88e29b9d 100644 --- a/models/migrations/v1_21/v262.go +++ b/models/migrations/v1_21/v262.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v263.go b/models/migrations/v1_21/v263.go index 2c7cbadf0d..55c418bde0 100644 --- a/models/migrations/v1_21/v263.go +++ b/models/migrations/v1_21/v263.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "fmt" diff --git a/models/migrations/v1_21/v264.go b/models/migrations/v1_21/v264.go index d737ef03b3..7fc0ec6024 100644 --- a/models/migrations/v1_21/v264.go +++ b/models/migrations/v1_21/v264.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "context" diff --git a/models/migrations/v1_21/v265.go b/models/migrations/v1_21/v265.go index 800eb95f72..b6892acc27 100644 --- a/models/migrations/v1_21/v265.go +++ b/models/migrations/v1_21/v265.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v266.go b/models/migrations/v1_21/v266.go index 79a5f5e14c..440549e868 100644 --- a/models/migrations/v1_21/v266.go +++ b/models/migrations/v1_21/v266.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v267.go b/models/migrations/v1_21/v267.go index bc0e954bdc..394139a17e 100644 --- a/models/migrations/v1_21/v267.go +++ b/models/migrations/v1_21/v267.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_21/v268.go b/models/migrations/v1_21/v268.go index 332793ff07..b677d2383e 100644 --- a/models/migrations/v1_21/v268.go +++ b/models/migrations/v1_21/v268.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v269.go b/models/migrations/v1_21/v269.go index 475ec02380..042040927d 100644 --- a/models/migrations/v1_21/v269.go +++ b/models/migrations/v1_21/v269.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v270.go b/models/migrations/v1_21/v270.go index b9cc84d3ac..ab7c5660ba 100644 --- a/models/migrations/v1_21/v270.go +++ b/models/migrations/v1_21/v270.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v271.go b/models/migrations/v1_21/v271.go index 098f6499d5..05e1af1351 100644 --- a/models/migrations/v1_21/v271.go +++ b/models/migrations/v1_21/v271.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 + import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_21/v272.go b/models/migrations/v1_21/v272.go index a729c49f1b..14c1e0c4b0 100644 --- a/models/migrations/v1_21/v272.go +++ b/models/migrations/v1_21/v272.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 + import ( "xorm.io/xorm" ) diff --git a/models/migrations/v1_21/v273.go b/models/migrations/v1_21/v273.go index 61c79f4a76..e614a56a7d 100644 --- a/models/migrations/v1_21/v273.go +++ b/models/migrations/v1_21/v273.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 + import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_21/v274.go b/models/migrations/v1_21/v274.go index df5994f159..d0b557a151 100644 --- a/models/migrations/v1_21/v274.go +++ b/models/migrations/v1_21/v274.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 + import ( "time" diff --git a/models/migrations/v1_21/v275.go b/models/migrations/v1_21/v275.go index 78804a59d6..2bfe5c72fa 100644 --- a/models/migrations/v1_21/v275.go +++ b/models/migrations/v1_21/v275.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go index 9d22c9052e..3ab7e22cd0 100644 --- a/models/migrations/v1_21/v276.go +++ b/models/migrations/v1_21/v276.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "context" diff --git a/models/migrations/v1_21/v277.go b/models/migrations/v1_21/v277.go index 12529160b7..0c102eddde 100644 --- a/models/migrations/v1_21/v277.go +++ b/models/migrations/v1_21/v277.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v278.go b/models/migrations/v1_21/v278.go index d6a462d1e7..846f228678 100644 --- a/models/migrations/v1_21/v278.go +++ b/models/migrations/v1_21/v278.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_21/v279.go b/models/migrations/v1_21/v279.go index 2abd1bbe84..beb39effe1 100644 --- a/models/migrations/v1_21/v279.go +++ b/models/migrations/v1_21/v279.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_21 //nolint +package v1_21 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/main_test.go b/models/migrations/v1_22/main_test.go index efd8dbaa8c..ac8facd6aa 100644 --- a/models/migrations/v1_22/main_test.go +++ b/models/migrations/v1_22/main_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "testing" diff --git a/models/migrations/v1_22/v280.go b/models/migrations/v1_22/v280.go index a8ee4a3bf7..2271cb6089 100644 --- a/models/migrations/v1_22/v280.go +++ b/models/migrations/v1_22/v280.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/v281.go b/models/migrations/v1_22/v281.go index fc1866aa83..129ec2cba0 100644 --- a/models/migrations/v1_22/v281.go +++ b/models/migrations/v1_22/v281.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_22/v282.go b/models/migrations/v1_22/v282.go index baad9e0916..eed64c30f7 100644 --- a/models/migrations/v1_22/v282.go +++ b/models/migrations/v1_22/v282.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/v283.go b/models/migrations/v1_22/v283.go index 0a45c51245..0eca031b65 100644 --- a/models/migrations/v1_22/v283.go +++ b/models/migrations/v1_22/v283.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "fmt" diff --git a/models/migrations/v1_22/v283_test.go b/models/migrations/v1_22/v283_test.go index e89a7cbfc2..743f860466 100644 --- a/models/migrations/v1_22/v283_test.go +++ b/models/migrations/v1_22/v283_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "testing" diff --git a/models/migrations/v1_22/v284.go b/models/migrations/v1_22/v284.go index 2b95078980..31b38f6aed 100644 --- a/models/migrations/v1_22/v284.go +++ b/models/migrations/v1_22/v284.go @@ -1,7 +1,8 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 + import ( "xorm.io/xorm" ) diff --git a/models/migrations/v1_22/v285.go b/models/migrations/v1_22/v285.go index a55cc17c04..fed89f670e 100644 --- a/models/migrations/v1_22/v285.go +++ b/models/migrations/v1_22/v285.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "time" diff --git a/models/migrations/v1_22/v286.go b/models/migrations/v1_22/v286.go index 1fcde33202..f3ba50dbb6 100644 --- a/models/migrations/v1_22/v286.go +++ b/models/migrations/v1_22/v286.go @@ -1,6 +1,6 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "errors" diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index 4702e4c37c..b4a50f6fcb 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "testing" diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go index c8b1593286..5fd901f9de 100644 --- a/models/migrations/v1_22/v287.go +++ b/models/migrations/v1_22/v287.go @@ -1,7 +1,7 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go index 58c3152ac3..2b42a33c38 100644 --- a/models/migrations/v1_22/v287_test.go +++ b/models/migrations/v1_22/v287_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "strconv" diff --git a/models/migrations/v1_22/v288.go b/models/migrations/v1_22/v288.go index 7c93bfcc66..26c850c218 100644 --- a/models/migrations/v1_22/v288.go +++ b/models/migrations/v1_22/v288.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_22/v289.go b/models/migrations/v1_22/v289.go index b9941aadd9..78689a4ffa 100644 --- a/models/migrations/v1_22/v289.go +++ b/models/migrations/v1_22/v289.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_22/v290.go b/models/migrations/v1_22/v290.go index 9c54d4e87c..0f4d78410c 100644 --- a/models/migrations/v1_22/v290.go +++ b/models/migrations/v1_22/v290.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_22/v291.go b/models/migrations/v1_22/v291.go index 74726fae96..823a644a95 100644 --- a/models/migrations/v1_22/v291.go +++ b/models/migrations/v1_22/v291.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_22/v292.go b/models/migrations/v1_22/v292.go index beca556aee..440f48ce80 100644 --- a/models/migrations/v1_22/v292.go +++ b/models/migrations/v1_22/v292.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 // NOTE: noop the original migration has bug which some projects will be skip, so // these projects will have no default board. diff --git a/models/migrations/v1_22/v293.go b/models/migrations/v1_22/v293.go index 53cc719294..5299b8618f 100644 --- a/models/migrations/v1_22/v293.go +++ b/models/migrations/v1_22/v293.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_22/v293_test.go b/models/migrations/v1_22/v293_test.go index cfe4345143..2c8f7683a8 100644 --- a/models/migrations/v1_22/v293_test.go +++ b/models/migrations/v1_22/v293_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "testing" diff --git a/models/migrations/v1_22/v294.go b/models/migrations/v1_22/v294.go index 20e261fb1b..8776e51a16 100644 --- a/models/migrations/v1_22/v294.go +++ b/models/migrations/v1_22/v294.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "fmt" diff --git a/models/migrations/v1_22/v294_test.go b/models/migrations/v1_22/v294_test.go index a1d702cb77..1cf03d6120 100644 --- a/models/migrations/v1_22/v294_test.go +++ b/models/migrations/v1_22/v294_test.go @@ -1,10 +1,9 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( - "slices" "testing" "code.gitea.io/gitea/models/migrations/base" @@ -44,7 +43,7 @@ func Test_AddUniqueIndexForProjectIssue(t *testing.T) { for _, index := range tables[0].Indexes { if index.Type == schemas.UniqueType { found = true - slices.Equal(index.Cols, []string{"project_id", "issue_id"}) + assert.ElementsMatch(t, index.Cols, []string{"project_id", "issue_id"}) break } } diff --git a/models/migrations/v1_22/v295.go b/models/migrations/v1_22/v295.go index 17bdadb4ad..319b1a399b 100644 --- a/models/migrations/v1_22/v295.go +++ b/models/migrations/v1_22/v295.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_22/v296.go b/models/migrations/v1_22/v296.go index 1ecacab95f..75350f9f65 100644 --- a/models/migrations/v1_22/v296.go +++ b/models/migrations/v1_22/v296.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_22/v297.go b/models/migrations/v1_22/v297.go index 7d4b506925..9a4405f266 100644 --- a/models/migrations/v1_22/v297.go +++ b/models/migrations/v1_22/v297.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import ( "code.gitea.io/gitea/models/perm" diff --git a/models/migrations/v1_22/v298.go b/models/migrations/v1_22/v298.go index b9f3b95ade..7700173a00 100644 --- a/models/migrations/v1_22/v298.go +++ b/models/migrations/v1_22/v298.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_22 //nolint +package v1_22 import "xorm.io/xorm" diff --git a/models/migrations/v1_23/main_test.go b/models/migrations/v1_23/main_test.go index b7948bd4dd..f7b2caed83 100644 --- a/models/migrations/v1_23/main_test.go +++ b/models/migrations/v1_23/main_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "testing" diff --git a/models/migrations/v1_23/v299.go b/models/migrations/v1_23/v299.go index e5fde3749b..11021d8855 100644 --- a/models/migrations/v1_23/v299.go +++ b/models/migrations/v1_23/v299.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" diff --git a/models/migrations/v1_23/v300.go b/models/migrations/v1_23/v300.go index 51de43da5e..13c6489c5e 100644 --- a/models/migrations/v1_23/v300.go +++ b/models/migrations/v1_23/v300.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" diff --git a/models/migrations/v1_23/v301.go b/models/migrations/v1_23/v301.go index 99c8e3d8ea..ed8e9ef059 100644 --- a/models/migrations/v1_23/v301.go +++ b/models/migrations/v1_23/v301.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" diff --git a/models/migrations/v1_23/v302.go b/models/migrations/v1_23/v302.go index 5d2e9b1438..e4a50b3ec8 100644 --- a/models/migrations/v1_23/v302.go +++ b/models/migrations/v1_23/v302.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_23/v302_test.go b/models/migrations/v1_23/v302_test.go index 29e85ae9d9..b008b6fc03 100644 --- a/models/migrations/v1_23/v302_test.go +++ b/models/migrations/v1_23/v302_test.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "testing" diff --git a/models/migrations/v1_23/v303.go b/models/migrations/v1_23/v303.go index 1e36388930..dc541a9535 100644 --- a/models/migrations/v1_23/v303.go +++ b/models/migrations/v1_23/v303.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_23/v304.go b/models/migrations/v1_23/v304.go index e108f47779..35d4d4881a 100644 --- a/models/migrations/v1_23/v304.go +++ b/models/migrations/v1_23/v304.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" diff --git a/models/migrations/v1_23/v304_test.go b/models/migrations/v1_23/v304_test.go index 955219d3f9..c3dfa5e7e7 100644 --- a/models/migrations/v1_23/v304_test.go +++ b/models/migrations/v1_23/v304_test.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "testing" diff --git a/models/migrations/v1_23/v305.go b/models/migrations/v1_23/v305.go index 4d881192b2..3762279de1 100644 --- a/models/migrations/v1_23/v305.go +++ b/models/migrations/v1_23/v305.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_23/v306.go b/models/migrations/v1_23/v306.go index a1e698fe31..c5c89dbeb8 100644 --- a/models/migrations/v1_23/v306.go +++ b/models/migrations/v1_23/v306.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import "xorm.io/xorm" diff --git a/models/migrations/v1_23/v307.go b/models/migrations/v1_23/v307.go index ef7f5f2c3f..54a69d250b 100644 --- a/models/migrations/v1_23/v307.go +++ b/models/migrations/v1_23/v307.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_23/v308.go b/models/migrations/v1_23/v308.go index 1e8a9b0af2..695fdfcc2d 100644 --- a/models/migrations/v1_23/v308.go +++ b/models/migrations/v1_23/v308.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_23/v309.go b/models/migrations/v1_23/v309.go index 5b39398443..e629b718a8 100644 --- a/models/migrations/v1_23/v309.go +++ b/models/migrations/v1_23/v309.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_23/v310.go b/models/migrations/v1_23/v310.go index c856a708f9..074b1c54d3 100644 --- a/models/migrations/v1_23/v310.go +++ b/models/migrations/v1_23/v310.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_23/v311.go b/models/migrations/v1_23/v311.go index 21293d83be..ef48085c79 100644 --- a/models/migrations/v1_23/v311.go +++ b/models/migrations/v1_23/v311.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_23 //nolint +package v1_23 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_24/v312.go b/models/migrations/v1_24/v312.go index 367a6c4947..823b0eae40 100644 --- a/models/migrations/v1_24/v312.go +++ b/models/migrations/v1_24/v312.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_24 //nolint +package v1_24 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_24/v313.go b/models/migrations/v1_24/v313.go index ee9d479340..7e6cda6bfd 100644 --- a/models/migrations/v1_24/v313.go +++ b/models/migrations/v1_24/v313.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_24 //nolint +package v1_24 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_24/v314.go b/models/migrations/v1_24/v314.go index e537be13b5..51cb2e34aa 100644 --- a/models/migrations/v1_24/v314.go +++ b/models/migrations/v1_24/v314.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_24 //nolint +package v1_24 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_24/v315.go b/models/migrations/v1_24/v315.go index 22a72c31e9..52b9b44785 100644 --- a/models/migrations/v1_24/v315.go +++ b/models/migrations/v1_24/v315.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_24 //nolint +package v1_24 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_24/v316.go b/models/migrations/v1_24/v316.go index e7f04333cc..14e888f9ee 100644 --- a/models/migrations/v1_24/v316.go +++ b/models/migrations/v1_24/v316.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_24 //nolint +package v1_24 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_24/v317.go b/models/migrations/v1_24/v317.go index 3da5a4a078..a13db2dd27 100644 --- a/models/migrations/v1_24/v317.go +++ b/models/migrations/v1_24/v317.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_24 //nolint +package v1_24 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_24/v318.go b/models/migrations/v1_24/v318.go index 3e08c3d504..9b4a540960 100644 --- a/models/migrations/v1_24/v318.go +++ b/models/migrations/v1_24/v318.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_24 //nolint +package v1_24 import ( "code.gitea.io/gitea/models/perm" diff --git a/models/migrations/v1_24/v319.go b/models/migrations/v1_24/v319.go index 6571ddf75b..648081f74e 100644 --- a/models/migrations/v1_24/v319.go +++ b/models/migrations/v1_24/v319.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_24 //nolint +package v1_24 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_24/v320.go b/models/migrations/v1_24/v320.go index 1d34444826..ebef71939c 100644 --- a/models/migrations/v1_24/v320.go +++ b/models/migrations/v1_24/v320.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_24 //nolint +package v1_24 import ( "code.gitea.io/gitea/modules/json" diff --git a/models/migrations/v1_6/v70.go b/models/migrations/v1_6/v70.go index 74434a84a1..41f0966942 100644 --- a/models/migrations/v1_6/v70.go +++ b/models/migrations/v1_6/v70.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_6 //nolint +package v1_6 import ( "fmt" diff --git a/models/migrations/v1_6/v71.go b/models/migrations/v1_6/v71.go index 586187228b..2b11f57c92 100644 --- a/models/migrations/v1_6/v71.go +++ b/models/migrations/v1_6/v71.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_6 //nolint +package v1_6 import ( "fmt" diff --git a/models/migrations/v1_6/v72.go b/models/migrations/v1_6/v72.go index 04cef9a170..9fad88a1b6 100644 --- a/models/migrations/v1_6/v72.go +++ b/models/migrations/v1_6/v72.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_6 //nolint +package v1_6 import ( "fmt" diff --git a/models/migrations/v1_7/v73.go b/models/migrations/v1_7/v73.go index b5a748aae3..e0b7a28537 100644 --- a/models/migrations/v1_7/v73.go +++ b/models/migrations/v1_7/v73.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_7 //nolint +package v1_7 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_7/v74.go b/models/migrations/v1_7/v74.go index f0567e3c9b..376be37a24 100644 --- a/models/migrations/v1_7/v74.go +++ b/models/migrations/v1_7/v74.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_7 //nolint +package v1_7 import "xorm.io/xorm" diff --git a/models/migrations/v1_7/v75.go b/models/migrations/v1_7/v75.go index fa7430970c..ef11575466 100644 --- a/models/migrations/v1_7/v75.go +++ b/models/migrations/v1_7/v75.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_7 //nolint +package v1_7 import ( "xorm.io/builder" diff --git a/models/migrations/v1_8/v76.go b/models/migrations/v1_8/v76.go index d3fbd94deb..81e9307549 100644 --- a/models/migrations/v1_8/v76.go +++ b/models/migrations/v1_8/v76.go @@ -1,7 +1,7 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "fmt" diff --git a/models/migrations/v1_8/v77.go b/models/migrations/v1_8/v77.go index 8b19993924..4fe5ebe635 100644 --- a/models/migrations/v1_8/v77.go +++ b/models/migrations/v1_8/v77.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_8/v78.go b/models/migrations/v1_8/v78.go index 8f041c1484..e67f464131 100644 --- a/models/migrations/v1_8/v78.go +++ b/models/migrations/v1_8/v78.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "code.gitea.io/gitea/models/migrations/base" diff --git a/models/migrations/v1_8/v79.go b/models/migrations/v1_8/v79.go index eb3a9ed0f4..3f50114d5a 100644 --- a/models/migrations/v1_8/v79.go +++ b/models/migrations/v1_8/v79.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "code.gitea.io/gitea/modules/setting" diff --git a/models/migrations/v1_8/v80.go b/models/migrations/v1_8/v80.go index cebbbead28..6f9df47a93 100644 --- a/models/migrations/v1_8/v80.go +++ b/models/migrations/v1_8/v80.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import "xorm.io/xorm" diff --git a/models/migrations/v1_8/v81.go b/models/migrations/v1_8/v81.go index a100dc1ef7..3c2acc6458 100644 --- a/models/migrations/v1_8/v81.go +++ b/models/migrations/v1_8/v81.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_8 //nolint +package v1_8 import ( "fmt" diff --git a/models/migrations/v1_9/v82.go b/models/migrations/v1_9/v82.go index 26806dd645..c685d3b86b 100644 --- a/models/migrations/v1_9/v82.go +++ b/models/migrations/v1_9/v82.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "fmt" diff --git a/models/migrations/v1_9/v83.go b/models/migrations/v1_9/v83.go index 10e6c45875..a0cd57f7c5 100644 --- a/models/migrations/v1_9/v83.go +++ b/models/migrations/v1_9/v83.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "code.gitea.io/gitea/modules/timeutil" diff --git a/models/migrations/v1_9/v84.go b/models/migrations/v1_9/v84.go index c7155fe9cf..423915ae57 100644 --- a/models/migrations/v1_9/v84.go +++ b/models/migrations/v1_9/v84.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_9/v85.go b/models/migrations/v1_9/v85.go index a23d7c5d6e..48e1cd5dc4 100644 --- a/models/migrations/v1_9/v85.go +++ b/models/migrations/v1_9/v85.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "fmt" diff --git a/models/migrations/v1_9/v86.go b/models/migrations/v1_9/v86.go index cf2725d158..9464ff0cf6 100644 --- a/models/migrations/v1_9/v86.go +++ b/models/migrations/v1_9/v86.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "xorm.io/xorm" diff --git a/models/migrations/v1_9/v87.go b/models/migrations/v1_9/v87.go index fa01b6e5e3..81a4ebf80d 100644 --- a/models/migrations/v1_9/v87.go +++ b/models/migrations/v1_9/v87.go @@ -1,7 +1,7 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package v1_9 //nolint +package v1_9 import ( "xorm.io/xorm" diff --git a/models/organization/org.go b/models/organization/org.go index dc889ea17f..0f3aef146c 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -602,8 +602,3 @@ func getUserTeamIDsQueryBuilder(orgID, userID int64) *builder.Builder { "team_user.uid": userID, }) } - -// TeamsWithAccessToRepo returns all teams that have given access level to the repository. -func (org *Organization) TeamsWithAccessToRepo(ctx context.Context, repoID int64, mode perm.AccessMode) ([]*Team, error) { - return GetTeamsWithAccessToRepo(ctx, org.ID, repoID, mode) -} diff --git a/models/organization/org_list.go b/models/organization/org_list.go index 78ac0e704a..81457191fe 100644 --- a/models/organization/org_list.go +++ b/models/organization/org_list.go @@ -50,8 +50,8 @@ type SearchOrganizationsOptions struct { // FindOrgOptions finds orgs options type FindOrgOptions struct { db.ListOptions - UserID int64 - IncludePrivate bool + UserID int64 + IncludeVisibility structs.VisibleType } func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { @@ -65,11 +65,10 @@ func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { func (opts FindOrgOptions) ToConds() builder.Cond { var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization} if opts.UserID > 0 { - cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludePrivate))) - } - if !opts.IncludePrivate { - cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic}) + cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludeVisibility == structs.VisibleTypePrivate))) } + // public=0, limited=1, private=2 + cond = cond.And(builder.Lte{"`user`.visibility": opts.IncludeVisibility}) return cond } @@ -77,6 +76,16 @@ func (opts FindOrgOptions) ToOrders() string { return "`user`.lower_name ASC" } +func DoerViewOtherVisibility(doer, other *user_model.User) structs.VisibleType { + if doer == nil || other == nil { + return structs.VisibleTypePublic + } + if doer.IsAdmin || doer.ID == other.ID { + return structs.VisibleTypePrivate + } + return structs.VisibleTypeLimited +} + // GetOrgsCanCreateRepoByUserID returns a list of organizations where given user ID // are allowed to create repos. func GetOrgsCanCreateRepoByUserID(ctx context.Context, userID int64) ([]*Organization, error) { diff --git a/models/organization/org_list_test.go b/models/organization/org_list_test.go index e859d87c84..a2a25c6f91 100644 --- a/models/organization/org_list_test.go +++ b/models/organization/org_list_test.go @@ -10,25 +10,32 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" "github.com/stretchr/testify/assert" ) -func TestCountOrganizations(t *testing.T) { +func TestOrgList(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + t.Run("CountOrganizations", testCountOrganizations) + t.Run("FindOrgs", testFindOrgs) + t.Run("GetUserOrgsList", testGetUserOrgsList) + t.Run("LoadOrgListTeams", testLoadOrgListTeams) + t.Run("DoerViewOtherVisibility", testDoerViewOtherVisibility) +} + +func testCountOrganizations(t *testing.T) { expected, err := db.GetEngine(db.DefaultContext).Where("type=?", user_model.UserTypeOrganization).Count(&organization.Organization{}) assert.NoError(t, err) - cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludePrivate: true}) + cnt, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{IncludeVisibility: structs.VisibleTypePrivate}) assert.NoError(t, err) assert.Equal(t, expected, cnt) } -func TestFindOrgs(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testFindOrgs(t *testing.T) { orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: 4, - IncludePrivate: true, + UserID: 4, + IncludeVisibility: structs.VisibleTypePrivate, }) assert.NoError(t, err) if assert.Len(t, orgs, 1) { @@ -36,22 +43,20 @@ func TestFindOrgs(t *testing.T) { } orgs, err = db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: 4, - IncludePrivate: false, + UserID: 4, }) assert.NoError(t, err) assert.Empty(t, orgs) total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: 4, - IncludePrivate: true, + UserID: 4, + IncludeVisibility: structs.VisibleTypePrivate, }) assert.NoError(t, err) assert.EqualValues(t, 1, total) } -func TestGetUserOrgsList(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetUserOrgsList(t *testing.T) { orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4}) assert.NoError(t, err) if assert.Len(t, orgs, 1) { @@ -61,8 +66,7 @@ func TestGetUserOrgsList(t *testing.T) { } } -func TestLoadOrgListTeams(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testLoadOrgListTeams(t *testing.T) { orgs, err := organization.GetUserOrgsList(db.DefaultContext, &user_model.User{ID: 4}) assert.NoError(t, err) assert.Len(t, orgs, 1) @@ -71,3 +75,10 @@ func TestLoadOrgListTeams(t *testing.T) { assert.Len(t, teamsMap, 1) assert.Len(t, teamsMap[3], 5) } + +func testDoerViewOtherVisibility(t *testing.T) { + assert.Equal(t, structs.VisibleTypePublic, organization.DoerViewOtherVisibility(nil, nil)) + assert.Equal(t, structs.VisibleTypeLimited, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 2})) + assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1}, &user_model.User{ID: 1})) + assert.Equal(t, structs.VisibleTypePrivate, organization.DoerViewOtherVisibility(&user_model.User{ID: 1, IsAdmin: true}, &user_model.User{ID: 2})) +} diff --git a/models/organization/team_repo.go b/models/organization/team_repo.go index 53edd203a8..b3e266dbc7 100644 --- a/models/organization/team_repo.go +++ b/models/organization/team_repo.go @@ -9,6 +9,8 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" + + "xorm.io/builder" ) // TeamRepo represents an team-repository relation. @@ -48,26 +50,27 @@ func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error { return err } -// GetTeamsWithAccessToRepo returns all teams in an organization that have given access level to the repository. -func GetTeamsWithAccessToRepo(ctx context.Context, orgID, repoID int64, mode perm.AccessMode) ([]*Team, error) { +// GetTeamsWithAccessToAnyRepoUnit returns all teams in an organization that have given access level to the repository special unit. +// This function is only used for finding some teams that can be used as branch protection allowlist or reviewers, it isn't really used for access control. +// FIXME: TEAM-UNIT-PERMISSION this logic is not complete, search the fixme keyword to see more details +func GetTeamsWithAccessToAnyRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type, unitTypesMore ...unit.Type) ([]*Team, error) { teams := make([]*Team, 0, 5) - return teams, db.GetEngine(ctx).Where("team.authorize >= ?", mode). - Join("INNER", "team_repo", "team_repo.team_id = team.id"). - And("team_repo.org_id = ?", orgID). - And("team_repo.repo_id = ?", repoID). - OrderBy("name"). - Find(&teams) -} -// GetTeamsWithAccessToRepoUnit returns all teams in an organization that have given access level to the repository special unit. -func GetTeamsWithAccessToRepoUnit(ctx context.Context, orgID, repoID int64, mode perm.AccessMode, unitType unit.Type) ([]*Team, error) { - teams := make([]*Team, 0, 5) - return teams, db.GetEngine(ctx).Where("team_unit.access_mode >= ?", mode). + sub := builder.Select("team_id").From("team_unit"). + Where(builder.Expr("team_unit.team_id = team.id")). + And(builder.In("team_unit.type", append([]unit.Type{unitType}, unitTypesMore...))). + And(builder.Expr("team_unit.access_mode >= ?", mode)) + + err := db.GetEngine(ctx). Join("INNER", "team_repo", "team_repo.team_id = team.id"). - Join("INNER", "team_unit", "team_unit.team_id = team.id"). And("team_repo.org_id = ?", orgID). And("team_repo.repo_id = ?", repoID). - And("team_unit.type = ?", unitType). + And(builder.Or( + builder.Expr("team.authorize >= ?", mode), + builder.In("team.id", sub), + )). OrderBy("name"). Find(&teams) + + return teams, err } diff --git a/models/organization/team_repo_test.go b/models/organization/team_repo_test.go index c0d6750df9..73a06a93c2 100644 --- a/models/organization/team_repo_test.go +++ b/models/organization/team_repo_test.go @@ -22,7 +22,7 @@ func TestGetTeamsWithAccessToRepoUnit(t *testing.T) { org41 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 41}) repo61 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 61}) - teams, err := organization.GetTeamsWithAccessToRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests) + teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(db.DefaultContext, org41.ID, repo61.ID, perm.AccessModeRead, unit.TypePullRequests) assert.NoError(t, err) if assert.Len(t, teams, 2) { assert.EqualValues(t, 21, teams[0].ID) diff --git a/models/packages/container/search.go b/models/packages/container/search.go index 5df35117ce..9321d9eb41 100644 --- a/models/packages/container/search.go +++ b/models/packages/container/search.go @@ -25,6 +25,7 @@ type BlobSearchOptions struct { Digest string Tag string IsManifest bool + OnlyLead bool Repository string } @@ -43,7 +44,10 @@ func (opts *BlobSearchOptions) toConds() builder.Cond { cond = cond.And(builder.Eq{"package_version.lower_version": strings.ToLower(opts.Tag)}) } if opts.IsManifest { - cond = cond.And(builder.Eq{"package_file.lower_name": ManifestFilename}) + cond = cond.And(builder.Eq{"package_file.lower_name": container_module.ManifestFilename}) + } + if opts.OnlyLead { + cond = cond.And(builder.Eq{"package_file.is_lead": true}) } if opts.Digest != "" { var propsCond builder.Cond = builder.Eq{ @@ -73,11 +77,9 @@ func GetContainerBlob(ctx context.Context, opts *BlobSearchOptions) (*packages.P pfds, err := getContainerBlobsLimit(ctx, opts, 1) if err != nil { return nil, err - } - if len(pfds) != 1 { + } else if len(pfds) == 0 { return nil, ErrContainerBlobNotExist } - return pfds[0], nil } @@ -233,7 +235,7 @@ func SearchImageTags(ctx context.Context, opts *ImageTagsSearchOptions) ([]*pack func SearchExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) ([]*packages.PackageFile, error) { var cond builder.Cond = builder.Eq{ "package_version.is_internal": true, - "package_version.lower_version": UploadVersion, + "package_version.lower_version": container_module.UploadVersion, "package.type": packages.TypeContainer, } cond = cond.And(builder.Lt{"package_file.created_unix": time.Now().Add(-olderThan).Unix()}) diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index 1ea181c723..2d43dc3046 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -103,10 +103,10 @@ func (pd *PackageDescriptor) CalculateBlobSize() int64 { // GetPackageDescriptor gets the package description for a version func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDescriptor, error) { - return getPackageDescriptor(ctx, pv, cache.NewEphemeralCache()) + return GetPackageDescriptorWithCache(ctx, pv, cache.NewEphemeralCache()) } -func getPackageDescriptor(ctx context.Context, pv *PackageVersion, c *cache.EphemeralCache) (*PackageDescriptor, error) { +func GetPackageDescriptorWithCache(ctx context.Context, pv *PackageVersion, c *cache.EphemeralCache) (*PackageDescriptor, error) { p, err := cache.GetWithEphemeralCache(ctx, c, "package", pv.PackageID, GetPackageByID) if err != nil { return nil, err @@ -270,7 +270,7 @@ func GetPackageDescriptors(ctx context.Context, pvs []*PackageVersion) ([]*Packa func getPackageDescriptors(ctx context.Context, pvs []*PackageVersion, c *cache.EphemeralCache) ([]*PackageDescriptor, error) { pds := make([]*PackageDescriptor, 0, len(pvs)) for _, pv := range pvs { - pd, err := getPackageDescriptor(ctx, pv, c) + pd, err := GetPackageDescriptorWithCache(ctx, pv, c) if err != nil { return nil, err } diff --git a/models/packages/nuget/search.go b/models/packages/nuget/search.go index 7a505ff08f..a4b23f31d5 100644 --- a/models/packages/nuget/search.go +++ b/models/packages/nuget/search.go @@ -33,7 +33,7 @@ func SearchVersions(ctx context.Context, opts *packages_model.PackageSearchOptio Where(cond). OrderBy("package.name ASC") if opts.Paginator != nil { - skip, take := opts.GetSkipTake() + skip, take := opts.Paginator.GetSkipTake() inner = inner.Limit(take, skip) } diff --git a/models/packages/package_file.go b/models/packages/package_file.go index 270cb32fdf..bf877485d6 100644 --- a/models/packages/package_file.go +++ b/models/packages/package_file.go @@ -115,6 +115,11 @@ func DeleteFileByID(ctx context.Context, fileID int64) error { return err } +func UpdateFile(ctx context.Context, pf *PackageFile, cols []string) error { + _, err := db.GetEngine(ctx).ID(pf.ID).Cols(cols...).Update(pf) + return err +} + // PackageFileSearchOptions are options for SearchXXX methods type PackageFileSearchOptions struct { OwnerID int64 diff --git a/models/packages/package_property.go b/models/packages/package_property.go index e0170016cf..7ddbfd97e9 100644 --- a/models/packages/package_property.go +++ b/models/packages/package_property.go @@ -66,6 +66,20 @@ func UpdateProperty(ctx context.Context, pp *PackageProperty) error { return err } +func InsertOrUpdateProperty(ctx context.Context, refType PropertyType, refID int64, name, value string) error { + pp := PackageProperty{RefType: refType, RefID: refID, Name: name} + ok, err := db.GetEngine(ctx).Get(&pp) + if err != nil { + return err + } + if ok { + _, err = db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", refType, refID, name).Cols("value").Update(&PackageProperty{Value: value}) + return err + } + _, err = InsertProperty(ctx, refType, refID, name, value) + return err +} + // DeleteAllProperties deletes all properties of a ref func DeleteAllProperties(ctx context.Context, refType PropertyType, refID int64) error { _, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Delete(&PackageProperty{}) @@ -78,8 +92,8 @@ func DeletePropertyByID(ctx context.Context, propertyID int64) error { return err } -// DeletePropertyByName deletes properties by name -func DeletePropertyByName(ctx context.Context, refType PropertyType, refID int64, name string) error { +// DeletePropertiesByName deletes properties by name +func DeletePropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) error { _, err := db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Delete(&PackageProperty{}) return err } diff --git a/models/packages/package_version.go b/models/packages/package_version.go index bb7fd895f8..5672e0efbf 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/util" "xorm.io/builder" + "xorm.io/xorm" ) // ErrDuplicatePackageVersion indicates a duplicated package version error @@ -187,7 +188,7 @@ type PackageSearchOptions struct { HasFileWithName string // only results are found which are associated with a file with the specific name HasFiles optional.Option[bool] // only results are found which have associated files Sort VersionSort - db.Paginator + Paginator db.Paginator } func (opts *PackageSearchOptions) ToConds() builder.Cond { @@ -282,6 +283,18 @@ func (opts *PackageSearchOptions) configureOrderBy(e db.Engine) { e.Desc("package_version.id") // Sort by id for stable order with duplicates in the other field } +func searchVersionsBySession(sess *xorm.Session, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { + opts.configureOrderBy(sess) + pvs := make([]*PackageVersion, 0, 10) + if opts.Paginator != nil { + sess = db.SetSessionPagination(sess, opts.Paginator) + count, err := sess.FindAndCount(&pvs) + return pvs, count, err + } + err := sess.Find(&pvs) + return pvs, int64(len(pvs)), err +} + // SearchVersions gets all versions of packages matching the search options func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*PackageVersion, int64, error) { sess := db.GetEngine(ctx). @@ -289,16 +302,7 @@ func SearchVersions(ctx context.Context, opts *PackageSearchOptions) ([]*Package Table("package_version"). Join("INNER", "package", "package.id = package_version.package_id"). Where(opts.ToConds()) - - opts.configureOrderBy(sess) - - if opts.Paginator != nil { - sess = db.SetSessionPagination(sess, opts) - } - - pvs := make([]*PackageVersion, 0, 10) - count, err := sess.FindAndCount(&pvs) - return pvs, count, err + return searchVersionsBySession(sess, opts) } // SearchLatestVersions gets the latest version of every package matching the search options @@ -316,15 +320,7 @@ func SearchLatestVersions(ctx context.Context, opts *PackageSearchOptions) ([]*P Join("INNER", "package", "package.id = package_version.package_id"). Where(builder.In("package_version.id", in)) - opts.configureOrderBy(sess) - - if opts.Paginator != nil { - sess = db.SetSessionPagination(sess, opts) - } - - pvs := make([]*PackageVersion, 0, 10) - count, err := sess.FindAndCount(&pvs) - return pvs, count, err + return searchVersionsBySession(sess, opts) } // ExistVersion checks if a version matching the search options exist diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index 45efb192c8..7de43ecd07 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -42,6 +42,7 @@ func (p *Permission) IsAdmin() bool { // HasAnyUnitAccess returns true if the user might have at least one access mode to any unit of this repository. // It doesn't count the "public(anonymous/everyone) access mode". +// TODO: most calls to this function should be replaced with `HasAnyUnitAccessOrPublicAccess` func (p *Permission) HasAnyUnitAccess() bool { for _, v := range p.unitsMode { if v >= perm_model.AccessModeRead { @@ -267,7 +268,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use perm.units = repo.Units // anonymous user visit private repo. - // TODO: anonymous user visit public unit of private repo??? if user == nil && repo.IsPrivate { perm.AccessMode = perm_model.AccessModeNone return perm, nil @@ -286,7 +286,8 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } // Prevent strangers from checking out public repo of private organization/users - // Allow user if they are collaborator of a repo within a private user or a private organization but not a member of the organization itself + // Allow user if they are a collaborator of a repo within a private user or a private organization but not a member of the organization itself + // TODO: rename it to "IsOwnerVisibleToDoer" if !organization.HasOrgOrUserVisible(ctx, repo.Owner, user) && !isCollaborator { perm.AccessMode = perm_model.AccessModeNone return perm, nil @@ -304,7 +305,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } - // plain user + // plain user TODO: this check should be replaced, only need to check collaborator access mode perm.AccessMode, err = accessLevel(ctx, user, repo) if err != nil { return perm, err @@ -314,6 +315,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use return perm, nil } + // now: the owner is visible to doer, if the repo is public, then the min access mode is read + minAccessMode := util.Iif(!repo.IsPrivate && !user.IsRestricted, perm_model.AccessModeRead, perm_model.AccessModeNone) + perm.AccessMode = max(perm.AccessMode, minAccessMode) + + // get units mode from teams + teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) + if err != nil { + return perm, err + } + if len(teams) == 0 { + return perm, nil + } + perm.unitsMode = make(map[unit.Type]perm_model.AccessMode) // Collaborators on organization @@ -323,12 +337,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } } - // get units mode from teams - teams, err := organization.GetUserRepoTeams(ctx, repo.OwnerID, user.ID, repo.ID) - if err != nil { - return perm, err - } - // if user in an owner team for _, team := range teams { if team.HasAdminAccess() { @@ -339,19 +347,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use } for _, u := range repo.Units { - var found bool for _, team := range teams { + unitAccessMode := minAccessMode if teamMode, exist := team.UnitAccessModeEx(ctx, u.Type); exist { - perm.unitsMode[u.Type] = max(perm.unitsMode[u.Type], teamMode) - found = true - } - } - - // for a public repo on an organization, a non-restricted user has read permission on non-team defined units. - if !found && !repo.IsPrivate && !user.IsRestricted { - if _, ok := perm.unitsMode[u.Type]; !ok { - perm.unitsMode[u.Type] = perm_model.AccessModeRead + unitAccessMode = max(perm.unitsMode[u.Type], unitAccessMode, teamMode) } + perm.unitsMode[u.Type] = unitAccessMode } } @@ -408,13 +409,13 @@ func IsUserRepoAdmin(ctx context.Context, repo *repo_model.Repository, user *use // AccessLevel returns the Access a user has to a repository. Will return NoneAccess if the // user does not have access. -func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint +func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (perm_model.AccessMode, error) { //nolint:revive // export stutter return AccessLevelUnit(ctx, user, repo, unit.TypeCode) } // AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the // user does not have access. -func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint +func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint:revive // export stutter perm, err := GetUserRepoPermission(ctx, repo, user) if err != nil { return perm_model.AccessModeNone, err diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go index 024f4400b3..c8675b1ded 100644 --- a/models/perm/access/repo_permission_test.go +++ b/models/perm/access/repo_permission_test.go @@ -6,12 +6,16 @@ package access import ( "testing" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" perm_model "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestHasAnyUnitAccess(t *testing.T) { @@ -152,3 +156,45 @@ func TestUnitAccessMode(t *testing.T) { } assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map") } + +func TestGetUserRepoPermission(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo + require.NoError(t, repo32.LoadOwner(ctx)) + require.True(t, repo32.Owner.IsOrganization()) + + require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{})) + org := repo32.Owner + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + team := &organization.Team{OrgID: org.ID, LowerName: "test_team"} + require.NoError(t, db.Insert(ctx, team)) + + t.Run("DoerInTeamWithNoRepo", func(t *testing.T) { + require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID})) + perm, err := GetUserRepoPermission(ctx, repo32, user) + require.NoError(t, err) + assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) + assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo + }) + + require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID})) + require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone})) + t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) { + perm, err := GetUserRepoPermission(ctx, repo32, user) + require.NoError(t, err) + assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) + assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode]) + assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues]) + }) + + require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{})) + require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite})) + t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) { + perm, err := GetUserRepoPermission(ctx, repo32, user) + require.NoError(t, err) + assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) + assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode]) + assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeIssues]) + }) +} diff --git a/models/project/column_test.go b/models/project/column_test.go index 5b93e7760f..6a615090a5 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -110,7 +110,7 @@ func Test_NewColumn(t *testing.T) { assert.NoError(t, err) assert.Len(t, columns, 3) - for i := 0; i < maxProjectColumns-3; i++ { + for i := range maxProjectColumns - 3 { err := NewColumn(db.DefaultContext, &Column{ Title: fmt.Sprintf("column-%d", i+4), ProjectID: project1.ID, diff --git a/models/project/project.go b/models/project/project.go index d27e053094..f516466854 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -129,11 +129,11 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) { return err } -func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint +func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint:revive // export stutter return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID) } -func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint +func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint:revive // export stutter return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID) } diff --git a/models/pull/review_state.go b/models/pull/review_state.go index e46a22a49d..137af00eab 100644 --- a/models/pull/review_state.go +++ b/models/pull/review_state.go @@ -6,6 +6,7 @@ package pull import ( "context" "fmt" + "maps" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" @@ -100,9 +101,7 @@ func mergeFiles(oldFiles, newFiles map[string]ViewedState) map[string]ViewedStat return oldFiles } - for file, viewed := range newFiles { - oldFiles[file] = viewed - } + maps.Copy(oldFiles, newFiles) return oldFiles } diff --git a/models/renderhelper/repo_comment.go b/models/renderhelper/repo_comment.go index 7c40eded44..ae0fbf0abd 100644 --- a/models/renderhelper/repo_comment.go +++ b/models/renderhelper/repo_comment.go @@ -44,30 +44,31 @@ type RepoCommentOptions struct { DeprecatedRepoName string // it is only a patch for the non-standard "markup" api DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api CurrentRefPath string // eg: "branch/main" or "commit/11223344" + FootnoteContextID string // the extra context ID for footnotes, used to avoid conflicts with other footnotes in the same page } func NewRenderContextRepoComment(ctx context.Context, repo *repo_model.Repository, opts ...RepoCommentOptions) *markup.RenderContext { - helper := &RepoComment{ - repoLink: repo.Link(), - opts: util.OptionalArg(opts), - } + helper := &RepoComment{opts: util.OptionalArg(opts)} rctx := markup.NewRenderContext(ctx) helper.ctx = rctx + var metas map[string]string if repo != nil { helper.repoLink = repo.Link() helper.commitChecker = newCommitChecker(ctx, repo) - rctx = rctx.WithMetas(repo.ComposeCommentMetas(ctx)) + metas = repo.ComposeCommentMetas(ctx) } else { - // this is almost dead code, only to pass the incorrect tests - helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) - rctx = rctx.WithMetas(map[string]string{ - "user": helper.opts.DeprecatedOwnerName, - "repo": helper.opts.DeprecatedRepoName, - - "markdownNewLineHardBreak": "true", - "markupAllowShortIssuePattern": "true", - }) + // repo can be nil when rendering a commit message in user's dashboard feedback whose repository has been deleted + metas = map[string]string{} + if helper.opts.DeprecatedOwnerName != "" { + // this is almost dead code, only to pass the incorrect tests + helper.repoLink = fmt.Sprintf("%s/%s", helper.opts.DeprecatedOwnerName, helper.opts.DeprecatedRepoName) + metas["user"] = helper.opts.DeprecatedOwnerName + metas["repo"] = helper.opts.DeprecatedRepoName + } + metas["markdownNewLineHardBreak"] = "true" + metas["markupAllowShortIssuePattern"] = "true" } - rctx = rctx.WithHelper(helper) + metas["footnoteContextId"] = helper.opts.FootnoteContextID + rctx = rctx.WithMetas(metas).WithHelper(helper) return rctx } diff --git a/models/renderhelper/repo_comment_test.go b/models/renderhelper/repo_comment_test.go index 776152db96..3b13bff73c 100644 --- a/models/renderhelper/repo_comment_test.go +++ b/models/renderhelper/repo_comment_test.go @@ -72,4 +72,11 @@ func TestRepoComment(t *testing.T) { <a href="/user2/repo1/commit/1234/image" target="_blank" rel="nofollow noopener"><img src="/user2/repo1/commit/1234/image" alt="./image"/></a></p> `, rendered) }) + + t.Run("NoRepo", func(t *testing.T) { + rctx := NewRenderContextRepoComment(t.Context(), nil).WithMarkupType(markdown.MarkupName) + rendered, err := markup.RenderString(rctx, "any") + assert.NoError(t, err) + assert.Equal(t, "<p>any</p>\n", rendered) + }) } diff --git a/models/repo/pushmirror_test.go b/models/repo/pushmirror_test.go index e19749d93a..9fb7471147 100644 --- a/models/repo/pushmirror_test.go +++ b/models/repo/pushmirror_test.go @@ -39,8 +39,6 @@ func TestPushMirrorsIterate(t *testing.T) { Interval: 0, }) - time.Sleep(1 * time.Millisecond) - repo_model.PushMirrorsIterate(db.DefaultContext, 1, func(idx int, bean any) error { m, ok := bean.(*repo_model.PushMirror) assert.True(t, ok) diff --git a/models/repo/release.go b/models/repo/release.go index 663d310bc0..59f4caf5aa 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -161,6 +161,11 @@ func UpdateRelease(ctx context.Context, rel *Release) error { return err } +func UpdateReleaseNumCommits(ctx context.Context, rel *Release) error { + _, err := db.GetEngine(ctx).ID(rel.ID).Cols("num_commits").Update(rel) + return err +} + // AddReleaseAttachments adds a release attachments func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs []string) (err error) { // Check attachments @@ -175,7 +180,7 @@ func AddReleaseAttachments(ctx context.Context, releaseID int64, attachmentUUIDs } attachments[i].ReleaseID = releaseID // No assign value could be 0, so ignore AllCols(). - if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil { + if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Cols("release_id").Update(attachments[i]); err != nil { return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err) } } @@ -418,8 +423,8 @@ func UpdateReleasesMigrationsByType(ctx context.Context, gitServiceType structs. return err } -// PushUpdateDeleteTagsContext updates a number of delete tags with context -func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []string) error { +// PushUpdateDeleteTags updates a number of delete tags with context +func PushUpdateDeleteTags(ctx context.Context, repo *Repository, tags []string) error { if len(tags) == 0 { return nil } @@ -448,58 +453,6 @@ func PushUpdateDeleteTagsContext(ctx context.Context, repo *Repository, tags []s return nil } -// PushUpdateDeleteTag must be called for any push actions to delete tag -func PushUpdateDeleteTag(ctx context.Context, repo *Repository, tagName string) error { - rel, err := GetRelease(ctx, repo.ID, tagName) - if err != nil { - if IsErrReleaseNotExist(err) { - return nil - } - return fmt.Errorf("GetRelease: %w", err) - } - if rel.IsTag { - if _, err = db.DeleteByID[Release](ctx, rel.ID); err != nil { - return fmt.Errorf("Delete: %w", err) - } - } else { - rel.IsDraft = true - rel.NumCommits = 0 - rel.Sha1 = "" - if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil { - return fmt.Errorf("Update: %w", err) - } - } - - return nil -} - -// SaveOrUpdateTag must be called for any push actions to add tag -func SaveOrUpdateTag(ctx context.Context, repo *Repository, newRel *Release) error { - rel, err := GetRelease(ctx, repo.ID, newRel.TagName) - if err != nil && !IsErrReleaseNotExist(err) { - return fmt.Errorf("GetRelease: %w", err) - } - - if rel == nil { - rel = newRel - if _, err = db.GetEngine(ctx).Insert(rel); err != nil { - return fmt.Errorf("InsertOne: %w", err) - } - } else { - rel.Sha1 = newRel.Sha1 - rel.CreatedUnix = newRel.CreatedUnix - rel.NumCommits = newRel.NumCommits - rel.IsDraft = false - if rel.IsTag && newRel.PublisherID > 0 { - rel.PublisherID = newRel.PublisherID - } - if _, err = db.GetEngine(ctx).ID(rel.ID).AllCols().Update(rel); err != nil { - return fmt.Errorf("Update: %w", err) - } - } - return nil -} - // RemapExternalUser ExternalUserRemappable interface func (r *Release) RemapExternalUser(externalName string, externalID, userID int64) error { r.OriginalAuthor = externalName diff --git a/models/repo/repo.go b/models/repo/repo.go index 5aae02c6d8..34d1bf55f6 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -653,7 +653,7 @@ func (repo *Repository) AllowsPulls(ctx context.Context) bool { // CanEnableEditor returns true if repository meets the requirements of web editor. func (repo *Repository) CanEnableEditor() bool { - return !repo.IsMirror + return !repo.IsMirror && !repo.IsArchived } // DescriptionHTML does special handles to description and return HTML string. diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 02c228e8a0..f2cdd2f284 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -359,7 +359,7 @@ func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond { } // SearchRepositoryCondition creates a query condition according search repository options -func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { +func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond { cond := builder.NewCond() if opts.Private { @@ -449,7 +449,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { if opts.Keyword != "" { // separate keyword subQueryCond := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { + for v := range strings.SplitSeq(opts.Keyword, ",") { if opts.TopicOnly { subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)}) } else { @@ -464,7 +464,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { keywordCond := builder.In("id", subQuery) if !opts.TopicOnly { likes := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { + for v := range strings.SplitSeq(opts.Keyword, ",") { likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)}) // If the string looks like "org/repo", match against that pattern too @@ -551,18 +551,18 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { // SearchRepository returns repositories based on search options, // it returns results in given range and number of total results. -func SearchRepository(ctx context.Context, opts *SearchRepoOptions) (RepositoryList, int64, error) { +func SearchRepository(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) { cond := SearchRepositoryCondition(opts) return SearchRepositoryByCondition(ctx, opts, cond, true) } // CountRepository counts repositories based on search options, -func CountRepository(ctx context.Context, opts *SearchRepoOptions) (int64, error) { +func CountRepository(ctx context.Context, opts SearchRepoOptions) (int64, error) { return db.GetEngine(ctx).Where(SearchRepositoryCondition(opts)).Count(new(Repository)) } // SearchRepositoryByCondition search repositories by condition -func SearchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) { +func SearchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, cond builder.Cond, loadAttributes bool) (RepositoryList, int64, error) { sess, count, err := searchRepositoryByCondition(ctx, opts, cond) if err != nil { return nil, 0, err @@ -590,23 +590,25 @@ func SearchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, c return repos, count, nil } -func searchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) { - if opts.Page <= 0 { - opts.Page = 1 +func searchRepositoryByCondition(ctx context.Context, opts SearchRepoOptions, cond builder.Cond) (db.Engine, int64, error) { + page := opts.Page + if page <= 0 { + page = 1 } - if len(opts.OrderBy) == 0 { - opts.OrderBy = db.SearchOrderByAlphabetically + orderBy := opts.OrderBy + if len(orderBy) == 0 { + orderBy = db.SearchOrderByAlphabetically } args := make([]any, 0) if opts.PriorityOwnerID > 0 { - opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", opts.OrderBy)) + orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_id = ? THEN 0 ELSE owner_id END, %s", orderBy)) args = append(args, opts.PriorityOwnerID) } else if strings.Count(opts.Keyword, "/") == 1 { // With "owner/repo" search times, prioritise results which match the owner field orgName := strings.Split(opts.Keyword, "/")[0] - opts.OrderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", opts.OrderBy)) + orderBy = db.SearchOrderBy(fmt.Sprintf("CASE WHEN owner_name LIKE ? THEN 0 ELSE 1 END, %s", orderBy)) args = append(args, orgName) } @@ -623,9 +625,9 @@ func searchRepositoryByCondition(ctx context.Context, opts *SearchRepoOptions, c } } - sess = sess.Where(cond).OrderBy(opts.OrderBy.String(), args...) + sess = sess.Where(cond).OrderBy(orderBy.String(), args...) if opts.PageSize > 0 { - sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + sess = sess.Limit(opts.PageSize, (page-1)*opts.PageSize) } return sess, count, nil } @@ -689,14 +691,14 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu // SearchRepositoryByName takes keyword and part of repository name to search, // it returns results in given range and number of total results. -func SearchRepositoryByName(ctx context.Context, opts *SearchRepoOptions) (RepositoryList, int64, error) { +func SearchRepositoryByName(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) { opts.IncludeDescription = false return SearchRepository(ctx, opts) } // SearchRepositoryIDs takes keyword and part of repository name to search, // it returns results in given range and number of total results. -func SearchRepositoryIDs(ctx context.Context, opts *SearchRepoOptions) ([]int64, int64, error) { +func SearchRepositoryIDs(ctx context.Context, opts SearchRepoOptions) ([]int64, int64, error) { opts.IncludeDescription = false cond := SearchRepositoryCondition(opts) @@ -740,7 +742,7 @@ func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user } // GetUserRepositories returns a list of repositories of given user. -func GetUserRepositories(ctx context.Context, opts *SearchRepoOptions) (RepositoryList, int64, error) { +func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (RepositoryList, int64, error) { if len(opts.OrderBy) == 0 { opts.OrderBy = "updated_unix DESC" } @@ -767,5 +769,5 @@ func GetUserRepositories(ctx context.Context, opts *SearchRepoOptions) (Reposito sess = sess.Where(cond).OrderBy(opts.OrderBy.String()) repos := make(RepositoryList, 0, opts.PageSize) - return repos, count, db.SetSessionPagination(sess, opts).Find(&repos) + return repos, count, db.SetSessionPagination(sess, &opts).Find(&repos) } diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index ca6007f6c7..7eb76416c2 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -17,162 +17,162 @@ import ( func getTestCases() []struct { name string - opts *repo_model.SearchRepoOptions + opts repo_model.SearchRepoOptions count int } { testCases := []struct { name string - opts *repo_model.SearchRepoOptions + opts repo_model.SearchRepoOptions count int }{ { name: "PublicRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, Collaborate: optional.Some(false)}, count: 7, }, { name: "PublicAndPrivateRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFirstPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitSecondPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 2, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitThirdPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicAndPrivateRepositoriesByNameWithPagesizeLimitFourthPage", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 3, PageSize: 5}, Private: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "PublicRepositoriesOfUser", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Collaborate: optional.Some(false)}, count: 2, }, { name: "PublicRepositoriesOfUser2", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Collaborate: optional.Some(false)}, count: 0, }, { name: "PublicRepositoriesOfOrg3", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Collaborate: optional.Some(false)}, count: 2, }, { name: "PublicAndPrivateRepositoriesOfUser", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, Collaborate: optional.Some(false)}, count: 4, }, { name: "PublicAndPrivateRepositoriesOfUser2", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, Collaborate: optional.Some(false)}, count: 0, }, { name: "PublicAndPrivateRepositoriesOfOrg3", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true, Collaborate: optional.Some(false)}, count: 4, }, { name: "PublicRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15}, count: 5, }, { name: "PublicRepositoriesOfUser2IncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18}, count: 1, }, { name: "PublicRepositoriesOfOrg3IncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20}, count: 3, }, { name: "PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true}, count: 9, }, { name: "PublicAndPrivateRepositoriesOfUser2IncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true}, count: 4, }, { name: "PublicAndPrivateRepositoriesOfOrg3IncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 20, Private: true}, count: 7, }, { name: "PublicRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Collaborate: optional.Some(false)}, count: 1, }, { name: "PublicAndPrivateRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, Private: true, Collaborate: optional.Some(false)}, count: 2, }, { name: "AllPublic/PublicRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{PageSize: 10}, AllPublic: true, Collaborate: optional.Some(false)}, count: 7, }, { name: "AllPublic/PublicAndPrivateRepositoriesByName", - opts: &repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{Keyword: "big_test_", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, AllPublic: true, Collaborate: optional.Some(false)}, count: 14, }, { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)}, count: 34, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)}, count: 39, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", - opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true}, + opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true}, count: 15, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName", - opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true}, + opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true}, count: 13, }, { name: "AllPublic/PublicRepositoriesOfOrganization", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)}, count: 34, }, { name: "AllTemplates", - opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)}, + opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Template: optional.Some(true)}, count: 2, }, { name: "OwnerSlashRepoSearch", - opts: &repo_model.SearchRepoOptions{Keyword: "user/repo2", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, + opts: repo_model.SearchRepoOptions{Keyword: "user/repo2", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, count: 2, }, { name: "OwnerSlashSearch", - opts: &repo_model.SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, + opts: repo_model.SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, count: 4, }, } @@ -184,7 +184,7 @@ func TestSearchRepository(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) // test search public repository on explore page - repos, count, err := repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err := repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -199,7 +199,7 @@ func TestSearchRepository(t *testing.T) { } assert.Equal(t, int64(1), count) - repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -213,7 +213,7 @@ func TestSearchRepository(t *testing.T) { assert.Len(t, repos, 2) // test search private repository on explore page - repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -229,7 +229,7 @@ func TestSearchRepository(t *testing.T) { } assert.Equal(t, int64(1), count) - repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -244,14 +244,14 @@ func TestSearchRepository(t *testing.T) { assert.Len(t, repos, 3) // Test non existing owner - repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, &repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) + repos, count, err = repo_model.SearchRepositoryByName(db.DefaultContext, repo_model.SearchRepoOptions{OwnerID: unittest.NonexistentID}) assert.NoError(t, err) assert.Empty(t, repos) assert.Equal(t, int64(0), count) // Test search within description - repos, count, err = repo_model.SearchRepository(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -268,7 +268,7 @@ func TestSearchRepository(t *testing.T) { assert.Equal(t, int64(1), count) // Test NOT search within description - repos, count, err = repo_model.SearchRepository(db.DefaultContext, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(db.DefaultContext, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: 1, PageSize: 10, @@ -374,22 +374,22 @@ func TestSearchRepositoryByTopicName(t *testing.T) { testCases := []struct { name string - opts *repo_model.SearchRepoOptions + opts repo_model.SearchRepoOptions count int }{ { name: "AllPublic/SearchPublicRepositoriesFromTopicAndName", - opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"}, + opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql"}, count: 2, }, { name: "AllPublic/OnlySearchPublicRepositoriesFromTopic", - opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true}, + opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql", TopicOnly: true}, count: 1, }, { name: "AllPublic/OnlySearchMultipleKeywordPublicRepositoriesFromTopic", - opts: &repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql,golang", TopicOnly: true}, + opts: repo_model.SearchRepoOptions{OwnerID: 21, AllPublic: true, Keyword: "graphql,golang", TopicOnly: true}, count: 2, }, } diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index 8a7dbfe340..a5207bc22a 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -185,10 +185,8 @@ func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { } func (cfg *ActionsConfig) DisableWorkflow(file string) { - for _, workflow := range cfg.DisabledWorkflows { - if file == workflow { - return - } + if slices.Contains(cfg.DisabledWorkflows, file) { + return } cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) diff --git a/models/repo/transfer.go b/models/repo/transfer.go index b4a3592cbc..3fb8cb69ab 100644 --- a/models/repo/transfer.go +++ b/models/repo/transfer.go @@ -61,7 +61,7 @@ func (err ErrRepoTransferInProgress) Unwrap() error { } // RepoTransfer is used to manage repository transfers -type RepoTransfer struct { //nolint +type RepoTransfer struct { //nolint:revive // export stutter ID int64 `xorm:"pk autoincr"` DoerID int64 Doer *user_model.User `xorm:"-"` diff --git a/models/repo/update.go b/models/repo/update.go index 8a15477a80..64065f11c4 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -40,15 +40,15 @@ func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime t return err } -// UpdateRepositoryColsWithAutoTime updates repository's columns -func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, cols ...string) error { - _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).Update(repo) +// UpdateRepositoryColsWithAutoTime updates repository's columns and the timestamp fields automatically +func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, colName string, moreColNames ...string) error { + _, err := db.GetEngine(ctx).ID(repo.ID).Cols(append([]string{colName}, moreColNames...)...).Update(repo) return err } -// UpdateRepositoryColsNoAutoTime updates repository's columns and but applies time change automatically -func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, cols ...string) error { - _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).NoAutoTime().Update(repo) +// UpdateRepositoryColsNoAutoTime updates repository's columns, doesn't change timestamp field automatically +func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, colName string, moreColNames ...string) error { + _, err := db.GetEngine(ctx).ID(repo.ID).Cols(append([]string{colName}, moreColNames...)...).NoAutoTime().Update(repo) return err } diff --git a/models/repo/upload.go b/models/repo/upload.go index fb57fb6c51..20a8fa26fe 100644 --- a/models/repo/upload.go +++ b/models/repo/upload.go @@ -124,7 +124,7 @@ func DeleteUploads(ctx context.Context, uploads ...*Upload) (err error) { defer committer.Close() ids := make([]int64, len(uploads)) - for i := 0; i < len(uploads); i++ { + for i := range uploads { ids[i] = uploads[i].ID } if err = db.DeleteByIDs[Upload](ctx, ids...); err != nil { diff --git a/models/unit/unit.go b/models/unit/unit.go index 4ca676802f..c0560678ca 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -6,6 +6,7 @@ package unit import ( "errors" "fmt" + "slices" "strings" "sync/atomic" @@ -204,22 +205,12 @@ func LoadUnitConfig() error { // UnitGlobalDisabled checks if unit type is global disabled func (u Type) UnitGlobalDisabled() bool { - for _, ud := range DisabledRepoUnitsGet() { - if u == ud { - return true - } - } - return false + return slices.Contains(DisabledRepoUnitsGet(), u) } // CanBeDefault checks if the unit type can be a default repo unit func (u *Type) CanBeDefault() bool { - for _, nadU := range NotAllowedDefaultRepoUnits { - if *u == nadU { - return false - } - } - return true + return !slices.Contains(NotAllowedDefaultRepoUnits, *u) } // Unit is a section of one repository diff --git a/models/user/avatar.go b/models/user/avatar.go index 3d9fc4452f..542bd93b98 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -5,7 +5,6 @@ package user import ( "context" - "crypto/md5" "fmt" "image/png" "io" @@ -106,7 +105,7 @@ func (u *User) IsUploadAvatarChanged(data []byte) bool { if !u.UseCustomAvatar || len(u.Avatar) == 0 { return true } - avatarID := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data))))) + avatarID := avatar.HashAvatar(u.ID, data) return u.Avatar != avatarID } diff --git a/models/user/badge.go b/models/user/badge.go index 3ff3530a36..e475ceb748 100644 --- a/models/user/badge.go +++ b/models/user/badge.go @@ -19,7 +19,7 @@ type Badge struct { } // UserBadge represents a user badge -type UserBadge struct { //nolint:revive +type UserBadge struct { //nolint:revive // export stutter ID int64 `xorm:"pk autoincr"` BadgeID int64 UserID int64 `xorm:"INDEX"` diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index 0e52950cfd..c0666246b0 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -4,6 +4,7 @@ package user_test import ( + "slices" "testing" "code.gitea.io/gitea/models/db" @@ -100,12 +101,7 @@ func TestListEmails(t *testing.T) { assert.Greater(t, count, int64(5)) contains := func(match func(s *user_model.SearchEmailResult) bool) bool { - for _, v := range emails { - if match(v) { - return true - } - } - return false + return slices.ContainsFunc(emails, match) } assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 18 })) diff --git a/models/user/search.go b/models/user/search.go index f4436be09a..cfd0d011bc 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -137,7 +137,7 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess // SearchUsers takes options i.e. keyword and part of user name to search, // it returns results in given range and number of total results. -func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _ int64, _ error) { +func SearchUsers(ctx context.Context, opts SearchUserOptions) (users []*User, _ int64, _ error) { sessCount := opts.toSearchQueryBase(ctx) defer sessCount.Close() count, err := sessCount.Count(new(User)) @@ -152,7 +152,7 @@ func SearchUsers(ctx context.Context, opts *SearchUserOptions) (users []*User, _ sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String()) defer sessQuery.Close() if opts.Page > 0 { - sessQuery = db.SetSessionPagination(sessQuery, opts) + sessQuery = db.SetSessionPagination(sessQuery, &opts) } // the sql may contain JOIN, so we must only select User related columns diff --git a/models/user/user.go b/models/user/user.go index d7331d79f0..7c871bf575 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -831,6 +831,20 @@ type CountUserFilter struct { IsActive optional.Option[bool] } +// HasUsers checks whether there are any users in the database, or only one user exists. +func HasUsers(ctx context.Context) (ret struct { + HasAnyUser, HasOnlyOneUser bool +}, err error, +) { + res, err := db.GetEngine(ctx).Table(&User{}).Cols("id").Limit(2).Query() + if err != nil { + return ret, fmt.Errorf("error checking user existence: %w", err) + } + ret.HasAnyUser = len(res) != 0 + ret.HasOnlyOneUser = len(res) == 1 + return ret, nil +} + // CountUsers returns number of users. func CountUsers(ctx context.Context, opts *CountUserFilter) int64 { return countUsers(ctx, opts) @@ -1151,8 +1165,8 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([ } for _, c := range oldCommits { - user, ok := emailUserMap[c.Author.Email] - if !ok { + user := emailUserMap.GetByEmail(c.Author.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? + if user == nil { user = &User{ Name: c.Author.Name, Email: c.Author.Email, @@ -1166,7 +1180,15 @@ func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) ([ return newCommits, nil } -func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, error) { +type EmailUserMap struct { + m map[string]*User +} + +func (eum *EmailUserMap) GetByEmail(email string) *User { + return eum.m[strings.ToLower(email)] +} + +func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) { if len(emails) == 0 { return nil, nil } @@ -1176,7 +1198,7 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e for _, email := range emails { if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) { username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress) - needCheckUserNames.Add(username) + needCheckUserNames.Add(strings.ToLower(username)) } else { needCheckEmails.Add(strings.ToLower(email)) } @@ -1203,8 +1225,7 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e for _, email := range emailAddresses { user := users[email.UID] if user != nil { - results[user.Email] = user - results[user.GetPlaceholderEmail()] = user + results[email.LowerEmail] = user } } } @@ -1214,10 +1235,9 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e return nil, err } for _, user := range users { - results[user.Email] = user - results[user.GetPlaceholderEmail()] = user + results[strings.ToLower(user.GetPlaceholderEmail())] = user } - return results, nil + return &EmailUserMap{results}, nil } // GetUserByEmail returns the user object by given e-mail if exists. diff --git a/models/user/user_list.go b/models/user/user_list.go index 4241905058..1b6a27dd86 100644 --- a/models/user/user_list.go +++ b/models/user/user_list.go @@ -17,10 +17,7 @@ func GetUsersMapByIDs(ctx context.Context, userIDs []int64) (map[int64]*User, er left := len(userIDs) for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + limit := min(left, db.DefaultMaxInSize) err := db.GetEngine(ctx). In("id", userIDs[:limit]). Find(&userMaps) diff --git a/models/user/user_test.go b/models/user/user_test.go index dd232abe2e..a2597ba3f5 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -58,13 +58,33 @@ func TestUserEmails(t *testing.T) { assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"})) }) t.Run("GetUsersByEmails", func(t *testing.T) { - m, err := user_model.GetUsersByEmails(db.DefaultContext, []string{"user1@example.com", "user2@" + setting.Service.NoReplyAddress}) - require.NoError(t, err) - require.Len(t, m, 4) - assert.EqualValues(t, 1, m["user1@example.com"].ID) - assert.EqualValues(t, 1, m["user1@"+setting.Service.NoReplyAddress].ID) - assert.EqualValues(t, 2, m["user2@example.com"].ID) - assert.EqualValues(t, 2, m["user2@"+setting.Service.NoReplyAddress].ID) + defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")() + testGetUserByEmail := func(t *testing.T, email string, uid int64) { + m, err := user_model.GetUsersByEmails(db.DefaultContext, []string{email}) + require.NoError(t, err) + user := m.GetByEmail(email) + if uid == 0 { + require.Nil(t, user) + return + } + require.NotNil(t, user) + assert.Equal(t, uid, user.ID) + } + cases := []struct { + Email string + UID int64 + }{ + {"UseR1@example.com", 1}, + {"user1-2@example.COM", 1}, + {"USER2@" + setting.Service.NoReplyAddress, 2}, + {"user4@example.com", 4}, + {"no-such", 0}, + } + for _, c := range cases { + t.Run(c.Email, func(t *testing.T) { + testGetUserByEmail(t, c.Email, c.UID) + }) + } }) } @@ -88,7 +108,7 @@ func TestCanCreateOrganization(t *testing.T) { func TestSearchUsers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - testSuccess := func(opts *user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) { + testSuccess := func(opts user_model.SearchUserOptions, expectedUserOrOrgIDs []int64) { users, _, err := user_model.SearchUsers(db.DefaultContext, opts) assert.NoError(t, err) cassText := fmt.Sprintf("ids: %v, opts: %v", expectedUserOrOrgIDs, opts) @@ -100,61 +120,61 @@ func TestSearchUsers(t *testing.T) { } // test orgs - testOrgSuccess := func(opts *user_model.SearchUserOptions, expectedOrgIDs []int64) { + testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) { opts.Type = user_model.UserTypeOrganization testSuccess(opts, expectedOrgIDs) } - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1, PageSize: 2}}, []int64{3, 6}) - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 2, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 2, PageSize: 2}}, []int64{7, 17}) - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 3, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 3, PageSize: 2}}, []int64{19, 25}) - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 4, PageSize: 2}}, []int64{26, 41}) - testOrgSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 5, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 5, PageSize: 2}}, []int64{42}) - testOrgSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 6, PageSize: 2}}, + testOrgSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 6, PageSize: 2}}, []int64{}) // test users - testUserSuccess := func(opts *user_model.SearchUserOptions, expectedUserIDs []int64) { + testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) { opts.Type = user_model.UserTypeIndividual testSuccess(opts, expectedUserIDs) } - testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}}, + testUserSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}}, []int64{1, 2, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(false)}, []int64{9}) - testUserSuccess(&user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 2, 4, 5, 8, 10, 11, 12, 13, 14, 15, 16, 18, 20, 21, 24, 27, 28, 29, 30, 32, 34, 37, 38, 39, 40}) - testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{Keyword: "user1", OrderBy: "id ASC", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) // order by name asc default - testUserSuccess(&user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{Keyword: "user1", ListOptions: db.ListOptions{Page: 1}, IsActive: optional.Some(true)}, []int64{1, 10, 11, 12, 13, 14, 15, 16, 18}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsAdmin: optional.Some(true)}, []int64{1}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsRestricted: optional.Some(true)}, []int64{29}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsProhibitLogin: optional.Some(true)}, []int64{37}) - testUserSuccess(&user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)}, + testUserSuccess(user_model.SearchUserOptions{ListOptions: db.ListOptions{Page: 1}, IsTwoFactorEnabled: optional.Some(true)}, []int64{24}) } @@ -184,9 +204,9 @@ func TestHashPasswordDeterministic(t *testing.T) { b := make([]byte, 16) u := &user_model.User{} algos := hash.RecommendedHashAlgorithms - for j := 0; j < len(algos); j++ { + for j := range algos { u.PasswdHashAlgo = algos[j] - for i := 0; i < 50; i++ { + for range 50 { // generate a random password rand.Read(b) pass := string(b) @@ -513,11 +533,8 @@ func TestIsUserVisibleToViewer(t *testing.T) { } func Test_ValidateUser(t *testing.T) { - oldSetting := setting.Service.AllowedUserVisibilityModesSlice - defer func() { - setting.Service.AllowedUserVisibilityModesSlice = oldSetting - }() - setting.Service.AllowedUserVisibilityModesSlice = []bool{true, false, true} + defer test.MockVariableValue(&setting.Service.AllowedUserVisibilityModesSlice, []bool{true, false, true})() + kases := map[*user_model.User]bool{ {ID: 1, Visibility: structs.VisibleTypePublic}: true, {ID: 2, Visibility: structs.VisibleTypeLimited}: false, @@ -586,12 +603,7 @@ func TestDisabledUserFeatures(t *testing.T) { testValues := container.SetOf(setting.UserFeatureDeletion, setting.UserFeatureManageSSHKeys, setting.UserFeatureManageGPGKeys) - - oldSetting := setting.Admin.ExternalUserDisableFeatures - defer func() { - setting.Admin.ExternalUserDisableFeatures = oldSetting - }() - setting.Admin.ExternalUserDisableFeatures = testValues + defer test.MockVariableValue(&setting.Admin.ExternalUserDisableFeatures, testValues)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 97ad373027..b234d9ffee 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -240,7 +240,7 @@ func CreateWebhooks(ctx context.Context, ws []*Webhook) error { if len(ws) == 0 { return nil } - for i := 0; i < len(ws); i++ { + for i := range ws { ws[i].Type = strings.TrimSpace(ws[i].Type) } return db.Insert(ctx, ws) diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index e8a2547c65..edad8fc996 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -73,7 +73,7 @@ func TestWebhook_EventsArray(t *testing.T) { "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", "pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release", - "package", "status", "workflow_job", + "package", "status", "workflow_run", "workflow_job", }, (&Webhook{ HookEvent: &webhook_module.HookEvent{SendEverything: true}, diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index a538b6e290..27bcafa649 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -6,6 +6,7 @@ package actions import ( "bytes" "io" + "slices" "strings" "code.gitea.io/gitea/modules/git" @@ -43,21 +44,23 @@ func IsWorkflow(path string) bool { return strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows") } -func ListWorkflows(commit *git.Commit) (git.Entries, error) { - tree, err := commit.SubTree(".gitea/workflows") +func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { + rpath := ".gitea/workflows" + tree, err := commit.SubTree(rpath) if _, ok := err.(git.ErrNotExist); ok { - tree, err = commit.SubTree(".github/workflows") + rpath = ".github/workflows" + tree, err = commit.SubTree(rpath) } if _, ok := err.(git.ErrNotExist); ok { - return nil, nil + return "", nil, nil } if err != nil { - return nil, err + return "", nil, err } entries, err := tree.ListEntriesRecursiveFast() if err != nil { - return nil, err + return "", nil, err } ret := make(git.Entries, 0, len(entries)) @@ -66,7 +69,7 @@ func ListWorkflows(commit *git.Commit) (git.Entries, error) { ret = append(ret, entry) } } - return ret, nil + return rpath, ret, nil } func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { @@ -102,7 +105,7 @@ func DetectWorkflows( payload api.Payloader, detectSchedule bool, ) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { - entries, err := ListWorkflows(commit) + _, entries, err := ListWorkflows(commit) if err != nil { return nil, nil, err } @@ -147,7 +150,7 @@ func DetectWorkflows( } func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) { - entries, err := ListWorkflows(commit) + _, entries, err := ListWorkflows(commit) if err != nil { return nil, err } @@ -243,6 +246,10 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventPackage: return matchPackageEvent(payload.(*api.PackagePayload), evt) + case // workflow_run + webhook_module.HookEventWorkflowRun: + return matchWorkflowRunEvent(payload.(*api.WorkflowRunPayload), evt) + default: log.Warn("unsupported event %q", triggedEvent) return false @@ -311,6 +318,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa matchTimes++ } case "paths": + if refName.IsTag() { + matchTimes++ + break + } filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) @@ -324,6 +335,10 @@ func matchPushEvent(commit *git.Commit, pushPayload *api.PushPayload, evt *jobpa } } case "paths-ignore": + if refName.IsTag() { + matchTimes++ + break + } filesChanged, err := commit.GetFilesChangedSinceCommit(pushPayload.Before) if err != nil { log.Error("GetFilesChangedSinceCommit [commit_sha1: %s]: %v", commit.ID.String(), err) @@ -554,21 +569,12 @@ func matchPullRequestReviewEvent(prPayload *api.PullRequestPayload, evt *jobpars actions = append(actions, "submitted", "edited") } - matched := false for _, val := range vals { - for _, action := range actions { - if glob.MustCompile(val, '/').Match(action) { - matched = true - break - } - } - if matched { + if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) { + matchTimes++ break } } - if matched { - matchTimes++ - } default: log.Warn("pull request review event unsupported condition %q", cond) } @@ -603,21 +609,12 @@ func matchPullRequestReviewCommentEvent(prPayload *api.PullRequestPayload, evt * actions = append(actions, "created", "edited") } - matched := false for _, val := range vals { - for _, action := range actions { - if glob.MustCompile(val, '/').Match(action) { - matched = true - break - } - } - if matched { + if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) { + matchTimes++ break } } - if matched { - matchTimes++ - } default: log.Warn("pull request review comment event unsupported condition %q", cond) } @@ -698,3 +695,53 @@ func matchPackageEvent(payload *api.PackagePayload, evt *jobparser.Event) bool { } return matchTimes == len(evt.Acts()) } + +func matchWorkflowRunEvent(payload *api.WorkflowRunPayload, evt *jobparser.Event) bool { + // with no special filter parameters + if len(evt.Acts()) == 0 { + return true + } + + matchTimes := 0 + // all acts conditions should be satisfied + for cond, vals := range evt.Acts() { + switch cond { + case "types": + action := payload.Action + for _, val := range vals { + if glob.MustCompile(val, '/').Match(action) { + matchTimes++ + break + } + } + case "workflows": + workflow := payload.Workflow + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, []string{workflow.Name}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "branches": + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Skip(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + case "branches-ignore": + patterns, err := workflowpattern.CompilePatterns(vals...) + if err != nil { + break + } + if !workflowpattern.Filter(patterns, []string{payload.WorkflowRun.HeadBranch}, &workflowpattern.EmptyTraceWriter{}) { + matchTimes++ + } + default: + log.Warn("workflow run event unsupported condition %q", cond) + } + } + return matchTimes == len(evt.Acts()) +} diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index c8e1e553fe..e23431651d 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -125,6 +125,24 @@ func TestDetectMatched(t *testing.T) { yamlOn: "on: schedule", expected: true, }, + { + desc: "push to tag matches workflow with paths condition (should skip paths check)", + triggedEvent: webhook_module.HookEventPush, + payload: &api.PushPayload{ + Ref: "refs/tags/v1.0.0", + Before: "0000000", + Commits: []*api.PayloadCommit{ + { + ID: "abcdef123456", + Added: []string{"src/main.go"}, + Message: "Release v1.0.0", + }, + }, + }, + commit: nil, + yamlOn: "on:\n push:\n paths:\n - src/**", + expected: true, + }, } for _, tc := range testCases { diff --git a/modules/assetfs/embed.go b/modules/assetfs/embed.go new file mode 100644 index 0000000000..95176372d1 --- /dev/null +++ b/modules/assetfs/embed.go @@ -0,0 +1,375 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package assetfs + +import ( + "bytes" + "compress/gzip" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "strings" + "sync" + "time" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" +) + +type EmbeddedFile interface { + io.ReadSeeker + fs.ReadDirFile + ReadDir(n int) ([]fs.DirEntry, error) +} + +type EmbeddedFileInfo interface { + fs.FileInfo + fs.DirEntry + GetGzipContent() ([]byte, bool) +} + +type decompressor interface { + io.Reader + Close() error + Reset(io.Reader) error +} + +type embeddedFileInfo struct { + fs *embeddedFS + fullName string + data []byte + + BaseName string `json:"n"` + OriginSize int64 `json:"s,omitempty"` + DataBegin int64 `json:"b,omitempty"` + DataLen int64 `json:"l,omitempty"` + Children []*embeddedFileInfo `json:"c,omitempty"` +} + +func (fi *embeddedFileInfo) GetGzipContent() ([]byte, bool) { + // when generating the bindata, if the compressed data equals or is larger than the original data, we store the original data + if fi.DataLen == fi.OriginSize { + return nil, false + } + return fi.data, true +} + +type EmbeddedFileBase struct { + info *embeddedFileInfo + dataReader io.ReadSeeker + seekPos int64 +} + +func (f *EmbeddedFileBase) ReadDir(n int) ([]fs.DirEntry, error) { + // this method is used to satisfy the "func (f ioFile) ReadDir(...)" in httpfs + l, err := f.info.fs.ReadDir(f.info.fullName) + if err != nil { + return nil, err + } + if n < 0 || n > len(l) { + return l, nil + } + return l[:n], nil +} + +type EmbeddedOriginFile struct { + EmbeddedFileBase +} + +type EmbeddedCompressedFile struct { + EmbeddedFileBase + decompressor decompressor + decompressorPos int64 +} + +type embeddedFS struct { + meta func() *EmbeddedMeta + + files map[string]*embeddedFileInfo + filesMu sync.RWMutex + + data []byte +} + +type EmbeddedMeta struct { + Root *embeddedFileInfo +} + +func NewEmbeddedFS(data []byte) fs.ReadDirFS { + efs := &embeddedFS{data: data, files: make(map[string]*embeddedFileInfo)} + efs.meta = sync.OnceValue(func() *EmbeddedMeta { + var meta EmbeddedMeta + p := bytes.LastIndexByte(data, '\n') + if p < 0 { + return &meta + } + if err := json.Unmarshal(data[p+1:], &meta); err != nil { + panic("embedded file is not valid") + } + return &meta + }) + return efs +} + +var _ fs.ReadDirFS = (*embeddedFS)(nil) + +func (e *embeddedFS) ReadDir(name string) (l []fs.DirEntry, err error) { + fi, err := e.getFileInfo(name) + if err != nil { + return nil, err + } + if !fi.IsDir() { + return nil, fs.ErrNotExist + } + l = make([]fs.DirEntry, len(fi.Children)) + for i, child := range fi.Children { + l[i], err = e.getFileInfo(name + "/" + child.BaseName) + if err != nil { + return nil, err + } + } + return l, nil +} + +func (e *embeddedFS) getFileInfo(fullName string) (*embeddedFileInfo, error) { + // no need to do heavy "path.Clean()" because we don't want to support "foo/../bar" or absolute paths + fullName = strings.TrimPrefix(fullName, "./") + if fullName == "" { + fullName = "." + } + + e.filesMu.RLock() + fi := e.files[fullName] + e.filesMu.RUnlock() + if fi != nil { + return fi, nil + } + + fields := strings.Split(fullName, "/") + fi = e.meta().Root + if fullName != "." { + found := true + for _, field := range fields { + for _, child := range fi.Children { + if found = child.BaseName == field; found { + fi = child + break + } + } + if !found { + return nil, fs.ErrNotExist + } + } + } + + e.filesMu.Lock() + defer e.filesMu.Unlock() + if fi != nil { + fi.fs = e + fi.fullName = fullName + fi.data = e.data[fi.DataBegin : fi.DataBegin+fi.DataLen] + e.files[fullName] = fi // do not cache nil, otherwise keeping accessing random non-existing file will cause OOM + return fi, nil + } + return nil, fs.ErrNotExist +} + +func (e *embeddedFS) Open(name string) (fs.File, error) { + info, err := e.getFileInfo(name) + if err != nil { + return nil, err + } + base := EmbeddedFileBase{info: info} + base.dataReader = bytes.NewReader(base.info.data) + if info.DataLen != info.OriginSize { + decomp, err := gzip.NewReader(base.dataReader) + if err != nil { + return nil, err + } + return &EmbeddedCompressedFile{EmbeddedFileBase: base, decompressor: decomp}, nil + } + return &EmbeddedOriginFile{base}, nil +} + +var ( + _ EmbeddedFileInfo = (*embeddedFileInfo)(nil) + _ EmbeddedFile = (*EmbeddedOriginFile)(nil) + _ EmbeddedFile = (*EmbeddedCompressedFile)(nil) +) + +func (f *EmbeddedOriginFile) Read(p []byte) (n int, err error) { + return f.dataReader.Read(p) +} + +func (f *EmbeddedCompressedFile) Read(p []byte) (n int, err error) { + if f.decompressorPos > f.seekPos { + if err = f.decompressor.Reset(bytes.NewReader(f.info.data)); err != nil { + return 0, err + } + f.decompressorPos = 0 + } + if f.decompressorPos < f.seekPos { + if _, err = io.CopyN(io.Discard, f.decompressor, f.seekPos-f.decompressorPos); err != nil { + return 0, err + } + f.decompressorPos = f.seekPos + } + n, err = f.decompressor.Read(p) + f.decompressorPos += int64(n) + f.seekPos = f.decompressorPos + return n, err +} + +func (f *EmbeddedFileBase) Seek(offset int64, whence int) (int64, error) { + switch whence { + case io.SeekStart: + f.seekPos = offset + case io.SeekCurrent: + f.seekPos += offset + case io.SeekEnd: + f.seekPos = f.info.OriginSize + offset + } + return f.seekPos, nil +} + +func (f *EmbeddedFileBase) Stat() (fs.FileInfo, error) { + return f.info, nil +} + +func (f *EmbeddedOriginFile) Close() error { + return nil +} + +func (f *EmbeddedCompressedFile) Close() error { + return f.decompressor.Close() +} + +func (fi *embeddedFileInfo) Name() string { + return fi.BaseName +} + +func (fi *embeddedFileInfo) Size() int64 { + return fi.OriginSize +} + +func (fi *embeddedFileInfo) Mode() fs.FileMode { + return util.Iif(fi.IsDir(), fs.ModeDir|0o555, 0o444) +} + +func (fi *embeddedFileInfo) ModTime() time.Time { + return getExecutableModTime() +} + +func (fi *embeddedFileInfo) IsDir() bool { + return fi.Children != nil +} + +func (fi *embeddedFileInfo) Sys() any { + return nil +} + +func (fi *embeddedFileInfo) Type() fs.FileMode { + return util.Iif(fi.IsDir(), fs.ModeDir, 0) +} + +func (fi *embeddedFileInfo) Info() (fs.FileInfo, error) { + return fi, nil +} + +// getExecutableModTime returns the modification time of the executable file. +// In bindata, we can't use the ModTime of the files because we need to make the build reproducible +var getExecutableModTime = sync.OnceValue(func() (modTime time.Time) { + exePath, err := os.Executable() + if err != nil { + return modTime + } + exePath, err = filepath.Abs(exePath) + if err != nil { + return modTime + } + exePath, err = filepath.EvalSymlinks(exePath) + if err != nil { + return modTime + } + st, err := os.Stat(exePath) + if err != nil { + return modTime + } + return st.ModTime() +}) + +func GenerateEmbedBindata(fsRootPath, outputFile string) error { + output, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + defer output.Close() + + meta := &EmbeddedMeta{} + meta.Root = &embeddedFileInfo{} + var outputOffset int64 + var embedFiles func(parent *embeddedFileInfo, fsPath, embedPath string) error + embedFiles = func(parent *embeddedFileInfo, fsPath, embedPath string) error { + dirEntries, err := os.ReadDir(fsPath) + if err != nil { + return err + } + for _, dirEntry := range dirEntries { + if err != nil { + return err + } + if dirEntry.IsDir() { + child := &embeddedFileInfo{ + BaseName: dirEntry.Name(), + Children: []*embeddedFileInfo{}, // non-nil means it's a directory + } + parent.Children = append(parent.Children, child) + if err = embedFiles(child, filepath.Join(fsPath, dirEntry.Name()), path.Join(embedPath, dirEntry.Name())); err != nil { + return err + } + } else { + data, err := os.ReadFile(filepath.Join(fsPath, dirEntry.Name())) + if err != nil { + return err + } + var compressed bytes.Buffer + gz, _ := gzip.NewWriterLevel(&compressed, gzip.BestCompression) + if _, err = gz.Write(data); err != nil { + return err + } + if err = gz.Close(); err != nil { + return err + } + + // only use the compressed data if it is smaller than the original data + outputBytes := util.Iif(len(compressed.Bytes()) < len(data), compressed.Bytes(), data) + child := &embeddedFileInfo{ + BaseName: dirEntry.Name(), + OriginSize: int64(len(data)), + DataBegin: outputOffset, + DataLen: int64(len(outputBytes)), + } + if _, err = output.Write(outputBytes); err != nil { + return err + } + outputOffset += child.DataLen + parent.Children = append(parent.Children, child) + } + } + return nil + } + + if err = embedFiles(meta.Root, fsRootPath, ""); err != nil { + return err + } + jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL + if err != nil { + return err + } + _, _ = output.Write([]byte{'\n'}) + _, err = output.Write(jsonBuf) + return err +} diff --git a/modules/assetfs/embed_test.go b/modules/assetfs/embed_test.go new file mode 100644 index 0000000000..06598da4c4 --- /dev/null +++ b/modules/assetfs/embed_test.go @@ -0,0 +1,98 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package assetfs + +import ( + "bytes" + "io/fs" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEmbed(t *testing.T) { + tmpDir := t.TempDir() + tmpDataDir := tmpDir + "/data" + _ = os.MkdirAll(tmpDataDir+"/foo/bar", 0o755) + _ = os.WriteFile(tmpDataDir+"/a.txt", []byte("a"), 0o644) + _ = os.WriteFile(tmpDataDir+"/foo/bar/b.txt", bytes.Repeat([]byte("a"), 1000), 0o644) + _ = os.WriteFile(tmpDataDir+"/foo/c.txt", []byte("c"), 0o644) + require.NoError(t, GenerateEmbedBindata(tmpDataDir, tmpDir+"/out.dat")) + + data, err := os.ReadFile(tmpDir + "/out.dat") + require.NoError(t, err) + efs := NewEmbeddedFS(data) + + // test a non-existing file + _, err = fs.ReadFile(efs, "not exist") + assert.ErrorIs(t, err, fs.ErrNotExist) + + // test a normal file (no compression) + content, err := fs.ReadFile(efs, "a.txt") + require.NoError(t, err) + assert.Equal(t, "a", string(content)) + fi, err := fs.Stat(efs, "a.txt") + require.NoError(t, err) + _, ok := fi.(EmbeddedFileInfo).GetGzipContent() + assert.False(t, ok) + + // test a compressed file + content, err = fs.ReadFile(efs, "foo/bar/b.txt") + require.NoError(t, err) + assert.Equal(t, bytes.Repeat([]byte("a"), 1000), content) + fi, err = fs.Stat(efs, "foo/bar/b.txt") + require.NoError(t, err) + assert.False(t, fi.Mode().IsDir()) + assert.True(t, fi.Mode().IsRegular()) + gzipContent, ok := fi.(EmbeddedFileInfo).GetGzipContent() + assert.True(t, ok) + assert.Greater(t, len(gzipContent), 1) + assert.Less(t, len(gzipContent), 1000) + + // test list root directory + entries, err := fs.ReadDir(efs, ".") + require.NoError(t, err) + assert.Len(t, entries, 2) + assert.Equal(t, "a.txt", entries[0].Name()) + assert.False(t, entries[0].IsDir()) + + // test list subdirectory + entries, err = fs.ReadDir(efs, "foo") + require.NoError(t, err) + require.Len(t, entries, 2) + assert.Equal(t, "bar", entries[0].Name()) + assert.True(t, entries[0].IsDir()) + assert.Equal(t, "c.txt", entries[1].Name()) + assert.False(t, entries[1].IsDir()) + + // test directory mode + fi, err = fs.Stat(efs, "foo") + require.NoError(t, err) + assert.True(t, fi.IsDir()) + assert.True(t, fi.Mode().IsDir()) + assert.False(t, fi.Mode().IsRegular()) + + // test httpfs + hfs := http.FS(efs) + hf, err := hfs.Open("foo/bar/b.txt") + require.NoError(t, err) + hi, err := hf.Stat() + require.NoError(t, err) + fiEmbedded, ok := hi.(EmbeddedFileInfo) + require.True(t, ok) + gzipContent, ok = fiEmbedded.GetGzipContent() + assert.True(t, ok) + assert.Greater(t, len(gzipContent), 1) + assert.Less(t, len(gzipContent), 1000) + + // test httpfs directory listing + hf, err = hfs.Open("foo") + require.NoError(t, err) + dirs, err := hf.Readdir(1) + require.NoError(t, err) + assert.Len(t, dirs, 1) +} diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go index 4f3811ba2b..ce55475bd9 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -52,8 +52,8 @@ func Local(name, base string, sub ...string) *Layer { } // Bindata returns a new Layer with the given name, it serves files from the given bindata asset. -func Bindata(name string, fs http.FileSystem) *Layer { - return &Layer{name: name, fs: fs} +func Bindata(name string, fs fs.FS) *Layer { + return &Layer{name: name, fs: http.FS(fs)} } // LayeredFS is a layered asset file-system. It works like http.FileSystem, but it can have multiple layers. diff --git a/modules/auth/openid/discovery_cache_test.go b/modules/auth/openid/discovery_cache_test.go index 7d4b27c5df..f3d7dd226e 100644 --- a/modules/auth/openid/discovery_cache_test.go +++ b/modules/auth/openid/discovery_cache_test.go @@ -26,7 +26,8 @@ func (s *testDiscoveredInfo) OpLocalID() string { } func TestTimedDiscoveryCache(t *testing.T) { - dc := newTimedDiscoveryCache(1 * time.Second) + ttl := 50 * time.Millisecond + dc := newTimedDiscoveryCache(ttl) // Put some initial values dc.Put("foo", &testDiscoveredInfo{}) // openid.opEndpoint: "a", openid.opLocalID: "b", openid.claimedID: "c"}) @@ -41,8 +42,8 @@ func TestTimedDiscoveryCache(t *testing.T) { // Attempt to get a non-existent value assert.Nil(t, dc.Get("bar")) - // Sleep one second and try retrieve again - time.Sleep(1 * time.Second) + // Sleep for a while and try to retrieve again + time.Sleep(ttl * 3 / 2) assert.Nil(t, dc.Get("foo")) } diff --git a/modules/auth/password/hash/common.go b/modules/auth/password/hash/common.go index 487c0738f4..d5e2c34314 100644 --- a/modules/auth/password/hash/common.go +++ b/modules/auth/password/hash/common.go @@ -18,7 +18,7 @@ func parseIntParam(value, param, algorithmName, config string, previousErr error return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed } -func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { //nolint:unparam +func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { //nolint:unparam // algorithmName is always argon2 parsed, err := strconv.ParseUint(value, 10, 64) if err != nil { log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config) diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index c66b62937f..a1e101dd62 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -101,7 +101,7 @@ func Generate(n int) (string, error) { buffer := make([]byte, n) maxInt := big.NewInt(int64(len(validChars))) for { - for j := 0; j < n; j++ { + for j := range n { rnd, err := rand.Int(rand.Reader, maxInt) if err != nil { return "", err diff --git a/modules/auth/password/password_test.go b/modules/auth/password/password_test.go index 6c35dc86bd..0fea593c85 100644 --- a/modules/auth/password/password_test.go +++ b/modules/auth/password/password_test.go @@ -50,7 +50,7 @@ func TestComplexity_Generate(t *testing.T) { test := func(t *testing.T, modes []string) { testComplextity(modes) - for i := 0; i < maxCount; i++ { + for range maxCount { pwd, err := Generate(pwdLen) assert.NoError(t, err) assert.Len(t, pwd, pwdLen) diff --git a/modules/auth/password/pwn/pwn.go b/modules/auth/password/pwn/pwn.go index f77ce9f40b..99a6ca6cea 100644 --- a/modules/auth/password/pwn/pwn.go +++ b/modules/auth/password/pwn/pwn.go @@ -101,7 +101,7 @@ func (c *Client) CheckPassword(pw string, padding bool) (int, error) { } defer resp.Body.Close() - for _, pair := range strings.Split(string(body), "\n") { + for pair := range strings.SplitSeq(string(body), "\n") { parts := strings.Split(pair, ":") if len(parts) != 2 { continue diff --git a/modules/avatar/identicon/block.go b/modules/avatar/identicon/block.go index cb1803a231..fc8ce90212 100644 --- a/modules/avatar/identicon/block.go +++ b/modules/avatar/identicon/block.go @@ -24,8 +24,8 @@ func drawBlock(img *image.Paletted, x, y, size, angle int, points []int) { rotate(points, m, m, angle) } - for i := 0; i < size; i++ { - for j := 0; j < size; j++ { + for i := range size { + for j := range size { if pointInPolygon(i, j, points) { img.SetColorIndex(x+i, y+j, 1) } diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go index 87bd87796e..ee92416a53 100644 --- a/modules/avatar/identicon/identicon.go +++ b/modules/avatar/identicon/identicon.go @@ -134,7 +134,7 @@ func drawBlocks(p *image.Paletted, size int, c, b1, b2 blockFunc, b1Angle, b2Ang // then we make it left-right mirror, so we didn't draw 3/6/9 before for x := 0; x < size/2; x++ { - for y := 0; y < size; y++ { + for y := range size { p.SetColorIndex(size-x, y, p.ColorIndexAt(x, y)) } } diff --git a/modules/cache/cache.go b/modules/cache/cache.go index a434c13b67..039caa9fbc 100644 --- a/modules/cache/cache.go +++ b/modules/cache/cache.go @@ -24,7 +24,7 @@ func Init() error { if err != nil { return err } - for i := 0; i < 10; i++ { + for range 10 { if err = c.Ping(); err == nil { break } diff --git a/modules/cache/cache_redis.go b/modules/cache/cache_redis.go index c5b52a2086..7473c938af 100644 --- a/modules/cache/cache_redis.go +++ b/modules/cache/cache_redis.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/nosql" - "gitea.com/go-chi/cache" //nolint:depguard + "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here "github.com/redis/go-redis/v9" ) diff --git a/modules/cache/cache_twoqueue.go b/modules/cache/cache_twoqueue.go index 1eda2debc4..c8db686e57 100644 --- a/modules/cache/cache_twoqueue.go +++ b/modules/cache/cache_twoqueue.go @@ -10,7 +10,7 @@ import ( "code.gitea.io/gitea/modules/json" - mc "gitea.com/go-chi/cache" //nolint:depguard + mc "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here lru "github.com/hashicorp/golang-lru/v2" ) diff --git a/modules/cache/string_cache.go b/modules/cache/string_cache.go index 4f659616f5..3562b7a926 100644 --- a/modules/cache/string_cache.go +++ b/modules/cache/string_cache.go @@ -11,7 +11,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - chi_cache "gitea.com/go-chi/cache" //nolint:depguard + chi_cache "gitea.com/go-chi/cache" //nolint:depguard // we wrap this package here ) type GetJSONError struct { diff --git a/modules/charset/charset.go b/modules/charset/charset.go index 1855446a98..597ce5120c 100644 --- a/modules/charset/charset.go +++ b/modules/charset/charset.go @@ -164,7 +164,7 @@ func DetectEncoding(content []byte) (string, error) { } times := 1024 / len(content) detectContent = make([]byte, 0, times*len(content)) - for i := 0; i < times; i++ { + for range times { detectContent = append(detectContent, content...) } } else { diff --git a/modules/charset/charset_test.go b/modules/charset/charset_test.go index 1fb362654d..cd2e3b9aaa 100644 --- a/modules/charset/charset_test.go +++ b/modules/charset/charset_test.go @@ -242,7 +242,7 @@ func stringMustEndWith(t *testing.T, expected, value string) { func TestToUTF8WithFallbackReader(t *testing.T) { resetDefaultCharsetsOrder() - for testLen := 0; testLen < 2048; testLen++ { + for testLen := range 2048 { pattern := " test { () }\n" input := "" for len(input) < testLen { diff --git a/modules/structs/commit_status.go b/modules/commitstatus/commit_status.go index dc880ef5eb..a0ab4e7186 100644 --- a/modules/structs/commit_status.go +++ b/modules/commitstatus/commit_status.go @@ -1,11 +1,11 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package structs +package commitstatus // CommitStatusState holds the state of a CommitStatus -// It can be "pending", "success", "error" and "failure" -type CommitStatusState string +// swagger:enum CommitStatusState +type CommitStatusState string //nolint:revive // export stutter const ( // CommitStatusPending is for when the CommitStatus is Pending @@ -18,35 +18,14 @@ const ( CommitStatusFailure CommitStatusState = "failure" // CommitStatusWarning is for when the CommitStatus is Warning CommitStatusWarning CommitStatusState = "warning" + // CommitStatusSkipped is for when CommitStatus is Skipped + CommitStatusSkipped CommitStatusState = "skipped" ) -var commitStatusPriorities = map[CommitStatusState]int{ - CommitStatusError: 0, - CommitStatusFailure: 1, - CommitStatusWarning: 2, - CommitStatusPending: 3, - CommitStatusSuccess: 4, -} - func (css CommitStatusState) String() string { return string(css) } -// NoBetterThan returns true if this State is no better than the given State -// This function only handles the states defined in CommitStatusPriorities -func (css CommitStatusState) NoBetterThan(css2 CommitStatusState) bool { - // NoBetterThan only handles the 5 states above - if _, exist := commitStatusPriorities[css]; !exist { - return false - } - - if _, exist := commitStatusPriorities[css2]; !exist { - return false - } - - return commitStatusPriorities[css] <= commitStatusPriorities[css2] -} - // IsPending represents if commit status state is pending func (css CommitStatusState) IsPending() bool { return css == CommitStatusPending @@ -71,3 +50,32 @@ func (css CommitStatusState) IsFailure() bool { func (css CommitStatusState) IsWarning() bool { return css == CommitStatusWarning } + +// IsSkipped represents if commit status state is skipped +func (css CommitStatusState) IsSkipped() bool { + return css == CommitStatusSkipped +} + +type CommitStatusStates []CommitStatusState //nolint:revive // export stutter + +// According to https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference +// > Additionally, a combined state is returned. The state is one of: +// > failure if any of the contexts report as error or failure +// > pending if there are no statuses or a context is pending +// > success if the latest status for all contexts is success +func (css CommitStatusStates) Combine() CommitStatusState { + successCnt := 0 + for _, state := range css { + switch { + case state.IsError() || state.IsFailure(): + return CommitStatusFailure + case state.IsPending(): + case state.IsSuccess() || state.IsWarning() || state.IsSkipped(): + successCnt++ + } + } + if successCnt > 0 && successCnt == len(css) { + return CommitStatusSuccess + } + return CommitStatusPending +} diff --git a/modules/commitstatus/commit_status_test.go b/modules/commitstatus/commit_status_test.go new file mode 100644 index 0000000000..10d8f20aa4 --- /dev/null +++ b/modules/commitstatus/commit_status_test.go @@ -0,0 +1,201 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package commitstatus + +import "testing" + +func TestCombine(t *testing.T) { + tests := []struct { + name string + states CommitStatusStates + expected CommitStatusState + }{ + // 0 states + { + name: "empty", + states: CommitStatusStates{}, + expected: CommitStatusPending, + }, + // 1 state + { + name: "pending", + states: CommitStatusStates{CommitStatusPending}, + expected: CommitStatusPending, + }, + { + name: "success", + states: CommitStatusStates{CommitStatusSuccess}, + expected: CommitStatusSuccess, + }, + { + name: "error", + states: CommitStatusStates{CommitStatusError}, + expected: CommitStatusFailure, + }, + { + name: "failure", + states: CommitStatusStates{CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "warning", + states: CommitStatusStates{CommitStatusWarning}, + expected: CommitStatusSuccess, + }, + // 2 states + { + name: "pending and success", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess}, + expected: CommitStatusPending, + }, + { + name: "pending and error", + states: CommitStatusStates{CommitStatusPending, CommitStatusError}, + expected: CommitStatusFailure, + }, + { + name: "pending and failure", + states: CommitStatusStates{CommitStatusPending, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "pending and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusWarning}, + expected: CommitStatusPending, + }, + { + name: "success and error", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusError}, + expected: CommitStatusFailure, + }, + { + name: "success and failure", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "success and warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusWarning}, + expected: CommitStatusSuccess, + }, + { + name: "error and failure", + states: CommitStatusStates{CommitStatusError, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "error and warning", + states: CommitStatusStates{CommitStatusError, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "failure and warning", + states: CommitStatusStates{CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + // 3 states + { + name: "pending, success and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusWarning}, + expected: CommitStatusPending, + }, + { + name: "pending, success and error", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusError}, + expected: CommitStatusFailure, + }, + { + name: "pending, success and failure", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "pending, error and failure", + states: CommitStatusStates{CommitStatusPending, CommitStatusError, CommitStatusFailure}, + expected: CommitStatusFailure, + }, + { + name: "success, error and warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusError, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "success, failure and warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "error, failure and warning", + states: CommitStatusStates{CommitStatusError, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "success, warning and skipped", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusWarning, CommitStatusSkipped}, + expected: CommitStatusSuccess, + }, + // All success + { + name: "all success", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusSuccess}, + expected: CommitStatusSuccess, + }, + // All pending + { + name: "all pending", + states: CommitStatusStates{CommitStatusPending, CommitStatusPending, CommitStatusPending}, + expected: CommitStatusPending, + }, + { + name: "all skipped", + states: CommitStatusStates{CommitStatusSkipped, CommitStatusSkipped, CommitStatusSkipped}, + expected: CommitStatusSuccess, + }, + // 4 states + { + name: "pending, success, error and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusError, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "pending, success, failure and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "pending, error, failure and warning", + states: CommitStatusStates{CommitStatusPending, CommitStatusError, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "success, error, failure and warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusError, CommitStatusFailure, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "mixed states", + states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusError, CommitStatusWarning}, + expected: CommitStatusFailure, + }, + { + name: "mixed states with all success", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusPending, CommitStatusWarning}, + expected: CommitStatusPending, + }, + { + name: "all success with warning", + states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusSuccess, CommitStatusWarning}, + expected: CommitStatusSuccess, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.states.Combine() + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} diff --git a/modules/fileicon/entry.go b/modules/fileicon/entry.go index e4ded363e5..0326c2bfa8 100644 --- a/modules/fileicon/entry.go +++ b/modules/fileicon/entry.go @@ -6,17 +6,17 @@ package fileicon import "code.gitea.io/gitea/modules/git" type EntryInfo struct { - FullName string + BaseName string EntryMode git.EntryMode SymlinkToMode git.EntryMode IsOpen bool } -func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo { - ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()} +func EntryInfoFromGitTreeEntry(commit *git.Commit, fullPath string, gitEntry *git.TreeEntry) *EntryInfo { + ret := &EntryInfo{BaseName: gitEntry.Name(), EntryMode: gitEntry.Mode()} if gitEntry.IsLink() { - if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() { - ret.SymlinkToMode = te.Mode() + if res, err := git.EntryFollowLink(commit, fullPath, gitEntry); err == nil && res.TargetEntry.IsDir() { + ret.SymlinkToMode = res.TargetEntry.Mode() } } return ret diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go index 449f527ee8..5361592d8a 100644 --- a/modules/fileicon/material.go +++ b/modules/fileicon/material.go @@ -5,7 +5,6 @@ package fileicon import ( "html/template" - "path" "strings" "sync" @@ -134,7 +133,7 @@ func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string { return "folder-git" } - fileNameLower := strings.ToLower(path.Base(entry.FullName)) + fileNameLower := strings.ToLower(entry.BaseName) if entry.EntryMode.IsDir() { if s, ok := m.rules.FolderNames[fileNameLower]; ok { return s diff --git a/modules/fileicon/material_test.go b/modules/fileicon/material_test.go index 68353d2189..d2a769eaac 100644 --- a/modules/fileicon/material_test.go +++ b/modules/fileicon/material_test.go @@ -20,8 +20,8 @@ func TestMain(m *testing.M) { func TestFindIconName(t *testing.T) { unittest.PrepareTestEnv(t) p := fileicon.DefaultMaterialIconProvider() - assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.php", EntryMode: git.EntryModeBlob})) - assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.PHP", EntryMode: git.EntryModeBlob})) - assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.js", EntryMode: git.EntryModeBlob})) - assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{FullName: "foo.vba", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.php", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "php", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.PHP", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "javascript", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.js", EntryMode: git.EntryModeBlob})) + assert.Equal(t, "visualstudio", p.FindIconName(&fileicon.EntryInfo{BaseName: "foo.vba", EntryMode: git.EntryModeBlob})) } diff --git a/modules/git/blame.go b/modules/git/blame.go index 6eb583a6b9..659dec34a1 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -132,18 +132,22 @@ func (r *BlameReader) Close() error { } // CreateBlameReader creates reader for given repository, commit and file -func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) { - reader, stdout, err := os.Pipe() - if err != nil { - return nil, err - } +func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) { + var ignoreRevsFileName string + var ignoreRevsFileCleanup func() + defer func() { + if err != nil && ignoreRevsFileCleanup != nil { + ignoreRevsFileCleanup() + } + }() cmd := NewCommandNoGlobals("blame", "--porcelain") - var ignoreRevsFileName string - var ignoreRevsFileCleanup func() // TODO: maybe it should check the returned err in a defer func to make sure the cleanup could always be executed correctly if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore { - ignoreRevsFileName, ignoreRevsFileCleanup = tryCreateBlameIgnoreRevsFile(commit) + ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit) + if err != nil && !IsErrNotExist(err) { + return nil, err + } if ignoreRevsFileName != "" { // Possible improvement: use --ignore-revs-file /dev/stdin on unix // There is no equivalent on Windows. May be implemented if Gitea uses an external git backend. @@ -154,6 +158,10 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file) done := make(chan error, 1) + reader, stdout, err := os.Pipe() + if err != nil { + return nil, err + } go func() { stderr := bytes.Buffer{} // TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close" @@ -182,33 +190,29 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath }, nil } -func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func()) { +func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func(), error) { entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs") if err != nil { - log.Error("Unable to get .git-blame-ignore-revs file: GetTreeEntryByPath: %v", err) - return "", nil + return "", nil, err } r, err := entry.Blob().DataAsync() if err != nil { - log.Error("Unable to get .git-blame-ignore-revs file data: DataAsync: %v", err) - return "", nil + return "", nil, err } defer r.Close() f, cleanup, err := setting.AppDataTempDir("git-repo-content").CreateTempFileRandom("git-blame-ignore-revs") if err != nil { - log.Error("Unable to get .git-blame-ignore-revs file data: CreateTempFileRandom: %v", err) - return "", nil + return "", nil, err } filename := f.Name() _, err = io.Copy(f, r) _ = f.Close() if err != nil { cleanup() - log.Error("Unable to get .git-blame-ignore-revs file data: Copy: %v", err) - return "", nil + return "", nil, err } - return filename, cleanup + return filename, cleanup, nil } diff --git a/modules/git/blob.go b/modules/git/blob.go index b7857dbbc6..40d8f44e79 100644 --- a/modules/git/blob.go +++ b/modules/git/blob.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "errors" "io" + "strings" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" @@ -21,17 +22,22 @@ func (b *Blob) Name() string { return b.name } -// GetBlobContent Gets the limited content of the blob as raw text -func (b *Blob) GetBlobContent(limit int64) (string, error) { +// GetBlobBytes Gets the limited content of the blob +func (b *Blob) GetBlobBytes(limit int64) ([]byte, error) { if limit <= 0 { - return "", nil + return nil, nil } dataRc, err := b.DataAsync() if err != nil { - return "", err + return nil, err } defer dataRc.Close() - buf, err := util.ReadWithLimit(dataRc, int(limit)) + return util.ReadWithLimit(dataRc, int(limit)) +} + +// GetBlobContent Gets the limited content of the blob as raw text +func (b *Blob) GetBlobContent(limit int64) (string, error) { + buf, err := b.GetBlobBytes(limit) return string(buf), err } @@ -63,42 +69,44 @@ func (b *Blob) GetBlobLineCount(w io.Writer) (int, error) { } } -// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string -func (b *Blob) GetBlobContentBase64() (string, error) { +// GetBlobContentBase64 Reads the content of the blob with a base64 encoding and returns the encoded string +func (b *Blob) GetBlobContentBase64(originContent *strings.Builder) (string, error) { dataRc, err := b.DataAsync() if err != nil { return "", err } defer dataRc.Close() - pr, pw := io.Pipe() - encoder := base64.NewEncoder(base64.StdEncoding, pw) - - go func() { - _, err := io.Copy(encoder, dataRc) - _ = encoder.Close() - - if err != nil { - _ = pw.CloseWithError(err) - } else { - _ = pw.Close() + base64buf := &strings.Builder{} + encoder := base64.NewEncoder(base64.StdEncoding, base64buf) + buf := make([]byte, 32*1024) +loop: + for { + n, err := dataRc.Read(buf) + if n > 0 { + if originContent != nil { + _, _ = originContent.Write(buf[:n]) + } + if _, err := encoder.Write(buf[:n]); err != nil { + return "", err + } + } + switch { + case errors.Is(err, io.EOF): + break loop + case err != nil: + return "", err } - }() - - out, err := io.ReadAll(pr) - if err != nil { - return "", err } - return string(out), nil + _ = encoder.Close() + return base64buf.String(), nil } // GuessContentType guesses the content type of the blob. func (b *Blob) GuessContentType() (typesniffer.SniffedType, error) { - r, err := b.DataAsync() + buf, err := b.GetBlobBytes(typesniffer.SniffContentSize) if err != nil { return typesniffer.SniffedType{}, err } - defer r.Close() - - return typesniffer.DetectContentTypeFromReader(r) + return typesniffer.DetectContentType(buf), nil } diff --git a/modules/git/command.go b/modules/git/command.go index eaaa4969d0..22f1d02339 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -47,6 +47,7 @@ type Command struct { globalArgsLength int brokenArgs []string cmd *exec.Cmd // for debug purpose only + configArgs []string } func logArgSanitize(arg string) string { @@ -196,6 +197,16 @@ func (c *Command) AddDashesAndList(list ...string) *Command { return c } +func (c *Command) AddConfig(key, value string) *Command { + kv := key + "=" + value + if !isSafeArgumentValue(kv) { + c.brokenArgs = append(c.brokenArgs, key) + } else { + c.configArgs = append(c.configArgs, "-c", kv) + } + return c +} + // ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs // In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead func ToTrustedCmdArgs(args []string) TrustedCmdArgs { @@ -321,7 +332,7 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error { startTime := time.Now() - cmd := exec.CommandContext(ctx, c.prog, c.args...) + cmd := exec.CommandContext(ctx, c.prog, append(c.configArgs, c.args...)...) c.cmd = cmd // for debug purpose only if opts.Env == nil { cmd.Env = os.Environ() diff --git a/modules/git/commit.go b/modules/git/commit.go index cd50c51151..ed4876e7b3 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -20,7 +20,8 @@ import ( // Commit represents a git commit. type Commit struct { - Tree + Tree // FIXME: bad design, this field can be nil if the commit is from "last commit cache" + ID ObjectID // The ID of this commit object Author *Signature Committer *Signature @@ -34,7 +35,7 @@ type Commit struct { // CommitSignature represents a git commit signature part. type CommitSignature struct { Signature string - Payload string // TODO check if can be reconstruct from the rest of commit information to not have duplicate data + Payload string } // Message returns the commit message. Same as retrieving CommitMessage directly. @@ -277,8 +278,8 @@ func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommits var keywords, authors, committers []string var after, before string - fields := strings.Fields(searchString) - for _, k := range fields { + fields := strings.FieldsSeq(searchString) + for k := range fields { switch { case strings.HasPrefix(k, "author:"): authors = append(authors, strings.TrimPrefix(k, "author:")) diff --git a/modules/git/commit_info_nogogit.go b/modules/git/commit_info_nogogit.go index 7a6af0410b..1b45fc8a6c 100644 --- a/modules/git/commit_info_nogogit.go +++ b/modules/git/commit_info_nogogit.go @@ -7,8 +7,7 @@ package git import ( "context" - "fmt" - "io" + "maps" "path" "sort" @@ -40,9 +39,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath return nil, nil, err } - for pth, found := range commits { - revs[pth] = found - } + maps.Copy(revs, commits) } } else { sort.Strings(entryPaths) @@ -124,48 +121,25 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string, return nil, err } - batchStdinWriter, batchReader, cancel, err := commit.repo.CatFileBatch(ctx) - if err != nil { - return nil, err - } - defer cancel() - commitsMap := map[string]*Commit{} commitsMap[commit.ID.String()] = commit commitCommits := map[string]*Commit{} for path, commitID := range revs { - c, ok := commitsMap[commitID] - if ok { - commitCommits[path] = c + if len(commitID) == 0 { continue } - if len(commitID) == 0 { + c, ok := commitsMap[commitID] + if ok { + commitCommits[path] = c continue } - _, err := batchStdinWriter.Write([]byte(commitID + "\n")) - if err != nil { - return nil, err - } - _, typ, size, err := ReadBatchLine(batchReader) + c, err := commit.repo.GetCommit(commitID) // Ensure the commit exists in the repository if err != nil { return nil, err } - if typ != "commit" { - if err := DiscardFull(batchReader, size+1); err != nil { - return nil, err - } - return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID) - } - c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size)) - if err != nil { - return nil, err - } - if _, err := batchReader.Discard(1); err != nil { - return nil, err - } commitCommits[path] = c } diff --git a/modules/git/commit_reader.go b/modules/git/commit_reader.go index 228bbaf314..eb8f4c6322 100644 --- a/modules/git/commit_reader.go +++ b/modules/git/commit_reader.go @@ -6,10 +6,44 @@ package git import ( "bufio" "bytes" + "fmt" "io" - "strings" ) +const ( + commitHeaderGpgsig = "gpgsig" + commitHeaderGpgsigSha256 = "gpgsig-sha256" +) + +func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, headerValue []byte) error { + if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' { + headerValue = headerValue[:len(headerValue)-1] // remove trailing newline + } + switch headerKey { + case "tree": + objID, err := NewIDFromString(string(headerValue)) + if err != nil { + return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err) + } + commit.Tree = *NewTree(gitRepo, objID) + case "parent": + objID, err := NewIDFromString(string(headerValue)) + if err != nil { + return fmt.Errorf("invalid parent ID %q: %w", string(headerValue), err) + } + commit.Parents = append(commit.Parents, objID) + case "author": + commit.Author.Decode(headerValue) + case "committer": + commit.Committer.Decode(headerValue) + case commitHeaderGpgsig, commitHeaderGpgsigSha256: + // if there are duplicate "gpgsig" and "gpgsig-sha256" headers, then the signature must have already been invalid + // so we don't need to handle duplicate headers here + commit.Signature = &CommitSignature{Signature: string(headerValue)} + } + return nil +} + // CommitFromReader will generate a Commit from a provided reader // We need this to interpret commits from cat-file or cat-file --batch // @@ -21,90 +55,46 @@ func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) Committer: &Signature{}, } - payloadSB := new(strings.Builder) - signatureSB := new(strings.Builder) - messageSB := new(strings.Builder) - message := false - pgpsig := false - - bufReader, ok := reader.(*bufio.Reader) - if !ok { - bufReader = bufio.NewReader(reader) - } - -readLoop: + bufReader := bufio.NewReader(reader) + inHeader := true + var payloadSB, messageSB bytes.Buffer + var headerKey string + var headerValue []byte for { line, err := bufReader.ReadBytes('\n') - if err != nil { - if err == io.EOF { - if message { - _, _ = messageSB.Write(line) - } - _, _ = payloadSB.Write(line) - break readLoop - } - return nil, err + if err != nil && err != io.EOF { + return nil, fmt.Errorf("unable to read commit %q: %w", objectID.String(), err) } - if pgpsig { - if len(line) > 0 && line[0] == ' ' { - _, _ = signatureSB.Write(line[1:]) - continue - } - pgpsig = false + if len(line) == 0 { + break } - if !message { - // This is probably not correct but is copied from go-gits interpretation... - trimmed := bytes.TrimSpace(line) - if len(trimmed) == 0 { - message = true - _, _ = payloadSB.Write(line) - continue - } - - split := bytes.SplitN(trimmed, []byte{' '}, 2) - var data []byte - if len(split) > 1 { - data = split[1] + if inHeader { + inHeader = !(len(line) == 1 && line[0] == '\n') // still in header if line is not just a newline + k, v, _ := bytes.Cut(line, []byte{' '}) + if len(k) != 0 || !inHeader { + if headerKey != "" { + if err = assignCommitFields(gitRepo, commit, headerKey, headerValue); err != nil { + return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err) + } + } + headerKey = string(k) // it also resets the headerValue to empty string if not inHeader + headerValue = v + } else { + headerValue = append(headerValue, v...) } - - switch string(split[0]) { - case "tree": - commit.Tree = *NewTree(gitRepo, MustIDFromString(string(data))) + if headerKey != commitHeaderGpgsig && headerKey != commitHeaderGpgsigSha256 { _, _ = payloadSB.Write(line) - case "parent": - commit.Parents = append(commit.Parents, MustIDFromString(string(data))) - _, _ = payloadSB.Write(line) - case "author": - commit.Author = &Signature{} - commit.Author.Decode(data) - _, _ = payloadSB.Write(line) - case "committer": - commit.Committer = &Signature{} - commit.Committer.Decode(data) - _, _ = payloadSB.Write(line) - case "encoding": - _, _ = payloadSB.Write(line) - case "gpgsig": - fallthrough - case "gpgsig-sha256": // FIXME: no intertop, so only 1 exists at present. - _, _ = signatureSB.Write(data) - _ = signatureSB.WriteByte('\n') - pgpsig = true } } else { _, _ = messageSB.Write(line) _, _ = payloadSB.Write(line) } } + commit.CommitMessage = messageSB.String() - commit.Signature = &CommitSignature{ - Signature: signatureSB.String(), - Payload: payloadSB.String(), - } - if len(commit.Signature.Signature) == 0 { - commit.Signature = nil + if commit.Signature != nil { + commit.Signature.Payload = payloadSB.String() } - return commit, nil } diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 64a0f53908..97ccecdacc 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -60,8 +60,7 @@ func TestGetFullCommitIDErrorSha256(t *testing.T) { } func TestCommitFromReaderSha256(t *testing.T) { - commitString := `9433b2a62b964c17a4485ae180f45f595d3e69d31b786087775e28c6b6399df0 commit 1114 -tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e + commitString := `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8 author Adam Majer <amajer@suse.de> 1698676906 +0100 committer Adam Majer <amajer@suse.de> 1698676906 +0100 @@ -112,8 +111,7 @@ VAEUo6ecdDxSpyt2naeg9pKus/BRi7P6g4B1hkk/zZstUX/QP4IQuAJbXjkvsC+X HKRr3NlRM/DygzTyj0gN74uoa0goCIbyAQhiT42nm0cuhM7uN/W0ayrlZjGF1cbR 8NCJUL2Nwj0ywKIavC99Ipkb8AsFwpVT6U6effs6 =xybZ ------END PGP SIGNATURE----- -`, commitFromReader.Signature.Signature) +-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature) assert.Equal(t, `tree e7f9e96dd79c09b078cac8b303a7d3b9d65ff9b734e86060a4d20409fd379f9e parent 26e9ccc29fad747e9c5d9f4c9ddeb7eff61cc45ef6a8dc258cbeb181afc055e8 author Adam Majer <amajer@suse.de> 1698676906 +0100 diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index f43e0081fd..81fb91dfc6 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -59,8 +59,7 @@ func TestGetFullCommitIDError(t *testing.T) { } func TestCommitFromReader(t *testing.T) { - commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074 -tree f1a6cb52b2d16773290cefe49ad0684b50a4f930 + commitString := `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930 parent 37991dec2c8e592043f47155ce4808d4580f9123 author silverwind <me@silverwind.io> 1563741793 +0200 committer silverwind <me@silverwind.io> 1563741793 +0200 @@ -108,8 +107,7 @@ sD53z/f0J+We4VZjY+pidvA9BGZPFVdR3wd3xGs8/oH6UWaLJAMGkLG6dDb3qDLm mfeFhT57UbE4qukTDIQ0Y0WM40UYRTakRaDY7ubhXgLgx09Cnp9XTVMsHgT6j9/i 1pxsB104XLWjQHTjr1JtiaBQEwFh9r2OKTcpvaLcbNtYpo7CzOs= =FRsO ------END PGP SIGNATURE----- -`, commitFromReader.Signature.Signature) +-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature) assert.Equal(t, `tree f1a6cb52b2d16773290cefe49ad0684b50a4f930 parent 37991dec2c8e592043f47155ce4808d4580f9123 author silverwind <me@silverwind.io> 1563741793 +0200 @@ -126,8 +124,7 @@ empty commit`, commitFromReader.Signature.Payload) } func TestCommitWithEncodingFromReader(t *testing.T) { - commitString := `feaf4ba6bc635fec442f46ddd4512416ec43c2c2 commit 1074 -tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 + commitString := `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 parent 47b24e7ab977ed31c5a39989d570847d6d0052af author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 committer KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 @@ -172,8 +169,7 @@ SONRzusmu5n3DgV956REL7x62h7JuqmBz/12HZkr0z0zgXkcZ04q08pSJATX5N1F yN+tWxTsWg+zhDk96d5Esdo9JMjcFvPv0eioo30GAERaz1hoD7zCMT4jgUFTQwgz jw4YcO5u =r3UU ------END PGP SIGNATURE----- -`, commitFromReader.Signature.Signature) +-----END PGP SIGNATURE-----`, commitFromReader.Signature.Signature) assert.Equal(t, `tree ca3fad42080dd1a6d291b75acdfc46e5b9b307e5 parent 47b24e7ab977ed31c5a39989d570847d6d0052af author KN4CK3R <admin@oldschoolhack.me> 1711702962 +0100 diff --git a/modules/git/diff_test.go b/modules/git/diff_test.go index 9a09347b30..7671fffcc1 100644 --- a/modules/git/diff_test.go +++ b/modules/git/diff_test.go @@ -154,7 +154,7 @@ func TestCutDiffAroundLine(t *testing.T) { } func BenchmarkCutDiffAroundLine(b *testing.B) { - for n := 0; n < b.N; n++ { + for b.Loop() { CutDiffAroundLine(strings.NewReader(exampleDiff), 3, true, 3) } } diff --git a/modules/git/error.go b/modules/git/error.go index 6c86d1b04d..7d131345d0 100644 --- a/modules/git/error.go +++ b/modules/git/error.go @@ -32,22 +32,6 @@ func (err ErrNotExist) Unwrap() error { return util.ErrNotExist } -// ErrSymlinkUnresolved entry.FollowLink error -type ErrSymlinkUnresolved struct { - Name string - Message string -} - -func (err ErrSymlinkUnresolved) Error() string { - return fmt.Sprintf("%s: %s", err.Name, err.Message) -} - -// IsErrSymlinkUnresolved if some error is ErrSymlinkUnresolved -func IsErrSymlinkUnresolved(err error) bool { - _, ok := err.(ErrSymlinkUnresolved) - return ok -} - // ErrBranchNotExist represents a "BranchNotExist" kind of error. type ErrBranchNotExist struct { Name string diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index 97e8ee4724..d9573a55d6 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -76,7 +76,7 @@ func (f Format) Parser(r io.Reader) *Parser { // would turn into "%0a%00". func (f Format) hexEscaped(delim []byte) string { escaped := "" - for i := 0; i < len(delim); i++ { + for i := range delim { escaped += "%" + hex.EncodeToString([]byte{delim[i]}) } return escaped diff --git a/modules/git/hook.go b/modules/git/hook.go index a6f6b18855..548a59971d 100644 --- a/modules/git/hook.go +++ b/modules/git/hook.go @@ -8,6 +8,7 @@ import ( "errors" "os" "path/filepath" + "slices" "strings" "code.gitea.io/gitea/modules/util" @@ -25,12 +26,7 @@ var ErrNotValidHook = errors.New("not a valid Git hook") // IsValidHookName returns true if given name is a valid Git hook. func IsValidHookName(name string) bool { - for _, hn := range hookNames { - if hn == name { - return true - } - } - return false + return slices.Contains(hookNames, name) } // Hook represents a Git hook. diff --git a/modules/git/key.go b/modules/git/key.go new file mode 100644 index 0000000000..2513c048b7 --- /dev/null +++ b/modules/git/key.go @@ -0,0 +1,15 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +// Based on https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat +const ( + SigningKeyFormatOpenPGP = "openpgp" // for GPG keys, the expected default of git cli + SigningKeyFormatSSH = "ssh" +) + +type SigningKey struct { + KeyID string + Format string +} diff --git a/modules/git/languagestats/language_stats_nogogit.go b/modules/git/languagestats/language_stats_nogogit.go index 34797263a6..94cf9fff8c 100644 --- a/modules/git/languagestats/language_stats_nogogit.go +++ b/modules/git/languagestats/language_stats_nogogit.go @@ -97,17 +97,17 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, } isVendored := optional.None[bool]() - isGenerated := optional.None[bool]() isDocumentation := optional.None[bool]() isDetectable := optional.None[bool]() attrs, err := checker.CheckPath(f.Name()) + attrLinguistGenerated := optional.None[bool]() if err == nil { if isVendored = attrs.GetVendored(); isVendored.ValueOrDefault(false) { continue } - if isGenerated = attrs.GetGenerated(); isGenerated.ValueOrDefault(false) { + if attrLinguistGenerated = attrs.GetGenerated(); attrLinguistGenerated.ValueOrDefault(false) { continue } @@ -169,7 +169,15 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, return nil, err } } - if !isGenerated.Has() && enry.IsGenerated(f.Name(), content) { + + // if "generated" attribute is set, use it, otherwise use enry.IsGenerated to guess + var isGenerated bool + if attrLinguistGenerated.Has() { + isGenerated = attrLinguistGenerated.Value() + } else { + isGenerated = enry.IsGenerated(f.Name(), content) + } + if isGenerated { continue } diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index cf9c10d7b4..cff2556083 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -13,7 +13,7 @@ import ( ) func getCacheKey(repoPath, commitID, entryPath string) string { - hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath))) + hashBytes := sha256.Sum256(fmt.Appendf(nil, "%s:%s:%s", repoPath, commitID, entryPath)) return fmt.Sprintf("last_commit:%x", hashBytes) } diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 3ee462f68e..dfdef38ef9 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -346,10 +346,7 @@ func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath st results := make([]string, len(paths)) remaining := len(paths) - nextRestart := (len(paths) * 3) / 4 - if nextRestart > 70 { - nextRestart = 70 - } + nextRestart := min((len(paths)*3)/4, 70) lastEmptyParent := head.ID.String() commitSinceLastEmptyParent := uint64(0) commitSinceNextRestart := uint64(0) diff --git a/modules/git/ref.go b/modules/git/ref.go index f20a175e42..56b2db858a 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -109,8 +109,8 @@ func (ref RefName) IsFor() bool { } func (ref RefName) nameWithoutPrefix(prefix string) string { - if strings.HasPrefix(string(ref), prefix) { - return strings.TrimPrefix(string(ref), prefix) + if after, ok := strings.CutPrefix(string(ref), prefix); ok { + return after } return "" } diff --git a/modules/git/repo.go b/modules/git/repo.go index 45937a8d5f..f1f6902773 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -28,6 +28,7 @@ type GPGSettings struct { Email string Name string PublicKeyContent string + Format string } const prettyLogFormat = `--pretty=format:%H` @@ -43,9 +44,9 @@ func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, erro return commits, nil } - parts := bytes.Split(logs, []byte{'\n'}) + parts := bytes.SplitSeq(logs, []byte{'\n'}) - for _, commitID := range parts { + for commitID := range parts { commit, err := repo.GetCommit(string(commitID)) if err != nil { return nil, err diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index a44fd8c0e1..4066a1ca7b 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -547,11 +547,11 @@ func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID s return "", runErr } - parts := bytes.Split(bytes.TrimSpace(stdout), []byte{'\n'}) + parts := bytes.SplitSeq(bytes.TrimSpace(stdout), []byte{'\n'}) // check the commits one by one until we find a commit contained by another branch // and we think this commit is the divergence point - for _, commitID := range parts { + for commitID := range parts { branches, err := repo.getBranches(env, string(commitID), 2) if err != nil { return "", err diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go index 8f91b4dce5..0021a7bda7 100644 --- a/modules/git/repo_gpg.go +++ b/modules/git/repo_gpg.go @@ -6,6 +6,7 @@ package git import ( "fmt" + "os" "strings" "code.gitea.io/gitea/modules/process" @@ -13,6 +14,14 @@ import ( // LoadPublicKeyContent will load the key from gpg func (gpgSettings *GPGSettings) LoadPublicKeyContent() error { + if gpgSettings.Format == SigningKeyFormatSSH { + content, err := os.ReadFile(gpgSettings.KeyID) + if err != nil { + return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err) + } + gpgSettings.PublicKeyContent = string(content) + return nil + } content, stderr, err := process.GetManager().Exec( "gpg -a --export", "gpg", "-a", "--export", gpgSettings.KeyID) @@ -44,6 +53,9 @@ func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, signingKey, _, _ := NewCommand("config", "--get", "user.signingkey").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) gpgSettings.KeyID = strings.TrimSpace(signingKey) + format, _, _ := NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) + gpgSettings.Format = strings.TrimSpace(format) + defaultEmail, _, _ := NewCommand("config", "--get", "user.email").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) gpgSettings.Email = strings.TrimSpace(defaultEmail) diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index 443a3a20d1..4879121a41 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -86,7 +86,7 @@ func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { return nil, err } filelist := make([]string, 0, len(filenames)) - for _, line := range bytes.Split(res, []byte{'\000'}) { + for line := range bytes.SplitSeq(res, []byte{'\000'}) { filelist = append(filelist, string(line)) } diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index c74618471a..c8d72eee02 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -39,8 +39,8 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { return "", err } - tagRefs := strings.Split(stdout, "\n") - for _, tagRef := range tagRefs { + tagRefs := strings.SplitSeq(stdout, "\n") + for tagRef := range tagRefs { if len(strings.TrimSpace(tagRef)) > 0 { fields := strings.Fields(tagRef) if strings.HasPrefix(fields[0], sha) && strings.HasPrefix(fields[1], TagPrefix) { @@ -62,7 +62,7 @@ func (repo *Repository) GetTagID(name string) (string, error) { return "", err } // Make sure exact match is used: "v1" != "release/v1" - for _, line := range strings.Split(stdout, "\n") { + for line := range strings.SplitSeq(stdout, "\n") { fields := strings.Fields(line) if len(fields) == 2 && fields[1] == "refs/tags/"+name { return fields[0], nil diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index 70e5aee023..309a73d759 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -15,7 +15,7 @@ import ( type CommitTreeOpts struct { Parents []string Message string - KeyID string + Key *SigningKey NoGPGSign bool AlwaysSign bool } @@ -43,8 +43,13 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt _, _ = messageBytes.WriteString(opts.Message) _, _ = messageBytes.WriteString("\n") - if opts.KeyID != "" || opts.AlwaysSign { - cmd.AddOptionFormat("-S%s", opts.KeyID) + if opts.Key != nil { + if opts.Key.Format != "" { + cmd.AddConfig("gpg.format", opts.Key.Format) + } + cmd.AddOptionFormat("-S%s", opts.Key.KeyID) + } else if opts.AlwaysSign { + cmd.AddOptionFormat("-S") } if opts.NoGPGSign { diff --git a/modules/git/tree.go b/modules/git/tree.go index f6fdff97d0..38fb45f3b1 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -56,7 +56,7 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error return nil, err } filelist := make([]string, 0, len(filenames)) - for _, line := range bytes.Split(res, []byte{'\000'}) { + for line := range bytes.SplitSeq(res, []byte{'\000'}) { filelist = append(filelist, string(line)) } diff --git a/modules/git/tree_blob_nogogit.go b/modules/git/tree_blob_nogogit.go index b7bcf40edd..b18d0fa05e 100644 --- a/modules/git/tree_blob_nogogit.go +++ b/modules/git/tree_blob_nogogit.go @@ -11,7 +11,7 @@ import ( ) // GetTreeEntryByPath get the tree entries according the sub dir -func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { +func (t *Tree) GetTreeEntryByPath(relpath string) (_ *TreeEntry, err error) { if len(relpath) == 0 { return &TreeEntry{ ptree: t, @@ -21,27 +21,25 @@ func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { }, nil } - // FIXME: This should probably use git cat-file --batch to be a bit more efficient relpath = path.Clean(relpath) parts := strings.Split(relpath, "/") - var err error + tree := t - for i, name := range parts { - if i == len(parts)-1 { - entries, err := tree.ListEntries() - if err != nil { - return nil, err - } - for _, v := range entries { - if v.Name() == name { - return v, nil - } - } - } else { - tree, err = tree.SubTree(name) - if err != nil { - return nil, err - } + for _, name := range parts[:len(parts)-1] { + tree, err = tree.SubTree(name) + if err != nil { + return nil, err + } + } + + name := parts[len(parts)-1] + entries, err := tree.ListEntries() + if err != nil { + return nil, err + } + for _, v := range entries { + if v.Name() == name { + return v, nil } } return nil, ErrNotExist{"", relpath} diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index a2e1579290..5099d8ee79 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -5,7 +5,7 @@ package git import ( - "io" + "path" "sort" "strings" @@ -24,77 +24,57 @@ func (te *TreeEntry) Type() string { } } -// FollowLink returns the entry pointed to by a symlink -func (te *TreeEntry) FollowLink() (*TreeEntry, error) { +type EntryFollowResult struct { + SymlinkContent string + TargetFullPath string + TargetEntry *TreeEntry +} + +func EntryFollowLink(commit *Commit, fullPath string, te *TreeEntry) (*EntryFollowResult, error) { if !te.IsLink() { - return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"} + return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q is not a symlink", fullPath) } - // read the link - r, err := te.Blob().DataAsync() - if err != nil { - return nil, err + // git's filename max length is 4096, hopefully a link won't be longer than multiple of that + const maxSymlinkSize = 20 * 4096 + if te.Blob().Size() > maxSymlinkSize { + return nil, util.ErrorWrap(util.ErrUnprocessableContent, "%q content exceeds symlink limit", fullPath) } - closed := false - defer func() { - if !closed { - _ = r.Close() - } - }() - buf := make([]byte, te.Size()) - _, err = io.ReadFull(r, buf) + + link, err := te.Blob().GetBlobContent(maxSymlinkSize) if err != nil { return nil, err } - _ = r.Close() - closed = true - - lnk := string(buf) - t := te.ptree - - // traverse up directories - for ; t != nil && strings.HasPrefix(lnk, "../"); lnk = lnk[3:] { - t = t.ptree + if strings.HasPrefix(link, "/") { + // It's said that absolute path will be stored as is in Git + return &EntryFollowResult{SymlinkContent: link}, util.ErrorWrap(util.ErrUnprocessableContent, "%q is an absolute symlink", fullPath) } - if t == nil { - return nil, ErrSymlinkUnresolved{te.Name(), "points outside of repo"} - } - - target, err := t.GetTreeEntryByPath(lnk) + targetFullPath := path.Join(path.Dir(fullPath), link) + targetEntry, err := commit.GetTreeEntryByPath(targetFullPath) if err != nil { - if IsErrNotExist(err) { - return nil, ErrSymlinkUnresolved{te.Name(), "broken link"} - } - return nil, err + return &EntryFollowResult{SymlinkContent: link}, err } - return target, nil + return &EntryFollowResult{SymlinkContent: link, TargetFullPath: targetFullPath, TargetEntry: targetEntry}, nil } -// FollowLinks returns the entry ultimately pointed to by a symlink -func (te *TreeEntry) FollowLinks(optLimit ...int) (*TreeEntry, error) { - if !te.IsLink() { - return nil, ErrSymlinkUnresolved{te.Name(), "not a symlink"} - } +func EntryFollowLinks(commit *Commit, firstFullPath string, firstTreeEntry *TreeEntry, optLimit ...int) (res *EntryFollowResult, err error) { limit := util.OptionalArg(optLimit, 10) - entry := te - for i := 0; i < limit; i++ { - if !entry.IsLink() { - break - } - next, err := entry.FollowLink() + treeEntry, fullPath := firstTreeEntry, firstFullPath + for range limit { + res, err = EntryFollowLink(commit, fullPath, treeEntry) if err != nil { - return nil, err + return res, err } - if next.ID == entry.ID { - return nil, ErrSymlinkUnresolved{entry.Name(), "recursive link"} + treeEntry, fullPath = res.TargetEntry, res.TargetFullPath + if !treeEntry.IsLink() { + break } - entry = next } - if entry.IsLink() { - return nil, ErrSymlinkUnresolved{te.Name(), "too many levels of symbolic links"} + if treeEntry.IsLink() { + return res, util.ErrorWrap(util.ErrUnprocessableContent, "%q has too many links", firstFullPath) } - return entry, nil + return res, nil } // returns the Tree pointed to by this TreeEntry, or nil if this is not a tree diff --git a/modules/git/tree_entry_common_test.go b/modules/git/tree_entry_common_test.go new file mode 100644 index 0000000000..8b63bbb993 --- /dev/null +++ b/modules/git/tree_entry_common_test.go @@ -0,0 +1,76 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "testing" + + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFollowLink(t *testing.T) { + r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare") + require.NoError(t, err) + defer r.Close() + + commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123") + require.NoError(t, err) + + // get the symlink + { + lnkFullPath := "foo/bar/link_to_hello" + lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello") + require.NoError(t, err) + assert.True(t, lnk.IsLink()) + + // should be able to dereference to target + res, err := EntryFollowLink(commit, lnkFullPath, lnk) + require.NoError(t, err) + assert.Equal(t, "hello", res.TargetEntry.Name()) + assert.Equal(t, "foo/nar/hello", res.TargetFullPath) + assert.False(t, res.TargetEntry.IsLink()) + assert.Equal(t, "b14df6442ea5a1b382985a6549b85d435376c351", res.TargetEntry.ID.String()) + } + + { + // should error when called on a normal file + entry, err := commit.Tree.GetTreeEntryByPath("file1.txt") + require.NoError(t, err) + res, err := EntryFollowLink(commit, "file1.txt", entry) + assert.ErrorIs(t, err, util.ErrUnprocessableContent) + assert.Nil(t, res) + } + + { + // should error for broken links + entry, err := commit.Tree.GetTreeEntryByPath("foo/broken_link") + require.NoError(t, err) + assert.True(t, entry.IsLink()) + res, err := EntryFollowLink(commit, "foo/broken_link", entry) + assert.ErrorIs(t, err, util.ErrNotExist) + assert.Equal(t, "nar/broken_link", res.SymlinkContent) + } + + { + // should error for external links + entry, err := commit.Tree.GetTreeEntryByPath("foo/outside_repo") + require.NoError(t, err) + assert.True(t, entry.IsLink()) + res, err := EntryFollowLink(commit, "foo/outside_repo", entry) + assert.ErrorIs(t, err, util.ErrNotExist) + assert.Equal(t, "../../outside_repo", res.SymlinkContent) + } + + { + // testing fix for short link bug + entry, err := commit.Tree.GetTreeEntryByPath("foo/link_short") + require.NoError(t, err) + res, err := EntryFollowLink(commit, "foo/link_short", entry) + assert.ErrorIs(t, err, util.ErrNotExist) + assert.Equal(t, "a", res.SymlinkContent) + } +} diff --git a/modules/git/tree_entry_gogit.go b/modules/git/tree_entry_gogit.go index eb9b012681..e6845f1c77 100644 --- a/modules/git/tree_entry_gogit.go +++ b/modules/git/tree_entry_gogit.go @@ -19,16 +19,12 @@ type TreeEntry struct { gogitTreeEntry *object.TreeEntry ptree *Tree - size int64 - sized bool - fullName string + size int64 + sized bool } // Name returns the name of the entry func (te *TreeEntry) Name() string { - if te.fullName != "" { - return te.fullName - } return te.gogitTreeEntry.Name } @@ -55,7 +51,7 @@ func (te *TreeEntry) Size() int64 { return te.size } -// IsSubModule if the entry is a sub module +// IsSubModule if the entry is a submodule func (te *TreeEntry) IsSubModule() bool { return te.gogitTreeEntry.Mode == filemode.Submodule } diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go index d815a8bc2e..f36c07bc2a 100644 --- a/modules/git/tree_entry_mode.go +++ b/modules/git/tree_entry_mode.go @@ -15,7 +15,7 @@ type EntryMode int // one of these. const ( // EntryModeNoEntry is possible if the file was added or removed in a commit. In the case of - // added the base commit will not have the file in its tree so a mode of 0o000000 is used. + // when adding the base commit doesn't have the file in its tree, a mode of 0o000000 is used. EntryModeNoEntry EntryMode = 0o000000 EntryModeBlob EntryMode = 0o100644 @@ -30,7 +30,7 @@ func (e EntryMode) String() string { return strconv.FormatInt(int64(e), 8) } -// IsSubModule if the entry is a sub module +// IsSubModule if the entry is a submodule func (e EntryMode) IsSubModule() bool { return e == EntryModeCommit } diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 0c0e1835f1..8fad96cdf8 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -18,7 +18,7 @@ type TreeEntry struct { sized bool } -// Name returns the name of the entry +// Name returns the name of the entry (base name) func (te *TreeEntry) Name() string { return te.name } @@ -57,7 +57,7 @@ func (te *TreeEntry) Size() int64 { return te.size } -// IsSubModule if the entry is a sub module +// IsSubModule if the entry is a submodule func (te *TreeEntry) IsSubModule() bool { return te.entryMode.IsSubModule() } diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go index 30eee13669..9ca82675e0 100644 --- a/modules/git/tree_entry_test.go +++ b/modules/git/tree_entry_test.go @@ -53,50 +53,3 @@ func TestEntriesCustomSort(t *testing.T) { assert.Equal(t, "bcd", entries[6].Name()) assert.Equal(t, "abc", entries[7].Name()) } - -func TestFollowLink(t *testing.T) { - r, err := openRepositoryWithDefaultContext("tests/repos/repo1_bare") - assert.NoError(t, err) - defer r.Close() - - commit, err := r.GetCommit("37991dec2c8e592043f47155ce4808d4580f9123") - assert.NoError(t, err) - - // get the symlink - lnk, err := commit.Tree.GetTreeEntryByPath("foo/bar/link_to_hello") - assert.NoError(t, err) - assert.True(t, lnk.IsLink()) - - // should be able to dereference to target - target, err := lnk.FollowLink() - assert.NoError(t, err) - assert.Equal(t, "hello", target.Name()) - assert.False(t, target.IsLink()) - assert.Equal(t, "b14df6442ea5a1b382985a6549b85d435376c351", target.ID.String()) - - // should error when called on normal file - target, err = commit.Tree.GetTreeEntryByPath("file1.txt") - assert.NoError(t, err) - _, err = target.FollowLink() - assert.EqualError(t, err, "file1.txt: not a symlink") - - // should error for broken links - target, err = commit.Tree.GetTreeEntryByPath("foo/broken_link") - assert.NoError(t, err) - assert.True(t, target.IsLink()) - _, err = target.FollowLink() - assert.EqualError(t, err, "broken_link: broken link") - - // should error for external links - target, err = commit.Tree.GetTreeEntryByPath("foo/outside_repo") - assert.NoError(t, err) - assert.True(t, target.IsLink()) - _, err = target.FollowLink() - assert.EqualError(t, err, "outside_repo: points outside of repo") - - // testing fix for short link bug - target, err = commit.Tree.GetTreeEntryByPath("foo/link_short") - assert.NoError(t, err) - _, err = target.FollowLink() - assert.EqualError(t, err, "link_short: broken link") -} diff --git a/modules/git/tree_gogit.go b/modules/git/tree_gogit.go index 421b0ecb0f..272b018ffd 100644 --- a/modules/git/tree_gogit.go +++ b/modules/git/tree_gogit.go @@ -69,7 +69,7 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) { seen := map[plumbing.Hash]bool{} walker := object.NewTreeWalker(t.gogitTree, true, seen) for { - fullName, entry, err := walker.Next() + _, entry, err := walker.Next() if err == io.EOF { break } @@ -84,7 +84,6 @@ func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) { ID: ParseGogitHash(entry.Hash), gogitTreeEntry: &entry, ptree: t, - fullName: fullName, } entries = append(entries, convertedEntry) } diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index 61e5482538..cae11c4b1b 100644 --- a/modules/git/tree_test.go +++ b/modules/git/tree_test.go @@ -19,7 +19,7 @@ func TestSubTree_Issue29101(t *testing.T) { assert.NoError(t, err) // old code could produce a different error if called multiple times - for i := 0; i < 10; i++ { + for range 10 { _, err = commit.SubTree("file1.txt") assert.Error(t, err) assert.True(t, IsErrNotExist(err)) diff --git a/modules/globallock/globallock_test.go b/modules/globallock/globallock_test.go index 0143fc6833..8d55d9f699 100644 --- a/modules/globallock/globallock_test.go +++ b/modules/globallock/globallock_test.go @@ -70,7 +70,7 @@ func testLockAndDo(t *testing.T) { count := 0 wg := sync.WaitGroup{} wg.Add(concurrency) - for i := 0; i < concurrency; i++ { + for range concurrency { go func() { defer wg.Done() err := LockAndDo(ctx, "test", func(ctx context.Context) error { diff --git a/modules/graceful/manager_windows.go b/modules/graceful/manager_windows.go index d776e0e9f9..457768d6ca 100644 --- a/modules/graceful/manager_windows.go +++ b/modules/graceful/manager_windows.go @@ -41,8 +41,7 @@ func (g *Manager) start() { // Make SVC process run := svc.Run - //lint:ignore SA1019 We use IsAnInteractiveSession because IsWindowsService has a different permissions profile - isAnInteractiveSession, err := svc.IsAnInteractiveSession() //nolint:staticcheck + isAnInteractiveSession, err := svc.IsAnInteractiveSession() //nolint:staticcheck // must use IsAnInteractiveSession because IsWindowsService has a different permissions profile if err != nil { log.Error("Unable to ascertain if running as an Windows Service: %v", err) return diff --git a/modules/hostmatcher/hostmatcher.go b/modules/hostmatcher/hostmatcher.go index 1069310316..15c6371422 100644 --- a/modules/hostmatcher/hostmatcher.go +++ b/modules/hostmatcher/hostmatcher.go @@ -6,6 +6,7 @@ package hostmatcher import ( "net" "path/filepath" + "slices" "strings" ) @@ -38,7 +39,7 @@ func isBuiltin(s string) bool { // ParseHostMatchList parses the host list HostMatchList func ParseHostMatchList(settingKeyHint, hostList string) *HostMatchList { hl := &HostMatchList{SettingKeyHint: settingKeyHint, SettingValue: hostList} - for _, s := range strings.Split(hostList, ",") { + for s := range strings.SplitSeq(hostList, ",") { s = strings.ToLower(strings.TrimSpace(s)) if s == "" { continue @@ -61,7 +62,7 @@ func ParseSimpleMatchList(settingKeyHint, matchList string) *HostMatchList { SettingKeyHint: settingKeyHint, SettingValue: matchList, } - for _, s := range strings.Split(matchList, ",") { + for s := range strings.SplitSeq(matchList, ",") { s = strings.ToLower(strings.TrimSpace(s)) if s == "" { continue @@ -98,10 +99,8 @@ func (hl *HostMatchList) checkPattern(host string) bool { } func (hl *HostMatchList) checkIP(ip net.IP) bool { - for _, pattern := range hl.patterns { - if pattern == "*" { - return true - } + if slices.Contains(hl.patterns, "*") { + return true } for _, builtin := range hl.builtins { switch builtin { diff --git a/modules/htmlutil/html.go b/modules/htmlutil/html.go index 0ab0e71689..efbc174b2e 100644 --- a/modules/htmlutil/html.go +++ b/modules/htmlutil/html.go @@ -7,6 +7,7 @@ import ( "fmt" "html/template" "slices" + "strings" ) // ParseSizeAndClass get size and class from string with default values @@ -31,6 +32,9 @@ func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int } func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { + if !strings.Contains(string(s), "%") || len(rawArgs) == 0 { + panic("HTMLFormat requires one or more arguments") + } args := slices.Clone(rawArgs) for i, v := range args { switch v := v.(type) { @@ -38,6 +42,8 @@ func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { // for most basic types (including template.HTML which is safe), just do nothing and use it case string: args[i] = template.HTMLEscapeString(v) + case template.URL: + args[i] = template.HTMLEscapeString(string(v)) case fmt.Stringer: args[i] = template.HTMLEscapeString(v.String()) default: diff --git a/modules/htmlutil/html_test.go b/modules/htmlutil/html_test.go index 5ff05d75b3..22258ce59d 100644 --- a/modules/htmlutil/html_test.go +++ b/modules/htmlutil/html_test.go @@ -10,6 +10,15 @@ import ( "github.com/stretchr/testify/assert" ) +type testStringer struct{} + +func (t testStringer) String() string { + return "&StringMethod" +} + func TestHTMLFormat(t *testing.T) { assert.Equal(t, template.HTML("<a>< < 1</a>"), HTMLFormat("<a>%s %s %d</a>", "<", template.HTML("<"), 1)) + assert.Equal(t, template.HTML("%!s(<nil>)"), HTMLFormat("%s", nil)) + assert.Equal(t, template.HTML("<>"), HTMLFormat("%s", template.URL("<>"))) + assert.Equal(t, template.HTML("&StringMethod &StringMethod"), HTMLFormat("%s %s", testStringer{}, &testStringer{})) } diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index 045b00d944..dd3efab7a5 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -79,7 +79,7 @@ func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag strin func checkIfNoneMatchIsValid(req *http.Request, etag string) bool { ifNoneMatch := req.Header.Get("If-None-Match") if len(ifNoneMatch) > 0 { - for _, item := range strings.Split(ifNoneMatch, ",") { + for item := range strings.SplitSeq(ifNoneMatch, ",") { item = strings.TrimPrefix(strings.TrimSpace(item), "W/") // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives if item == etag { return true diff --git a/modules/indexer/code/bleve/token/path/path.go b/modules/indexer/code/bleve/token/path/path.go index ae24e84974..6dfc12f146 100644 --- a/modules/indexer/code/bleve/token/path/path.go +++ b/modules/indexer/code/bleve/token/path/path.go @@ -51,7 +51,7 @@ func generatePathTokens(input analysis.TokenStream, reversed bool) analysis.Toke slices.Reverse(input) } - for i := 0; i < len(input); i++ { + for i := range input { var sb strings.Builder sb.Write(input[0].Term) diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go index 0089dd259f..41bc74e6ec 100644 --- a/modules/indexer/code/git.go +++ b/modules/indexer/code/git.go @@ -129,8 +129,8 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio changes.Updates = append(changes.Updates, updates...) return nil } - lines := strings.Split(stdout, "\n") - for _, line := range lines { + lines := strings.SplitSeq(stdout, "\n") + for line := range lines { line = strings.TrimSpace(line) if len(line) == 0 { continue diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index e37aff8e59..a7a5d7d2e3 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -77,7 +77,7 @@ func HighlightSearchResultCode(filename, language string, lineNums []int, code s // The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n` lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums))) - for i := 0; i < len(lines); i++ { + for i := range lines { lines[i] = &ResultLine{ Num: lineNums[i], FormattedContent: template.HTML(highlightedLines[i]), diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index 9e63ad1ad8..8f25c84b76 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -217,7 +217,7 @@ func PopulateIssueIndexer(ctx context.Context) error { return fmt.Errorf("shutdown before completion: %w", ctx.Err()) default: } - repos, _, err := repo_model.SearchRepositoryByName(ctx, &repo_model.SearchRepoOptions{ + repos, _, err := repo_model.SearchRepositoryByName(ctx, repo_model.SearchRepoOptions{ ListOptions: db_model.ListOptions{Page: page, PageSize: repo_model.RepositoryListDefaultPageSize}, OrderBy: db_model.SearchOrderByID, Private: true, diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index a42ec9a2bc..7aebbbcd58 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -8,7 +8,6 @@ package tests import ( - "context" "fmt" "slices" "testing" @@ -40,7 +39,7 @@ func TestIndexer(t *testing.T, indexer internal.Indexer) { data[v.ID] = v } require.NoError(t, indexer.Index(t.Context(), d...)) - require.NoError(t, waitData(indexer, int64(len(data)))) + waitData(t, indexer, int64(len(data))) } defer func() { @@ -54,13 +53,13 @@ func TestIndexer(t *testing.T, indexer internal.Indexer) { for _, v := range c.ExtraData { data[v.ID] = v } - require.NoError(t, waitData(indexer, int64(len(data)))) + waitData(t, indexer, int64(len(data))) defer func() { for _, v := range c.ExtraData { require.NoError(t, indexer.Delete(t.Context(), v.ID)) delete(data, v.ID) } - require.NoError(t, waitData(indexer, int64(len(data)))) + waitData(t, indexer, int64(len(data))) }() } @@ -751,22 +750,10 @@ func countIndexerData(data map[int64]*internal.IndexerData, f func(v *internal.I // waitData waits for the indexer to index all data. // Some engines like Elasticsearch index data asynchronously, so we need to wait for a while. -func waitData(indexer internal.Indexer, total int64) error { - var actual int64 - for i := 0; i < 100; i++ { - result, err := indexer.Search(context.Background(), &internal.SearchOptions{ - Paginator: &db.ListOptions{ - PageSize: 0, - }, - }) - if err != nil { - return err - } - actual = result.Total - if actual == total { - return nil - } - time.Sleep(100 * time.Millisecond) - } - return fmt.Errorf("waitData: expected %d, actual %d", total, actual) +func waitData(t *testing.T, indexer internal.Indexer, total int64) { + assert.Eventually(t, func() bool { + result, err := indexer.Search(t.Context(), &internal.SearchOptions{Paginator: &db.ListOptions{}}) + require.NoError(t, err) + return result.Total == total + }, 10*time.Second, 100*time.Millisecond, "expected total=%d", total) } diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go index 84ae90e4ed..192aaf8e01 100644 --- a/modules/issue/template/template.go +++ b/modules/issue/template/template.go @@ -8,6 +8,7 @@ import ( "fmt" "net/url" "regexp" + "slices" "strconv" "strings" @@ -447,12 +448,7 @@ func (o *valuedOption) IsChecked() bool { case api.IssueFormFieldTypeDropdown: checks := strings.Split(o.field.Get("form-field-"+o.field.ID), ",") idx := strconv.Itoa(o.index) - for _, v := range checks { - if v == idx { - return true - } - } - return false + return slices.Contains(checks, idx) case api.IssueFormFieldTypeCheckboxes: return o.field.Get(fmt.Sprintf("form-field-%s-%d", o.field.ID, o.index)) == "on" } diff --git a/modules/json/json.go b/modules/json/json.go index acd4118573..444dc8526a 100644 --- a/modules/json/json.go +++ b/modules/json/json.go @@ -3,11 +3,10 @@ package json -// Allow "encoding/json" import. import ( "bytes" "encoding/binary" - "encoding/json" //nolint:depguard + "encoding/json" //nolint:depguard // this package wraps it "io" jsoniter "github.com/json-iterator/go" diff --git a/modules/label/label.go b/modules/label/label.go index ce028aa9f3..3e68c4d26e 100644 --- a/modules/label/label.go +++ b/modules/label/label.go @@ -7,10 +7,10 @@ import ( "fmt" "regexp" "strings" -) + "sync" -// colorPattern is a regexp which can validate label color -var colorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") + "code.gitea.io/gitea/modules/util" +) // Label represents label information loaded from template type Label struct { @@ -21,6 +21,10 @@ type Label struct { ExclusiveOrder int `yaml:"exclusive_order,omitempty"` } +var colorPattern = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(`^#([\da-fA-F]{3}|[\da-fA-F]{6})$`) +}) + // NormalizeColor normalizes a color string to a 6-character hex code func NormalizeColor(color string) (string, error) { // normalize case @@ -31,8 +35,8 @@ func NormalizeColor(color string) (string, error) { color = "#" + color } - if !colorPattern.MatchString(color) { - return "", fmt.Errorf("bad color code: %s", color) + if !colorPattern().MatchString(color) { + return "", util.NewInvalidArgumentErrorf("invalid color: %s", color) } // convert 3-character shorthand into 6-character version diff --git a/modules/label/parser.go b/modules/label/parser.go index 511bac823f..2a10152062 100644 --- a/modules/label/parser.go +++ b/modules/label/parser.go @@ -72,7 +72,7 @@ func parseYamlFormat(fileName string, data []byte) ([]*Label, error) { func parseLegacyFormat(fileName string, data []byte) ([]*Label, error) { lines := strings.Split(string(data), "\n") list := make([]*Label, 0, len(lines)) - for i := 0; i < len(lines); i++ { + for i := range lines { line := strings.TrimSpace(lines[i]) if len(line) == 0 { continue @@ -108,7 +108,7 @@ func LoadTemplateDescription(fileName string) (string, error) { return "", err } - for i := 0; i < len(list); i++ { + for i := range list { if i > 0 { buf.WriteString(", ") } diff --git a/modules/lfs/pointer.go b/modules/lfs/pointer.go index ebde20f826..9c95613057 100644 --- a/modules/lfs/pointer.go +++ b/modules/lfs/pointer.go @@ -15,15 +15,13 @@ import ( "strings" ) +// spec: https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md const ( - blobSizeCutoff = 1024 + MetaFileMaxSize = 1024 // spec says the maximum size of a pointer file must be smaller than 1024 - // MetaFileIdentifier is the string appearing at the first line of LFS pointer files. - // https://github.com/git-lfs/git-lfs/blob/master/docs/spec.md - MetaFileIdentifier = "version https://git-lfs.github.com/spec/v1" + MetaFileIdentifier = "version https://git-lfs.github.com/spec/v1" // the first line of a pointer file - // MetaFileOidPrefix appears in LFS pointer files on a line before the sha256 hash. - MetaFileOidPrefix = "oid sha256:" + MetaFileOidPrefix = "oid sha256:" // spec says the only supported hash is sha256 at the moment ) var ( @@ -39,7 +37,7 @@ var ( // ReadPointer tries to read LFS pointer data from the reader func ReadPointer(reader io.Reader) (Pointer, error) { - buf := make([]byte, blobSizeCutoff) + buf := make([]byte, MetaFileMaxSize) n, err := io.ReadFull(reader, buf) if err != nil && err != io.ErrUnexpectedEOF { return Pointer{}, err @@ -65,6 +63,7 @@ func ReadPointerFromBuffer(buf []byte) (Pointer, error) { return p, ErrInvalidStructure } + // spec says "key/value pairs MUST be sorted alphabetically in ascending order (version is exception and must be the first)" oid := strings.TrimPrefix(splitLines[1], MetaFileOidPrefix) if len(oid) != 64 || !oidPattern.MatchString(oid) { return p, ErrInvalidOIDFormat diff --git a/modules/lfs/pointer_scanner_gogit.go b/modules/lfs/pointer_scanner_gogit.go index f4302c23bc..e153b8e24e 100644 --- a/modules/lfs/pointer_scanner_gogit.go +++ b/modules/lfs/pointer_scanner_gogit.go @@ -31,7 +31,7 @@ func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan c default: } - if blob.Size > blobSizeCutoff { + if blob.Size > MetaFileMaxSize { return nil } diff --git a/modules/lfstransfer/backend/util.go b/modules/lfstransfer/backend/util.go index 98ce0b1e62..afe02f799c 100644 --- a/modules/lfstransfer/backend/util.go +++ b/modules/lfstransfer/backend/util.go @@ -132,6 +132,7 @@ func newInternalRequestLFS(ctx context.Context, internalURL, method string, head return nil } req := private.NewInternalRequest(ctx, internalURL, method) + req.SetReadWriteTimeout(0) for k, v := range headers { req.Header(k, v) } diff --git a/modules/log/event_format.go b/modules/log/event_format.go index c23b3b411b..4cf471d223 100644 --- a/modules/log/event_format.go +++ b/modules/log/event_format.go @@ -212,7 +212,7 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms } } if hasColorValue { - msg = []byte(fmt.Sprintf(msgFormat, msgArgs...)) + msg = fmt.Appendf(nil, msgFormat, msgArgs...) } } // try to re-use the pre-formatted simple text message @@ -243,8 +243,8 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms buf = append(buf, msg...) if event.Stacktrace != "" && mode.StacktraceLevel <= event.Level { - lines := bytes.Split([]byte(event.Stacktrace), []byte("\n")) - for _, line := range lines { + lines := bytes.SplitSeq([]byte(event.Stacktrace), []byte("\n")) + for line := range lines { buf = append(buf, "\n\t"...) buf = append(buf, line...) } diff --git a/modules/log/flags.go b/modules/log/flags.go index 8064c91745..f409261150 100644 --- a/modules/log/flags.go +++ b/modules/log/flags.go @@ -123,7 +123,7 @@ func FlagsFromString(from string, def ...uint32) Flags { return Flags{defined: true, flags: def[0]} } flags := uint32(0) - for _, flag := range strings.Split(strings.ToLower(from), ",") { + for flag := range strings.SplitSeq(strings.ToLower(from), ",") { flags |= flagFromString[strings.TrimSpace(flag)] } return Flags{defined: true, flags: flags} diff --git a/modules/log/level_test.go b/modules/log/level_test.go index cd18a807d8..0e59af6cb7 100644 --- a/modules/log/level_test.go +++ b/modules/log/level_test.go @@ -32,11 +32,11 @@ func TestLevelMarshalUnmarshalJSON(t *testing.T) { assert.NoError(t, err) assert.Equal(t, INFO, testLevel.Level) - err = json.Unmarshal([]byte(fmt.Sprintf(`{"level":%d}`, 2)), &testLevel) + err = json.Unmarshal(fmt.Appendf(nil, `{"level":%d}`, 2), &testLevel) assert.NoError(t, err) assert.Equal(t, INFO, testLevel.Level) - err = json.Unmarshal([]byte(fmt.Sprintf(`{"level":%d}`, 10012)), &testLevel) + err = json.Unmarshal(fmt.Appendf(nil, `{"level":%d}`, 10012), &testLevel) assert.NoError(t, err) assert.Equal(t, INFO, testLevel.Level) @@ -51,5 +51,5 @@ func TestLevelMarshalUnmarshalJSON(t *testing.T) { } func makeTestLevelBytes(level string) []byte { - return []byte(fmt.Sprintf(`{"level":"%s"}`, level)) + return fmt.Appendf(nil, `{"level":"%s"}`, level) } diff --git a/modules/log/logger.go b/modules/log/logger.go index 3fc524d55e..8b89e0eb5a 100644 --- a/modules/log/logger.go +++ b/modules/log/logger.go @@ -45,6 +45,6 @@ type Logger interface { LevelLogger } -type LogStringer interface { //nolint:revive +type LogStringer interface { //nolint:revive // export stutter LogString() string } diff --git a/modules/markup/common/footnote.go b/modules/markup/common/footnote.go index 9a4f18ed7f..1ece436c66 100644 --- a/modules/markup/common/footnote.go +++ b/modules/markup/common/footnote.go @@ -197,7 +197,7 @@ func (b *footnoteBlockParser) Open(parent ast.Node, reader text.Reader, pc parse return nil, parser.NoChildren } open := pos + 1 - closure := util.FindClosure(line[pos+1:], '[', ']', false, false) //nolint + closure := util.FindClosure(line[pos+1:], '[', ']', false, false) //nolint:staticcheck // deprecated function closes := pos + 1 + closure next := closes + 1 if closure > -1 { @@ -287,7 +287,7 @@ func (s *footnoteParser) Parse(parent ast.Node, block text.Reader, pc parser.Con return nil } open := pos - closure := util.FindClosure(line[pos:], '[', ']', false, false) //nolint + closure := util.FindClosure(line[pos:], '[', ']', false, false) //nolint:staticcheck // deprecated function if closure < 0 { return nil } @@ -409,9 +409,9 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt _, _ = w.Write(n.Name) _, _ = w.WriteString(`"><a href="#fn:`) _, _ = w.Write(n.Name) - _, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) + _, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) // FIXME: here and below, need to keep the classes _, _ = w.WriteString(is) - _, _ = w.WriteString(`</a></sup>`) + _, _ = w.WriteString(` </a></sup>`) // the style doesn't work at the moment, so add a space to separate the names } return ast.WalkContinue, nil } diff --git a/modules/markup/console/console.go b/modules/markup/console/console.go index 06f3acfa68..492579b0a5 100644 --- a/modules/markup/console/console.go +++ b/modules/markup/console/console.go @@ -6,13 +6,14 @@ package console import ( "bytes" "io" - "path" + "unicode/utf8" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/typesniffer" + "code.gitea.io/gitea/modules/util" trend "github.com/buildkite/terminal-to-html/v3" - "github.com/go-enry/go-enry/v2" ) func init() { @@ -22,6 +23,8 @@ func init() { // Renderer implements markup.Renderer type Renderer struct{} +var _ markup.RendererContentDetector = (*Renderer)(nil) + // Name implements markup.Renderer func (Renderer) Name() string { return "console" @@ -40,15 +43,36 @@ func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { } // CanRender implements markup.RendererContentDetector -func (Renderer) CanRender(filename string, input io.Reader) bool { - buf, err := io.ReadAll(input) - if err != nil { +func (Renderer) CanRender(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) bool { + if !sniffedType.IsTextPlain() { return false } - if enry.GetLanguage(path.Base(filename), buf) != enry.OtherLanguage { + + s := util.UnsafeBytesToString(prefetchBuf) + rs := []rune(s) + cnt := 0 + firstErrPos := -1 + isCtrlSep := func(p int) bool { + return p < len(rs) && (rs[p] == ';' || rs[p] == 'm') + } + for i, c := range rs { + if c == 0 { + return false + } + if c == '\x1b' { + match := i+1 < len(rs) && rs[i+1] == '[' + if match && (isCtrlSep(i+2) || isCtrlSep(i+3) || isCtrlSep(i+4) || isCtrlSep(i+5)) { + cnt++ + } + } + if c == utf8.RuneError && firstErrPos == -1 { + firstErrPos = i + } + } + if firstErrPos != -1 && firstErrPos != len(rs)-1 { return false } - return bytes.ContainsRune(buf, '\x1b') + return cnt >= 2 // only render it as console output if there are at least two escape sequences } // Render renders terminal colors to HTML with all specific handling stuff. diff --git a/modules/markup/console/console_test.go b/modules/markup/console/console_test.go index 539f965ea1..d1192bebc2 100644 --- a/modules/markup/console/console_test.go +++ b/modules/markup/console/console_test.go @@ -8,23 +8,39 @@ import ( "testing" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/typesniffer" "github.com/stretchr/testify/assert" ) func TestRenderConsole(t *testing.T) { - var render Renderer - kases := map[string]string{ - "\x1b[37m\x1b[40mnpm\x1b[0m \x1b[0m\x1b[32minfo\x1b[0m \x1b[0m\x1b[35mit worked if it ends with\x1b[0m ok": "<span class=\"term-fg37 term-bg40\">npm</span> <span class=\"term-fg32\">info</span> <span class=\"term-fg35\">it worked if it ends with</span> ok", + cases := []struct { + input string + expected string + }{ + {"\x1b[37m\x1b[40mnpm\x1b[0m \x1b[0m\x1b[32minfo\x1b[0m \x1b[0m\x1b[35mit worked if it ends with\x1b[0m ok", `<span class="term-fg37 term-bg40">npm</span> <span class="term-fg32">info</span> <span class="term-fg35">it worked if it ends with</span> ok`}, + {"\x1b[1;2m \x1b[123m 啊", `<span class="term-fg2"> 啊</span>`}, + {"\x1b[1;2m \x1b[123m \xef", `<span class="term-fg2"> �</span>`}, + {"\x1b[1;2m \x1b[123m \xef \xef", ``}, + {"\x1b[12", ``}, + {"\x1b[1", ``}, + {"\x1b[FOO\x1b[", ``}, + {"\x1b[mFOO\x1b[m", `FOO`}, } - for k, v := range kases { + var render Renderer + for i, c := range cases { var buf strings.Builder - canRender := render.CanRender("test", strings.NewReader(k)) - assert.True(t, canRender) + st := typesniffer.DetectContentType([]byte(c.input)) + canRender := render.CanRender("test", st, []byte(c.input)) + if c.expected == "" { + assert.False(t, canRender, "case %d: expected not to render", i) + continue + } - err := render.Render(markup.NewRenderContext(t.Context()), strings.NewReader(k), &buf) + assert.True(t, canRender) + err := render.Render(markup.NewRenderContext(t.Context()), strings.NewReader(c.input), &buf) assert.NoError(t, err) - assert.Equal(t, v, buf.String()) + assert.Equal(t, c.expected, buf.String()) } } diff --git a/modules/markup/html.go b/modules/markup/html.go index 7c3bd93699..51afd4be00 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "regexp" + "slices" "strings" "sync" @@ -86,8 +87,8 @@ var globalVars = sync.OnceValue(func() *globalVarsType { // codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20" v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`) - // cleans: "<foo/bar", "<any words/", ("<html", "<head", "<script", "<style") - v.tagCleaner = regexp.MustCompile(`(?i)<(/?\w+/\w+|/[\w ]+/|/?(html|head|script|style\b))`) + // cleans: "<foo/bar", "<any words/", ("<html", "<head", "<script", "<style", "<?", "<%") + v.tagCleaner = regexp.MustCompile(`(?i)<(/?\w+/\w+|/[\w ]+/|/?(html|head|script|style|%|\?)\b)`) v.nulCleaner = strings.NewReplacer("\000", "") return v }) @@ -109,13 +110,7 @@ func CustomLinkURLSchemes(schemes []string) { if !validScheme.MatchString(s) { continue } - without := false - for _, sna := range xurls.SchemesNoAuthority { - if s == sna { - without = true - break - } - } + without := slices.Contains(xurls.SchemesNoAuthority, s) if without { s += ":" } else { @@ -253,7 +248,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output node, err := html.Parse(io.MultiReader( // prepend "<html><body>" strings.NewReader("<html><body>"), - // Strip out nuls - they're always invalid + // strip out NULLs (they're always invalid), and escape known tags bytes.NewReader(globalVars().tagCleaner.ReplaceAll([]byte(globalVars().nulCleaner.Replace(string(rawHTML))), []byte("<$1"))), // close the tags strings.NewReader("</body></html>"), @@ -320,6 +315,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod } processNodeAttrID(node) + processFootnoteNode(ctx, node) // FIXME: the footnote processing should be done in the "footnote.go" renderer directly if isEmojiNode(node) { // TextNode emoji will be converted to `<span class="emoji">`, then the next iteration will visit the "span" diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go index 967c327f36..fe7a034967 100644 --- a/modules/markup/html_commit.go +++ b/modules/markup/html_commit.go @@ -62,7 +62,7 @@ func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) { // if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence. ret.PosEnd-- ret.FullURL = ret.FullURL[:len(ret.FullURL)-1] - for i := 0; i < len(m); i++ { + for i := range m { m[i] = min(m[i], ret.PosEnd) } } diff --git a/modules/markup/html_issue_test.go b/modules/markup/html_issue_test.go index c68429641f..39cd9dcf6a 100644 --- a/modules/markup/html_issue_test.go +++ b/modules/markup/html_issue_test.go @@ -30,6 +30,7 @@ func TestRender_IssueList(t *testing.T) { rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{ "user": "test-user", "repo": "test-repo", "markupAllowShortIssuePattern": "true", + "footnoteContextId": "12345", }) out, err := markdown.RenderString(rctx, input) require.NoError(t, err) @@ -69,4 +70,22 @@ func TestRender_IssueList(t *testing.T) { </ul>`, ) }) + + t.Run("IssueFootnote", func(t *testing.T) { + test( + "foo[^1][^2]\n\n[^1]: bar\n[^2]: baz", + `<p>foo<sup id="fnref:user-content-1-12345"><a href="#fn:user-content-1-12345" rel="nofollow">1 </a></sup><sup id="fnref:user-content-2-12345"><a href="#fn:user-content-2-12345" rel="nofollow">2 </a></sup></p> +<div> +<hr/> +<ol> +<li id="fn:user-content-1-12345"> +<p>bar <a href="#fnref:user-content-1-12345" rel="nofollow">↩︎</a></p> +</li> +<li id="fn:user-content-2-12345"> +<p>baz <a href="#fnref:user-content-2-12345" rel="nofollow">↩︎</a></p> +</li> +</ol> +</div>`, + ) + }) } diff --git a/modules/markup/html_link.go b/modules/markup/html_link.go index 1ea0b14028..43faef1681 100644 --- a/modules/markup/html_link.go +++ b/modules/markup/html_link.go @@ -31,8 +31,8 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { // It makes page handling terrible, but we prefer GitHub syntax // And fall back to MediaWiki only when it is obvious from the look // Of text and link contents - sl := strings.Split(content, "|") - for _, v := range sl { + sl := strings.SplitSeq(content, "|") + for v := range sl { if equalPos := strings.IndexByte(v, '='); equalPos == -1 { // There is no equal in this argument; this is a mandatory arg if props["name"] == "" { diff --git a/modules/markup/html_node.go b/modules/markup/html_node.go index 68858b024a..4eb78fdd2b 100644 --- a/modules/markup/html_node.go +++ b/modules/markup/html_node.go @@ -15,6 +15,14 @@ func isAnchorIDUserContent(s string) bool { return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-") } +func isAnchorIDFootnote(s string) bool { + return strings.HasPrefix(s, "fnref:user-content-") || strings.HasPrefix(s, "fn:user-content-") +} + +func isAnchorHrefFootnote(s string) bool { + return strings.HasPrefix(s, "#fnref:user-content-") || strings.HasPrefix(s, "#fn:user-content-") +} + func processNodeAttrID(node *html.Node) { // Add user-content- to IDs and "#" links if they don't already have them, // and convert the link href to a relative link to the host root @@ -27,6 +35,18 @@ func processNodeAttrID(node *html.Node) { } } +func processFootnoteNode(ctx *RenderContext, node *html.Node) { + for idx, attr := range node.Attr { + if (attr.Key == "id" && isAnchorIDFootnote(attr.Val)) || + (attr.Key == "href" && isAnchorHrefFootnote(attr.Val)) { + if footnoteContextID := ctx.RenderOptions.Metas["footnoteContextId"]; footnoteContextID != "" { + node.Attr[idx].Val = attr.Val + "-" + footnoteContextID + } + continue + } + } +} + func processNodeA(ctx *RenderContext, node *html.Node) { for idx, attr := range node.Attr { if attr.Key == "href" { @@ -43,8 +63,11 @@ func processNodeA(ctx *RenderContext, node *html.Node) { func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { next = img.NextSibling + attrSrc, hasLazy := "", false for i, imgAttr := range img.Attr { + hasLazy = hasLazy || imgAttr.Key == "loading" && imgAttr.Val == "lazy" if imgAttr.Key != "src" { + attrSrc = imgAttr.Val continue } @@ -52,8 +75,8 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { isLinkable := imgSrcOrigin != "" && !strings.HasPrefix(imgSrcOrigin, "data:") // By default, the "<img>" tag should also be clickable, - // because frontend use `<img>` to paste the re-scaled image into the markdown, - // so it must match the default markdown image behavior. + // because frontend uses `<img>` to paste the re-scaled image into the Markdown, + // so it must match the default Markdown image behavior. cnt := 0 for p := img.Parent; isLinkable && p != nil && cnt < 2; p = p.Parent { if hasParentAnchor := p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor { @@ -78,6 +101,9 @@ func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) { imgAttr.Val = camoHandleLink(imgAttr.Val) img.Attr[i] = imgAttr } + if !RenderBehaviorForTesting.DisableAdditionalAttributes && !hasLazy && !strings.HasPrefix(attrSrc, "data:") { + img.Attr = append(img.Attr, html.Attribute{Key: "loading", Val: "lazy"}) + } return next } diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 58f71bdd7b..5fdbf43f7c 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -525,6 +525,10 @@ func TestPostProcess(t *testing.T) { test("<script>a</script>", `<script>a</script>`) test("<STYLE>a", `<STYLE>a`) test("<style>a</STYLE>", `<style>a</STYLE>`) + + // other special tags, our special behavior + test("<?php\nfoo", "<?php\nfoo") + test("<%asp\nfoo", "<%asp\nfoo") } func TestIssue16020(t *testing.T) { diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 79df547c2c..3b788432ba 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -182,10 +182,7 @@ func render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error rc := &RenderConfig{Meta: markup.RenderMetaAsDetails} buf, _ = ExtractMetadataBytes(buf, rc) - metaLength := bufWithMetadataLength - len(buf) - if metaLength < 0 { - metaLength = 0 - } + metaLength := max(bufWithMetadataLength-len(buf), 0) rc.metaLength = metaLength pc.Set(renderConfigKey, rc) diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 2310895fc3..4eb01bcc2d 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -47,7 +47,7 @@ func TestRender_StandardLinks(t *testing.T) { func TestRender_Images(t *testing.T) { setting.AppURL = AppURL - test := func(input, expected string) { + render := func(input, expected string) { buffer, err := markdown.RenderString(markup.NewTestRenderContext(FullURL), input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) @@ -59,27 +59,32 @@ func TestRender_Images(t *testing.T) { result := util.URLJoin(FullURL, url) // hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now - test( + render( "", `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`) - test( + render( "[["+title+"|"+url+"]]", `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`) - test( + render( "[]("+href+")", `<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`) - test( + render( "", `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`) - test( + render( "[["+title+"|"+url+"]]", `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`) - test( + render( "[]("+href+")", `<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`) + + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, false)() + render( + "<a><img src='a.jpg'></a>", // by the way, empty "a" tag will be removed + `<p dir="auto"><img src="http://localhost:3000/user13/repo11/a.jpg" loading="lazy"/></p>`) } func TestTotal_RenderString(t *testing.T) { @@ -223,7 +228,7 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno <dd>This is another definition of the second term.</dd> </dl> <h3 id="user-content-footnotes">Footnotes</h3> -<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1</a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2</a></sup></p> +<p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1 </a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2 </a></sup></p> <div> <hr/> <ol> @@ -252,7 +257,7 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno return username == "r-lyeh" }, }) - for i := 0; i < len(sameCases); i++ { + for i := range sameCases { line, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), sameCases[i]) assert.NoError(t, err) assert.Equal(t, testAnswers[i], string(line)) diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go index 412e4d0dee..95a336a02c 100644 --- a/modules/markup/markdown/math/block_renderer.go +++ b/modules/markup/markdown/math/block_renderer.go @@ -42,7 +42,7 @@ func (r *BlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) { l := n.Lines().Len() - for i := 0; i < l; i++ { + for i := range l { line := n.Lines().At(i) _, _ = w.Write(util.EscapeHTML(line.Value(source))) } @@ -51,8 +51,8 @@ func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { n := node.(*Block) if entering { - code := giteaUtil.Iif(n.Inline, "", `<pre class="code-block is-loading">`) + `<code class="language-math display">` - _ = r.renderInternal.FormatWithSafeAttrs(w, template.HTML(code)) + codeHTML := giteaUtil.Iif[template.HTML](n.Inline, "", `<pre class="code-block is-loading">`) + `<code class="language-math display">` + _, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(codeHTML))) r.writeLines(w, source, n) } else { _, _ = w.WriteString(`</code>` + giteaUtil.Iif(n.Inline, "", `</pre>`) + "\n") diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go index d000a7b317..eeeb60cc7e 100644 --- a/modules/markup/markdown/math/inline_renderer.go +++ b/modules/markup/markdown/math/inline_renderer.go @@ -28,7 +28,7 @@ func NewInlineRenderer(renderInternal *internal.RenderInternal) renderer.NodeRen func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if entering { - _ = r.renderInternal.FormatWithSafeAttrs(w, `<code class="language-math">`) + _, _ = w.WriteString(string(r.renderInternal.ProtectSafeAttrs(`<code class="language-math">`))) for c := n.FirstChild(); c != nil; c = c.NextSibling() { segment := c.(*ast.Text).Segment value := util.EscapeHTML(segment.Value(source)) diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go index 3f74adeaef..283d289d48 100644 --- a/modules/markup/markdown/meta_test.go +++ b/modules/markup/markdown/meta_test.go @@ -60,7 +60,7 @@ func TestExtractMetadata(t *testing.T) { func TestExtractMetadataBytes(t *testing.T) { t.Run("ValidFrontAndBody", func(t *testing.T) { var meta IssueTemplate - body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta) + body, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest), &meta) assert.NoError(t, err) assert.Equal(t, bodyTest, string(body)) assert.Equal(t, metaTest, meta) @@ -69,19 +69,19 @@ func TestExtractMetadataBytes(t *testing.T) { t.Run("NoFirstSeparator", func(t *testing.T) { var meta IssueTemplate - _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest)), &meta) + _, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", frontTest, sepTest, bodyTest), &meta) assert.Error(t, err) }) t.Run("NoLastSeparator", func(t *testing.T) { var meta IssueTemplate - _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest)), &meta) + _, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", sepTest, frontTest, bodyTest), &meta) assert.Error(t, err) }) t.Run("NoBody", func(t *testing.T) { var meta IssueTemplate - body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta) + body, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", sepTest, frontTest, sepTest), &meta) assert.NoError(t, err) assert.Empty(t, string(body)) assert.Equal(t, metaTest, meta) diff --git a/modules/markup/markdown/transform_blockquote.go b/modules/markup/markdown/transform_blockquote.go index 3a8c6fa018..bf17f01681 100644 --- a/modules/markup/markdown/transform_blockquote.go +++ b/modules/markup/markdown/transform_blockquote.go @@ -46,7 +46,7 @@ func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.N if !ok { return "", nil } - val1 := string(node1.Text(reader.Source())) //nolint:staticcheck + val1 := string(node1.Text(reader.Source())) //nolint:staticcheck // Text is deprecated attentionType := strings.ToLower(val1) if g.attentionTypes.Contains(attentionType) { return attentionType, []ast.Node{node1} diff --git a/modules/markup/markdown/transform_codespan.go b/modules/markup/markdown/transform_codespan.go index bccc43aad2..c2e4295bc2 100644 --- a/modules/markup/markdown/transform_codespan.go +++ b/modules/markup/markdown/transform_codespan.go @@ -68,7 +68,7 @@ func cssColorHandler(value string) bool { } func (g *ASTTransformer) transformCodeSpan(_ *markup.RenderContext, v *ast.CodeSpan, reader text.Reader) { - colorContent := v.Text(reader.Source()) //nolint:staticcheck + colorContent := v.Text(reader.Source()) //nolint:staticcheck // Text is deprecated if cssColorHandler(string(colorContent)) { v.AppendChild(v, NewColorPreview(colorContent)) } diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go index 5f8a12794d..a229a7b1a4 100644 --- a/modules/markup/markdown/transform_heading.go +++ b/modules/markup/markdown/transform_heading.go @@ -16,10 +16,10 @@ import ( func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]Header) { for _, attr := range v.Attributes() { if _, ok := attr.Value.([]byte); !ok { - v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) + v.SetAttribute(attr.Name, fmt.Appendf(nil, "%v", attr.Value)) } } - txt := v.Text(reader.Source()) //nolint:staticcheck + txt := v.Text(reader.Source()) //nolint:staticcheck // Text is deprecated header := Header{ Text: util.UnsafeBytesToString(txt), Level: v.Level, diff --git a/modules/markup/mdstripper/mdstripper.go b/modules/markup/mdstripper/mdstripper.go index c589926b5e..6e392444b4 100644 --- a/modules/markup/mdstripper/mdstripper.go +++ b/modules/markup/mdstripper/mdstripper.go @@ -46,7 +46,7 @@ func (r *stripRenderer) Render(w io.Writer, source []byte, doc ast.Node) error { coalesce := prevSibIsText r.processString( w, - v.Text(source), //nolint:staticcheck + v.Text(source), //nolint:staticcheck // Text is deprecated coalesce) if v.SoftLineBreak() { r.doubleSpace(w) diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index 35f90eb46c..b6e9c348b7 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -4,12 +4,12 @@ package markup import ( - "bytes" "io" "path" "strings" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/typesniffer" ) // Renderer defines an interface for rendering markup file to HTML @@ -37,7 +37,7 @@ type ExternalRenderer interface { // RendererContentDetector detects if the content can be rendered // by specified renderer type RendererContentDetector interface { - CanRender(filename string, input io.Reader) bool + CanRender(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) bool } var ( @@ -60,13 +60,9 @@ func GetRendererByFileName(filename string) Renderer { } // DetectRendererType detects the markup type of the content -func DetectRendererType(filename string, input io.Reader) string { - buf, err := io.ReadAll(input) - if err != nil { - return "" - } +func DetectRendererType(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) string { for _, renderer := range renderers { - if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, bytes.NewReader(buf)) { + if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, sniffedType, prefetchBuf) { return renderer.Name() } } diff --git a/modules/markup/sanitizer_default.go b/modules/markup/sanitizer_default.go index 14161eb533..0fbf0f0b24 100644 --- a/modules/markup/sanitizer_default.go +++ b/modules/markup/sanitizer_default.go @@ -4,6 +4,7 @@ package markup import ( + "html/template" "io" "net/url" "regexp" @@ -52,6 +53,8 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") + policy.AllowAttrs("loading").OnElements("img") + // Allow generally safe attributes (reference: https://github.com/jch/html-pipeline) generalSafeAttrs := []string{ "abbr", "accept", "accept-charset", @@ -90,9 +93,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { return policy } -// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist. -func Sanitize(s string) string { - return GetDefaultSanitizer().defaultPolicy.Sanitize(s) +// Sanitize use default sanitizer policy to sanitize a string +func Sanitize(s string) template.HTML { + return template.HTML(GetDefaultSanitizer().defaultPolicy.Sanitize(s)) } // SanitizeReader sanitizes a Reader diff --git a/modules/markup/sanitizer_default_test.go b/modules/markup/sanitizer_default_test.go index 5282916944..e5ba018e1b 100644 --- a/modules/markup/sanitizer_default_test.go +++ b/modules/markup/sanitizer_default_test.go @@ -69,6 +69,6 @@ func TestSanitizer(t *testing.T) { } for i := 0; i < len(testCases); i += 2 { - assert.Equal(t, testCases[i+1], Sanitize(testCases[i])) + assert.Equal(t, testCases[i+1], string(Sanitize(testCases[i]))) } } diff --git a/modules/migration/schemas_bindata.go b/modules/migration/schemas_bindata.go index c5db3b3461..695c2c1135 100644 --- a/modules/migration/schemas_bindata.go +++ b/modules/migration/schemas_bindata.go @@ -3,6 +3,28 @@ //go:build bindata +//go:generate go run ../../build/generate-bindata.go ../../modules/migration/schemas bindata.dat + package migration -//go:generate go run ../../build/generate-bindata.go ../../modules/migration/schemas migration bindata.go +import ( + "io" + "io/fs" + "path" + "sync" + + _ "embed" + + "code.gitea.io/gitea/modules/assetfs" +) + +//go:embed bindata.dat +var bindata []byte + +var BuiltinAssets = sync.OnceValue(func() fs.FS { + return assetfs.NewEmbeddedFS(bindata) +}) + +func openSchema(filename string) (io.ReadCloser, error) { + return BuiltinAssets().Open(path.Base(filename)) +} diff --git a/modules/migration/schemas_static.go b/modules/migration/schemas_static.go deleted file mode 100644 index 8a0c340a65..0000000000 --- a/modules/migration/schemas_static.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build bindata - -package migration - -import ( - "io" - "path" -) - -func openSchema(filename string) (io.ReadCloser, error) { - return Assets.Open(path.Base(filename)) -} diff --git a/modules/optional/option.go b/modules/optional/option.go index ccbad259c2..6075c6347e 100644 --- a/modules/optional/option.go +++ b/modules/optional/option.go @@ -5,6 +5,12 @@ package optional import "strconv" +// Option is a generic type that can hold a value of type T or be empty (None). +// +// It must use the slice type to work with "chi" form values binding: +// * non-existing value are represented as an empty slice (None) +// * existing value is represented as a slice with one element (Some) +// * multiple values are represented as a slice with multiple elements (Some), the Value is the first element (not well-defined in this case) type Option[T any] []T func None[T any]() Option[T] { diff --git a/modules/optional/serialization_test.go b/modules/optional/serialization_test.go index 21d3ad8470..cf81a94cfc 100644 --- a/modules/optional/serialization_test.go +++ b/modules/optional/serialization_test.go @@ -4,7 +4,7 @@ package optional_test import ( - std_json "encoding/json" //nolint:depguard + std_json "encoding/json" //nolint:depguard // for testing purpose "testing" "code.gitea.io/gitea/modules/json" diff --git a/modules/options/options_bindata.go b/modules/options/options_bindata.go index 29151cb3cb..b2321d7eb5 100644 --- a/modules/options/options_bindata.go +++ b/modules/options/options_bindata.go @@ -3,6 +3,21 @@ //go:build bindata +//go:generate go run ../../build/generate-bindata.go ../../options bindata.dat + package options -//go:generate go run ../../build/generate-bindata.go ../../options options bindata.go +import ( + "sync" + + _ "embed" + + "code.gitea.io/gitea/modules/assetfs" +) + +//go:embed bindata.dat +var bindata []byte + +var BuiltinAssets = sync.OnceValue(func() *assetfs.Layer { + return assetfs.Bindata("builtin(bindata)", assetfs.NewEmbeddedFS(bindata)) +}) diff --git a/modules/options/dynamic.go b/modules/options/options_dynamic.go index 085492d11c..085492d11c 100644 --- a/modules/options/dynamic.go +++ b/modules/options/options_dynamic.go diff --git a/modules/options/static.go b/modules/options/static.go deleted file mode 100644 index 72b28e990e..0000000000 --- a/modules/options/static.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build bindata - -package options - -import ( - "code.gitea.io/gitea/modules/assetfs" -) - -func BuiltinAssets() *assetfs.Layer { - return assetfs.Bindata("builtin(bindata)", Assets) -} diff --git a/models/packages/container/const.go b/modules/packages/container/const.go index 0dfbda051d..6c7c9b46d1 100644 --- a/models/packages/container/const.go +++ b/modules/packages/container/const.go @@ -4,6 +4,8 @@ package container const ( + ContentTypeDockerDistributionManifestV2 = "application/vnd.docker.distribution.manifest.v2+json" + ManifestFilename = "manifest.json" UploadVersion = "_upload" ) diff --git a/modules/packages/container/metadata.go b/modules/packages/container/metadata.go index 2a41fb9105..3ef0684d13 100644 --- a/modules/packages/container/metadata.go +++ b/modules/packages/container/metadata.go @@ -71,14 +71,34 @@ type Manifest struct { Size int64 `json:"size"` } +func IsMediaTypeValid(mt string) bool { + return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.") +} + +func IsMediaTypeImageManifest(mt string) bool { + return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json") +} + +func IsMediaTypeImageIndex(mt string) bool { + return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json") +} + // ParseImageConfig parses the metadata of an image config -func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) { - if strings.EqualFold(mt, helm.ConfigMediaType) { +func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) { + if strings.EqualFold(mediaType, helm.ConfigMediaType) { return parseHelmConfig(r) } // fallback to OCI Image Config - return parseOCIImageConfig(r) + // FIXME: this fallback is not right, we should strictly check the media type in the future + metadata, err := parseOCIImageConfig(r) + if err != nil { + if !IsMediaTypeImageManifest(mediaType) { + return &Metadata{Platform: "unknown/unknown"}, nil + } + return nil, err + } + return metadata, nil } func parseOCIImageConfig(r io.Reader) (*Metadata, error) { diff --git a/modules/packages/container/metadata_test.go b/modules/packages/container/metadata_test.go index 665499b2e6..0f2d702925 100644 --- a/modules/packages/container/metadata_test.go +++ b/modules/packages/container/metadata_test.go @@ -11,6 +11,7 @@ import ( oci "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseImageConfig(t *testing.T) { @@ -58,4 +59,8 @@ func TestParseImageConfig(t *testing.T) { assert.ElementsMatch(t, []string{author}, metadata.Authors) assert.Equal(t, projectURL, metadata.ProjectURL) assert.Equal(t, repositoryURL, metadata.RepositoryURL) + + metadata, err = ParseImageConfig("anything-unknown", strings.NewReader("")) + require.NoError(t, err) + assert.Equal(t, &Metadata{Platform: "unknown/unknown"}, metadata) } diff --git a/modules/packages/content_store.go b/modules/packages/content_store.go index 37612556d7..dadb7eaefc 100644 --- a/modules/packages/content_store.go +++ b/modules/packages/content_store.go @@ -28,8 +28,7 @@ func NewContentStore() *ContentStore { return contentStore } -// Get gets a package blob -func (s *ContentStore) Get(key BlobHash256Key) (storage.Object, error) { +func (s *ContentStore) OpenBlob(key BlobHash256Key) (storage.Object, error) { return s.store.Open(KeyToRelativePath(key)) } diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 8ba4dbfba7..11b5123c27 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -58,7 +58,7 @@ type PackageMetadata struct { Time map[string]time.Time `json:"time,omitempty"` Homepage string `json:"homepage,omitempty"` Keywords []string `json:"keywords,omitempty"` - Repository Repository `json:"repository,omitempty"` + Repository Repository `json:"repository"` Author User `json:"author"` ReadmeFilename string `json:"readmeFilename,omitempty"` Users map[string]bool `json:"users,omitempty"` @@ -75,7 +75,7 @@ type PackageMetadataVersion struct { Author User `json:"author"` Homepage string `json:"homepage,omitempty"` License string `json:"license,omitempty"` - Repository Repository `json:"repository,omitempty"` + Repository Repository `json:"repository"` Keywords []string `json:"keywords,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"` BundleDependencies []string `json:"bundleDependencies,omitempty"` diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index d1d0263387..362d0470d5 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -23,5 +23,5 @@ type Metadata struct { OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"` Bin map[string]string `json:"bin,omitempty"` Readme string `json:"readme,omitempty"` - Repository Repository `json:"repository,omitempty"` + Repository Repository `json:"repository"` } diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go index 1e98ddffde..a122590bf1 100644 --- a/modules/packages/nuget/metadata.go +++ b/modules/packages/nuget/metadata.go @@ -57,14 +57,24 @@ type Package struct { // Metadata represents the metadata of a Nuget package type Metadata struct { - Description string `json:"description,omitempty"` - ReleaseNotes string `json:"release_notes,omitempty"` - Readme string `json:"readme,omitempty"` - Authors string `json:"authors,omitempty"` - ProjectURL string `json:"project_url,omitempty"` - RepositoryURL string `json:"repository_url,omitempty"` - RequireLicenseAcceptance bool `json:"require_license_acceptance"` - Dependencies map[string][]Dependency `json:"dependencies,omitempty"` + Authors string `json:"authors,omitempty"` + Copyright string `json:"copyright,omitempty"` + Description string `json:"description,omitempty"` + DevelopmentDependency bool `json:"development_dependency,omitempty"` + IconURL string `json:"icon_url,omitempty"` + Language string `json:"language,omitempty"` + LicenseURL string `json:"license_url,omitempty"` + MinClientVersion string `json:"min_client_version,omitempty"` + Owners string `json:"owners,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Readme string `json:"readme,omitempty"` + ReleaseNotes string `json:"release_notes,omitempty"` + RepositoryURL string `json:"repository_url,omitempty"` + RequireLicenseAcceptance bool `json:"require_license_acceptance"` + Tags string `json:"tags,omitempty"` + Title string `json:"title,omitempty"` + + Dependencies map[string][]Dependency `json:"dependencies,omitempty"` } // Dependency represents a dependency of a Nuget package @@ -74,24 +84,30 @@ type Dependency struct { } // https://learn.microsoft.com/en-us/nuget/reference/nuspec +// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Packaging/compiler/resources/nuspec.xsd type nuspecPackage struct { Metadata struct { - ID string `xml:"id"` - Version string `xml:"version"` - Authors string `xml:"authors"` - RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` + // required fields + Authors string `xml:"authors"` + Description string `xml:"description"` + ID string `xml:"id"` + Version string `xml:"version"` + + // optional fields + Copyright string `xml:"copyright"` + DevelopmentDependency bool `xml:"developmentDependency"` + IconURL string `xml:"iconUrl"` + Language string `xml:"language"` + LicenseURL string `xml:"licenseUrl"` + MinClientVersion string `xml:"minClientVersion,attr"` + Owners string `xml:"owners"` ProjectURL string `xml:"projectUrl"` - Description string `xml:"description"` - ReleaseNotes string `xml:"releaseNotes"` Readme string `xml:"readme"` - PackageTypes struct { - PackageType []struct { - Name string `xml:"name,attr"` - } `xml:"packageType"` - } `xml:"packageTypes"` - Repository struct { - URL string `xml:"url,attr"` - } `xml:"repository"` + ReleaseNotes string `xml:"releaseNotes"` + RequireLicenseAcceptance bool `xml:"requireLicenseAcceptance"` + Tags string `xml:"tags"` + Title string `xml:"title"` + Dependencies struct { Dependency []struct { ID string `xml:"id,attr"` @@ -107,6 +123,14 @@ type nuspecPackage struct { } `xml:"dependency"` } `xml:"group"` } `xml:"dependencies"` + PackageTypes struct { + PackageType []struct { + Name string `xml:"name,attr"` + } `xml:"packageType"` + } `xml:"packageTypes"` + Repository struct { + URL string `xml:"url,attr"` + } `xml:"repository"` } `xml:"metadata"` } @@ -167,13 +191,23 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) { } m := &Metadata{ - Description: p.Metadata.Description, - ReleaseNotes: p.Metadata.ReleaseNotes, Authors: p.Metadata.Authors, + Copyright: p.Metadata.Copyright, + Description: p.Metadata.Description, + DevelopmentDependency: p.Metadata.DevelopmentDependency, + IconURL: p.Metadata.IconURL, + Language: p.Metadata.Language, + LicenseURL: p.Metadata.LicenseURL, + MinClientVersion: p.Metadata.MinClientVersion, + Owners: p.Metadata.Owners, ProjectURL: p.Metadata.ProjectURL, + ReleaseNotes: p.Metadata.ReleaseNotes, RepositoryURL: p.Metadata.Repository.URL, RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, - Dependencies: make(map[string][]Dependency), + Tags: p.Metadata.Tags, + Title: p.Metadata.Title, + + Dependencies: make(map[string][]Dependency), } if p.Metadata.Readme != "" { @@ -227,13 +261,13 @@ func ParseNuspecMetaData(archive *zip.Reader, r io.Reader) (*Package, error) { func toNormalizedVersion(v *version.Version) string { var buf bytes.Buffer segments := v.Segments64() - fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) + _, _ = fmt.Fprintf(&buf, "%d.%d.%d", segments[0], segments[1], segments[2]) if len(segments) > 3 && segments[3] > 0 { - fmt.Fprintf(&buf, ".%d", segments[3]) + _, _ = fmt.Fprintf(&buf, ".%d", segments[3]) } pre := v.Prerelease() if pre != "" { - fmt.Fprint(&buf, "-", pre) + _, _ = fmt.Fprint(&buf, "-", pre) } return buf.String() } diff --git a/modules/packages/nuget/metadata_test.go b/modules/packages/nuget/metadata_test.go index f466492f8a..90c3e8dfeb 100644 --- a/modules/packages/nuget/metadata_test.go +++ b/modules/packages/nuget/metadata_test.go @@ -12,44 +12,62 @@ import ( ) const ( - id = "System.Gitea" - semver = "1.0.1" - authors = "Gitea Authors" - projectURL = "https://gitea.io" - description = "Package Description" - releaseNotes = "Package Release Notes" - readme = "Readme" - repositoryURL = "https://gitea.io/gitea/gitea" - targetFramework = ".NETStandard2.1" - dependencyID = "System.Text.Json" - dependencyVersion = "5.0.0" + authors = "Gitea Authors" + copyright = "Package Copyright" + dependencyID = "System.Text.Json" + dependencyVersion = "5.0.0" + developmentDependency = true + description = "Package Description" + iconURL = "https://gitea.io/favicon.png" + id = "System.Gitea" + language = "Package Language" + licenseURL = "https://gitea.io/license" + minClientVersion = "1.0.0.0" + owners = "Package Owners" + projectURL = "https://gitea.io" + readme = "Readme" + releaseNotes = "Package Release Notes" + repositoryURL = "https://gitea.io/gitea/gitea" + requireLicenseAcceptance = true + tags = "tag_1 tag_2 tag_3" + targetFramework = ".NETStandard2.1" + title = "Package Title" + versionStr = "1.0.1" ) const nuspecContent = `<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> - <metadata> - <id>` + id + `</id> - <version>` + semver + `</version> - <authors>` + authors + `</authors> - <requireLicenseAcceptance>true</requireLicenseAcceptance> - <projectUrl>` + projectURL + `</projectUrl> - <description>` + description + `</description> - <releaseNotes>` + releaseNotes + `</releaseNotes> - <repository url="` + repositoryURL + `" /> - <readme>README.md</readme> - <dependencies> - <group targetFramework="` + targetFramework + `"> - <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" /> - </group> - </dependencies> - </metadata> + <metadata minClientVersion="` + minClientVersion + `"> + <authors>` + authors + `</authors> + <copyright>` + copyright + `</copyright> + <description>` + description + `</description> + <developmentDependency>true</developmentDependency> + <iconUrl>` + iconURL + `</iconUrl> + <id>` + id + `</id> + <language>` + language + `</language> + <licenseUrl>` + licenseURL + `</licenseUrl> + <owners>` + owners + `</owners> + <projectUrl>` + projectURL + `</projectUrl> + <readme>README.md</readme> + <releaseNotes>` + releaseNotes + `</releaseNotes> + <repository url="` + repositoryURL + `" /> + <requireLicenseAcceptance>true</requireLicenseAcceptance> + <tags>` + tags + `</tags> + <title>` + title + `</title> + <version>` + versionStr + `</version> + <dependencies> + <group targetFramework="` + targetFramework + `"> + <dependency id="` + dependencyID + `" version="` + dependencyVersion + `" exclude="Build,Analyzers" /> + </group> + </dependencies> + </metadata> </package>` const symbolsNuspecContent = `<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> <metadata> <id>` + id + `</id> - <version>` + semver + `</version> + <version>` + versionStr + `</version> <description>` + description + `</description> <packageTypes> <packageType name="SymbolsPackage" /> @@ -140,14 +158,26 @@ func TestParsePackageMetaData(t *testing.T) { assert.NotNil(t, np) assert.Equal(t, DependencyPackage, np.PackageType) - assert.Equal(t, id, np.ID) - assert.Equal(t, semver, np.Version) assert.Equal(t, authors, np.Metadata.Authors) - assert.Equal(t, projectURL, np.Metadata.ProjectURL) assert.Equal(t, description, np.Metadata.Description) - assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes) + assert.Equal(t, id, np.ID) + assert.Equal(t, versionStr, np.Version) + + assert.Equal(t, copyright, np.Metadata.Copyright) + assert.Equal(t, developmentDependency, np.Metadata.DevelopmentDependency) + assert.Equal(t, iconURL, np.Metadata.IconURL) + assert.Equal(t, language, np.Metadata.Language) + assert.Equal(t, licenseURL, np.Metadata.LicenseURL) + assert.Equal(t, minClientVersion, np.Metadata.MinClientVersion) + assert.Equal(t, owners, np.Metadata.Owners) + assert.Equal(t, projectURL, np.Metadata.ProjectURL) assert.Equal(t, readme, np.Metadata.Readme) + assert.Equal(t, releaseNotes, np.Metadata.ReleaseNotes) assert.Equal(t, repositoryURL, np.Metadata.RepositoryURL) + assert.Equal(t, requireLicenseAcceptance, np.Metadata.RequireLicenseAcceptance) + assert.Equal(t, tags, np.Metadata.Tags) + assert.Equal(t, title, np.Metadata.Title) + assert.Len(t, np.Metadata.Dependencies, 1) assert.Contains(t, np.Metadata.Dependencies, targetFramework) deps := np.Metadata.Dependencies[targetFramework] @@ -180,7 +210,7 @@ func TestParsePackageMetaData(t *testing.T) { assert.Equal(t, SymbolsPackage, np.PackageType) assert.Equal(t, id, np.ID) - assert.Equal(t, semver, np.Version) + assert.Equal(t, versionStr, np.Version) assert.Equal(t, description, np.Metadata.Description) assert.Empty(t, np.Metadata.Dependencies) }) diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index 81bf0371a0..9c952e1f10 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -34,7 +34,7 @@ type PortablePdbList []*PortablePdb func (l PortablePdbList) Close() { for _, pdb := range l { - pdb.Content.Close() + _ = pdb.Content.Close() } } @@ -65,7 +65,7 @@ func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { buf, err := packages.CreateHashedBufferFromReader(f) - f.Close() + _ = f.Close() if err != nil { return err @@ -73,12 +73,12 @@ func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { id, err := ParseDebugHeaderID(buf) if err != nil { - buf.Close() + _ = buf.Close() return fmt.Errorf("Invalid PDB file: %w", err) } if _, err := buf.Seek(0, io.SeekStart); err != nil { - buf.Close() + _ = buf.Close() return err } diff --git a/modules/packages/nuget/symbol_extractor_test.go b/modules/packages/nuget/symbol_extractor_test.go index 711ad6d096..e841e377d9 100644 --- a/modules/packages/nuget/symbol_extractor_test.go +++ b/modules/packages/nuget/symbol_extractor_test.go @@ -24,14 +24,14 @@ func TestExtractPortablePdb(t *testing.T) { var buf bytes.Buffer archive := zip.NewWriter(&buf) w, _ := archive.Create(name) - w.Write(content) - archive.Close() + _, _ = w.Write(content) + _ = archive.Close() return buf.Bytes() } t.Run("MissingPdbFiles", func(t *testing.T) { var buf bytes.Buffer - zip.NewWriter(&buf).Close() + _ = zip.NewWriter(&buf).Close() pdbs, err := ExtractPortablePdb(bytes.NewReader(buf.Bytes()), int64(buf.Len())) assert.ErrorIs(t, err, ErrMissingPdbFiles) diff --git a/modules/packages/rubygems/marshal.go b/modules/packages/rubygems/marshal.go index 4e6a5fc5f8..1505221acc 100644 --- a/modules/packages/rubygems/marshal.go +++ b/modules/packages/rubygems/marshal.go @@ -250,7 +250,7 @@ func (e *MarshalEncoder) marshalArray(arr reflect.Value) error { return err } - for i := 0; i < length; i++ { + for i := range length { if err := e.marshal(arr.Index(i).Interface()); err != nil { return err } diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go index 24c4262ab7..85beb57607 100644 --- a/modules/packages/swift/metadata.go +++ b/modules/packages/swift/metadata.go @@ -47,7 +47,7 @@ type Metadata struct { Keywords []string `json:"keywords,omitempty"` RepositoryURL string `json:"repository_url,omitempty"` License string `json:"license,omitempty"` - Author Person `json:"author,omitempty"` + Author Person `json:"author"` Manifests map[string]*Manifest `json:"manifests,omitempty"` } diff --git a/modules/public/public.go b/modules/public/public.go index 7f8ce29056..a7eace1538 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -44,7 +44,7 @@ func FileHandlerFunc() http.HandlerFunc { func parseAcceptEncoding(val string) container.Set[string] { parts := strings.Split(val, ";") types := make(container.Set[string]) - for _, v := range strings.Split(parts[0], ",") { + for v := range strings.SplitSeq(parts[0], ",") { types.Add(strings.TrimSpace(v)) } return types @@ -89,19 +89,16 @@ func handleRequest(w http.ResponseWriter, req *http.Request, fs http.FileSystem, servePublicAsset(w, req, fi, fi.ModTime(), f) } -type GzipBytesProvider interface { - GzipBytes() []byte -} - // servePublicAsset serve http content func servePublicAsset(w http.ResponseWriter, req *http.Request, fi os.FileInfo, modtime time.Time, content io.ReadSeeker) { setWellKnownContentType(w, fi.Name()) httpcache.SetCacheControlInHeader(w.Header(), httpcache.CacheControlForPublicStatic()) encodings := parseAcceptEncoding(req.Header.Get("Accept-Encoding")) - if encodings.Contains("gzip") { - // try to provide gzip content directly from bindata (provided by vfsgenÛ°CompressedFileInfo) - if compressed, ok := fi.(GzipBytesProvider); ok { - rdGzip := bytes.NewReader(compressed.GzipBytes()) + fiEmbedded, _ := fi.(assetfs.EmbeddedFileInfo) + if encodings.Contains("gzip") && fiEmbedded != nil { + // try to provide gzip content directly from bindata + if gzipBytes, ok := fiEmbedded.GetGzipContent(); ok { + rdGzip := bytes.NewReader(gzipBytes) // all gzipped static files (from bindata) are managed by Gitea, so we can make sure every file has the correct ext name // then we can get the correct Content-Type, we do not need to do http.DetectContentType on the decompressed data if w.Header().Get("Content-Type") == "" { @@ -113,5 +110,4 @@ func servePublicAsset(w http.ResponseWriter, req *http.Request, fi os.FileInfo, } } http.ServeContent(w, req, fi.Name(), modtime, content) - return } diff --git a/modules/public/public_bindata.go b/modules/public/public_bindata.go index 4878f88ad1..2dcf3e72e4 100644 --- a/modules/public/public_bindata.go +++ b/modules/public/public_bindata.go @@ -5,4 +5,19 @@ package public -//go:generate go run ../../build/generate-bindata.go ../../public public bindata.go true +//go:generate go run ../../build/generate-bindata.go ../../public bindata.dat + +import ( + "sync" + + _ "embed" + + "code.gitea.io/gitea/modules/assetfs" +) + +//go:embed bindata.dat +var bindata []byte + +var BuiltinAssets = sync.OnceValue(func() *assetfs.Layer { + return assetfs.Bindata("builtin(bindata)", assetfs.NewEmbeddedFS(bindata)) +}) diff --git a/modules/public/serve_dynamic.go b/modules/public/public_dynamic.go index a668b17c34..a668b17c34 100644 --- a/modules/public/serve_dynamic.go +++ b/modules/public/public_dynamic.go diff --git a/modules/public/serve_static.go b/modules/public/serve_static.go deleted file mode 100644 index e79085021e..0000000000 --- a/modules/public/serve_static.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2016 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build bindata - -package public - -import ( - "time" - - "code.gitea.io/gitea/modules/assetfs" - "code.gitea.io/gitea/modules/timeutil" -) - -var _ GzipBytesProvider = (*vfsgenÛ°CompressedFileInfo)(nil) - -// GlobalModTime provide a global mod time for embedded asset files -func GlobalModTime(filename string) time.Time { - return timeutil.GetExecutableModTime() -} - -func BuiltinAssets() *assetfs.Layer { - return assetfs.Bindata("builtin(bindata)", Assets) -} diff --git a/modules/queue/base_levelqueue_common.go b/modules/queue/base_levelqueue_common.go index 78d3b85a8a..d37093b84d 100644 --- a/modules/queue/base_levelqueue_common.go +++ b/modules/queue/base_levelqueue_common.go @@ -83,7 +83,7 @@ func prepareLevelDB(cfg *BaseConfig) (conn string, db *leveldb.DB, err error) { } conn = cfg.ConnStr } - for i := 0; i < 10; i++ { + for range 10 { if db, err = nosql.GetManager().GetLevelDB(conn); err == nil { break } diff --git a/modules/queue/base_redis.go b/modules/queue/base_redis.go index a1e234943d..bea0fd7a98 100644 --- a/modules/queue/base_redis.go +++ b/modules/queue/base_redis.go @@ -29,7 +29,7 @@ func newBaseRedisGeneric(cfg *BaseConfig, unique bool) (baseQueue, error) { client := nosql.GetManager().GetRedisClient(cfg.ConnStr) var err error - for i := 0; i < 10; i++ { + for range 10 { err = client.Ping(graceful.GetManager().ShutdownContext()).Err() if err == nil { break diff --git a/modules/queue/base_test.go b/modules/queue/base_test.go index 1a96ac1e1d..8e7c18d740 100644 --- a/modules/queue/base_test.go +++ b/modules/queue/base_test.go @@ -87,7 +87,7 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error) // test blocking push if queue is full for i := 0; i < cfg.Length; i++ { - err = q.PushItem(ctx, []byte(fmt.Sprintf("item-%d", i))) + err = q.PushItem(ctx, fmt.Appendf(nil, "item-%d", i)) assert.NoError(t, err) } ctxTimed, cancel = context.WithTimeout(ctx, 10*time.Millisecond) diff --git a/modules/queue/manager.go b/modules/queue/manager.go index 079e2bee7a..ae6c51872d 100644 --- a/modules/queue/manager.go +++ b/modules/queue/manager.go @@ -6,6 +6,7 @@ package queue import ( "context" "errors" + "maps" "sync" "time" @@ -70,9 +71,7 @@ func (m *Manager) ManagedQueues() map[int64]ManagedWorkerPoolQueue { defer m.mu.Unlock() queues := make(map[int64]ManagedWorkerPoolQueue, len(m.Queues)) - for k, v := range m.Queues { - queues[k] = v - } + maps.Copy(queues, m.Queues) return queues } diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go index 487c2f1a92..a6c369d5f9 100644 --- a/modules/queue/workerqueue_test.go +++ b/modules/queue/workerqueue_test.go @@ -77,17 +77,17 @@ func TestWorkerPoolQueueUnhandled(t *testing.T) { runCount := 2 // we can run these tests even hundreds times to see its stability t.Run("1/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { test(t, setting.QueueSettings{BatchLength: 1, MaxWorkers: 1}) } }) t.Run("3/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { test(t, setting.QueueSettings{BatchLength: 3, MaxWorkers: 1}) } }) t.Run("4/5", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { test(t, setting.QueueSettings{BatchLength: 4, MaxWorkers: 5}) } }) @@ -96,17 +96,17 @@ func TestWorkerPoolQueueUnhandled(t *testing.T) { func TestWorkerPoolQueuePersistence(t *testing.T) { runCount := 2 // we can run these tests even hundreds times to see its stability t.Run("1/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 1, MaxWorkers: 1, Length: 100}) } }) t.Run("3/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 3, MaxWorkers: 1, Length: 100}) } }) t.Run("4/5", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 4, MaxWorkers: 5, Length: 100}) } }) @@ -141,7 +141,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett q, _ := newWorkerPoolQueueForTest("pr_patch_checker_test", queueSetting, testHandler, true) stop := runWorkerPoolQueue(q) - for i := 0; i < testCount; i++ { + for i := range testCount { _ = q.Push("task-" + strconv.Itoa(i)) } close(startWhenAllReady) @@ -186,7 +186,7 @@ func TestWorkerPoolQueueActiveWorkers(t *testing.T) { q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 1, Length: 100}, handler, false) stop := runWorkerPoolQueue(q) - for i := 0; i < 5; i++ { + for i := range 5 { assert.NoError(t, q.Push(i)) } @@ -202,7 +202,7 @@ func TestWorkerPoolQueueActiveWorkers(t *testing.T) { q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 3, Length: 100}, handler, false) stop = runWorkerPoolQueue(q) - for i := 0; i < 15; i++ { + for i := range 15 { assert.NoError(t, q.Push(i)) } @@ -274,7 +274,7 @@ func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) { } q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 2, Length: 100}, handler, false) stop := runWorkerPoolQueue(q) - for i := 0; i < 100; i++ { + for i := range 100 { assert.NoError(t, q.Push(i)) } time.Sleep(500 * time.Millisecond) diff --git a/modules/repository/branch.go b/modules/repository/branch.go index 2bf9930f19..30aa0a6e85 100644 --- a/modules/repository/branch.go +++ b/modules/repository/branch.go @@ -41,11 +41,12 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, if err != nil { return 0, fmt.Errorf("GetObjectFormat: %w", err) } - _, err = db.GetEngine(ctx).ID(repo.ID).Update(&repo_model.Repository{ObjectFormatName: objFmt.Name()}) - if err != nil { - return 0, fmt.Errorf("UpdateRepository: %w", err) + if objFmt.Name() != repo.ObjectFormatName { + repo.ObjectFormatName = objFmt.Name() + if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "object_format_name"); err != nil { + return 0, fmt.Errorf("UpdateRepositoryColsWithAutoTime: %w", err) + } } - repo.ObjectFormatName = objFmt.Name() // keep consistent with db allBranches := container.Set[string]{} { diff --git a/modules/repository/init.go b/modules/repository/init.go index 91d4889782..12e9606c74 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -125,7 +125,7 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg } labels := make([]*issues_model.Label, len(list)) - for i := 0; i < len(list); i++ { + for i := range list { labels[i] = &issues_model.Label{ Name: list[i].Name, Exclusive: list[i].Exclusive, diff --git a/modules/repository/repo.go b/modules/repository/repo.go index bc147a4dd5..ad4a53b858 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -9,13 +9,10 @@ import ( "fmt" "io" "strings" - "time" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/lfs" @@ -59,118 +56,6 @@ func SyncRepoTags(ctx context.Context, repoID int64) error { return SyncReleasesWithTags(ctx, repo, gitRepo) } -// SyncReleasesWithTags synchronizes release table with repository tags -func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error { - log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name) - - // optimized procedure for pull-mirrors which saves a lot of time (in - // particular for repos with many tags). - if repo.IsMirror { - return pullMirrorReleaseSync(ctx, repo, gitRepo) - } - - existingRelTags := make(container.Set[string]) - opts := repo_model.FindReleasesOptions{ - IncludeDrafts: true, - IncludeTags: true, - ListOptions: db.ListOptions{PageSize: 50}, - RepoID: repo.ID, - } - for page := 1; ; page++ { - opts.Page = page - rels, err := db.Find[repo_model.Release](gitRepo.Ctx, opts) - if err != nil { - return fmt.Errorf("unable to GetReleasesByRepoID in Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) - } - if len(rels) == 0 { - break - } - for _, rel := range rels { - if rel.IsDraft { - continue - } - commitID, err := gitRepo.GetTagCommitID(rel.TagName) - if err != nil && !git.IsErrNotExist(err) { - return fmt.Errorf("unable to GetTagCommitID for %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err) - } - if git.IsErrNotExist(err) || commitID != rel.Sha1 { - if err := repo_model.PushUpdateDeleteTag(ctx, repo, rel.TagName); err != nil { - return fmt.Errorf("unable to PushUpdateDeleteTag: %q in Repo[%d:%s/%s]: %w", rel.TagName, repo.ID, repo.OwnerName, repo.Name, err) - } - } else { - existingRelTags.Add(strings.ToLower(rel.TagName)) - } - } - } - - _, err := gitRepo.WalkReferences(git.ObjectTag, 0, 0, func(sha1, refname string) error { - tagName := strings.TrimPrefix(refname, git.TagPrefix) - if existingRelTags.Contains(strings.ToLower(tagName)) { - return nil - } - - if err := PushUpdateAddTag(ctx, repo, gitRepo, tagName, sha1, refname); err != nil { - // sometimes, some tags will be sync failed. i.e. https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tag/?h=v2.6.11 - // this is a tree object, not a tag object which created before git - log.Error("unable to PushUpdateAddTag: %q to Repo[%d:%s/%s]: %v", tagName, repo.ID, repo.OwnerName, repo.Name, err) - } - - return nil - }) - return err -} - -// PushUpdateAddTag must be called for any push actions to add tag -func PushUpdateAddTag(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, tagName, sha1, refname string) error { - tag, err := gitRepo.GetTagWithID(sha1, tagName) - if err != nil { - return fmt.Errorf("unable to GetTag: %w", err) - } - commit, err := gitRepo.GetTagCommit(tag.Name) - if err != nil { - return fmt.Errorf("unable to get tag Commit: %w", err) - } - - sig := tag.Tagger - if sig == nil { - sig = commit.Author - } - if sig == nil { - sig = commit.Committer - } - - var author *user_model.User - createdAt := time.Unix(1, 0) - - if sig != nil { - author, err = user_model.GetUserByEmail(ctx, sig.Email) - if err != nil && !user_model.IsErrUserNotExist(err) { - return fmt.Errorf("unable to GetUserByEmail for %q: %w", sig.Email, err) - } - createdAt = sig.When - } - - commitsCount, err := commit.CommitsCount() - if err != nil { - return fmt.Errorf("unable to get CommitsCount: %w", err) - } - - rel := repo_model.Release{ - RepoID: repo.ID, - TagName: tagName, - LowerTagName: strings.ToLower(tagName), - Sha1: commit.ID.String(), - NumCommits: commitsCount, - CreatedUnix: timeutil.TimeStamp(createdAt.Unix()), - IsTag: true, - } - if author != nil { - rel.PublisherID = author.ID - } - - return repo_model.SaveOrUpdateTag(ctx, repo, &rel) -} - // StoreMissingLfsObjectsInRepository downloads missing LFS objects func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, lfsClient lfs.Client) error { contentStore := lfs.NewContentStore() @@ -286,18 +171,19 @@ func (shortRelease) TableName() string { return "release" } -// pullMirrorReleaseSync is a pull-mirror specific tag<->release table +// SyncReleasesWithTags is a tag<->release table // synchronization which overwrites all Releases from the repository tags. This // can be relied on since a pull-mirror is always identical to its -// upstream. Hence, after each sync we want the pull-mirror release set to be +// upstream. Hence, after each sync we want the release set to be // identical to the upstream tag set. This is much more efficient for // repositories like https://github.com/vim/vim (with over 13000 tags). -func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error { - log.Trace("pullMirrorReleaseSync: rebuilding releases for pull-mirror Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name) - tags, numTags, err := gitRepo.GetTagInfos(0, 0) +func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error { + log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name) + tags, _, err := gitRepo.GetTagInfos(0, 0) if err != nil { return fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) } + var added, deleted, updated int err = db.WithTx(ctx, func(ctx context.Context) error { dbReleases, err := db.Find[shortRelease](ctx, repo_model.FindReleasesOptions{ RepoID: repo.ID, @@ -318,9 +204,7 @@ func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, git TagName: tag.Name, LowerTagName: strings.ToLower(tag.Name), Sha1: tag.Object.String(), - // NOTE: ignored, since NumCommits are unused - // for pull-mirrors (only relevant when - // displaying releases, IsTag: false) + // NOTE: ignored, The NumCommits value is calculated and cached on demand when the UI requires it. NumCommits: -1, CreatedUnix: timeutil.TimeStamp(tag.Tagger.When.Unix()), IsTag: true, @@ -349,13 +233,14 @@ func pullMirrorReleaseSync(ctx context.Context, repo *repo_model.Repository, git return fmt.Errorf("unable to update tag %s for pull-mirror Repo[%d:%s/%s]: %w", tag.Name, repo.ID, repo.OwnerName, repo.Name, err) } } + added, deleted, updated = len(deletes), len(updates), len(inserts) return nil }) if err != nil { return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) } - log.Trace("pullMirrorReleaseSync: done rebuilding %d releases", numTags) + log.Trace("SyncReleasesWithTags: %d tags added, %d tags deleted, %d tags updated", added, deleted, updated) return nil } diff --git a/modules/reqctx/datastore.go b/modules/reqctx/datastore.go index d025dad7f3..1d4bee613f 100644 --- a/modules/reqctx/datastore.go +++ b/modules/reqctx/datastore.go @@ -6,6 +6,7 @@ package reqctx import ( "context" "io" + "maps" "sync" "code.gitea.io/gitea/modules/process" @@ -22,9 +23,7 @@ func (ds ContextData) GetData() ContextData { } func (ds ContextData) MergeFrom(other ContextData) ContextData { - for k, v := range other { - ds[k] = v - } + maps.Copy(ds, other) return ds } diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index 5d94a9641f..409588dc44 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -97,7 +97,7 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) { // decodeEnvironmentKey decode the environment key to section and key // The environment key is in the form of GITEA__SECTION__KEY or GITEA__SECTION__KEY__FILE -func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) { //nolint:unparam +func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, section, key string, useFileValue bool) { if !strings.HasPrefix(envKey, prefixGitea) { return false, "", "", false } diff --git a/modules/setting/config_env_test.go b/modules/setting/config_env_test.go index 217ea53860..7d270ac21a 100644 --- a/modules/setting/config_env_test.go +++ b/modules/setting/config_env_test.go @@ -73,6 +73,9 @@ func TestDecodeEnvironmentKey(t *testing.T) { assert.Equal(t, "sec", section) assert.Equal(t, "KEY", key) assert.True(t, file) + + ok, _, _, _ = decodeEnvironmentKey("PREFIX__", "", "PREFIX__SEC__KEY") + assert.True(t, ok) } func TestEnvironmentToConfig(t *testing.T) { diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index a0c53a1032..09eaaefdaf 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -15,7 +15,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" - "gopkg.in/ini.v1" //nolint:depguard + "gopkg.in/ini.v1" //nolint:depguard // wrapper for this package ) type ConfigKey interface { diff --git a/modules/setting/git_test.go b/modules/setting/git_test.go index 818bcf9df6..0d7f634abf 100644 --- a/modules/setting/git_test.go +++ b/modules/setting/git_test.go @@ -6,6 +6,8 @@ package setting import ( "testing" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) @@ -36,12 +38,8 @@ diff.algorithm = other } func TestGitReflog(t *testing.T) { - oldGit := Git - oldGitConfig := GitConfig - defer func() { - Git = oldGit - GitConfig = oldGitConfig - }() + defer test.MockVariableValue(&Git) + defer test.MockVariableValue(&GitConfig) // default reflog config without legacy options cfg, err := NewConfigProviderFromData(``) diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index e34baae012..ace7eec70e 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -96,7 +96,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) { // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing func IndexerGlobFromString(globstr string) []*GlobMatcher { extarr := make([]*GlobMatcher, 0, 10) - for _, expr := range strings.Split(strings.ToLower(globstr), ",") { + for expr := range strings.SplitSeq(strings.ToLower(globstr), ",") { expr = strings.TrimSpace(expr) if expr != "" { if g, err := GlobMatcherCompile(expr, '.', '/'); err != nil { diff --git a/modules/setting/log.go b/modules/setting/log.go index 614d9ee75a..59866c7605 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -227,8 +227,8 @@ func initLoggerByName(manager *log.LoggerManager, rootCfg ConfigProvider, logger } var eventWriters []log.EventWriter - modes := strings.Split(modeVal, ",") - for _, modeName := range modes { + modes := strings.SplitSeq(modeVal, ",") + for modeName := range modes { modeName = strings.TrimSpace(modeName) if modeName == "" { continue diff --git a/modules/setting/markup.go b/modules/setting/markup.go index 365af05fcf..057b0650c3 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -149,8 +149,8 @@ func loadMarkupFrom(rootCfg ConfigProvider) { func newMarkupSanitizer(name string, sec ConfigSection) { rule, ok := createMarkupSanitizerRule(name, sec) if ok { - if strings.HasPrefix(name, "sanitizer.") { - names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2) + if after, found := strings.CutPrefix(name, "sanitizer."); found { + names := strings.SplitN(after, ".", 2) name = names[0] } for _, renderer := range ExternalMarkupRenderers { diff --git a/modules/setting/mirror.go b/modules/setting/mirror.go index 3aa530a1f4..300711789d 100644 --- a/modules/setting/mirror.go +++ b/modules/setting/mirror.go @@ -48,11 +48,7 @@ func loadMirrorFrom(rootCfg ConfigProvider) { Mirror.MinInterval = 1 * time.Minute } if Mirror.DefaultInterval < Mirror.MinInterval { - if time.Hour*8 < Mirror.MinInterval { - Mirror.DefaultInterval = Mirror.MinInterval - } else { - Mirror.DefaultInterval = time.Hour * 8 - } + Mirror.DefaultInterval = max(time.Hour*8, Mirror.MinInterval) log.Warn("Mirror.DefaultInterval is less than Mirror.MinInterval, set to %s", Mirror.DefaultInterval.String()) } } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index c6bdc65b32..318cf41108 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -100,11 +100,13 @@ var ( SigningKey string SigningName string SigningEmail string + SigningFormat string InitialCommit []string CRUDActions []string `ini:"CRUD_ACTIONS"` Merges []string Wiki []string DefaultTrustModel string + TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"` } `ini:"repository.signing"` }{ DetectedCharsetsOrder: []string{ @@ -242,20 +244,24 @@ var ( SigningKey string SigningName string SigningEmail string + SigningFormat string InitialCommit []string CRUDActions []string `ini:"CRUD_ACTIONS"` Merges []string Wiki []string DefaultTrustModel string + TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"` }{ SigningKey: "default", SigningName: "", SigningEmail: "", + SigningFormat: "openpgp", // git.SigningKeyFormatOpenPGP InitialCommit: []string{"always"}, CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, Wiki: []string{"never"}, DefaultTrustModel: "collaborator", + TrustedSSHKeys: []string{}, }, } RepoRootPath string diff --git a/modules/setting/security.go b/modules/setting/security.go index 3ae4c005c7..153b6bc944 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -111,7 +111,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) { if SecretKey == "" { // FIXME: https://github.com/go-gitea/gitea/issues/16832 // Until it supports rotating an existing secret key, we shouldn't move users off of the widely used default value - SecretKey = "!#@FDEWREWR&*(" //nolint:gosec + SecretKey = "!#@FDEWREWR&*(" } CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible") diff --git a/modules/setting/ssh.go b/modules/setting/ssh.go index da8cdf58d2..900fc6ade2 100644 --- a/modules/setting/ssh.go +++ b/modules/setting/ssh.go @@ -51,9 +51,6 @@ var SSH = struct { StartBuiltinServer: false, Domain: "", Port: 22, - ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"}, - ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"}, - ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"}, MinimumKeySizeCheck: true, MinimumKeySizes: map[string]int{"ed25519": 256, "ed25519-sk": 256, "ecdsa": 256, "ecdsa-sk": 256, "rsa": 3071}, ServerHostKeys: []string{"ssh/gitea.rsa", "ssh/gogs.rsa"}, @@ -107,21 +104,20 @@ func loadSSHFrom(rootCfg ConfigProvider) { homeDir = strings.ReplaceAll(homeDir, "\\", "/") SSH.RootPath = filepath.Join(homeDir, ".ssh") - serverCiphers := sec.Key("SSH_SERVER_CIPHERS").Strings(",") - if len(serverCiphers) > 0 { - SSH.ServerCiphers = serverCiphers - } - serverKeyExchanges := sec.Key("SSH_SERVER_KEY_EXCHANGES").Strings(",") - if len(serverKeyExchanges) > 0 { - SSH.ServerKeyExchanges = serverKeyExchanges - } - serverMACs := sec.Key("SSH_SERVER_MACS").Strings(",") - if len(serverMACs) > 0 { - SSH.ServerMACs = serverMACs - } + if err = sec.MapTo(&SSH); err != nil { log.Fatal("Failed to map SSH settings: %v", err) } + + serverCiphers := sec.Key("SSH_SERVER_CIPHERS").Strings(",") + SSH.ServerCiphers = util.Iif(len(serverCiphers) > 0, serverCiphers, nil) + + serverKeyExchanges := sec.Key("SSH_SERVER_KEY_EXCHANGES").Strings(",") + SSH.ServerKeyExchanges = util.Iif(len(serverKeyExchanges) > 0, serverKeyExchanges, nil) + + serverMACs := sec.Key("SSH_SERVER_MACS").Strings(",") + SSH.ServerMACs = util.Iif(len(serverMACs) > 0, serverMACs, nil) + for i, key := range SSH.ServerHostKeys { if !filepath.IsAbs(key) { SSH.ServerHostKeys[i] = filepath.Join(AppDataPath, key) diff --git a/modules/setting/storage.go b/modules/setting/storage.go index e1d9b1fa7a..ee246158d9 100644 --- a/modules/setting/storage.go +++ b/modules/setting/storage.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "strings" ) @@ -30,12 +31,7 @@ var storageTypes = []StorageType{ // IsValidStorageType returns true if the given storage type is valid func IsValidStorageType(storageType StorageType) bool { - for _, t := range storageTypes { - if t == storageType { - return true - } - } - return false + return slices.Contains(storageTypes, storageType) } // MinioStorageConfig represents the configuration for a minio storage @@ -162,7 +158,7 @@ const ( targetSecIsSec // target section is from the name seciont [name] ) -func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) { //nolint:unparam +func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetSecType, error) { //nolint:unparam // FIXME: targetSecType is always 0, wrong design? targetSec, err := rootCfg.GetSection(storageSectionName + "." + typ) if err != nil { if !IsValidStorageType(StorageType(typ)) { @@ -287,7 +283,7 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType, return &storage, nil } -func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl +func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl // duplicates azure setup var storage Storage storage.Type = StorageType(targetSec.Key("STORAGE_TYPE").String()) if err := targetSec.MapTo(&storage.MinioConfig); err != nil { @@ -316,7 +312,7 @@ func getStorageForMinio(targetSec, overrideSec ConfigSection, tp targetSecType, return &storage, nil } -func getStorageForAzureBlob(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl +func getStorageForAzureBlob(targetSec, overrideSec ConfigSection, tp targetSecType, name string) (*Storage, error) { //nolint:dupl // duplicates minio setup var storage Storage storage.Type = StorageType(targetSec.Key("STORAGE_TYPE").String()) if err := targetSec.MapTo(&storage.AzureBlobConfig); err != nil { diff --git a/modules/ssh/init.go b/modules/ssh/init.go index fdc11632e2..cfb0d5693a 100644 --- a/modules/ssh/init.go +++ b/modules/ssh/init.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) func Init() error { @@ -23,9 +24,11 @@ func Init() error { if setting.SSH.StartBuiltinServer { Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) - log.Info("SSH server started on %s. Cipher list (%v), key exchange algorithms (%v), MACs (%v)", + log.Info("SSH server started on %q. Ciphers: %v, key exchange algorithms: %v, MACs: %v", net.JoinHostPort(setting.SSH.ListenHost, strconv.Itoa(setting.SSH.ListenPort)), - setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs, + util.Iif[any](setting.SSH.ServerCiphers == nil, "default", setting.SSH.ServerCiphers), + util.Iif[any](setting.SSH.ServerKeyExchanges == nil, "default", setting.SSH.ServerKeyExchanges), + util.Iif[any](setting.SSH.ServerMACs == nil, "default", setting.SSH.ServerMACs), ) return nil } diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index ff0ad34a0d..3fea4851c7 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -333,7 +333,7 @@ func sshConnectionFailed(conn net.Conn, err error) { log.Warn("Failed authentication attempt from %s", conn.RemoteAddr()) } -// Listen starts a SSH server listens on given port. +// Listen starts an SSH server listening on given port. func Listen(host string, port int, ciphers, keyExchanges, macs []string) { srv := ssh.Server{ Addr: net.JoinHostPort(host, strconv.Itoa(port)), diff --git a/modules/structs/admin_user.go b/modules/structs/admin_user.go index f7c6d10ba0..c68b59a897 100644 --- a/modules/structs/admin_user.go +++ b/modules/structs/admin_user.go @@ -8,8 +8,11 @@ import "time" // CreateUserOption create user options type CreateUserOption struct { - SourceID int64 `json:"source_id"` + SourceID int64 `json:"source_id"` + // identifier of the user, provided by the external authenticator (if configured) + // default: empty LoginName string `json:"login_name"` + // username of the user // required: true Username string `json:"username" binding:"Required;Username;MaxSize(40)"` FullName string `json:"full_name" binding:"MaxSize(100)"` @@ -32,6 +35,8 @@ type CreateUserOption struct { type EditUserOption struct { // required: true SourceID int64 `json:"source_id"` + // identifier of the user, provided by the external authenticator (if configured) + // default: empty // required: true LoginName string `json:"login_name" binding:"Required"` // swagger:strfmt email diff --git a/modules/structs/commit_status_test.go b/modules/structs/commit_status_test.go deleted file mode 100644 index 88e09aadc1..0000000000 --- a/modules/structs/commit_status_test.go +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package structs - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestNoBetterThan(t *testing.T) { - type args struct { - css CommitStatusState - css2 CommitStatusState - } - var unExpectedState CommitStatusState - tests := []struct { - name string - args args - want bool - }{ - { - name: "success is no better than success", - args: args{ - css: CommitStatusSuccess, - css2: CommitStatusSuccess, - }, - want: true, - }, - { - name: "success is no better than pending", - args: args{ - css: CommitStatusSuccess, - css2: CommitStatusPending, - }, - want: false, - }, - { - name: "success is no better than failure", - args: args{ - css: CommitStatusSuccess, - css2: CommitStatusFailure, - }, - want: false, - }, - { - name: "success is no better than error", - args: args{ - css: CommitStatusSuccess, - css2: CommitStatusError, - }, - want: false, - }, - { - name: "pending is no better than success", - args: args{ - css: CommitStatusPending, - css2: CommitStatusSuccess, - }, - want: true, - }, - { - name: "pending is no better than pending", - args: args{ - css: CommitStatusPending, - css2: CommitStatusPending, - }, - want: true, - }, - { - name: "pending is no better than failure", - args: args{ - css: CommitStatusPending, - css2: CommitStatusFailure, - }, - want: false, - }, - { - name: "pending is no better than error", - args: args{ - css: CommitStatusPending, - css2: CommitStatusError, - }, - want: false, - }, - { - name: "failure is no better than success", - args: args{ - css: CommitStatusFailure, - css2: CommitStatusSuccess, - }, - want: true, - }, - { - name: "failure is no better than pending", - args: args{ - css: CommitStatusFailure, - css2: CommitStatusPending, - }, - want: true, - }, - { - name: "failure is no better than failure", - args: args{ - css: CommitStatusFailure, - css2: CommitStatusFailure, - }, - want: true, - }, - { - name: "failure is no better than error", - args: args{ - css: CommitStatusFailure, - css2: CommitStatusError, - }, - want: false, - }, - { - name: "error is no better than success", - args: args{ - css: CommitStatusError, - css2: CommitStatusSuccess, - }, - want: true, - }, - { - name: "error is no better than pending", - args: args{ - css: CommitStatusError, - css2: CommitStatusPending, - }, - want: true, - }, - { - name: "error is no better than failure", - args: args{ - css: CommitStatusError, - css2: CommitStatusFailure, - }, - want: true, - }, - { - name: "error is no better than error", - args: args{ - css: CommitStatusError, - css2: CommitStatusError, - }, - want: true, - }, - { - name: "unExpectedState is no better than success", - args: args{ - css: unExpectedState, - css2: CommitStatusSuccess, - }, - want: false, - }, - { - name: "unExpectedState is no better than unExpectedState", - args: args{ - css: unExpectedState, - css2: unExpectedState, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := tt.args.css.NoBetterThan(tt.args.css2) - assert.Equal(t, tt.want, result) - }) - } -} diff --git a/modules/structs/git_blob.go b/modules/structs/git_blob.go index 96770cc62e..643b69ed37 100644 --- a/modules/structs/git_blob.go +++ b/modules/structs/git_blob.go @@ -10,4 +10,7 @@ type GitBlobResponse struct { URL string `json:"url"` SHA string `json:"sha"` Size int64 `json:"size"` + + LfsOid *string `json:"lfs_oid,omitempty"` + LfsSize *int64 `json:"lfs_size,omitempty"` } diff --git a/modules/structs/hook.go b/modules/structs/hook.go index aaa9fbc9d3..ac779a5740 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -71,7 +71,8 @@ type PayloadUser struct { // Full name of the commit author Name string `json:"name"` // swagger:strfmt email - Email string `json:"email"` + Email string `json:"email"` + // username of the user UserName string `json:"username"` } @@ -286,6 +287,8 @@ const ( HookIssueReOpened HookIssueAction = "reopened" // HookIssueEdited edited HookIssueEdited HookIssueAction = "edited" + // HookIssueDeleted is an issue action for deleting an issue + HookIssueDeleted HookIssueAction = "deleted" // HookIssueAssigned assigned HookIssueAssigned HookIssueAction = "assigned" // HookIssueUnassigned unassigned @@ -470,6 +473,22 @@ func (p *CommitStatusPayload) JSONPayload() ([]byte, error) { return json.MarshalIndent(p, "", " ") } +// WorkflowRunPayload represents a payload information of workflow run event. +type WorkflowRunPayload struct { + Action string `json:"action"` + Workflow *ActionWorkflow `json:"workflow"` + WorkflowRun *ActionWorkflowRun `json:"workflow_run"` + PullRequest *PullRequest `json:"pull_request,omitempty"` + Organization *Organization `json:"organization,omitempty"` + Repo *Repository `json:"repository"` + Sender *User `json:"sender"` +} + +// JSONPayload implements Payload +func (p *WorkflowRunPayload) JSONPayload() ([]byte, error) { + return json.MarshalIndent(p, "", " ") +} + // WorkflowJobPayload represents a payload information of workflow job event. type WorkflowJobPayload struct { Action string `json:"action"` diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 6a6b74c34e..df0be8f9ec 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -203,7 +203,7 @@ func (l *IssueTemplateStringSlice) UnmarshalYAML(value *yaml.Node) error { if err != nil { return err } - for _, v := range strings.Split(str, ",") { + for v := range strings.SplitSeq(str, ",") { if v = strings.TrimSpace(v); v == "" { continue } diff --git a/modules/structs/issue_tracked_time.go b/modules/structs/issue_tracked_time.go index a3904af80e..befcfb323d 100644 --- a/modules/structs/issue_tracked_time.go +++ b/modules/structs/issue_tracked_time.go @@ -14,7 +14,7 @@ type AddTimeOption struct { Time int64 `json:"time" binding:"Required"` // swagger:strfmt date-time Created time.Time `json:"created"` - // User who spent the time (optional) + // username of the user who spent the time working on the issue (optional) User string `json:"user_name"` } @@ -26,7 +26,8 @@ type TrackedTime struct { // Time in seconds Time int64 `json:"time"` // deprecated (only for backwards compatibility) - UserID int64 `json:"user_id"` + UserID int64 `json:"user_id"` + // username of the user UserName string `json:"user_name"` // deprecated (only for backwards compatibility) IssueID int64 `json:"issue_id"` diff --git a/modules/structs/org.go b/modules/structs/org.go index f93b3b6493..33b45c6344 100644 --- a/modules/structs/org.go +++ b/modules/structs/org.go @@ -15,6 +15,7 @@ type Organization struct { Location string `json:"location"` Visibility string `json:"visibility"` RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` + // username of the organization // deprecated UserName string `json:"username"` } @@ -30,6 +31,7 @@ type OrganizationPermissions struct { // CreateOrgOption options for creating an organization type CreateOrgOption struct { + // username of the organization // required: true UserName string `json:"username" binding:"Required;Username;MaxSize(40)"` FullName string `json:"full_name" binding:"MaxSize(100)"` diff --git a/modules/structs/release.go b/modules/structs/release.go index c7378645c2..fac86ca7a2 100644 --- a/modules/structs/release.go +++ b/modules/structs/release.go @@ -33,6 +33,7 @@ type Release struct { type CreateReleaseOption struct { // required: true TagName string `json:"tag_name" binding:"Required"` + TagMessage string `json:"tag_message"` Target string `json:"target_commitish"` Title string `json:"name"` Note string `json:"body"` diff --git a/modules/structs/repo.go b/modules/structs/repo.go index c48e71bc4e..abc8076387 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -113,7 +113,7 @@ type Repository struct { // enum: sha1,sha256 ObjectFormatName string `json:"object_format_name"` // swagger:strfmt date-time - MirrorUpdated time.Time `json:"mirror_updated,omitempty"` + MirrorUpdated time.Time `json:"mirror_updated"` RepoTransfer *RepoTransfer `json:"repo_transfer"` Topics []string `json:"topics"` Licenses []string `json:"licenses"` @@ -359,7 +359,7 @@ type MigrateRepoOptions struct { // required: true RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` - // enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase + // enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase,codecommit Service string `json:"service"` AuthUsername string `json:"auth_username"` AuthPassword string `json:"auth_password"` diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index 75f8e188dd..ac1c288270 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -57,7 +57,7 @@ type ActionWorkflow struct { HTMLURL string `json:"html_url"` BadgeURL string `json:"badge_url"` // swagger:strfmt date-time - DeletedAt time.Time `json:"deleted_at,omitempty"` + DeletedAt time.Time `json:"deleted_at"` } // ActionWorkflowResponse returns a ActionWorkflow @@ -86,9 +86,39 @@ type ActionArtifact struct { // ActionWorkflowRun represents a WorkflowRun type ActionWorkflowRun struct { - ID int64 `json:"id"` - RepositoryID int64 `json:"repository_id"` - HeadSha string `json:"head_sha"` + ID int64 `json:"id"` + URL string `json:"url"` + HTMLURL string `json:"html_url"` + DisplayTitle string `json:"display_title"` + Path string `json:"path"` + Event string `json:"event"` + RunAttempt int64 `json:"run_attempt"` + RunNumber int64 `json:"run_number"` + RepositoryID int64 `json:"repository_id,omitempty"` + HeadSha string `json:"head_sha"` + HeadBranch string `json:"head_branch,omitempty"` + Status string `json:"status"` + Actor *User `json:"actor,omitempty"` + TriggerActor *User `json:"trigger_actor,omitempty"` + Repository *Repository `json:"repository,omitempty"` + HeadRepository *Repository `json:"head_repository,omitempty"` + Conclusion string `json:"conclusion,omitempty"` + // swagger:strfmt date-time + StartedAt time.Time `json:"started_at"` + // swagger:strfmt date-time + CompletedAt time.Time `json:"completed_at"` +} + +// ActionWorkflowRunsResponse returns ActionWorkflowRuns +type ActionWorkflowRunsResponse struct { + Entries []*ActionWorkflowRun `json:"workflow_runs"` + TotalCount int64 `json:"total_count"` +} + +// ActionWorkflowJobsResponse returns ActionWorkflowJobs +type ActionWorkflowJobsResponse struct { + Entries []*ActionWorkflowJob `json:"jobs"` + TotalCount int64 `json:"total_count"` } // ActionArtifactsResponse returns ActionArtifacts @@ -104,9 +134,9 @@ type ActionWorkflowStep struct { Status string `json:"status"` Conclusion string `json:"conclusion,omitempty"` // swagger:strfmt date-time - StartedAt time.Time `json:"started_at,omitempty"` + StartedAt time.Time `json:"started_at"` // swagger:strfmt date-time - CompletedAt time.Time `json:"completed_at,omitempty"` + CompletedAt time.Time `json:"completed_at"` } // ActionWorkflowJob represents a WorkflowJob @@ -129,9 +159,9 @@ type ActionWorkflowJob struct { // swagger:strfmt date-time CreatedAt time.Time `json:"created_at"` // swagger:strfmt date-time - StartedAt time.Time `json:"started_at,omitempty"` + StartedAt time.Time `json:"started_at"` // swagger:strfmt date-time - CompletedAt time.Time `json:"completed_at,omitempty"` + CompletedAt time.Time `json:"completed_at"` } // ActionRunnerLabel represents a Runner Label diff --git a/modules/structs/repo_branch.go b/modules/structs/repo_branch.go index 55c98d60b9..5416f43b0d 100644 --- a/modules/structs/repo_branch.go +++ b/modules/structs/repo_branch.go @@ -136,6 +136,7 @@ type UpdateBranchProtectionPriories struct { type MergeUpstreamRequest struct { Branch string `json:"branch"` + FfOnly bool `json:"ff_only"` } type MergeUpstreamResponse struct { diff --git a/modules/structs/repo_file.go b/modules/structs/repo_file.go index b0e0bd979e..5a86db868b 100644 --- a/modules/structs/repo_file.go +++ b/modules/structs/repo_file.go @@ -22,6 +22,23 @@ type FileOptions struct { Signoff bool `json:"signoff"` } +type FileOptionsWithSHA struct { + FileOptions + // the blob ID (SHA) for the file that already exists, it is required for changing existing files + // required: true + SHA string `json:"sha" binding:"Required"` +} + +func (f *FileOptions) GetFileOptions() *FileOptions { + return f +} + +type FileOptionsInterface interface { + GetFileOptions() *FileOptions +} + +var _ FileOptionsInterface = (*FileOptions)(nil) + // CreateFileOptions options for creating files // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type CreateFileOptions struct { @@ -31,29 +48,16 @@ type CreateFileOptions struct { ContentBase64 string `json:"content"` } -// Branch returns branch name -func (o *CreateFileOptions) Branch() string { - return o.FileOptions.BranchName -} - // DeleteFileOptions options for deleting files (used for other File structs below) // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type DeleteFileOptions struct { - FileOptions - // sha is the SHA for the file that already exists - // required: true - SHA string `json:"sha" binding:"Required"` -} - -// Branch returns branch name -func (o *DeleteFileOptions) Branch() string { - return o.FileOptions.BranchName + FileOptionsWithSHA } // UpdateFileOptions options for updating files // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type UpdateFileOptions struct { - DeleteFileOptions + FileOptionsWithSHA // content must be base64 encoded // required: true ContentBase64 string `json:"content"` @@ -61,23 +65,21 @@ type UpdateFileOptions struct { FromPath string `json:"from_path" binding:"MaxSize(500)"` } -// Branch returns branch name -func (o *UpdateFileOptions) Branch() string { - return o.FileOptions.BranchName -} +// FIXME: there is no LastCommitID in FileOptions, actually it should be an alternative to the SHA in ChangeFileOperation // ChangeFileOperation for creating, updating or deleting a file type ChangeFileOperation struct { - // indicates what to do with the file + // indicates what to do with the file: "create" for creating a new file, "update" for updating an existing file, + // "upload" for creating or updating a file, "rename" for renaming a file, and "delete" for deleting an existing file. // required: true - // enum: create,update,delete + // enum: create,update,upload,rename,delete Operation string `json:"operation" binding:"Required"` // path to the existing or new file // required: true Path string `json:"path" binding:"Required;MaxSize(500)"` - // new or updated file content, must be base64 encoded + // new or updated file content, it must be base64 encoded ContentBase64 string `json:"content"` - // sha is the SHA for the file that already exists, required for update or delete + // the blob ID (SHA) for the file that already exists, required for changing existing files SHA string `json:"sha"` // old path of the file to move FromPath string `json:"from_path"` @@ -92,20 +94,10 @@ type ChangeFilesOptions struct { Files []*ChangeFileOperation `json:"files" binding:"Required"` } -// Branch returns branch name -func (o *ChangeFilesOptions) Branch() string { - return o.FileOptions.BranchName -} - -// FileOptionInterface provides a unified interface for the different file options -type FileOptionInterface interface { - Branch() string -} - // ApplyDiffPatchFileOptions options for applying a diff patch // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type ApplyDiffPatchFileOptions struct { - DeleteFileOptions + FileOptions // required: true Content string `json:"content"` } @@ -117,16 +109,24 @@ type FileLinksResponse struct { HTMLURL *string `json:"html"` } +type ContentsExtResponse struct { + FileContents *ContentsResponse `json:"file_contents,omitempty"` + DirContents []*ContentsResponse `json:"dir_contents,omitempty"` +} + // ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content type ContentsResponse struct { - Name string `json:"name"` - Path string `json:"path"` - SHA string `json:"sha"` - LastCommitSHA string `json:"last_commit_sha"` + Name string `json:"name"` + Path string `json:"path"` + SHA string `json:"sha"` + + LastCommitSHA *string `json:"last_commit_sha,omitempty"` // swagger:strfmt date-time - LastCommitterDate time.Time `json:"last_committer_date"` + LastCommitterDate *time.Time `json:"last_committer_date,omitempty"` // swagger:strfmt date-time - LastAuthorDate time.Time `json:"last_author_date"` + LastAuthorDate *time.Time `json:"last_author_date,omitempty"` + LastCommitMessage *string `json:"last_commit_message,omitempty"` + // `type` will be `file`, `dir`, `symlink`, or `submodule` Type string `json:"type"` Size int64 `json:"size"` @@ -143,6 +143,9 @@ type ContentsResponse struct { // `submodule_git_url` is populated when `type` is `submodule`, otherwise null SubmoduleGitURL *string `json:"submodule_git_url"` Links *FileLinksResponse `json:"_links"` + + LfsOid *string `json:"lfs_oid,omitempty"` + LfsSize *int64 `json:"lfs_size,omitempty"` } // FileCommitResponse contains information generated from a Git commit for a repo's file. diff --git a/modules/structs/status.go b/modules/structs/status.go index c1d8b902ec..a9779541ff 100644 --- a/modules/structs/status.go +++ b/modules/structs/status.go @@ -5,17 +5,19 @@ package structs import ( "time" + + "code.gitea.io/gitea/modules/commitstatus" ) // CommitStatus holds a single status of a single Commit type CommitStatus struct { - ID int64 `json:"id"` - State CommitStatusState `json:"status"` - TargetURL string `json:"target_url"` - Description string `json:"description"` - URL string `json:"url"` - Context string `json:"context"` - Creator *User `json:"creator"` + ID int64 `json:"id"` + State commitstatus.CommitStatusState `json:"status"` + TargetURL string `json:"target_url"` + Description string `json:"description"` + URL string `json:"url"` + Context string `json:"context"` + Creator *User `json:"creator"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time @@ -24,19 +26,19 @@ type CommitStatus struct { // CombinedStatus holds the combined state of several statuses for a single commit type CombinedStatus struct { - State CommitStatusState `json:"state"` - SHA string `json:"sha"` - TotalCount int `json:"total_count"` - Statuses []*CommitStatus `json:"statuses"` - Repository *Repository `json:"repository"` - CommitURL string `json:"commit_url"` - URL string `json:"url"` + State commitstatus.CommitStatusState `json:"state"` + SHA string `json:"sha"` + TotalCount int `json:"total_count"` + Statuses []*CommitStatus `json:"statuses"` + Repository *Repository `json:"repository"` + CommitURL string `json:"commit_url"` + URL string `json:"url"` } // CreateStatusOption holds the information needed to create a new CommitStatus for a Commit type CreateStatusOption struct { - State CommitStatusState `json:"state"` - TargetURL string `json:"target_url"` - Description string `json:"description"` - Context string `json:"context"` + State commitstatus.CommitStatusState `json:"state"` + TargetURL string `json:"target_url"` + Description string `json:"description"` + Context string `json:"context"` } diff --git a/modules/structs/user.go b/modules/structs/user.go index 5ed677f239..89349cda2c 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -15,9 +15,9 @@ import ( type User struct { // the user's id ID int64 `json:"id"` - // the user's username + // login of the user, same as `username` UserName string `json:"login"` - // the user's authentication sign-in name. + // identifier of the user, provided by the external authenticator (if configured) // default: empty LoginName string `json:"login_name"` // The ID of the user's Authentication Source @@ -35,9 +35,9 @@ type User struct { // Is the user an administrator IsAdmin bool `json:"is_admin"` // swagger:strfmt date-time - LastLogin time.Time `json:"last_login,omitempty"` + LastLogin time.Time `json:"last_login"` // swagger:strfmt date-time - Created time.Time `json:"created,omitempty"` + Created time.Time `json:"created"` // Is user restricted Restricted bool `json:"restricted"` // Is user active diff --git a/modules/structs/user_email.go b/modules/structs/user_email.go index 9319667e8f..01895a0058 100644 --- a/modules/structs/user_email.go +++ b/modules/structs/user_email.go @@ -11,6 +11,7 @@ type Email struct { Verified bool `json:"verified"` Primary bool `json:"primary"` UserID int64 `json:"user_id"` + // username of the user UserName string `json:"username"` } diff --git a/modules/structs/user_gpgkey.go b/modules/structs/user_gpgkey.go index ff9b0aea1d..deae70de33 100644 --- a/modules/structs/user_gpgkey.go +++ b/modules/structs/user_gpgkey.go @@ -21,9 +21,9 @@ type GPGKey struct { CanCertify bool `json:"can_certify"` Verified bool `json:"verified"` // swagger:strfmt date-time - Created time.Time `json:"created_at,omitempty"` + Created time.Time `json:"created_at"` // swagger:strfmt date-time - Expires time.Time `json:"expires_at,omitempty"` + Expires time.Time `json:"expires_at"` } // GPGKeyEmail an email attached to a GPGKey diff --git a/modules/structs/user_key.go b/modules/structs/user_key.go index c4c41207e1..16225a852a 100644 --- a/modules/structs/user_key.go +++ b/modules/structs/user_key.go @@ -15,8 +15,8 @@ type PublicKey struct { Title string `json:"title,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` // swagger:strfmt date-time - Created time.Time `json:"created_at,omitempty"` - Updated time.Time `json:"last_used_at,omitempty"` + Created time.Time `json:"created_at"` + Updated time.Time `json:"last_used_at"` Owner *User `json:"user,omitempty"` ReadOnly bool `json:"read_only,omitempty"` KeyType string `json:"key_type,omitempty"` diff --git a/modules/templates/eval/eval_test.go b/modules/templates/eval/eval_test.go index c9e514b5eb..f956f6cbdf 100644 --- a/modules/templates/eval/eval_test.go +++ b/modules/templates/eval/eval_test.go @@ -12,7 +12,7 @@ import ( ) func tokens(s string) (a []any) { - for _, v := range strings.Fields(s) { + for v := range strings.FieldsSeq(s) { a = append(a, v) } return a diff --git a/modules/templates/helper.go b/modules/templates/helper.go index d55d4f87c5..ff3f7cfda1 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -6,7 +6,6 @@ package templates import ( "fmt" - "html" "html/template" "net/url" "strconv" @@ -38,9 +37,7 @@ func NewFuncMap() template.FuncMap { "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. "Iif": iif, "Eval": evalTokens, - "SafeHTML": safeHTML, "HTMLFormat": htmlFormat, - "HTMLEscape": htmlEscape, "QueryEscape": queryEscape, "QueryBuild": QueryBuild, "JSEscape": jsEscapeSafe, @@ -165,30 +162,9 @@ func NewFuncMap() template.FuncMap { } } -// safeHTML render raw as HTML -func safeHTML(s any) template.HTML { - switch v := s.(type) { - case string: - return template.HTML(v) - case template.HTML: - return v - } - panic(fmt.Sprintf("unexpected type %T", s)) -} - -// SanitizeHTML sanitizes the input by pre-defined markdown rules +// SanitizeHTML sanitizes the input by default sanitization rules. func SanitizeHTML(s string) template.HTML { - return template.HTML(markup.Sanitize(s)) -} - -func htmlEscape(s any) template.HTML { - switch v := s.(type) { - case string: - return template.HTML(html.EscapeString(v)) - case template.HTML: - return v - } - panic(fmt.Sprintf("unexpected type %T", s)) + return markup.Sanitize(s) } func htmlFormat(s any, args ...any) template.HTML { diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index 529284f7e8..8073a6e5f5 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -42,7 +42,7 @@ var ( var ErrTemplateNotInitialized = errors.New("template system is not initialized, check your log for errors") -func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive +func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ctx context.Context) error { //nolint:revive // we don't use ctx, only pass it to the template executor name := string(tplName) if respWriter, ok := w.(http.ResponseWriter); ok { if respWriter.Header().Get("Content-Type") == "" { @@ -57,7 +57,7 @@ func (h *HTMLRender) HTML(w io.Writer, status int, tplName TplName, data any, ct return t.Execute(w, data) } -func (h *HTMLRender) TemplateLookup(name string, ctx context.Context) (TemplateExecutor, error) { //nolint:revive +func (h *HTMLRender) TemplateLookup(name string, ctx context.Context) (TemplateExecutor, error) { //nolint:revive // we don't use ctx, only pass it to the template executor tmpls := h.templates.Load() if tmpls == nil { return nil, ErrTemplateNotInitialized @@ -251,7 +251,7 @@ func extractErrorLine(code []byte, lineNum, posNum int, target string) string { b := bufio.NewReader(bytes.NewReader(code)) var line []byte var err error - for i := 0; i < lineNum; i++ { + for i := range lineNum { if line, err = b.ReadBytes('\n'); err != nil { if i == lineNum-1 && errors.Is(err, io.EOF) { err = nil diff --git a/modules/templates/scopedtmpl/scopedtmpl.go b/modules/templates/scopedtmpl/scopedtmpl.go index 2722ba97a2..34e8b9ad70 100644 --- a/modules/templates/scopedtmpl/scopedtmpl.go +++ b/modules/templates/scopedtmpl/scopedtmpl.go @@ -7,6 +7,7 @@ import ( "fmt" "html/template" "io" + "maps" "reflect" "sync" texttemplate "text/template" @@ -40,9 +41,7 @@ func (t *ScopedTemplate) Funcs(funcMap template.FuncMap) { panic("cannot add new functions to frozen template set") } t.all.Funcs(funcMap) - for k, v := range funcMap { - t.parseFuncs[k] = v - } + maps.Copy(t.parseFuncs, funcMap) } func (t *ScopedTemplate) New(name string) *template.Template { @@ -103,31 +102,28 @@ func escapeTemplate(t *template.Template) error { return nil } -//nolint:unused type htmlTemplate struct { - escapeErr error - text *texttemplate.Template + _/*escapeErr*/ error + text *texttemplate.Template } -//nolint:unused type textTemplateCommon struct { - tmpl map[string]*template.Template // Map from name to defined templates. - muTmpl sync.RWMutex // protects tmpl - option struct { + _/*tmpl*/ map[string]*template.Template + _/*muTmpl*/ sync.RWMutex + _/*option*/ struct { missingKey int } - muFuncs sync.RWMutex // protects parseFuncs and execFuncs - parseFuncs texttemplate.FuncMap - execFuncs map[string]reflect.Value + muFuncs sync.RWMutex + _/*parseFuncs*/ texttemplate.FuncMap + execFuncs map[string]reflect.Value } -//nolint:unused type textTemplate struct { - name string + _/*name*/ string *parse.Tree *textTemplateCommon - leftDelim string - rightDelim string + _/*leftDelim*/ string + _/*rightDelim*/ string } func ptr[T, P any](ptr *P) *T { @@ -159,9 +155,7 @@ func newScopedTemplateSet(all *template.Template, name string) (*scopedTemplateS textTmplPtr.muFuncs.Lock() ts.execFuncs = map[string]reflect.Value{} - for k, v := range textTmplPtr.execFuncs { - ts.execFuncs[k] = v - } + maps.Copy(ts.execFuncs, textTmplPtr.execFuncs) textTmplPtr.muFuncs.Unlock() var collectTemplates func(nodes []parse.Node) @@ -220,9 +214,7 @@ func (ts *scopedTemplateSet) newExecutor(funcMap map[string]any) TemplateExecuto tmpl := texttemplate.New("") tmplPtr := ptr[textTemplate](tmpl) tmplPtr.execFuncs = map[string]reflect.Value{} - for k, v := range ts.execFuncs { - tmplPtr.execFuncs[k] = v - } + maps.Copy(tmplPtr.execFuncs, ts.execFuncs) if funcMap != nil { tmpl.Funcs(funcMap) } diff --git a/modules/templates/static.go b/modules/templates/static.go deleted file mode 100644 index b5a7e561ec..0000000000 --- a/modules/templates/static.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2016 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build bindata - -package templates - -import ( - "time" - - "code.gitea.io/gitea/modules/assetfs" - "code.gitea.io/gitea/modules/timeutil" -) - -// GlobalModTime provide a global mod time for embedded asset files -func GlobalModTime(filename string) time.Time { - return timeutil.GetExecutableModTime() -} - -func BuiltinAssets() *assetfs.Layer { - return assetfs.Bindata("builtin(bindata)", Assets) -} diff --git a/modules/templates/templates_bindata.go b/modules/templates/templates_bindata.go index 6f1d3cf539..a919591ecf 100644 --- a/modules/templates/templates_bindata.go +++ b/modules/templates/templates_bindata.go @@ -3,6 +3,21 @@ //go:build bindata +//go:generate go run ../../build/generate-bindata.go ../../templates bindata.dat + package templates -//go:generate go run ../../build/generate-bindata.go ../../templates templates bindata.go true +import ( + "sync" + + _ "embed" + + "code.gitea.io/gitea/modules/assetfs" +) + +//go:embed bindata.dat +var bindata []byte + +var BuiltinAssets = sync.OnceValue(func() *assetfs.Layer { + return assetfs.Bindata("builtin(bindata)", assetfs.NewEmbeddedFS(bindata)) +}) diff --git a/modules/templates/dynamic.go b/modules/templates/templates_dynamic.go index e1babd83c9..e1babd83c9 100644 --- a/modules/templates/dynamic.go +++ b/modules/templates/templates_dynamic.go diff --git a/modules/templates/util_json.go b/modules/templates/util_json.go index 71a4e23d36..29a04290fa 100644 --- a/modules/templates/util_json.go +++ b/modules/templates/util_json.go @@ -9,11 +9,11 @@ import ( "code.gitea.io/gitea/modules/json" ) -type JsonUtils struct{} //nolint:revive +type JsonUtils struct{} //nolint:revive // variable naming triggers on Json, wants JSON var jsonUtils = JsonUtils{} -func NewJsonUtils() *JsonUtils { //nolint:revive +func NewJsonUtils() *JsonUtils { //nolint:revive // variable naming triggers on Json, wants JSON return &jsonUtils } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 8d9ba1000c..1056c42643 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -38,8 +38,8 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { // RenderCommitMessage renders commit message with XSS-safe and special links. func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) template.HTML { cleanMsg := template.HTMLEscapeString(msg) - // we can safely assume that it will not return any error, since there - // shouldn't be any special HTML. + // we can safely assume that it will not return any error, since there shouldn't be any special HTML. + // "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed. fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) if err != nil { log.Error("PostProcessCommitMessage: %v", err) @@ -47,7 +47,7 @@ func (ut *RenderUtils) RenderCommitMessage(msg string, repo *repo.Repository) te } msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") if len(msgLines) == 0 { - return template.HTML("") + return "" } return renderCodeBlock(template.HTML(msgLines[0])) } @@ -122,8 +122,23 @@ func (ut *RenderUtils) RenderIssueSimpleTitle(text string) template.HTML { return ret } -// RenderLabel renders a label +func (ut *RenderUtils) RenderLabelWithLink(label *issues_model.Label, link any) template.HTML { + var attrHref template.HTML + switch link.(type) { + case template.URL, string: + attrHref = htmlutil.HTMLFormat(`href="%s"`, link) + default: + panic(fmt.Sprintf("unexpected type %T for link", link)) + } + return ut.renderLabelWithTag(label, "a", attrHref) +} + func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { + return ut.renderLabelWithTag(label, "span", "") +} + +// RenderLabel renders a label +func (ut *RenderUtils) renderLabelWithTag(label *issues_model.Label, tagName, tagAttrs template.HTML) template.HTML { locale := ut.ctx.Value(translation.ContextKey).(translation.Locale) var extraCSSClasses string textColor := util.ContrastColor(label.Color) @@ -137,8 +152,8 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { if labelScope == "" { // Regular label - return htmlutil.HTMLFormat(`<div class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s">%s</div>`, - extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name)) + return htmlutil.HTMLFormat(`<%s %s class="ui label %s" style="color: %s !important; background-color: %s !important;" data-tooltip-content title="%s"><span class="gt-ellipsis">%s</span></%s>`, + tagName, tagAttrs, extraCSSClasses, textColor, label.Color, descriptionText, ut.RenderEmoji(label.Name), tagName) } // Scoped label @@ -152,7 +167,7 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { // Ensure we add the same amount of contrast also near 0 and 1. darken := contrast + math.Max(luminance+contrast-1.0, 0.0) lighten := contrast + math.Max(contrast-luminance, 0.0) - // Compute factor to keep RGB values proportional. + // Compute the factor to keep RGB values proportional. darkenFactor := math.Max(luminance-darken, 0.0) / math.Max(luminance, 1.0/255.0) lightenFactor := math.Min(luminance+lighten, 1.0) / math.Max(luminance, 1.0/255.0) @@ -173,26 +188,29 @@ func (ut *RenderUtils) RenderLabel(label *issues_model.Label) template.HTML { if label.ExclusiveOrder > 0 { // <scope> | <label> | <order> - return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+ + return htmlutil.HTMLFormat(`<%s %s class="ui label %s scope-parent" data-tooltip-content title="%s">`+ `<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+ `<div class="ui label scope-middle" style="color: %s !important; background-color: %s !important">%s</div>`+ `<div class="ui label scope-right">%d</div>`+ - `</span>`, + `</%s>`, + tagName, tagAttrs, extraCSSClasses, descriptionText, textColor, scopeColor, scopeHTML, textColor, itemColor, itemHTML, - label.ExclusiveOrder) + label.ExclusiveOrder, + tagName) } // <scope> | <label> - return htmlutil.HTMLFormat(`<span class="ui label %s scope-parent" data-tooltip-content title="%s">`+ + return htmlutil.HTMLFormat(`<%s %s class="ui label %s scope-parent" data-tooltip-content title="%s">`+ `<div class="ui label scope-left" style="color: %s !important; background-color: %s !important">%s</div>`+ `<div class="ui label scope-right" style="color: %s !important; background-color: %s !important">%s</div>`+ - `</span>`, + `</%s>`, + tagName, tagAttrs, extraCSSClasses, descriptionText, textColor, scopeColor, scopeHTML, textColor, itemColor, itemHTML, - ) + tagName) } // RenderEmoji renders html text with emoji post processors @@ -218,7 +236,7 @@ func reactionToEmoji(reaction string) template.HTML { return template.HTML(fmt.Sprintf(`<img alt=":%s:" src="%s/assets/img/emoji/%s.png"></img>`, reaction, setting.StaticURLPrefix, url.PathEscape(reaction))) } -func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive +func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:revive // variable naming triggers on Html, wants HTML output, err := markdown.RenderString(markup.NewRenderContext(ut.ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), input) if err != nil { log.Error("RenderString: %v", err) @@ -235,7 +253,8 @@ func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink strin if label == nil { continue } - htmlCode += fmt.Sprintf(`<a href="%s?labels=%d">%s</a>`, baseLink, label.ID, ut.RenderLabel(label)) + link := fmt.Sprintf("%s?labels=%d", baseLink, label.ID) + htmlCode += string(ut.RenderLabelWithLink(label, template.URL(link))) } htmlCode += "</span>" return template.HTML(htmlCode) diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 9b51d0cd57..5c37f084df 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -205,6 +205,17 @@ func TestRenderLabels(t *testing.T) { issue = &issues.Issue{IsPull: true} expected = `/owner/repo/pulls?labels=123` assert.Contains(t, ut.RenderLabels([]*issues.Label{label}, "/owner/repo", issue), expected) + + expectedLabel := `<a href="<>" class="ui label " style="color: #fff !important; background-color: label-color !important;" data-tooltip-content title=""><span class="gt-ellipsis">label-name</span></a>` + assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, "<>"))) + assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, template.URL("<>")))) + + label = &issues.Label{ID: 123, Name: "</>", Exclusive: true} + expectedLabel = `<a href="" class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important"><</div><div class="ui label scope-right" style="color: #fff !important; background-color: #000000 !important">></div></a>` + assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, ""))) + label = &issues.Label{ID: 123, Name: "</>", Exclusive: true, ExclusiveOrder: 1} + expectedLabel = `<a href="" class="ui label scope-parent" data-tooltip-content title=""><div class="ui label scope-left" style="color: #fff !important; background-color: #000000 !important"><</div><div class="ui label scope-middle" style="color: #fff !important; background-color: #000000 !important">></div><div class="ui label scope-right">1</div></a>` + assert.Equal(t, expectedLabel, string(ut.RenderLabelWithLink(label, ""))) } func TestUserMention(t *testing.T) { diff --git a/modules/test/utils.go b/modules/test/utils.go index 3051d3d286..53c6a3ed52 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -17,6 +17,7 @@ import ( // RedirectURL returns the redirect URL of a http response. // It also works for JSONRedirect: `{"redirect": "..."}` +// FIXME: it should separate the logic of checking from header and JSON body func RedirectURL(resp http.ResponseWriter) string { loc := resp.Header().Get("Location") if loc != "" { @@ -34,6 +35,15 @@ func RedirectURL(resp http.ResponseWriter) string { return "" } +func ParseJSONError(buf []byte) (ret struct { + ErrorMessage string `json:"errorMessage"` + RenderFormat string `json:"renderFormat"` +}, +) { + _ = json.Unmarshal(buf, &ret) + return ret +} + func IsNormalPageCompleted(s string) bool { return strings.Contains(s, `<footer class="page-footer"`) && strings.Contains(s, `</html>`) } diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index 8e970aa2be..60e281d403 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -92,7 +92,7 @@ func (w *testLoggerWriterCloser) Reset() { // Printf takes a format and args and prints the string to os.Stdout func Printf(format string, args ...any) { if !log.CanColorStdout { - for i := 0; i < len(args); i++ { + for i := range args { if c, ok := args[i].(*log.ColoredValue); ok { args[i] = c.Value() } diff --git a/modules/timeutil/executable.go b/modules/timeutil/executable.go deleted file mode 100644 index 57ae8b2a9d..0000000000 --- a/modules/timeutil/executable.go +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package timeutil - -import ( - "os" - "path/filepath" - "sync" - "time" - - "code.gitea.io/gitea/modules/log" -) - -var ( - executablModTime = time.Now() - executablModTimeOnce sync.Once -) - -// GetExecutableModTime get executable file modified time of current process. -func GetExecutableModTime() time.Time { - executablModTimeOnce.Do(func() { - exePath, err := os.Executable() - if err != nil { - log.Error("os.Executable: %v", err) - return - } - - exePath, err = filepath.Abs(exePath) - if err != nil { - log.Error("filepath.Abs: %v", err) - return - } - - exePath, err = filepath.EvalSymlinks(exePath) - if err != nil { - log.Error("filepath.EvalSymlinks: %v", err) - return - } - - st, err := os.Stat(exePath) - if err != nil { - log.Error("os.Stat: %v", err) - return - } - - executablModTime = st.ModTime() - }) - return executablModTime -} diff --git a/modules/typesniffer/typesniffer.go b/modules/typesniffer/typesniffer.go index 8cb3d278ce..2e8d9c4a1e 100644 --- a/modules/typesniffer/typesniffer.go +++ b/modules/typesniffer/typesniffer.go @@ -6,18 +6,14 @@ package typesniffer import ( "bytes" "encoding/binary" - "fmt" - "io" "net/http" "regexp" "slices" "strings" - - "code.gitea.io/gitea/modules/util" + "sync" ) -// Use at most this many bytes to determine Content Type. -const sniffLen = 1024 +const SniffContentSize = 1024 const ( MimeTypeImageSvg = "image/svg+xml" @@ -26,22 +22,30 @@ const ( MimeTypeApplicationOctetStream = "application/octet-stream" ) -var ( - svgComment = regexp.MustCompile(`(?s)<!--.*?-->`) - svgTagRegex = regexp.MustCompile(`(?si)\A\s*(?:(<!DOCTYPE\s+svg([\s:]+.*?>|>))\s*)*<svg\b`) - svgTagInXMLRegex = regexp.MustCompile(`(?si)\A<\?xml\b.*?\?>\s*(?:(<!DOCTYPE\s+svg([\s:]+.*?>|>))\s*)*<svg\b`) -) - -// SniffedType contains information about a blobs type. +var globalVars = sync.OnceValue(func() (ret struct { + svgComment, svgTagRegex, svgTagInXMLRegex *regexp.Regexp +}, +) { + ret.svgComment = regexp.MustCompile(`(?s)<!--.*?-->`) + ret.svgTagRegex = regexp.MustCompile(`(?si)\A\s*(?:(<!DOCTYPE\s+svg([\s:]+.*?>|>))\s*)*<svg\b`) + ret.svgTagInXMLRegex = regexp.MustCompile(`(?si)\A<\?xml\b.*?\?>\s*(?:(<!DOCTYPE\s+svg([\s:]+.*?>|>))\s*)*<svg\b`) + return ret +}) + +// SniffedType contains information about a blob's type. type SniffedType struct { contentType string } -// IsText etects if content format is plain text. +// IsText detects if the content format is text family, including text/plain, text/html, text/css, etc. func (ct SniffedType) IsText() bool { return strings.Contains(ct.contentType, "text/") } +func (ct SniffedType) IsTextPlain() bool { + return strings.Contains(ct.contentType, "text/plain") +} + // IsImage detects if data is an image format func (ct SniffedType) IsImage() bool { return strings.Contains(ct.contentType, "image/") @@ -57,12 +61,12 @@ func (ct SniffedType) IsPDF() bool { return strings.Contains(ct.contentType, "application/pdf") } -// IsVideo detects if data is an video format +// IsVideo detects if data is a video format func (ct SniffedType) IsVideo() bool { return strings.Contains(ct.contentType, "video/") } -// IsAudio detects if data is an video format +// IsAudio detects if data is a video format func (ct SniffedType) IsAudio() bool { return strings.Contains(ct.contentType, "audio/") } @@ -103,33 +107,34 @@ func detectFileTypeBox(data []byte) (brands []string, found bool) { return brands, true } -// DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty. +// DetectContentType extends http.DetectContentType with more content types. Defaults to text/plain if input is empty. func DetectContentType(data []byte) SniffedType { if len(data) == 0 { - return SniffedType{"text/unknown"} + return SniffedType{"text/plain"} } ct := http.DetectContentType(data) - if len(data) > sniffLen { - data = data[:sniffLen] + if len(data) > SniffContentSize { + data = data[:SniffContentSize] } + vars := globalVars() // SVG is unsupported by http.DetectContentType, https://github.com/golang/go/issues/15888 detectByHTML := strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html") detectByXML := strings.Contains(ct, "text/xml") if detectByHTML || detectByXML { - dataProcessed := svgComment.ReplaceAll(data, nil) + dataProcessed := vars.svgComment.ReplaceAll(data, nil) dataProcessed = bytes.TrimSpace(dataProcessed) - if detectByHTML && svgTagRegex.Match(dataProcessed) || - detectByXML && svgTagInXMLRegex.Match(dataProcessed) { + if detectByHTML && vars.svgTagRegex.Match(dataProcessed) || + detectByXML && vars.svgTagInXMLRegex.Match(dataProcessed) { ct = MimeTypeImageSvg } } if strings.HasPrefix(ct, "audio/") && bytes.HasPrefix(data, []byte("ID3")) { // The MP3 detection is quite inaccurate, any content with "ID3" prefix will result in "audio/mpeg". - // So remove the "ID3" prefix and detect again, if result is text, then it must be text content. + // So remove the "ID3" prefix and detect again, then if the result is "text", it must be text content. // This works especially because audio files contain many unprintable/invalid characters like `0x00` ct2 := http.DetectContentType(data[3:]) if strings.HasPrefix(ct2, "text/") { @@ -155,15 +160,3 @@ func DetectContentType(data []byte) SniffedType { } return SniffedType{ct} } - -// DetectContentTypeFromReader guesses the content type contained in the reader. -func DetectContentTypeFromReader(r io.Reader) (SniffedType, error) { - buf := make([]byte, sniffLen) - n, err := util.ReadAtMost(r, buf) - if err != nil { - return SniffedType{}, fmt.Errorf("DetectContentTypeFromReader io error: %w", err) - } - buf = buf[:n] - - return DetectContentType(buf), nil -} diff --git a/modules/typesniffer/typesniffer_test.go b/modules/typesniffer/typesniffer_test.go index 3e5db3308b..a0c824b912 100644 --- a/modules/typesniffer/typesniffer_test.go +++ b/modules/typesniffer/typesniffer_test.go @@ -4,7 +4,6 @@ package typesniffer import ( - "bytes" "encoding/base64" "encoding/hex" "strings" @@ -17,7 +16,7 @@ func TestDetectContentTypeLongerThanSniffLen(t *testing.T) { // Pre-condition: Shorter than sniffLen detects SVG. assert.Equal(t, "image/svg+xml", DetectContentType([]byte(`<!-- Comment --><svg></svg>`)).contentType) // Longer than sniffLen detects something else. - assert.NotEqual(t, "image/svg+xml", DetectContentType([]byte(`<!-- `+strings.Repeat("x", sniffLen)+` --><svg></svg>`)).contentType) + assert.NotEqual(t, "image/svg+xml", DetectContentType([]byte(`<!-- `+strings.Repeat("x", SniffContentSize)+` --><svg></svg>`)).contentType) } func TestIsTextFile(t *testing.T) { @@ -116,22 +115,13 @@ func TestIsAudio(t *testing.T) { assert.True(t, DetectContentType([]byte("ID3Toy\n====\t* hi 🌞, ..."+"🌛"[0:2])).IsText()) // test ID3 tag with incomplete UTF8 char } -func TestDetectContentTypeFromReader(t *testing.T) { - mp3, _ := base64.StdEncoding.DecodeString("SUQzBAAAAAABAFRYWFgAAAASAAADbWFqb3JfYnJhbmQAbXA0MgBUWFhYAAAAEQAAA21pbm9yX3Zl") - st, err := DetectContentTypeFromReader(bytes.NewReader(mp3)) - assert.NoError(t, err) - assert.True(t, st.IsAudio()) -} - func TestDetectContentTypeOgg(t *testing.T) { oggAudio, _ := hex.DecodeString("4f67675300020000000000000000352f0000000000007dc39163011e01766f72626973000000000244ac0000000000000071020000000000b8014f6767530000") - st, err := DetectContentTypeFromReader(bytes.NewReader(oggAudio)) - assert.NoError(t, err) + st := DetectContentType(oggAudio) assert.True(t, st.IsAudio()) oggVideo, _ := hex.DecodeString("4f676753000200000000000000007d9747ef000000009b59daf3012a807468656f7261030201001e00110001e000010e00020000001e00000001000001000001") - st, err = DetectContentTypeFromReader(bytes.NewReader(oggVideo)) - assert.NoError(t, err) + st = DetectContentType(oggVideo) assert.True(t, st.IsVideo()) } diff --git a/modules/util/error.go b/modules/util/error.go index 8e67d5a82f..6b2721618e 100644 --- a/modules/util/error.go +++ b/modules/util/error.go @@ -17,8 +17,8 @@ var ( ErrNotExist = errors.New("resource does not exist") // also implies HTTP 404 ErrAlreadyExist = errors.New("resource already exists") // also implies HTTP 409 - // ErrUnprocessableContent implies HTTP 422, syntax of the request content was correct, - // but server was unable to process the contained instructions + // ErrUnprocessableContent implies HTTP 422, the syntax of the request content is correct, + // but the server is unable to process the contained instructions ErrUnprocessableContent = errors.New("unprocessable content") ) diff --git a/modules/util/remove.go b/modules/util/remove.go index d1e38faf5f..3db0b5a796 100644 --- a/modules/util/remove.go +++ b/modules/util/remove.go @@ -15,7 +15,7 @@ const windowsSharingViolationError syscall.Errno = 32 // Remove removes the named file or (empty) directory with at most 5 attempts. func Remove(name string) error { var err error - for i := 0; i < 5; i++ { + for range 5 { err = os.Remove(name) if err == nil { break @@ -44,7 +44,7 @@ func Remove(name string) error { // RemoveAll removes the named file or (empty) directory with at most 5 attempts. func RemoveAll(name string) error { var err error - for i := 0; i < 5; i++ { + for range 5 { err = os.RemoveAll(name) if err == nil { break @@ -73,7 +73,7 @@ func RemoveAll(name string) error { // Rename renames (moves) oldpath to newpath with at most 5 attempts. func Rename(oldpath, newpath string) error { var err error - for i := 0; i < 5; i++ { + for i := range 5 { err = os.Rename(oldpath, newpath) if err == nil { break diff --git a/modules/util/rotatingfilewriter/writer_test.go b/modules/util/rotatingfilewriter/writer_test.go index 88392797b3..f6ea1d50ae 100644 --- a/modules/util/rotatingfilewriter/writer_test.go +++ b/modules/util/rotatingfilewriter/writer_test.go @@ -23,7 +23,7 @@ func TestCompressOldFile(t *testing.T) { ng, err := os.OpenFile(nonGzip, os.O_CREATE|os.O_WRONLY, 0o660) assert.NoError(t, err) - for i := 0; i < 999; i++ { + for range 999 { f.WriteString("This is a test file\n") ng.WriteString("This is a test file\n") } diff --git a/modules/util/string.go b/modules/util/string.go index 19cf75b8b3..03c0df96a3 100644 --- a/modules/util/string.go +++ b/modules/util/string.go @@ -103,7 +103,7 @@ func UnsafeStringToBytes(s string) []byte { func SplitTrimSpace(input, sep string) []string { input = strings.TrimSpace(input) var stringList []string - for _, s := range strings.Split(input, sep) { + for s := range strings.SplitSeq(input, sep) { if s = strings.TrimSpace(s); s != "" { stringList = append(stringList, s) } diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index 9f6cf5201a..ba383ba195 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -7,6 +7,7 @@ import ( "net" "net/url" "regexp" + "slices" "strings" "sync" @@ -55,12 +56,7 @@ func IsValidSiteURL(uri string) bool { return false } - for _, scheme := range setting.Service.ValidSiteURLSchemes { - if scheme == u.Scheme { - return true - } - } - return false + return slices.Contains(setting.Service.ValidSiteURLSchemes, u.Scheme) } // IsEmailDomainListed checks whether the domain of an email address diff --git a/modules/validation/helpers_test.go b/modules/validation/helpers_test.go index 52f383f698..6a982965f6 100644 --- a/modules/validation/helpers_test.go +++ b/modules/validation/helpers_test.go @@ -7,6 +7,7 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) @@ -47,7 +48,7 @@ func Test_IsValidURL(t *testing.T) { } func Test_IsValidExternalURL(t *testing.T) { - setting.AppURL = "https://try.gitea.io/" + defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() cases := []struct { description string @@ -89,7 +90,7 @@ func Test_IsValidExternalURL(t *testing.T) { } func Test_IsValidExternalTrackerURLFormat(t *testing.T) { - setting.AppURL = "https://try.gitea.io/" + defer test.MockVariableValue(&setting.AppURL, "https://try.gitea.io/")() cases := []struct { description string diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 03e188f509..ee4eca976e 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -50,7 +50,7 @@ func AssignForm(form any, data map[string]any) { } func getRuleBody(field reflect.StructField, prefix string) string { - for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { + for rule := range strings.SplitSeq(field.Tag.Get("binding"), ";") { if strings.HasPrefix(rule, prefix) { return rule[len(prefix) : len(rule)-1] } diff --git a/modules/web/router.go b/modules/web/router.go index da06b955b1..5812ff69d4 100644 --- a/modules/web/router.go +++ b/modules/web/router.go @@ -125,8 +125,8 @@ func (r *Router) Methods(methods, pattern string, h ...any) { middlewares, handlerFunc := wrapMiddlewareAndHandler(r.curMiddlewares, h) fullPattern := r.getPattern(pattern) if strings.Contains(methods, ",") { - methods := strings.Split(methods, ",") - for _, method := range methods { + methods := strings.SplitSeq(methods, ",") + for method := range methods { r.chiRouter.With(middlewares...).Method(strings.TrimSpace(method), fullPattern, handlerFunc) } } else { diff --git a/modules/web/router_path.go b/modules/web/router_path.go index baf1b522af..ce041eedab 100644 --- a/modules/web/router_path.go +++ b/modules/web/router_path.go @@ -6,6 +6,7 @@ package web import ( "net/http" "regexp" + "slices" "strings" "code.gitea.io/gitea/modules/container" @@ -36,11 +37,21 @@ func (g *RouterPathGroup) ServeHTTP(resp http.ResponseWriter, req *http.Request) g.r.chiRouter.NotFoundHandler().ServeHTTP(resp, req) } +type RouterPathGroupPattern struct { + re *regexp.Regexp + params []routerPathParam + middlewares []any +} + // MatchPath matches the request method, and uses regexp to match the path. -// The pattern uses "<...>" to define path parameters, for example: "/<name>" (different from chi router) -// It is only designed to resolve some special cases which chi router can't handle. +// The pattern uses "<...>" to define path parameters, for example, "/<name>" (different from chi router) +// It is only designed to resolve some special cases that chi router can't handle. // For most cases, it shouldn't be used because it needs to iterate all rules to find the matched one (inefficient). func (g *RouterPathGroup) MatchPath(methods, pattern string, h ...any) { + g.MatchPattern(methods, g.PatternRegexp(pattern), h...) +} + +func (g *RouterPathGroup) MatchPattern(methods string, pattern *RouterPathGroupPattern, h ...any) { g.matchers = append(g.matchers, newRouterPathMatcher(methods, pattern, h...)) } @@ -96,29 +107,35 @@ func isValidMethod(name string) bool { return false } -func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { - middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, h) +func newRouterPathMatcher(methods string, patternRegexp *RouterPathGroupPattern, h ...any) *routerPathMatcher { + middlewares, handlerFunc := wrapMiddlewareAndHandler(patternRegexp.middlewares, h) p := &routerPathMatcher{methods: make(container.Set[string]), middlewares: middlewares, handlerFunc: handlerFunc} - for _, method := range strings.Split(methods, ",") { + for method := range strings.SplitSeq(methods, ",") { method = strings.TrimSpace(method) if !isValidMethod(method) { panic("invalid HTTP method: " + method) } p.methods.Add(method) } + p.re, p.params = patternRegexp.re, patternRegexp.params + return p +} + +func patternRegexp(pattern string, h ...any) *RouterPathGroupPattern { + p := &RouterPathGroupPattern{middlewares: slices.Clone(h)} re := []byte{'^'} lastEnd := 0 for lastEnd < len(pattern) { start := strings.IndexByte(pattern[lastEnd:], '<') if start == -1 { - re = append(re, pattern[lastEnd:]...) + re = append(re, regexp.QuoteMeta(pattern[lastEnd:])...) break } end := strings.IndexByte(pattern[lastEnd+start:], '>') if end == -1 { panic("invalid pattern: " + pattern) } - re = append(re, pattern[lastEnd:lastEnd+start]...) + re = append(re, regexp.QuoteMeta(pattern[lastEnd:lastEnd+start])...) partName, partExp, _ := strings.Cut(pattern[lastEnd+start+1:lastEnd+start+end], ":") lastEnd += start + end + 1 @@ -140,7 +157,10 @@ func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher p.params = append(p.params, param) } re = append(re, '$') - reStr := string(re) - p.re = regexp.MustCompile(reStr) + p.re = regexp.MustCompile(string(re)) return p } + +func (g *RouterPathGroup) PatternRegexp(pattern string, h ...any) *RouterPathGroupPattern { + return patternRegexp(pattern, h...) +} diff --git a/modules/web/router_test.go b/modules/web/router_test.go index 21619012ea..1cee2b879b 100644 --- a/modules/web/router_test.go +++ b/modules/web/router_test.go @@ -34,7 +34,7 @@ func TestPathProcessor(t *testing.T) { testProcess := func(pattern, uri string, expectedPathParams map[string]string) { chiCtx := chi.NewRouteContext() chiCtx.RouteMethod = "GET" - p := newRouterPathMatcher("GET", pattern, http.NotFound) + p := newRouterPathMatcher("GET", patternRegexp(pattern), http.NotFound) assert.True(t, p.matchPath(chiCtx, uri), "use pattern %s to process uri %s", pattern, uri) assert.Equal(t, expectedPathParams, chiURLParamsToMap(chiCtx), "use pattern %s to process uri %s", pattern, uri) } @@ -56,18 +56,20 @@ func TestRouter(t *testing.T) { recorder.Body = buff type resultStruct struct { - method string - pathParams map[string]string - handlerMark string + method string + pathParams map[string]string + handlerMarks []string } - var res resultStruct + var res resultStruct h := func(optMark ...string) func(resp http.ResponseWriter, req *http.Request) { mark := util.OptionalArg(optMark, "") return func(resp http.ResponseWriter, req *http.Request) { res.method = req.Method res.pathParams = chiURLParamsToMap(chi.RouteContext(req.Context())) - res.handlerMark = mark + if mark != "" { + res.handlerMarks = append(res.handlerMarks, mark) + } } } @@ -77,6 +79,8 @@ func TestRouter(t *testing.T) { if stop := req.FormValue("stop"); stop != "" && (mark == "" || mark == stop) { h(stop)(resp, req) resp.WriteHeader(http.StatusOK) + } else if mark != "" { + res.handlerMarks = append(res.handlerMarks, mark) } } } @@ -108,7 +112,7 @@ func TestRouter(t *testing.T) { m.Delete("", h()) }) m.PathGroup("/*", func(g *RouterPathGroup) { - g.MatchPath("GET", `/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2"), h("match-path")) + g.MatchPattern("GET", g.PatternRegexp(`/<dir:*>/<file:[a-z]{1,2}>`, stopMark("s2")), stopMark("s3"), h("match-path")) }, stopMark("s1")) }) }) @@ -126,31 +130,31 @@ func TestRouter(t *testing.T) { } t.Run("RootRouter", func(t *testing.T) { - testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMark: "not-found:/"}) + testRoute(t, "GET /the-user/the-repo/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/"}}) testRoute(t, "GET /the-user/the-repo/pulls", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, - handlerMark: "list-issues-b", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "pulls"}, + handlerMarks: []string{"list-issues-b"}, }) testRoute(t, "GET /the-user/the-repo/issues/123", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "view-issue", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, + handlerMarks: []string{"view-issue"}, }) testRoute(t, "GET /the-user/the-repo/issues/123?stop=hijack", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, - handlerMark: "hijack", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "type": "issues", "index": "123"}, + handlerMarks: []string{"hijack"}, }) testRoute(t, "POST /the-user/the-repo/issues/123/update", resultStruct{ - method: "POST", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, - handlerMark: "update-issue", + method: "POST", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "index": "123"}, + handlerMarks: []string{"update-issue"}, }) }) t.Run("Sub Router", func(t *testing.T) { - testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMark: "not-found:/api/v1"}) + testRoute(t, "GET /api/v1/other", resultStruct{method: "GET", handlerMarks: []string{"not-found:/api/v1"}}) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches", resultStruct{ method: "GET", pathParams: map[string]string{"username": "the-user", "reponame": "the-repo"}, @@ -179,31 +183,37 @@ func TestRouter(t *testing.T) { t.Run("MatchPath", func(t *testing.T) { testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "match-path", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3", "match-path"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1%2fd2/fn", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, - handlerMark: "match-path", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1%2fd2/fn", "dir": "d1%2fd2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3", "match-path"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/000", resultStruct{ - method: "GET", - pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, - handlerMark: "not-found:/api/v1", + method: "GET", + pathParams: map[string]string{"reponame": "the-repo", "username": "the-user", "*": "d1/d2/000"}, + handlerMarks: []string{"s1", "not-found:/api/v1"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s1", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, - handlerMark: "s1", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn"}, + handlerMarks: []string{"s1"}, }) testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s2", resultStruct{ - method: "GET", - pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, - handlerMark: "s2", + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2"}, + }) + + testRoute(t, "GET /api/v1/repos/the-user/the-repo/branches/d1/d2/fn?stop=s3", resultStruct{ + method: "GET", + pathParams: map[string]string{"username": "the-user", "reponame": "the-repo", "*": "d1/d2/fn", "dir": "d1/d2", "file": "fn"}, + handlerMarks: []string{"s1", "s2", "s3"}, }) }) } diff --git a/modules/web/routing/logger.go b/modules/web/routing/logger.go index e3843b1402..3bca9b3420 100644 --- a/modules/web/routing/logger.go +++ b/modules/web/routing/logger.go @@ -103,7 +103,10 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) { status = v.WrittenStatus() } logf := logInfo - if strings.HasPrefix(req.RequestURI, "/assets/") { + // lower the log level for some specific requests, in most cases these logs are not useful + if strings.HasPrefix(req.RequestURI, "/assets/") /* static assets */ || + req.RequestURI == "/user/events" /* Server-Sent Events (SSE) handler */ || + req.RequestURI == "/api/actions/runner.v1.RunnerService/FetchTask" /* Actions Runner polling */ { logf = logTrace } message := completedMessage diff --git a/modules/webhook/type.go b/modules/webhook/type.go index 72ffde26a1..89c6a4bfe5 100644 --- a/modules/webhook/type.go +++ b/modules/webhook/type.go @@ -38,6 +38,7 @@ const ( HookEventPullRequestReview HookEventType = "pull_request_review" // Actions event only HookEventSchedule HookEventType = "schedule" + HookEventWorkflowRun HookEventType = "workflow_run" HookEventWorkflowJob HookEventType = "workflow_job" ) @@ -67,6 +68,7 @@ func AllEvents() []HookEventType { HookEventRelease, HookEventPackage, HookEventStatus, + HookEventWorkflowRun, HookEventWorkflowJob, } } diff --git a/modules/zstd/zstd_test.go b/modules/zstd/zstd_test.go index c3ca8e78f7..7fd30484ca 100644 --- a/modules/zstd/zstd_test.go +++ b/modules/zstd/zstd_test.go @@ -16,7 +16,7 @@ import ( ) func TestWriterReader(t *testing.T) { - testData := prepareTestData(t, 20_000_000) + testData := prepareTestData(t, 1_000_000) result := bytes.NewBuffer(nil) @@ -64,7 +64,7 @@ func TestWriterReader(t *testing.T) { } func TestSeekableWriterReader(t *testing.T) { - testData := prepareTestData(t, 20_000_000) + testData := prepareTestData(t, 2_000_000) result := bytes.NewBuffer(nil) @@ -109,7 +109,7 @@ func TestSeekableWriterReader(t *testing.T) { reader, err := NewSeekableReader(assertReader) require.NoError(t, err) - _, err = reader.Seek(10_000_000, io.SeekStart) + _, err = reader.Seek(1_000_000, io.SeekStart) require.NoError(t, err) data := make([]byte, 1000) @@ -117,7 +117,7 @@ func TestSeekableWriterReader(t *testing.T) { require.NoError(t, err) require.NoError(t, reader.Close()) - assert.Equal(t, testData[10_000_000:10_000_000+1000], data) + assert.Equal(t, testData[1_000_000:1_000_000+1000], data) // Should seek 3 times, // the first two times are for getting the index, diff --git a/options/fileicon/material-icon-rules.json b/options/fileicon/material-icon-rules.json index 826eb40e87..a20244d6ad 100644 --- a/options/fileicon/material-icon-rules.json +++ b/options/fileicon/material-icon-rules.json @@ -6629,6 +6629,7 @@ "fish": "console", "exp": "console", "nu": "console", + "xsh": "console", "ps1": "powershell", "psm1": "powershell", "psd1": "powershell", @@ -6700,7 +6701,6 @@ "lbx": "lbx", "tex": "tex", "sty": "sty", - "cls": "cls", "ltx": "ltx", "dtx": "dtx", "pptx": "powerpoint", @@ -7380,6 +7380,8 @@ "gnu": "gnuplot", "smk": "snakemake", "snakemake": "snakemake", + "cpn": "coloredpetrinets", + "pnml": "coloredpetrinets", "yaml-tmlanguage": "yaml", "tmlanguage": "xml", "cljx": "clojure", @@ -7446,6 +7448,7 @@ "hintrc": "jsonc", "babelrc": "jsonc", "jmd": "juliamarkdown", + "cls": "tex", "ctx": "latex", "mak": "makefile", "mkd": "markdown", diff --git a/options/fileicon/material-icon-svgs.json b/options/fileicon/material-icon-svgs.json index cf06555b03..5fb182b731 100644 --- a/options/fileicon/material-icon-svgs.json +++ b/options/fileicon/material-icon-svgs.json @@ -97,7 +97,6 @@ "cline": "<svg viewBox='0 0 16 16'><path fill='#42a5f5' d='M8 1a2 2 0 0 0-2 2H5C3.338 3 2 4.338 2 6v1L1 9l1 2v1c0 1.662 1.338 3 3 3h6c1.662 0 3-1.338 3-3v-1l1-2-1-2V6c0-1.662-1.338-3-3-3h-1a2 2 0 0 0-2-2M6 7c.554 0 1 .446 1 1v2c0 .554-.446 1-1 1s-1-.446-1-1V8c0-.554.446-1 1-1m4 0c.554 0 1 .446 1 1v2c0 .554-.446 1-1 1s-1-.446-1-1V8c0-.554.446-1 1-1'/></svg>", "clojure": "<svg viewBox='0 0 256 256'><path fill='#64dd17' d='M123.456 129.975a507 507 0 0 0-3.54 7.846c-4.406 9.981-9.284 22.127-11.066 29.908-.64 2.77-1.037 6.205-1.03 10.013 0 1.506.081 3.09.21 4.702a58.1 58.1 0 0 0 19.98 3.559 58.2 58.2 0 0 0 18.29-2.98c-1.352-1.237-2.642-2.554-3.816-4.038-7.796-9.942-12.146-24.512-19.028-49.01m-28.784-49.39C79.782 91.08 70.039 108.387 70.002 128c.037 19.32 9.487 36.403 24.002 46.94 3.56-14.83 12.485-28.41 25.868-55.63a219 219 0 0 0-2.714-7.083c-3.708-9.3-9.059-20.102-13.834-24.993-2.435-2.555-5.389-4.763-8.652-6.648'/><path fill='#7cb342' d='M178.532 194.535c-7.683-.963-14.023-2.124-19.57-4.081a69.4 69.4 0 0 1-30.958 7.249c-38.491 0-69.693-31.198-69.698-69.7 0-20.891 9.203-39.62 23.764-52.392-3.895-.94-7.956-1.49-12.104-1.482-20.45.193-42.037 11.51-51.025 42.075-.84 4.45-.64 7.813-.64 11.8 0 60.591 49.12 109.715 109.705 109.715 37.104 0 69.882-18.437 89.732-46.633-10.736 2.675-21.06 3.955-29.902 3.982-3.314 0-6.425-.177-9.305-.53'/><path fill='#29b6f6' d='M157.922 173.271c.678.336 2.213.884 4.35 1.49 14.375-10.553 23.717-27.552 23.754-46.764h-.005c-.055-32.03-25.974-57.945-58.011-58.009a58.2 58.2 0 0 0-18.213 2.961c11.779 13.426 17.443 32.613 22.922 53.6l.01.025c.01.017 1.752 5.828 4.743 13.538 2.97 7.7 7.203 17.231 11.818 24.178 3.03 4.655 6.363 8 8.632 8.981'/><path fill='#1e88e5' d='M128.009 18.29c-36.746 0-69.25 18.089-89.16 45.826 10.361-6.49 20.941-8.83 30.174-8.747 12.753.037 22.779 3.991 27.589 6.696a51 51 0 0 1 3.345 2.131 69.4 69.4 0 0 1 28.049-5.894c38.496.004 69.703 31.202 69.709 69.698h-.006c0 19.409-7.938 36.957-20.736 49.594 3.142.352 6.492.571 9.912.554 12.15.006 25.284-2.675 35.13-10.956 6.42-5.408 11.798-13.327 14.78-25.199.584-4.586.92-9.247.92-13.991 0-60.588-49.116-109.715-109.705-109.715'/></svg>", "cloudfoundry": "<svg viewBox='0 0 24 24'><path fill='#0288d1' d='M14.13 10.1c.6-.5.98-1.19.97-1.96-.01-1.47-1.4-2.66-3.1-2.66S8.91 6.66 8.9 8.13c-.01.77.37 1.47.97 1.96.75.61 1.36 1.84 1.4 3.18.04 1.49-.06 3.22-.12 4.1-.8.11-1.36.38-1.36.69 0 .41.99.89 2.22.89s2.22-.48 2.22-.89c0-.31-.56-.58-1.36-.69-.06-.88-.16-2.6-.12-4.1.04-1.34.65-2.57 1.4-3.18z'/><path fill='#78909c' d='M17.89 18.77a2 2 0 0 1 .37-.11 2.3 2.3 0 0 1 .43-.05l-.02-.12c0-.07-.04-.16-.07-.26l-.05-.13c-.02-.04-.04-.08-.06-.11l-.06-.11a4 4 0 0 1-.44-.1 3 3 0 0 1-.4-.14c-.22-.09-.43-.2-.54-.32a.41.41 0 0 1-.16-.34.3.3 0 0 1 .07-.15.5.5 0 0 1 .16-.12l-.13-.07c-.04-.02-.08-.05-.14-.07-.05-.03-.11-.05-.17-.08-.11-.05-.22-.11-.32-.14l-.15-.06a3 3 0 0 1-.34.02c-.12 0-.25 0-.37-.01-.24-.02-.49-.05-.7-.12a2 2 0 0 1-.28-.1 1.3 1.3 0 0 1-.21-.11c-.11-.08-.19-.16-.19-.25l-.17-.03c-.1-.02-.33-.05-.47-.07 0 .02-.02.05-.02.09l-.04.17c-.07.35.13.7.47.8.83.24 1.46.64 1.64 1.13.37.99-1.18 1.96-3.52 1.96-2.35 0-3.89-.97-3.53-1.96.18-.48.78-.87 1.59-1.11a.72.72 0 0 0 .49-.84l-.05-.24-.31.04c-.14.02-.27.04-.37.06l-.17.03c.01.08-.05.17-.16.25a1.4 1.4 0 0 1-.2.12 2 2 0 0 1-.27.1 3 3 0 0 1-.33.08 3 3 0 0 1-.36.05c-.25.02-.5.02-.73 0l-.04.02c-.02.01-.07.03-.11.05l-.31.15c-.11.05-.21.12-.29.16l-.1.05-.03.02c.16.07.24.17.26.28.04.11-.01.23-.12.35a1.5 1.5 0 0 1-.22.18 2 2 0 0 1-.32.17 3.4 3.4 0 0 1-.83.26.5.5 0 0 0-.05.11l-.09.25-.04.26a.4.4 0 0 0-.01.12c.32 0 .61.06.83.14.25.08.4.21.52.36.09.16.13.33.05.53-.06.19-.22.41-.47.6l.04.03.12.08c.1.07.23.17.38.26l.44.24c.06.03.12.06.16.07l.06.03c.36-.15.75-.23 1.14-.28a3.3 3.3 0 0 1 .57-.01c.19.01.38.02.55.07.18.05.35.09.5.16a3 3 0 0 1 .42.24c.26.17.45.42.54.68l.33.01c.2.01.46.02.73.01l.72-.03.32-.03a1 1 0 0 1 .17-.38 1 1 0 0 1 .14-.16 1.6 1.6 0 0 1 .55-.39 2.3 2.3 0 0 1 .48-.17c.18-.04.35-.07.54-.08q.285-.015.57 0a4 4 0 0 1 1.15.24l.2-.11c.11-.07.28-.15.41-.25s.27-.19.35-.26l.13-.13c-.27-.19-.43-.39-.52-.58a.64.64 0 0 1-.06-.27.4.4 0 0 1 .06-.25c.07-.16.25-.27.45-.37M9.95 13.9s.35-1.32-1.17-3.03l-.05-.05a4.2 4.2 0 0 1-.98-2.7c0-2.32 1.89-4.2 4.2-4.2h.1a4.2 4.2 0 0 1 4.2 4.2 4.2 4.2 0 0 1-.99 2.7c-.01.01-.02.03-.05.05-1.34 1.35-1.17 3.03-1.17 3.03a6.14 6.14 0 0 0 4.13-5.75c.02-3.24-2.6-6-5.83-6.15h-.7c-3.23.16-5.85 2.92-5.83 6.15.01 2.59 1.67 4.9 4.13 5.75z'/></svg>", - "cls.clone": "<svg viewBox='0 0 1024 1024'><path fill='#FF4081' d='M80 192 64 320h32c16-80 16-96 63.242-96H176c8.837 0 16 7.163 16 16v352c0 8.837 0 16-32 16h-32v32h192v-32h-32c-32 0-32-7.163-32-16V240c0-8.837 7.163-16 16-16h16c48 0 48 16 64 96h32l-16-128zm560 0v32c16 0 45.713 0 52.57 16L776 434.666 708.57 592c-6.857 16-52.57 16-68.57 16v32h128v-32s-34.285 0-27.428-16L792 472l51.428 120c3.103 7.24-1.52 16-11.428 16v32h128v-32c-16 0-45.713 0-52.57-16L824 397.334 891.43 240c6.857-16 52.57-16 68.57-16v-32H832v32s34.285 0 27.428 16L808 360l-51.428-120c-3.103-7.24 1.52-16 11.428-16v-32zM320 384v32h32c32 0 32 7.163 32 16v352c0 8.837 0 16-32 16h-32v32h304l16-128h-32c-16 80-16 96-64 96h-64c-32 0-32-7.163-32-16V624h80c8.837 0 16 0 16 32v16h32V544h-32v16c0 32-7.163 32-16 32h-80V432c0-8.837 0-16 32-16h64c48 0 48 16 64 96h32l-16-128z'/></svg>", "cmake": "<svg viewBox='0 0 24 24'><path fill='#1e88e5' d='M11.94 2.984 2.928 21.017l9.875-8.47z'/><path fill='#e53935' d='m11.958 2.982.002.29 1.312 14.499-.002.006.023.26 7.363 2.978h.415l-.158-.31-.114-.228h-.001l-8.84-17.494z'/><path fill='#7cb342' d='m8.558 16.13-5.627 4.884h17.743v-.016z'/></svg>", "coala": "<svg viewBox='0 0 24 24'><path xmlns='http://www.w3.org/2000/svg' fill='#90a4ae' d='M22 8.95c0-2.59-1.74-3.63-3.89-3.63-.9 0-1.83.2-2.6.57-1-.69-2.17-1.09-3.51-1.09s-2.51.41-3.51 1.09c-.78-.38-1.7-.57-2.6-.57C3.74 5.32 2 6.35 2 8.95c0 2.14 1.18 3.56 2.8 4-.02.3-.04.6-.04.89 0 3.18 2.51 4.26 4.77 4.63.61.45 1.49.73 2.47.73s1.86-.28 2.47-.73c2.26-.36 4.77-1.44 4.77-4.63 0-.3-.01-.6-.04-.89 1.62-.44 2.8-1.87 2.8-4'/><path xmlns='http://www.w3.org/2000/svg' fill='#f8bbd0' d='M7.31 6.9c-.18-.02-.35-.03-.53-.03-1.72 0-3.11.83-3.11 2.9 0 1.2.47 2.12 1.19 2.68.26-2.11 1.11-4.12 2.45-5.55m9.91-.03c-.18 0-.35.01-.53.03 1.34 1.43 2.19 3.44 2.45 5.55.72-.56 1.19-1.48 1.19-2.68 0-2.07-1.39-2.9-3.11-2.9'/><path xmlns='http://www.w3.org/2000/svg' fill='#263238' d='M14.07 15.21c0 1.86-.96 2.33-2.07 2.33s-2.07-.47-2.07-2.33.96-3.36 2.07-3.36 2.07 1.51 2.07 3.36M9.5 11.75a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0m7.5 0a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0'/></svg>", "cobol": "<svg viewBox='0 0 32 32'><path fill='#0288d1' d='M12 0h8v4h-8z'/><path fill='#0288d1' d='M16 5A11 11 0 1 1 5 16 11.01 11.01 0 0 1 16 5m0-3a14 14 0 1 0 14 14A14 14 0 0 0 16 2'/><path fill='#0288d1' d='M32 12v8h-4v-8zm-1.858 12.485-5.657 5.657-2.313-2.313 5.657-5.657zM7.514 30.143l-5.657-5.657 2.814-2.814 5.657 5.657zM12 28h8v4h-8zm15.329-17.672L21.672 4.67l2.814-2.814 5.657 5.657zM3 12v8H0v-8zm7.328-7.329L4.67 10.328 1.857 7.514l5.657-5.657zM20 10h-4a6 6 0 0 0 0 12h4v-4h-4a2 2 0 0 1 0-4h4z'/></svg>", @@ -109,6 +108,7 @@ "coderabbit-ai": "<svg viewBox='0 0 16 16'><path fill='#F4511E' d='M15 8.913A6.85 6.85 0 0 1 12.74 14h-1.68c.035-.162-.052-.274-.165-.35-.236-.162-.385-.431-.35-.71.093-.735.545-1.674 2.194-2.536 1.115-.588 1.32-1.8 1.398-1.978.113-.294.082-.543-.154-.781-.427-.431-.889-.822-1.449-1.075-.786-.365-1.588-.345-2.384-.051-.139.05-.108-.056-.118-.101a5.3 5.3 0 0 0-.545-1.481c-.519-.923-1.243-1.603-2.322-1.831-.16-.036-.324-.046-.488-.066-.077-.01-.118.01-.098.101.144.68.345 1.334.781 1.892.2.259.473.431.74.609.283.192.58.37.848.588.252.213.457.461.555.817C9.467 7.01 9.45 7 9.44 6.986c-.822-1.289-2.43-1.857-4.048-1.39-.15.04-.129.091-.057.198.468.71 1.11 1.217 1.87 1.608.565.289 1.151.527 1.788.608.288.036.15.228.195.457.062.355.211.466.16.436-.977-.386-1.788-.538-2.461-.538-2.78 0-3.232 2.617-3.211 2.648-.041-.016-.71-.259-.858.4-.154.67.755 1.106.755 1.106.118-.791.837-.964.925-.984-.072.04-.54.31-.684 1.045-.123.65.396 1.222.591 1.42h-1.15A6.87 6.87 0 0 1 1 8.913C1 5.093 4.129 2 7.997 2 11.867 2 15 5.094 15 8.913M9.914 14h-.74a.27.27 0 0 0 .072-.142c.062-.355-.247-.426-.247-.426H7.294s.678-.03 1.3-.284c.616-.259 1.114-.71 1.202-.832a1.93 1.93 0 0 0-.031 1.517.32.32 0 0 0 .149.167'/></svg>", "coffee": "<svg viewBox='0 0 32 32'><path fill='#42a5f5' d='M4 26h24v2H4zM28 4H7a1 1 0 0 0-1 1v13a4 4 0 0 0 4 4h10a4 4 0 0 0 4-4v-4h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2m0 8h-4V6h4Z'/></svg>", "coldfusion": "<svg viewBox='0 0 32 32'><path fill='#0d3858' stroke='#4dd0e1' stroke-width='2' d='M3.009 3.009h25.983v25.983H3.009z'/><path fill='#4dd0e1' d='M24 9.5v-1a.5.5 0 0 0-.5-.5H22a2 2 0 0 0-2 2v2h-1.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5H20v7.5a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5V14h1.5a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5H22v-2h1.5a.5.5 0 0 0 .5-.5M12 20a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h3.5a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5H12a4 4 0 0 0-4 4v4a4 4 0 0 0 4 4h3.5a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5Z'/></svg>", + "coloredpetrinets": "<svg viewBox='0 0 16 16'><rect width='3' height='1' x='9' y='7.5' fill='#b0bec5' rx='0' ry='.032'/><rect width='3' height='1' x='4' y='7.5' fill='#b0bec5' rx='0' ry='.032'/><circle cx='3' cy='8' r='2' fill='#4caf50'/><circle cx='13' cy='8' r='2' fill='#ff9800'/><rect width='4' height='4' x='6' y='6' fill='#00bcd4' rx='.59'/></svg>", "command": "<svg viewBox='0 0 32 32'><path fill='#90a4ae' d='M24 18h-3v-4h3a6 6 0 1 0-6-6v3h-4V8a6 6 0 1 0-6 6h3v4H8a6 6 0 1 0 6 6v-3h4v3a6 6 0 1 0 6-6M21 8a3 3 0 1 1 3 3h-3ZM11 24a3 3 0 1 1-3-3h3Zm0-13H8a3 3 0 1 1 3-3Zm7 7h-4v-4h4Zm6 9a3.003 3.003 0 0 1-3-3v-3h3a3 3 0 0 1 0 6'/></svg>", "commitizen": "<svg viewBox='0 0 32 32'><path fill='#64b5f6' d='M29.422 17.4 17.4 29.422a1.986 1.986 0 0 1-2.8 0L2.578 17.4a1.986 1.986 0 0 1 0-2.8L14.6 2.578a1.986 1.986 0 0 1 2.8 0l8.01 8.012L23 13a2 2 0 0 0-.74-.14A2.13 2.13 0 0 0 20.37 14H12a2.08 2.08 0 0 0-1.86-1.14 2.14 2.14 0 0 0 0 4.28A2.08 2.08 0 0 0 12 16h8l-3.82 3.83h-.01a1.9 1.9 0 0 0-.63-.1 2.135 2.135 0 1 0 2.14 2.13 1.8 1.8 0 0 0-.1-.61l4.17-4.17a2 2 0 0 0 .51.06A2.14 2.14 0 0 0 24.4 15a2 2 0 0 0-.06-.51l2.49-2.48 2.592 2.59a1.986 1.986 0 0 1 0 2.8'/></svg>", "commitlint": "<svg viewBox='0 0 24 24'><path fill='#009688' d='M2.47 2.922V8.37h1.813V2.922zm12.708 1.816a7.27 7.27 0 0 0-6.946 5.127L6.1 12l2.133 2.133c.916 2.969 3.677 5.13 6.945 5.13 4.013 0 7.262-3.25 7.262-7.263s-3.25-7.262-7.262-7.262m2.942 3.703 1.342 1.63-5.49 5.488-3.179-3.467 1.34-1.34 1.838 1.838zM3.377 10.184c-.998 0-1.816.817-1.816 1.816a1.817 1.817 0 1 0 1.816-1.816M2.47 15.63v5.448h1.814V15.63z'/></svg>", diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 2a3bd3e743..aeefaf6064 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1330,7 +1330,6 @@ editor.update=Aktualizovat %s editor.delete=Odstranit %s editor.patch=PoužÃt záplatu editor.patching=ZáplatovánÃ: -editor.fail_to_apply_patch=Nelze použÃt záplatu „%s“ editor.new_patch=Nová záplata editor.commit_message_desc=PÅ™idat volitelný rozÅ¡ÃÅ™ený popis… editor.signoff_desc=PÅ™idat Signed-off-by podpis pÅ™ispÄ›vatele na konec zprávy o commitu. @@ -1348,8 +1347,6 @@ editor.branch_already_exists=VÄ›tev „%s“ již existuje v tomto repozitáři. editor.directory_is_a_file=Jméno adresáře „%s“ je již použito jako jméno souboru v tomto repozitáři. editor.file_is_a_symlink=`„%s“ je symbolický odkaz. Symbolické odkazy nemohou být upravovány ve webovém editoru` editor.filename_is_a_directory=Jméno souboru „%s“ je již použito jako jméno adresáře v tomto repozitáři. -editor.file_editing_no_longer_exists=Upravovaný soubor „%s“ již nenà souÄástà tohoto repozitáře. -editor.file_deleting_no_longer_exists=Odstraňovaný soubor „%s“ již nenà souÄástà tohoto repozitáře. editor.file_changed_while_editing=Obsah souboru byl zmÄ›nÄ›n od doby, kdy jste zaÄaly s úpravou. <a target="_blank" rel="noopener noreferrer" href="%s">KliknÄ›te zde</a>, abyste je zobrazili, nebo <strong>potvrÄte zmÄ›ny jeÅ¡tÄ› jednou</strong> pro jejich pÅ™epsánÃ. editor.file_already_exists=Soubor „%s“ již existuje v tomto repozitáři. editor.commit_id_not_matching=ID commitu se neshoduje s ID, když jsi zaÄal/a s úpravami. Odevzdat do záplatové vÄ›tve a poté slouÄit. @@ -1357,8 +1354,6 @@ editor.push_out_of_date=Nahránà se zdá být zastaralé. editor.commit_empty_file_header=Odevzdat prázdný soubor editor.commit_empty_file_text=Soubor, který se chystáte odevzdat, je prázdný. PokraÄovat? editor.no_changes_to_show=Žádné zmÄ›ny k zobrazenÃ. -editor.fail_to_update_file=NepodaÅ™ilo se aktualizovat/vytvoÅ™it soubor „%s“. -editor.fail_to_update_file_summary=Chybové hlášenÃ: editor.push_rejected_no_message=ZmÄ›na byla serverem zamÃtnuta bez zprávy. ProsÃm, zkontrolujte háÄky Gitu. editor.push_rejected=ZmÄ›na byla serverem zamÃtnuta. ProsÃm, zkontrolujte háÄky Gitu. editor.push_rejected_summary=Úplná zpráva o odmÃtnutÃ: @@ -1373,6 +1368,7 @@ editor.require_signed_commit=VÄ›tev vyžaduje podepsaný commit editor.cherry_pick=Cherry-pick %s na: editor.revert=Vrátit %s na: + commits.desc=Procházet historii zmÄ›n zdrojového kódu. commits.commits=Commity commits.no_commits=Žádné spoleÄné commity. „%s“ a „%s“ majà zcela odliÅ¡nou historii. @@ -1921,7 +1917,6 @@ pulls.cmd_instruction_checkout_title=Checkout pulls.cmd_instruction_checkout_desc=Z vaÅ¡eho repositáře projektu se podÃvejte na novou vÄ›tev a vyzkouÅ¡ejte zmÄ›ny. pulls.cmd_instruction_merge_title=SlouÄit pulls.cmd_instruction_merge_desc=SluÄte zmÄ›ny a aktualizujte je na Gitea. -pulls.cmd_instruction_merge_warning=VarovánÃ: Tato operace nemůže slouÄit požadavek na nataženÃ, protože „autodetekce manuálnÃch slouÄenÓ nebyla povolena pulls.clear_merge_message=Vymazat zprávu o slouÄenà pulls.clear_merge_message_hint=Vymazánà zprávy o slouÄenà odstranà pouze obsah zprávy a ponechá generované pÅ™Ãdavky gitu jako "Co-AuthoreBy …". @@ -2327,7 +2322,6 @@ settings.event_repository=Repozitář settings.event_repository_desc=Repozitář vytvoÅ™en nebo smazán. settings.event_header_issue=Události úkolů settings.event_issues=Úkoly -settings.event_issues_desc=Úkol otevÅ™en, uzavÅ™en, znovu otevÅ™en nebo upraven. settings.event_issue_assign=Úkol pÅ™iÅ™azen settings.event_issue_assign_desc=Úkol pÅ™iÅ™azen nebo nepÅ™iÅ™azen. settings.event_issue_label=Úkol oÅ¡tÃtkován @@ -2338,7 +2332,6 @@ settings.event_issue_comment=Komentář k úkolu settings.event_issue_comment_desc=Komentář úkolu pÅ™idán, upraven nebo smazán. settings.event_header_pull_request=Události pull requestu settings.event_pull_request=Pull request -settings.event_pull_request_desc=Pull request otevÅ™en, uzavÅ™en, znovu otevÅ™en nebo upraven. settings.event_pull_request_assign=Pull request pÅ™iÅ™azen settings.event_pull_request_assign_desc=Pull request pÅ™iÅ™azen nebo nepÅ™iÅ™azen. settings.event_pull_request_label=Pull request oÅ¡tÃtkován @@ -2780,15 +2773,13 @@ settings.visibility.private_shortname=Soukromý settings.update_settings=Upravit nastavenà settings.update_setting_success=Nastavenà organizace bylo upraveno. -settings.change_orgname_prompt=Poznámka: ZmÄ›na názvu organizace také zmÄ›nà adresu URL vašà organizace a uvolnà staré jméno této organizace. -settings.change_orgname_redirect_prompt=Staré jméno bude pÅ™esmÄ›rovávat, dokud nebude znovu obsazeno. + + settings.update_avatar_success=Avatar organizace byl aktualizován. settings.delete=Smazat organizaci settings.delete_account=Smazat tuto organizaci settings.delete_prompt=Organizace bude trvale odstranÄ›na. Tato zmÄ›na <strong>NEMŮŽE</strong> být vrácena! settings.confirm_delete_account=Potvrdit smazánà -settings.delete_org_title=Smazat organizaci -settings.delete_org_desc=Tato organizace bude trvale smazána. PokraÄovat? settings.hooks_desc=PÅ™idat webové háÄky, které budou spouÅ¡tÄ›ny pro <strong>vÅ¡echny repozitáře</strong> v této organizaci. settings.labels_desc=PÅ™idejte Å¡tÃtky, které mohou být použity pro úkoly <strong>vÅ¡ech repositářů</strong> v rámci této organizace. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index f115dee247..a1ff2f914e 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -113,9 +113,11 @@ copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden write=Verfassen preview=Vorschau loading=Laden… +files=Dateien error=Fehler error404=Die Seite, die Du versuchst aufzurufen, <strong>existiert nicht</strong> oder <strong>Du bist nicht berechtigt</strong>, diese anzusehen. +error503=Der Server konnte deine Anfrage nicht abschließen. Bitte versuche es später erneut. go_back=Zurück invalid_data=Ungültige Daten: %v @@ -128,6 +130,7 @@ pin=Anheften unpin=Loslösen artifacts=Artefakte +expired=Abgelaufen confirm_delete_artifact=Bist du sicher, dass du das Artefakt '%s' löschen möchtest? archived=Archiviert @@ -169,6 +172,10 @@ search=Suche ... type_tooltip=Suchmodus fuzzy=Ähnlich fuzzy_tooltip=Ergebnisse einbeziehen, die dem Suchbegriff ähnlich sind +words=Wörter +words_tooltip=Nur Suchbegriffe einbeziehen, die den Suchbegriffen exakt entsprechen +regexp=Regexp +regexp_tooltip=Nur Suchbegriffe einbeziehen, die dem Regexp exakt entsprechen exact=Exakt exact_tooltip=Nur Suchbegriffe einbeziehen, die dem exakten Suchbegriff entsprechen repo_kind=Repositories durchsuchen ... @@ -444,6 +451,7 @@ use_scratch_code=Einmalpasswort verwenden twofa_scratch_used=Du hast dein Einmalpasswort verwendet. Du wurdest zu den Einstellung der Zwei-Faktor-Authentifizierung umgeleitet, dort kannst du dein Gerät abmelden oder ein neues Einmalpasswort erzeugen. twofa_passcode_incorrect=Ungültige PIN. Wenn du dein Gerät verloren hast, verwende dein Einmalpasswort. twofa_scratch_token_incorrect=Das Einmalpasswort ist falsch. +twofa_required=Du musst die Zwei-Faktor-Authentifizierung einrichten, um Zugriff auf die Repositories zu erhalten, oder versuche dich erneut anzumelden. login_userpass=Anmelden login_openid=OpenID oauth_signup_tab=Neues Konto registrieren @@ -452,6 +460,7 @@ oauth_signup_submit=Konto vervollständigen oauth_signin_tab=Mit existierendem Konto verbinden oauth_signin_title=Anmelden um verbundenes Konto zu autorisieren oauth_signin_submit=Konto verbinden +oauth.signin.error.general=Beim Verarbeiten der Autorisierungsanfrage ist ein Fehler aufgetreten: %s. Wenn dieser Fehler weiterhin besteht, wende dich bitte an den Administrator. oauth.signin.error.access_denied=Die Autorisierungsanfrage wurde abgelehnt. oauth.signin.error.temporarily_unavailable=Autorisierung fehlgeschlagen, da der Authentifizierungsserver vorübergehend nicht verfügbar ist. Bitte versuch es später erneut. oauth_callback_unable_auto_reg=Automatische Registrierung ist aktiviert, aber der OAuth2-Provider %[1]s hat fehlende Felder zurückgegeben: %[2]s, kann den Account nicht automatisch erstellen. Bitte erstelle oder verbinde einen Account oder kontaktieren den Administrator. @@ -724,6 +733,8 @@ public_profile=Öffentliches Profil biography_placeholder=Erzähle uns ein wenig über Dich selbst! (Du kannst Markdown verwenden) location_placeholder=Teile Deinen ungefähren Standort mit anderen profile_desc=Lege fest, wie dein Profil anderen Benutzern angezeigt wird. Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und webbasierte Git-Operationen verwendet. +password_username_disabled=Du bist nicht berechtigt, den Benutzernamen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details. +password_full_name_disabled=Du bist nicht berechtigt, den vollständigen Namen zu ändern. Bitte kontaktiere Deinen Seitenadministrator für weitere Details. full_name=Vollständiger Name website=Webseite location=Standort @@ -918,6 +929,9 @@ permission_not_set=Nicht festgelegt permission_no_access=Kein Zugriff permission_read=Lesen permission_write=Lesen und Schreiben +permission_anonymous_read=Anonymes Lesen +permission_everyone_read=Alle lesen +permission_everyone_write=Alle schreiben access_token_desc=Ausgewählte Token-Berechtigungen beschränken die Authentifizierung auf die entsprechenden <a %s>API</a>-Routen. Lies die <a %s>Dokumentation</a> für mehr Informationen. at_least_one_permission=Du musst mindestens eine Berechtigung auswählen, um ein Token zu erstellen permissions_list=Berechtigungen: @@ -1020,6 +1034,9 @@ new_repo_helper=Ein Repository enthält alle Projektdateien, einschließlich des owner=Besitzer owner_helper=Einige Organisationen könnten in der Dropdown-Liste nicht angezeigt werden, da die Anzahl an Repositories begrenzt ist. repo_name=Repository-Name +repo_name_profile_public_hint=.profile ist ein spezielles Repository, mit dem du die README.md zu deinem öffentlichen Organisationsprofil hinzufügen kannst, das für jeden sichtbar ist. Stelle sicher, dass es öffentlich ist und initialisiere es mit einer README im Profilverzeichnis, um loszulegen. +repo_name_profile_private_hint=.profile ist ein spezielles Repository, mit dem du die README.md zu deinem privaten Organisationsprofil hinzufügen kannst, das nur für Organisationsmitglieder sichtbar ist. Stelle sicher, dass es privat ist und initialisiere es mit einer README im Profilverzeichnis, um loszulegen. +repo_name_helper=Ein guter Repository-Name besteht normalerweise aus kurzen, unvergesslichen und einzigartigen Schlagwörtern. Ein Repository namens ".profile" or ".profile-private" kann verwendet werden, um die README.md auf dem Benutzer- oder Organisationsprofil anzuzeigen. repo_size=Repository-Größe template=Template template_select=Vorlage auswählen @@ -1116,6 +1133,7 @@ blame.ignore_revs=Revisionen in <a href="%s">.git-blame-ignore-revs</a> werden i blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in <a href="%s">.git-blame-ignore-revs</a>. user_search_tooltip=Zeigt maximal 30 Benutzer +tree_path_not_found=Pfad %[1]s existiert nicht in %[2]s transfer.accept=Übertragung Akzeptieren transfer.accept_desc=`Übertragung nach "%s"` @@ -1126,6 +1144,7 @@ transfer.no_permission_to_reject=Du hast keine Berechtigung, diesen Transfer abz desc.private=Privat desc.public=Öffentlich +desc.public_access=Öffentlicher Zugriff desc.template=Template desc.internal=Intern desc.archived=Archiviert @@ -1233,6 +1252,7 @@ create_new_repo_command=Erstelle ein neues Repository von der Kommandozeile aus push_exist_repo=Bestehendes Repository via Kommandozeile pushen empty_message=Dieses Repository hat keinen Inhalt. broken_message=Die Git-Daten, die diesem Repository zugrunde liegen, können nicht gelesen werden. Kontaktiere den Administrator deiner Instanz oder lösche dieses Repository. +no_branch=Dieses Repository hat keine Branches. code=Code code.desc=Zugriff auf Quellcode, Dateien, Commits und Branches. @@ -1331,7 +1351,6 @@ editor.update=%s aktualisiert editor.delete=%s gelöscht editor.patch=Patch anwenden editor.patching=Patche: -editor.fail_to_apply_patch=Patch "%s" nicht anwendbar editor.new_patch=Neuer Patch editor.commit_message_desc=Eine ausführlichere (optionale) Beschreibung hinzufügen… editor.signoff_desc=Am Ende der Commit Nachricht einen Signed-off-by Anhang vom Committer hinzufügen. @@ -1344,13 +1363,13 @@ editor.new_branch_name_desc=Neuer Branchname… editor.cancel=Abbrechen editor.filename_cannot_be_empty=Der Dateiname darf nicht leer sein. editor.filename_is_invalid=Ungültiger Dateiname: "%s". +editor.commit_email=Commit-E-Mail-Adresse +editor.invalid_commit_email=Die E-Mail-Adresse für den Commit ist ungültig. editor.branch_does_not_exist=Branch "%s" existiert nicht in diesem Repository. editor.branch_already_exists=Branch "%s" existiert bereits in diesem Repository. editor.directory_is_a_file=Der Verzeichnisname "%s" wird bereits als Dateiname in diesem Repository verwendet. editor.file_is_a_symlink=`"%s" ist ein symbolischer Link. Symbolische Links können mit dem Web-Editor nicht bearbeitet werden` editor.filename_is_a_directory=Der Dateiname "%s" wird bereits als Verzeichnisname in diesem Repository verwendet. -editor.file_editing_no_longer_exists=Die bearbeitete Datei "%s" existiert nicht mehr in diesem Repository. -editor.file_deleting_no_longer_exists=Die zu löschende Datei "%s" existiert nicht mehr in diesem Repository. editor.file_changed_while_editing=Der Inhalt der Datei hat sich seit dem Beginn der Bearbeitung geändert. <a target="_blank" rel="noopener noreferrer" href="%s">Hier klicken</a>, um die Änderungen anzusehen, oder <strong>Änderungen erneut comitten</strong>, um sie zu überschreiben. editor.file_already_exists=Eine Datei mit dem Namen '%s' existiert bereits in diesem Repository. editor.commit_id_not_matching=Die Commit-ID stimmt nicht mit der ID überein, bei welcher du mit der Bearbeitung begonnen hast. Commite in einen Patch-Branch und merge daraufhin. @@ -1358,8 +1377,6 @@ editor.push_out_of_date=Der Push scheint veraltet zu sein. editor.commit_empty_file_header=Leere Datei committen editor.commit_empty_file_text=Die Datei, die du commiten willst, ist leer. Fortfahren? editor.no_changes_to_show=Keine Änderungen vorhanden. -editor.fail_to_update_file=Fehler beim Aktualisieren/Erstellen der Datei "%s". -editor.fail_to_update_file_summary=Fehlermeldung: editor.push_rejected_no_message=Die Änderung wurde vom Server ohne Nachricht abgelehnt. Bitte überprüfe die Git Hooks. editor.push_rejected=Die Änderung wurde vom Server abgelehnt. Bitte überprüfe die Git Hooks. editor.push_rejected_summary=Vollständige Ablehnungsmeldung: @@ -1374,6 +1391,7 @@ editor.require_signed_commit=Branch erfordert einen signierten Commit editor.cherry_pick=Cherry-Picke %s von: editor.revert=%s zurücksetzen auf: + commits.desc=Durchsuche die Quellcode-Änderungshistorie. commits.commits=Commits commits.no_commits=Keine gemeinsamen Commits. "%s" und "%s" haben vollständig unterschiedliche Historien. @@ -1392,6 +1410,7 @@ commits.signed_by_untrusted_user_unmatched=Signiert von nicht vertrauenswürdige commits.gpg_key_id=GPG-Schlüssel-ID commits.ssh_key_fingerprint=SSH-Key-Fingerabdruck commits.view_path=An diesem Punkt im Verlauf anzeigen +commits.view_file_diff=Änderungen an dieser Datei in diesem Commit anzeigen commit.operations=Operationen commit.revert=Zurücksetzen @@ -1452,6 +1471,8 @@ issues.filter_milestones=Meilenstein filtern issues.filter_projects=Projekt filtern issues.filter_labels=Label filtern issues.filter_reviewers=Reviewer filtern +issues.filter_no_results=Keine Ergebnisse +issues.filter_no_results_placeholder=Versuche, deine Suchfilter anzupassen. issues.new=Neues Issue issues.new.title_empty=Der Titel kann nicht leer sein issues.new.labels=Label @@ -1527,11 +1548,14 @@ issues.filter_project=Projekt issues.filter_project_all=Alle Projekte issues.filter_project_none=Kein Projekt issues.filter_assignee=Zuständig +issues.filter_assignee_no_assignee=Niemandem zugewiesen +issues.filter_assignee_any_assignee=Jemandem zugewiesen issues.filter_poster=Autor issues.filter_user_placeholder=Benutzer suchen issues.filter_user_no_select=Alle Benutzer issues.filter_type=Typ issues.filter_type.all_issues=Alle Issues +issues.filter_type.all_pull_requests=Alle Pull-Requests issues.filter_type.assigned_to_you=Dir zugewiesen issues.filter_type.created_by_you=Von dir erstellt issues.filter_type.mentioning_you=Hat dich erwähnt @@ -1623,12 +1647,15 @@ issues.save=Speichern issues.label_title=Labelname issues.label_description=Labelbeschreibung issues.label_color=Labelfarbe +issues.label_color_invalid=Ungültige Farbe issues.label_exclusive=Exklusiv issues.label_archive=Label archivieren issues.label_archived_filter=Archivierte Labels anzeigen issues.label_archive_tooltip=Archivierte Labels werden bei der Suche nach Label standardmäßig von den Vorschlägen ausgeschlossen. issues.label_exclusive_desc=Nenne das Label <code>Bereich/Element</code> um es gegenseitig ausschließend mit anderen <code>Bereich/</code> Labels zu machen. issues.label_exclusive_warning=Alle im Konflikt stehenden Labels werden beim Bearbeiten der Labels eines Issues oder eines Pull-Requests entfernt. +issues.label_exclusive_order=Sortierreihenfolge +issues.label_exclusive_order_tooltip=Exklusive Labels im gleichen Bereich werden nach dieser numerischen Reihenfolge sortiert. issues.label_count=%d Label issues.label_open_issues=%d offene Issues issues.label_edit=Bearbeiten @@ -1680,14 +1707,18 @@ issues.timetracker_timer_manually_add=Zeit hinzufügen issues.time_estimate_set=Geschätzte Zeit festlegen issues.time_estimate_display=Schätzung: %s +issues.change_time_estimate_at=Zeitschätzung geändert zu <b>%[1]s</b> %[2]s issues.remove_time_estimate_at=Zeitschätzung %s entfernt issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig issues.start_tracking_history=hat die Zeiterfassung %s gestartet issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird issues.tracking_already_started=`Du hast die Zeiterfassung bereits in <a href="%s">diesem Issue</a> gestartet!` +issues.stop_tracking=Timer stoppen +issues.stop_tracking_history=hat für <b>%[1]s</b> %[2]s gearbeitet issues.cancel_tracking=Verwerfen issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen` issues.del_time=Diese Zeiterfassung löschen +issues.add_time_history=hat <b>%[1]s</b> %[2]s gearbeitete Zeit hinzugefügt issues.del_time_history=`hat %s gearbeitete Zeit gelöscht` issues.add_time_manually=Zeit manuell hinzufügen issues.add_time_hours=Stunden @@ -1847,6 +1878,7 @@ pulls.add_prefix=<strong>%s</strong> Präfix hinzufügen pulls.remove_prefix=<strong>%s</strong> Präfix entfernen pulls.data_broken=Dieser Pull-Requests ist kaputt, da Fork-Informationen gelöscht wurden. pulls.files_conflicted=Dieser Pull-Request hat Änderungen, die im Widerspruch zum Ziel-Branch stehen. +pulls.is_checking=Die Konfliktprüfung läuft noch. Bitte aktualisiere die Seite in wenigen Augenblicken. pulls.is_ancestor=Dieser Branch ist bereits im Zielbranch enthalten. Es gibt nichts zu mergen. pulls.is_empty=Die Änderungen an diesem Branch sind bereits auf dem Zielbranch. Dies wird ein leerer Commit sein. pulls.required_status_check_failed=Einige erforderliche Prüfungen waren nicht erfolgreich. @@ -1916,11 +1948,11 @@ pulls.outdated_with_base_branch=Dieser Branch enthält nicht die neusten Commits pulls.close=Pull-Request schließen pulls.closed_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> geschlossen` pulls.reopened_at=`hat diesen Pull-Request <a id="%[1]s" href="#%[1]s">%[2]s</a> wieder geöffnet` +pulls.cmd_instruction_hint=Zeige Kommandozeilenanweisungen pulls.cmd_instruction_checkout_title=Checkout pulls.cmd_instruction_checkout_desc=Wechsle auf einen neuen Branch in deinem lokalen Repository und teste die Änderungen. pulls.cmd_instruction_merge_title=Mergen pulls.cmd_instruction_merge_desc=Die Änderungen mergen und auf Gitea aktualisieren. -pulls.cmd_instruction_merge_warning=Warnung: Dieser Vorgang kann den Pull-Request nicht mergen, da "manueller Merge" nicht aktiviert wurde pulls.clear_merge_message=Merge-Nachricht löschen pulls.clear_merge_message_hint=Das Löschen der Merge-Nachricht wird nur den Inhalt der Commit-Nachricht entfernen und generierte Git-Trailer wie "Co-Authored-By …" erhalten. @@ -1944,6 +1976,7 @@ pulls.upstream_diverging_prompt_behind_1=Dieser Branch ist %[1]d Commit hinter % pulls.upstream_diverging_prompt_behind_n=Dieser Branch ist %[1]d Commits hinter %[2]s pulls.upstream_diverging_prompt_base_newer=Der Basis-Branch %s hat neue Änderungen pulls.upstream_diverging_merge=Fork synchronisieren +pulls.upstream_diverging_merge_confirm=Möchtest du „%[1]s“ in „%[2]s“ mergen? pull.deleted_branch=(gelöscht):%s pull.agit_documentation=Dokumentation zu AGit durchschauen @@ -2103,6 +2136,12 @@ contributors.contribution_type.deletions=Löschungen settings=Einstellungen settings.desc=In den Einstellungen kannst du die Einstellungen des Repositories anpassen settings.options=Repository +settings.public_access=Öffentlicher Zugriff +settings.public_access_desc=Konfiguriere die Zugriffsrechte öffentlicher Besucher, um die Standardwerte dieses Repositorys zu überschreiben. +settings.public_access.docs.not_set=Nicht gesetzt: keine zusätzliche öffentliche Zugriffsberechtigung. Die Berechtigung der Besucher folgt den Sichtbarkeits- und Mitgliedsberechtigungen des Repositorys. +settings.public_access.docs.anonymous_read=Anonymes Lesen: Nicht angemeldete Benutzer können mit Leseberechtigung auf die Einheit zugreifen. +settings.public_access.docs.everyone_read=Alle Lesen: Alle angemeldeten Benutzer können mit Leseberechtigung auf die Einheit zugreifen. Leseberechtigung für Issues/Pull-Request-Einheiten bedeutet auch, dass Benutzer neue Issues/Pull-Requests erstellen können. +settings.public_access.docs.everyone_write=Alle Schreiben: Alle angemeldeten Benutzer haben Schreibrechte auf die Einheit. Nur die Wiki-Einheit unterstützt diese Berechtigung. settings.collaboration=Mitarbeiter settings.collaboration.admin=Administrator settings.collaboration.write=Schreibrechte @@ -2316,6 +2355,8 @@ settings.event_fork=Fork settings.event_fork_desc=Repository geforkt. settings.event_wiki=Wiki settings.event_wiki_desc=Wiki-Seite erstellt, umbenannt, bearbeitet oder gelöscht. +settings.event_statuses=Status +settings.event_statuses_desc=Commit-Status von der API aktualisiert. settings.event_release=Release settings.event_release_desc=Release in einem Repository veröffentlicht, aktualisiert oder gelöscht. settings.event_push=Push @@ -2325,7 +2366,6 @@ settings.event_repository=Repository settings.event_repository_desc=Repository erstellt oder gelöscht. settings.event_header_issue=Issue Ereignisse settings.event_issues=Issues -settings.event_issues_desc=Issue geöffnet, geschlossen, wieder geöffnet oder bearbeitet. settings.event_issue_assign=Issue zugewiesen settings.event_issue_assign_desc=Issue zugewiesen oder Zuweisung entfernt. settings.event_issue_label=Issue mit Label versehen @@ -2336,7 +2376,6 @@ settings.event_issue_comment=Issue-Kommentar settings.event_issue_comment_desc=Issue-Kommentar angelegt, geändert oder gelöscht. settings.event_header_pull_request=Pull-Request-Ereignisse settings.event_pull_request=Pull-Request -settings.event_pull_request_desc=Pull-Request geöffnet, geschlossen, wieder geöffnet oder bearbeitet. settings.event_pull_request_assign=Pull-Request zugewiesen settings.event_pull_request_assign_desc=Pull-Request zugewiesen oder Zuweisung entfernt. settings.event_pull_request_label=Pull-Request mit Label versehen @@ -2353,6 +2392,9 @@ settings.event_pull_request_review_request=Überprüfung des Pull-Requests angef settings.event_pull_request_review_request_desc=Überprüfung des Pull-Requests angefragt oder die Anfrage entfernt. settings.event_pull_request_approvals=Zustimmungen zum Pull-Request settings.event_pull_request_merge=Pull-Request-Merge +settings.event_header_workflow=Workflow-Ereignisse +settings.event_workflow_job=Workflow-Jobs +settings.event_workflow_job_desc=Gitea Actions Workflow Job in Warteschlange, wartend, in Bearbeitung oder abgeschlossen. settings.event_package=Paket settings.event_package_desc=Paket wurde in einem Repository erstellt oder gelöscht. settings.branch_filter=Branch-Filter @@ -2615,6 +2657,9 @@ diff.image.overlay=Überlagert diff.has_escaped=Diese Zeile enthält versteckte Unicode-Zeichen diff.show_file_tree=Dateibaum anzeigen diff.hide_file_tree=Dateibaum ausblenden +diff.submodule_added=Submodul %[1]s hinzugefügt bei %[2]s +diff.submodule_deleted=Submodul %[1]s gelöscht von %[2]s +diff.submodule_updated=Submodul %[1]s aktualisiert: %[2]s releases.desc=Behalte den Überblick über Versionen und Downloads. release.releases=Releases @@ -2685,6 +2730,7 @@ branch.restore_success=Branch "%s" wurde wiederhergestellt. branch.restore_failed=Wiederherstellung vom Branch "%s" gescheitert. branch.protected_deletion_failed=Branch "%s" ist geschützt und kann nicht gelöscht werden. branch.default_deletion_failed=Branch "%s" kann nicht gelöscht werden, da dieser Branch der Standard-Branch ist. +branch.default_branch_not_exist=Standardzweig „%s“ existiert nicht. branch.restore=Branch "%s" wiederherstellen branch.download=Branch "%s" herunterladen branch.rename=Branch "%s" umbenennen @@ -2699,6 +2745,8 @@ branch.create_branch_operation=Branch erstellen branch.new_branch=Neue Branch erstellen branch.new_branch_from=Neuen Branch von "%s" erstellen branch.renamed=Branch %s wurde in %s umbenannt. +branch.rename_default_or_protected_branch_error=Nur Administratoren können Standard- oder geschützte Branches umbenennen. +branch.rename_protected_branch_failed=Dieser Branch ist durch globale Schutzregeln geschützt. tag.create_tag=Tag %s erstellen tag.create_tag_operation=Tag erstellen @@ -2773,15 +2821,13 @@ settings.visibility.private_shortname=Privat settings.update_settings=Einstellungen speichern settings.update_setting_success=Organisationseinstellungen wurden aktualisiert. -settings.change_orgname_prompt=Hinweis: Das Ändern des Organisationsnamens wird auch die URL deiner Organisation ändern und den alten Namen freigeben. -settings.change_orgname_redirect_prompt=Der alte Name wird weiterleiten, bis er wieder beansprucht wird. + + settings.update_avatar_success=Der Organisationsavatar wurde aktualisiert. settings.delete=Organisation löschen settings.delete_account=Diese Organisation löschen settings.delete_prompt=Die Organisation wird dauerhaft gelöscht. Dies <strong>KANN NICHT</strong> rückgängig gemacht werden! settings.confirm_delete_account=Löschen bestätigen -settings.delete_org_title=Organisation löschen -settings.delete_org_desc=Diese Organisation wird dauerhaft gelöscht. Fortfahren? settings.hooks_desc=Webhooks hinzufügen, die für <strong>alle</strong> Repositories dieser Organisation ausgelöst werden. settings.labels_desc=Labels hinzufügen, die für <strong>alle Repositories</strong> dieser Organisation genutzt werden können. @@ -2853,7 +2899,11 @@ teams.invite.title=Du wurdest eingeladen, dem Team <strong>%s</strong> in der Or teams.invite.by=Von %s eingeladen teams.invite.description=Bitte klicke auf die folgende Schaltfläche, um dem Team beizutreten. +view_as_role=Ansehen als: %s +view_as_public_hint=Du siehst die README als ein öffentlicher Benutzer. +view_as_member_hint=Du siehst die README als ein Mitglied dieser Organisation. +worktime=Arbeitszeit worktime.date_range_start=Startdatum worktime.date_range_end=Enddatum worktime.query=Abfrage @@ -3354,6 +3404,8 @@ monitor.previous=Letzte Ausführung monitor.execute_times=Ausführungen monitor.process=Laufende Prozesse monitor.stacktrace=Stacktraces +monitor.trace=Ablaufverfolgung +monitor.performance_logs=Leistungsprotokolle monitor.processes_count=%d Prozesse monitor.download_diagnosis_report=Diagnosebericht herunterladen monitor.desc=Beschreibung @@ -3528,6 +3580,7 @@ versions=Versionen versions.view_all=Alle anzeigen dependency.id=ID dependency.version=Version +search_in_external_registry=In %s suchen alpine.registry=Richte diese Registry ein, indem Du die URL in die <code>/etc/apk/repositories</code>-Datei hinzufügst: alpine.registry.key=Lade den öffentlichen RSA-Key der Registry in den <code>/etc/apk/keys/</code>-Ordner, um die Signatur zu überprüfen: alpine.registry.info=Wähle $branch und $repository aus der Liste unten. @@ -3557,6 +3610,8 @@ conda.install=Um das Paket mit Conda zu installieren, führe den folgenden Befeh container.details.type=Container-Image Typ container.details.platform=Plattform container.pull=Downloade das Container-Image aus der Kommandozeile: +container.images=Images +container.digest=Digest container.multi_arch=Betriebsystem / Architektur container.layers=Container-Image Ebenen container.labels=Labels @@ -3664,8 +3719,13 @@ none=Noch keine Secrets vorhanden. creation.description=Beschreibung creation.name_placeholder=Groß-/Kleinschreibung wird ignoriert, nur alphanumerische Zeichen oder Unterstriche, darf nicht mit GITEA_ oder GITHUB_ beginnen creation.value_placeholder=Beliebigen Inhalt eingeben. Leerzeichen am Anfang und Ende werden weggelassen. +creation.description_placeholder=Gib eine Kurzbeschreibung ein (optional). +save_success=Das Secret "%s" wurde gespeichert. +save_failed=Secret konnte nicht gespeichert werden. +add_secret=Secret hinzufügen +edit_secret=Secret bearbeiten deletion=Secret entfernen deletion.description=Das Entfernen eines Secrets kann nicht rückgängig gemacht werden. Fortfahren? deletion.success=Das Secret wurde entfernt. @@ -3743,6 +3803,10 @@ runs.no_workflows.documentation=Weitere Informationen zu Gitea Actions findest d runs.no_runs=Der Workflow hat noch keine Ausführungen. runs.empty_commit_message=(leere Commit-Nachricht) runs.expire_log_message=Protokolle wurden geleert, weil sie zu alt waren. +runs.delete=Workflow-Ausführung löschen +runs.delete.description=Bist du sicher, dass du diese Workflow-Ausführung dauerhaft löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden. +runs.not_done=Diese Workflow-Ausführung ist noch nicht abgeschlossen. +runs.view_workflow_file=Workflow-Datei anzeigen workflow.disable=Workflow deaktivieren workflow.disable_success=Workflow '%s' erfolgreich deaktiviert. @@ -3754,6 +3818,7 @@ workflow.not_found=Workflow '%s' wurde nicht gefunden. workflow.run_success=Workflow '%s' erfolgreich ausgeführt. workflow.from_ref=Nutze Workflow von workflow.has_workflow_dispatch=Dieser Workflow hat einen workflow_dispatch Event-Trigger. +workflow.has_no_workflow_dispatch=Der Workflow '%s' hat keinen workflow_dispatch Event-Trigger. need_approval_desc=Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich. @@ -3781,6 +3846,8 @@ deleted.display_name=Gelöschtes Projekt type-1.display_name=Individuelles Projekt type-2.display_name=Repository-Projekt type-3.display_name=Organisationsprojekt +enter_fullscreen=Vollbild +exit_fullscreen=Vollbild verlassen [git.filemode] changed_filemode=%[1]s → %[2]s diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 444fbd26c9..4a7fe0fab7 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -1190,7 +1190,6 @@ editor.update=ΕνημÎÏωση %s editor.delete=ΔιαγÏαφή %s editor.patch=ΕφαÏμογή ΔιόÏθωσης editor.patching=ΕπιδιόÏθωση: -editor.fail_to_apply_patch=`Αδυναμία εφαÏμογής της επιδιόÏθωσης "%s"` editor.new_patch=ÎÎα ΔιόÏθωση editor.commit_message_desc=Î Ïοσθήκη Ï€ÏοαιÏετικής εκτενοÏÏ‚ πεÏιγÏαφής… editor.signoff_desc=Î ÏοσθÎστε Îνα Ï€Ïόσθετο Signed-off-by στο Ï„Îλος του μηνÏματος καταγÏαφής της υποβολής. @@ -1208,15 +1207,11 @@ editor.branch_already_exists=Ο κλάδος "%s" υπάÏχει ήδη σε Î±Ï editor.directory_is_a_file=Το όνομα φακÎλου "%s" χÏησιμοποιείται ήδη ως όνομα αÏχείου σε αυτό το αποθετήÏιο. editor.file_is_a_symlink=`Το "%s" είναι συμβολικός σÏνδεσμος. Οι συμβολικοί σÏνδεσμοι δεν μποÏοÏν να επεξεÏγαστοÏν στην ενσωματωμÎνη εφαÏμογή` editor.filename_is_a_directory=Το όνομα αÏχείου "%s" χÏησιμοποιείται ήδη ως όνομα φακÎλου σε αυτό το αποθετήÏιο. -editor.file_editing_no_longer_exists=Το αÏχείο "%s" που επεξεÏγάζεται, δεν υπάÏχει πλÎον σε αυτό το αποθετήÏιο. -editor.file_deleting_no_longer_exists=Το αÏχείο "%s" που διαγÏάφεται, δεν υπάÏχει πλÎον σε αυτό το αποθετήÏιο. editor.file_changed_while_editing=Τα πεÏιεχόμενα του αÏχείου άλλαξαν από τότε που ξεκίνησε η επεξεÏγασία. <a target="_blank" rel="noopener noreferrer" href="%s">Κάντε κλικ εδώ</a> για να τα δείτε ή <strong>Υποβολή Αλλαγών ξανά</strong> για να τα αντικαταστήσετε. editor.file_already_exists=Ένα αÏχείο με το όνομα "%s" υπάÏχει ήδη σε αυτό το αποθετήÏιο. editor.commit_empty_file_header=Υποβολή ενός ÎºÎµÎ½Î¿Ï Î±Ïχείου editor.commit_empty_file_text=Το αÏχείο που Ï€Ïόκειται να υποβληθεί είναι κενό. ΣυνÎχεια; editor.no_changes_to_show=Δεν υπάÏχουν αλλαγÎÏ‚ για εμφάνιση. -editor.fail_to_update_file=Αποτυχία ενημÎÏωσης/δημιουÏγίας του αÏχείου "%s". -editor.fail_to_update_file_summary=Μήνυμα Σφάλματος: editor.push_rejected_no_message=Η αλλαγή αποÏÏίφθηκε από το διακομιστή χωÏίς κάποιο μήνυμα. ΠαÏακαλώ ελÎγξτε τα ΆγκιστÏα Git. editor.push_rejected=Η αλλαγή αποÏÏίφθηκε από τον διακομιστή. ΠαÏακαλώ ελÎγξτε τα ΆγκιστÏα Git. editor.push_rejected_summary=Μήνυμα ΠλήÏους ΑπόÏÏιψης: @@ -1231,6 +1226,7 @@ editor.require_signed_commit=Ο κλάδος απαιτεί υπογεγÏαμμ editor.cherry_pick=Ανθολόγηση (cherry-pic) του %s στο: editor.revert=ΑπόσυÏση του %s στο: + commits.desc=Δείτε το ιστοÏικό αλλαγών του πηγαίου κώδικα. commits.commits=ΥποβολÎÏ‚ commits.no_commits=Δεν υπάÏχουν κοινÎÏ‚ υποβολÎÏ‚. Τα "%s" και "%s" Îχουν εντελώς διαφοÏετικÎÏ‚ ιστοÏίες. @@ -2117,7 +2113,6 @@ settings.event_repository=ΑποθετήÏιο settings.event_repository_desc=Το αποθετήÏιο δημιουÏγήθηκε ή διαγÏάφηκε. settings.event_header_issue=Γεγονότα Ζητήματος settings.event_issues=Ζητήματα -settings.event_issues_desc=Το ζήτημα άνοιξε, Îκλεισε, ανοίχθηκε εκ νÎου ή επεξεÏγάστηκε. settings.event_issue_assign=Ζήτημα ΑνατÎθηκε settings.event_issue_assign_desc=Ζήτημα εκχωÏημÎνο ή μη εκχωÏημÎνο. settings.event_issue_label=Σήμανση Ζητήματος @@ -2128,7 +2123,6 @@ settings.event_issue_comment=Σχόλιο Ζητήματος settings.event_issue_comment_desc=Το σχόλιο στο ζήτημα δημιουÏγήθηκε, επεξεÏγάστηκε ή διαγÏάφηκε. settings.event_header_pull_request=Γεγονότα Pull Requests settings.event_pull_request=Pull Request -settings.event_pull_request_desc=Το pull request άνοιξε, Îκλεισε, άνοιξε εκ νÎου ή επεξεÏγάστηκε. settings.event_pull_request_assign=Το Pull Request ΑνατÎθηκε settings.event_pull_request_assign_desc=Το pull request ανατÎθηκε ή Îγινε αδιάθετο. settings.event_pull_request_label=Σήμανση Pull Request @@ -2505,15 +2499,13 @@ settings.visibility.private_shortname=Ιδιωτικός settings.update_settings=ΕνημÎÏωση Ρυθμίσεων settings.update_setting_success=Οι Ïυθμίσεις του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï Îχουν ενημεÏωθεί. -settings.change_orgname_prompt=Σημείωση: Η αλλαγή του ονόματος του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï Î¸Î± αλλάξει επίσης τη διεÏθυνση URL του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï ÏƒÎ±Ï‚ και θα απελευθεÏώσει το παλιό όνομα. -settings.change_orgname_redirect_prompt=Το παλιό όνομα θα ανακατευθÏνει μÎχÏι να διεκδικηθεί. + + settings.update_avatar_success=Η εικόνα του οÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï Îχει ενημεÏωθεί. settings.delete=ΔιαγÏαφή ΟÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï settings.delete_account=ΔιαγÏαφή Î‘Ï…Ï„Î¿Ï Î¤Î¿Ï… ΟÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï settings.delete_prompt=Ο οÏγανισμός θα αφαιÏεθεί οÏιστικά. Αυτό το <strong>ΔΕΠΜΠΟΡΕΙ</strong> να αναιÏεθεί! settings.confirm_delete_account=Επιβεβαίωση ΔιαγÏαφής -settings.delete_org_title=ΔιαγÏαφή ΟÏÎ³Î±Î½Î¹ÏƒÎ¼Î¿Ï -settings.delete_org_desc=Αυτός ο οÏγανισμός θα διαγÏαφεί οÏιστικά. ΣυνÎχεια; settings.hooks_desc=Î Ïοσθήκη webhooks που θα ενεÏγοποιοÏνται για <strong>όλα τα αποθετήÏια</strong> κάτω από αυτό τον οÏγανισμό. settings.labels_desc=Î Ïοσθήκη σημάτων που μποÏοÏν να χÏησιμοποιηθοÏν σε ζητήματα για <strong>όλα τα αποθετήÏια</strong> κάτω από αυτό τον οÏγανισμό. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4384ebc3d7..f979fc814d 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -421,6 +421,7 @@ remember_me.compromised = The login token is not valid anymore which may indicat forgot_password_title= Forgot Password forgot_password = Forgot password? need_account = Need an account? +sign_up_tip = You are registering the first account in the system, which has administrator privileges. Please carefully remember your username and password. If you forget the username or password, please refer to the Gitea documentation to recover the account. sign_up_now = Register now. sign_up_successful = Account was successfully created. Welcome! confirmation_mail_sent_prompt_ex = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If your registration email address is incorrect, you can sign in again and change it. @@ -1228,7 +1229,7 @@ migrate.migrating_issues = Migrating Issues migrate.migrating_pulls = Migrating Pull Requests migrate.cancel_migrating_title = Cancel Migration migrate.cancel_migrating_confirm = Do you want to cancel this migration? -migrating_status = Migrating status +migration_status = Migration status mirror_from = mirror of forked_from = forked from @@ -1332,7 +1333,9 @@ editor.upload_file = Upload File editor.edit_file = Edit File editor.preview_changes = Preview Changes editor.cannot_edit_lfs_files = LFS files cannot be edited in the web interface. +editor.cannot_edit_too_large_file = The file is too large to be edited. editor.cannot_edit_non_text_files = Binary files cannot be edited in the web interface. +editor.file_not_editable_hint = But you can still rename or move it. editor.edit_this_file = Edit File editor.this_file_locked = File is locked editor.must_be_on_a_branch = You must be on a branch to make or propose changes to this file. @@ -1352,7 +1355,7 @@ editor.update = Update %s editor.delete = Delete %s editor.patch = Apply Patch editor.patching = Patching: -editor.fail_to_apply_patch = Unable to apply patch "%s" +editor.fail_to_apply_patch = Unable to apply patch editor.new_patch = New Patch editor.commit_message_desc = Add an optional extended description… editor.signoff_desc = Add a Signed-off-by trailer by the committer at the end of the commit log message. @@ -1372,8 +1375,7 @@ editor.branch_already_exists = Branch "%s" already exists in this repository. editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository. editor.file_is_a_symlink = `"%s" is a symbolic link. Symbolic links cannot be edited in the web editor` editor.filename_is_a_directory = Filename "%s" is already used as a directory name in this repository. -editor.file_editing_no_longer_exists = The file being edited, "%s", no longer exists in this repository. -editor.file_deleting_no_longer_exists = The file being deleted, "%s", no longer exists in this repository. +editor.file_modifying_no_longer_exists = The file being modified, "%s", no longer exists in this repository. editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them. editor.file_already_exists = A file named "%s" already exists in this repository. editor.commit_id_not_matching = The Commit ID does not match the ID when you began editing. Commit into a patch branch and then merge. @@ -1381,8 +1383,6 @@ editor.push_out_of_date = The push appears to be out of date. editor.commit_empty_file_header = Commit an empty file editor.commit_empty_file_text = The file you're about to commit is empty. Proceed? editor.no_changes_to_show = There are no changes to show. -editor.fail_to_update_file = Failed to update/create file "%s". -editor.fail_to_update_file_summary = Error Message: editor.push_rejected_no_message = The change was rejected by the server without a message. Please check Git Hooks. editor.push_rejected = The change was rejected by the server. Please check Git Hooks. editor.push_rejected_summary = Full Rejection Message: @@ -1396,6 +1396,15 @@ editor.user_no_push_to_branch = User cannot push to branch editor.require_signed_commit = Branch requires a signed commit editor.cherry_pick = Cherry-pick %s onto: editor.revert = Revert %s onto: +editor.failed_to_commit = Failed to commit changes. +editor.failed_to_commit_summary = Error Message: + +editor.fork_create = Fork Repository to Propose Changes +editor.fork_create_description = You can not edit this repository directly. Instead you can create a fork, make edits and create a pull request. +editor.fork_edit_description = You can not edit this repository directly. The changes will be written to your fork <b>%s</b>, so you can create a pull request. +editor.fork_not_editable = You have forked this repository but your fork is not editable. +editor.fork_failed_to_push_branch = Failed to push branch %s to your repository. +editor.fork_branch_exists = Branch "%s" already exists in your fork, please choose a new branch name. commits.desc = Browse source code change history. commits.commits = Commits @@ -1433,7 +1442,6 @@ commitstatus.success = Success ext_issues = Access to External Issues ext_issues.desc = Link to an external issue tracker. -projects = Projects projects.desc = Manage issues and pulls in projects. projects.description = Description (optional) projects.description_placeholder = Description @@ -1561,6 +1569,7 @@ issues.filter_user_placeholder = Search users issues.filter_user_no_select = All users issues.filter_type = Type issues.filter_type.all_issues = All issues +issues.filter_type.all_pull_requests = All pull requests issues.filter_type.assigned_to_you = Assigned to you issues.filter_type.created_by_you = Created by you issues.filter_type.mentioning_you = Mentioning you @@ -1652,6 +1661,7 @@ issues.save = Save issues.label_title = Name issues.label_description = Description issues.label_color = Color +issues.label_color_invalid = Invalid color issues.label_exclusive = Exclusive issues.label_archive = Archive Label issues.label_archived_filter = Show archived labels @@ -1716,6 +1726,8 @@ issues.remove_time_estimate_at = removed time estimate %s issues.time_estimate_invalid = Time estimate format is invalid issues.start_tracking_history = started working %s issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed +issues.stopwatch_already_stopped = The timer for this issue is already stopped +issues.stopwatch_already_created = The timer for this issue already exists issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!` issues.stop_tracking = Stop Timer issues.stop_tracking_history = worked for <b>%[1]s</b> %[2]s @@ -1957,7 +1969,7 @@ pulls.cmd_instruction_checkout_title = Checkout pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes. pulls.cmd_instruction_merge_title = Merge pulls.cmd_instruction_merge_desc = Merge the changes and update on Gitea. -pulls.cmd_instruction_merge_warning = Warning: This operation can not merge pull request because "autodetect manual merge" was not enable +pulls.cmd_instruction_merge_warning = Warning: This operation cannot merge pull request because "autodetect manual merge" is not enabled. pulls.clear_merge_message = Clear merge message pulls.clear_merge_message_hint = Clearing the merge message will only remove the commit message content and keep generated git trailers such as "Co-Authored-By …". @@ -2153,6 +2165,7 @@ settings.collaboration.write = Write settings.collaboration.read = Read settings.collaboration.owner = Owner settings.collaboration.undefined = Undefined +settings.collaboration.per_unit = Unit Permissions settings.hooks = Webhooks settings.githooks = Git Hooks settings.basic_settings = Basic Settings @@ -2371,7 +2384,7 @@ settings.event_repository = Repository settings.event_repository_desc = Repository created or deleted. settings.event_header_issue = Issue Events settings.event_issues = Issues -settings.event_issues_desc = Issue opened, closed, reopened, or edited. +settings.event_issues_desc = Issue opened, closed, reopened, edited or deleted. settings.event_issue_assign = Issue Assigned settings.event_issue_assign_desc = Issue assigned or unassigned. settings.event_issue_label = Issue Labeled @@ -2382,7 +2395,7 @@ settings.event_issue_comment = Issue Comment settings.event_issue_comment_desc = Issue comment created, edited, or deleted. settings.event_header_pull_request = Pull Request Events settings.event_pull_request = Pull Request -settings.event_pull_request_desc = Pull request opened, closed, reopened, or edited. +settings.event_pull_request_desc = Pull request opened, closed, reopened, edited or deleted. settings.event_pull_request_assign = Pull Request Assigned settings.event_pull_request_assign_desc = Pull request assigned or unassigned. settings.event_pull_request_label = Pull Request Labeled @@ -2400,6 +2413,8 @@ settings.event_pull_request_review_request_desc = Pull request review requested settings.event_pull_request_approvals = Pull Request Approvals settings.event_pull_request_merge = Pull Request Merge settings.event_header_workflow = Workflow Events +settings.event_workflow_run = Workflow Run +settings.event_workflow_run_desc = Gitea Actions Workflow run queued, waiting, in progress, or completed. settings.event_workflow_job = Workflow Jobs settings.event_workflow_job_desc = Gitea Actions Workflow job queued, waiting, in progress, or completed. settings.event_package = Package @@ -2754,6 +2769,8 @@ branch.new_branch_from = Create new branch from "%s" branch.renamed = Branch %s was renamed to %s. branch.rename_default_or_protected_branch_error = Only admins can rename default or protected branches. branch.rename_protected_branch_failed = This branch is protected by glob-based protection rules. +branch.commits_divergence_from = Commits divergence: %[1]d behind and %[2]d ahead of %[3]s +branch.commits_no_divergence = The same as branch %[1]s tag.create_tag = Create tag %s tag.create_tag_operation = Create tag @@ -2767,6 +2784,7 @@ topic.done = Done topic.count_prompt = You cannot select more than 25 topics topic.format_prompt = Topics must start with a letter or number, can include dashes ('-') and dots ('.'), can be up to 35 characters long. Letters must be lowercase. +find_file.follow_symlink= Follow this symlink to where it is pointing at find_file.go_to_file = Go to file find_file.no_matching = No matching file found @@ -2807,6 +2825,7 @@ team_permission_desc = Permission team_unit_desc = Allow Access to Repository Sections team_unit_disabled = (Disabled) +form.name_been_taken = The organisation name "%s" has already been taken. form.name_reserved = The organization name "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name. form.create_org_not_allowed = You are not allowed to create an organization. @@ -2828,15 +2847,28 @@ settings.visibility.private_shortname = Private settings.update_settings = Update Settings settings.update_setting_success = Organization settings have been updated. -settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name. -settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed. + +settings.rename = Rename Organization +settings.rename_desc = Changing the organization name will also change your organization's URL and free the old name. +settings.rename_success = Organization %[1]s have been renamed to %[2]s successfully. +settings.rename_no_change = Organization name is no change. +settings.rename_new_org_name = New Organization Name +settings.rename_failed = Rename Organization failed because of internal error +settings.rename_notices_1 = This operation <strong>CANNOT</strong> be undone. +settings.rename_notices_2 = The old name will redirect until it is claimed. + settings.update_avatar_success = The organization's avatar has been updated. settings.delete = Delete Organization settings.delete_account = Delete This Organization settings.delete_prompt = The organization will be permanently removed. This <strong>CANNOT</strong> be undone! +settings.name_confirm = Enter the organization name as confirmation: +settings.delete_notices_1 = This operation <strong>CANNOT</strong> be undone. +settings.delete_notices_2 = This operation will permanently delete all the <strong>repositories</strong> of <strong>%s</strong> including code, issues, comments, wiki data and collaborator settings. +settings.delete_notices_3 = This operation will permanently delete all the <strong>packages</strong> of <strong>%s</strong>. +settings.delete_notices_4 = This operation will permanently delete all the <strong>projects</strong> of <strong>%s</strong>. settings.confirm_delete_account = Confirm Deletion -settings.delete_org_title = Delete Organization -settings.delete_org_desc = This organization will be deleted permanently. Continue? +settings.delete_failed = Delete Organization failed because of internal error +settings.delete_successful = Organization <b>%s</b> has been deleted successfully. settings.hooks_desc = Add webhooks which will be triggered for <strong>all repositories</strong> under this organization. settings.labels_desc = Add labels which can be used on issues for <strong>all repositories</strong> under this organization. @@ -3813,8 +3845,10 @@ runs.no_runs = The workflow has no runs yet. runs.empty_commit_message = (empty commit message) runs.expire_log_message = Logs have been purged because they were too old. runs.delete = Delete workflow run +runs.cancel = Cancel workflow run runs.delete.description = Are you sure you want to permanently delete this workflow run? This action cannot be undone. runs.not_done = This workflow run is not done. +runs.view_workflow_file = View workflow file workflow.disable = Disable Workflow workflow.disable_success = Workflow '%s' disabled successfully. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 521583395e..cd4f0fd9eb 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -1180,7 +1180,6 @@ editor.update=Actualizar %s editor.delete=Eliminar %s editor.patch=Aplicar parche editor.patching=Parcheando: -editor.fail_to_apply_patch=`No se puede aplicar el parche "%s"` editor.new_patch=Nuevo parche editor.commit_message_desc=Añadir una descripción extendida opcional… editor.signoff_desc=Añadir un trailer firmado por el committer al final del mensaje de registro de confirmación. @@ -1198,15 +1197,11 @@ editor.branch_already_exists=La rama "%s" ya existe en este repositorio. editor.directory_is_a_file=Nombre del directorio "%s" ya se utiliza como nombre de archivo en este repositorio. editor.file_is_a_symlink=`"%s" es un enlace simbólico. Los enlaces simbólicos no se pueden editar en el editor web` editor.filename_is_a_directory=Nombre de archivo "%s" ya se utiliza como nombre de directorio en este repositorio. -editor.file_editing_no_longer_exists=El archivo que se está editando, "%s", ya no existe en este repositorio. -editor.file_deleting_no_longer_exists=El archivo que se está eliminando, "%s", ya no existe en este repositorio. editor.file_changed_while_editing=Desde que comenzó a editar, el contenido del archivo ha sido cambiado. <a target="_blank" rel="noopener noreferrer" href="%s">Haga clic aquÃ</a> para ver qué ha cambiado o <strong>presione confirmar de nuevo</strong> para sobrescribir los cambios. editor.file_already_exists=Ya existe un archivo llamado "%s" en este repositorio. editor.commit_empty_file_header=Commit un archivo vacÃo editor.commit_empty_file_text=El archivo que estás tratando de commit está vacÃo. ¿Proceder? editor.no_changes_to_show=No existen cambios para mostrar. -editor.fail_to_update_file=Error al actualizar/crear el archivo "%s". -editor.fail_to_update_file_summary=Mensaje de error editor.push_rejected_no_message=El cambio fue rechazado por el servidor sin un mensaje. Por favor, compruebe Git Hooks. editor.push_rejected=El cambio fue rechazado por el servidor. Por favor, comprueba los Git Hooks. editor.push_rejected_summary=Mensaje completo de rechazo @@ -1221,6 +1216,7 @@ editor.require_signed_commit=Esta rama requiere un commit firmado editor.cherry_pick=Hacer Cherry-pick %s en: editor.revert=Revertir %s en: + commits.desc=Ver el historial de cambios de código fuente. commits.commits=Commits commits.no_commits=No hay commits en común. "%s" y "%s" tienen historias totalmente diferentes. @@ -2101,7 +2097,6 @@ settings.event_repository=Repositorio settings.event_repository_desc=Repositorio creado o eliminado. settings.event_header_issue=Eventos de incidencias settings.event_issues=Incidencias -settings.event_issues_desc=Incidencia abierta, cerrada, reabierta o editada. settings.event_issue_assign=Incidencia asignada settings.event_issue_assign_desc=Incidencia asignada o no asignada. settings.event_issue_label=Incidencia etiquetada @@ -2112,7 +2107,6 @@ settings.event_issue_comment=Comentario de incidencia settings.event_issue_comment_desc=Comentario de incidencias creado, editado o borrado. settings.event_header_pull_request=Eventos de Pull Requests settings.event_pull_request=Pull Request -settings.event_pull_request_desc=Pull request abierto, cerrado, reabierto o editado. settings.event_pull_request_assign=Pull Request asignado settings.event_pull_request_assign_desc=Pull Request asignado o no asignado. settings.event_pull_request_label=Pull Request Etiquetado @@ -2487,15 +2481,13 @@ settings.visibility.private_shortname=Privado settings.update_settings=Actualizar configuración settings.update_setting_success=Configuración de la organización se han actualizado. -settings.change_orgname_prompt=Nota: Cambiar el nombre de la organización también cambiará la URL de su organización y liberará el nombre antiguo. -settings.change_orgname_redirect_prompt=El nombre antiguo se redirigirá hasta que se reclame. + + settings.update_avatar_success=Se ha actualizado el avatar de la organización. settings.delete=Eliminar organización settings.delete_account=Eliminar esta organización settings.delete_prompt=La organización será eliminada permanentemente. ¡Esta acción <strong>NO PUEDE</strong> deshacerse! settings.confirm_delete_account=Confirmar eliminación -settings.delete_org_title=Eliminar organización -settings.delete_org_desc=Esta organización se eliminará permanentemente. ¿Continuar? settings.hooks_desc=Añadir webhooks que serán ejecutados para <strong>todos los repositorios</strong> de esta organización. settings.labels_desc=Añadir etiquetas que pueden ser utilizadas en problemas para <strong>todos los repositorios</strong> bajo esta organización. diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 18abc0f401..aee09ac278 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -943,13 +943,13 @@ editor.file_changed_while_editing=Ù…ØØªÙˆØ§ÛŒ پرونده تغییر Ù…ÛŒÚ©Ù†Ø editor.commit_empty_file_header=کامیت کردن یک پرونده خالی editor.commit_empty_file_text=ÙØ§ÛŒÙ„ÛŒ Ú©Ù‡ درخواست ارسال دارید خالی است. ادامه بدم? editor.no_changes_to_show=تغییری برای نمایش وجود ندارد. -editor.fail_to_update_file_summary=متن خطا: editor.push_rejected_summary=متن کامل پیام دلیل رد شدن: editor.add_subdir=Ø§ÙØ²ÙˆØ¯Ù† پوشه… editor.no_commit_to_branch=نمی‌توان به طور مستقیم درمورد شاخه نطر داد زیرا: editor.user_no_push_to_branch=کاربر نمیتواند به شاخه ارسال کند editor.require_signed_commit=شاخه یک کامیت امضا شده لازم دارد + commits.desc=تاریخچه تغییرات کد منبع را مرور کنید. commits.commits=کامیت‌ها commits.nothing_to_compare=این شاخه ها برابرند. @@ -1632,7 +1632,6 @@ settings.event_repository=مخزن settings.event_repository_desc=مخزن ساخته یا ØØ°Ù شد. settings.event_header_issue=رویدادهای مساله settings.event_issues=مسائل -settings.event_issues_desc=مساله باز شد، بسته شد، دوباره باز شد، یا ویرایش شد. settings.event_issue_assign=مساله تعیین شد settings.event_issue_assign_desc=مساله واگذار شده یا واگذار نشده است. settings.event_issue_label=مساله برجسب خورد @@ -1643,7 +1642,6 @@ settings.event_issue_comment=دیدگاه های مسئله settings.event_issue_comment_desc=نظر در مسئله ایجاد شد، ویرایش شد یا ØØ°Ù شد. settings.event_header_pull_request=رویداد های درخواست pull settings.event_pull_request=تقاضای واکشی -settings.event_pull_request_desc=درخواست pull باز شد، بسته شد، دوباره باز شد، یا ویرایش شد. settings.event_pull_request_assign=درخواست pull واگذار شد settings.event_pull_request_assign_desc=درخواست pull واگذاری شده یا واگذاری نشده. settings.event_pull_request_label=درخواست pull برچسب دار شد @@ -1920,14 +1918,13 @@ settings.visibility.private_shortname=پوشیده settings.update_settings=به‌ روزرسانی تنظیمات settings.update_setting_success=تنظیمات این سازمان به‌روز شد. -settings.change_orgname_redirect_prompt=نام قدیمی تا زمانی Ú©Ù‡ ادعا شود تغییر مسیر Ù…ÛŒ دهد. + + settings.update_avatar_success=آواتار این سازمان به‌روز شد. settings.delete=ØØ°Ù سازمان settings.delete_account=ØØ°Ù این سازمان settings.delete_prompt=سازمان برای همیشه ØØ°Ù خواهد شد. این قابل برگشت <strong>نخواهد بود</strong>! settings.confirm_delete_account=تاییدیه ØØ°Ù -settings.delete_org_title=ØØ°Ù سازمان -settings.delete_org_desc=سازمان برای همیشه ØØ°Ù خواهد شد. آیا همچنان ادامه می‌دهید؟ settings.hooks_desc=Ø§ÙØ²ÙˆØ¯Ù† webhook های Ú©Ù‡ برای<strong> تمام مخازن</strong> این سازمان اجرا میشود. settings.labels_desc=تگ هایی را اضاÙÙ‡ کنید Ú©Ù‡ می‌توانند برای مشکلات <strong>همه مخازن</strong> ØªØØª این سازمان Ø§Ø³ØªÙØ§Ø¯Ù‡ شوند. diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index b925d6f43a..3b4033b8b3 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -764,6 +764,7 @@ editor.no_changes_to_show=Ei muutoksia näytettäväksi. editor.add_subdir=Lisää hakemisto… editor.require_signed_commit=Haara vaatii vahvistetun commitin + commits.commits=Commitit commits.nothing_to_compare=Nämä haarat vastaavat toisiaan. commits.search_all=Kaikki haarat @@ -1128,7 +1129,6 @@ settings.event_repository=Repo settings.event_repository_desc=Repo luotu tai poistettu. settings.event_header_issue=Ongelmien tapahtumat settings.event_issues=Ongelmat -settings.event_issues_desc=Ongelma avattu, suljettu, avattu uudelleen tai muokattu. settings.event_issue_assign=Ongelma määritetty settings.event_issue_assign_desc=Ongelma osoitettu tai osoitus poistettu. settings.event_issue_label_desc=Ongelman tunnisteet päivitetty tai tyhjennetty. @@ -1318,11 +1318,12 @@ settings.visibility.private=Yksityinen (näkyvä vain organisaation jäsenille) settings.visibility.private_shortname=Yksityinen settings.update_settings=Päivitä asetukset + + settings.delete=Poista organisaatio settings.delete_account=Poista tämä organisaatio settings.delete_prompt=Organisaatio poistetaan pysyvästi, ja tätä <strong>EI VOI</strong> peruuttaa myöhemmin! settings.confirm_delete_account=Vahvista poisto -settings.delete_org_title=Poista organisaatio settings.hooks_desc=Lisää webkoukkuja, jotka suoritetaan <strong>kaikissa repoissa</strong> tässä organisaatiossa. diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 16fe024968..03614832e9 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -421,6 +421,7 @@ remember_me.compromised=Le jeton de connexion n’est plus valide, ce qui peut i forgot_password_title=Mot de passe oublié forgot_password=Mot de passe oublié ? need_account=Besoin d‘un compte ? +sign_up_tip=Vous êtes en train d’enregistrer le premier compte du système, doté des privilèges administrateur. Veuillez conserver précieusement ses nom d’utilisateur et mot de passe. En cas d’oublie, consultez la documentation de Gitea pour en récupérer l’accès. sign_up_now=Inscrivez-vous dès maintenant ! sign_up_successful=Le compte a été créé avec succès. Bienvenue ! confirmation_mail_sent_prompt_ex=Un nouveau courriel de confirmation a été envoyé à <b>%s</b>. Veuillez vérifier votre boîte de réception dans la prochaine %s pour terminer le processus d’inscription. Si votre adresse courriel est incorrecte, vous pouvez vous reconnecter et la modifier. @@ -1228,6 +1229,7 @@ migrate.migrating_issues=Migration des tickets migrate.migrating_pulls=Migration des demandes d'ajout migrate.cancel_migrating_title=Annuler la migration migrate.cancel_migrating_confirm=Voulez-vous abandonner cette migration ? +migration_status=Statut de la migration mirror_from=miroir de forked_from=bifurqué depuis @@ -1331,7 +1333,9 @@ editor.upload_file=Téléverser un fichier editor.edit_file=Modifier le fichier editor.preview_changes=Aperçu des modifications editor.cannot_edit_lfs_files=Les fichiers LFS ne peuvent pas être modifiés dans l'interface web. +editor.cannot_edit_too_large_file=Le fichier est trop gros pour être édité. editor.cannot_edit_non_text_files=Les fichiers binaires ne peuvent pas être édités dans l'interface web. +editor.file_not_editable_hint=Mais vous pouvez toujours le renommer ou le déplacer. editor.edit_this_file=Modifier le fichier editor.this_file_locked=Le fichier est verrouillé editor.must_be_on_a_branch=Vous devez être sur une branche pour appliquer ou proposer des modifications à ce fichier. @@ -1343,7 +1347,7 @@ editor.name_your_file=Nommez votre fichier… editor.filename_help=Ajoutez un dossier en entrant son nom suivi d'une barre oblique ('/'). Supprimez un dossier avec un retour arrière au début du champ. editor.or=ou editor.cancel_lower=Annuler -editor.commit_signed_changes=Réviser les changements (signé) +editor.commit_signed_changes=Réviser les changements signés editor.commit_changes=Réviser les changements editor.add_tmpl=Ajouter {filename} editor.add=Ajouter %s @@ -1351,7 +1355,7 @@ editor.update=Actualiser %s editor.delete=Supprimer %s editor.patch=Appliquer le correctif editor.patching=Correction: -editor.fail_to_apply_patch=`Impossible d'appliquer le correctif "%s"` +editor.fail_to_apply_patch=Impossible d’appliquer le correctif. editor.new_patch=Nouveau correctif editor.commit_message_desc=Ajouter une description détaillée facultative… editor.signoff_desc=Créditer l'auteur "Signed-off-by:" en pied de révision. @@ -1371,8 +1375,7 @@ editor.branch_already_exists=La branche "%s" existe déjà dans ce dépôt. editor.directory_is_a_file=Le nom de dossier "%s" est déjà utilisé comme nom de fichier dans ce dépôt. editor.file_is_a_symlink=`« %s » est un lien symbolique. Ce type de fichiers ne peut être modifié dans l'éditeur web.` editor.filename_is_a_directory=« %s » est déjà utilisé comme nom de dossier dans ce dépôt. -editor.file_editing_no_longer_exists=Impossible de modifier le fichier « %s » car il n’existe plus dans ce dépôt. -editor.file_deleting_no_longer_exists=Impossible de supprimer le fichier « %s » car il n’existe plus dans ce dépôt. +editor.file_modifying_no_longer_exists=Le fichier en cours d’édition, '%s', n’existe plus dans ce dépôt. editor.file_changed_while_editing=Le contenu du fichier a changé depuis que vous avez commencé à éditer. <a target="_blank" rel="noopener noreferrer" href="%s">Cliquez ici</a> pour voir les changements ou <strong>soumettez de nouveau</strong> pour les écraser. editor.file_already_exists=Un fichier nommé "%s" existe déjà dans ce dépôt. editor.commit_id_not_matching=L’ID de la révision ne correspond pas à l’ID lorsque vous avez commencé à éditer. Faites une révision dans une branche de correctif puis fusionnez. @@ -1380,8 +1383,6 @@ editor.push_out_of_date=Cet envoi semble être obsolète. editor.commit_empty_file_header=Réviser un fichier vide editor.commit_empty_file_text=Le fichier que vous allez réviser est vide. Continuer ? editor.no_changes_to_show=Il n’y a aucune modification à afficher. -editor.fail_to_update_file=Impossible de mettre à jour/créer le fichier "%s". -editor.fail_to_update_file_summary=Message d'erreur : editor.push_rejected_no_message=La modification a été rejetée par le serveur sans message. Veuillez vérifier les Git Hooks. editor.push_rejected=La modification a été rejetée par le serveur. Veuillez vérifier vos Git Hooks. editor.push_rejected_summary=Message de rejet complet : @@ -1395,6 +1396,15 @@ editor.user_no_push_to_branch=L'utilisateur ne peut pas pousser vers la branche editor.require_signed_commit=Cette branche nécessite une révision signée editor.cherry_pick=Picorer %s vers: editor.revert=Rétablir %s sur: +editor.failed_to_commit=Impossible de réviser les modifications. +editor.failed_to_commit_summary=Message d’erreur : + +editor.fork_create=Bifurquer le Dépôt pour proposer vos modifications +editor.fork_create_description=Vous ne pouvez pas modifier ce dépôt directement. Cependant, vous pouvez bifurquer ce dépôt, et créer une demande d’ajout avec vos contributions. +editor.fork_edit_description=Vous ne pouvez pas modifier ce dépôt directement. Les modifications seront écrites sur une bifurcation <b>%s</b>, et ainsi faire une demande d’ajout. +editor.fork_not_editable=Vous avez bifurqué ce dépôt mais votre copie n’est pas modifiable. +editor.fork_failed_to_push_branch=Impossible de pousser la branche %s vers votre dépôt. +editor.fork_branch_exists=La branche « %s » existe déjà dans votre bifurcation, veuillez choisir un nouveau nom de branche. commits.desc=Naviguer dans l'historique des modifications. commits.commits=Révisions @@ -1559,6 +1569,7 @@ issues.filter_user_placeholder=Rechercher des utilisateurs issues.filter_user_no_select=Tous les utilisateurs issues.filter_type=Type issues.filter_type.all_issues=Tous les tickets +issues.filter_type.all_pull_requests=Toutes les demandes d’ajout issues.filter_type.assigned_to_you=Qui vous sont assignés issues.filter_type.created_by_you=Créés par vous issues.filter_type.mentioning_you=Vous mentionnant @@ -1650,6 +1661,7 @@ issues.save=Enregistrer issues.label_title=Nom du label issues.label_description=Description du label issues.label_color=Couleur du label +issues.label_color_invalid=Couleur invalide issues.label_exclusive=Exclusif issues.label_archive=Archivé issues.label_archived_filter=Afficher les labels archivés @@ -1714,6 +1726,8 @@ issues.remove_time_estimate_at=a supprimé le temps estimé %s issues.time_estimate_invalid=Le format du temps estimé est invalide issues.start_tracking_history=`a commencé son travail %s.` issues.tracker_auto_close=Le minuteur sera automatiquement arrêté quand le ticket sera fermé. +issues.stopwatch_already_stopped=Le minuteur pour ce ticket est déjà arrêtée. +issues.stopwatch_already_created=Le minuteur pour ce ticket existe déjà . issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur <a href="%s">un autre ticket</a> !` issues.stop_tracking=Arrêter le minuteur issues.stop_tracking_history=a travaillé <b>%[1]s</b> %[2]s @@ -2151,6 +2165,7 @@ settings.collaboration.write=Écriture settings.collaboration.read=Lecture settings.collaboration.owner=Propriétaire settings.collaboration.undefined=Indéfini +settings.collaboration.per_unit=Permissions de ressource settings.hooks=Webhooks settings.githooks=Déclencheurs Git settings.basic_settings=Paramètres de base @@ -2369,7 +2384,7 @@ settings.event_repository=Dépôt settings.event_repository_desc=Dépôt créé ou supprimé. settings.event_header_issue=Événements de ticket settings.event_issues=Ticket -settings.event_issues_desc=Ticket ouvert, rouvert, fermé ou modifié. +settings.event_issues_desc=Ticket ouvert, rouvert, fermé, modifié ou supprimé. settings.event_issue_assign=Assignation settings.event_issue_assign_desc=Ticket assigné ou dé-assigné. settings.event_issue_label=Labellisation @@ -2380,7 +2395,7 @@ settings.event_issue_comment=Commentaire settings.event_issue_comment_desc=Commentaire créé, modifié ou supprimé. settings.event_header_pull_request=Événements de demande d'ajout settings.event_pull_request=Demande d'ajout -settings.event_pull_request_desc=Demande d’ajout ouverte, rouverte, fermée ou modifiée. +settings.event_pull_request_desc=Demande d’ajout ouverte, rouverte, fermée, modifiée ou supprimée. settings.event_pull_request_assign=Assignation settings.event_pull_request_assign_desc=Demande d'ajout assignée ou non assignée. settings.event_pull_request_label=Labellisation @@ -2398,8 +2413,10 @@ settings.event_pull_request_review_request_desc=Création ou suppresion de deman settings.event_pull_request_approvals=Approbations de demande d'ajout settings.event_pull_request_merge=Fusion de demande d'ajout settings.event_header_workflow=Événements du flux de travail +settings.event_workflow_run=Exécution du flux de travail +settings.event_workflow_run_desc=Tâche du flux de travail Gitea Actions ajoutée, en attente, en cours ou terminée. settings.event_workflow_job=Tâches du flux de travail -settings.event_workflow_job_desc=Tâche du flux de travail Gitea Actions en file d’attente, en attente, en cours ou terminée. +settings.event_workflow_job_desc=Travaux du flux de travail Gitea Actions en file d’attente, en attente, en cours ou terminée. settings.event_package=Paquet settings.event_package_desc=Paquet créé ou supprimé. settings.branch_filter=Filtre de branche @@ -2752,6 +2769,8 @@ branch.new_branch_from=`Créer une nouvelle branche à partir de "%s"` branch.renamed=La branche %s à été renommée en %s. branch.rename_default_or_protected_branch_error=Seuls les administrateurs peuvent renommer les branches par défaut ou protégées. branch.rename_protected_branch_failed=Cette branche est protégée par des règles de protection basées sur des globs. +branch.commits_divergence_from=Divergence de révisions : %[1]d en retard et %[2]d en avance sur %[3]s +branch.commits_no_divergence=Identique à la branche %[1]s tag.create_tag=Créer l'étiquette %s tag.create_tag_operation=Créer une étiquette @@ -2805,6 +2824,7 @@ team_permission_desc=Autorisation team_unit_desc=Permettre l’accès aux Sections du dépôt team_unit_disabled=(Désactivé) +form.name_been_taken=Le nom d’organisation « %s » a déjà été utilisé. form.name_reserved=Le nom d'organisation "%s" est réservé. form.name_pattern_not_allowed=Le motif « %s » n'est pas autorisé dans un nom d'organisation. form.create_org_not_allowed=Vous n'êtes pas autorisé à créer une organisation. @@ -2826,15 +2846,28 @@ settings.visibility.private_shortname=Privé settings.update_settings=Appliquer les paramètres settings.update_setting_success=Les paramètres de l'organisation ont été mis à jour. -settings.change_orgname_prompt=Remarque : Changer le nom de l'organisation changera également l'URL de votre organisation et libèrera l'ancien nom. -settings.change_orgname_redirect_prompt=L'ancien nom d'utilisateur redirigera jusqu'à ce qu'il soit réclamé. + +settings.rename=Renommer l’organisation +settings.rename_desc=Changer le nom de l’organisation changera également l’URL de votre organisation et libèrera l’ancien nom. +settings.rename_success=L’organisation %[1]s a bien été renommé en %[2]s. +settings.rename_no_change=Le nom de l’organisation n’a pas été modifié. +settings.rename_new_org_name=Nouveau nom d’organisation +settings.rename_failed=Le renommage de l’organisation a échoué en raison d’une erreur interne. +settings.rename_notices_1=Cette opération <strong>ne peut pas </strong> être annulée. +settings.rename_notices_2=L’ancien nom redirigera jusqu'à ce qu'il soit réclamé. + settings.update_avatar_success=L'avatar de l'organisation a été mis à jour. settings.delete=Supprimer l'organisation settings.delete_account=Supprimer cette organisation settings.delete_prompt=Cette organisation sera supprimée définitivement. Cette action est <strong>IRRÉVERSIBLE</strong> ! +settings.name_confirm=Entrez le nom de l’organisation pour confirmer : +settings.delete_notices_1=Cette opération <strong>ne peut pas </strong> être annulée. +settings.delete_notices_2=Cette opération supprimera définitivement <strong>tous les dépôts</strong> de <strong>%s</strong>, y compris le code, les tickets, les commentaires, les données de wiki et les accès des collaborateurs. +settings.delete_notices_3=Cette opération supprimera définitivement <strong>tous les paquets</strong> de <strong>%s</strong>. +settings.delete_notices_4=Cette opération supprimera définitivement <strong>tous les projets</strong> de <strong>%s</strong>. settings.confirm_delete_account=Confirmer la suppression -settings.delete_org_title=Supprimer l'organisation -settings.delete_org_desc=Cette organisation sera supprimée définitivement. Voulez-vous continuer ? +settings.delete_failed=La suppression de l’organisation a échoué en raison d’une erreur interne. +settings.delete_successful=L’organisation <b>%s</b> a été supprimée avec succès. settings.hooks_desc=Vous pouvez ajouter des webhooks qui seront activés pour <strong>tous les dépôts</strong> de cette organisation. settings.labels_desc=Ajoute des labels qui peuvent être utilisés sur les tickets pour <strong>tous les dépôts</strong> de cette organisation. @@ -3430,12 +3463,12 @@ monitor.queue.type=Type monitor.queue.exemplar=Type d'exemple monitor.queue.numberworkers=Nombre de processus monitor.queue.activeworkers=Processus actifs -monitor.queue.maxnumberworkers=Nombre maximale de processus +monitor.queue.maxnumberworkers=Nombre maximal de processus monitor.queue.numberinqueue=Position dans la queue monitor.queue.review_add=Examiner / ajouter des processus monitor.queue.settings.title=Paramètres du réservoir monitor.queue.settings.desc=Les bassins croissent proportionnellement au besoin de leurs exécuteurs. -monitor.queue.settings.maxnumberworkers=Nombre maximale de processus +monitor.queue.settings.maxnumberworkers=Nombre maximal de processus monitor.queue.settings.maxnumberworkers.placeholder=Actuellement %[1]d monitor.queue.settings.maxnumberworkers.error=Le nombre de processus doit être un nombre monitor.queue.settings.submit=Appliquer les paramètres @@ -3811,8 +3844,10 @@ runs.no_runs=Le flux de travail n'a pas encore d'exécution. runs.empty_commit_message=(message de révision vide) runs.expire_log_message=Les journaux ont été supprimés car ils étaient trop anciens. runs.delete=Supprimer cette exécution +runs.cancel=Annuler l’exécution du flux runs.delete.description=Êtes-vous sûr de vouloir supprimer définitivement cette exécution ? Cette action ne peut pas être annulée. runs.not_done=Cette exécution du flux de travail n’est pas terminée. +runs.view_workflow_file=Voir le fichier du flux de travail workflow.disable=Désactiver le flux de travail workflow.disable_success=Le flux de travail « %s » a bien été désactivé. diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 362ebbd999..7c7cb58dcf 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -421,6 +421,7 @@ remember_me.compromised=NÃl an comhartha logála isteach bailà nÃos mó a d'f forgot_password_title=Dearmad ar an bPasfhocal forgot_password=Dearmad ar an bPasfhocal? need_account=An bhfuil cuntas ag teastáil uait? +sign_up_tip=Tá tú ag clárú an chéad chuntais sa chóras, a bhfuil pribhléidà riarthóra aige. Cuimhnigh go cúramach ar d’ainm úsáideora agus do phasfhocal. Má dhéanann tú dearmad ar an ainm úsáideora nó ar an pasfhocal, féach ar dhoiciméadacht Gitea le do thoil chun an cuntas a aisghabháil. sign_up_now=Cláraigh anois. sign_up_successful=CruthaÃodh cuntas go rathúil. Fáilte romhat! confirmation_mail_sent_prompt_ex=Tá rÃomhphost dearbhaithe nua seolta chuig <b>%s</b>. Seiceáil do bhosca isteach laistigh den chéad %s eile chun an próiseas clárúcháin a chur i gcrÃch. Má tá do sheoladh rÃomhphoist clárúcháin mÃcheart, is féidir leat sÃniú isteach arÃs agus é a athrú. @@ -1228,7 +1229,7 @@ migrate.migrating_issues=Saincheisteanna Imirce migrate.migrating_pulls=Iarratais Tarraingthe á n-Imirce migrate.cancel_migrating_title=Cealaigh Imirce migrate.cancel_migrating_confirm=Ar mhaith leat an imirce seo a chealú? -migrating_status=Stádas imirce +migration_status=Stádas imirce mirror_from=scáthán de forked_from=forcailte ó @@ -1332,7 +1333,9 @@ editor.upload_file=Uaslódáil Comhad editor.edit_file=Cuir Comhad in eagar editor.preview_changes=Athruithe Réamhamhar editor.cannot_edit_lfs_files=Nà féidir comhaid LFS a chur in eagar sa chomhéadan gréasáin. +editor.cannot_edit_too_large_file=Tá an comhad rómhór le cur in eagar. editor.cannot_edit_non_text_files=Nà féidir comhaid dhénártha a chur in eagar sa chomhéadan gréasáin. +editor.file_not_editable_hint=Ach is féidir leat é a athainmniú nó a bhogadh fós. editor.edit_this_file=Cuir Comhad in eagar editor.this_file_locked=Tá an comhad faoi ghlas editor.must_be_on_a_branch=Caithfidh tú a bheith ar bhrainse chun athruithe a dhéanamh nó a mholadh ar an gcomhad seo. @@ -1352,7 +1355,7 @@ editor.update=Nuashonraigh %s editor.delete=Scrios %s editor.patch=Cuir paiste i bhfeidh editor.patching=Paisteáil: -editor.fail_to_apply_patch=Nà féidir paiste "%s" a chur i bhfeidhm +editor.fail_to_apply_patch=Nà féidir an paiste a chur i bhfeidhm editor.new_patch=Paiste Nua editor.commit_message_desc=Cuir cur sÃos leathnaithe roghnach leis… editor.signoff_desc=Cuir leantóir sÃnithe ag an gcoiteoir ag deireadh na teachtaireachta logála tiomanta. @@ -1372,8 +1375,7 @@ editor.branch_already_exists=Tá brainse "%s" ann cheana féin sa stóras seo. editor.directory_is_a_file=Úsáidtear ainm eolaire "%s" cheana féin mar ainm comhaid sa stóras seo. editor.file_is_a_symlink=Is nasc siombalach é "%s". Nà féidir naisc shiombalacha a chur in eagar san eagarthóir gréasáin editor.filename_is_a_directory=Úsáidtear ainm comhaid "%s" cheana féin mar ainm eolaire sa stóras seo. -editor.file_editing_no_longer_exists=NÃl an comhad atá á chur in eagar, "%s", ann sa stóras seo a thuilleadh. -editor.file_deleting_no_longer_exists=NÃl an comhad atá á scriosadh, "%s", ann sa stóras seo a thuilleadh. +editor.file_modifying_no_longer_exists=NÃl an comhad atá á mhodhnú, "%s", sa stóras seo a thuilleadh. editor.file_changed_while_editing=Tá athrú tagtha ar ábhar an chomhad ó thosaigh tú ag eagarthóireacht <a target="_blank" rel="noopener noreferrer" href="%s">Cliceáil anseo</a> chun iad a fheiceáil nó Athru <strong>ithe a Tiomantas arÃs</strong> chun iad a fhorscrÃobh. editor.file_already_exists=Tá comhad darb ainm "%s" ann cheana féin sa stóras seo. editor.commit_id_not_matching=Nà mheaitseálann an ID Tiomanta leis an ID nuair a thosaigh tú ag eagarthóireacht. Tiomanta isteach i mbrainse paiste agus ansin cumaisc. @@ -1381,8 +1383,6 @@ editor.push_out_of_date=Is cosúil go bhfuil an brú as dáta. editor.commit_empty_file_header=Tiomantas comhad folamh editor.commit_empty_file_text=Tá an comhad atá tú ar tà tiomantas folamh. Ar aghaidh? editor.no_changes_to_show=NÃl aon athruithe le taispeáint. -editor.fail_to_update_file=Theip ar nuashonrú/cruthú comhad "%s". -editor.fail_to_update_file_summary=Teachtaireacht Earráide: editor.push_rejected_no_message=Dhiúltaigh an freastalaà an t-athrú gan teachtaireacht. Seiceáil Git Hooks le do thoil. editor.push_rejected=Dhiúltaigh an freastalaà an t-athrú. Seiceáil Git Hooks le do thoil. editor.push_rejected_summary=Teachtaireacht Diúltaithe Iomlán: @@ -1396,6 +1396,15 @@ editor.user_no_push_to_branch=Nà féidir leis an úsáideoir brúigh go dtà an editor.require_signed_commit=ÉilÃonn an Brainse tiomantas sÃnithe editor.cherry_pick=Roghnaigh silÃnà %s ar: editor.revert=Fill %s ar: +editor.failed_to_commit=Theip ar athruithe a chur i bhfeidhm. +editor.failed_to_commit_summary=Teachtaireacht Earráide: + +editor.fork_create=Stóras Forc chun Athruithe a Mholadh +editor.fork_create_description=Nà féidir leat an stóras seo a chur in eagar go dÃreach. Ina áit sin, is féidir leat forc a chruthú, eagarthóireachtaà a dhéanamh agus iarratas tarraingthe a chruthú. +editor.fork_edit_description=Nà féidir leat an stóras seo a chur in eagar go dÃreach. ScrÃobhfar na hathruithe chuig do fhorc <b>%s</b>, ionas gur féidir leat iarratas tarraingthe a chruthú. +editor.fork_not_editable=Tá forc déanta agat ar an stóras seo ach nà féidir do fhorc a chur in eagar. +editor.fork_failed_to_push_branch=Theip ar bhrainse %s a bhrú chuig do stóras. +editor.fork_branch_exists=Tá brainse "%s" ann cheana féin i do fhorc, roghnaigh ainm brainse nua le do thoil. commits.desc=Brabhsáil stair athraithe cód foinse. commits.commits=Tiomáintà @@ -1560,6 +1569,7 @@ issues.filter_user_placeholder=Cuardaigh úsáideoirà issues.filter_user_no_select=Gach úsáideoir issues.filter_type=Cineál issues.filter_type.all_issues=Gach saincheist +issues.filter_type.all_pull_requests=Gach iarratas tarraingt issues.filter_type.assigned_to_you=Sannta duit issues.filter_type.created_by_you=Cruthaithe agat issues.filter_type.mentioning_you=Ag tagairt duit @@ -1651,6 +1661,7 @@ issues.save=Sábháil issues.label_title=Ainm issues.label_description=Cur sÃos issues.label_color=Dath +issues.label_color_invalid=Dath neamhbhailà issues.label_exclusive=Eisiach issues.label_archive=Lipéad Cartlann issues.label_archived_filter=Taispeáin lipéid cartlainne @@ -1715,6 +1726,8 @@ issues.remove_time_estimate_at=baineadh meastachán ama %s issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailà issues.start_tracking_history=thosaigh ag obair %s issues.tracker_auto_close=Stopfar ama go huathoibrÃoch nuair a dhúnfar an tsaincheist seo +issues.stopwatch_already_stopped=Tá an lasc ama don cheist seo stoptha cheana féin +issues.stopwatch_already_created=Tá an lasc ama don cheist seo ann cheana féin issues.tracking_already_started=`Tá tús curtha agat cheana féin ag rianú ama ar <a href="%s">eagrán eile</a>!` issues.stop_tracking=Stad uaineadóir issues.stop_tracking_history=d'oibrigh do <b>%[1]s</b> %[2]s @@ -1956,7 +1969,7 @@ pulls.cmd_instruction_checkout_title=Seiceáil pulls.cmd_instruction_checkout_desc=Ó stór tionscadail, seiceáil brainse nua agus déan tástáil ar na hathruithe. pulls.cmd_instruction_merge_title=Cumaisc pulls.cmd_instruction_merge_desc=Cumaisc na hathruithe agus nuashonrú ar Gitea. -pulls.cmd_instruction_merge_warning=Rabhadh: Nà féidir leis an oibrÃocht seo an t-iarratas tarraingthe a chumasc toisc nach raibh "cumasc láimhe uathoibrÃoch" cumasaithe +pulls.cmd_instruction_merge_warning=Rabhadh: Nà féidir iarratas tarraingthe cumaisc a dhéanamh leis an oibrÃocht seo mar nach bhfuil "autodetect manual merge" cumasaithe. pulls.clear_merge_message=Glan an teachtaireacht chumaisc pulls.clear_merge_message_hint=Má imrÃtear an teachtaireacht chumaisc nà bhainfear ach ábhar na teachtaireachta tiomanta agus coimeádfar leantóirà git ginte ar nós "Co-Authored-By …". @@ -2152,6 +2165,7 @@ settings.collaboration.write=ScrÃobh settings.collaboration.read=Léigh settings.collaboration.owner=Úinéir settings.collaboration.undefined=Neamhshainithe +settings.collaboration.per_unit=Ceadanna Aonaid settings.hooks=Crúcaà Gréasán settings.githooks=Crúcanna Git settings.basic_settings=Socruithe Bunúsacha @@ -2370,7 +2384,7 @@ settings.event_repository=Stóras settings.event_repository_desc=Stóras a cruthaÃodh nó a scriosadh. settings.event_header_issue=Imeachtaà Eisiúint settings.event_issues=Saincheisteanna -settings.event_issues_desc=OsclaÃodh, dúnadh, athosclaÃodh nó cuireadh an cheist in eagar. +settings.event_issues_desc=Ceist oscailte, dúnta, athoscailte, eagarthóireachta nó scriosta. settings.event_issue_assign=Saincheist Sannaithe settings.event_issue_assign_desc=Eisiúint sannta nó neamhshannta. settings.event_issue_label=Eisiúint Lipéadaithe @@ -2381,7 +2395,7 @@ settings.event_issue_comment=Trácht Eisiúna settings.event_issue_comment_desc=Trácht eisiúna cruthaithe, curtha in eagar nó a scriosadh. settings.event_header_pull_request=Tarraingt Imeachtaà Iarratas settings.event_pull_request=Iarratas Tarraingthe -settings.event_pull_request_desc=Iarratas tarraingthe oscailte, dúnta, athoscailte nó curtha in eagar. +settings.event_pull_request_desc=Iarratas tarraingthe a osclaÃodh, a dúnadh, a hathosclaÃodh, a cuireadh in eagar nó a scriosadh. settings.event_pull_request_assign=Iarratas Tarraingthe Sannta settings.event_pull_request_assign_desc=Iarratas tarraingthe sannta nó neamhshannta. settings.event_pull_request_label=Iarratas Tarraingthe Lipéadaithe @@ -2399,6 +2413,8 @@ settings.event_pull_request_review_request_desc=Tarraing athbhreithniú iarratai settings.event_pull_request_approvals=Ceaduithe Iarratais Tarraing settings.event_pull_request_merge=Cumaisc Iarratas Tarraing settings.event_header_workflow=Imeachtaà Sreabhadh Oibre +settings.event_workflow_run=Rith Sreabhadh Oibre +settings.event_workflow_run_desc=Tá rith Sreabhadh Oibre GnÃomhartha Gitea sa scuaine, ag fanacht, ar siúl, nó crÃochnaithe. settings.event_workflow_job=Poist Sreabhadh Oibre settings.event_workflow_job_desc=Gitea Actions Sreabhadh oibre post ciúáilte, ag fanacht, ar siúl, nó crÃochnaithe. settings.event_package=Pacáiste @@ -2753,6 +2769,8 @@ branch.new_branch_from=`Cruthaigh brainse nua ó "%s"` branch.renamed=AinmnÃodh brainse %s go %s. branch.rename_default_or_protected_branch_error=Nà féidir ach le riarthóirà brainsà réamhshocraithe nó cosanta a athainmniú. branch.rename_protected_branch_failed=Tá an brainse seo faoi chosaint ag rialacha cosanta domhanda. +branch.commits_divergence_from=Déanann sé dialltacht a thiomnú: %[1]d taobh thiar agus %[2]d chun tosaigh ar %[3]s +branch.commits_no_divergence=Mar an gcéanna le brainse %[1]s tag.create_tag=Cruthaigh clib %s tag.create_tag_operation=Cruthaigh clib @@ -2766,6 +2784,7 @@ topic.done=Déanta topic.count_prompt=Nà féidir leat nÃos mó ná 25 topaicà a roghnú topic.format_prompt=Nà mór do thopaicà tosú le litir nó uimhir, is féidir daiseanna ('-') agus poncanna ('.') a áireamh, a bheith suas le 35 carachtar ar fad. Nà mór litreacha a bheith i litreacha beaga. +find_file.follow_symlink=Lean an nasc siombalach seo go dtà an áit a bhfuil sé ag pointeáil air find_file.go_to_file=Téigh go dtà an comhad find_file.no_matching=NÃl aon chomhad meaitseála le fáil @@ -2806,6 +2825,7 @@ team_permission_desc=Cead team_unit_desc=Ceadaigh Rochtain ar Rannóga Stóras team_unit_disabled=(DÃchumasaithe) +form.name_been_taken=Tá ainm na heagraÃochta "%s" tógtha cheana féin. form.name_reserved=Tá an t-ainm eagraÃochta "%s" curtha in áirithe. form.name_pattern_not_allowed=Nà cheadaÃtear an patrún "%s" in ainm eagraÃochta. form.create_org_not_allowed=NÃl cead agat eagraÃocht a chruthú. @@ -2827,15 +2847,28 @@ settings.visibility.private_shortname=PrÃobháideach settings.update_settings=Nuashonrú Socruithe settings.update_setting_success=NuashonraÃodh socruithe eagraÃochta. -settings.change_orgname_prompt=Nóta: Athróidh ainm na heagraÃochta ag athrú URL d'eagraÃochta agus saorfar an sean-ainm. -settings.change_orgname_redirect_prompt=Déanfaidh an sean-ainm a atreorú go dtà go n-éilÃtear é. + +settings.rename=Athainmnigh an EagraÃocht +settings.rename_desc=Má athraÃonn tú ainm na heagraÃochta, athrófar URL d’eagraÃochta freisin agus saorfar an seanainm. +settings.rename_success=AthainmnÃodh an eagraÃocht %[1]s go %[2]s go rathúil. +settings.rename_no_change=NÃl aon athrú ar ainm na heagraÃochta. +settings.rename_new_org_name=Ainm Nua na hEagraÃochta +settings.rename_failed=Theip ar athainmniú na hEagraÃochta mar gheall ar earráid inmheánach +settings.rename_notices_1=Nà <strong>FÉIDIR</strong> an oibrÃocht seo a chealú. +settings.rename_notices_2=Déanfar an seanainm a atreorú go dtà go n-éileofar é. + settings.update_avatar_success=NuashonraÃodh avatar na heagraÃochta. settings.delete=Scrios EagraÃocht settings.delete_account=Scrios an EagraÃocht seo settings.delete_prompt=Bainfear an eagraÃocht go buan. <strong>Nà FÉIDIR</strong> é seo a chealú! +settings.name_confirm=Cuir isteach ainm na heagraÃochta mar dheimhniú: +settings.delete_notices_1=Nà <strong>FÉIDIR</strong> an oibrÃocht seo a chealú. +settings.delete_notices_2=Scriosfaidh an oibrÃocht seo go buan gach <strong>stórais</strong> de chuid <strong>%s</strong>, lena n-áirÃtear cód, saincheisteanna, tuairimÃ, sonraà vicà agus socruithe comhoibritheora. +settings.delete_notices_3=Scriosfaidh an oibrÃocht seo gach <strong>pacáiste</strong> de chuid <strong>%s</strong> go buan. +settings.delete_notices_4=Scriosfaidh an oibrÃocht seo gach <strong>tionscadal</strong> de chuid <strong>%s</strong> go buan. settings.confirm_delete_account=Deimhnigh scriosadh -settings.delete_org_title=Scrios EagraÃocht -settings.delete_org_desc=Scriosfar an eagraÃocht seo go buan. Lean ar aghaidh? +settings.delete_failed=Theip ar Scriosadh na hEagraÃochta mar gheall ar earráid inmheánach +settings.delete_successful=Scriosadh an eagraÃocht <b>%s</b> go rathúil. settings.hooks_desc=Cuir crúcaà gréasán in leis a spreagfar do <strong>gach stóras</strong> faoin eagraÃocht seo. settings.labels_desc=Cuir lipéid leis ar féidir iad a úsáid ar shaincheisteanna do <strong>gach stóras</strong> faoin eagraÃocht seo. @@ -3812,8 +3845,10 @@ runs.no_runs=NÃl aon rith ag an sreabhadh oibre fós. runs.empty_commit_message=(teachtaireacht tiomantas folamh) runs.expire_log_message=Glanadh logaà toisc go raibh siad ró-sean. runs.delete=Scrios rith sreabha oibre +runs.cancel=Cealaigh rith an tsreabha oibre runs.delete.description=An bhfuil tú cinnte gur mian leat an rith sreabha oibre seo a scriosadh go buan? Nà féidir an gnÃomh seo a chealú. runs.not_done=NÃl an rith sreabha oibre seo crÃochnaithe. +runs.view_workflow_file=Féach ar chomhad sreabha oibre workflow.disable=DÃchumasaigh sreabhadh oibre workflow.disable_success=D'éirigh le sreabhadh oibre '%s' a dhÃchumasú. diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index ebc6d5c801..e823b325a5 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -711,6 +711,7 @@ editor.commit_empty_file_header=Egy üres fájl commitolása editor.no_changes_to_show=Nincsen megjelenÃthetÅ‘ változás. editor.add_subdir=Mappa hozzáadása… + commits.commits=Commit-ok commits.search_all=Minden ág commits.author=SzerzÅ‘ @@ -1174,13 +1175,13 @@ settings.visibility.private_shortname=Privát settings.update_settings=BeállÃtások frissÃtése settings.update_setting_success=A szervezet beállÃtásai frissültek. + + settings.update_avatar_success=A szervezet avatarja frissÃtve. settings.delete=Szervezet törlése settings.delete_account=A szervezet törlése settings.delete_prompt=A szervezet véglegesen el lesz távolÃtva. <strong>NEM</strong> vonható vissza! settings.confirm_delete_account=Törlés megerÅ‘sÃtése -settings.delete_org_title=Szervezet törlése -settings.delete_org_desc=Ez a szervezet véglegesen törölve lesz. Folytatható? settings.hooks_desc=Webhook hozzáadása a szervezet <strong>összes tárolójához</strong>. diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 54b0499d96..38f65c74ff 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -717,6 +717,7 @@ editor.new_branch_name_desc=Nama branch baru… editor.cancel=Membatalkan editor.no_changes_to_show=Tidak ada perubahan untuk ditampilkan. + commits.commits=Melakukan commits.author=Penulis commits.message=Pesan @@ -1055,10 +1056,11 @@ settings.visibility.private_shortname=Pribadi settings.update_settings=Perbarui Setelan settings.update_setting_success=Pengaturan organisasi telah diperbarui. + + settings.delete=Menghapus Organisasi settings.delete_account=Menghapus Organisasi Ini settings.confirm_delete_account=Konfirmasi Penghapusan -settings.delete_org_title=Menghapus Organisasi settings.hooks_desc=Tambahkan webhooks yang akan dipicu untuk <strong>semua repositori</strong> di bawah organisasi ini. diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 42ecfabe22..3b4c605801 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -689,7 +689,7 @@ editor.create_new_branch=Búðu til <strong>nýja grein</strong> og sameiningarb editor.create_new_branch_np=Búðu til <strong>nýja grein</strong> fyrir þetta framlag. editor.new_branch_name_desc=Heiti nýjar greinar… editor.cancel=Hætta við -editor.fail_to_update_file_summary=Villuskilaboð: + commits.commits=Framlög commits.author=Höfundur @@ -1027,11 +1027,9 @@ settings.event_release=Útgáfa settings.event_push=Push settings.event_repository=Hugbúnaðarsafn settings.event_issues=Vandamál -settings.event_issues_desc=Vandamál opið, lokað, enduropnað eða breytt. settings.event_issue_label=Vandamál Lýst settings.event_issue_comment=Ummæli um Vandamál settings.event_pull_request=Sameiningarbeiðni -settings.event_pull_request_desc=Sameiningarbeiðni opnuð, lokuð, enduropnuð eða breytt. settings.active=Virkt settings.update_webhook=Uppfæra Vefkrók settings.slack_token=Táknlykill @@ -1118,6 +1116,8 @@ settings.visibility.private_shortname=Einka settings.update_settings=Uppfæra Stillingar + + members.private=Faldir members.owner=Eigandi members.member=Meðlimur diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index 569d3f54e1..2d4d5b71d0 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -1014,7 +1014,6 @@ editor.file_changed_while_editing=I contenuti di questo file hanno subito dei ca editor.commit_empty_file_header=Commit di un file vuoto editor.commit_empty_file_text=Il file che stai per effettuare il commit è vuoto. Procedere? editor.no_changes_to_show=Non ci sono cambiamenti da mostrare. -editor.fail_to_update_file_summary=Messaggio d'errore: editor.push_rejected_no_message=La modifica è stata rifiutata dal server senza un messaggio. Controlla Git Hooks. editor.push_rejected=La modifica è stata rifiutata dal server. Controlla Git Hooks. editor.push_rejected_summary=Messaggio Di Rifiuto Completo: @@ -1025,6 +1024,7 @@ editor.require_signed_commit=Il branch richiede un commit firmato editor.cherry_pick=Cherry-pick %s suto: editor.revert=Ripristina %s su: + commits.desc=Sfoglia la cronologia di modifiche del codice rogente. commits.commits=Commit commits.nothing_to_compare=Questi rami sono uguali. @@ -1190,7 +1190,7 @@ issues.context.edit=Modifica issues.context.delete=Elimina issues.reopen_issue=Riapri issues.create_comment=Commento -issues.closed_at=`chiuso questo probleam <a id="%[1]s" href="#%[1]s">%[2]s</a>` +issues.closed_at=`ha chiuso questo problema <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.reopened_at=`riaperto questo problema <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commit_ref_at=`ha fatto riferimento a questa issue dal commit <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_issue_from=`<a href="%[3]s">ha fatto riferimento a questo problema %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` @@ -1764,7 +1764,6 @@ settings.event_repository=Repository settings.event_repository_desc=Repository creato o eliminato. settings.event_header_issue=Eventi dei Problemi settings.event_issues=Issues -settings.event_issues_desc=Issue aperto, chiuso, riaperto o modificato. settings.event_issue_assign=Issue Assegnato settings.event_issue_assign_desc=Issue assegnata o non assegnata. settings.event_issue_label=Issue etichettato @@ -1775,7 +1774,6 @@ settings.event_issue_comment=Commento Issue settings.event_issue_comment_desc=Commento issue creato, modificato o rimosso. settings.event_header_pull_request=Eventi di Pull Request settings.event_pull_request=Pull Request -settings.event_pull_request_desc=Pull request aperta, chiusa, riaperta o modificata. settings.event_pull_request_assign=Pull Request assegnata settings.event_pull_request_assign_desc=Pull request assegnata o non assegnata. settings.event_pull_request_label=Pull Request etichettata @@ -2078,14 +2076,13 @@ settings.visibility.private_shortname=Privato settings.update_settings=Aggiorna Impostazioni settings.update_setting_success=Le impostazioni dell'organizzazione sono state aggiornate. -settings.change_orgname_redirect_prompt=Il vecchio nome reindirizzerà fino a quando non sarà richiesto. + + settings.update_avatar_success=L'avatar dell'organizzazione è stato aggiornato. settings.delete=Elimina organizzazione settings.delete_account=Elimina questa organizzazione settings.delete_prompt=L'organizzazione verrà rimossa definitivamente. Questa operazione <strong>NON PUÃ’</strong> essere annullata! settings.confirm_delete_account=Conferma Eliminazione -settings.delete_org_title=Elimina organizzazione -settings.delete_org_desc=Questa organizzazione verrà eliminata definitivamente. Continuare? settings.hooks_desc=Aggiungi i webhooks che verranno attivati per <strong>tutti i repository</strong> sotto questa organizzazione. settings.labels_desc=Aggiungi i webhooks che verranno attivati per <strong>tutti i repository</strong> sotto questa organizzazione. diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 2a871da6aa..d983dbab21 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -130,6 +130,7 @@ pin=ピン留゠unpin=ピン留ã‚解除 artifacts=æˆæžœç‰© +expired=期é™åˆ‡ã‚Œ confirm_delete_artifact=アーティファクト %s を削除ã—ã¦ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ archived=アーカイブ @@ -420,6 +421,7 @@ remember_me.compromised=ãƒã‚°ã‚¤ãƒ³ãƒˆãƒ¼ã‚¯ãƒ³ã¯ã‚‚ã†æœ‰åйã§ã¯ãªãã€ã‚ forgot_password_title=パスワードを忘れ㟠forgot_password=パスワードをãŠå¿˜ã‚Œã§ã™ã‹ï¼Ÿ need_account=アカウントãŒå¿…è¦ã§ã™ã‹ï¼Ÿ +sign_up_tip=管ç†è€…権é™ã‚’æŒã¤ã€ã“ã®ã‚·ã‚¹ãƒ†ãƒ ã®æœ€åˆã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’登録ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ ユーザーåã¨ãƒ‘スワードをよã覚ãˆã¦ãŠã„ã¦ãã ã•ã„。 ユーザーåã¾ãŸã¯ãƒ‘スワードを忘れãŸå ´åˆã¯ã€Giteaã®ãƒ‰ã‚ュメントをå‚ç…§ã—ã¦ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’復元ã—ã¦ãã ã•ã„。 sign_up_now=登録ã¯ã“ã¡ã‚‰ã€‚ sign_up_successful=アカウントã¯ç„¡äº‹ã«ä½œæˆã•れã¾ã—ãŸã€‚よã†ã“ã! confirmation_mail_sent_prompt_ex=æ–°ã—ã„確èªãƒ¡ãƒ¼ãƒ«ã‚’ <b>%s</b> ã«é€ä¿¡ã—ã¾ã—ãŸã€‚ %s以内ã«ãƒ¡ãƒ¼ãƒ«ãƒœãƒƒã‚¯ã‚¹ã‚’確èªã—ã€ç™»éŒ²æ‰‹ç¶šãを完了ã—ã¦ãã ã•ã„。 登録メールアドレスãŒé–“é•ã£ã¦ã„ã‚‹å ´åˆã¯ã€ã‚‚ã†ã„ã¡ã©ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã™ã‚‹ã¨å¤‰æ›´ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ @@ -1330,7 +1332,9 @@ editor.upload_file=ファイルをアップãƒãƒ¼ãƒ‰ editor.edit_file=ファイルを編集 editor.preview_changes=変更をプレビュー editor.cannot_edit_lfs_files=LFSã®ãƒ•ァイルã¯Webインターフェースã§ç·¨é›†ã§ãã¾ã›ã‚“。 +editor.cannot_edit_too_large_file=ã“ã®ãƒ•ァイルã¯å¤§ãã™ãŽã‚‹ãŸã‚ã€ç·¨é›†ã§ãã¾ã›ã‚“。 editor.cannot_edit_non_text_files=ãƒã‚¤ãƒŠãƒªãƒ•ァイルã¯Webインターフェースã§ç·¨é›†ã§ãã¾ã›ã‚“。 +editor.file_not_editable_hint=åå‰ã®å¤‰æ›´ã‚„移動ã¯å¯èƒ½ã§ã™ã€‚ editor.edit_this_file=ファイルを編集 editor.this_file_locked=ファイルã¯ãƒãƒƒã‚¯ã•れã¦ã„ã¾ã™ editor.must_be_on_a_branch=ã“ã®ãƒ•ァイルを変更ã—ãŸã‚Šå¤‰æ›´ã®ææ¡ˆã‚’ã™ã‚‹ã«ã¯ã€ãƒ–ランãƒä¸Šã«ã„ã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚ @@ -1350,7 +1354,7 @@ editor.update=%s ã‚’æ›´æ–° editor.delete=%s を削除 editor.patch=パッãƒã®é©ç”¨ editor.patching=パッãƒ: -editor.fail_to_apply_patch=`パッãƒã‚’é©ç”¨ã§ãã¾ã›ã‚“ "%s"` +editor.fail_to_apply_patch=パッãƒã‚’é©ç”¨ã§ãã¾ã›ã‚“ editor.new_patch=æ–°ã—ã„パッムeditor.commit_message_desc=詳細ãªèª¬æ˜Žã‚’è¿½åŠ â€¦ editor.signoff_desc=コミットãƒã‚°ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®æœ€å¾Œã«ã‚³ãƒŸãƒƒã‚¿ãƒ¼ã® Signed-off-by è¡Œã‚’è¿½åŠ @@ -1370,8 +1374,7 @@ editor.branch_already_exists=ブランム"%s" ã¯ã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªã«æ—¢ editor.directory_is_a_file=ディレクトリå "%s" ã¯ã™ã§ã«ãƒªãƒã‚¸ãƒˆãƒªå†…ã®ãƒ•ァイルã§ä½¿ç”¨ã•れã¦ã„ã¾ã™ã€‚ editor.file_is_a_symlink=`"%s" ã¯ã‚·ãƒ³ãƒœãƒªãƒƒã‚¯ãƒªãƒ³ã‚¯ã§ã™ã€‚ シンボリックリンクã¯Webエディターã§ç·¨é›†ã§ãã¾ã›ã‚“。` editor.filename_is_a_directory=ファイルå "%s" ã¯ã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªä¸Šã§ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªåã¨ã—ã¦ã™ã§ã«ä½¿ç”¨ã•れã¦ã„ã¾ã™ã€‚ -editor.file_editing_no_longer_exists=編集ä¸ã®ãƒ•ァイル "%s" ãŒã€ã‚‚ã†ãƒªãƒã‚¸ãƒˆãƒªå†…ã«ã‚りã¾ã›ã‚“。 -editor.file_deleting_no_longer_exists=削除ã—よã†ã¨ã—ãŸãƒ•ァイル "%s" ãŒã€ã™ã§ã«ãƒªãƒã‚¸ãƒˆãƒªå†…ã«ã‚りã¾ã›ã‚“。 +editor.file_modifying_no_longer_exists=ä¿®æ£ä¸ã®ãƒ•ァイル "%s" ãŒã€ã™ã§ã«ãƒªãƒã‚¸ãƒˆãƒªå†…ã«ã‚りã¾ã›ã‚“。 editor.file_changed_while_editing=ã‚ãªãŸãŒç·¨é›†ã‚’é–‹å§‹ã—ãŸã‚ã¨ã€ãƒ•ァイルã®å†…容ãŒå¤‰æ›´ã•れã¾ã—ãŸã€‚ <a target="_blank" rel="noopener noreferrer" href="%s">ã“ã“をクリック</a>ã—ã¦ä½•ãŒå¤‰æ›´ã•れãŸã‹ç¢ºèªã™ã‚‹ã‹ã€<strong>ã‚‚ã†ä¸€åº¦"変更をコミット"をクリック</strong>ã—ã¦ä¸Šæ›¸ãã—ã¾ã™ã€‚ editor.file_already_exists=ファイル "%s" ã¯ã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªã«æ—¢ã«å˜åœ¨ã—ã¾ã™ã€‚ editor.commit_id_not_matching=コミットIDãŒç·¨é›†ã‚’é–‹å§‹ã—ãŸã¨ãã®IDã¨ä¸€è‡´ã—ã¾ã›ã‚“。 パッãƒç”¨ã®ãƒ–ランãƒã«ã‚³ãƒŸãƒƒãƒˆã—ãŸã‚ã¨ãƒžãƒ¼ã‚¸ã—ã¦ãã ã•ã„。 @@ -1379,8 +1382,6 @@ editor.push_out_of_date=ã“ã®ãƒ—ãƒƒã‚·ãƒ¥ã¯æœ€æ–°ã§ã¯ãªã„よã†ã§ã™ã€‚ editor.commit_empty_file_header=空ファイルã®ã‚³ãƒŸãƒƒãƒˆ editor.commit_empty_file_text=コミットã—よã†ã¨ã—ã¦ã„るファイルã¯ç©ºã§ã™ã€‚ 続行ã—ã¾ã™ã‹ï¼Ÿ editor.no_changes_to_show=表示ã™ã‚‹å¤‰æ›´ç®‡æ‰€ã¯ã‚りã¾ã›ã‚“。 -editor.fail_to_update_file=ファイル "%s" を作æˆã¾ãŸã¯å¤‰æ›´ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ -editor.fail_to_update_file_summary=エラーメッセージ: editor.push_rejected_no_message=サーãƒãƒ¼ãŒãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’出ã•ãšã«å¤‰æ›´ã‚’æ‹’å¦ã—ã¾ã—ãŸã€‚ Git フックを確èªã—ã¦ãã ã•ã„。 editor.push_rejected=サーãƒãƒ¼ãŒå¤‰æ›´ã‚’æ‹’å¦ã—ã¾ã—ãŸã€‚ Gitフックを確èªã—ã¦ãã ã•ã„。 editor.push_rejected_summary=æ‹’å¦ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸å…¨ä½“: @@ -1394,6 +1395,9 @@ editor.user_no_push_to_branch=ユーザーã¯ãƒ–ランãƒã«ãƒ—ッシュã§ãã editor.require_signed_commit=ブランãƒã§ã¯ç½²åã•れãŸã‚³ãƒŸãƒƒãƒˆãŒå¿…é ˆã§ã™ editor.cherry_pick=ãƒã‚§ãƒªãƒ¼ãƒ”ック %s: editor.revert=リãƒãƒ¼ãƒˆ %s: +editor.failed_to_commit=変更ã®ã‚³ãƒŸãƒƒãƒˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ +editor.failed_to_commit_summary=エラーメッセージ: + commits.desc=ソースコードã®å¤‰æ›´å±¥æ´ã‚’å‚ç…§ã—ã¾ã™ã€‚ commits.commits=コミット @@ -1558,6 +1562,7 @@ issues.filter_user_placeholder=ユーザーを検索 issues.filter_user_no_select=ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ issues.filter_type=タイプ issues.filter_type.all_issues=ã™ã¹ã¦ã®ã‚¤ã‚·ãƒ¥ãƒ¼ +issues.filter_type.all_pull_requests=ã™ã¹ã¦ã®ãƒ—ルリクエスト issues.filter_type.assigned_to_you=è‡ªåˆ†ãŒæ‹…当 issues.filter_type.created_by_you=自分ãŒä½œæˆ issues.filter_type.mentioning_you=自分ãŒé–¢ä¿‚ @@ -1649,6 +1654,7 @@ issues.save=ä¿å˜ issues.label_title=åå‰ issues.label_description=説明 issues.label_color=カラー +issues.label_color_invalid=無効ãªè‰²ã§ã™ issues.label_exclusive=排他 issues.label_archive=アーカイブ ラベル issues.label_archived_filter=アーカイブã•れãŸãƒ©ãƒ™ãƒ«ã‚’表示 @@ -1954,7 +1960,6 @@ pulls.cmd_instruction_checkout_title=ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¦ãƒˆ pulls.cmd_instruction_checkout_desc=プãƒã‚¸ã‚§ã‚¯ãƒˆãƒªãƒã‚¸ãƒˆãƒªã‹ã‚‰æ–°ã—ã„ブランãƒã‚’ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¦ãƒˆã—ã€å¤‰æ›´å†…容をテストã—ã¾ã™ã€‚ pulls.cmd_instruction_merge_title=マージ pulls.cmd_instruction_merge_desc=変更内容をマージã—ã¦ã€Giteaã«åæ˜ ã—ã¾ã™ã€‚ -pulls.cmd_instruction_merge_warning=è¦å‘Š: 「手動マージã®è‡ªå‹•検出ã€ãŒæœ‰åйã§ã¯ãªã„ãŸã‚ã€ã“ã®æ“作ã§ã¯ãƒ—ルリクエストをマージã§ãã¾ã›ã‚“ pulls.clear_merge_message=マージメッセージをクリア pulls.clear_merge_message_hint=マージメッセージã®ã‚¯ãƒªã‚¢ã¯ã€ã‚³ãƒŸãƒƒãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã®é™¤åŽ»ã ã‘を行ã„ã¾ã™ã€‚ 生æˆã•れãŸGitトレーラー("Co-Authored-By …" ç‰)ã¯ãã®ã¾ã¾æ®‹ã‚Šã¾ã™ã€‚ @@ -2368,7 +2373,6 @@ settings.event_repository=リãƒã‚¸ãƒˆãƒª settings.event_repository_desc=リãƒã‚¸ãƒˆãƒªãŒä½œæˆãƒ»å‰Šé™¤ã•れãŸã¨ã。 settings.event_header_issue=イシューã®ã‚¤ãƒ™ãƒ³ãƒˆ settings.event_issues=イシュー -settings.event_issues_desc=イシューãŒã‚ªãƒ¼ãƒ—ン・クãƒãƒ¼ã‚ºãƒ»å†ã‚ªãƒ¼ãƒ—ン・編集ã•れãŸã¨ã。 settings.event_issue_assign=イシューã®ã‚¢ã‚µã‚¤ãƒ³ settings.event_issue_assign_desc=ã‚¤ã‚·ãƒ¥ãƒ¼ã®æ‹…当者ãŒå‰²ã‚Šå½“ã¦ã‚‰ã‚ŒãŸã¨ãã€è§£é™¤ã•れãŸã¨ã。 settings.event_issue_label=イシューã®ãƒ©ãƒ™ãƒ« @@ -2379,7 +2383,6 @@ settings.event_issue_comment=イシューã¸ã®ã‚³ãƒ¡ãƒ³ãƒˆ settings.event_issue_comment_desc=イシューã¸ã®ã‚³ãƒ¡ãƒ³ãƒˆãŒä½œæˆãƒ»ç·¨é›†ãƒ»å‰Šé™¤ã•れãŸã¨ã。 settings.event_header_pull_request=プルリクエストã®ã‚¤ãƒ™ãƒ³ãƒˆ settings.event_pull_request=プルリクエスト -settings.event_pull_request_desc=プルリクエストãŒã‚ªãƒ¼ãƒ—ン・クãƒãƒ¼ã‚ºãƒ»å†ã‚ªãƒ¼ãƒ—ン・編集ã•れãŸã¨ã。 settings.event_pull_request_assign=プルリクエストã®ã‚¢ã‚µã‚¤ãƒ³ settings.event_pull_request_assign_desc=ãƒ—ãƒ«ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®æ‹…当者ãŒå‰²ã‚Šå½“ã¦ãƒ»è§£é™¤ã•れãŸã¨ã。 settings.event_pull_request_label=プルリクエストã®ãƒ©ãƒ™ãƒ« @@ -2397,6 +2400,8 @@ settings.event_pull_request_review_request_desc=プルリクエストã®ãƒ¬ãƒ“ムsettings.event_pull_request_approvals=ãƒ—ãƒ«ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®æ‰¿èª settings.event_pull_request_merge=プルリクエストã®ãƒžãƒ¼ã‚¸ settings.event_header_workflow=ワークフãƒãƒ¼ã‚¤ãƒ™ãƒ³ãƒˆ +settings.event_workflow_run=ワークフãƒãƒ¼å®Ÿè¡Œ +settings.event_workflow_run_desc=Gitea Actions ã®ãƒ¯ãƒ¼ã‚¯ãƒ•ãƒãƒ¼å®Ÿè¡ŒãŒã€ã‚ューã«è¿½åŠ ã€å¾…機ä¸ã€å®Ÿè¡Œä¸ã€å®Œäº†ã«ãªã£ãŸã¨ã。 settings.event_workflow_job=ワークフãƒãƒ¼ã‚¸ãƒ§ãƒ– settings.event_workflow_job_desc=Gitea Actions ã®ãƒ¯ãƒ¼ã‚¯ãƒ•ãƒãƒ¼ã‚¸ãƒ§ãƒ–ãŒã€ã‚ューã«è¿½åŠ ã€å¾…機ä¸ã€å®Ÿè¡Œä¸ã€å®Œäº†ã«ãªã£ãŸã¨ã。 settings.event_package=パッケージ @@ -2804,6 +2809,7 @@ team_permission_desc=æ¨©é™ team_unit_desc=リãƒã‚¸ãƒˆãƒªã®ã‚»ã‚¯ã‚·ãƒ§ãƒ³ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’è¨±å¯ team_unit_disabled=(無効) +form.name_been_taken=組織å "%s" ã¯æ—¢ã«ä½¿ç”¨ã•れã¦ã„ã¾ã™ã€‚ form.name_reserved=組織å "%s" ã¯äºˆç´„ã•れã¦ã„ã¾ã™ã€‚ form.name_pattern_not_allowed=`"%s" ã®å½¢å¼ã¯çµ„ç¹”åã«ä½¿ç”¨ã§ãã¾ã›ã‚“。` form.create_org_not_allowed=組織を作æˆã™ã‚‹æ¨©é™ãŒã‚りã¾ã›ã‚“。 @@ -2825,15 +2831,28 @@ settings.visibility.private_shortname=プライベート settings.update_settings=è¨å®šã®æ›´æ–° settings.update_setting_success=組織ã®è¨å®šã‚’æ›´æ–°ã—ã¾ã—ãŸã€‚ -settings.change_orgname_prompt=注æ„: 組織åを変更ã™ã‚‹ã¨çµ„ç¹”ã®URLも変更ã•れã€å¤ã„åå‰ã¯è§£æ”¾ã•れã¾ã™ã€‚ -settings.change_orgname_redirect_prompt=å¤ã„åå‰ã¯ã€å†ä½¿ç”¨ã•れã¦ã„ãªã„é™ã‚Šãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã—ã¾ã™ã€‚ + +settings.rename=組織åã®å¤‰æ›´ +settings.rename_desc=組織åを変更ã™ã‚‹ã¨çµ„ç¹”ã®URLも変更ã•れã€å¤ã„åå‰ã¯è§£æ”¾ã•れã¾ã™ã€‚ +settings.rename_success=組織 %[1]s ã® %[2]s ã¸ã®æ”¹åã«æˆåŠŸã—ã¾ã—ãŸã€‚ +settings.rename_no_change=組織åã®å¤‰æ›´ã¯ã‚りã¾ã›ã‚“。 +settings.rename_new_org_name=æ–°ã—ã„組織å +settings.rename_failed=内部エラーã®ãŸã‚組織åを変更ã§ãã¾ã›ã‚“ã§ã—㟠+settings.rename_notices_1=ã“ã®æ“作ã¯<strong>å…ƒã«æˆ»ã›ã¾ã›ã‚“</strong> 。 +settings.rename_notices_2=å¤ã„åå‰ã¯ã€å†ä½¿ç”¨ã•れるã¾ã§ã¯ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã—ã¾ã™ã€‚ + settings.update_avatar_success=組織ã®ã‚¢ãƒã‚¿ãƒ¼ã‚’æ›´æ–°ã—ã¾ã—ãŸã€‚ settings.delete=組織を削除 settings.delete_account=ã“ã®çµ„織を削除 settings.delete_prompt=çµ„ç¹”ã¯æ’ä¹…çš„ã«å‰Šé™¤ã•れã¾ã™ã€‚ å…ƒã«æˆ»ã™ã“ã¨ã¯<strong>ã§ãã¾ã›ã‚“</strong>ï¼ +settings.name_confirm=確èªã®ãŸã‚組織åを入力: +settings.delete_notices_1=ã“ã®æ“作ã¯<strong>å…ƒã«æˆ»ã›ã¾ã›ã‚“</strong> 。 +settings.delete_notices_2=ã“ã®æ“作ã«ã‚ˆã‚Šã€<strong>%s</strong>ã®ã™ã¹ã¦ã®<strong>リãƒã‚¸ãƒˆãƒª</strong>ãŒæ’ä¹…çš„ã«å‰Šé™¤ã•れã¾ã™ã€‚ コードã€ã‚¤ã‚·ãƒ¥ãƒ¼ã€ã‚³ãƒ¡ãƒ³ãƒˆã€Wikiデータã€å…±åŒä½œæ¥è€…ã®è¨å®šã‚‚å«ã¾ã‚Œã¾ã™ã€‚ +settings.delete_notices_3=ã“ã®æ“作ã«ã‚ˆã‚Šã€<strong>%s</strong>ã®ã™ã¹ã¦ã®<strong>パッケージ</strong>ãŒæ’ä¹…çš„ã«å‰Šé™¤ã•れã¾ã™ã€‚ +settings.delete_notices_4=ã“ã®æ“作ã«ã‚ˆã‚Šã€<strong>%s</strong>ã®ã™ã¹ã¦ã®<strong>プãƒã‚¸ã‚§ã‚¯ãƒˆ</strong>ãŒæ’ä¹…çš„ã«å‰Šé™¤ã•れã¾ã™ã€‚ settings.confirm_delete_account=å‰Šé™¤ã‚’ç¢ºèª -settings.delete_org_title=組織ã®å‰Šé™¤ -settings.delete_org_desc=組織をæ’ä¹…çš„ã«å‰Šé™¤ã—ã¾ã™ã€‚ 続行ã—ã¾ã™ã‹ï¼Ÿ +settings.delete_failed=内部エラーã®ãŸã‚組織を削除ã§ãã¾ã›ã‚“ã§ã—㟠+settings.delete_successful=組織ã®<b>%s</b>ã®å‰Šé™¤ã«æˆåŠŸã—ã¾ã—ãŸã€‚ settings.hooks_desc=ã“ã®çµ„ç¹”ã®<strong>ã™ã¹ã¦ã®ãƒªãƒã‚¸ãƒˆãƒª</strong>ã§ãƒˆãƒªã‚¬ãƒ¼ã•れるWebhookã‚’è¿½åŠ ã—ã¾ã™ã€‚ settings.labels_desc=ã“ã®çµ„ç¹”ã®<strong>ã™ã¹ã¦ã®ãƒªãƒã‚¸ãƒˆãƒª</strong>ã§ä½¿ç”¨å¯èƒ½ãªã‚¤ã‚·ãƒ¥ãƒ¼ãƒ©ãƒ™ãƒ«ã‚’è¿½åŠ ã—ã¾ã™ã€‚ @@ -3809,6 +3828,10 @@ runs.no_workflows.documentation=Gitea Actions ã®è©³ç´°ã«ã¤ã„ã¦ã¯ã€<a targ runs.no_runs=ワークフãƒãƒ¼ã¯ã¾ã 実行ã•れã¦ã„ã¾ã›ã‚“。 runs.empty_commit_message=(空ã®ã‚³ãƒŸãƒƒãƒˆãƒ¡ãƒƒã‚»ãƒ¼ã‚¸) runs.expire_log_message=ãƒã‚°ã¯å¤ã™ãŽã‚‹ãŸã‚消去ã•れã¦ã„ã¾ã™ã€‚ +runs.delete=ワークフãƒãƒ¼ã®å®Ÿè¡Œã‚’削除 +runs.delete.description=ã“ã®ãƒ¯ãƒ¼ã‚¯ãƒ•ãƒãƒ¼ã‚’完全ã«å‰Šé™¤ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿã“ã®æ“作ã¯å…ƒã«æˆ»ã›ã¾ã›ã‚“。 +runs.not_done=ã“ã®ãƒ¯ãƒ¼ã‚¯ãƒ•ãƒãƒ¼ã®å®Ÿè¡Œã¯å®Œäº†ã—ã¦ã„ã¾ã›ã‚“。 +runs.view_workflow_file=ワークフãƒãƒ¼ãƒ•ァイルを表示 workflow.disable=ワークフãƒãƒ¼ã‚’無効ã«ã™ã‚‹ workflow.disable_success=ワークフãƒãƒ¼ '%s' ãŒç„¡åйã«ãªã‚Šã¾ã—ãŸã€‚ diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 22bf3e1641..6f36ad905c 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -648,6 +648,7 @@ editor.filename_cannot_be_empty=파ì¼ëª…ì´ ë¹ˆì¹¸ìž…ë‹ˆë‹¤. editor.no_changes_to_show=í‘œì‹œí• ë³€ê²½ì‚¬í•ì´ ì—†ìŠµë‹ˆë‹¤. editor.add_subdir=경로 추가... + commits.desc=소스 코드 변경 ë‚´ì— íƒìƒ‰ commits.commits=커밋 commits.search_all=ëª¨ë“ ë¸Œëžœì¹˜ @@ -1151,12 +1152,12 @@ settings.visibility.private_shortname=비공개 settings.update_settings=ì„¤ì • ì—…ë°ì´íЏ settings.update_setting_success=ì¡°ì§ ì„¤ì •ì´ ë³€ê²½ë˜ì—ˆìŠµë‹ˆë‹¤. + + settings.update_avatar_success=ì¡°ì§ì˜ 아바타가 ê°±ì‹ ë˜ì—ˆìŠµë‹ˆë‹¤. settings.delete=ì¡°ì§ ì‚ì œ settings.delete_account=ì´ ì¡°ì§ì„ ì‚ì œí•©ë‹ˆë‹¤. settings.confirm_delete_account=ì‚ì œ ìŠ¹ì¸ -settings.delete_org_title=ì¡°ì§ ì‚ì œ -settings.delete_org_desc=ì´ ì¡°ì§ì´ ì˜êµ¬ížˆ ì‚ì œë©ë‹ˆë‹¤. ê³„ì† í•˜ì‹œê² ìŠµë‹ˆê¹Œ? members.membership_visibility=íšŒì› í‘œì‹œ: diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index a746f8738c..5df5ec35d3 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -1196,7 +1196,6 @@ editor.update=Atjaunot %s editor.delete=DzÄ“st %s editor.patch=Pielietot ielÄpu editor.patching=Pielieto ielÄpu: -editor.fail_to_apply_patch=`NeizdevÄs pielietot ielÄpu "%s"` editor.new_patch=Jauns ielÄps editor.commit_message_desc=Pievienot neobligÄtu paplaÅ¡inÄtu aprakstu… editor.signoff_desc=Pievienot revÄ«zijas žurnÄla ziņojuma beigÄs Signed-off-by ar revÄ«zijas autoru. @@ -1214,15 +1213,11 @@ editor.branch_already_exists=Atzars "%s" Å¡ajÄ repozitorijÄ jau eksistÄ“. editor.directory_is_a_file=Direktorijas nosaukums "%s" vecÄka ceÄ¼Ä ir fails nevis direktorija Å¡ajÄ repozitorijÄ. editor.file_is_a_symlink=Fails "%s" ir norÄde, kuru nav iespÄ“jams labot no tÄ«mekļa redaktora editor.filename_is_a_directory=Faila nosaukums "%s" sakrÄ«t ar direktorijas nosaukumu Å¡ajÄ repozitorijÄ. -editor.file_editing_no_longer_exists=Fails "%s", ko labojat, vairs neeksistÄ“ Å¡ajÄ repozitorijÄ. -editor.file_deleting_no_longer_exists=Fails "%s", ko dzēšat, vairs neeksistÄ“ Å¡ajÄ repozitorijÄ. editor.file_changed_while_editing=Faila saturs ir mainÄ«jies kopÅ¡ sÄkÄt to labot. Noklikšķiniet <a target="_blank" rel="noopener noreferrer" href="%s">Å¡eit</a>, lai apskatÄ«tu, vai <strong>NosÅ«tiet izmaiņas atkÄrtoti</strong>, lai pÄrrakstÄ«tu. editor.file_already_exists=Fails ar nosaukumu "%s" Å¡ajÄ repozitorijÄ jau eksistÄ“. editor.commit_empty_file_header=IesÅ«tÄ«t tukÅ¡u failu editor.commit_empty_file_text=Fails, ko vÄ“laties iesÅ«tÄ«t, ir tukÅ¡s. Vai turpinÄt? editor.no_changes_to_show=Nav izmaiņu, ko rÄdÄ«t. -editor.fail_to_update_file=NeizdevÄs atjaunot/izveidot failu "%s". -editor.fail_to_update_file_summary=Kļūdas ziņojums: editor.push_rejected_no_message=Izmaiņu iesÅ«tīšana tika noraidÄ«ta, bet serveris neatgrieza paziņojumu. PÄrbaudiet git ÄÄ·us Å¡im repozitorijam. editor.push_rejected=Serveris noraidÄ«ja Å¡o izmaiņu. PÄrbaudiet git ÄÄ·us. editor.push_rejected_summary=Pilns noraidīšanas ziņojums: @@ -1237,6 +1232,7 @@ editor.require_signed_commit=AtzarÄ var iesÅ«tÄ«t tikai parakstÄ«tas revÄ«zijas editor.cherry_pick=IzlasÄ«t %s uz: editor.revert=Atgriezt %s uz: + commits.desc=PÄrlÅ«kot pirmkoda izmaiņu vÄ“sturi. commits.commits=RevÄ«zijas commits.no_commits=Nav kopÄ«gu revÄ«ziju. Atzariem "%s" un "%s" ir pilnÄ«bÄ atšķirÄ«ga izmaiņu vÄ“sture. @@ -2123,7 +2119,6 @@ settings.event_repository=Repozitorijs settings.event_repository_desc=Repozitorijs izveidots vai dzÄ“sts. settings.event_header_issue=ProblÄ“mu notikumi settings.event_issues=ProblÄ“mas -settings.event_issues_desc=ProblÄ“ma atvÄ“rta, aizvÄ“rta, atkÄrtoti atvÄ“rta vai mainÄ«ta. settings.event_issue_assign=ProblÄ“mas atbildÄ«gie settings.event_issue_assign_desc=ProblÄ“mai piešķirti vai noņemti atbildÄ«gie. settings.event_issue_label=ProblÄ“mu iezÄ«mes @@ -2134,7 +2129,6 @@ settings.event_issue_comment=ProblÄ“mas komentÄrs settings.event_issue_comment_desc=ProblÄ“mas komentÄrs pievienots, labots vai dzÄ“sts. settings.event_header_pull_request=Izmaiņu pieprasÄ«juma notikumi settings.event_pull_request=Izmaiņu pieprasÄ«jums -settings.event_pull_request_desc=Izmaiņu pieprasÄ«jums atvÄ“rts, aizvÄ“rts, atkÄrtoti atvÄ“rts vai mainÄ«ts. settings.event_pull_request_assign=Izmaiņu pieprasÄ«juma atbildÄ«gie settings.event_pull_request_assign_desc=Izmaiņu pieprasÄ«jumam piešķirti vai noņemti atbildÄ«gie. settings.event_pull_request_label=Izmaiņu pieprasÄ«juma iezÄ«mes @@ -2509,15 +2503,13 @@ settings.visibility.private_shortname=PrivÄta settings.update_settings=MainÄ«t iestatÄ«jumus settings.update_setting_success=OrganizÄcijas iestatÄ«jumi tika saglabÄti. -settings.change_orgname_prompt=PiezÄ«me: organizÄcijas nosaukuma maiņa izmainÄ«s arÄ« organizÄcijas URL un atbrÄ«vos veco nosaukumu. -settings.change_orgname_redirect_prompt=Vecais vÄrds pÄrsÅ«tÄ«s uz jauno, kamÄ“r vien tas nebÅ«s izmantots. + + settings.update_avatar_success=OrganizÄcijas attÄ“ls tika saglabÄts. settings.delete=DzÄ“st organizÄciju settings.delete_account=DzÄ“st Å¡o organizÄciju settings.delete_prompt=Å Ä« darbÄ«ba pilnÄ«bÄ dzÄ“sÄ«s Å¡o organizÄciju, kÄ arÄ« tÄ ir <strong>NEATGRIEZENISKA</strong>! settings.confirm_delete_account=ApstiprinÄt dzēšanu -settings.delete_org_title=DzÄ“st organizÄciju -settings.delete_org_desc=OrganizÄcija tiks dzÄ“sta neatgriezeniski. Vai turpinÄt? settings.labels_desc=Pievienojiet iezÄ«mes, kas var tikt izmantotas <strong>visos</strong> šīs organizÄcijas repozitorijos. diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index b6887ee9e0..460736cf7f 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -1012,7 +1012,6 @@ editor.file_changed_while_editing=De bestandsinhoud is veranderd sinds je bent b editor.commit_empty_file_header=Commit een leeg bestand editor.commit_empty_file_text=Het bestand dat u wilt committen is leeg. Doorgaan? editor.no_changes_to_show=Er zijn geen wijzigingen om weer te geven. -editor.fail_to_update_file_summary=Foutmelding: editor.push_rejected_no_message=De wijziging is afgewezen door de server zonder bericht. Controleer de Git Hooks alsjeblieft. editor.push_rejected=De wijziging is afgewezen door de server. Controleer Controleer de Git Hooks alsjeblieft. editor.push_rejected_summary=Volledig afwijzingsbericht: @@ -1023,6 +1022,7 @@ editor.require_signed_commit=Branch vereist een ondertekende commit editor.cherry_pick=Cherry-pick %s op: editor.revert=%s ongedaan maken op: + commits.desc=Bekijk de broncode-wijzigingsgeschiedenis. commits.commits=Commits commits.nothing_to_compare=Deze branches zijn gelijk. @@ -1707,7 +1707,6 @@ settings.event_repository=Repository settings.event_repository_desc=Repository gemaakt of verwijderd. settings.event_header_issue=Issue gebeurtenissen settings.event_issues=Kwesties -settings.event_issues_desc=Issue geopend, gesloten, heropend of bewerkt. settings.event_issue_assign=Probleem toegekend settings.event_issue_assign_desc=Issue toegewezen of niet-toegewezen. settings.event_issue_label=Issue gelabeld @@ -1718,7 +1717,6 @@ settings.event_issue_comment=Issue commentaar settings.event_issue_comment_desc=Issue reactie aangemaakt, bewerkt of verwijderd. settings.event_header_pull_request=Pull Request Events settings.event_pull_request=Pull request -settings.event_pull_request_desc=Pull request geopend, gesloten, heropend of bewerkt. settings.event_pull_request_assign=Pull request toegewezen settings.event_pull_request_assign_desc=Pull request toegewezen of niet-toegewezen. settings.event_pull_request_label=Pull-aanvraag gelabeld @@ -1989,13 +1987,13 @@ settings.visibility.private=Privé (alleen zichtbaar voor organisatieleden) settings.visibility.private_shortname=Privé settings.update_settings=Instellingen bijwerken + + settings.update_avatar_success=De avatar van de organisatie is aangepast. settings.delete=Verwijder organisatie settings.delete_account=Verwijder deze organisatie settings.delete_prompt=Deze organisatie zal permanent worden verwijderd. U kunt dit <strong>NIET</strong> ongedaan maken! settings.confirm_delete_account=Bevestig verwijdering -settings.delete_org_title=Verwijder organisatie -settings.delete_org_desc=Deze organisatie zal permanent verwijderd worden. Doorgaan? settings.hooks_desc=Een webhook toevoegen die door <strong>alle repositories</strong> in deze organisatie getriggerd kan worden. settings.labels_desc=Voeg labels toe die kunnen worden gebruikt bij problemen voor <strong>alle repositories</strong> in deze organisatie. diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 42a33f9ce4..d841658a9c 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -942,13 +942,13 @@ editor.file_changed_while_editing=Zawartość pliku zmieniÅ‚a siÄ™, odkÄ…d rozpo editor.commit_empty_file_header=Commituj pusty plik editor.commit_empty_file_text=Plik, który zamierzasz commitować, jest pusty. Kontynuować? editor.no_changes_to_show=Brak zmian do pokazania. -editor.fail_to_update_file_summary=Komunikat błędu: editor.push_rejected_summary=PeÅ‚ny komunikat odrzucenia: editor.add_subdir=Dodaj katalog… editor.no_commit_to_branch=Zatwierdzanie bezpoÅ›rednio do tej gałęzi nie jest możliwe, ponieważ: editor.user_no_push_to_branch=Użytkownik nie może wypychać do gałęzi editor.require_signed_commit=Gałąź wymaga podpisanych commitów + commits.desc=PrzeglÄ…daj historiÄ™ zmian kodu źródÅ‚owego. commits.commits=Commity commits.search_all=Wszystkie gałęzie @@ -1594,7 +1594,6 @@ settings.event_repository=Repozytorium settings.event_repository_desc=Repozytorium stworzone lub usuniÄ™te. settings.event_header_issue=Zdarzenia zgÅ‚oszeÅ„ settings.event_issues=ZgÅ‚oszenia -settings.event_issues_desc=ZgÅ‚oszenie otwarte, zamkniÄ™te, ponownie otwarte lub zmodyfikowane. settings.event_issue_assign=ZgÅ‚oszenie przypisane settings.event_issue_assign_desc=ZgÅ‚oszenie przypisane bÄ…dź nieprzypisane. settings.event_issue_label=ZgÅ‚oszenie oznaczone @@ -1605,7 +1604,6 @@ settings.event_issue_comment=Komentarz w zgÅ‚oszeniu settings.event_issue_comment_desc=Komentarz w zgÅ‚oszeniu stworzony, edytowany lub usuniÄ™ty. settings.event_header_pull_request=Zdarzenia Pull Requestów settings.event_pull_request=Pull Request -settings.event_pull_request_desc=Pull request otwarty, zamkniÄ™ty, ponownie otwarty lub zmodyfikowany. settings.event_pull_request_assign=Pull Request przypisany settings.event_pull_request_assign_desc=Pull Request przypisany bÄ…dz nieprzypisany. settings.event_pull_request_label=Pull Request zaetykietowany @@ -1862,14 +1860,13 @@ settings.visibility.private_shortname=Prywatny settings.update_settings=Aktualizuj ustawienia settings.update_setting_success=Ustawienia organizacji zostaÅ‚y zaktualizowane. -settings.change_orgname_redirect_prompt=Stara nazwa bÄ™dzie przekierowywaÅ‚a dopóki ktoÅ› jej nie zajmie. + + settings.update_avatar_success=Awatar organizacji zostaÅ‚ zaktualizowany. settings.delete=UsuÅ„ organizacjÄ™ settings.delete_account=UsuÅ„ tÄ… organizacjÄ™ settings.delete_prompt=Organizacja zostanie trwale usuniÄ™ta. Tej akcji <strong>NIE MOÅ»NA</strong> cofnąć! settings.confirm_delete_account=Potwierdź usuniÄ™cie -settings.delete_org_title=UsuÅ„ organizacjÄ™ -settings.delete_org_desc=Ta organizacja zostanie trwale usuniÄ™ta. Kontynuować? settings.hooks_desc=Dodaj webhooki, uruchamiane dla <strong>wszystkich repozytoriów</strong> w tej organizacji. settings.labels_desc=Dodaj etykiety, które mogÄ… być używane w zgÅ‚oszeniach dla <strong>wszystkich repozytoriów</strong> w tej organizacji. diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 8ee675e6e0..32e007577b 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1191,7 +1191,6 @@ editor.update=Atualizar %s editor.delete=Excluir %s editor.patch=Aplicar Correção editor.patching=Corrigindo: -editor.fail_to_apply_patch=`Não foi possÃvel aplicar a correção "%s"` editor.new_patch=Nova correção editor.commit_message_desc=Adicione uma descrição detalhada (opcional)... editor.signoff_desc=Adicione um assinado-por-committer no final do log do commit. @@ -1209,15 +1208,11 @@ editor.branch_already_exists=Branch "%s" já existe neste repositório. editor.directory_is_a_file=O nome do diretório "%s" já é usado como um nome de arquivo neste repositório. editor.file_is_a_symlink=`"%s" é um link simbólico. Links simbólicos não podem ser editados no editor da web` editor.filename_is_a_directory=O nome do arquivo "%s" já é usado como um nome de diretório neste repositório. -editor.file_editing_no_longer_exists=O arquivo que está sendo editado, "%s", não existe mais neste repositório. -editor.file_deleting_no_longer_exists=O arquivo a ser excluÃdo, "%s", não existe mais neste repositório. editor.file_changed_while_editing=O conteúdo do arquivo mudou desde que você começou a editar. <a target="_blank" rel="noopener noreferrer" href="%s">Clique aqui</a> para ver o que foi editado ou <strong>clique em Aplicar commit das alterações novamemente</strong> para sobreescrever estas alterações. editor.file_already_exists=Um arquivo com nome "%s" já existe neste repositório. editor.commit_empty_file_header=Fazer commit de um arquivo vazio editor.commit_empty_file_text=O arquivo que você está prestes fazer commit está vazio. Continuar? editor.no_changes_to_show=Nenhuma alteração a mostrar. -editor.fail_to_update_file=Falha ao atualizar/criar arquivo "%s". -editor.fail_to_update_file_summary=Mensagem de erro: editor.push_rejected_no_message=A alteração foi rejeitada pelo servidor sem uma mensagem. Por favor, verifique os Hooks Git. editor.push_rejected=A alteração foi rejeitada pelo servidor. Por favor, verifique os Hooks Git. editor.push_rejected_summary=Mensagem completa de rejeição: @@ -1232,6 +1227,7 @@ editor.require_signed_commit=Branch requer um commit assinado editor.cherry_pick=Cherry-pick %s para: editor.revert=Reverter %s para: + commits.desc=Veja o histórico de alterações do código de fonte. commits.commits=Commits commits.no_commits=Nenhum commit em comum. "%s" e "%s" tem históricos completamente diferentes. @@ -2104,7 +2100,6 @@ settings.event_repository=Repositório settings.event_repository_desc=Repositório criado ou excluÃdo. settings.event_header_issue=Eventos da Issue settings.event_issues=Issues -settings.event_issues_desc=Issue aberta, fechada, reaberta ou editada. settings.event_issue_assign=Issue AtribuÃda settings.event_issue_assign_desc=Issue atribuÃda ou não atribuÃda. settings.event_issue_label=Issue Rotulada @@ -2115,7 +2110,6 @@ settings.event_issue_comment=Comentário da issue settings.event_issue_comment_desc=Comentário da issue criado, editado ou excluÃdo. settings.event_header_pull_request=Eventos de Pull Request settings.event_pull_request=Pull request -settings.event_pull_request_desc=Pull request aberto, fechado, reaberto ou editado. settings.event_pull_request_assign=Pull Request AtribuÃdo settings.event_pull_request_assign_desc=Pull request atribuÃdo ou desatribuÃdo. settings.event_pull_request_label=Pull Request Rotulado @@ -2468,14 +2462,13 @@ settings.visibility.private_shortname=Privado settings.update_settings=Atualizar Configurações settings.update_setting_success=Configurações da organização foram atualizadas. -settings.change_orgname_redirect_prompt=O nome antigo irá redirecionar até que seja reivindicado. + + settings.update_avatar_success=O avatar da organização foi atualizado. settings.delete=Excluir organização settings.delete_account=Excluir esta organização settings.delete_prompt=A organização será excluÃda permanentemente. Isto <strong>NÃO PODERÃ</strong> ser desfeito! settings.confirm_delete_account=Confirmar exclusão -settings.delete_org_title=Excluir organização -settings.delete_org_desc=Essa organização será excluÃda permanentemente. Continuar? settings.hooks_desc=Adicionar Webhooks que serão acionados para <strong>todos os repositórios</strong> desta organização. settings.labels_desc=Adicionar rótulos que possam ser usadas em issues para <strong>todos os repositórios</strong> desta organização. diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 8da4216272..740de78809 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -421,6 +421,7 @@ remember_me.compromised=O identificador da sessão já não é válido, o que po forgot_password_title=Esqueci-me da senha forgot_password=Esqueceu a sua senha? need_account=Precisa de uma conta? +sign_up_tip=Você está a registar a primeira conta no sistema, que tem privilégios de administrador. Guarde cuidadosamente o nome de utilizador e a senha. Se se esquecer do nome de utilizador ou da senha, consulte a documentação do Gitea para recuperar a conta. sign_up_now=Inscreva-se agora. sign_up_successful=A conta foi criada com sucesso. Bem-vindo/a! confirmation_mail_sent_prompt_ex=Foi enviado um email de confirmação para <b>%s</b>. Verifique a sua caixa de entrada dentro de %s para completar o processo de registo. Se o seu endereço de email de registo estiver errado, pode iniciar a sessão novamente e mudá-lo. @@ -1228,6 +1229,7 @@ migrate.migrating_issues=Migrando questões migrate.migrating_pulls=Migrando pedidos de integração migrate.cancel_migrating_title=Cancelar migração migrate.cancel_migrating_confirm=Quer cancelar esta migração? +migration_status=Estado da migração mirror_from=réplica de forked_from=derivado de @@ -1331,7 +1333,9 @@ editor.upload_file=Carregar ficheiro editor.edit_file=Editar ficheiro editor.preview_changes=Pré-visualizar modificações editor.cannot_edit_lfs_files=Ficheiros LFS não podem ser editados na interface web. +editor.cannot_edit_too_large_file=O ficheiro é demasiado grande para ser editado. editor.cannot_edit_non_text_files=Ficheiros binários não podem ser editados na interface da web. +editor.file_not_editable_hint=Mas ainda pode renomeá-lo ou movê-lo. editor.edit_this_file=Editar ficheiro editor.this_file_locked=Ficheiro bloqueado editor.must_be_on_a_branch=Tem que estar num ramo para fazer ou propor modificações neste ficheiro. @@ -1351,7 +1355,7 @@ editor.update=Modificar %s editor.delete=Eliminar %s editor.patch=Aplicar remendo (patch) editor.patching=Remendando (patching): -editor.fail_to_apply_patch=`Não foi possÃvel aplicar o remendo (patch) "%s"` +editor.fail_to_apply_patch=Não foi possÃvel aplicar o remendo (patch) editor.new_patch=Novo remendo (patch) editor.commit_message_desc=Adicionar uma descrição alargada opcional… editor.signoff_desc=Adicionar "Assinado-por" seguido do autor do cometimento no fim da mensagem do registo de cometimentos. @@ -1371,8 +1375,7 @@ editor.branch_already_exists=O ramo "%s" já existe neste repositório. editor.directory_is_a_file=O nome da pasta "%s" já é usado como um nome de ficheiro neste repositório. editor.file_is_a_symlink=`"%s" é uma ligação simbólica. Ligações simbólicas não podem ser editadas no editor web` editor.filename_is_a_directory=O nome de ficheiro "%s" já está a ser usado como um nome de pasta neste repositório. -editor.file_editing_no_longer_exists=O ficheiro que está a ser editado, "%s", já não existe neste repositório. -editor.file_deleting_no_longer_exists=O ficheiro que está a ser eliminado, "%s", já não existe neste repositório. +editor.file_modifying_no_longer_exists=O ficheiro que está a ser modificado, "%s", já não existe neste repositório. editor.file_changed_while_editing=O conteúdo do ficheiro mudou desde que começou a editar. <a target="_blank" rel="noopener noreferrer" href="%s">Clique aqui</a> para ver as modificações ou clique em <strong>Cometer novamente</strong> para escrever por cima. editor.file_already_exists=Já existe um ficheiro com o nome "%s" neste repositório. editor.commit_id_not_matching=O ID do cometimento não corresponde ao ID de quando começou a editar. Faça o cometimento para um ramo de remendo (patch) e depois faça a integração. @@ -1380,8 +1383,6 @@ editor.push_out_of_date=O envio parece estar obsoleto. editor.commit_empty_file_header=Cometer um ficheiro vazio editor.commit_empty_file_text=O ficheiro que está prestes a cometer está vazio. Quer continuar? editor.no_changes_to_show=Não existem modificações para mostrar. -editor.fail_to_update_file=Falhou ao modificar/criar o ficheiro "%s". -editor.fail_to_update_file_summary=Mensagem de erro: editor.push_rejected_no_message=A modificação foi rejeitada pelo servidor sem qualquer mensagem. Verifique os Automatismos do Git. editor.push_rejected=A modificação foi rejeitada pelo servidor. Verifique os Automatismos do Git. editor.push_rejected_summary=Mensagem completa de rejeição: @@ -1395,6 +1396,15 @@ editor.user_no_push_to_branch=O utilizador não pode enviar para o ramo editor.require_signed_commit=O ramo requer um cometimento assinado editor.cherry_pick=Escolher a dedo %s para: editor.revert=Reverter %s para: +editor.failed_to_commit=Falhou ao cometer as modificações. +editor.failed_to_commit_summary=Mensagem de erro: + +editor.fork_create=Faça uma derivação do repositório para propor modificações +editor.fork_create_description=Não pode editar este repositório. Ao invés disso, crie uma derivação, faça as modificações nessa derivação e crie um pedido de integração. +editor.fork_edit_description=Não pode editar este repositório. As modificações irão ser escritas na sua derivação <b>%s</b>, para que possa criar um pedido de integração. +editor.fork_not_editable=Fez uma derivação deste repositório, mas a sua derivação não é editável. +editor.fork_failed_to_push_branch=Falhou ao enviar o ramo %s para o seu repositório. +editor.fork_branch_exists=O ramo "%s" já existe na sua derivação, escolha outro nome para o ramo. commits.desc=Navegar pelo histórico de modificações no código fonte. commits.commits=Cometimentos @@ -1523,7 +1533,7 @@ issues.remove_labels=removeu os rótulos %s %s issues.add_remove_labels=adicionou o(s) rótulo(s) %s e removeu %s %s issues.add_milestone_at=`adicionou esta questão à etapa <b>%s</b> %s` issues.add_project_at=`adicionou esta questão ao planeamento <b>%s</b> %s` -issues.move_to_column_of_project=`isto foi movido para %s dentro de %s em %s` +issues.move_to_column_of_project=`moveu isto para %s em %s %s` issues.change_milestone_at=`modificou a etapa de <b>%s</b> para <b>%s</b> %s` issues.change_project_at=`modificou o planeamento de <b>%s</b> para <b>%s</b> %s` issues.remove_milestone_at=`removeu esta questão da etapa <b>%s</b> %s` @@ -1552,13 +1562,14 @@ issues.filter_project=Planeamento issues.filter_project_all=Todos os planeamentos issues.filter_project_none=Nenhum planeamento issues.filter_assignee=Encarregado -issues.filter_assignee_no_assignee=Não atribuÃdo -issues.filter_assignee_any_assignee=AtribuÃdo a qualquer pessoa +issues.filter_assignee_no_assignee=Não atribuÃda +issues.filter_assignee_any_assignee=AtribuÃda a alguém issues.filter_poster=Autor(a) issues.filter_user_placeholder=Procurar utilizadores issues.filter_user_no_select=Todos os utilizadores issues.filter_type=Tipo issues.filter_type.all_issues=Todas as questões +issues.filter_type.all_pull_requests=Todos os pedidos de integração issues.filter_type.assigned_to_you=AtribuÃdas a si issues.filter_type.created_by_you=Criadas por si issues.filter_type.mentioning_you=Mencionando a si @@ -1650,6 +1661,7 @@ issues.save=Guardar issues.label_title=Nome do rótulo issues.label_description=Descrição do rótulo issues.label_color=Cor do rótulo +issues.label_color_invalid=Cor inválida issues.label_exclusive=Exclusivo issues.label_archive=Arquivar rótulo issues.label_archived_filter=Mostrar rótulos arquivados @@ -1714,6 +1726,8 @@ issues.remove_time_estimate_at=removeu a estimativa de tempo %s issues.time_estimate_invalid=O formato da estimativa de tempo é inválido issues.start_tracking_history=começou a trabalhar %s issues.tracker_auto_close=O cronómetro será parado automaticamente quando esta questão for fechada +issues.stopwatch_already_stopped=O cronómetro para esta questão já está parado +issues.stopwatch_already_created=O cronómetro para esta questão já existe issues.tracking_already_started=`Você já iniciou a contagem de tempo <a href="%s">noutra questão</a>!` issues.stop_tracking=Parar cronómetro issues.stop_tracking_history=trabalhou durante <b>%[1]s</b> %[2]s @@ -1955,7 +1969,7 @@ pulls.cmd_instruction_checkout_title=Checkout pulls.cmd_instruction_checkout_desc=A partir do seu repositório, crie um novo ramo e teste nele as modificações. pulls.cmd_instruction_merge_title=Integrar pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea. -pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque "auto-identificar integração manual" não estava habilitado +pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque a opção "auto-identificar integração manual" não está habilitada. pulls.clear_merge_message=Apagar mensagem de integração pulls.clear_merge_message_hint=Apagar a mensagem de integração apenas remove o conteúdo da mensagem de cometimento e mantém os rodapés do git, tais como "Co-Autorado-Por …". @@ -2151,6 +2165,7 @@ settings.collaboration.write=Escrita settings.collaboration.read=Leitura settings.collaboration.owner=Proprietário(a) settings.collaboration.undefined=Não definido +settings.collaboration.per_unit=Permissões da Unidade settings.hooks=Automatismos web settings.githooks=Automatismos do Git settings.basic_settings=Configurações básicas @@ -2369,7 +2384,7 @@ settings.event_repository=Repositório settings.event_repository_desc=Repositório criado ou eliminado. settings.event_header_issue=Eventos da questão settings.event_issues=Questões -settings.event_issues_desc=Questão aberta, fechada, reaberta ou editada. +settings.event_issues_desc=Questão aberta, fechada, reaberta, editada ou eliminada. settings.event_issue_assign=Questão atribuÃda settings.event_issue_assign_desc=Encarregado atribuÃdo ou retirado à questão. settings.event_issue_label=Questão com rótulo @@ -2380,7 +2395,7 @@ settings.event_issue_comment=Comentário da questão settings.event_issue_comment_desc=Comentário da questão criado, editado ou eliminado. settings.event_header_pull_request=Eventos de pedidos de integração settings.event_pull_request=Pedido de integração -settings.event_pull_request_desc=Pedido de integração aberto, fechado, reaberto ou editado. +settings.event_pull_request_desc=Pedido de integração aberto, fechado, reaberto, editado ou eliminado. settings.event_pull_request_assign=Encarregado atribuÃdo ao pedido de integração settings.event_pull_request_assign_desc=Encarregado atribuÃdo ou retirado ao pedido de integração. settings.event_pull_request_label=Rótulo atribuÃdo ao pedido de integração @@ -2398,8 +2413,10 @@ settings.event_pull_request_review_request_desc=A revisão do pedido de integraà settings.event_pull_request_approvals=Aprovações do pedido de integração settings.event_pull_request_merge=Integração constante no pedido settings.event_header_workflow=Eventos da sequência de trabalho +settings.event_workflow_run=Execução da sequência de trabalho +settings.event_workflow_run_desc=A execução da sequência de trabalho das operações do Gitea foi colocada em fila, está em espera, em andamento ou concluÃda. settings.event_workflow_job=Trabalhos da sequência de trabalho -settings.event_workflow_job_desc=O trabalho da sequência de trabalho das operações do Gitea foi colocado em fila, está em espera, em andamento ou concluÃda. +settings.event_workflow_job_desc=O trabalho da sequência de trabalho das operações do Gitea foi colocado em fila, está em espera, em andamento ou concluÃdo. settings.event_package=Pacote settings.event_package_desc=Pacote criado ou eliminado num repositório. settings.branch_filter=Filtro de ramos @@ -2752,6 +2769,8 @@ branch.new_branch_from=`Criar um novo ramo a partir do ramo "%s"` branch.renamed=O ramo %s foi renomeado para %s. branch.rename_default_or_protected_branch_error=Só os administradores é que podem renomear o ramo principal ou ramos protegidos. branch.rename_protected_branch_failed=Este ramo está protegido por regras de salvaguarda baseadas em padrões glob. +branch.commits_divergence_from=Divergência nos cometimentos: %[1]d atrás e %[2]d à frente de %[3]s +branch.commits_no_divergence=Idêntico ao ramo %[1]s tag.create_tag=Criar etiqueta %s tag.create_tag_operation=Criar etiqueta @@ -2765,6 +2784,7 @@ topic.done=ConcluÃdo topic.count_prompt=Não pode escolher mais do que 25 tópicos topic.format_prompt=Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') ou pontos ('.') e podem ter até 35 caracteres. As letras têm que ser minúsculas. +find_file.follow_symlink=Seguir esta ligação simbólica para onde ela está apontando find_file.go_to_file=Ir para o ficheiro find_file.no_matching=Não foi encontrado qualquer ficheiro correspondente @@ -2805,6 +2825,7 @@ team_permission_desc=Permissão team_unit_desc=Permitir acesso à s secções do repositório team_unit_disabled=(desabilitada) +form.name_been_taken=O nome da organização "%s" já foi tomado. form.name_reserved=O nome de organização "%s" está reservado. form.name_pattern_not_allowed=O padrão "%s" não é permitido no nome de uma organização. form.create_org_not_allowed=Não tem permissão para criar uma organização. @@ -2826,15 +2847,28 @@ settings.visibility.private_shortname=Privado settings.update_settings=Modificar configurações settings.update_setting_success=As configurações da organização foram modificadas. -settings.change_orgname_prompt=Nota: Mudar o nome da organização também irá mudar o URL da organização e libertar o nome antigo. -settings.change_orgname_redirect_prompt=O nome antigo, enquanto não for reivindicado, irá reencaminhar para o novo. + +settings.rename=Renomear organização +settings.rename_desc=Mudar o nome da organização também irá mudar o URL da organização e libertar o nome antigo. +settings.rename_success=A organização %[1]s foi renomeada para %[2]s com sucesso. +settings.rename_no_change=O nome da organização não foi alterado. +settings.rename_new_org_name=Novo nome da organização +settings.rename_failed=A renomeação da organização falhou por causa de um erro interno +settings.rename_notices_1=Esta operação <strong>NÃO PODERÃ</strong> ser revertida. +settings.rename_notices_2=O antigo nome, enquanto não for reivindicado, irá reencaminhar para o novo. + settings.update_avatar_success=O avatar da organização foi modificado. settings.delete=Eliminar organização settings.delete_account=Eliminar esta organização settings.delete_prompt=A organização será removida permanentemente. Essa operação <strong>NÃO PODERÃ</strong> ser revertida! +settings.name_confirm=Insira o nome da organização para confirmar: +settings.delete_notices_1=Esta operação <strong>NÃO PODERÃ</strong> ser revertida. +settings.delete_notices_2=Esta operação irá eliminar de forma permanente todos os <strong>repositórios</strong> de <strong>%s</strong>, incluindo código-fonte, questões, comentários, dados do wiki e configurações dos colaboradores. +settings.delete_notices_3=Esta operação irá eliminar de forma permanente todos os <strong>pacotes</strong> de <strong>%s</strong>. +settings.delete_notices_4=Esta operação irá eliminar de forma permanente todos os <strong>planeamentos</strong> de <strong>%s</strong>. settings.confirm_delete_account=Confirme a eliminação -settings.delete_org_title=Eliminar organização -settings.delete_org_desc=Esta organização será eliminada permanentemente. Quer continuar? +settings.delete_failed=A eliminação da organização falhou por causa de um erro interno +settings.delete_successful=A organização <b>%s</b> foi eliminada com sucesso. settings.hooks_desc=Adicionar automatismos web que serão despoletados para <strong>todos os repositórios</strong> desta organização. settings.labels_desc=Adicionar rótulos que possam ser usados em questões para <strong>todos os repositórios</strong> desta organização. @@ -3811,8 +3845,10 @@ runs.no_runs=A sequência de trabalho ainda não foi executada. runs.empty_commit_message=(mensagem de cometimento vazia) runs.expire_log_message=Os registros foram removidos porque eram muito antigos. runs.delete=Eliminar execução da sequência de trabalho +runs.cancel=Cancelar a execução da sequência de trabalho runs.delete.description=Tem a certeza que pretende eliminar permanentemente a execução desta sequência de trabalho? Esta operação não poderá ser desfeita. runs.not_done=A execução desta sequência de trabalho ainda não terminou. +runs.view_workflow_file=Ver ficheiro da sequência de trabalho workflow.disable=Desabilitar sequência de trabalho workflow.disable_success=A sequência de trabalho '%s' foi desabilitada com sucesso. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index c65d08a4cf..a44365f43c 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1169,7 +1169,6 @@ editor.update=Обновить %s editor.delete=Удалить %s editor.patch=Применить патч editor.patching=ИÑправление: -editor.fail_to_apply_patch=Ðевозможно применить патч «%s» editor.new_patch=Ðовый патч editor.commit_message_desc=Добавьте необÑзательное раÑширенное опиÑание… editor.signoff_desc=Добавить трейлер Signed-off-by Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¾Ð¼ коммита в конце ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ ÐºÐ¾Ð¼Ð¼Ð¸Ñ‚Ð°. @@ -1187,15 +1186,11 @@ editor.branch_already_exists=Ветка «%s» уже ÑущеÑтвует в Ñ editor.directory_is_a_file=Ð˜Ð¼Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ð° «%s» уже иÑпользуетÑÑ Ð² качеÑтве имени файла в Ñтом репозитории. editor.file_is_a_symlink=`«%s» ÑвлÑетÑÑ ÑимволичеÑкой ÑÑылкой. СимволичеÑкие ÑÑылки невозможно отредактировать в веб-редакторе` editor.filename_is_a_directory=Ð˜Ð¼Ñ Ñ„Ð°Ð¹Ð»Ð° «%s» уже иÑпользуетÑÑ Ð² качеÑтве каталога в Ñтом репозитории. -editor.file_editing_no_longer_exists=Редактируемый файл «%s» больше не ÑущеÑтвует в Ñтом репозитории. -editor.file_deleting_no_longer_exists=УдалÑемый файл «%s» больше не ÑущеÑтвует в Ñтом репозитории. editor.file_changed_while_editing=Содержимое файла изменилоÑÑŒ Ñ Ð¼Ð¾Ð¼ÐµÐ½Ñ‚Ð° начала редактированиÑ. <a target="_blank" rel="noopener noreferrer" href="%s">Ðажмите здеÑÑŒ</a>, чтобы увидеть, что было изменено, или <strong>ЗафикÑировать Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñнова</strong>, чтобы заменить их. editor.file_already_exists=Файл Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ «%s» уже ÑущеÑтвует в репозитории. editor.commit_empty_file_header=Закоммитить пуÑтой файл editor.commit_empty_file_text=Файл, который вы ÑобираетеÑÑŒ зафикÑировать, пуÑÑ‚. Продолжить? editor.no_changes_to_show=Ðет изменений. -editor.fail_to_update_file=Ðе удалоÑÑŒ обновить/Ñоздать файл «%s». -editor.fail_to_update_file_summary=Ошибка: editor.push_rejected_no_message=Изменение отклонено Ñервером без ÑообщениÑ. ПожалуйÑта, проверьте хуки Git. editor.push_rejected=Изменение отклонено Ñервером. ПожалуйÑта, проверьте хуки Git. editor.push_rejected_summary=Полное Ñообщение об отклонении: @@ -1210,6 +1205,7 @@ editor.require_signed_commit=Ветка ожидает подпиÑанный к editor.cherry_pick=ПеренеÑти Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ %s в: editor.revert=Откатить %s к: + commits.desc=ПроÑмотр иÑтории изменений иÑходного кода. commits.commits=Коммитов commits.no_commits=Ðет общих коммитов. «%s» и «%s» имеют Ñовершенно разные иÑтории. @@ -2074,7 +2070,6 @@ settings.event_repository=Репозиторий settings.event_repository_desc=Репозиторий Ñоздан или удален. settings.event_header_issue=Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸ settings.event_issues=Задачи -settings.event_issues_desc=Задача открыта, закрыта, переоткрыта или отредактирована. settings.event_issue_assign=Ðазначена задача settings.event_issue_assign_desc=Задача назначена или ÑнÑта Ñ Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ. settings.event_issue_label=Ярлык задачи @@ -2085,7 +2080,6 @@ settings.event_issue_comment=Комментарии в задаче settings.event_issue_comment_desc=Комментарий Ñоздан, изменён или удалён. settings.event_header_pull_request=Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа на ÑлиÑние settings.event_pull_request=Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние -settings.event_pull_request_desc=Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние открыт, закрыт, переоткрыт или отредактирован. settings.event_pull_request_assign=ЗапроÑа на ÑлиÑние назначен settings.event_pull_request_assign_desc=Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние назначен или не назначен. settings.event_pull_request_label=Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние отмечен @@ -2455,15 +2449,13 @@ settings.visibility.private_shortname=Приватный settings.update_settings=Обновить наÑтройки settings.update_setting_success=ÐаÑтройки организации обновлены. -settings.change_orgname_prompt=Обратите внимание: изменение Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ñ Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ð¸ также изменит URL вашей организации и оÑвободит Ñтарое имÑ. -settings.change_orgname_redirect_prompt=Старое Ð¸Ð¼Ñ Ð±ÑƒÐ´ÐµÑ‚ перенаправлено до тех пор, пока оно не будет введено. + + settings.update_avatar_success=Ðватар организации обновлён. settings.delete=Удалить организацию settings.delete_account=Удалить Ñту организацию settings.delete_prompt=Ðто дейÑтвие <strong>БЕЗВОЗВРÐТÐО</strong> удалит Ñту организацию навÑегда! settings.confirm_delete_account=Подтвердить удаление -settings.delete_org_title=Удалить организацию -settings.delete_org_desc=Ðта Ð¾Ñ€Ð³Ð°Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð±ÑƒÐ´ÐµÑ‚ безвозвратно удалена. Продолжить? settings.hooks_desc=Добавьте веб-хуки, которые будет вызыватьÑÑ Ð´Ð»Ñ <strong>вÑех репозиториев</strong> под Ñтой организации. settings.labels_desc=Добавьте метки, которые могут быть иÑпользованы в задачах Ð´Ð»Ñ <strong>вÑех репозиториев</strong> Ñтой организации. diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index a209187aff..286433074e 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -917,13 +917,13 @@ editor.file_changed_while_editing=ඔබ සංස්කරණය කිරීචeditor.commit_empty_file_header=හිස් ගොනුවක් à¶šà·à¶´ කරන්න editor.commit_empty_file_text=ඔබ à¶šà·à¶´ කිරීමට යන ගොනුව හිස් ය. ඉදිරියට? editor.no_changes_to_show=පෙන්වීමට කිසිදු වෙනසක් à¶±à·à¶. -editor.fail_to_update_file_summary=දà·à·‚ පණිවිඩය: editor.push_rejected_summary=පූර්ණ à¶´à·Šà¶»à¶à·’à¶šà·Šà·‚à·šà¶´ පණිවිඩය: editor.add_subdir=ඩිරෙක්ටරියක් à¶‘à¶šà·Š කරන්න… editor.no_commit_to_branch=à·à·à¶›à·à·€à¶§ කෙලින්ම à¶šà·à¶´à·€à·’ය නොහà·à¶šà·’ නිසà·: editor.user_no_push_to_branch=පරිà·à·“ලකයà·à¶§ à·à·à¶›à·à·€à¶§ à¶à¶½à·Šà¶½à·” à¶šà·… නොහà·à¶š editor.require_signed_commit=à·à·à¶›à·à·€à¶§ à¶…à¶à·Šà·ƒà¶±à·Š à¶šà·… à¶šà·à¶´à·€à·“මක් à¶…à·€à·à·Šà¶º වේ + commits.desc=මූලà·à·à·Šà¶» කේචවෙනස් කිරීමේ ඉà¶à·’à·„à·à·ƒà¶º පිරික්සන්න. commits.commits=විවරයන් commits.nothing_to_compare=මෙම à·à·à¶›à· සමà·à¶± වේ. @@ -1596,7 +1596,6 @@ settings.event_repository=à¶šà·à·‚්ඨය settings.event_repository_desc=ගබඩà·à·€ නිර්මà·à¶«à¶º කරන ලද හ෠මක෠දමන ලදි. settings.event_header_issue=නිකුà¶à·Š කිරීමේ සිදුවීම් settings.event_issues=à¶œà·à¶§à·…à·” -settings.event_issues_desc=නිකුà¶à·Š කිරීම විවෘචකිරීම, වස෠දà·à¶¸à·“ම, à¶±à·à·€à¶ විවෘචකිරීම හ෠සංස්කරණය කිරීම. settings.event_issue_assign=පවර෠ඇà¶à·’ නිකුà¶à·”à·€ settings.event_issue_assign_desc=පවර෠ඇà¶à·’ හ෠පවර෠නොමà·à¶à·’ නිකුà¶à·Š කිරීම. settings.event_issue_label=ලේබල් නිකුà¶à·Š @@ -1607,7 +1606,6 @@ settings.event_issue_comment=නිකුà¶à·”à·€ settings.event_issue_comment_desc=නිකුà¶à·Š අදහස් නිර්මà·à¶«à¶º, සංස්කරණය, හ෠මකà·. settings.event_header_pull_request=ඉල්ලීම් සිදුවීම් අදින්න settings.event_pull_request=ඉල්ලීම අදින්න -settings.event_pull_request_desc=අදින්න ඉල්ලීම විවෘà¶, වසà·, à¶±à·à·€à¶ විවෘà¶, හ෠සංස්කරණය. settings.event_pull_request_assign=පවර෠ඉල්ලීම අදින්න settings.event_pull_request_assign_desc=පවර෠ඇà¶à·’ හ෠පවර෠නොමà·à¶à·’ ඉල්ලීම අදින්න. settings.event_pull_request_label=ලේබල් ඉල්ලීම අදින්න @@ -1882,14 +1880,13 @@ settings.visibility.private_shortname=පෞද්ගලික settings.update_settings=à·ƒà·à¶šà·ƒà·”ම් යà·à·€à¶à·Šà¶šà·à¶½ කරන්න settings.update_setting_success=සංවිධà·à¶±à¶ºà·š à·ƒà·à¶šà·ƒà·”ම් යà·à·€à¶à·Šà¶šà·à¶½ à¶šà¶» ඇà¶. -settings.change_orgname_redirect_prompt=à¶´à·à¶»à¶«à·’ නම ඉල්ල෠සිටින à¶à·”රු à¶±à·à·€à¶ හරව෠යවයි. + + settings.update_avatar_success=සංවිධà·à¶±à¶ºà·š à¶…à·€à¶à·à¶»à¶º යà·à·€à¶à·Šà¶šà·à¶½à·“à¶± à¶šà¶» ඇà¶. settings.delete=සංවිධà·à¶±à¶º මකන්න settings.delete_account=මෙම සංවිධà·à¶±à¶º මකන්න settings.delete_prompt=සංවිධà·à¶±à¶º ස්ථිරවම ඉවà¶à·Š කරනු à¶½à·à¶¶à·š. මෙම <strong></strong> à¶…à·„à·à·ƒà·’ à¶šà·… නොහà·à¶š! settings.confirm_delete_account=මකà·à¶¯à·à¶¸à·“ම à¶à·„වුරු කරන්න -settings.delete_org_title=සංවිධà·à¶±à¶º මකන්න -settings.delete_org_desc=මෙම සංවිධà·à¶±à¶º ස්ථිරවම මක෠දමනු ඇà¶. දිගටම? settings.hooks_desc=මෙම සංවිධà·à¶±à¶º යටà¶à·š <strong>සියලුම ගබඩà·à·€à¶±à·Š</strong> සඳහ෠මුලපුරනු ලබන වෙබ් කොකු à¶‘à¶šà¶à·” කරන්න. settings.labels_desc=මෙම සංවිධà·à¶±à¶º යටà¶à·š <strong>සියලුම ගබඩà·à·€à¶½à¶¯à·“</strong> සඳහ෠ගà·à¶§à·…à·” සඳහ෠භà·à·€à·’à¶à· à¶šà·… à·„à·à¶šà·’ ලේබල් à¶‘à¶šà¶à·” කරන්න. diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index e461075e53..5ea6a7508e 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -1007,6 +1007,7 @@ editor.commit_empty_file_text=Súbor, ktorý sa chystáte odoslaÅ¥, je prázdny. editor.no_commit_to_branch=Nedá sa odoslaÅ¥ priamo do vetvy, pretože: editor.require_signed_commit=Vetva vyžaduje podpÃsaný commit + commits.commits=Commity commits.search_all=VÅ¡etky vetvy commits.author=Autor @@ -1219,6 +1220,8 @@ lower_repositories=repozitáre settings.visibility.private=Súkromná ​​(viditeľné iba pre Älenov organizácie) settings.visibility.private_shortname=Súkromný + + settings.hooks_desc=Pridajte webhooky, ktoré sa spustia nad <strong>vÅ¡etkými repozitármi</strong> v rámci tejto organizácie. diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 04428aeab2..7041db9ac3 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -777,12 +777,12 @@ editor.file_changed_while_editing=Filens innehÃ¥ll har ändrats sedan du pÃ¥bör editor.commit_empty_file_header=Committa en tom fil editor.commit_empty_file_text=Filen du vill committa är tom. Vill du fortsätta? editor.no_changes_to_show=Det finns inga ändringar att visa. -editor.fail_to_update_file_summary=Felmeddelande: editor.add_subdir=Lägga till en katalog… editor.no_commit_to_branch=Det gick inte att committa direkt till branchen för: editor.user_no_push_to_branch=Användaren kan inte pusha till branchen editor.require_signed_commit=Branchen kräver en signerad commit + commits.desc=Bläddra i källkodens förändringshistorik. commits.commits=Incheckningar commits.search_all=Alla brancher @@ -1524,13 +1524,13 @@ settings.visibility.private=Privat (synlig endast för organisationens medlemmar settings.visibility.private_shortname=Privat settings.update_settings=Uppdatera inställningar + + settings.update_avatar_success=Organisationens avatar har uppdateras. settings.delete=Tag bort organisation settings.delete_account=Tag bort denna organisation settings.delete_prompt=Organisationen kommer tas bort permanent, och det gÃ¥r <strong>INTE</strong> att Ã¥ngra detta! settings.confirm_delete_account=Bekräfta borttagning -settings.delete_org_title=Ta bort organisation -settings.delete_org_desc=Denna organisation kommer tas bort permanent. Vill du fortsätta? settings.hooks_desc=Lägg till webbhook som triggas för <strong>alla utvecklingskataloger</strong> under denna organisationen. settings.labels_desc=Lägg till etiketter som kan användas till ärenden för <strong>alla utvecklingskataloger</strong> under denna organisation. diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 43ff6495c1..43802b3ac2 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -421,6 +421,7 @@ remember_me.compromised=Oturum açma tokeni artık geçerli deÄŸil, bu ele geçi forgot_password_title=Åžifremi unuttum forgot_password=Åžifrenizi mi unuttunuz? need_account=Bir hesaba mı ihtiyacın var? +sign_up_tip=Sistemdeki yönetici ayrıcalıklarına sahip olan ilk hesabı oluÅŸturuyorsunuz. Lütfen kullanıcı adınızı ve parolanızı unutmamaya dikkat edin. Kullanıcı adı veya parolayı unutursanız, hesabı kurtarmak için lütfen Gitea belgelerine bakın. sign_up_now=Hemen kaydolun. sign_up_successful=Hesap baÅŸarılı bir ÅŸekilde oluÅŸturuldu. HoÅŸgeldiniz! confirmation_mail_sent_prompt_ex=Yeni bir doÄŸrulama e-postası <b>%s</b> adresine gönderildi. Lütfen kayıt sürecini tamamlamak için %s içinde gelen kutunuzu denetleyin. EÄŸer kayıt e-posta adresiniz hatalı ise, tekrar oturum açıp deÄŸiÅŸtirebilirsiniz. @@ -984,8 +985,10 @@ scan_this_image=Kim doÄŸrulama uygulamanızla bu görüntüyü tarayın: or_enter_secret=Veya gizli ÅŸeyi girin: %s then_enter_passcode=Ve uygulamada gösterilen ÅŸifreyi girin: passcode_invalid=Åžifre geçersiz. Tekrar deneyin. +twofa_enrolled=Hesabınız baÅŸarıyla kaydedildi. Tek kullanımlık geçici kodunuz (%s) tekrar gösterilmeyeceÄŸi için güvenilir bir yerde saklayın. twofa_failed_get_secret=Gizlilik elde edilemedi. +webauthn_desc=Güvenlik anahtarları, ÅŸifreleme anahtarlarını içeren donanım aygıtlarıdır. İki aÅŸamalı kimlik doÄŸrulama için kullanılabilirler. Güvenlik anahtarları <a rel="noreferrer" target="_blank" href="%s">WebAuthn Authenticator</a> standardını desteklemelidir. webauthn_register_key=Güvenlik Anahtarı Ekle webauthn_nickname=Takma Ad webauthn_delete_key=Güvenlik Anahtarını Kaldır @@ -1034,6 +1037,7 @@ owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle aà repo_name=Depo İsmi repo_name_profile_public_hint=.profile herkese açık organizasyonunuzun profiline herkesin görüntüleyebileceÄŸi bir README.md dosyası eklemek için kullanabileceÄŸiniz özel bir depodur. BaÅŸlamak için herkese açık olduÄŸundan ve profile dizininde README ile baÅŸladığınızdan emin olun. repo_name_profile_private_hint=.profile-private organizasyonunuzun üye profiline sadece organizasyon üyelerinin görüntüleyebileceÄŸi bir README.md eklemek için kullanabileceÄŸiniz özel bir depodur. BaÅŸlamak için özel olduÄŸundan ve profil dizininde README ile baÅŸladığınızdan emin olun. +repo_name_helper=İyi depo isimleri kısa, akılda kalıcı ve benzersiz anahtar kelimeler kullanır. “.profile†veya ‘.profile-private’ adlı bir depo, kullanıcı/kuruluÅŸ profili için bir README.md eklemek için kullanılabilir. repo_size=Depo Boyutu template=Åžablon template_select=Bir ÅŸablon seçin. @@ -1211,6 +1215,7 @@ migrate.gogs.description=Notabug.org veya diÄŸer Gogs sunucularından veri aktar migrate.onedev.description=Code.onedev.io ve diÄŸer OneDev sunucularından veri aktar. migrate.codebase.description=Codebasehq.com sitesinden veri aktar. migrate.gitbucket.description=GitBucket sunucularından veri aktar. +migrate.codecommit.description=AWS CodeCommit'ten veri aktar. migrate.codecommit.aws_access_key_id=AWS EriÅŸim Anahtarı KimliÄŸi migrate.codecommit.aws_secret_access_key=AWS Gizli EriÅŸim Anahtarı migrate.codecommit.https_git_credentials_username=HTTPS Git KimliÄŸi Kullanıcı Adı @@ -1224,6 +1229,7 @@ migrate.migrating_issues=Konuları Taşıma migrate.migrating_pulls=DeÄŸiÅŸiklik İsteklerini Taşıma migrate.cancel_migrating_title=Göçü İptal Et migrate.cancel_migrating_confirm=Bu göçü iptal etmek istiyor musunuz? +migration_status=Aktarma durumu mirror_from=ÅŸunun yansıması forked_from=ÅŸundan çatallanmış @@ -1293,6 +1299,7 @@ ambiguous_runes_header=`Bu dosya muÄŸlak Evrensel Kodlu karakter içeriyor` ambiguous_runes_description=`Bu dosya, baÅŸka karakterlerle karıştırılabilecek evrensel kodlu karakter içeriyor. EÄŸer bunu kasıtlı olarak yaptıysanız bu uyarıyı yok sayabilirsiniz. Gizli karakterleri göstermek için Kaçış Karakterli düğmesine tıklayın.` invisible_runes_line=`Bu satırda görünmez evrensel kodlu karakter var` ambiguous_runes_line=`Bu satırda muÄŸlak evrensel kodlu karakter var` +ambiguous_character=`%[1]c [U+%04[1]X], %[2]c [U+%04[2]X] ile karıştırılabilir` escape_control_characters=Kaçış Karakterli unescape_control_characters=Kaçış Karaktersiz @@ -1325,7 +1332,9 @@ editor.upload_file=Dosya Yükle editor.edit_file=Dosyayı Düzenle editor.preview_changes=DeÄŸiÅŸiklikleri Önizle editor.cannot_edit_lfs_files=LFS dosyaları web arayüzünde düzenlenemez. +editor.cannot_edit_too_large_file=Bu dosya düzenlenmek için çok büyük. editor.cannot_edit_non_text_files=Bu tür dosyalar web arayüzünden düzenlenemez. +editor.file_not_editable_hint=Ama yine de ismini deÄŸiÅŸtirebilir veya taşıyabilirsiniz. editor.edit_this_file=Dosyayı Düzenle editor.this_file_locked=Dosya kilitlendi editor.must_be_on_a_branch=Bu dosyada deÄŸiÅŸiklik yapmak veya önermek için bir dalda olmalısınız. @@ -1345,7 +1354,7 @@ editor.update=%s Güncelle editor.delete=%s Sil editor.patch=Yama Uygula editor.patching=Yamalanıyor: -editor.fail_to_apply_patch=`"%s" yaması uygulanamıyor` +editor.fail_to_apply_patch=Yama uygulanamıyor editor.new_patch=Yeni Yama editor.commit_message_desc=İsteÄŸe baÄŸlı uzun bir açıklama ekleyin… editor.signoff_desc=İşleme günlüğü mesajının sonuna iÅŸleyen tarafından imzalanan bir fragman ekleyin. @@ -1365,8 +1374,7 @@ editor.branch_already_exists=Bu depoda "%s" dalı zaten var. editor.directory_is_a_file=Dizin adı "%s" zaten bu depoda bir dosya adı olarak kullanılmaktadır. editor.file_is_a_symlink=`"%s" sembolik bir baÄŸlantıdır. Sembolik baÄŸlantılar web düzenleyicisinde düzenlenemez` editor.filename_is_a_directory=Dosya adı "%s" zaten bu depoda bir dizin adı olarak kullanılmaktadır. -editor.file_editing_no_longer_exists=Düzenlenmekte olan "%s" dosyası artık bu depoda yer almıyor. -editor.file_deleting_no_longer_exists=Silinen "%s" dosyası artık bu depoda yer almıyor. +editor.file_modifying_no_longer_exists=DeÄŸiÅŸtirilmekte olan "%s" dosyası artık bu depoda yer almıyor. editor.file_changed_while_editing=Düzenlemeye baÅŸladığınızdan beri dosya içeriÄŸi deÄŸiÅŸti. Görmek için <a target="_blank" rel="noopener noreferrer" href="%s">burayı tıklayın</a> veya üzerine yazmak için <strong>deÄŸiÅŸiklikleri yine de iÅŸleyin</strong>. editor.file_already_exists=Bu depoda "%s" isimli bir dosya zaten var. editor.commit_id_not_matching=İşleme ID'si, düzenlemeye baÅŸladığınız ID ile uyuÅŸmuyor, bir yama dalına iÅŸleme yapın ve sonra birleÅŸtirin. @@ -1374,8 +1382,6 @@ editor.push_out_of_date=İtme eskimiÅŸ. editor.commit_empty_file_header=BoÅŸ bir dosya iÅŸle editor.commit_empty_file_text=İşlemek üzere olduÄŸunuz dosya boÅŸ. Devam edilsin mi? editor.no_changes_to_show=Gösterilecek deÄŸiÅŸiklik yok. -editor.fail_to_update_file=`"%s" dosyası güncellenemedi/oluÅŸturulamadı.` -editor.fail_to_update_file_summary=Hata Mesajı: editor.push_rejected_no_message=DeÄŸiÅŸiklik, bir ileti olmadan sunucu tarafından reddedildi. Git Hooks'u kontrol edin. editor.push_rejected=DeÄŸiÅŸiklik sunucu tarafından reddedildi. Lütfen Git Hooks'u kontrol edin. editor.push_rejected_summary=Tam Red Mesajı: @@ -1389,6 +1395,15 @@ editor.user_no_push_to_branch=Kullanıcı dala gönderemez editor.require_signed_commit=Dal imzalı bir iÅŸleme gerektirir editor.cherry_pick=%s ÅŸunun üzerine cımbızla: editor.revert=%s ÅŸuna geri döndür: +editor.failed_to_commit=DeÄŸiÅŸikliklerin iÅŸlenmesi baÅŸarısız oldu. +editor.failed_to_commit_summary=Hata Mesajı: + +editor.fork_create=DeÄŸiÅŸiklikler Önermek için Depoyu Çatallayın +editor.fork_create_description=Bu depoyu doÄŸrudan düzenleyemezsiniz. Bunun yerine bir çatal oluÅŸturabilir, düzenlemeler yapabilir ve bir deÄŸiÅŸiklik isteÄŸi oluÅŸturabilirsiniz. +editor.fork_edit_description=Bu depoyu doÄŸrudan düzenleyemezsiniz. DeÄŸiÅŸiklikler <b>%s</b> çatalınıza yazılacak, böylece bir deÄŸiÅŸiklik isteÄŸi oluÅŸturabilirsiniz. +editor.fork_not_editable=Bu deponun çatalını oluÅŸturdunuz ancak çatalınız düzenlenemez durumda. +editor.fork_failed_to_push_branch=%s dalını deponuza gönderme baÅŸarısız oldu. +editor.fork_branch_exists=`"%s" dalı çatalınızda zaten mevcut, lütfen yeni bir dal adı seçin.` commits.desc=Kaynak kodu deÄŸiÅŸiklik geçmiÅŸine göz atın. commits.commits=İşleme @@ -1546,9 +1561,13 @@ issues.filter_project_all=Tüm projeler issues.filter_project_none=Proje yok issues.filter_assignee=Atanan issues.filter_assignee_no_assignee=Hiç kimseye atanmamış +issues.filter_assignee_any_assignee=Herhangi bir kimseye atanmış issues.filter_poster=Yazar +issues.filter_user_placeholder=Kullanıcıları ara +issues.filter_user_no_select=Tüm kullanıcılar issues.filter_type=Tür issues.filter_type.all_issues=Tüm konular +issues.filter_type.all_pull_requests=Tüm deÄŸiÅŸiklik istekleri issues.filter_type.assigned_to_you=Size atanan issues.filter_type.created_by_you=Sizin oluÅŸturduklarınız issues.filter_type.mentioning_you=Sizden bahsedilen @@ -1640,12 +1659,14 @@ issues.save=Kaydet issues.label_title=Etiket adı issues.label_description=Etiket açıklaması issues.label_color=Etiket rengi +issues.label_color_invalid=Geçersiz renk issues.label_exclusive=Özel issues.label_archive=Etiketi ArÅŸivle issues.label_archived_filter=ArÅŸivlenmiÅŸ etiketleri göster issues.label_archive_tooltip=ArÅŸivlenmiÅŸ etiketler, etiket araması yapılırken varsayılan olarak önerilerin dışında tutuluyor. issues.label_exclusive_desc=<code>Kapsam/öğe</code> etiketini, diÄŸer <code>kapsam/</code> etiketleriyle ayrışık olacak ÅŸekilde adlandırın. issues.label_exclusive_warning=Çakışan kapsamlı etiketler, bir konu veya deÄŸiÅŸiklik isteÄŸi etiketleri düzenlenirken kaldırılacaktır. +issues.label_exclusive_order_tooltip=Aynı baÄŸlamdaki özel etiketler bu sayısal sıralamaya göre sıralanacaktır. issues.label_count=%d etiket issues.label_open_issues=%d açık konu issues.label_edit=Düzenle @@ -1690,12 +1711,25 @@ issues.delete.title=Bu konu silinsin mi? issues.delete.text=Bu konuyu gerçekten silmek istiyor musunuz? (Bu iÅŸlem tüm içeriÄŸi kalıcı olarak silecektir. ArÅŸivde tutma niyetiniz varsa silmek yerine kapatmayı düşünün) issues.tracker=Zaman Takibi - +issues.timetracker_timer_start=Zamanlayıcıyı baÅŸlat +issues.timetracker_timer_stop=Zamanlayıcıyı durdur +issues.timetracker_timer_discard=Zamanlayıcıyı kaldır +issues.timetracker_timer_manually_add=Zaman Ekle + +issues.time_estimate_set=Tahmini zaman ayarla +issues.time_estimate_display=Tahmin: %s +issues.change_time_estimate_at=zaman tahmini deÄŸiÅŸtirildi: <b>%[1]s</b>%[2]s +issues.remove_time_estimate_at=zaman tahmini %s kaldırıldı +issues.time_estimate_invalid=Zaman tahmini biçimi hatalı issues.tracker_auto_close=Bu konu kapatıldığında zamanlayıcı otomatik olarak durur issues.tracking_already_started=`<a href="%s">baÅŸka bir konuda</a> zaten zaman izleyici baÅŸlattınız!` +issues.stop_tracking=Zamanlayıcıyı Durdur +issues.stop_tracking_history=<b>%[1]s</b> %[2]s için çalıştı +issues.cancel_tracking=Yoksay issues.cancel_tracking_history=`%s zaman takibini iptal etti` issues.del_time=Bu zaman kaydını sil issues.del_time_history=`%s harcanan zaman silindi` +issues.add_time_manually=El ile Zaman Ekle issues.add_time_hours=Saat issues.add_time_minutes=Dakika issues.add_time_sum_to_small=Zaman girilmedi. @@ -1755,6 +1789,7 @@ issues.dependency.add_error_dep_not_same_repo=Her iki konu da aynı depoda olmal issues.review.self.approval=Kendi deÄŸiÅŸiklik isteÄŸinizi onaylayamazsınız. issues.review.self.rejection=Kendi deÄŸiÅŸiklik isteÄŸinizde deÄŸiÅŸiklik isteyemezsiniz. issues.review.approve=%s bu deÄŸiÅŸiklikleri onayladı +issues.review.comment=%s incelendi issues.review.dismissed=%s incelemesini %s reddetti issues.review.dismissed_label=Reddedildi issues.review.left_comment=bir yorum yaptı @@ -1779,7 +1814,12 @@ issues.review.hide_resolved=Çözülenleri gizle issues.review.resolve_conversation=KonuÅŸmayı çöz issues.review.un_resolve_conversation=KonuÅŸmayı çözme issues.review.resolved_by=bu konuÅŸmayı çözümlenmiÅŸ olarak iÅŸaretledi -issues.review.commented=Yorum Yap +issues.review.commented=Yorum +issues.review.official=Onaylandı +issues.review.requested=İnceleme bekliyor +issues.review.rejected=DeÄŸiÅŸiklik istendi +issues.review.stale=Onaydan sonra güncellendi +issues.review.unofficial=Sayılmamış onay issues.assignee.error=Beklenmeyen bir hata nedeniyle tüm atananlar eklenmedi. issues.reference_issue.body=Gövde issues.content_history.deleted=silindi @@ -1847,11 +1887,14 @@ pulls.add_prefix=<strong>%s</strong> ön ekini ekle pulls.remove_prefix=<strong>%s</strong> ön ekini kaldır pulls.data_broken=Bu deÄŸiÅŸiklik isteÄŸi, çatallama bilgilerinin eksik olması nedeniyle bozuldu. pulls.files_conflicted=Bu deÄŸiÅŸiklik isteÄŸinde, hedef dalla çakışan deÄŸiÅŸiklikler var. +pulls.is_checking=BirleÅŸtirme çakışması denetleniyor ... pulls.is_ancestor=Bu dal zaten hedef dalda mevcut. BirleÅŸtirilecek bir ÅŸey yok. pulls.is_empty=Bu daldaki deÄŸiÅŸiklikler zaten hedef dalda mevcut. Bu boÅŸ bir iÅŸleme olacaktır. pulls.required_status_check_failed=Bazı gerekli denetimler baÅŸarılı olmadı. pulls.required_status_check_missing=Gerekli bazı kontroller eksik. pulls.required_status_check_administrator=Yönetici olarak, bu deÄŸiÅŸiklik isteÄŸini yine de birleÅŸtirebilirsiniz. +pulls.blocked_by_approvals=Bu deÄŸiÅŸiklik isteÄŸinin henüz yeterli onayı yok. %d onay var, %d onay gerekiyor. +pulls.blocked_by_approvals_whitelisted=Bu deÄŸiÅŸiklik isteÄŸi henüz yeterince onaya sahip deÄŸil. İzinli kullanıcılar veya takımlar %d/%d onay verdi. pulls.blocked_by_rejection=Bu deÄŸiÅŸiklik isteÄŸinde resmi bir gözden geçiren tarafından istenen deÄŸiÅŸiklikler var. pulls.blocked_by_official_review_requests=Bu deÄŸiÅŸiklik isteÄŸinde resmi inceleme istekleri var. pulls.blocked_by_outdated_branch=Bu deÄŸiÅŸiklik isteÄŸi eskidiÄŸi için engellendi. @@ -1893,7 +1936,9 @@ pulls.unrelated_histories=BirleÅŸtirme BaÅŸarısız: BirleÅŸtirme baÅŸlığı ve pulls.merge_out_of_date=BirleÅŸtirme BaÅŸarısız: BirleÅŸtirme oluÅŸturulurken, taban güncellendi. İpucu: Tekrar deneyin. pulls.head_out_of_date=BirleÅŸtirme BaÅŸarısız: BirleÅŸtirme oluÅŸturulurken, ana güncellendi. İpucu: Tekrar deneyin. pulls.has_merged=BaÅŸarısız: DeÄŸiÅŸiklik isteÄŸi birleÅŸtirildi, yeniden birleÅŸtiremez veya hedef dalı deÄŸiÅŸtiremezsiniz. +pulls.push_rejected=Gönderme BaÅŸarısız Oldu: Gönderme reddedildi. Bu depo için Git İstemcilerini inceleyin. pulls.push_rejected_summary=Tam Red Mesajı +pulls.push_rejected_no_message=Gönderme BaÅŸarısız Oldu: Gönderme reddedildi, ancak uzak bir mesaj yoktu. Bu depo için Git İstemcilerini inceleyin pulls.open_unmerged_pull_exists=`Aynı özelliklere sahip bekleyen bir deÄŸiÅŸiklik isteÄŸi (#%d) olduÄŸundan yeniden açma iÅŸlemini gerçekleÅŸtiremezsiniz.` pulls.status_checking=Bazı denetlemeler beklemede pulls.status_checks_success=Tüm denetlemeler baÅŸarılı oldu @@ -2313,7 +2358,6 @@ settings.event_repository=Depo settings.event_repository_desc=Depo oluÅŸturuldu veya silindi. settings.event_header_issue=Konu Olayları settings.event_issues=Konular -settings.event_issues_desc=Konu açıldı, kapatıldı, yeniden açıldı veya düzenlendi. settings.event_issue_assign=Konu Atandı settings.event_issue_assign_desc=Konu atandı veya atanmadı. settings.event_issue_label=Konu Etiketlendi @@ -2324,7 +2368,6 @@ settings.event_issue_comment=Konu Yorumu settings.event_issue_comment_desc=Konu yorumu eklendiÄŸinde, düzenlendiÄŸinde veya silindiÄŸinde. settings.event_header_pull_request=DeÄŸiÅŸiklik İsteÄŸi Olayları settings.event_pull_request=İstek Çek -settings.event_pull_request_desc=DeÄŸiÅŸiklik isteÄŸi açıldı, kapatıldı, yeniden açıldı veya düzenlendi. settings.event_pull_request_assign=DeÄŸiÅŸiklik İsteÄŸi Atandı settings.event_pull_request_assign_desc=DeÄŸiÅŸiklik isteÄŸi atanmış veya atanmamış. settings.event_pull_request_label=DeÄŸiÅŸiklik İsteÄŸi Etiketlendi @@ -2344,6 +2387,7 @@ settings.event_pull_request_merge=DeÄŸiÅŸiklik İsteÄŸi BirleÅŸtirme settings.event_package=Paket settings.event_package_desc=Bir depoda paket oluÅŸturuldu veya silindi. settings.branch_filter=Dal filtresi +settings.branch_filter_desc=Gönderme, dal oluÅŸturma ve dal silme olayları için glob deseni olarak belirtilen dal beyaz listesi. BoÅŸsa veya <code>*</code> ise, tüm dallar için olaylar raporlanır. Sözdizimi için <a href="%[1]s">%[2]s</a> belgelerine bakın. Örnekler: <code>master</code>, <code>{master,release*}</code>. settings.authorization_header=Yetkilendirme BaÅŸlığı settings.authorization_header_desc=Mevcutsa isteklere yetkilendirme baÅŸlığı olarak eklenecektir. Örnekler: %s. settings.active=Etkin @@ -2730,15 +2774,13 @@ settings.visibility.private_shortname=Özel settings.update_settings=Ayarları Güncelle settings.update_setting_success=Organizasyon ayarları güncellendi. -settings.change_orgname_prompt=Not: Organizasyon adını deÄŸiÅŸtirmek organizasyonunuzun URL'sini de deÄŸiÅŸtirecek ve eski ismi serbest bıracaktır. -settings.change_orgname_redirect_prompt=Eski ad, talep edilene kadar yeniden yönlendirilecektir. + + settings.update_avatar_success=Organizasyonun resmi güncellendi. settings.delete=Organizasyonu Sil settings.delete_account=Bu Organizasyonu Sil settings.delete_prompt=Organizasyon kalıcı olarak kaldırılacaktır. Bu iÅŸlem <strong>GERİ ALINAMAZ</strong>! settings.confirm_delete_account=Silmeyi Onaylıyorum -settings.delete_org_title=Organizasyonu Sil -settings.delete_org_desc=Bu organizasyon kalıcı olarak silinecektir. Devam edilsin mi? settings.hooks_desc=Bu organizasyon altındaki <strong>tüm depolar</strong> için tetiklenecek webhook'lar ekle. settings.labels_desc=Bu organizasyonun altındaki <strong>tüm depolar</strong> ile ilgili konularda kullanılabilecek etiketler ekleyin. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index 6aed70491b..ebd178c87b 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -2,7 +2,9 @@ home=Головна dashboard=Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ explore=ОглÑд help=Довідка +logo=Логотип sign_in=Увійти +sign_in_with_provider=Увійдіть за допомогою %s sign_in_or=або sign_out=Вийти sign_up=РеєÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ @@ -15,35 +17,50 @@ template=Шаблон language=Мова notifications=Ð¡Ð¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ active_stopwatch=Трекер робочого чаÑу +tracked_time_summary=ПідÑумок відÑтежуваного чаÑу на оÑнові фільтрів ÑпиÑку задач create_new=Створити… user_profile_and_more=Профіль Ñ– налаштуваннÑ… signed_in_as=Увійшов Ñк +enable_javascript=Ð”Ð»Ñ Ñ€Ð¾Ð±Ð¾Ñ‚Ð¸ цього Ñайту потрібен JavaScript. toc=ЗміÑÑ‚ licenses=Ліцензії return_to_gitea=ПовернутиÑÑ Ð´Ð¾ Gitea +more_items=Більше елементів username=Ім'Ñ ÐºÑ€Ð¸Ñтувача email=ÐдреÑа електронної пошти password=Пароль -access_token=Токен ДоÑтупу -re_type=ÐŸÑ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ +access_token=Токен доÑтупу +re_type=Підтвердити пароль captcha=CAPTCHA -twofa=Двофакторна Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ñ–Ñ +twofa=Двофакторна Ð°Ð²Ñ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ twofa_scratch=Двофакторний одноразовий пароль passcode=Код доÑтупу +webauthn_insert_key=Ð’Ñтавте ключ безпеки +webauthn_sign_in=ÐатиÑніть кнопку на вашому ключі безпеки. Якщо ваш ключ без фізичної кнопки, поновно вÑтавте ключ. +webauthn_press_button=Будь лаÑка, натиÑніть кнопку на вашому ключі безпеки… +webauthn_use_twofa=ВикориÑтовуйте двофакторний код із Вашого телефона +webauthn_error=Ðе вдалоÑÑ Ð¿Ñ€Ð¾Ñ‡Ð¸Ñ‚Ð°Ñ‚Ð¸ ваш ключ безпеки. +webauthn_unsupported_browser=Ваш браузер наразі не підтримує WebAuthn. +webauthn_error_unknown=СталаÑÑ Ð½ÐµÐ²Ñ–Ð´Ð¾Ð¼Ð° помилка. Будь лаÑка, Ñпробуйте ще раз. +webauthn_error_insecure=`WebAuthn підтримує тільки безпечні з’єднаннÑ. Ð”Ð»Ñ Ñ‚ÐµÑÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾ HTTP, ви можете ÑкориÑтатиÑÑ "localhost" або "127.0.0.1"` +webauthn_error_unable_to_process=Сервер не зміг обробити ваш запит. +webauthn_error_duplicated=Ключ безпеки не підходить Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ запиту. ПереконайтеÑÑ, що ключ ще не зареєÑтровано. +webauthn_error_empty=Ви повинні вÑтановити назву Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ ключа. +webauthn_error_timeout=Ð§Ð°Ñ Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¸Ñ‡ÐµÑ€Ð¿Ð°Ð½Ð¾, перш ніж ваш ключ було прочитано. Перезавантажте Ñторінку та Ñпробуйте ще раз. webauthn_reload=Оновити -repository=Репозиторій +repository=Сховище organization=ÐžÑ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ mirror=Дзеркало issue_milestone=Етап -new_repo=Ðовий репозиторій +new_repo=Ðове Ñховище new_migrate=Ðова Ð¼Ñ–Ð³Ñ€Ð°Ñ†Ñ–Ñ new_mirror=Ðове дзеркало -new_fork=Ðовий репозиторій - ÐºÐ¾Ð¿Ñ–Ñ new_org=Ðова Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ new_project=Ðовий проєкт +new_project_column=Ðовий Ñтовпець manage_org=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñми admin_panel=Панель ÐдмініÑтратора account_settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу @@ -53,7 +70,7 @@ your_starred=Обрані your_settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ all=УÑÑ– -sources=ВлаÑні +sources=Джерела mirrors=Дзеркала collaborative=Спільні forks=Форки @@ -65,303 +82,446 @@ milestones=Етапи ok=OK cancel=Відмінити +retry=Повторіть Ñпробу +rerun=ПерезапуÑтити +rerun_all=ПерезапуÑтити вÑÑ– Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ save=Зберегти add=Додати add_all=Додати вÑе remove=Видалити remove_all=Видалити вÑе +remove_label_str=`Видалити елемент "%s"` edit=Редагувати +view=ПереглÑнути +test=ТеÑÑ‚ enabled=Увімкнено disabled=Вимкнено +locked=Заблоковано copy=Копіювати copy_url=Копіювати URL +copy_hash=Копіювати хеш +copy_content=Копіювати вміÑÑ‚ copy_branch=Копіювати назву гілки +copy_path=Копіювати шлÑÑ… copy_success=Скопійовано! copy_error=Ðе вдалоÑÑ Ñкопіювати +copy_type_unsupported=Цей тип файлу не можна Ñкопіювати write=ПиÑати preview=Попередній переглÑд loading=ЗавантаженнÑ… +files=Файли error=Помилка -error404=Сторінка, до Ñкої ви намагаєтеÑÑ Ð·Ð²ÐµÑ€Ð½ÑƒÑ‚Ð¸ÑÑ Ð°Ð±Ð¾ до <strong>, не Ñ–Ñнує</strong> або <strong>Ви не маєте права</strong> на Ñ—Ñ— переглÑд. +error404=Сторінка, Ñку ви намагаєтеÑÑ Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ð¸, <strong>не Ñ–Ñнує</strong> або <strong>ви не маєте права</strong> на Ñ—Ñ— переглÑд. +error503=Сервер не зміг виконати ваш запит. Будь лаÑка, Ñпробуйте пізніше. +go_back=Ðазад +invalid_data=ÐедійÑні дані: %v never=Ðіколи +unknown=Ðевідомо +rss_feed=Стрічка RSS +pin=Закріпити +unpin=Відкріпити +artifacts=Ðртефакти +expired=ПроÑтрочено +confirm_delete_artifact=Справді видалити артефакт '%s' ? -archived=Ðрхівовані +archived=Ðрхівовано -concept_code_repository=Репозиторій +concept_code_repository=Сховище concept_user_organization=ÐžÑ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ +show_timestamps=Показувати чаÑові мітки +show_log_seconds=Показувати Ñекунди +show_full_screen=Показати на веÑÑŒ екран +download_logs=Завантажити журнали +confirm_delete_selected=Підтверджуєте Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… вибраних елементів? name=Ðазва +value=Ð—Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ +readme=Файл readme filter=Фільтр -filter.is_archived=Ðрхівовані +filter.clear=ОчиÑтити фільтр +filter.is_archived=Ðрхівовано +filter.not_archived=Ðе архівовано +filter.is_fork=Відгалужено +filter.not_fork=Ðе відгалужено +filter.is_mirror=Віддзеркалено +filter.not_mirror=Ðе віддзеркалено filter.is_template=Шаблон -filter.public=Публічний +filter.not_template=Ðе шаблон +filter.public=Публічна filter.private=Приватний +no_results_found=Ðічого не знайдено. +internal_error_skipped=ТрапилаÑÑŒ Ð²Ð½ÑƒÑ‚Ñ€Ñ–ÑˆÐ½Ñ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ°, але пропущена: %s [search] +search=Пошук... +type_tooltip=Тип пошуку +fuzzy=Ðеточний +fuzzy_tooltip=Включити результати, Ñкі також близько відповідають пошуковому запиту +words=Слова +words_tooltip=Включати тільки результати, Ñкі відповідають пошуковим Ñловам +regexp=РегулÑрний вираз +regexp_tooltip=Включати тільки результати, Ñкі відповідають пошуковому запиту регулÑрного виразу +exact=Точний +exact_tooltip=Включати тільки результати, Ñкі відповідають точному пошуковому запиту +repo_kind=Пошук Ñховищ... +user_kind=Пошук кориÑтувачів... +org_kind=Пошук організацій... +team_kind=Пошук команд... +code_kind=Пошук коду... +code_search_unavailable=Пошук коду наразі недоÑтупний. Будь лаÑка, звернітьÑÑ Ð´Ð¾ адмініÑтратора Ñайту. +code_search_by_git_grep=Поточні результати пошуку коду надаютьÑÑ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¾ÑŽ "git grep". Результати можуть бути кращими, Ñкщо адмініÑтратор Ñайту увімкне індекÑатор Ñховища. +package_kind=Пошук пакетів... +project_kind=Пошук проєктів... +branch_kind=Пошук гілок... +tag_kind=Пошук за мітками... +commit_kind=Пошук комітів... +no_results=Ðе знайдено жодного збігу. +issue_kind=Пошук задач... +pull_kind=Пошук запитів на злиттÑ... +keyword_search_unavailable=Пошук за ключовими Ñловами наразі недоÑтупний. Будь лаÑка, звернітьÑÑ Ð´Ð¾ адмініÑтратора Ñайту. [aria] +navbar=Панель навігації +footer=Ðижній колонтитул +footer.software=Про програмне Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ +footer.links=ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ [heatmap] +number_of_contributions_in_the_last_12_months=%s внеÑків за оÑтанні 12 міÑÑців +no_contributions=Ðемає внеÑків +less=Менше +more=Більше [editor] +buttons.heading.tooltip=Додати заголовок +buttons.bold.tooltip=Додати грубий текÑÑ‚ +buttons.italic.tooltip=Додати курÑивний текÑÑ‚ +buttons.quote.tooltip=Цитувати текÑÑ‚ +buttons.code.tooltip=Додати код +buttons.link.tooltip=Додати поÑÐ¸Ð»Ð°Ð½Ð½Ñ +buttons.list.unordered.tooltip=Додати ÑпиÑок +buttons.list.ordered.tooltip=Додати нумерований ÑпиÑок +buttons.list.task.tooltip=Додати ÑпиÑок завдань +buttons.table.add.tooltip=Додати таблицю buttons.table.add.insert=Додати +buttons.table.rows=Ð Ñдки +buttons.table.cols=Стовпці +buttons.mention.tooltip=Згадати кориÑтувача або команду +buttons.ref.tooltip=ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° задачу або запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +buttons.switch_to_legacy.tooltip=ВикориÑтовувати заÑтарілий редактор +buttons.enable_monospace_font=Увімкнути моноширинний шрифт +buttons.disable_monospace_font=Вимкнути моноширинний шрифт [filter] +string.asc=Ð - Я +string.desc=Я - Ð [error] occurred=СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° +report_message=Якщо ви вважаєте, що це помилка Gitea, будь лаÑка, Ñпробуйте відшукати відповідну проблему на <a href="https://github.com/go-gitea/gitea/issues">GitHub</a> або Ñтворіть нову, Ñкщо необхідно. +not_found=Ціль не знайдено. network_error=Помилка мережі [startpage] app_desc=Зручний влаÑний ÑÐµÑ€Ð²Ñ–Ñ Ñ…Ð¾Ñтингу репозиторіїв Git install=Легко вÑтановити +install_desc=ПроÑто <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-binary">запуÑтіть двійковий файл</a> Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ñ— платформи, ÑкориÑтайтеÑÑ <a target="_blank" rel="noopener noreferrer" href="https://github.com/go-gitea/gitea/tree/master/docker">Docker</a>, або вÑтановіть <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com/installation/install-from-package">ÑиÑтемою ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð°ÐºÑƒÐ½ÐºÐ°Ð¼Ð¸</a>. platform=ПлатформонезалежніÑть +platform_desc=Gitea запуÑкаєтьÑÑ Ð±ÑƒÐ´ÑŒ-де, де <a target=«_blank» rel=«noopener noreferrer» href=«%s»>Go</a> може компілюватиÑÑŒ: на Windows, macOS, Linux, ARM тощо. Виберіть платформу, Ñку любите! lightweight=ÐевибагливіÑть -lightweight_desc=Gitea має низькі вимоги до реÑурÑів та може працювати на недорогому Raspberry Pi. Збережіть Ñвою машину енергію! +lightweight_desc=Gitea має мінімальні вимоги Ñ– може працювати на недорогому Raspberry Pi. Заощаджуйте реÑурÑи вашої машини! license=Відкритий вихідний код [install] install=Ð’ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ +installing_desc=Ð’ÑтановленнÑ, будь лаÑка, зачекайте... title=Початкова ÐºÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ -docker_helper=Якщо ви запуÑкаєте Gitea вÑередині Docker, будь лаÑка уважно прочитайте <a target="_blank" rel="noopener" href="%s">документацію</a> перед тим, Ñк щоÑÑŒ змінити на цій Ñторінці. +docker_helper=Якщо ви запуÑкаєте Gitea у Docker, будь лаÑка, прочитайте <a target="_blank" rel="noopener noreferrer" href="%s">документацію</a> перед тим, Ñк змінювати будь-Ñкі налаштуваннÑ. +require_db_desc=Gitea потребує MySQL, PostgreSQL, MSSQL, SQLite3 або TiDB (протокол MySQL). db_title=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð±Ð°Ð·Ð¸ даних db_type=Тип бази даних host=ХоÑÑ‚ user=Ім'Ñ ÐºÑ€Ð¸Ñтувача password=Пароль -db_name=Ім'Ñ Ð±Ð°Ð·Ð¸ даних +db_name=Ðазва бази даних db_schema=Схема -db_schema_helper=Залиште пуÑтим Ð´Ð»Ñ Ð±Ð°Ð·Ð¸ даних за замовчуваннÑм ("публічна"). +db_schema_helper=Залиште пуÑтим Ð´Ð»Ñ Ñ‚Ð¸Ð¿Ð¾Ð²Ð¾Ñ— Ñхеми бази даних ("публічна"). ssl_mode=SSL path=ШлÑÑ… -sqlite_helper=ШлÑÑ… до файлу Ð´Ð»Ñ Ð±Ð°Ð·Ð¸ даних SQLite3.<br>Введіть абÑолютний шлÑÑ…, Ñкщо ви запуÑкаєте GÑ–tea Ñк ÑервіÑ. +sqlite_helper=ШлÑÑ… до файлу бази даних SQLite3.<br>Введіть абÑолютний шлÑÑ…, Ñкщо ви запуÑкаєте GÑ–tea Ñк ÑервіÑ. reinstall_error=Ви намагаєтеÑÑ Ð²Ñтановити в наÑвну базу даних Gitea reinstall_confirm_message=Повторне вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð² наÑвну базу даних Gitea може Ñпричинити багато проблем. Ð’ більшоÑті випадків, ви повинні викориÑтовувати Ñвій наÑвний "app.ini" Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑку Gitea. Якщо ви знаєте, що робите, Ñпробуйте наÑтупне: -reinstall_confirm_check_1=Дані зашифровані з викориÑтаннÑм SECRET_KEY з app.ini можуть бути втрачені: кориÑтувачі не зможуть увійти з 2FA/OTP Ñ– дзеркала можуть працювати некоректно. Ð’Ñтановлюючи цей прапорець, ви підтверджуєте, що в поточному файлі app.ini вказано правильне Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ SECRET_KEY. -reinstall_confirm_check_2=Репозиторії та Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð½ÐµÐ¾Ð±Ñ…Ñ–Ð´Ð½Ð¾ повторно Ñинхронізувати. Ð’Ñтановлюючи цей прапорець, ви підтверджуєте, що ви Ñинхронізуватимете хуки репозиторіїв та authorized_keys вручну. Ви підтверджуєте, що Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ Ñ– дзеркала Ñ” правильними. -reinstall_confirm_check_3=Ви підтверджуєте, що повніÑтю впевнені в тому, що Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ екземплÑра Gitea вказано правильне Ñ€Ð¾Ð·Ñ‚Ð°ÑˆÑƒÐ²Ð°Ð½Ð½Ñ app.ini та екземплÑÑ€ Ñлід вÑтановити повторно. Ви підтверджуєте, що уÑвідомлюєте вищенаведені ризики. +reinstall_confirm_check_1=Дані, зашифровані за допомогою SECRET_KEY в app.ini, можуть бути втрачені: кориÑтувачі не зможуть увійти за допомогою 2FA/OTP, а дзеркала можуть працювати некоректно. Ð’Ñтановивши цей прапорець, ви підтверджуєте, що поточний файл app.ini міÑтить правильний SECRET_KEY. +reinstall_confirm_check_2=Можливо, потрібно буде повторно Ñинхронізувати Ñховища та налаштуваннÑ. Ð’Ñтановивши цей прапорець, ви підтверджуєте, що будете реÑинхронізувати хуки Ð´Ð»Ñ Ñховищ Ñ– файл authorized_keys вручну. Ви підтверджуєте, що забезпечите правильніÑть налаштувань Ñховища Ñ– дзеркала. +reinstall_confirm_check_3=Ви підтверджуєте, що абÑолютно впевнені, що Gitea працює з правильним розташуваннÑм файлу app.ini, Ñ– що вам потрібно перевÑтановити програму. Ви підтверджуєте, що уÑвідомлюєте вищевказані ризики. err_empty_db_path=ШлÑÑ… до файлу бази даних SQLite3 не може бути порожнім. -no_admin_and_disable_registration=Ви не можете вимкнути реєÑтрацію до ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу адмініÑтратора. +no_admin_and_disable_registration=Ви не можете вимкнути реєÑтрацію без ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу адмініÑтратора. err_empty_admin_password=Пароль адмініÑтратора не може бути порожнім. err_empty_admin_email=Електронна адреÑа адмініÑтратора не може бути порожньою. -err_admin_name_is_reserved=Ðеправильне ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача-адмініÑтратора - ім'Ñ Ð·Ð°Ñ€ÐµÐ·ÐµÑ€Ð²Ð¾Ð²Ð°Ð½Ðµ -err_admin_name_pattern_not_allowed=Ім'Ñ Ð°Ð´Ð¼Ñ–Ð½Ñ–Ñтратора недійÑне, це ім'Ñ Ð¿Ñ–Ð´Ð¿Ð°Ð´Ð°Ñ” під зарезервований шаблон -err_admin_name_is_invalid=Ðеправильне ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача-адмініÑтратора +err_admin_name_is_reserved=Ðеправильне ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача адмініÑтратора - ім'Ñ Ð·Ð°Ñ€ÐµÐ·ÐµÑ€Ð²Ð¾Ð²Ð°Ð½Ð¾ +err_admin_name_pattern_not_allowed=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача адмініÑтратора недійÑне, воно відповідає зарезервованому шаблону +err_admin_name_is_invalid=ÐедійÑне ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача адмініÑтратора general_title=Загальні Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ app_name=Ðазва Ñайту app_name_helper=Тут ви можете ввеÑти назву Ñвоєї компанії. -repo_path=Кореневий шлÑÑ… Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ -repo_path_helper=Ð’ÑÑ– вилучені Git репозиторії будуть збережені в цей каталог. -lfs_path=Кореневої шлÑÑ… Git LFS -lfs_path_helper=У цій папці будуть зберігатиÑÑ Ñ„Ð°Ð¹Ð»Ð¸ Git LFS. Залиште порожнім, щоб вимкнути LFS. -run_user=ЗапуÑк від імені КориÑтувача +repo_path=Кореневий шлÑÑ… Ñховища +repo_path_helper=До цього каталогу буде збережено віддалені Ñховища Git. +lfs_path=Кореневий шлÑÑ… Git LFS +lfs_path_helper=У цій теці будуть зберігатиÑÑ Ñ„Ð°Ð¹Ð»Ð¸ Git LFS. Залиште порожнім, щоб вимкнути. +run_user=Виконати Ñк +run_user_helper=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача операційної ÑиÑтеми, від імені Ñкого запуÑкаєтьÑÑ Gitea. Зауважте, що цей кориÑтувач повинен мати доÑтуп до кореневого шлÑху Ñховища. domain=Домен Ñервера -domain_helper=Домен або адреÑа хоÑта Ñервера. +domain_helper=Домен або хоÑÑ‚-адреÑа Ñервера. ssh_port=Порт SSH Ñервера ssh_port_helper=Ðомер порту, Ñкий викориÑтовує SSH Ñервер. Залиште порожнім, щоб вимкнути SSH. http_port=Gitea HTTP порт -http_port_helper=Ðомер порту, Ñкий буде проÑлуховуватиÑÑ Giteas веб-Ñервером. +http_port_helper=Ðомер порту, Ñкий буде проÑлуховуватиÑÑ Ñервером Giteas. app_url=Базова URL-адреÑа Gitea -app_url_helper=Базова адреÑа Ð´Ð»Ñ HTTP(S) ÐºÐ»Ð¾Ð½ÑƒÐ²Ð°Ð½Ð½Ñ Ñ‡ÐµÑ€ÐµÐ· URL та повідомлень електронної пошти. -log_root_path=ШлÑÑ… до лог файлу -log_root_path_helper=Файли журналу будуть запиÑані в цей каталог. - -optional_title=Додаткові Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ -email_title=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Email -smtp_addr=SMTP хоÑÑ‚ -smtp_port=SMTP порт -smtp_from=ВідправлÑти Email від імені -smtp_from_helper=Електронна пошта Ð´Ð»Ñ Ð²Ð¸ÐºÐ¾Ñ€Ð¸ÑÑ‚Ð°Ð½Ð½Ñ Ð² GÑ–tea. Введіть звичайну електронну адреÑу або викориÑтовуйте формат: "Ім'Ñ" <email@example.com>. -mailer_user=SMTP Ім'Ñ ÐºÑ€Ð¸Ñтувача -mailer_password=SMTP Пароль -register_confirm=Потрібно підтвердити електронну пошту Ð´Ð»Ñ Ñ€ÐµÑ”Ñтрації +app_url_helper=Базова адреÑа Ð´Ð»Ñ URL-Ð°Ð´Ñ€ÐµÑ ÐºÐ»Ð¾Ð½Ñ–Ð² HTTP(S) та Ñповіщень електронною поштою. +log_root_path=ШлÑÑ… до журналу +log_root_path_helper=Файли журналу будуть запиÑані в цю теку. + +optional_title=Ðеобов'Ñзкові Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ +email_title=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾Ñ— пошти +smtp_addr=Сервер SMTP +smtp_port=Порт SMTP +smtp_from=Відправити лиÑта від імені +smtp_from_invalid=ÐдреÑа "ÐадіÑлати лиÑта Ñк" недійÑна +smtp_from_helper=ÐдреÑа електронної пошти, Ñку буде викориÑтовувати Gitea. Введіть звичайну адреÑу електронної пошти або викориÑтовуйте формат «Ім'Ñ» <email@example.com>. +mailer_user=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача SMTP +mailer_password=Пароль SMTP +register_confirm=Вимагати Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾ÑŽ поштою Ð´Ð»Ñ Ñ€ÐµÑ”Ñтрації mail_notify=Увімкнути ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾ÑŽ поштою -server_service_title=Сервер Ñ– Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð¾Ð²Ð½Ñ–ÑˆÐ½Ñ–Ñ… Ñлужб +server_service_title=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñервера Ñ– Ñторонніх ÑервіÑів offline_mode=Увімкнути локальний режим -offline_mode_popup=Відключити Ñторонні мережі доÑтавки контенту Ñ– обÑлуговувати вÑÑ– реÑурÑи локально. +offline_mode_popup=Вимкнути Ñторонні мережі доÑтавки контенту та обÑлуговувати вÑÑ– реÑурÑи локально. disable_gravatar=Вимкнути Gravatar -disable_gravatar_popup=Відключити Gravatar Ñ– Ñторонні джерела аватарів. Якщо кориÑтувач не завантажить аватар локально то за замовчуваннÑм буде викориÑтовуватиÑÑ Ñтандартний аватар. -federated_avatar_lookup=Увімкнути федеративні аватари -federated_avatar_lookup_popup=Увімкнути зовнішний Ðватар за допомогою Libravatar. -disable_registration=Вимкнути ÑамоÑтійну реєÑтрацію -disable_registration_popup=Вимкнути ÑамоÑтійну реєÑтрацію кориÑтувачів, тільки адмініÑтратор може Ñтворювати нові облікові запиÑи. +disable_gravatar_popup=Вимкнути Gravatar та Ñторонні джерела аватарок. Якщо кориÑтувач локально не завантажить аватар, буде викориÑтовуватиÑÑ Ñ‚Ð¸Ð¿Ð¾Ð²Ð¸Ð¹ аватар. +federated_avatar_lookup=Увімкнути зовнішні аватари +federated_avatar_lookup_popup=Увімкнути пошук об'єднаних аватарів за допомогою Libravatar. +disable_registration=Вимкнути реєÑтрацію +disable_registration_popup=Вимкнути реєÑтрацію кориÑтувачів, тільки адмініÑтратор може Ñтворювати нові облікові запиÑи. allow_only_external_registration_popup=Дозволити реєÑтрацію тільки через Ñторонні ÑервіÑи -openid_signin=Увімкнути реєÑтрацію за допомогою OpenID -openid_signin_popup=Увімкнути вхід за допомогою OpenID. +openid_signin=Увімкнути вхід за допомогою OpenID +openid_signin_popup=Увімкнути вхід кориÑтувачів за допомогою OpenID. openid_signup=Увімкнути ÑамоÑтійну реєÑтрацію за допомогою OpenID openid_signup_popup=Увімкнути ÑамореєÑтрацію кориÑтувачів на оÑнові OpenID. -enable_captcha=Увімкнути CAPTCHA при реєÑтрації -enable_captcha_popup=Вимагати перевірку CAPTCHA при ÑамоÑтійній реєÑтрації кориÑтувача. +enable_captcha=Увімкнути CAPTCHA Ð´Ð»Ñ Ñ€ÐµÑ”Ñтрації +enable_captcha_popup=Вимагати CAPTCHA Ð´Ð»Ñ ÑамореєÑтрації кориÑтувачів. require_sign_in_view=Вимагати авторизації Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ³Ð»Ñду Ñторінок +require_sign_in_view_popup=Обмежити доÑтуп до Ñторінки лише Ð´Ð»Ñ Ð·Ð°Ñ€ÐµÑ”Ñтрованих кориÑтувачів. Відвідувачі побачать тільки Ñторінки входу Ñ– реєÑтрації. admin_setting_desc=Ð¡Ñ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу адмініÑтратора необов'Ñзково. Перший зареєÑтрований кориÑтувач автоматично Ñтає адмініÑтратором. admin_title=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу адмініÑтратора admin_name=Ім'Ñ ÐºÑ€Ð¸Ñтувача ÐдмініÑтратора admin_password=Пароль -confirm_password=ÐŸÑ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ +confirm_password=Підтвердити пароль admin_email=ÐдреÑа електронної пошти -install_btn_confirm=Ð’ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Gitea -test_git_failed=Ðе в змозі перевірити 'git' команду: %v +install_btn_confirm=Ð’Ñтановити Gitea +test_git_failed=Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ команду 'git': %v sqlite3_not_available=Ð¦Ñ Ð²ÐµÑ€ÑÑ–Ñ Gitea не підтримує SQLite3. Будь лаÑка, завантажте офіційну бінарну верÑÑ–ÑŽ з %s (не верÑÑ–ÑŽ gobuild). -invalid_db_setting=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð±Ð°Ð·Ð¸ даних Ñ” некоректними: %v -invalid_repo_path=Помилковий шлÑÑ… до ÐºÐ¾Ñ€ÐµÐ½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ: %v -invalid_app_data_path=Ðекоректний шлÑÑ… до даних програми: %v -run_user_not_match=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача 'run as' не Ñ” поточним ім'Ñм кориÑтувача: %s -> %s -internal_token_failed=Ðе вдалоÑÑ Ð·Ð³ÐµÐ½ÐµÑ€ÑƒÐ²Ð°Ñ‚Ð¸ внутрішній токен: %v -secret_key_failed=Ðе вдалоÑÑ Ð·Ð³ÐµÐ½ÐµÑ€ÑƒÐ²Ð°Ñ‚Ð¸ Ñекретний ключ: %v -save_config_failed=Ðе в змозі зберегти конфігурацію: %v -invalid_admin_setting=ÐеприпуÑтимі Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу адмініÑтратора: %v -invalid_log_root_path=ÐеприпуÑтимий шлÑÑ… Ð´Ð»Ñ Ð»Ð¾Ð³Ñ–Ð²: %v -default_keep_email_private=Приховати адреÑу електронної пошти за замовчуваннÑм -default_keep_email_private_popup=Приховати адреÑу електронної пошти нових облікових запиÑів за замовчуваннÑм. -default_allow_create_organization=Дозволити ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ð¹ за замовчуваннÑм -default_allow_create_organization_popup=Дозволити новим обліковим запиÑам кориÑтувачів Ñтворювати організації за замовчуваннÑм. -default_enable_timetracking=Увімкнути відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‡Ð°Ñу за замовчуваннÑм -default_enable_timetracking_popup=Включити відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‡Ð°Ñу Ð´Ð»Ñ Ð½Ð¾Ð²Ð¸Ñ… репозиторіїв за замовчуваннÑм. +invalid_db_setting=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð±Ð°Ð·Ð¸ даних недійÑні: %v +invalid_db_table=База даних таблиці "%s" Ñ” недійÑною: %v +invalid_repo_path=Кореневий шлÑÑ… до Ñховища невірний: %v +invalid_app_data_path=ШлÑÑ… до даних додатка невірний: %v +run_user_not_match=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача “Виконати Ñк†не Ñ” поточним ім'Ñм кориÑтувача: %s -> %s +internal_token_failed=Ðе вдалоÑÑ Ñтворити внутрішній токен: %v +secret_key_failed=Ðе вдалоÑÑ Ñтворити Ñекретний ключ: %v +save_config_failed=Ðе вдалоÑÑ Ð·Ð±ÐµÑ€ÐµÐ³Ñ‚Ð¸ конфігурацію: %v +invalid_admin_setting=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу адмініÑтратора Ñ” недійÑним: %v +invalid_log_root_path=Ðеправильний шлÑÑ… до журналу: %v +default_keep_email_private=Типово приховувати адреÑи електронної пошти +default_keep_email_private_popup=Типово приховувати адреÑи електронної пошти нових облікових запиÑів. +default_allow_create_organization=За замовчуваннÑм дозволити ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ð¹ +default_allow_create_organization_popup=За замовчуваннÑм дозволити новим кориÑтувачам Ñтворювати організації. +default_enable_timetracking=За замовчуваннÑм увімкнути відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‡Ð°Ñу +default_enable_timetracking_popup=Увімкнути відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‡Ð°Ñу Ð´Ð»Ñ Ð½Ð¾Ð²Ð¸Ñ… Ñховищ за замовчуваннÑм. no_reply_address=Прихований поштовий домен -no_reply_address_helper=Доменне ім'Ñ Ð´Ð»Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів із прихованою електронною адреÑою. Ðаприклад, ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача 'joe' буде входити в Git Ñк 'joe@noreply.example.org', Ñкщо Ð´Ð»Ñ Ð¿Ñ€Ð¸Ñ…Ð¾Ð²Ð°Ð½Ð¾Ð³Ð¾ домену електронної пошти вÑтановлено 'noreply.example.org'. +no_reply_address_helper=Доменне ім'Ñ Ð´Ð»Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів із прихованою електронною адреÑою. Ðаприклад, ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача 'Joe' буде зображатиÑÑ Ð² Git Ñк 'joe@noreply.example.org', Ñкщо прихований домен електронної пошти вÑтановлено 'noreply.example.org'. password_algorithm=Ðлгоритм Ñ…ÐµÑˆÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ +invalid_password_algorithm=ÐедійÑний хеш-алгоритм Ð¿Ð°Ñ€Ð¾Ð»Ñ +password_algorithm_helper=Ð’Ñтановіть алгоритм Ñ…ÐµÑˆÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ. Ðлгоритми мають різні вимоги та ÑтійкіÑть. Ðлгоритм argon2 Ñ” доÑить безпечним, але викориÑтовує багато пам'Ñті Ñ– може бути недоречним Ð´Ð»Ñ Ð¼Ð°Ð»Ð¸Ñ… ÑиÑтем. +enable_update_checker=Увімкнути перевірку оновлень +enable_update_checker_helper=Періодично перевірÑти наÑвніÑть нових верÑій, підключаючиÑÑŒ до gitea.io. +env_config_keys=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ Ñередовища +env_config_keys_prompt=ÐаÑтупні змінні Ñередовища також будуть заÑтоÑовані до вашого файлу конфігурації: +config_write_file_prompt=Ці параметри будуть запиÑані в: %s [home] -uname_holder=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача або Ел. пошта +nav_menu=Меню навігації +uname_holder=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача або адреÑа електронної пошти password_holder=Пароль switch_dashboard_context=Переключити контекÑÑ‚ панелі ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ -my_repos=Репозиторії -show_more_repos=Показати більше репозиторіїв… -collaborative_repos=Спільні репозиторії +my_repos=Сховища +show_more_repos=Показати більше Ñховищ… +collaborative_repos=Спільні Ñховища my_orgs=Мої організації my_mirrors=Мої дзеркала view_home=ПереглÑнути %s filter=Інші фільтри -filter_by_team_repositories=Фільтрувати за репозиторіÑми команд +filter_by_team_repositories=Фільтрувати за Ñховищами команд feed_of=`Стрічка "%s"` show_archived=Ðрхівовані -show_both_archived_unarchived=Показано архівовані Ñ– не архівовані +show_both_archived_unarchived=Показано архівовані Ñ– неархівовані show_only_archived=Показано тільки архівовані -show_only_unarchived=Показано тільки не архівовані +show_only_unarchived=Показано тільки неархівовані show_private=Приватні show_both_private_public=Показано публічні та приватні show_only_private=Показано тільки приватні show_only_public=Показано тільки публічні -issues.in_your_repos=Ð’ ваших репозиторіÑÑ… +issues.in_your_repos=У ваших Ñховищах +guide_title=Жодної активноÑті +guide_desc=Ðаразі ви не Ñтежите за жодним Ñховищем або кориÑтувачем, тому нема чого відображати. Ви можете переглÑнути Ñховища або кориÑтувачів, Ñкі Ð²Ð°Ñ Ñ†Ñ–ÐºÐ°Ð²Ð»Ñть, за поÑиланнÑми нижче. +explore_repos=ОглÑд Ñховищ +explore_users=ОглÑд кориÑтувачів +empty_org=Організацій поки що немає. +empty_repo=Сховищ поки що немає. [explore] -repos=Репозиторії +repos=Сховища users=КориÑтувачі organizations=Організації +go_to=Перейти до code=Код code_last_indexed_at=ОÑтанні індекÑовані %s +relevant_repositories_tooltip=Сховища, Ñкі Ñ” відгалуженими або не мають теми, піктограми й опиÑу, приховуютьÑÑ. +relevant_repositories=Показано лише важливі Ñховища, <a href="%s">показати нефільтровані результати</a>. [auth] create_new_account=РеєÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу -disable_register_prompt=Вибачте, можливіÑть реєÑтрації відключена. Будь лаÑка, зв'ÑжітьÑÑ Ð· адмініÑтратором Ñайту. +already_have_account=Вже зареєÑтровані? +sign_in_now=Увійдіть зараз! +disable_register_prompt=РеєÑтрацію вимкнено. Будь лаÑка, зв'ÑжітьÑÑ Ð· адмініÑтратором Ñайту. disable_register_mail=ÐŸÑ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ñ€ÐµÑ”Ñтрації електронною поштою вимкнено. +manual_activation_only=ЗвернітьÑÑ Ð´Ð¾ адмініÑтратора Ñайту Ð´Ð»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ—. remember_me=Запам’Ñтати цей приÑтрій +remember_me.compromised=Токен Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ більше не дійÑний, що може Ñвідчити про Ñкомпрометований обліковий запиÑ. Перевірте Ñвій обліковий Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° наÑвніÑть незвичайних дій. forgot_password_title=Забув пароль forgot_password=Забули пароль? -must_change_password=Оновіть Ñвій пароль -allow_password_change=Вимагати в кориÑтувача змінити пароль (рекомендуєтьÑÑ) -reset_password_mail_sent_prompt=Електронний лиÑÑ‚ із підтвердженнÑм надіÑлано <b>%s</b>. Перевірте папку 'Вхідні' в межах наÑтупних %s, щоб завершити Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу. +need_account=Потрібен обліковий запиÑ? +sign_up_tip=Ви реєÑтруєте перший обліковий Ð·Ð°Ð¿Ð¸Ñ Ñƒ ÑиÑтемі, з правами адмініÑтратора. Будь лаÑка, уважно запам'Ñтайте Ñвоє ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача та пароль. Якщо ви Ñ—Ñ… забудете, звернітьÑÑ Ð´Ð¾ документації Gitea, щоб відновити обліковий запиÑ. +sign_up_now=ЗареєÑтруватиÑÑ. +sign_up_successful=Обліковий Ð·Ð°Ð¿Ð¸Ñ Ñтворено уÑпішно. Вітаю! +confirmation_mail_sent_prompt_ex=Ðовий лиÑÑ‚ з підтвердженнÑм було надіÑлано на <b>%s</b>. Будь лаÑка, перевірте Ñвою поштову Ñкриньку протÑгом наÑтупних %s, щоб завершити Ð¿Ñ€Ð¾Ñ†ÐµÑ Ñ€ÐµÑ”Ñтрації. Якщо ви вказали невірну адреÑу електронної пошти, ви можете увійти ще раз Ñ– змінити Ñ—Ñ—. +must_change_password=Оновити пароль +allow_password_change=Вимагати від кориÑтувача змінити пароль (рекомендовано) +reset_password_mail_sent_prompt=Ðа адреÑу <b>%s</b> було надіÑлано лиÑÑ‚ із підтвердженнÑм. Будь лаÑка, перевірте Ñвою поштову Ñкриньку протÑгом наÑтупних %s, щоб завершити Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу. active_your_account=Ðктивувати обліковий Ð·Ð°Ð¿Ð¸Ñ account_activated=Обліковий Ð·Ð°Ð¿Ð¸Ñ Ð°ÐºÑ‚Ð¸Ð²Ð¾Ð²Ð°Ð½Ð¾ -prohibit_login=Вхід заборонений -resent_limit_prompt=Вибачте, ви вже запроÑили активацію по електронній пошті нещодавно. Будь лаÑка, зачекайте 3 хвилини, а потім Ñпробуйте ще раз. -has_unconfirmed_mail=Привіт %s, у Ð²Ð°Ñ Ñ” непідтверджена електронна адреÑа (<b>%s </b>). Якщо ви не отримали електронний лиÑÑ‚ із підтвердженнÑм або вам потрібно надіÑлати новий, натиÑніть на кнопку нижче. -resend_mail=ÐатиÑніть тут, щоб виÑлати лиÑÑ‚ активації знову -email_not_associate=Ð¦Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð° пошта не пов'Ñзана ні з одним обліковим запиÑом. -send_reset_mail=ÐадіÑлати електронний лиÑÑ‚ Ð´Ð»Ñ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу +prohibit_login=Вхід заборонено +prohibit_login_desc=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ð¾ Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ, звернітьÑÑ Ð´Ð¾ адмініÑтратора Ñайту. +resent_limit_prompt=Ви вже надÑилали запит на активацію нещодавно. Зачекайте 3 хвилини Ñ– Ñпробуйте ще раз. +has_unconfirmed_mail=Привіт %s, у Ð²Ð°Ñ Ð½ÐµÐ¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð° адреÑа електронної пошти (<b>%s</b>). Якщо ви не отримали лиÑта з підтвердженнÑм або вам потрібно надіÑлати новий, будь лаÑка, натиÑніть кнопку нижче. +change_unconfirmed_mail_address=Якщо ваша адреÑа електронної пошти Ð´Ð»Ñ Ñ€ÐµÑ”Ñтрації невірна, ви можете змінити Ñ—Ñ— тут Ñ– надіÑлати новий лиÑÑ‚ з підтвердженнÑм. +resend_mail=ÐатиÑніть тут, щоб повторно надіÑлати лиÑÑ‚ з активацією +email_not_associate=Ð¦Ñ Ð°Ð´Ñ€ÐµÑа електронної пошти не пов'Ñзана з жодним обліковим запиÑом. +send_reset_mail=ÐадіÑлати лиÑÑ‚ Ð´Ð»Ñ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу reset_password=Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу -invalid_code=Цей код Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð½ÐµÐ´Ñ–Ð¹Ñний або закінчивÑÑ. +invalid_code=Ваш код Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð½ÐµÐ´Ñ–Ð¹Ñний або його термін дії закінчивÑÑ. +invalid_code_forgot_password=Ваш код Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð½ÐµÐ´Ñ–Ð¹Ñний або термін дії минув. ÐатиÑніть <a href="%s">тут</a>, щоб почати новий ÑеанÑ. +invalid_password=Ваш пароль не збігаєтьÑÑ Ð· паролем, Ñкий викориÑтовувавÑÑ Ð¿Ñ€Ð¸ Ñтворенні облікового запиÑу. reset_password_helper=Відновити обліковий Ð·Ð°Ð¿Ð¸Ñ +reset_password_wrong_user=Ви увійшли Ñк %s, але поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу призначене Ð´Ð»Ñ %s password_too_short=Довжина Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð½Ðµ може бути меншою за %d Ñимволів. -non_local_account=Ðелокальні акаунти не можуть змінити пароль через Gitea. +non_local_account=Ðелокальні кориÑтувачі не можуть оновити Ñвій пароль через Ð²ÐµÐ±Ñ–Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Gitea. verify=Підтвердити scratch_code=Одноразовий пароль -use_scratch_code=ВикориÑтовувати одноразовий пароль -twofa_scratch_used=Ви викориÑтовували одноразовий пароль. Ви були перенаправлені на Ñторінку налаштувань Ð´Ð»Ñ Ð³ÐµÐ½ÐµÑ€Ð°Ñ†Ñ–Ñ— нового коду або Ð²Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð´Ð²ÑƒÑ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð¾Ñ— автентифікації. -twofa_passcode_incorrect=Ваш пароль Ñ” невірним. Якщо ви втратили приÑтрій, викориÑтовуйте ваш одноразовий пароль. +use_scratch_code=СкориÑтатиÑÑŒ одноразовим паролем +twofa_scratch_used=Ви ÑкориÑталиÑÑ Ð¾Ð´Ð½Ð¾Ñ€Ð°Ð·Ð¾Ð²Ð¸Ð¼ паролем. Ð’Ð°Ñ Ð¿ÐµÑ€ÐµÐ½Ð°Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¾ на Ñторінку налаштувань двофакторної автентифікації, щоб видалити приÑтрій або згенерувати новий одноразовий пароль. +twofa_passcode_incorrect=Ваш пароль неправильний. Якщо ви загубили Ñвій приÑтрій, ÑкориÑтайтеÑÑ Ð¾Ð´Ð½Ð¾Ñ€Ð°Ð·Ð¾Ð²Ð¸Ð¼ паролем. twofa_scratch_token_incorrect=Ðевірний одноразовий пароль. login_userpass=Увійти login_openid=OpenID oauth_signup_tab=ЗареєÑтрувати обліковий Ð·Ð°Ð¿Ð¸Ñ -oauth_signup_title=Повний новий обліковий Ð·Ð°Ð¿Ð¸Ñ -oauth_signup_submit=Повний обліковий Ð·Ð°Ð¿Ð¸Ñ -oauth_signin_tab=ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° Ñ–Ñнуючий обліковий Ð·Ð°Ð¿Ð¸Ñ +oauth_signup_title=Завершити реєÑтрацію облікового запиÑу +oauth_signup_submit=Поповнити обліковий Ð·Ð°Ð¿Ð¸Ñ +oauth_signin_tab=Прив'Ñзати до Ñ–Ñнуючого облікового запиÑу oauth_signin_title=Увійдіть щоб авторизувати пов'Ñзаний обліковий Ð·Ð°Ð¿Ð¸Ñ oauth_signin_submit=Прив'Ñзати обліковий Ð·Ð°Ð¿Ð¸Ñ +oauth.signin.error.general=Під Ñ‡Ð°Ñ Ð¾Ð±Ñ€Ð¾Ð±ÐºÐ¸ запиту на авторизацію ÑталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ°: %s. Якщо Ñ†Ñ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° повторитьÑÑ, звернітьÑÑ Ð´Ð¾ адмініÑтратора Ñайту. +oauth.signin.error.access_denied=Запит на авторизацію відхилено. +oauth.signin.error.temporarily_unavailable=Помилка авторизації, Ñервер автентифікації тимчаÑово недоÑтупний. Спробуйте пізніше. openid_connect_submit=Під’єднатиÑÑ openid_connect_title=ПідключитиÑÑ Ð´Ð¾ Ñ–Ñнуючого облікового запиÑу -openid_connect_desc=Вибраний OpenID URI невідомий. Пов'Ñжіть його з новим обліковим запиÑом тут. +openid_connect_desc=Обраний OpenID URI невідомий. Зв'Ñжіть його тут з новим обліковим запиÑом. openid_register_title=Створити новий обліковий Ð·Ð°Ð¿Ð¸Ñ -openid_register_desc=Вибраний OpenID URI невідомий. Пов'Ñжіть йогоз новим обліковим запиÑом тут. +openid_register_desc=Обраний OpenID URI невідомий. Зв'Ñжіть його тут з новим обліковим запиÑом. +openid_signin_desc=Введіть Ñвій OpenID URI. Ðаприклад: alice.openid.example.org або https://openid.example.org/alice. disable_forgot_password_mail=Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу вимкнено, оÑкільки не налаштована електронна пошта. Будь лаÑка, зв'ÑжітьÑÑ Ð· адмініÑтратором Ñайту. -disable_forgot_password_mail_admin=Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу доÑтупне лише піÑÐ»Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾Ñ— пошти. Будь лаÑка, налаштуйте ел. пошту Ð´Ð»Ñ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу. -email_domain_blacklisted=З вказаним email реєÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ Ð½ÐµÐ¼Ð¾Ð¶Ð»Ð¸Ð²Ð°. +disable_forgot_password_mail_admin=Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу доÑтупне лише за наÑвноÑті електронної пошти. Будь лаÑка, налаштуйте електронну пошту, щоб увімкнути Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу. +email_domain_blacklisted=Ви не можете зареєÑтруватиÑÑ Ð· адреÑою електронної пошти. authorize_application=Ðвторизувати програму authorize_redirect_notice=Ð’Ð°Ñ Ð±ÑƒÐ´Ðµ переадреÑовано до %s, Ñкщо ви авторизуєте цю програму. authorize_application_created_by=Ð¦Ñ Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð° Ñтворена %s. -authorize_application_description=Якщо ви надаÑте цей доÑтуп, то він матиме доÑтуп до вÑÑ–Ñ… ваших даних облікового запиÑу, включаючи приватні репозиторії та організації. -authorize_title=Ðвторизуйвати "%s" Ð´Ð»Ñ Ð´Ð¾Ñтупу до вашого облікового запиÑу? +authorize_application_description=Якщо ви авторизуєте цю програму, їй буде надано дозвіл на Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ Ð²Ñієї інформації вашого облікового запиÑу, включно з приватними Ñховищами та організаціÑми. +authorize_title=Ðвторизувати "%s" Ð´Ð»Ñ Ð´Ð¾Ñтупу до вашого облікового запиÑу? authorization_failed=Помилка авторизації -sspi_auth_failed=Помилка SSPI-автентифікації +authorization_failed_desc=ÐÐ²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ñ–Ñ Ð½Ðµ вдалаÑÑ, оÑкільки ми виÑвили недійÑний запит. ЗвернітьÑÑ Ð´Ð¾ розробника програми, Ñку ви намагалиÑÑ Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·ÑƒÐ²Ð°Ñ‚Ð¸. +sspi_auth_failed=Помилка автентифікації SSPI +password_pwned=Пароль, Ñкий ви обрали, знаходитьÑÑ Ð² <a target="_blank" rel="noopener noreferrer" href="https://haveibeenpwned.com/Passwords">ÑпиÑку викрадених паролів</a>, раніше викритих в публічних витоках даних. Спробуйте ще раз з іншим паролем Ñ– подумайте про зміну цього Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð´ÐµÑ–Ð½Ð´Ðµ. password_pwned_err=Ðе вдалоÑÑ Ð²Ð¸ÐºÐ¾Ð½Ð°Ñ‚Ð¸ запит до HaveIBeenPwed +last_admin=Ðе можна видалити оÑтаннього адмініÑтратора. Повинен бути хоча б один адмініÑтратор. +signin_passkey=Увійти за допомогою ключа доÑтупу +back_to_sign_in=ПовернутиÑÑ Ð´Ð¾ авторизації [mail] view_it_on=ПереглÑнути на %s -link_not_working_do_paste=Ðе працює? Спробуйте Ñкопіювати та вÑтавити його в Ñвій браузер. +reply=або надішліть відповідь безпоÑередньо на цей електронний лиÑÑ‚ +link_not_working_do_paste=Ðе працює? Спробуйте Ñкопіювати та вÑтавити його у браузер. hi_user_x=Привіт <b>%s</b>, activate_account=Будь лаÑка, активуйте ваш обліковий Ð·Ð°Ð¿Ð¸Ñ activate_account.title=%s, будь лаÑка, активуйте Ñвій обліковий Ð·Ð°Ð¿Ð¸Ñ activate_account.text_1=Привіт, <b>%[1]s</b>, дÑкуємо за реєÑтрацію на %[2]s! -activate_account.text_2=Перейдіть за цим поÑиланнÑм, щоб активувати ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð² <b>%s</b>: +activate_account.text_2=Будь лаÑка, перейдіть за наÑтупним поÑиланнÑм, щоб активувати Ñвій обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¿Ñ€Ð¾Ñ‚Ñгом <b>%s</b>: -activate_email=Підтвердить вашу адреÑу електронної пошти -activate_email.text=Перейдіть за цим поÑиланнÑм, щоб підтвердити вашу електронну адреÑу в <b>%s</b>: +activate_email=Підтвердіть адреÑу електронної пошти +activate_email.title=%s, будь лаÑка, підтвердіть вашу адреÑу електронної пошти +activate_email.text=Будь лаÑка, перейдіть за наÑтупним поÑиланнÑм протÑгом <b>%s</b>, щоб підтвердити Ñвою електронну адреÑу: +register_notify=ЛаÑкаво проÑимо до %s register_notify.title=%[1]s, лаÑкаво проÑимо до %[2]s -register_notify.text_1=це ваша е-пошта Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ñ€ÐµÑ”Ñтрації Ð´Ð»Ñ %s! +register_notify.text_1=це лиÑÑ‚ з підтвердженнÑм реєÑтрації на %s! register_notify.text_2=Тепер ви можете увійти Ñк: %s. -register_notify.text_3=Якщо цей обліковий Ð·Ð°Ð¿Ð¸Ñ Ð±ÑƒÐ»Ð¾ Ñтворено Ð´Ð»Ñ Ð²Ð°Ñ, будь лаÑка, Ñпочатку <a href="%s">вÑтановіть Ñвій пароль</a>. +register_notify.text_3=Якщо цей обліковий Ð·Ð°Ð¿Ð¸Ñ Ñтворено Ð´Ð»Ñ Ð²Ð°Ñ, будь лаÑка, Ñпершу <a href="%s">вÑтановіть пароль</a>. -reset_password=Ð’Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ облікового запиÑу -reset_password.title=%s, ви відправили запит на Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу -reset_password.text=Перейдіть за цим поÑиланнÑм, щоб відновити ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð² <b>%s</b>: +reset_password=Відновити обліковий Ð·Ð°Ð¿Ð¸Ñ +reset_password.title=%s, ви надіÑлали запит на Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу +reset_password.text=Щоб відновити обліковий запиÑ, перейдіть за наÑтупним поÑиÐ»Ð°Ð½Ð½Ñм протÑгом <b>%s</b>: register_success=РеєÑÑ‚Ñ€Ð°Ñ†Ñ–Ñ ÑƒÑпішна -issue_assigned.pull=@%[1]s призначив вам запит Ð·Ð»Ð¸Ñ‚Ñ‚Ñ %[2]s в репозиторії %[3]s. -issue_assigned.issue=@%[1]s призначив вам задачу %[2]s у репозиторії %[3]s. +issue_assigned.pull=@%[1]s призначив вам запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ %[2]s в Ñховищі %[3]s. +issue_assigned.issue=@%[1]s призначив вам задачу %[2]s в Ñховищі %[3]s. issue.x_mentioned_you=<b>@%s</b> згадав ваÑ: -issue.action.force_push=<b>%[1]s</b> force-pushed <b>%[2]s</b> з %[3]s в %[4]s. -issue.action.push_1=<b>@%[1]s</b> надіÑлав %[3]d коміти %[2]s -issue.action.push_n=<b>@%[1]s</b> відправив %[3]d коміти до %[2]s -issue.action.close=<b>@%[1]s</b> закрито #%[2]d. +issue.action.push_1=<b>@%[1]s</b> надіÑлав %[3]d коміт в %[2]s +issue.action.push_n=<b>@%[1]s</b> надіÑлав %[3]d коміти до %[2]s +issue.action.close=<b>@%[1]s</b> закрив #%[2]d. issue.action.reopen=<b>@%[1]s</b> заново відкрив #%[2]d. issue.action.merge=<b>@%[1]s</b> об'єднав #%[2]d до %[3]s. -issue.action.approve=<b>@%[1]s</b> затвердили цей запит на злиттÑ. -issue.action.reject=<b>@%[1]s</b> запитують зміни на цей запит на злиттÑ. -issue.action.review=<b>@%[1]s</b> прокоментували цей запит на злиттÑ. -issue.action.review_dismissed=<b>@%[1]s</b> відхилено оÑтанній відгук від %[2]s Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ запиту на злиттÑ. -issue.action.ready_for_review=<b>@%[1]s</b> позначили цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñк готовий до розглÑду. -issue.action.new=<b>@%[1]s</b> Ñтворили #%[2]d. +issue.action.approve=<b>@%[1]s</b> затвердив цей запит на злиттÑ. +issue.action.reject=<b>@%[1]s</b> запроÑив зміни у цьому запиті на злиттÑ. +issue.action.review=<b>@%[1]s</b> прокоментував цей запит на злиттÑ. +issue.action.review_dismissed=<b>@%[1]s</b> відхилив оÑтанній відгук від %[2]s на цей запит на злиттÑ. +issue.action.ready_for_review=<b>@%[1]s</b> позначив цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñк готовий до розглÑду. +issue.action.new=<b>@%[1]s</b> Ñтворив #%[2]d. issue.in_tree_path=Ð’ %s: release.new.subject=%s в %s випущено @@ -372,35 +532,40 @@ release.downloads=ЗвантаженнÑ: release.download.zip=Вихідний код (ZIP) release.download.targz=Вихідний код (TAR.GZ) -repo.transfer.subject_to=%s бажає передати"%s" в %s -repo.transfer.subject_to_you=%s бажає передати"%s" вам +repo.transfer.subject_to=%s хоче перенеÑти "%s" в %s +repo.transfer.subject_to_you=%s хоче передати"%s" вам repo.transfer.to_you=вам repo.transfer.body=Щоб прийнÑти або відхилити перейдіть до %s або проÑто ігноруйте. repo.collaborator.added.subject=%s додав Ð²Ð°Ñ Ð´Ð¾ %s -repo.collaborator.added.text=Ви були додані в ÑкоÑті Ñпівавтора репозиторію: +repo.collaborator.added.text=Ð’Ð°Ñ Ð´Ð¾Ð´Ð°Ð»Ð¸ Ñк Ñпівавтора до Ñховища: +team_invite.subject=%[1]s запрошує Ð²Ð°Ñ Ð¿Ñ€Ð¸Ñ”Ð´Ð½Ð°Ñ‚Ð¸ÑÑ Ð´Ð¾ організації %[2]s +team_invite.text_1=%[1]s запрошує Ð²Ð°Ñ Ð´Ð¾ команди %[2]s в організації %[3]s. +team_invite.text_2=Перейдіть за поÑиланнÑм, щоб приєднатиÑÑ Ð´Ð¾ команди: +team_invite.text_3=Примітка: Це Ð·Ð°Ð¿Ñ€Ð¾ÑˆÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ðµ Ð´Ð»Ñ %[1]s. Якщо ви не очікували цього запрошеннÑ, ви можете проігнорувати це повідомленнÑ. [modal] yes=Так no=ÐÑ– +confirm=Підтвердити cancel=Відмінити -modify=ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ +modify=Оновити [form] UserName=Ð†Ð¼â€™Ñ ÐºÐ¾Ñ€Ð¸Ñтувача -RepoName=Ðазва репозиторію +RepoName=Ðазва Ñховища Email=ÐдреÑа електронної пошти Password=Пароль -Retype=ÐŸÑ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ -SSHTitle=Iм'Ñ SSH ключа +Retype=Підтвердити пароль +SSHTitle=Ðазва ключа SSH HttpsUrl=ÐдреÑа HTTPS PayloadUrl=URL обробника TeamName=Ðазва команди AuthName=Ðазва авторизації -AdminEmail=Email адмініÑтратора +AdminEmail=ÐдреÑа електронної пошти адмініÑтратора -NewBranchName=Ім'Ñ Ð½Ð¾Ð²Ð¾Ñ— гілки +NewBranchName=Ðазва нової гілки CommitSummary=Резюме коміту CommitMessage=ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ CommitChoice=Вибір коміта @@ -410,76 +575,127 @@ Content=ЗміÑÑ‚ SSPISeparatorReplacement=Розділювач SSPIDefaultLanguage=Типова мова -require_error=` не може бути пуÑтим.` -alpha_dash_error=` повинен міÑтити тільки літерно-цифрові Ñимволи, Ð´ÐµÑ„Ñ–Ñ ('-') та підкреÑÐ»ÐµÐ½Ð½Ñ ('_'). ` -alpha_dash_dot_error=` повинен міÑтити тільки літерно-цифрові Ñимволи, Ð´ÐµÑ„Ñ–Ñ ('-') , підкреÑÐ»ÐµÐ½Ð½Ñ ('_') та точки ('.'). ` -git_ref_name_error=` повинен бути правильним поÑилальним ім'Ñм Git.` +require_error=` не може бути порожнім.` +alpha_dash_error=` повинен міÑтити тільки алфавітно-цифрові Ñимволи, Ð´ÐµÑ„Ñ–Ñ ('-') та підкреÑÐ»ÐµÐ½Ð½Ñ ('_').` +alpha_dash_dot_error=` повинен міÑтити тільки алфавітно-цифрові Ñимволи, Ð´ÐµÑ„Ñ–Ñ ('-'), підкреÑÐ»ÐµÐ½Ð½Ñ ('_') та крапку ('.').` +git_ref_name_error=` повинен бути правильно Ñформованим ім'Ñм-поÑиланнÑм на Git.` size_error=` повинен бути розмір %s.` -min_size_error=` повинен бути принаймні %s Ñимволів.` -max_size_error=` повинен бути не більш Ñк %s Ñимволів.` -email_error=` не Ñ” адреÑою електронної пошти.` -glob_pattern_error=` неприпуÑтимий шаблон glob: %s.` -regex_pattern_error=` неприпуÑтимий шаблон regex: %s.` +min_size_error=` має міÑтити принаймні %s Ñимволів.` +max_size_error=` має міÑтити не більше %s Ñимволів.` +email_error=` не Ñ” дійÑною адреÑою електронної пошти.` +url_error=`"%s" не Ñ” дійÑною URL-адреÑою.` +include_error=` повинен міÑтити підрÑдок "%s".` +glob_pattern_error=` недійÑний шаблон glob: %s.` +regex_pattern_error=` недійÑний шаблон регулÑрного виразу: %s.` +username_error=` може міÑтити лише алфавітно-цифрові Ñимволи ('0-9', 'a-z', 'A-Z'), Ð´ÐµÑ„Ñ–Ñ ('-'), підкреÑÐ»ÐµÐ½Ð½Ñ ('_') та крапку ('.'). Ðе може починатиÑÑ Ð°Ð±Ð¾ закінчуватиÑÑ Ð½ÐµÐ°Ð»Ñ„Ð°Ð²Ñ–Ñ‚Ð½Ð¸Ð¼Ð¸ Ñимволами; поÑлідовні неалфавітні Ñимволи також заборонені.` unknown_error=Ðевідома помилка: captcha_incorrect=Код CAPTCHA неправильний. -password_not_match=Паролі не Ñпівпадають. -lang_select_error=Оберіть мову з переліку. - -username_been_taken=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача вже зайнÑто. -username_change_not_local_user=Ðелокальні кориÑтувачі не можуть змінити Ñвоє ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача. -repo_name_been_taken=Ім'Ñ Ñ€ÐµÐ¿Ð¾Ð·Ñ–Ñ‚Ð¾Ñ€Ñ–ÑŽ вже викориÑтовуєтьÑÑ. -repository_files_already_exist=Файли вже Ñ–Ñнують Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ репозитарію. ЗвернітьÑÑ Ð´Ð¾ ÑиÑтемного адмініÑтратора. -repository_files_already_exist.adopt=Файли вже Ñ–Ñнують Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ репозиторію Ñ– можуть бути лише прийнÑті. -repository_files_already_exist.delete=Файли вже Ñ–Ñнують Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñховища. Ви повинні видалити Ñ—Ñ…. -repository_files_already_exist.adopt_or_delete=Файли вже Ñ–Ñнують Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ репозиторію. Їх можливо прийнÑти або видалити. -visit_rate_limit=ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ ÑˆÐ²Ð¸Ð´ÐºÐ¾Ñті віддаленого доÑтупу. -2fa_auth_required=Ð”Ð»Ñ Ð²Ñ–Ð´Ð´Ð°Ð»ÐµÐ½Ð¾Ð³Ð¾ доÑтупу необхідна двуфакторна аутентифікаціÑ. -org_name_been_taken=Ðазва організації вже зайнÑто. -team_name_been_taken=Ðазва команди вже зайнÑто. -team_no_units_error=Дозволити доÑтуп до принаймні одного розділу репозитарію. -email_been_used=Ð¦Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð° адреÑа вже викориÑтовуєтьÑÑ. -email_invalid=ÐдреÑа електронної пошти помилкова. +password_not_match=Паролі не збігаютьÑÑ. +lang_select_error=Оберіть мову зі ÑпиÑку. + +username_been_taken=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача вже зайнÑте. +username_change_not_local_user=Ðелокальні кориÑтувачі не можуть змінювати Ñвоє ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача. +change_username_disabled=Зміна імені кориÑтувача відключена. +change_full_name_disabled=Зміна повного імені відключена. +username_has_not_been_changed=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача не змінено +repo_name_been_taken=Ðазва Ñховища вже викориÑтовуєтьÑÑ. +repository_force_private=ПримуÑову приватніÑть ввімкнено: приватні Ñховища не можна зробити загальнодоÑтупними. +repository_files_already_exist=Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñховища вже Ñ–Ñнують файли. ЗвернітьÑÑ Ð´Ð¾ ÑиÑтемного адмініÑтратора. +repository_files_already_exist.adopt=Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñховища вже Ñ–Ñнують файли, Ñ– Ñ—Ñ… можна лише прийнÑти. +repository_files_already_exist.delete=Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñховища вже Ñ–Ñнують файли. Ви повинні видалити Ñ—Ñ…. +repository_files_already_exist.adopt_or_delete=Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñховища вже Ñ–Ñнують файли. Прийміть Ñ—Ñ… або видаліть. +visit_rate_limit=Віддалений доÑтуп відхилено у зв'Ñзку з обмеженнÑм кількоÑті Ñпроб. +2fa_auth_required=Ð”Ð»Ñ Ð²Ñ–Ð´Ð´Ð°Ð»ÐµÐ½Ð¾Ð³Ð¾ доÑтупу необхідна двофакторна автентифікаціÑ. +org_name_been_taken=Ðазва організації вже зайнÑта. +team_name_been_taken=Ðазва команди вже зайнÑта. +team_no_units_error=Дозволити доÑтуп до принаймні одного розділу Ñховища. +email_been_used=ÐдреÑа електронної пошти вже викориÑтовуєтьÑÑ. +email_invalid=ÐдреÑа електронної пошти недійÑна. +email_domain_is_not_allowed=Домен електронної пошти кориÑтувача <b>%s</b> конфліктує з EMAIL_DOMAIN_ALLOWLIST або EMAIL_DOMAIN_BLOCKLIST. ПереконайтеÑÑ, що ваша Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð¾Ñ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð°. +openid_been_used=ÐдреÑа OpenID '%s' вже викориÑтовуєтьÑÑ. username_password_incorrect=Ðеправильне ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача або пароль. -password_complexity=Пароль не відповідає вимогам до ÑкладноÑті: -password_lowercase_one=Принаймні одна буква в нижньому регіÑтрі -password_uppercase_one=Принаймні одна буква в верхньому регіÑтрі +password_complexity=Пароль не відповідає вимогам ÑкладноÑті: +password_lowercase_one=Принаймні один Ñимвол нижнього регіÑтру +password_uppercase_one=Принаймні один Ñимвол верхнього регіÑтру password_digit_one=Принаймні одна цифра -password_special_one=Принаймні один Ñпеціальний Ñимвол (пунктуаціÑ, дужки, лапки тощо) -enterred_invalid_repo_name=Ðевірно введено ім'Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ. -enterred_invalid_org_name=Ðевірно введено ім'Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ—. -enterred_invalid_owner_name=Ім'Ñ Ð½Ð¾Ð²Ð¾Ð³Ð¾ влаÑника не Ñ” дійÑним. -enterred_invalid_password=Введений вами пароль некоректний. -user_not_exist=Даний кориÑтувач не Ñ–Ñнує. +password_special_one=Принаймні один Ñпеціальний Ñимвол (розділові знаки, дужки, лапки тощо) +enterred_invalid_repo_name=Ви ввели неправильну назву Ñховища. +enterred_invalid_org_name=Ви ввели неправильну назву організації. +enterred_invalid_owner_name=Ім'Ñ Ð½Ð¾Ð²Ð¾Ð³Ð¾ влаÑника недійÑне. +enterred_invalid_password=Ви ввели неправильний пароль. +unset_password=КориÑтувач не вÑтановив пароль. +unsupported_login_type=Тип входу не підтримуєтьÑÑ Ð´Ð»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу. +user_not_exist=КориÑтувач не Ñ–Ñнує. team_not_exist=Команда не Ñ–Ñнує. -last_org_owner=Ви не можете видалити оÑтаннього кориÑтувача з команди 'влаÑники'. У кожній команді має бути принаймні один влаÑник. +last_org_owner=Ви не можете видалити оÑтаннього кориÑтувача з групи 'влаÑників'. Ð’ організації має бути принаймні один влаÑник. cannot_add_org_to_team=Організацію неможливо додати Ñк учаÑника команди. - -invalid_ssh_key=Ðеможливо перевірити ваш SSH ключ: %s -invalid_gpg_key=Ðеможливо перевірити ваш GPG ключ: %s -invalid_ssh_principal=Ðекоректний відповідальний: %s +duplicate_invite_to_team=КориÑтувача вже запрошено Ñк члена команди. +organization_leave_success=Ви уÑпішно покинули організацію %s. + +invalid_ssh_key=Ðе вдаєтьÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ ключ SSH: %s +invalid_gpg_key=Ðе вдаєтьÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ ключ GPG: %s +invalid_ssh_principal=Ðевірна ідентичніÑть: %s +must_use_public_key=Ðаданий вами ключ — приватний. Будь лаÑка, нікуди не завантажуйте Ñвій приватний ключ. ÐатоміÑть викориÑтовуйте публічний ключ. +unable_verify_ssh_key=Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ ключ SSH, перевірте його на наÑвніÑть помилок. auth_failed=Помилка автентифікації: %v +still_own_repo=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð²Ð¾Ð»Ð¾Ð´Ñ–Ñ” одним або декількома Ñховищами, Ñпершу видаліть або перенеÑіть Ñ—Ñ…. +still_has_org=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ñ” учаÑником однієї або декількох організацій, Ñпершу залиште Ñ—Ñ…. +still_own_packages=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð²Ð¾Ð»Ð¾Ð´Ñ–Ñ” одним або декількома пакетами, Ñпершу видаліть Ñ—Ñ…. +org_still_own_repo=Ð¦Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð²Ñе ще володіє одним або декількома Ñховищами, Ñпочатку видаліть або перенеÑіть Ñ—Ñ…. +org_still_own_packages=Ð¦Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð²Ñе ще має один або декілька пакетів, Ñпочатку видаліть Ñ—Ñ…. target_branch_not_exist=Цільової гілки не Ñ–Ñнує. +target_ref_not_exist=Цільове поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ðµ Ñ–Ñнує %s +admin_cannot_delete_self=Ви не можете видалити Ñебе, допоки ви адмініÑтратор. Будь лаÑка, Ñпочатку видаліть права адмініÑтратора. [user] -change_avatar=Змінити Ñвій аватар… -repositories=Репозиторії +change_avatar=Змінити аватар… +joined_on=ПриєднавÑÑ(-лаÑÑŒ) %s +repositories=Сховища activity=Публічна активніÑть -followers=Читачі +followers=ПоÑлідовники show_more=Показати більше -starred=Обрані Репозиторії -watched=ВідÑтежувані репозиторії -projects=Проєкт +starred=Обрані Ñховища +watched=ВідÑтежувані Ñховища +code=Код +projects=Проєкти overview=ОглÑд -following=Читає -follow=ПідпиÑатиÑÑ -unfollow=ВідпиÑатиÑÑ +following=ВідÑтежувані +follow=Стежити +unfollow=Ðе Ñтежити user_bio=Ð‘Ñ–Ð¾Ð³Ñ€Ð°Ñ„Ñ–Ñ -disabled_public_activity=Цей кориÑтувач вимкнув публічний показ діÑльноÑті. - - +disabled_public_activity=Цей кориÑтувач вимкнув публічну видиміÑть активноÑті. +email_visibility.limited=Ваша електронна пошта видима Ð´Ð»Ñ Ð²ÑÑ–Ñ… автентифікованих кориÑтувачів +email_visibility.private=Вашу адреÑу електронної пошти бачитимете лише ви та адмініÑтратори +show_on_map=Показати це міÑце на карті +settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача + +form.name_reserved=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача "%s" зарезервовано. +form.name_pattern_not_allowed=Шаблон "%s" не дозволено в імені кориÑтувача. +form.name_chars_not_allowed=Ð†Ð¼â€™Ñ ÐºÐ¾Ñ€Ð¸Ñтувача "%s" міÑтить неприпуÑтимі Ñимволи. + +block.block=Заблокувати +block.block.user=Заблокувати кориÑтувача +block.block.org=Заблокувати кориÑтувача Ð´Ð»Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ— +block.block.failure=Ðе вдалоÑÑ Ð·Ð°Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ñ‚Ð¸ кориÑтувача: %s +block.unblock=Розблокувати +block.unblock.failure=Ðе вдалоÑÑ Ñ€Ð¾Ð·Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ñ‚Ð¸ кориÑтувача: %s +block.blocked=Ви заблокували цього кориÑтувача. +block.title=Заблокувати кориÑтувача +block.info=Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача не дозволÑÑ” йому взаємодіÑти зі Ñховищами, наприклад, відкривати або коментувати запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð°Ð±Ð¾ задачі. ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача. +block.info_1=Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача запобігає наÑтупним діÑм у вашому обліковому запиÑÑ– та ваших Ñховищах: +block.info_2=Ñлідкують за вашим обліковим запиÑом +block.info_3=надÑилати вам ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ @згадавши ваше ім'Ñ +block.info_6=Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ñ‚Ñ Ñ‚Ð° ÐºÐ¾Ð¼ÐµÐ½Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð°Ð´Ð°Ñ‡ або запитів на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +block.user_to_block=Блокувати КориÑтувача +block.note=Примітка +block.note.title=Ðеобов’Ñзкова примітка: +block.note.info=Ðотатка не видима Ð´Ð»Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ð¾Ð³Ð¾ кориÑтувача. +block.note.edit=Редагувати нотатку +block.list=Заблоковані кориÑтувачі +block.list.none=Ви не заблокували жодного кориÑтувача. [settings] profile=Профіль @@ -488,336 +704,457 @@ appearance=Зовнішній виглÑд password=Пароль security=Безпека avatar=Ðватар -ssh_gpg_keys=SSH / GPG ключі +ssh_gpg_keys=Ключі SSH / GPG social=Соціальні облікові запиÑи applications=Додатки orgs=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñми repos=Репозиторії delete=Видалити обліковий Ð·Ð°Ð¿Ð¸Ñ +twofa=Двофакторна Ð°Ð²Ñ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ (TOTP) account_link=Прив'Ñзані облікові запиÑи organization=Організації +uid=UID +webauthn=Двофакторна Ð°Ð²Ñ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ (ключі безпеки) public_profile=ЗагальнодоÑтупний профіль +biography_placeholder=Розкажіть нам трохи про Ñебе! (Ви можете викориÑтовувати Markdown) +location_placeholder=ДілитиÑÑ Ñвоїм приблизним географічним положеннÑм з іншими +profile_desc=Керуйте тим, Ñк ваш профіль буде показано іншим кориÑтувачам. Ваша оÑновна адреÑа електронної пошти викориÑтовуватиметьÑÑ Ð´Ð»Ñ Ñповіщень, Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ñ‚Ð° веб-операцій Git. +password_username_disabled=Вам не дозволено змінювати ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача. Будь лаÑка, зв'ÑжітьÑÑ Ð· адмініÑтратором Ñайту Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð±Ñ–Ð»ÑŒÑˆ докладної інформації. +password_full_name_disabled=Вам не дозволено змінювати ваше ім'Ñ. Будь лаÑка, зв'ÑжітьÑÑ Ð· адмініÑтратором Ñайту Ð´Ð»Ñ Ð±Ñ–Ð»ÑŒÑˆ докладної інформації. full_name=Повне ім'Ñ website=Веб-Ñайт location=МіÑÑ†ÐµÐ·Ð½Ð°Ñ…Ð¾Ð´Ð¶ÐµÐ½Ð½Ñ update_theme=Оновити тему update_profile=Оновити профіль update_language=Оновити мову +update_language_not_found=Мова "%s" недоÑтупна. update_language_success=Мову оновлено. update_profile_success=Профіль уÑпішно оновлено. -change_username=Ваше Ім'Ñ ÐºÑ€Ð¸Ñтувача було змінено. +change_username=Ваше ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача змінено. +change_username_prompt=Примітка: Зміна імені кориÑтувача також змінює URL-адреÑу облікового запиÑу. +change_username_redirect_prompt=Старе ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача буде перенаправлÑтиÑÑ Ð½Ð° нове, поки хтоÑÑŒ не викориÑтає його. continue=Продовжити cancel=Відмінити language=Мова ui=Тема +hidden_comment_types=Приховані типи коментарів +hidden_comment_types_description=Позначені тут типи коментарів не будуть показані на Ñторінках проблем. Ðаприклад, позначка «Мітка» вилучає вÑÑ– коментарі «{user} додав/вилучив {label}». +hidden_comment_types.ref_tooltip=Коментарі, де на цю задачу було зроблено поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð· іншого випуÑку/коміту/… +hidden_comment_types.issue_ref_tooltip=Коментарі, в Ñких кориÑтувач змінює гілку/тег, пов'Ñзані з нею +comment_type_group_reference=ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ comment_type_group_label=Мітка comment_type_group_milestone=Етап comment_type_group_assignee=Виконавець comment_type_group_title=Заголовок comment_type_group_branch=Гілка +comment_type_group_time_tracking=Облік чаÑу +comment_type_group_deadline=Крайній Ñтрок +comment_type_group_dependency=ЗалежніÑть +comment_type_group_lock=Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ +comment_type_group_review_request=Запит на перевірку +comment_type_group_pull_request_push=Додані коміти comment_type_group_project=Проєкт +comment_type_group_issue_ref=ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° задачу +saved_successfully=Ваші Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÑƒÑпішно збережено. privacy=ПриватніÑть +keep_activity_private=Приховати активніÑть зі Ñторінки профілю keep_activity_private_popup=Показувати вашу активніÑть лише Вам та адмініÑтраторам lookup_avatar_by_mail=Знайти Ðватар за адреÑою електронної пошти federated_avatar_lookup=Знайти зовнішній аватар -enable_custom_avatar=Увімкнути кориÑтувацькі аватари -choose_new_avatar=Оберіть новий аватар +enable_custom_avatar=Увімкнути кориÑтувацький аватар +choose_new_avatar=Обрати новий аватар update_avatar=Оновити аватар delete_current_avatar=Видалити поточний аватар uploaded_avatar_not_a_image=Завантажений файл не Ñ” зображеннÑм. -update_avatar_success=Ваш аватар був змінений. +uploaded_avatar_is_too_big=Розмір завантаженого файлу (%d KiB) перевищує макÑимальний розмір (%d KiB). +update_avatar_success=Ваш аватар оновлено. update_user_avatar_success=Ðватар кориÑтувача оновлено. +cropper_prompt=Ви можете відредагувати Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð¿ÐµÑ€ÐµÐ´ збереженнÑм. Відредаговане Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð±ÑƒÐ´Ðµ збережено Ñк PNG. change_password=Оновити пароль old_password=Поточний пароль new_password=Ðовий пароль +retype_new_password=Підтвердити новий пароль password_incorrect=Поточний пароль неправильний. -change_password_success=Ваш пароль був оновлений. Тепер увійдіть в ÑиÑтему, викориÑтовуючи новий пароль. -password_change_disabled=Ðелокальні акаунти не можуть змінити пароль через Gitea. +change_password_success=Ваш пароль оновлено. Відтепер входьте в ÑиÑтему, викориÑтовуючи новий пароль. +password_change_disabled=Ðелокальні кориÑтувачі не можуть оновити Ñвій пароль через Ð²ÐµÐ±Ñ–Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Gitea. emails=ÐдреÑа електронної пошти -manage_emails=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð´Ñ€ÐµÑами ел. пошти -manage_themes=Виберіть тему за замовчуваннÑм -manage_openid=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ OpenID +manage_emails=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð´Ñ€ÐµÑами електронної пошти +manage_themes=Обрати типову тему +manage_openid=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð´Ñ€ÐµÑами OpenID +email_desc=Ваша оÑновна адреÑа електронної пошти викориÑтовуватиметьÑÑ Ð´Ð»Ñ Ñповіщень, Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ñ–, за умови, що вона не прихована, Ð´Ð»Ñ Ð²ÐµÐ±-операцій з Git. theme_desc=Ð¦Ñ Ñ‚ÐµÐ¼Ð° буде типовою Ð´Ð»Ñ Ð²Ñього Ñайту. +theme_colorblindness_help=Підтримка тем колірної Ñліпоти +theme_colorblindness_prompt=Gitea щойно отримала деÑкі теми з базовою підтримкою колірної Ñліпоти, в Ñких визначено лише кілька кольорів. Робота над вÑе ще триває. Ще більше покращень можна зробити, визначивши більше кольорів у CSS-файлах теми. primary=ОÑновний activated=Ðктивовано requires_activation=Потрібна Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ -primary_email=Зробити оÑновним +primary_email=Зробити оÑновною activate_email=ÐадіÑлати активацію -activations_pending=Ðктивації в очікуванні +activations_pending=ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ— +can_not_add_email_activations_pending=ВідбуваєтьÑÑ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ, Ñпробуйте ще раз через кілька хвилин, Ñкщо хочете додати нову адреÑу електронної пошти. delete_email=Видалити email_deletion=Видалити адреÑу електронної пошти -email_deletion_desc=Електронна адреÑа та пов'Ñзана з нею Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð±ÑƒÐ´Ðµ видалена з вашого облікового запиÑу. Git коміти, здійÑнені через цю електронну адреÑу, залишитьÑÑ Ð±ÐµÐ· змін. Продовжити? -email_deletion_success=ÐдреÑу електронної пошти було видалено. +email_deletion_desc=ÐдреÑа електронної пошти та пов'Ñзана з нею Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð±ÑƒÐ´Ðµ видалена з вашого облікового запиÑу. Коміти Git, здійÑнені через цю адреÑу електронну пошту, залишитьÑÑ Ð±ÐµÐ· змін. Продовжити? +email_deletion_success=ÐдреÑу електронної пошти видалено. theme_update_success=Тему оновлено. -theme_update_error=Вибрана тема не Ñ–Ñнує. +theme_update_error=Обрана тема не Ñ–Ñнує. openid_deletion=Видалити адреÑу OpenID -openid_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ†Ñ–Ñ”Ñ— OpenID-адреÑи з вашого облікового запиÑу заборонÑÑ” вам входити з ним. Продовжити? -openid_deletion_success=ÐдреÑа OpenID була видалена. +openid_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ†Ñ–Ñ”Ñ— адреÑи OpenID не дозволить вам увійти за нею. Продовжити? +openid_deletion_success=ÐдреÑу OpenID видалено. add_new_email=Додати нову адреÑу електронної пошти add_new_openid=Додати новий OpenID URI add_email=Додати адреÑу електронної пошти add_openid=Додати OpenID URI +add_email_confirmation_sent=Електронний лиÑÑ‚ із підтвердженнÑм було відправлено на '%s', будь лаÑка, перевірте вашу поштову Ñкриньку протÑгом наÑтупних %s, щоб підтвердити адреÑу. add_email_success=Додано нову адреÑу електронної пошти. -email_preference_set_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾Ñ— пошти уÑпішно вÑтановлені. -add_openid_success=Ðова адреÑа OpenID була додана. +email_preference_set_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾Ñ— пошти уÑпішно вÑтановлено. +add_openid_success=Додано нову адреÑу OpenID. keep_email_private=Приховати адреÑу електронної пошти -openid_desc=OpenID дозволÑÑ” делегувати аутентифікацію зовнішньому поÑтачальнику поÑлуг. +openid_desc=OpenID дозволÑÑ” делегувати автентифікацію зовнішньому поÑтачальнику поÑлуг. -manage_ssh_keys=Керувати SSH ключами -manage_ssh_principals=Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ SSH Ñертифікатами кориÑтувачів -manage_gpg_keys=Керувати GPG ключами +manage_ssh_keys=Керувати ключами SSH +manage_ssh_principals=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñ–Ð´ÐµÐ½Ñ‚Ð¸Ñ‡Ð½Ð¾ÑÑ‚Ñми Ñертифікатів SSH +manage_gpg_keys=Керувати ключами SSH add_key=Додати ключ -ssh_desc=Ці відкриті SSH-ключі пов'Ñзані з вашим обліковим запиÑом. Відповідні приватні ключі дозволÑють отримати повний доÑтуп до ваших репозиторіїв. -principal_desc=Ці наÑтройки SSH Ñертифікатів вказані у вашому обліковому запиÑÑ– та надають повний доÑтуп до ваших репозиторіїв. -gpg_desc=Ці публічні ключі GPG пов'Ñзані з вашим обліковим запиÑом. Тримайте Ñвої приватні ключі в безпеці, оÑкільки вони дозволÑють здійÑнювати перевірку комітів. -ssh_helper=<strong>Потрібна допомога?</strong> ДивітьÑÑ Ð³Ñ–Ð´ на GitHub з <a href="%s"> генерації ключів SSH</a> або Ð²Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ <a href="%s">типових неполадок SSH</a>. -gpg_helper=<strong> Потрібна допомога? </strong> ПереглÑньте поÑібник GitHub <a href="%s"> про GPG </a>. -add_new_key=Додати SSH ключ -add_new_gpg_key=Додати GPG ключ +ssh_desc=Ці публічні ключі SSH пов'Ñзані з вашим обліковим запиÑом. Відповідні приватні ключі надають повний доÑтуп до ваших Ñховищ. +principal_desc=Ці ідентифікатори Ñертифікатів SSH прив'Ñзані до вашого облікового запиÑу Ñ– надають повний доÑтуп до ваших Ñховищ. +gpg_desc=Ці публічні ключі GPG пов'Ñзані з вашим обліковим запиÑом. Зберігайте Ñвої приватні ключі в безпеці, оÑкільки вони дозволÑють підтверджувати коміти. +ssh_helper=<strong>Потрібна допомога?</strong> ОзнайомтеÑÑ Ð· інÑтрукцією GitHub щодо <a href="%s">ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð²Ð»Ð°Ñних ключів SSH</a> або Ð²Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ <a href="%s">типових неполадок SSH.</a> +gpg_helper=<strong>Потрібна допомога?</strong> ПереглÑньте поÑібник GitHub <a href="%s">про GPG</a>. +add_new_key=Додати ключ SSH +add_new_gpg_key=Додати ключ GPG key_content_ssh_placeholder=ПочинаєтьÑÑ Ð· 'ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ecdsa-sha2-nistp256@openssh.com', або 'sk-ssh-ed25519@openssh.com' key_content_gpg_placeholder=ПочинаєтьÑÑ Ð· '-----BEGIN PGP PUBLIC KEY BLOCK-----' -add_new_principal=Додати кориÑтувача -ssh_key_been_used=Цей SSH ключ вже був додано до Ñервера. +add_new_principal=Додати ідентичніÑть +ssh_key_been_used=Цей ключ SSH вже було додано до Ñервера. ssh_key_name_used=Ключ SSH з таким ім'Ñм вже Ñ–Ñнує у вашому обліковому запиÑÑ–. -ssh_principal_been_used=Цей кориÑтувач вже був доданий на Ñервер. +ssh_principal_been_used=Цю ідентичніÑть вже було додано до Ñервера. gpg_key_id_used=Публічний ключ GPG з таким Ñамим ідентифікатором вже Ñ–Ñнує. -gpg_no_key_email_found=Цей ключ GPG не відповідає жодній активованій поштовій адреÑÑ–, Ñка пов'Ñзана з вашим обліковим запиÑом. Його вÑе рівно можна додати, Ñкщо ви підпишете наданий токен. -gpg_key_matched_identities=Відповідні отримувачі: -gpg_key_matched_identities_long=Вбудовані ідентифікатори цього ключа збігаютьÑÑ Ð· наÑтупними активованими адреÑами електронної пошти вказаного кориÑтувача. Коміти, Ñкі відповідають цим адреÑам, можуть бути підтверджені цим ключем. +gpg_no_key_email_found=Цей ключ GPG не відповідає жодній активованій адреÑÑ– електронної пошти, пов'Ñзаній з вашим обліковим запиÑом. Його вÑе одно можна додати, Ñкщо ви підпишете наданий токен. +gpg_key_matched_identities=Відповідні ідентичноÑті: +gpg_key_matched_identities_long=Вбудовані ідентифікатори цього ключа збігаютьÑÑ Ð· наÑтупними активованими адреÑами електронної пошти цього кориÑтувача. За допомогою цього ключа можна перевірÑти коміти, що відповідають цим адреÑам електронної пошти. gpg_key_verified=Перевірений ключ -gpg_key_verified_long=Ключ перевірений за допомогою токена Ñ– може бути викориÑтано Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñ–Ð², Ñкі відповідають будь-Ñкій з активованих Ð°Ð´Ñ€ÐµÑ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾Ñ— пошти Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ кориÑтувача, на додачу до будь-Ñких відповідних ідентифікацій Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ ключа. +gpg_key_verified_long=Ключ був перевірений токеном Ñ– може бути викориÑтаний Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ комітів, що відповідають будь-Ñким активованим адреÑам електронної пошти Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ кориÑтувача, а також будь-Ñких ідентифікаторів, що відповідають цьому ключу. gpg_key_verify=Підтвердити -gpg_invalid_token_signature=Ðаданий ключ GPG, Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ñ– токен не Ñпівпадають або токен заÑтарів. +gpg_invalid_token_signature=Ðадані ключ GPG, Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ñ– токен не збігаютьÑÑ Ð°Ð±Ð¾ токен заÑтарілий. gpg_token_required=Вам потрібно надати Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð´Ð»Ñ Ð½Ð¸Ð¶Ñ‡ÐµÐ²ÐºÐ°Ð·Ð°Ð½Ð¾Ð³Ð¾ токена gpg_token=Токен gpg_token_help=Ви можете Ñтворити Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð·Ð° допомогою: gpg_token_signature=ТекÑтовий (armored) Ð¿Ñ–Ð´Ð¿Ð¸Ñ GPG key_signature_gpg_placeholder=`ПочинаєтьÑÑ Ð· "-----BEGIN PGP SIGNATURE-----"` +verify_gpg_key_success=Ключ GPG '%s' перевірено. ssh_key_verified=Перевірений ключ -ssh_key_verify=Підтвердити +ssh_key_verified_long=Ключ було перевірено за допомогою токена. Його можна викориÑтовувати Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ комітів, що відповідають будь-Ñким активованим адреÑам електронної пошти цього кориÑтувача. +ssh_key_verify=Перевірити +ssh_invalid_token_signature=Ключ SSH, Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ñ– токен не збігаютьÑÑ Ð°Ð±Ð¾ токен заÑтарілий. ssh_token_required=Вам потрібно надати Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð´Ð»Ñ Ð½Ð¸Ð¶Ñ‡ÐµÐ²ÐºÐ°Ð·Ð°Ð½Ð¾Ð³Ð¾ токена ssh_token=Токен ssh_token_help=Ви можете Ñтворити Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð·Ð° допомогою: +ssh_token_signature=ТекÑтовий Ð¿Ñ–Ð´Ð¿Ð¸Ñ SSH +key_signature_ssh_placeholder=`ПочинаєтьÑÑ Ð· "-----BEGIN SSH SIGNATURE-----"` +verify_ssh_key_success=Ключ SSH '%s' перевірено. subkeys=Підключі -key_id=ID ключа -key_name=Ім'Ñ ÐºÐ»ÑŽÑ‡Ð° +key_id=Ідентифікатор ключа +key_name=Ðазва ключа key_content=ЗміÑÑ‚ principal_content=ЗміÑÑ‚ +add_key_success=Ключ SSH '%s' додано. +add_gpg_key_success=Ключ GPG '%s' додано. +add_principal_success=Було додано SSH Ñертифікат ідентичноÑті '%s'. delete_key=Видалити -ssh_key_deletion=Видалити SSH ключ -gpg_key_deletion=Видалити GPG ключ -ssh_principal_deletion=Видалити SSH Ñертифікат кориÑтувача +ssh_key_deletion=Видалити ключ SSH +gpg_key_deletion=Видалити ключ GPG +ssh_principal_deletion=Видалити ідентичніÑть Ñертифікату SSH ssh_key_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ»ÑŽÑ‡Ð° SSH ÑкаÑовує доÑтуп до вашого облікового запиÑу. Продовжити? -gpg_key_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ GPG ключа ÑкаÑовує перевірку підпиÑаних ним комітів. Продовжити? -ssh_principal_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ»ÑŽÑ‡Ð° SSH ÑкаÑовує доÑтуп до вашого облікового запиÑу. Продовжити? -ssh_key_deletion_success=SSH ключ був видалений. -gpg_key_deletion_success=GPG було видалено. -ssh_principal_deletion_success=КориÑтувача видалено. +gpg_key_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ»ÑŽÑ‡Ð° GPG ÑкаÑовує перевірку підпиÑаних ним комітів. Продовжити? +ssh_principal_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ–Ð´ÐµÐ½Ñ‚Ð¸Ñ‡Ð½Ð¾Ñті Ñертифіката SSH ÑкаÑовує доÑтуп до вашого облікового запиÑу. Продовжити? +ssh_key_deletion_success=Ключ SSH видалено. +gpg_key_deletion_success=Ключ GPG видалено. +ssh_principal_deletion_success=ІдентичніÑть видалено. +added_on=Додано %s +valid_until_date=ДійÑно до %s valid_forever=ДійÑний завжди -last_used=ОÑтаннє викориÑÑ‚Ð°Ð½Ð½Ñ -no_activity=Жодної діÑльноÑті -can_read_info=Читати -can_write_info=ÐапиÑати -key_state_desc=Цей ключ викориÑтовувавÑÑ Ð² оÑтанні 7 днів -token_state_desc=Цей токен викориÑтовувавÑÑ Ð² оÑтанні 7 днів -principal_state_desc=УчаÑтник був на Ñайті в оÑтанні 7 днів +last_used=ВоÑтаннє викориÑтано +no_activity=ÐÐµÑ‰Ð¾Ð´Ð°Ð²Ð½Ñ Ð°ÐºÑ‚Ð¸Ð²Ð½Ñ–Ñть відÑÑƒÑ‚Ð½Ñ +can_read_info=Ð§Ð¸Ñ‚Ð°Ð½Ð½Ñ +can_write_info=Ð—Ð°Ð¿Ð¸Ñ +key_state_desc=Цей ключ викориÑтовувавÑÑ Ð¿Ñ€Ð¾Ñ‚Ñгом оÑтанніх 7 днів +token_state_desc=Цей токен викориÑтовувавÑÑ Ð¿Ñ€Ð¾Ñ‚Ñгом оÑтанніх 7 днів +principal_state_desc=Ð¦Ñ Ñ–Ð´ÐµÐ½Ñ‚Ð¸Ñ‡Ð½Ñ–Ñть викориÑтовувалаÑÑ Ð¿Ñ€Ð¾Ñ‚Ñгом оÑтанніх 7 днів show_openid=Показати у профілю -hide_openid=Ðе показувати у профілі +hide_openid=Приховати з профілю ssh_disabled=SSH вимкнено -ssh_externally_managed=Цей ключ SSH має зовнішнє ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ кориÑтувача -manage_social=Керувати зв'Ñзаними обліковими запиÑами Ñоціальних мереж +ssh_signonly=SSH наразі вимкнено, тому ці ключі викориÑтовуютьÑÑ Ð»Ð¸ÑˆÐµ Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ підпиÑу комітів. +ssh_externally_managed=Цей ключ SSH керуєтьÑÑ Ð·Ð·Ð¾Ð²Ð½Ñ– Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ кориÑтувача +manage_social=Керувати пов'Ñзаними обліковими запиÑами Ñоціальних мереж +social_desc=Ці облікові запиÑи Ñоціальних мереж можна викориÑтовувати Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ в ваш обліковий запиÑ. ПереконайтеÑÑ, що вÑÑ– вони належать вам. unbind=Від'єднати +unbind_success=Соціальний обліковий Ð·Ð°Ð¿Ð¸Ñ ÑƒÑпішно видалено. -manage_access_token=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñ‚Ð¾ÐºÐµÐ½Ð°Ð¼Ð¸ доÑтупу -generate_new_token=Згенерувати новий токен -tokens_desc=Ці токени надають доÑтуп до вашого облікового запиÑу за допомогою Gitea API. -token_name=Ім'Ñ Ñ‚Ð¾ÐºÐµÐ½Ñƒ +manage_access_token=Керувати токенами доÑтупу +generate_new_token=Створити новий токен +tokens_desc=Ці токени надають доÑтуп до вашого облікового запиÑу за допомогою API Gitea. +token_name=Ðазва токену generate_token=Згенерувати токен -generate_token_success=Ваш новий токен був Ñтворений. Скопіюйте його зараз, оÑкільки він не буде показаний знову. +generate_token_success=Ваш новий токен Ñтворено. Скопіюйте його зараз, оÑкільки він не буде показаний знову. generate_token_name_duplicate=Ðазва програми <strong>%s</strong> вже викориÑтовуєтьÑÑ. Будь лаÑка, викориÑтайте нову. delete_token=Видалити access_token_deletion=Видалити токен доÑтупу access_token_deletion_cancel_action=Відмінити access_token_deletion_confirm_action=Видалити -delete_token_success=Токен був знищений. Програми, що викориÑтовують його, більше не мають доÑтупу до вашого облікового запиÑу. -permission_read=Прочитані - -manage_oauth2_applications=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð°Ð¼Ð¸ OAuth2 -edit_oauth2_application=Редагувати програму OAuth2 -oauth2_applications_desc=Програми OAuth2 дають можливіÑть вашим Ñтороннім програмам надійно аутентифікувати кориÑтувачів у цьому екземплÑрі Gitea. -remove_oauth2_application=Видалити програму OAuth2 -remove_oauth2_application_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð¸ OAuth2 ÑкаÑовує доÑтуп до вÑÑ–Ñ… підпиÑаних маркерів доÑтупу. Продовжити? -remove_oauth2_application_success=Програму видалено. -create_oauth2_application=Створити нову програму OAuth2 -create_oauth2_application_button=Створити програму -oauth2_application_name=Ðазва програми +access_token_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ‚Ð¾ÐºÐµÐ½Ð° призведе до Ð²Ñ–Ð´ÐºÐ»Ð¸ÐºÐ°Ð½Ð½Ñ Ð´Ð¾Ñтупу до вашого облікового запиÑу Ð´Ð»Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÑ–Ð², Ñкі його викориÑтовують. Це неможливо ÑкаÑувати. Продовжити? +delete_token_success=Токен знищено. Додатки, що викориÑтовують його, більше не мають доÑтупу до вашого облікового запиÑу. +repo_and_org_access=ДоÑтуп до Ñховища та організації +permissions_public_only=Лише загальнодоÑтупні +permissions_access_all=Ð’ÑÑ– (загальнодоÑтупні, приватні та з обмеженим доÑтупом) +permission_not_set=Ðе вÑтановлено +permission_no_access=Ðемає доÑтупу +permission_read=Ð§Ð¸Ñ‚Ð°Ð½Ð½Ñ +permission_write=Ð§Ð¸Ñ‚Ð°Ð½Ð½Ñ Ñ– Ð·Ð°Ð¿Ð¸Ñ +permission_anonymous_read=Ðнонімне Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ +permission_everyone_read=Ð§Ð¸Ñ‚Ð°Ð½Ð½Ñ Ð´Ð»Ñ Ð²ÑÑ–Ñ… +permission_everyone_write=Ð—Ð°Ð¿Ð¸Ñ Ð´Ð»Ñ Ð²ÑÑ–Ñ… +access_token_desc=Обрані дозволи токена обмежують авторизацію лише відповідними маршрутами <a %s>API</a>. Читайте <a %s>документацію</a> Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ¾Ð²Ð¾Ñ— інформації. +at_least_one_permission=Ðеобхідно вибрати хоча б одне право доÑтупу Ð´Ð»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ñ‚Ð¾ÐºÐµÐ½Ð° +permissions_list=Дозволи: + +manage_oauth2_applications=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ°Ð¼Ð¸ OAuth2 +edit_oauth2_application=Редагувати додаток OAuth2 +oauth2_applications_desc=Додатки OAuth2 дозволÑють вашому Ñторонньому додатку безпечно автентифікувати кориÑтувачів у цьому екземплÑрі Gitea. +remove_oauth2_application=Видалити додаток OAuth2 +remove_oauth2_application_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ° OAuth2 ÑкаÑує доÑтуп до вÑÑ–Ñ… підпиÑаних токенів доÑтупу. Продовжити? +remove_oauth2_application_success=Додаток видалено. +create_oauth2_application=Створити новий додаток OAuth2 +create_oauth2_application_button=Створити додаток +create_oauth2_application_success=Ви уÑпішно Ñтворили новий додаток OAuth2. +update_oauth2_application_success=Ви уÑпішно оновили додаток OAuth2. +oauth2_application_name=Ðазва додатка +oauth2_confidential_client=Конфіденційний клієнт. Виберіть Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼, Ñкі зберігають конфіденційніÑть, наприклад, веб-програм. Ðе обирайте Ð´Ð»Ñ Ð½Ð°Ñ‚Ð¸Ð²Ð½Ð¸Ñ… додатків, зокрема ПК та мобільних додатків. +oauth2_skip_secondary_authorization=ПропуÑтити авторизацію Ð´Ð»Ñ Ð¿ÑƒÐ±Ð»Ñ–Ñ‡Ð½Ð¸Ñ… клієнтів піÑÐ»Ñ Ð½Ð°Ð´Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу один раз. <strong>Може Ñтановити ризик Ð´Ð»Ñ Ð±ÐµÐ·Ð¿ÐµÐºÐ¸.</strong> +oauth2_redirect_uris=URI Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ½Ð°Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ. Будь лаÑка, викориÑтовуйте новий Ñ€Ñдок Ð´Ð»Ñ ÐºÐ¾Ð¶Ð½Ð¾Ð³Ð¾ URI. save_application=Зберегти -oauth2_client_id=ID Клієнта +oauth2_client_id=Ідентифікатор клієнта oauth2_client_secret=Ключ клієнта oauth2_regenerate_secret=Відновити ключ -oauth2_regenerate_secret_hint=Ви втратили Ñвій ключ? +oauth2_regenerate_secret_hint=Втратили ключ? oauth2_application_edit=Редагувати oauth2_application_create_description=Програми OAuth2 надають вашим Ñтороннім програмам доÑтуп до облікових запиÑів кориÑтувачів у цьому екземплÑрі. +oauth2_application_remove_description=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ OAuth2 не дозволить додатку отримати доÑтуп до авторизованих облікових запиÑів кориÑтувачів на цьому Ñервері. Продовжити? +oauth2_application_locked=Gitea попередньо реєÑтрує деÑкі програми OAuth2 під Ñ‡Ð°Ñ Ð·Ð°Ð¿ÑƒÑку, Ñкщо це ввімкнено в конфігурації. Щоб запобігти неÑподіваній поведінці, Ñ—Ñ… не можна ні редагувати, ні видалÑти. Ð”Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ¾Ð²Ð¾Ñ— інформації звернітьÑÑ Ð´Ð¾ документації OAuth2. authorized_oauth2_applications=Ðвторизовані програми OAuth2 +authorized_oauth2_applications_description=Ви надали цим Ñтороннім додаткам доÑтуп до Ñвого облікового запиÑу Gitea. СкаÑуйте доÑтуп Ð´Ð»Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÑ–Ð², Ñкі вам більше не потрібні. revoke_key=Відкликати revoke_oauth2_grant=СкаÑувати доÑтуп -revoke_oauth2_grant_description=СкаÑÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу Ð´Ð»Ñ Ñ†Ñ–Ñ”Ñ— програми третьої Ñторони не дозволить їй отримувати доÑтуп до ваших даних. Ви впевнені? +revoke_oauth2_grant_description=СкаÑÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñтороннього додатка не дозволить йому отримувати доÑтуп до ваших даних. Ви впевнені? +revoke_oauth2_grant_success=ДоÑтуп уÑпішно ÑкаÑовано. -twofa_is_enrolled=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° даний Ñ‡Ð°Ñ <strong>викориÑтовує</strong> двофакторну автентифікацію. -twofa_not_enrolled=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð½Ð°Ñ€Ð°Ð·Ñ– не викориÑтовує двофакторну автентифікаціїю. +twofa_desc=Щоб захиÑтити Ñвій обліковий Ð·Ð°Ð¿Ð¸Ñ Ð²Ñ–Ð´ крадіжки паролÑ, ви можете викориÑтовувати Ñмартфон або інший приÑтрій Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¾Ð´Ð½Ð¾Ñ€Ð°Ð·Ð¾Ð²Ð¸Ñ… паролів, прив'Ñзаних до чаÑу (TOTP). +twofa_recovery_tip=Якщо ви втратите Ñвій приÑтрій, ви зможете ÑкориÑтатиÑÑ Ð¾Ð´Ð½Ð¾Ñ€Ð°Ð·Ð¾Ð²Ð¸Ð¼ ключем відновленнÑ, щоб відновити доÑтуп до Ñвого облікового запиÑу. +twofa_is_enrolled=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð½Ð°Ñ€Ð°Ð·Ñ– <strong>викориÑтовує</strong> двофакторну автентифікацію. +twofa_not_enrolled=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð½Ð°Ñ€Ð°Ð·Ñ– не викориÑтовує двофакторну автентифікацію. twofa_disable=Вимкнути двофакторну автентифікацію +twofa_scratch_token_regenerate=Регенерувати одноразовий ключ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ +twofa_scratch_token_regenerated=Ваш одноразовий ключ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ‚ÐµÐ¿ÐµÑ€ %s. Зберігайте його у безпечному міÑці, оÑкільки його більше не буде показано. twofa_enroll=Увімкнути двофакторну автентифікацію -twofa_disable_note=При необхідноÑті можна відключити двофакторну автентифікацію. +twofa_disable_note=За потреби ви можете вимкнути двофакторну автентифікацію. twofa_disable_desc=Ð’Ð¸Ð¼ÐºÐ½ÐµÐ½Ð½Ñ Ð´Ð²Ð¾Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð¾Ñ— автентифікації зробить ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¼ÐµÐ½Ñˆ безпечним. Продовжити? -twofa_disabled=Двофакторна Ð°Ð²Ñ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ Ð²Ð¸Ð¼ÐºÐ½ÐµÐ½Ð°. -scan_this_image=ПроÑкануйте це Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð²Ð°ÑˆÐ¸Ð¼ додатком Ð´Ð»Ñ Ð´Ð²ÑƒÑ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð¾Ñ— автентифікації: -or_enter_secret=Ðбо введіть Ñекрет: %s +regenerate_scratch_token_desc=Якщо ви втратили ключ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð°Ð±Ð¾ вже викориÑтовували його Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ, ви можете Ñкинути його тут. +twofa_disabled=Двофакторну автентифікацію вимкнено. +scan_this_image=ВідÑкануйте це Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð²Ð°ÑˆÐ¸Ð¼ додатком Ð´Ð»Ñ Ð´Ð²Ð¾Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð¾Ñ— автентифікації: +or_enter_secret=Ðбо введіть код: %s then_enter_passcode=І введіть пароль, Ñкий відображаєтьÑÑ Ð² додатку: passcode_invalid=Ðекоректний пароль. Спробуй ще раз. -twofa_failed_get_secret=Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ Ñекрет. +twofa_failed_get_secret=Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ код. +webauthn_register_key=Додати ключ безпеки +webauthn_nickname=ПÑевдонім +webauthn_delete_key=Видалити ключ безпеки +webauthn_delete_key_desc=Якщо ви видалите ключ безпеки, ви більше не зможете ввійти за його допомогою. Продовжити? +webauthn_key_loss_warning=Якщо ви втратите ключі безпеки, ви втратите доÑтуп до Ñвого облікового запиÑу. +webauthn_alternative_tip=Ви можете налаштувати додатковий метод автентифікації. -manage_account_links=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¸Ð¼Ð¸ запиÑами -manage_account_links_desc=Ці зовнішні акаунти прив'Ñзані до вашого аккаунту Gitea. +manage_account_links=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¸Ð²'Ñзаними обліковими запиÑами +manage_account_links_desc=Ці зовнішні облікові запиÑи прив'Ñзані до вашого облікового запиÑу Gitea. account_links_not_available=Ðаразі немає зовнішніх облікових запиÑів, пов'Ñзаних із вашим обліковим запиÑом Gitea. link_account=Прив'Ñзати обліковий Ð·Ð°Ð¿Ð¸Ñ -remove_account_link=Видалити облікові запиÑи +remove_account_link=Видалити обліковий Ð·Ð°Ð¿Ð¸Ñ remove_account_link_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð¿Ð¾Ð²'Ñзаного облікового запиÑу відкликає його доÑтуп до вашого облікового запиÑу Gitea. Продовжити? -remove_account_link_success=Зв'Ñзаний обліковий Ð·Ð°Ð¿Ð¸Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð¾. +remove_account_link_success=Прив'Ñзаний обліковий Ð·Ð°Ð¿Ð¸Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð¾. +hooks.desc=Додайте веб-хуки, Ñкі запуÑкатимутьÑÑ Ð´Ð»Ñ <strong>уÑÑ–Ñ… репозиторіїв</strong>, Ñкими ви володієте. -orgs_none=Ви не Ñ” учаÑником будь-Ñкої організації. +orgs_none=Ви не Ñ” членом організації. +repos_none=У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” Ñховищ. -delete_account=Видалити ваш обліковий Ð·Ð°Ð¿Ð¸Ñ -delete_prompt=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð¾Ñтаточно видалить обліковий Ð·Ð°Ð¿Ð¸Ñ ÐºÐ¾Ñ€Ð¸Ñтувача. Це <strong>ÐЕ МОЖЛИВО</strong> відмінити. -delete_with_all_comments=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¼Ð¾Ð»Ð¾Ð´ÑˆÐ¸Ð¹ за %s днів. Щоб уникнути коментарів-привидів, вÑÑ– запити/PR коментрарі будуть видалені з ним. -confirm_delete_account=ÐŸÑ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ -delete_account_title=Видалити цей обліковий Ð·Ð°Ð¿Ð¸Ñ +delete_account=Видалити обліковий Ð·Ð°Ð¿Ð¸Ñ +delete_prompt=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð¾Ñтаточно видалить ваш обліковий запиÑ. Її <strong>ÐЕ МОЖЛИВО</strong> ÑкаÑувати. +delete_with_all_comments=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¼Ð¾Ð»Ð¾Ð´ÑˆÐ¸Ð¹ за %s днів. Щоб уникнути коментарів-привидів, уÑÑ– ваші коментарі будуть видалені разом з ним. +confirm_delete_account=Підтвердити Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ +delete_account_title=Видалити обліковий Ð·Ð°Ð¿Ð¸Ñ delete_account_desc=Ви впевнені, що хочете оÑтаточно видалити цей обліковий запиÑ? -email_notifications.enable=Увімкнути ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ email -email_notifications.onmention=ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ email тільки коли згадують -email_notifications.disable=Вимкнути email ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ -email_notifications.submit=Ðалаштувати параметри email +email_notifications.enable=Увімкнути ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾ÑŽ поштою +email_notifications.onmention=ПовідомлÑти електронною поштою коли згадують +email_notifications.disable=Вимкнути ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾ÑŽ поштою +email_notifications.submit=Ðалаштувати параметри електронної пошти +email_notifications.andyourown=І ваші влаÑні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ visibility=ВидиміÑть кориÑтувача visibility.public=Публічний visibility.limited=Обмежений +visibility.limited_tooltip=ДоÑтупно лише Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·Ð¾Ð²Ð°Ð½Ð¸Ñ… кориÑтувачів visibility.private=Приватний +visibility.private_tooltip=ДоÑтупно лише Ð´Ð»Ñ Ñ‡Ð»ÐµÐ½Ñ–Ð² організацій, до Ñких ви долучилиÑÑ [repo] +new_repo_helper=Сховище міÑтить уÑÑ– файли проєкту, включно з Ñ–Ñторією ревізій. Ви вже розміщуєте його деінде? <a href="%s">ПеренеÑти Ñховище.</a> owner=ВлаÑник -owner_helper=ДеÑкі організації можуть не відображатиÑÑ Ñƒ випадаючому ÑпиÑку через макÑимальну кількіÑть репозиторііїв. -repo_name=Ðазва репозиторію -repo_size=Розмір репозиторію +owner_helper=ДеÑкі організації можуть не відображатиÑÑ Ñƒ ÑпиÑку через Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð½Ð° макÑимальну кількіÑть Ñховищ. +repo_name=Ðазва Ñховища +repo_name_profile_public_hint=.profile - це Ñпеціальне Ñховище, за допомогою Ñкого ви можете додати README.md до профілю вашої публічної організації, Ñкий буде видимим Ð´Ð»Ñ Ð²ÑÑ–Ñ…. ПереконайтеÑÑ, що він Ñ” публічним, та ініціалізуйте його за допомогою README у каталозі профілю. +repo_name_helper=У хороших назвах Ñховищ викориÑтовуютьÑÑ ÐºÐ¾Ñ€Ð¾Ñ‚ÐºÑ– ключові Ñлова, Ñкі легко запам'ÑтовуютьÑÑ Ñ‚Ð° Ñ” унікальними. Сховище з назвою «.profile» або «.profile-private» можна викориÑтовувати Ð´Ð»Ñ Ð´Ð¾Ð´Ð°Ð²Ð°Ð½Ð½Ñ README.md Ð´Ð»Ñ Ð¿Ñ€Ð¾Ñ„Ñ–Ð»ÑŽ кориÑтувача/організації. +repo_size=Розмір Ñховища template=Шаблон -template_select=Оберіть шаблон. -template_helper=Зробити репозиторій шаблоном -template_description=Шаблонні репозиторії дозволÑють кориÑтувачам генерувати нові репозиторії із такою ж Ñтруктурою директорій, файлами та додатковими налаштуваннÑми. +template_select=Обрати шаблон. +template_helper=Зробити Ñховище шаблоном +template_description=Шаблонні Ñховища дозволÑють кориÑтувачам Ñтворювати нові Ñховища з такою ж Ñтруктурою каталогів, файлами та додатковими налаштуваннÑми. visibility=ВидиміÑть -visibility_description=Тільки влаÑник або члени організації Ñкі мають віповідні права, зможуть побачити. +visibility_description=Тільки влаÑник або члени організації, Ñкщо вони мають дозвіл, зможуть його побачити. +visibility_helper=Зробити Ñховище приватним visibility_helper_forced=ÐдмініÑтратор вашого Ñайту налаштував параметри: вÑÑ– нові репозиторії будуть приватними. visibility_fork_helper=(Ці зміни вплинуть на вÑÑ– форки.) clone_helper=Потрібна допомога у клонуванні? Відвідайте Ñторінку <a target="_blank" rel="noopener" href="%s">Допомога</a>. fork_repo=Форкнути репозиторій fork_from=Форк з -fork_visibility_helper=Ðеможливо змінити видиміÑть форкнутого репозиторію. +fork_visibility_helper=Ðеможливо змінити видиміÑть розгалуженого Ñховища. +all_branches=УÑÑ– гілки +view_all_branches=ПереглÑнути вÑÑ– гілки +view_all_tags=ПереглÑнути вÑÑ– мітки use_template=ЗаÑтоÑувати цей шаблон +open_with_editor=Відкрити в %s download_zip=Завантажити ZIP download_tar=Завантажити TAR.GZ download_bundle=Завантажити BUNDLE -generate_repo=Згенерувати репозиторій -generate_from=Генерувати з +generate_repo=Створити Ñховище +generate_from=Створити з repo_desc=ÐžÐ¿Ð¸Ñ -repo_desc_helper=Введіть короткий Ð¾Ð¿Ð¸Ñ (опціонально) -repo_gitignore_helper=Виберіть шаблон .gitignore. -repo_gitignore_helper_desc=Оберіть з ÑпиÑку мовних шаблонів файли, Ñкі не будуть відÑтежуватиÑÑŒ. Типові артефакти, Ñкі генеруютьÑÑ Ð·Ð° допомогою інÑтрументів побудови кожної мови, за замовчуваннÑм включені до .gitignor. +repo_desc_helper=Введіть короткий Ð¾Ð¿Ð¸Ñ (необов'Ñзково) +repo_no_desc=Ðемає опиÑу +repo_lang=Мови +repo_gitignore_helper=Обрати шаблон .gitignore. +repo_gitignore_helper_desc=Оберіть з ÑпиÑку мовних шаблонів файли, Ñкі не Ñлід відÑтежувати. Типові артефакти, що генеруютьÑÑ Ñ–Ð½Ñтрументами збірки кожної мови, за замовчуваннÑм включені до .gitignor. issue_labels=Мітки задачі -issue_labels_helper=Вибрати мітку Ð´Ð»Ñ Ð·Ð°Ð´Ð°Ñ‡Ñ–. +issue_labels_helper=Виберіть набір міток задачі. license=Ð›Ñ–Ñ†ÐµÐ½Ð·Ñ–Ñ -license_helper=Виберіть ліцензійний файл. -license_helper_desc=Ð›Ñ–Ñ†ÐµÐ½Ð·Ñ–Ñ Ñ€ÐµÐ³ÑƒÐ»ÑŽÑ” те, що інші можуть Ñ– не можуть робити з вашим кодом. Ðе впевнені, що Ñаме підходить Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ проєкту? ДивітьÑÑ <a target="_blank" rel="noopener noreferrer" href="%s">Виберіть ліцензію.</a> +license_helper=Обрати файл ліцензії. +license_helper_desc=Ð›Ñ–Ñ†ÐµÐ½Ð·Ñ–Ñ Ð²Ð¸Ð·Ð½Ð°Ñ‡Ð°Ñ”, що інші можуть робити з вашим кодом, а що ні. Ðе впевнені, Ñка підходить Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ проєкту? ДивітьÑÑ <a target="_blank" rel="noopener noreferrer" href="%s">Вибір ліцензії.</a> +multiple_licenses=Кілька ліцензій +object_format=Формат об'єкту +object_format_helper=Формат об'єкту Ñховища. Ðеможливо буде змінити пізніше. SHA1 Ñ” найбільш ÑуміÑним. readme=README readme_helper=Виберіть шаблон README. -readme_helper_desc=Це міÑце, де ви можете напиÑати повний Ð¾Ð¿Ð¸Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ проєкту. -auto_init=Ініціалізувати репозиторій (Додає .gitignore, LICENSE та README) +readme_helper_desc=Тут ви можете повніÑтю опиÑати ваш проєкт. +auto_init=Ініціалізувати Ñховище (додає файли .gitignore, ліцензію та README) trust_model_helper=Виберіть модель довіри Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñу. Можливі варіанти: -trust_model_helper_collaborator=Співавтор: підпиÑи довіри від Ñпівавторів -trust_model_helper_committer=УчаÑник: довірені підпиÑи учаÑтників -trust_model_helper_collaborator_committer=Співавтор+Комітер: довірчі підпиÑи від Ñпівавторів, Ñкі відповідають комітеру -trust_model_helper_default=За замовчуваннÑм: викориÑтовувати Ñтандартну модель довіри Ð´Ð»Ñ Ñ†Ñ–Ñ”Ñ— уÑтановки -create_repo=Створити репозиторій +trust_model_helper_collaborator=Співавтор: довірÑти підпиÑам Ñпівавторів +trust_model_helper_committer=Комітер: довірÑти підпиÑам, Ñкі відповідають комітерам +trust_model_helper_collaborator_committer=Співавтор+Комітер: довірÑти підпиÑам Ñпівавторів, Ñкі відповідають комітеру +trust_model_helper_default=Типово: викориÑтовувати Ñтандартну модель довіри Ð´Ð»Ñ Ñ†Ñ–Ñ”Ñ— уÑтановки +create_repo=Створити Ñховище default_branch=Головна гілка -default_branch_helper=Гілка за замовчуваннÑм Ñ” базовою гілкою Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñ–Ð² на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ‚Ð° комітів коду. +default_branch_label=типово +default_branch_helper=Типова гілка Ñ” базовою гілкою Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñ–Ð² на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ‚Ð° комітів. mirror_prune=ОчиÑтити -mirror_prune_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð·Ð°Ñтарілих поÑилань Ñкі ви відÑлідковуєте -mirror_interval_invalid=Інтервал Ð´Ð·ÐµÑ€ÐºÐ°Ð»ÑŽÐ²Ð°Ð½Ð½Ñ Ñ” неприпуÑтимим. -mirror_address=ÐšÐ»Ð¾Ð½ÑƒÐ²Ð°Ð½Ð½Ñ Ð· URL-адреÑи -mirror_address_desc=ПоміÑтіть будь-Ñкі необхідні облікові дані у розділі ÐвторизаціÑ. -mirror_lfs=Склад великих файлів (LFS) -mirror_lfs_desc=Ðктивувати дзеркальне Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… LFS. +mirror_prune_desc=Видалити заÑтарілі поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° віддалені відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ +mirror_interval=Інтервал Ð´Ð·ÐµÑ€ÐºÐ°Ð»ÑŽÐ²Ð°Ð½Ð½Ñ (допуÑтимі одиниці виміру чаÑу 'h', 'm', 's'). 0 - щоб вимкнути періодичну Ñинхронізацію. (Мінімальний інтервал: %s) +mirror_interval_invalid=Інтервал Ð´Ð·ÐµÑ€ÐºÐ°Ð»ÑŽÐ²Ð°Ð½Ð½Ñ Ð½ÐµÐ´Ñ–Ð¹Ñний. +mirror_sync=Ñинхронізовано +mirror_sync_on_commit=Синхронізувати, коли надÑилаютьÑÑ ÐºÐ¾Ð¼Ñ–Ñ‚Ð¸ +mirror_address=Клонувати з URL-адреÑи +mirror_address_desc=Введіть необхідні облікові дані в розділі ÐвторизаціÑ. +mirror_address_url_invalid=URL-адреÑа недійÑна. Ðеобхідно правильно екранувати вÑÑ– компоненти URL-адреÑи. +mirror_address_protocol_invalid=URL-адреÑа недійÑна. ДопуÑтимі лише адреÑи http(s):// або git://. +mirror_lfs=Сховище великих файлів (LFS) +mirror_lfs_desc=Ðктивувати Ð´Ð·ÐµÑ€ÐºÐ°Ð»ÑŽÐ²Ð°Ð½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… LFS. mirror_lfs_endpoint=Кінцева точка LFS -mirror_lfs_endpoint_desc=Ð¡Ð¸Ð½Ñ…Ñ€Ð¾Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ñпробує викориÑтовувати url Ð´Ð»Ñ ÐºÐ»Ð¾Ð½Ñƒ щоб <a target="_blank" rel="noopener noreferrer" href="%s">визначити LFS-Ñервер</a>. Ви також можете вказати кінцеву точку кориÑтувача, Ñкщо дані репозиторію LFS зберігаютьÑÑ Ð² іншому міÑці. +mirror_lfs_endpoint_desc=Ð¡Ð¸Ð½Ñ…Ñ€Ð¾Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ñпробує викориÑтати URL-адреÑу клону Ð´Ð»Ñ <a target="_blank" rel="noopener noreferrer" href="%s">Ð²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ñервера LFS</a>. Ви також можете вказати влаÑну кінцеву точку, Ñкщо дані LFS Ñховища зберігаютьÑÑ Ð² іншому міÑці. mirror_last_synced=ОÑÑ‚Ð°Ð½Ð½Ñ ÑÐ¸Ð½Ñ…Ñ€Ð¾Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ mirror_password_placeholder=(без змін) -mirror_password_blank_placeholder=(відключено) +mirror_password_blank_placeholder=(Ðе вÑтановлено) mirror_password_help=Змініть ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача, щоб видалити збережений пароль. watchers=СпоÑтерігачі -stargazers=Зацікавлені +stargazers=Шанувальники +stars_remove_warning=Це видалить уÑÑ– зірки з цього Ñховища. forks=Форки -reactions_more=додати %d більше -unit_disabled=ÐдмініÑтратор Ñайту вимкнув цей розділ репозиторію. +stars=Зірки +reactions_more=Ñ– ще %d +unit_disabled=ÐдмініÑтратор Ñайту вимкнув цей розділ Ñховища. language_other=Інші -adopt_search=Введіть ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача Ð´Ð»Ñ Ð¿Ð¾ÑˆÑƒÐºÑƒ неприйнÑтних репозиторіїв... (залиште порожнім, щоб знайти вÑÑ–) -adopt_preexisting_label=ПрийнÑті файли -adopt_preexisting=ПрийнÑти вже Ñ–Ñнуючі файли -adopt_preexisting_content=Створити репозиторій з %s -adopt_preexisting_success=ПрийнÑти файли та Ñтворити репозиторій з %s +adopt_search=Введіть ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача Ð´Ð»Ñ Ð¿Ð¾ÑˆÑƒÐºÑƒ неприйнÑтих Ñховищ... (залиште порожнім, щоб знайти вÑÑ–) +adopt_preexisting_label=ПрийнÑти файли +adopt_preexisting=ПрийнÑти попередньо Ñтворені файли +adopt_preexisting_content=Створити Ñховище з %s +adopt_preexisting_success=ПрийнÑти файли та Ñтворити Ñховище з %s delete_preexisting_label=Видалити -delete_preexisting=Видалити Ñ–Ñнуючі файли +delete_preexisting=Видалити попередньо Ñтворені файли delete_preexisting_content=Видалити файли з %s delete_preexisting_success=Видалено неприйнÑті файли в %s blame_prior=ПереглÑнути анотацію, що передує цій зміні +blame.ignore_revs.failed=Ðе вдалоÑÑ Ð¿Ñ€Ð¾Ñ–Ð³Ð½Ð¾Ñ€ÑƒÐ²Ð°Ñ‚Ð¸ ревізії у <a href="%s">.git-blame-ignore-revs</a>. +user_search_tooltip=Показує не більше 30 кориÑтувачів +tree_path_not_found=ШлÑÑ… %[1]s не Ñ–Ñнує в %[2]s -transfer.accept=Дозволити транÑфер -transfer.reject=Відхилити транÑфер +transfer.accept=Дозволити Ð¿ÐµÑ€ÐµÐ¼Ñ–Ñ‰ÐµÐ½Ð½Ñ +transfer.accept_desc=`ПереміÑтити до "%s"` +transfer.reject=Відхилити Ð¿ÐµÑ€ÐµÐ¼Ñ–Ñ‰ÐµÐ½Ð½Ñ +transfer.reject_desc=`СкаÑувати Ð¿ÐµÑ€ÐµÐ¼Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð´Ð¾ "%s"` +transfer.no_permission_to_accept=У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” дозволу приймати цю передачу. +transfer.no_permission_to_reject=У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” дозволу на Ð²Ñ–Ð´Ñ…Ð¸Ð»ÐµÐ½Ð½Ñ Ñ†Ñ–Ñ”Ñ— передачі. desc.private=Приватний desc.public=Публічний +desc.public_access=Публічний доÑтуп desc.template=Шаблон desc.internal=Внутрішній -desc.archived=Ðрхівний +desc.archived=Ðрхівований +desc.sha256=SHA256 template.items=Елементи шаблону template.git_content=ВміÑÑ‚ Git (типова гілка) -template.git_hooks=Перехоплювачі Git -template.webhooks=Webhook'и +template.git_hooks=Хуки Git +template.git_hooks_tooltip=Ðаразі ви не можете змінювати або видалÑти Git-хуки піÑÐ»Ñ Ñ—Ñ… додаваннÑ. Виберіть це лише Ñкщо ви довірÑєте Ñховищу шаблонів. +template.webhooks=Веб-хуки template.topics=Теми template.avatar=Ðватар template.issue_labels=Мітки задачі template.one_item=Слід обрати хоча б один елемент шаблону -template.invalid=Слід обрати шаблонний репозиторій +template.invalid=Слід обрати шаблонне Ñховище -archive.issue.nocomment=Цей репозиторій архівовано. Ви не можете коментувати задачі. -archive.pull.nocomment=Це архівний репозитарій. Ви не можете коментувати пулл-реквеÑти. +archive.title=Це архівне Ñховище. Ви можете переглÑдати й клонувати файли, але не можете завантажувати Ñвої зміни або відкривати задачі чи запити на злиттÑ. +archive.title_date=Це архівне Ñховище на %s. Ви можете переглÑдати файли й клонувати його, але не можете завантажувати Ñвої зміни або відкривати задачі чи запити на злиттÑ. +archive.issue.nocomment=Це Ñховище архівовано. Ви не можете коментувати задачі. +archive.pull.nocomment=Це Ñховище архівовано. Ви не можете коментувати запити на злиттÑ. -form.reach_limit_of_creation_1=Ви вже доÑÑгли ліміту в %d репозиторіїв. -form.reach_limit_of_creation_n=Ви доÑÑгли макÑимальної кількоÑті %d Ñтворених репозиторіїв. +form.reach_limit_of_creation_1=Ви доÑÑгли макÑимальної кількоÑті %d Ñховища. +form.reach_limit_of_creation_n=Ви доÑÑгли макÑимальної кількоÑті %d Ñховищ. +form.name_reserved=Ðазву Ñховища '%s' зарезервовано. +form.name_pattern_not_allowed=Шаблон '%s' не дозволено в назві Ñховища. need_auth=ÐÐ²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ñ–Ñ migrate_options=Параметри міграції migrate_service=Ð¡ÐµÑ€Ð²Ñ–Ñ Ð¼Ñ–Ð³Ñ€Ð°Ñ†Ñ–Ñ— -migrate_options_lfs=ПеренеÑÐµÐ½Ð½Ñ LFS файлів +migrate_options_mirror_helper=Це Ñховище буде дзеркалом +migrate_options_lfs=ПеренеÑÐµÐ½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñ–Ð² LFS migrate_options_lfs_endpoint.label=Кінцева точка LFS -migrate_options_lfs_endpoint.description=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ð±ÑƒÐ´Ðµ намагатиÑÑ Ð²Ð¸ÐºÐ¾Ñ€Ð¸Ñтовувати ваш Git віддалено, щоб <a target="_blank" rel="noopener noreferrer" href="%s">визначати LFS Ñервер</a>. Ви також можете вказати Ñвою кінцеву точку, Ñкщо дані репозиторію LFS зберігаютьÑÑ Ð² іншому міÑці. -migrate_options_lfs_endpoint.description.local=Також підтримуютьÑÑ ÑˆÐ»Ñхи на локальному Ñервері. -migrate_items=Деталі міграції +migrate_options_lfs_endpoint.description=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ñпробує викориÑтати ваш Git віддалено, щоб <a target="_blank" rel="noopener noreferrer" href="%s">визначати Ñервер LFS</a>. Ви також можете вказати влаÑну кінцеву точку, Ñкщо дані Ñховища LFS зберігаютьÑÑ Ð² іншому міÑці. +migrate_options_lfs_endpoint.description.local=Також підтримуєтьÑÑ ÑˆÐ»ÑÑ… до локального Ñервера. +migrate_items=Елементи міграції migrate_items_wiki=Вікі migrate_items_milestones=Етапи migrate_items_labels=Мітки @@ -827,26 +1164,34 @@ migrate_items_merge_requests=Запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ migrate_items_releases=Релізи migrate_repo=ПеренеÑти репозиторій migrate.clone_address=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ / клонувати з URL-адреÑи -migrate.clone_address_desc=URL-адреÑа HTTP(S) або Git "clone" Ñ–Ñнуючого Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ +migrate.clone_address_desc=URL-адреÑа HTTP(S) або Git "clone" Ñ–Ñнуючого Ñховища +migrate.github_token_desc=Ви можете додати один або декілька токенів через кому, щоб пришвидшити міграцію через Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ ÑˆÐ²Ð¸Ð´ÐºÐ¾Ñті API GitHub. ПОПЕРЕДЖЕÐÐЯ: Ð—Ð»Ð¾Ð²Ð¶Ð¸Ð²Ð°Ð½Ð½Ñ Ñ†Ñ–Ñ”ÑŽ функцією може порушити політику поÑтачальника поÑлуг Ñ– призвеÑти до Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑу. migrate.clone_local_path=або шлÑÑ… до локального Ñерверу migrate.permission_denied=Вам не дозволено імпортувати локальні репозиторії. migrate.permission_denied_blocked=Ви не можете імпортувати з заборонених вузлів, будь лаÑка, попроÑіть адмініÑтратора перевірити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS. -migrate.invalid_lfs_endpoint=Помилкова кінцева точка LFS. +migrate.invalid_local_path=Локальний шлÑÑ… недійÑний. Він не Ñ–Ñнує або не Ñ” каталогом. +migrate.invalid_lfs_endpoint=Кінцева точка LFS недійÑна. migrate.failed=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ð½Ðµ вдалаÑÑ: %v migrate.migrate_items_options=Ð”Ð»Ñ Ð¿ÐµÑ€ÐµÐ½ÐµÑÐµÐ½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ¾Ð²Ð¸Ñ… елементів потрібен токен доÑтупу migrated_from=ПеренеÑено з <a href="%[1]s">%[2]s</a> migrated_from_fake=ПеренеÑено з %[1]s -migrate.migrate=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ð· %s +migrate.migrate=Мігрувати з %s migrate.migrating=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· <b>%s</b>... migrate.migrating_failed=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· <b>%s</b> не вдалаÑÑ. +migrate.migrating_failed.error=Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ½ÐµÑти: %s migrate.migrating_failed_no_addr=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ð½Ðµ вдалаÑÑ. -migrate.git.description=ПеренеÑÐµÐ½Ð½Ñ Ð»Ð¸ÑˆÐµ репозиторію з будь-Ñкої Ñлужби Git. +migrate.github.description=ПеренеÑти дані з github.com чи інших Ñерверів Github. +migrate.git.description=ПеренеÑти Ñховище з будь-Ñкого ÑервіÑу Git'у. migrate.gitlab.description=ПеренеÑти дані з gitlab.com та інших екземплÑрів GitLab. migrate.gitea.description=ПеренеÑти дані з gitea.com та інших екземплÑрів Gitea. migrate.gogs.description=ПеренеÑти дані з notabug.org та інших екземплÑрів Gogs. migrate.onedev.description=ПеренеÑти дані з code.onedev.io та інших екземплÑрів OneDev. migrate.codebase.description=ПеренеÑти дані з codebasehq.com. migrate.gitbucket.description=ПеренеÑти дані з екземплÑрів GitBucket. +migrate.codecommit.description=ПеренеÑÐµÐ½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… з AWS CodeCommit. +migrate.codecommit.aws_access_key_id=ID ключа доÑтупу AWS +migrate.codecommit.https_git_credentials_username=Ð†Ð¼â€™Ñ ÐºÐ¾Ñ€Ð¸Ñтувача HTTPS Git +migrate.codecommit.https_git_credentials_password=Пароль кориÑтувача HTTPS Git migrate.migrating_git=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Git даних migrate.migrating_topics=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ñ‚ÐµÐ¼ migrate.migrating_milestones=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ ÐµÑ‚Ð°Ð¿Ñ–Ð² @@ -854,26 +1199,34 @@ migrate.migrating_labels=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ð¼Ñ–Ñ‚Ð¾Ðº migrate.migrating_releases=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ñ€ÐµÐ»Ñ–Ð·Ñ–Ð² migrate.migrating_issues=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ð·Ð°Ð´Ð°Ñ‡ migrate.migrating_pulls=ÐœÑ–Ð³Ñ€Ð°Ñ†Ñ–Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñ–Ð² на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +migrate.cancel_migrating_title=СкаÑувати міграцію +migrate.cancel_migrating_confirm=Ви хочете ÑкаÑувати міграцію? +migration_status=CÑ‚Ð°Ñ‚ÑƒÑ Ð¼Ñ–Ð³Ñ€Ð°Ñ†Ñ–Ñ— mirror_from=дзеркало forked_from=форк від generated_from=згенеровано з -fork_from_self=Ви не можете форкнути репозиторій, так Ñк ви його влаÑник. +fork_from_self=Ви не можете форкнути влаÑне Ñховище. fork_guest_user=Увійдіть, щоб зробити форк репозитарію. watch_guest_user=Увійдіть, щоб Ñлідкувати за цим репозиторієм. star_guest_user=Увійдіть, щоб додати в обране цей репозиторій. unwatch=Ðе Ñтежити -watch=Слідкувати +watch=Стежити unstar=Видалити із обраних star=Ð’ обрані fork=Форк +action.blocked_user=Ðеможливо виконати дію, оÑкільки ви заблоковані влаÑником Ñховища. download_archive=Скачати репозиторій +more_operations=Інші операції quick_guide=Короткий поÑібник clone_this_repo=Кнонувати цей репозиторій +cite_this_repo=ПоÑлатиÑÑ Ð½Ð° це Ñховище create_new_repo_command=Створити новий репозиторій з командного Ñ€Ñдка push_exist_repo=Опублікувати Ñ–Ñнуючий репозиторій з командного Ñ€Ñдка -empty_message=Цей репозиторій порожній. +empty_message=Це Ñховище порожнє. +broken_message=Ðеможливо прочитати дані Git, що лежать в оÑнові цього Ñховища. ЗвернітьÑÑ Ð´Ð¾ адмініÑтратора Ñервера або видаліть Ñховище. +no_branch=Це Ñховище не має гілок. code=Код code.desc=ДоÑтуп до коду, файлів, комітів та гілок. @@ -887,96 +1240,153 @@ tags=Теги issues=Задачі pulls=Запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ projects=Проєкти +packages=Пакети +actions=Дії labels=Мітки org_labels_desc=Мітки Ñ€Ñ–Ð²Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ— можуть викориÑтовуватиÑÑ <strong>в уÑÑ–Ñ… репозиторіÑÑ…</strong> цієї організації org_labels_desc_manage=керувати milestone=Етап -milestones=Етап +milestones=Етапи commits=Коміти commit=Коміт release=Реліз releases=Релізи tag=Тег -released_this=випущені релізи +released_this=випуÑтив(-ла) +file.title=%s в %s file_raw=Ðеформатований file_history=ІÑÑ‚Ð¾Ñ€Ñ–Ñ file_view_source=ПереглÑнути вихідний код -file_view_rendered=ПереглÑнути відрендерено file_view_raw=ПереглÑд Raw file_permalink=ПоÑтійне поÑÐ¸Ð»Ð°Ð½Ð½Ñ -file_too_large=Цей файл завеликий щоб бути показаним. - +file_too_large=Файл занадто великий Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ. +file_is_empty=Файл порожній. +code_preview_line_from_to=Ð Ñдки від %[1]d до %[2]d в %[3]s +code_preview_line_in=Ð Ñдок %[1]d в %[2]s +invisible_runes_header=`Цей файл міÑтить невидимі Ñимволи Юнікоду` +invisible_runes_description=`Цей файл міÑтить невидимі Ñимволи Юнікоду, Ñкі не розрізнÑютьÑÑ Ð»ÑŽÐ´Ð¸Ð½Ð¾ÑŽ, але можуть по-різному оброблÑтиÑÑ ÐºÐ¾Ð¼Ð¿'ютером. Якщо ви вважаєте, що це зроблено навмиÑно, можете Ñміливо ігнорувати це попередженнÑ. Щоб показати Ñ—Ñ…, ÑкориÑтайтеÑÑ ÐºÐ½Ð¾Ð¿ÐºÐ¾ÑŽ Escape.` +ambiguous_runes_header=`Цей файл міÑтить неоднозначні Ñимволи Юнікоду` +ambiguous_runes_description=`Цей файл міÑтить Ñимволи Юнікоду, Ñкі можна Ñплутати з іншими Ñимволами. Якщо ви вважаєте, що це зроблено навмиÑно, можете Ñміливо ігнорувати це попередженнÑ. Щоб показати Ñ—Ñ…, ÑкориÑтайтеÑÑ ÐºÐ½Ð¾Ð¿ÐºÐ¾ÑŽ Escape.` +invisible_runes_line=`Цей Ñ€Ñдок міÑтить невидимі Ñимволи Юнікоду` +ambiguous_runes_line=`Цей Ñ€Ñдок міÑтить неоднозначні Ñимволи Юнікоду` +ambiguous_character=`%[1]c [U+%04[1]X] можна Ñплутати з %[2]c [U+%04[2]X]` + +escape_control_characters=Екранувати +unescape_control_characters=Відмінити ÐµÐºÑ€Ð°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ file_copy_permalink=Копіювати поÑтійне поÑÐ¸Ð»Ð°Ð½Ð½Ñ video_not_supported_in_browser=Ваш браузер не підтримує тег 'video' HTML5. audio_not_supported_in_browser=Ваш браузер не підтримує тег HTML5 'audio'. symbolic_link=Символічне поÑÐ¸Ð»Ð°Ð½Ð½Ñ +executable_file=Виконуваний файл +generated=Створено commit_graph=Графік комітів commit_graph.select=Виберіть гілки commit_graph.hide_pr_refs=Приховати запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -commit_graph.monochrome=Монохром +commit_graph.monochrome=Монохромний commit_graph.color=Колір -blame=Ð—Ð²Ð¸Ð½ÑƒÐ²Ð°Ñ‡ÐµÐ½Ð½Ñ +commit.contained_in=Цей коміт міÑтитьÑÑ Ð²: +commit.contained_in_default_branch=Цей коміт Ñ” чаÑтиною типової гілки +commit.load_referencing_branches_and_tags=Завантажити гілки та мітки, Ñкі поÑилаютьÑÑ Ð½Ð° цей коміт +blame=ÐÐ½Ð¾Ñ‚Ð°Ñ†Ñ–Ñ download_file=Завантажити файл normal_view=Звичайний виглÑд line=Ñ€Ñдок lines=Ñ€Ñдки +from_comment=(коментар) +editor.add_file=Додати файл editor.new_file=Ðовий файл editor.upload_file=Завантажити файл -editor.edit_file=Ð ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñƒ +editor.edit_file=Редагувати файл editor.preview_changes=Попередній переглÑд змін editor.cannot_edit_lfs_files=Файли LFS не можна редагувати в веб-інтерфейÑÑ–. +editor.cannot_edit_too_large_file=Файл занадто великий Ð´Ð»Ñ Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ. editor.cannot_edit_non_text_files=Бінарні файли не можливо редагувати у веб-інтерфейÑÑ–. +editor.file_not_editable_hint=Ðле ви вÑе ще можете перейменувати або переміÑтити його. editor.edit_this_file=Редагувати файл editor.this_file_locked=Файл заблоковано -editor.must_be_on_a_branch=Ви повинні бути у гілці щоб зробити, або запропонувати зміни до цього файлу. -editor.fork_before_edit=Ðеобхідно зробити форк цього репозиторій, щоб внеÑти або запропонувати зміни в цей файл. +editor.must_be_on_a_branch=Ви повинні бути у гілці щоб робити або пропонувати зміни до цього файлу. +editor.fork_before_edit=Ðеобхідно зробити форк цього Ñховища, щоб внеÑти або запропонувати зміни в цей файл. editor.delete_this_file=Видалити файл editor.must_have_write_access=Ви повинні мати доÑтуп на Ð·Ð°Ð¿Ð¸Ñ Ñ‰Ð¾Ð± запропонувати зміни до цього файлу. -editor.name_your_file=Дайте назву файлу… -editor.filename_help=Щоб додати каталог, наберіть його назву, а потім - коÑу риÑку ('/'). Щоб видалити каталог, перейдіть до початку Ð¿Ð¾Ð»Ñ Ñ– натиÑніть backspace. +editor.file_delete_success=Файл "%s" видалено. +editor.name_your_file=Ðазвіть файл… +editor.filename_help=Щоб додати каталог, наберіть його назву, а потім - прÑмий Ñлеш ('/'). Щоб видалити каталог, перейдіть до початку Ð¿Ð¾Ð»Ñ Ñ– натиÑніть видалити ліворуч. editor.or=або editor.cancel_lower=СкаÑувати editor.commit_signed_changes=ВнеÑти підпиÑані зміни -editor.commit_changes=Закомітити зміни +editor.commit_changes=ЗафікÑувати зміни +editor.add_tmpl=Додати '{filename}' +editor.add=Додати %s +editor.update=Оновити %s +editor.delete=Видалити %s +editor.patch=ЗаÑтоÑувати патч +editor.patching=ЗаÑтоÑÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¸Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½ÑŒ: +editor.new_patch=Ðовий патч editor.commit_message_desc=Додати необов'Ñзковий розширений опиÑ… -editor.signoff_desc=Додатиь Signed-off-by комітом в конці Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¶ÑƒÑ€Ð½Ð°Ð»Ñƒ комітів. -editor.commit_directly_to_this_branch=Зробіть коміт прÑмо в гілку <strong class="branch-name">%s</strong>. +editor.signoff_desc=Додати «ПідпиÑано комітером» в кінці Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ. +editor.commit_directly_to_this_branch=Зробити коміт безпоÑередньо в гілку <strong class="branch-name">%s</strong>. editor.create_new_branch=Створити <strong>нову гілку</strong> Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ коміту та відкрити запит на злиттÑ. editor.create_new_branch_np=Створити <strong>нову гілку</strong> Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ коміту. editor.propose_file_change=Запропонувати зміну файлу -editor.new_branch_name_desc=Ім'Ñ Ð½Ð¾Ð²Ð¾Ñ— гілки… +editor.new_branch_name=Ðазвіть нову гілку Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ коміту +editor.new_branch_name_desc=Ðазва нової гілки… editor.cancel=Відмінити -editor.filename_cannot_be_empty=Ім'Ñ Ñ„Ð°Ð¹Ð»Ñƒ не може бути порожнім. +editor.filename_cannot_be_empty=Ðазва файлу не може бути порожньою. +editor.filename_is_invalid=Ðазва файлу недійÑна: "%s". +editor.commit_email=Електронна пошта коміту +editor.invalid_commit_email=ÐдреÑа електронної пошти Ð´Ð»Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ недійÑна. +editor.file_is_a_symlink=`"%s" - це Ñимволічне поÑиланнÑ. Символічні поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ðµ можна редагувати у веб-редакторі` +editor.filename_is_a_directory=Ðазва файлу '%s' вже викориÑтовуєтьÑÑ Ñк назва каталогу у цьому Ñховищі. +editor.file_modifying_no_longer_exists=Редагований файл '%s' більше не Ñ–Ñнує в цьому Ñховищі. editor.file_changed_while_editing=ЗміÑÑ‚ файлу змінивÑÑ Ð· моменту початку редагуваннÑ. <a target="_blank" rel="noopener" href="%s"> ÐатиÑніть тут </a>, щоб переглÑнути що було змінено, або <strong>закомітьте зміни ще раз</strong>, щоб перепиÑати Ñ—Ñ…. editor.commit_empty_file_header=Закомітити порожній файл -editor.commit_empty_file_text=Файл, в комміті порожній. Продовжити? -editor.no_changes_to_show=Ðема змін Ð´Ð»Ñ Ð¿Ð¾ÐºÐ°Ð·Ñƒ. -editor.fail_to_update_file_summary=Помилка: +editor.commit_empty_file_text=Файл, Ñкий ви збираєтеÑÑ Ð·Ð°ÐºÐ¾Ð¼Ñ–Ñ‚Ð¸Ñ‚Ð¸, порожній. Продовжувати? +editor.no_changes_to_show=Ðемає змін. +editor.push_rejected_no_message=Зміну відхилено Ñервером без повідомленнÑ. Будь лаÑка, перевірте Git-хуки. +editor.push_rejected=Зміну відхилено Ñервером. Будь лаÑка, перевірте Git-хуки. editor.push_rejected_summary=Повне Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ відмову: editor.add_subdir=Додати каталог… +editor.unable_to_upload_files=Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ файли до '"%s". Помилка: %v +editor.upload_file_is_locked=Файл "%s" заблоковано %s. +editor.upload_files_to_dir=`Завантажити файли до "%s"` editor.no_commit_to_branch=Ðе вдалоÑÑ Ð²Ð½ÐµÑти коміт безпоÑередньо до гілки, тому що: -editor.user_no_push_to_branch=КориÑтувач не може здійÑнити пуш до гілки editor.require_signed_commit=Гілка вимагає підпиÑаного коміту +editor.revert=Повернути %s до: +editor.failed_to_commit=Ðе вдалоÑÑ Ð·Ð°Ñ„Ñ–ÐºÑувати зміни. +editor.failed_to_commit_summary=Помилка: + +editor.fork_edit_description=Ви не можете редагувати це Ñховище безпоÑередньо. Зміни буде запиÑано до вашого форку <b>%s</b>, тож ви зможете Ñтворити запит на злиттÑ. commits.desc=ПереглÑнути Ñ–Ñторію зміни коду. commits.commits=Коміти +commits.no_commits=Ðемає Ñпільних комітів. '%s' та '%s' мають різну Ñ–Ñторію. commits.nothing_to_compare=Ці гілки однакові. +commits.search_branch=Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° commits.search_all=УÑÑ– гілки commits.author=Ðвтор commits.message=ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ commits.date=Дата -commits.older=Давніше -commits.newer=Ðовіше +commits.older=Старіші +commits.newer=Ðовіші commits.signed_by=ПідпиÑано commits.signed_by_untrusted_user=ПідпиÑаний недовіреним кориÑтувачем -commits.signed_by_untrusted_user_unmatched=ПідпиÑаний недовіреним кориÑтувачем, Ñкий не відповідає комітеру +commits.signed_by_untrusted_user_unmatched=ПідпиÑано недовіреним кориÑтувачем, Ñкий не відповідає комітеру commits.gpg_key_id=Ідентифікатор GPG ключа +commits.ssh_key_fingerprint=Відбиток ключа SSH +commits.view_path=ПереглÑнути в Ñ–Ñторії +commits.view_file_diff=ПереглÑнути зміни до цього файлу в цьому коміті +commit.operations=Дії +commit.revert=Повернути до попереднього Ñтану +commit.revert-header=Повернути: %s +commit.revert-content=Виберіть гілку, до Ñкої хочете повернутиÑÑ: commitstatus.error=Помилка +commitstatus.failure=Ðевдача commitstatus.pending=ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ +commitstatus.success=УÑпіх ext_issues=ДоÑтуп до зовнішніх задач ext_issues.desc=ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° зовнішню ÑиÑтему відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ð·Ð°Ð´Ð°Ñ‡. @@ -986,23 +1396,35 @@ projects.description_placeholder=ÐžÐ¿Ð¸Ñ projects.create=Створити проєкт projects.title=Ðазва projects.new=Ðовий проєкт -projects.new_subheader=Координуйте, відÑтежуйте та оновлюйте інформацію про виконувану роботу в одному міÑці, аби проєкти залишалиÑÑ Ð¿Ñ€Ð¾Ð·Ð¾Ñ€Ð¸Ð¼Ð¸ та за розкладом. +projects.new_subheader=Координуйте, відÑтежуйте та оновлюйте Ñвою роботу в одному міÑці, щоб проєкти залишалиÑÑ Ð¿Ñ€Ð¾Ð·Ð¾Ñ€Ð¸Ð¼Ð¸ та виконувалиÑÑ Ð·Ð° графіком. +projects.create_success=Проєкт "%s" Ñтворено. projects.deletion=Видалити проєкт projects.deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ñ”ÐºÑ‚Ñƒ видалÑÑ” його з уÑÑ–Ñ… пов'Ñзаних задач. Продовжити? projects.deletion_success=Проєкт видалено. -projects.edit=Редагувати проєкти +projects.edit=Редагувати проєкт projects.edit_subheader=Проєкти організовують задачі та відÑтежують прогреÑ. -projects.modify=Оновити проєкт +projects.modify=Редагувати проєкт +projects.edit_success=Проєкт "%s" оновлено. projects.type.none=ВідÑутній projects.type.basic_kanban=Спрощений канбан projects.type.bug_triage=Ð¡Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð¼Ð¸Ð»Ð¾Ðº projects.template.desc=Шаблон проєкту -projects.template.desc_helper=Оберіть шаблон проєкту, аби почати +projects.template.desc_helper=Оберіть шаблон проєкту, щоб розпочати роботу +projects.column.edit=Редагувати Ñтовпець projects.column.edit_title=Ðазва projects.column.new_title=Ðазва +projects.column.new_submit=Створити Ñтовпець +projects.column.new=Ðовий Ñтовпець +projects.column.set_default=Ð’Ñтановити типово +projects.column.set_default_desc=Ð’Ñтановіть цей Ñтовпець типовим Ð´Ð»Ñ Ð·Ð°Ð´Ð°Ñ‡ Ñ– запитів на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð±ÐµÐ· категорії +projects.column.delete=Видалити Ñтовпець projects.column.color=Колір projects.open=Відкрити projects.close=Закрити +projects.column.assigned_to=Призначено +projects.card_type.desc=Попередні переглÑди картки +projects.card_type.images_and_text=Ð—Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ñ– текÑÑ‚ +projects.card_type.text_only=Лише текÑÑ‚ issues.desc=ÐžÑ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð·Ð²Ñ–Ñ‚Ñ–Ð² про помилки, завдань та етапів. issues.filter_assignees=Фільтр виконавців @@ -1010,8 +1432,10 @@ issues.filter_milestones=Фільтр етапів issues.filter_projects=Фільтр проєктів issues.filter_labels=Фільтр міток issues.filter_reviewers=Фільтр рецензентів +issues.filter_no_results=Ðемає результатів +issues.filter_no_results_placeholder=Спробуйте налаштувати Ñвої фільтри пошуку. issues.new=Ðова задача -issues.new.title_empty=Заголовок не може бути пуÑтим +issues.new.title_empty=Заголовок не може бути порожнім issues.new.labels=Мітки issues.new.no_label=Без мітки issues.new.clear_labels=ОчиÑтити мітки @@ -1022,15 +1446,21 @@ issues.new.open_projects=Відкриті проєкти issues.new.closed_projects=Закриті проєкти issues.new.no_items=Ðемає елементів issues.new.milestone=Етап -issues.new.no_milestone=Етап відÑутній +issues.new.no_milestone=Етапи відÑутні issues.new.clear_milestone=ОчиÑтити етап issues.new.assignees=Виконавці issues.new.clear_assignees=Прибрати виконавців -issues.new.no_assignees=Ðемає Ð²Ð¸ÐºÐ¾Ð½Ð°Ð²Ñ†Ñ -issues.choose.get_started=Початок роботи +issues.new.no_assignees=Ðемає виконавців +issues.new.no_reviewers=Ðемає рецензентів +issues.new.blocked_user=Ðе вдалоÑÑ Ñтворити задачу, тому що ви заблоковані влаÑником Ñховища. +issues.edit.blocked_user=Ðеможливо редагувати вміÑÑ‚, оÑкільки Ð²Ð°Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ð¾ автором або влаÑником Ñховища. +issues.choose.get_started=Розпочати issues.choose.open_external_link=Відкрити issues.choose.blank=Типово -issues.choose.blank_about=Створити задачу із шаблону за замовчуваннÑм. +issues.choose.blank_about=Створити задачу із Ñтандартного шаблону. +issues.choose.ignore_invalid_templates=ÐедійÑні шаблони проігноровано +issues.choose.invalid_templates=Знайдено %v недійÑний(Ñ…) шаблон(ів) +issues.choose.invalid_config=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ Ð·Ð°Ð´Ð°Ñ‡Ñ– міÑтить помилки: issues.no_ref=Ðе вказана гілка або тег issues.create=Створити задачу issues.new_label=Ðова мітка @@ -1040,7 +1470,8 @@ issues.create_label=Створити мітку issues.label_templates.title=Завантажити визначений набір міток issues.label_templates.info=Ще немає міток. ÐатиÑніть 'Ðова мітка' або викориÑтовуйте попередньо визначений набір міток: issues.label_templates.helper=Оберіть набір міток -issues.label_templates.use=ВикориÑтовувати набір міток +issues.label_templates.use=ВикориÑтати набір міток +issues.label_templates.fail_to_load_file=Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ файл шаблона мітки '%s': %v issues.add_label=додано %s з міткою %s issues.add_labels=додано %s з мітками %s issues.remove_label=видалено %s з міткою %s @@ -1066,27 +1497,40 @@ issues.delete_branch_at=`видалена гілка <b>%s</b> %s` issues.filter_label=Мітка issues.filter_label_exclude=`ВикориÑтовуйте <code>Alt</code> + <code>клік/Enter</code> Ð´Ð»Ñ Ð²Ð¸ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð¼Ñ–Ñ‚Ð¾Ðº` issues.filter_label_no_select=Ð’ÑÑ– мітки +issues.filter_label_select_no_label=Без мітки issues.filter_milestone=Етап +issues.filter_milestone_all=Ð’ÑÑ– етапи +issues.filter_milestone_none=Етапи відÑутні +issues.filter_milestone_open=Відкриті етапи +issues.filter_milestone_closed=Закриті етапи issues.filter_project=Проєкт +issues.filter_project_all=Ð’ÑÑ– проєкти issues.filter_project_none=Проєкт відÑутній issues.filter_assignee=Виконавець +issues.filter_assignee_no_assignee=Ðікому не приÑвоєно +issues.filter_assignee_any_assignee=Призначено будь-кому +issues.filter_poster=Ðвтор +issues.filter_user_placeholder=Пошук кориÑтувачів +issues.filter_user_no_select=УÑÑ– кориÑтувачі issues.filter_type=Тип issues.filter_type.all_issues=Ð’ÑÑ– задачі +issues.filter_type.all_pull_requests=УÑÑ– запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ issues.filter_type.assigned_to_you=Призначене вам issues.filter_type.created_by_you=Створено вами issues.filter_type.mentioning_you=Ð’Ð°Ñ Ð·Ð³Ð°Ð´Ð°Ð½Ð¾ -issues.filter_type.review_requested=Відгук запитано +issues.filter_type.review_requested=Запит на рецензію +issues.filter_type.reviewed_by_you=Перевірено вами issues.filter_sort=Сортувати issues.filter_sort.latest=Ðайновіші issues.filter_sort.oldest=ÐайÑтаріші -issues.filter_sort.recentupdate=Ðещодавно оновлено +issues.filter_sort.recentupdate=Ðещодавно оновлені issues.filter_sort.leastupdate=Ðайдавніше оновлені issues.filter_sort.mostcomment=Ðайбільш коментовані issues.filter_sort.leastcomment=Ðайменш коментовані -issues.filter_sort.nearduedate=Ðайновіша дата -issues.filter_sort.farduedate=ÐайÑтаріша дата -issues.filter_sort.moststars=Ðайбільш обраних -issues.filter_sort.feweststars=Ðайменш обраних +issues.filter_sort.nearduedate=Ðайближчий термін +issues.filter_sort.farduedate=Ðайвіддаленіший термін +issues.filter_sort.moststars=Ðайбільш фаворизовані +issues.filter_sort.feweststars=Ðайменш фаворизовані issues.filter_sort.mostforks=Ðайбільше форків issues.filter_sort.fewestforks=Ðайменше форків issues.action_open=Відкрити @@ -1096,13 +1540,16 @@ issues.action_milestone=Етап issues.action_milestone_no_select=Етап відÑутній issues.action_assignee=Виконавець issues.action_assignee_no_select=Ðемає Ð²Ð¸ÐºÐ¾Ð½Ð°Ð²Ñ†Ñ +issues.action_check=Ð’Ñтановити/знÑти позначку +issues.action_check_all=Ð’Ñтановити/знÑти позначку з уÑÑ–Ñ… елементів issues.opened_by=%[1]s відкрито <a href="%[2]s">%[3]s</a> -issues.opened_by_fake=відкрито %[1]s кориÑтувачем %[2]s +issues.opened_by_fake=%[1]s відкрито кориÑтувачем %[2]s issues.previous=Попередній issues.next=Далі issues.open_title=Відкрито issues.closed_title=Закрито issues.draft_title=Чернетка +issues.num_comments_1=%d коментар issues.num_comments=%d коментарів issues.commented_at=`прокоментував(ла) <a href="#%s">%s</a>` issues.delete_comment_confirm=Ви впевнені, що хочете видалити цей коментар? @@ -1111,33 +1558,56 @@ issues.context.quote_reply=Цитувати відповідь issues.context.reference_issue=ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð² новій задачі issues.context.edit=Редагувати issues.context.delete=Видалити +issues.no_content=Ðемає опиÑу. +issues.close=Закрити задачу +issues.comment_pull_merged_at=об'єднав(-ла) коміти %[1]s в %[2]s %[3]s +issues.comment_manually_pull_merged_at=вручну об'єднав(-ла) коміти %[1]s в %[2]s %[3]s +issues.close_comment_issue=Закрити з коментарем issues.reopen_issue=Відкрити знову +issues.reopen_comment_issue=Повторно відкрити з коментарем issues.create_comment=Коментар -issues.closed_at=`закрив цю задачу <a id="%[1]s" href="#%[1]s">%[2]s</a>` -issues.reopened_at=`повторно відкрив цю задачу <a id="%[1]s" href="#%[1]s">%[2]s</a>` +issues.comment.blocked_user=Ðеможливо Ñтворити або редагувати коментар, тому що ви заблоковані автором або влаÑником Ñховища. +issues.closed_at=`закрив(ла) цю задачу <a id="%[1]s" href="#%[1]s">%[2]s</a>` +issues.reopened_at=`повторно відкрив(ла) цю задачу <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.commit_ref_at=`згадано цю задачу в коміті <a id="%[1]s" href="#%[1]s">%[2]s</a>` -issues.ref_issue_from=`<a href="%[3]s">поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° цю задачу %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` +issues.ref_issue_from=`<a href="%[3]s"> вказав(ла) на цю задачу %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_pull_from=`<a href="%[3]s">поÑлавÑÑ Ð½Ð° цей запит Ð·Ð»Ð¸Ñ‚Ñ‚Ñ %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_closing_from=`<a href="%[3]s">згадав запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ %[4]s, Ñкі закриють цю задачу</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` -issues.ref_reopening_from=`<a href="%[3]s">згадав запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ %[4]s, Ñкі повторно відкриють цю задачу</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` +issues.ref_reopening_from=`<a href="%[3]s">згадав запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ %[4]s, Ñкий знову відкриє цю задачу</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_closed_from=`<a href="%[3]s">закрив цю задачу %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_reopened_from=`<a href="%[3]s">повторно відкрито цю задачу %[4]s</a> <a id="%[1]s" href="#%[1]s">%[2]s</a>` issues.ref_from=`із %[1]s` +issues.author=Ðвтор +issues.author_helper=Цей кориÑтувач Ñ” автором. issues.role.owner=ВлаÑник +issues.role.owner_helper=Цей кориÑтувач — влаÑник Ñховища. issues.role.member=УчаÑник +issues.role.member_helper=Цей кориÑтувач Ñ” учаÑником організації, Ñкій належить це Ñховище. +issues.role.collaborator=Співавтор +issues.role.collaborator_helper=Цей кориÑтувач був запрошений до Ñпівпраці у Ñховищі. +issues.role.first_time_contributor=УчаÑник, Ñкий вперше долучивÑÑ +issues.role.first_time_contributor_helper=Це перший внеÑок цього кориÑтувача в Ñховищі. +issues.role.contributor=Співавтор +issues.role.contributor_helper=Цей кориÑтувач раніше вже вноÑив зміни до Ñховища. issues.re_request_review=Повторно попроÑити рецензію issues.is_stale=З чаÑу оÑтанньої перевірки в цей PR було внеÑено деÑкі зміни issues.remove_request_review=Видалити запит Ñ€ÐµÑ†ÐµÐ½Ð·ÑƒÐ²Ð°Ð½Ð½Ñ issues.remove_request_review_block=Ðеможливо видалити запит Ñ€ÐµÑ†ÐµÐ½Ð·ÑƒÐ²Ð°Ð½Ð½Ñ -issues.dismiss_review=Відхилити рецензiÑŽ -issues.dismiss_review_warning=Ви впевнені, що хочете відхилити цей відгук? -issues.sign_in_require_desc=<a href="%s">ПідпишітьÑÑ</a> щоб приєднатиÑÑ Ð´Ð¾ обговореннÑ. +issues.dismiss_review=Відхилити рецензію +issues.dismiss_review_warning=Ви впевнені, що хочете відхилити рецензію? +issues.sign_in_require_desc=<a href="%s">Увійдіть</a>, щоб приєднатиÑÑ Ð´Ð¾ розмови. issues.edit=Редагувати issues.cancel=Відмінити issues.save=Зберегти issues.label_title=Ðазва мітки issues.label_description=ÐžÐ¿Ð¸Ñ Ð¼Ñ–Ñ‚ÐºÐ¸ -issues.label_color=Колір мітки +issues.label_color=Колір +issues.label_color_invalid=ÐедійÑний колір +issues.label_exclusive=ЕкÑклюзивно +issues.label_archive=Мітка архіву +issues.label_archived_filter=Показати архівовані мітки +issues.label_archive_tooltip=Ðрхівовані мітки типово виключаютьÑÑ Ð· пропозицій під Ñ‡Ð°Ñ Ð¿Ð¾ÑˆÑƒÐºÑƒ за мітками. +issues.label_exclusive_order=ПорÑдок ÑÐ¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ issues.label_count=%d міток issues.label_open_issues=%d відкритих задач issues.label_edit=Редагувати @@ -1145,9 +1615,9 @@ issues.label_delete=Видалити issues.label_modify=Редагувати мітку issues.label_deletion=Видалити мітку issues.label_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–Ñ‚ÐºÐ¸ видалÑÑ” Ñ—Ñ— з уÑÑ–Ñ… задач. Продовжити? -issues.label_deletion_success=Мітку було видалено. +issues.label_deletion_success=Мітку видалено. issues.label.filter_sort.alphabetically=За алфавітом -issues.label.filter_sort.reverse_alphabetically=З ÐºÑ–Ð½Ñ†Ñ Ð°Ð»Ñ„Ð°Ð²Ñ–Ñ‚Ñƒ +issues.label.filter_sort.reverse_alphabetically=У зворотному алфавітному порÑдку issues.label.filter_sort.by_size=Ðайменший розмір issues.label.filter_sort.reverse_by_size=Ðайбільший розмір issues.num_participants=%d учаÑників @@ -1155,17 +1625,21 @@ issues.attachment.open_tab=`ÐатиÑніть щоб побачити "%s" у Ð issues.attachment.download=`ÐатиÑніть щоб завантажити "%s"` issues.subscribe=ПідпиÑатиÑÑ issues.unsubscribe=ВідпиÑатиÑÑ -issues.lock=Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ -issues.unlock=Ð Ð¾Ð·Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ -issues.lock_duplicate=Задача не може бути заблокованим двічі. +issues.unpin=Відкріпити +issues.max_pinned=Ви не можете прикріпити більше задач +issues.pin_comment=прикріпив(-ла) %s +issues.unpin_comment=відкріпив(-ла) %s +issues.lock=Блокувати Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ +issues.unlock=Розблокувати Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ +issues.lock_duplicate=ÐžÐ±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð´Ð°Ñ‡Ñ– не може бути заблоковано двічі. issues.unlock_error=Ðе можливо розблокувати задачу, Ñка не заблокована. issues.lock_with_reason=заблоковано Ñк <strong>%s</strong> та обмежене Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñпівавторів %s issues.lock_no_reason=заблоковано та обмежене Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñпівавторів %s -issues.unlock_comment=розблоковане Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ %s +issues.unlock_comment=розблокував(ла) Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ %s issues.lock_confirm=Заблокувати issues.unlock_confirm=Розблокувати issues.lock.notice_1=- Інші кориÑтувачі не можуть додавати нові коментарі до цієї задачі. -issues.lock.notice_2=- Ви й інші Ñпівавтори, Ñкі мають доÑтуп до цього репозиторію, можете залишати коментарі, Ñкі інші можуть бачити. +issues.lock.notice_2=- Ви та інші Ñпівавтори, Ñкі мають доÑтуп до цього Ñховища, вÑе ще можете залишати коментарі, Ñкі можуть бачити інші. issues.lock.notice_3=- Ви завжди зможете розблокувати цю задачу в майбутньому. issues.unlock.notice_1=- Кожен зможе прокоментувати цю задачу ще раз. issues.unlock.notice_2=- Ви завжди зможете заблокувати цю задачу в майбутньому. @@ -1174,37 +1648,61 @@ issues.lock.title=Заблокувати Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ñ†Ñ–Ñ”Ñ— задРissues.unlock.title=Розблокувати Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ñ†Ñ–Ñ”Ñ— задачі. issues.comment_on_locked=Ви не можете коментувати заблоковану задачу. issues.delete=Видалити +issues.delete.title=Видалити цю задачу? +issues.delete.text=Ви дійÑно хочете видалити цю задачу? (Це видалить веÑÑŒ вміÑÑ‚. ÐатоміÑть подумайте про те, щоб закрити Ñ—Ñ— та зберегти в архіві) issues.tracker=ВідÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‡Ð°Ñу - +issues.timetracker_timer_start=ЗапуÑтити таймер +issues.timetracker_timer_stop=Зупинити таймер +issues.timetracker_timer_discard=Скинути таймер +issues.timetracker_timer_manually_add=Додати Ñ‡Ð°Ñ + +issues.time_estimate_set=Ð’Ñтановити орієнтовний Ñ‡Ð°Ñ +issues.time_estimate_display=Оцінка: %s +issues.change_time_estimate_at=змінено приблизний Ñ‡Ð°Ñ Ð½Ð° <b>%[1]s</b> %[2]s +issues.remove_time_estimate_at=видалено оцінку чаÑу %s +issues.time_estimate_invalid=Ðевірний формат розрахунку чаÑу issues.tracker_auto_close=Таймер буде автоматично зупинено, коли Ñ†Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° буде закрита issues.tracking_already_started=`Ви вже почали відÑтежувати Ñ‡Ð°Ñ Ð´Ð»Ñ <a href="%s">іншої задачі</a>!` +issues.stop_tracking=Зупинити таймер +issues.stop_tracking_history=працював Ð´Ð»Ñ <b>%[1]s</b> %[2]s +issues.cancel_tracking=Скинути +issues.cancel_tracking_history=`ÑкаÑував(-ла) відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‡Ð°Ñу %s` issues.del_time=Видалити цей журнал чаÑу +issues.add_time_history=додав(ла) витрачений Ñ‡Ð°Ñ <b>%[1]s</b> %[2]s issues.del_time_history=`видалив витрачений Ñ‡Ð°Ñ %s` +issues.add_time_manually=Вручну додати Ñ‡Ð°Ñ issues.add_time_hours=Години issues.add_time_minutes=Хвилини issues.add_time_sum_to_small=Ð§Ð°Ñ Ð½Ðµ введено. issues.time_spent_total=Загальний витрачений Ñ‡Ð°Ñ issues.time_spent_from_all_authors=`Загальний витрачений чаÑ: %s` -issues.due_date=Дата Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ -issues.invalid_due_date_format=Дата Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ð¼Ð°Ñ” бути в форматі 'ррр-мм-дд'. -issues.error_modifying_due_date=Ðе вдалоÑÑ Ð·Ð¼Ñ–Ð½Ð¸Ñ‚Ð¸ дату завершеннÑ. +issues.due_date=Строк Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ +issues.invalid_due_date_format=Формат Ñтроку Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ бути 'рррр-мм-дд'. +issues.error_modifying_due_date=Ðе вдалоÑÑ Ð·Ð¼Ñ–Ð½Ð¸Ñ‚Ð¸ термін виконаннÑ. issues.error_removing_due_date=Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ дату завершеннÑ. issues.push_commit_1=додав %d коміт %s issues.push_commits_n=додав %d коміти(-ів) %s -issues.force_push_codes=`примуÑово залито %[1]s з <a class="ui sha" href="%[3]s"><code>%[2]s</code></a> до <a class="ui sha" href="%[5]s"><code>%[4]s</code></a> %[6]s` +issues.force_push_codes=`примуÑово надіÑлано %[1]s з <a class="ui sha" href="%[3]s"><code>%[2]s</code></a> до <a class="ui sha" href="%[5]s"><code>%[4]s</code></a> %[6]s` issues.force_push_compare=ПорівнÑти issues.due_date_form=рррр-мм-дд issues.due_date_form_add=Додати дату Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ issues.due_date_form_edit=Редагувати issues.due_date_form_remove=Видалити +issues.due_date_not_writer=Вам потрібен доÑтуп на Ð·Ð°Ð¿Ð¸Ñ Ð´Ð¾ цього Ñховища, щоб оновити дату Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð·Ð°Ð´Ð°Ñ‡Ñ–. issues.due_date_not_set=Термін Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð½Ðµ вÑтановлений. issues.due_date_added=додав(ла) дату Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ %s %s +issues.due_date_modified=змінив(-ла) термін Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð· %[2]s на %[1]s %[3]s issues.due_date_remove=видалив(ла) дату Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ %s %s issues.due_date_overdue=ПроÑтрочено -issues.due_date_invalid=Термін дії не дійÑний або знаходитьÑÑ Ð·Ð° межами допуÑтимого діапазону. Будь лаÑка викориÑтовуйте формат 'yyyy-mm-dd'. +issues.due_date_invalid=Термін дії не дійÑний або знаходитьÑÑ Ð·Ð° межами діапазону. Будь лаÑка, викориÑтовуйте формат 'рррр-мм-дд'. issues.dependency.title=ЗалежноÑті +issues.dependency.issue_no_dependencies=ЗалежноÑтей не вÑтановлено. +issues.dependency.pr_no_dependencies=ЗалежноÑтей не вÑтановлено. +issues.dependency.no_permission_1=У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” дозволу на Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ %d залежноÑті +issues.dependency.no_permission_n=Ви не маєте дозволу на Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ %d залежноÑтей +issues.dependency.no_permission.can_remove=Ви не маєте дозволу на Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ Ñ†Ñ–Ñ”Ñ— залежноÑті, але можете видалити Ñ—Ñ—. issues.dependency.add=Додати залежніÑть… issues.dependency.cancel=Відмінити issues.dependency.remove=Видалити @@ -1212,39 +1710,45 @@ issues.dependency.remove_info=Видалити цю залежніÑть issues.dependency.added_dependency=`додав нову залежніÑть %s` issues.dependency.removed_dependency=`видалив залежніÑть %s` issues.dependency.pr_closing_blockedby=Ð—Ð°ÐºÑ€Ð¸Ñ‚Ñ‚Ñ Ñ†ÑŒÐ¾Ð³Ð¾ запиту Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ð±Ð»Ð¾ÐºÐ¾Ð²Ð°Ð½Ð¾ наÑтупними задачами -issues.dependency.issue_closing_blockedby=Ð—Ð°ÐºÑ€Ð¸Ñ‚Ñ‚Ñ Ñ†Ñ–Ñ”Ñ— задачи заблоковано наÑтупними задачами -issues.dependency.issue_close_blocks=Ð¦Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° блокує Ð·Ð°ÐºÑ€Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ð»ÐµÐ¶Ð½Ð¸Ñ… задач +issues.dependency.issue_closing_blockedby=Ð—Ð°ÐºÑ€Ð¸Ñ‚Ñ‚Ñ Ñ†Ñ–Ñ”Ñ— задачі заблоковано наÑтупними задачами +issues.dependency.issue_close_blocks=Ð¦Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° блокує Ð·Ð°ÐºÑ€Ð¸Ñ‚Ñ‚Ñ Ð½Ð°Ñтупних задач issues.dependency.pr_close_blocks=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð±Ð»Ð¾ÐºÑƒÑ” Ð·Ð°ÐºÑ€Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ð»ÐµÐ¶Ð½Ð¸Ñ… задач -issues.dependency.issue_close_blocked=Вам потрібно закрити вÑÑ– задачі, що блокують цю задачу, перед Ñ—Ñ— закриттÑм. +issues.dependency.issue_close_blocked=Перш ніж закрити це завданнÑ, вам потрібно закрити вÑÑ– завданнÑ, що блокують його. +issues.dependency.issue_batch_close_blocked=Ðеможливо пакетно закрити обрані задачі, оÑкільки задача #%d вÑе ще має відкриті залежноÑті issues.dependency.pr_close_blocked=Вам потрібно закрити вÑÑ– задачі, що блокують цей запит, перед його злиттÑм. issues.dependency.blocks_short=Блоки issues.dependency.blocked_by_short=Залежить від issues.dependency.remove_header=Видалити залежніÑть issues.dependency.issue_remove_text=Це призведе до Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð·Ð°Ð»ÐµÐ¶Ð½Ð¾Ñті з цієї задачі. Продовжити? -issues.dependency.pr_remove_text=Це призведе до Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð·Ð°Ð»ÐµÐ¶Ð½Ð¾Ñті з цього пулл-реквеÑту. Продовжити? +issues.dependency.pr_remove_text=Це вилучить залежніÑть з цього запиту на злиттÑ. Продовжити? issues.dependency.setting=Увімкнути залежноÑті Ð´Ð»Ñ Ð·Ð°Ð´Ð°Ñ‡ та запитів на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ issues.dependency.add_error_same_issue=Ви не можете зробити задачу залежною від Ñебе. -issues.dependency.add_error_dep_issue_not_exist=ЗалежніÑть Ð´Ð»Ñ Ð·Ð°Ð´Ð°Ñ‡Ñ– не Ñ–Ñнує. +issues.dependency.add_error_dep_issue_not_exist=Залежної задачі не Ñ–Ñнує. issues.dependency.add_error_dep_not_exist=ЗалежніÑть не Ñ–Ñнує. issues.dependency.add_error_dep_exists=ЗалежніÑть уже Ñ–Ñнує. issues.dependency.add_error_cannot_create_circular=Ви не можете Ñтворити залежніÑть з двома задачами, Ñкі блокують одна одну. issues.dependency.add_error_dep_not_same_repo=Обидві задачі повинні бути в одному репозиторії. -issues.review.self.approval=Ви не можете Ñхвалити влаÑний пулл-реквеÑÑ‚. +issues.review.self.approval=Ви не можете затвердити влаÑний запит на злиттÑ. issues.review.self.rejection=Ви не можете надіÑлати запит на зміну на влаÑний пулл-реквеÑÑ‚. -issues.review.approve=зміни затверджено %s -issues.review.dismissed=відхилено відгук %s %s +issues.review.approve=затвердив(ла) ці зміни %s +issues.review.comment=рецензовано %s +issues.review.dismissed=відхилив(ла) рецензію %s %s issues.review.dismissed_label=Відхилено issues.review.left_comment=додав коментар -issues.review.content.empty=Запрошуючи зміни, ви зобов'Ñзані залишити коментар з поÑÑненнÑми Ñвоїх побажань відноÑно Pull Request'а. +issues.review.content.empty=Вам потрібно залишити коментар із зазначеннÑм бажаної зміни (змін). issues.review.reject=зробив запит змін %s -issues.review.wait=попроÑив рецензію %s +issues.review.wait=запроÑили на рецензію %s issues.review.add_review_request=попроÑив рецензію від %s %s issues.review.remove_review_request=видалив запит на рецензію до %s %s issues.review.remove_review_request_self=відмовивÑÑ Ñ€ÐµÑ†ÐµÐ½Ð·ÑƒÐ²Ð°Ñ‚Ð¸ %s issues.review.pending=ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ +issues.review.pending.tooltip=Цей коментар наразі не видно іншим кориÑтувачам. Щоб відправити Ñвій коментар, виберіть "%s" -> "%s/%s/%s" у верхній чаÑтині Ñторінки. issues.review.review=Рецензії issues.review.reviewers=Рецензенти issues.review.outdated=ЗаÑтарілі +issues.review.outdated_description=ВміÑÑ‚ змінивÑÑ Ð· моменту ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ñ +issues.review.option.show_outdated_comments=Показати заÑтарілі коментарі +issues.review.option.hide_outdated_comments=Приховати заÑтарілі коментарі issues.review.show_outdated=Показати заÑтарілі issues.review.hide_outdated=Приховати заÑтарілі issues.review.show_resolved=Показати вирішене @@ -1252,7 +1756,12 @@ issues.review.hide_resolved=Приховати вирішене issues.review.resolve_conversation=Завершити Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ issues.review.un_resolve_conversation=Поновити Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ issues.review.resolved_by=позначив Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ð¼ -issues.review.commented=Коментар +issues.review.commented=Коментувати +issues.review.official=Затверджено +issues.review.requested=ОчікуєтьÑÑ Ñ€Ð¾Ð·Ð³Ð»Ñд +issues.review.rejected=Запит на зміни +issues.review.stale=Оновлено з моменту Ð·Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ +issues.review.unofficial=Ðевраховане Ð·Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ issues.assignee.error=Додано не вÑÑ–Ñ… виконавців через непередбачену помилку. issues.reference_issue.body=Тіло issues.content_history.deleted=видалено @@ -1260,21 +1769,39 @@ issues.content_history.edited=відредаговано issues.content_history.created=Ñтворено issues.content_history.delete_from_history=Видалити з Ñ–Ñторії issues.content_history.delete_from_history_confirm=Видалити з Ñ–Ñторії? -issues.content_history.options=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ +issues.content_history.options=Параметри +issues.reference_link=ПоÑиланнÑ: %s compare.compare_base=оÑнова compare.compare_head=порівнÑти pulls.desc=Увімкнути запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ‚Ð° оглÑд коду. pulls.new=Ðовий запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +pulls.new.blocked_user=Ðеможливо Ñтворити запит на злиттÑ, тому що ви заблоковані влаÑником Ñховища. +pulls.new.must_collaborator=Ви повинні бути учаÑником, щоб Ñтворити запит на злиттÑ. +pulls.edit.already_changed=Ðе вдалоÑÑ Ð·Ð±ÐµÑ€ÐµÐ³Ñ‚Ð¸ зміни до запиту на злиттÑ. Схоже, вміÑÑ‚ вже змінено іншим кориÑтувачем. Будь лаÑка, оновіть Ñторінку Ñ– Ñпробуйте редагувати ще раз, щоб уникнути перезапиÑу Ñ—Ñ… змін +pulls.view=ПереглÑнути запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ pulls.compare_changes=Ðовий запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +pulls.allow_edits_from_maintainers_desc=КориÑтувачі з доÑтупом на Ð·Ð°Ð¿Ð¸Ñ Ð´Ð¾ базової гілки також можуть завантажувати Ñвої зміни до цієї гілки +pulls.allow_edits_from_maintainers_err=ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ðµ вдалоÑÑ pulls.compare_changes_desc=ПорівнÑти дві гілки Ñ– Ñтворити запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð»Ñ Ð·Ð¼Ñ–Ð½. +pulls.has_viewed_file=ПереглÑдів +pulls.has_changed_since_last_review=Змінено з моменту вашого оÑтаннього відгуку +pulls.viewed_files_label=ПереглÑнуто %[1]d / %[2]d файлів +pulls.expand_files=Розгорнути вÑÑ– файли +pulls.collapse_files=Згорнути вÑÑ– файли pulls.compare_base=злити в -pulls.compare_compare=pull з -pulls.switch_comparison_type=Перемкнути виглÑд порівнÑÐ½Ð½Ñ +pulls.switch_comparison_type=Перемкнути тип порівнÑÐ½Ð½Ñ pulls.switch_head_and_base=ПомінÑти міÑцÑми оÑновну та базову гілку pulls.filter_branch=Фільтр по гілці +pulls.show_all_commits=Показати вÑÑ– коміти +pulls.show_changes_since_your_last_review=Показати зміни піÑÐ»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ оÑтаннього відгуку +pulls.showing_only_single_commit=ВідображаютьÑÑ Ð»Ð¸ÑˆÐµ зміни коміту %[1]s +pulls.showing_specified_commit_range=ВідображаютьÑÑ Ð»Ð¸ÑˆÐµ зміни між %[1]s..%[2]s +pulls.select_commit_hold_shift_for_range=Виберіть коміт. ÐатиÑніть клавішу Shift + клацніть, щоб виділити діапазон +pulls.filter_changes_by_commit=Фільтр за комітом pulls.nothing_to_compare=Ці гілки однакові. Ðемає необхідноÑті Ñтворювати запитів на злиттÑ. +pulls.nothing_to_compare_have_tag=Виділена гілка або мітка ідентичні. pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім. pulls.has_pull_request=`Запит Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð»Ñ Ñ†Ð¸Ñ… гілок вже Ñ–Ñнує: <a href="%[1]s">%[2]s#%[3]d</a>` pulls.create=Створити запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ @@ -1284,113 +1811,154 @@ pulls.change_target_branch_at=`змінена цільова гілка з <b>%s pulls.tab_conversation=ÐžÐ±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ pulls.tab_commits=Коміти pulls.tab_files=Змінені файли -pulls.reopen_to_merge=Будь лаÑка перевідкрийте цей запит щоб здіÑнити операцію злиттÑ. +pulls.reopen_to_merge=Будь лаÑка, заново відкрийте цей запит щоб виконати злиттÑ. pulls.cant_reopen_deleted_branch=Цей запит не можна повторно відкрити, оÑкільки гілку видалено. pulls.merged=Злито +pulls.merged_success=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ ÑƒÑпішно об'єднано Ñ– закрито +pulls.closed=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°ÐºÑ€Ð¸Ñ‚Ð¾ pulls.manually_merged=Ручне Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -pulls.is_closed=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð±ÑƒÐ»Ð¾ закрито. -pulls.title_wip_desc=`<a href="#">Почніть заголовок з <strong>%s</strong></a> щоб запобігти випадковому злиттю запитів.` -pulls.cannot_merge_work_in_progress=Цей пулл-реквеÑÑ‚ позначений Ñк прийнÑтий в опрацюваннÑ. -pulls.still_in_progress=Ð’Ñе ще в процеÑÑ–? +pulls.merged_info_text=Гілку %s тепер можна видалити. +pulls.is_closed=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°ÐºÑ€Ð¸Ñ‚Ð¾. +pulls.title_wip_desc=`<a href="#">Почніть заголовок з <strong>%s</strong></a> щоб запобігти випадковому об'єднанню.` +pulls.cannot_merge_work_in_progress=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ð¾Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¾ Ñк незавершений. +pulls.still_in_progress=Ще не закінчено? pulls.add_prefix=Додати Ð¿Ñ€ÐµÑ„Ñ–ÐºÑ <strong>%s</strong> pulls.remove_prefix=Видалити Ð¿Ñ€ÐµÑ„Ñ–ÐºÑ <strong>%s</strong> -pulls.data_broken=ЗміÑÑ‚ цього запиту було порушено внаÑлідок Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ–Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ— Форком. Цей запит Ñ‚ÑгнетьÑÑ Ñ‡ÐµÑ€ÐµÐ· відÑутніÑть інформації про вилученнÑ. -pulls.files_conflicted=Цей запит має зміни, що конфліктують з цільовою гілкою. +pulls.data_broken=Збій цього запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ‡ÐµÑ€ÐµÐ· відÑутніÑть інформації про форк. +pulls.files_conflicted=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¼Ð°Ñ” зміни, що конфліктують з цільовою гілкою. +pulls.is_checking=Перевірка конфліктів об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ (merge) ... +pulls.is_ancestor=Цю гілку вже включено до цільової гілки. Ðема чого об'єднувати. pulls.required_status_check_failed=ДеÑкі необхідні перевірки виконані з помилками. pulls.required_status_check_missing=Декілька з необхідних перевірок відÑутні. pulls.required_status_check_administrator=Як адмініÑтратор ви вÑе одно можете об'єднати цей запит на злиттÑ. -pulls.can_auto_merge_desc=Цей запит можна об'єднати автоматично. -pulls.cannot_auto_merge_desc=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ може бути злитий автоматично через конфлікти. -pulls.cannot_auto_merge_helper=Злийте вручну Ð´Ð»Ñ Ð²Ð¸Ñ€Ñ–ÑˆÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ„Ð»Ñ–ÐºÑ‚Ñ–Ð². -pulls.num_conflicting_files_1=%d конфліктуючий файл -pulls.num_conflicting_files_n=%d конфліктуючі файли +pulls.can_auto_merge_desc=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¼Ð¾Ð¶Ð½Ð° об'єднати автоматично. +pulls.cannot_auto_merge_desc=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ може бути об'єднано автоматично через конфлікти. +pulls.cannot_auto_merge_helper=Об'єднайте вручну Ð´Ð»Ñ Ð²Ð¸Ñ€Ñ–ÑˆÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ„Ð»Ñ–ÐºÑ‚Ñ–Ð². +pulls.num_conflicting_files_1=%d конфліктний файл +pulls.num_conflicting_files_n=%d конфліктних файлів pulls.approve_count_1=%d ÑÑ…Ð²Ð°Ð»ÐµÐ½Ð½Ñ pulls.approve_count_n=%d Ñхвалень pulls.reject_count_1=%d запит на зміну -pulls.reject_count_n=%d запити на зміну -pulls.waiting_count_1=очікуєтьÑÑ %d Ñ€ÐµÑ†ÐµÐ½Ð·Ñ–Ñ +pulls.reject_count_n=%d запитів на зміну +pulls.waiting_count_1=очікуєтьÑÑ %d рецензій pulls.waiting_count_n=очікуєтьÑÑ %d рецензії(й) -pulls.wrong_commit_id=id коміту повинен бути id коміту в цільовій гілці - -pulls.no_merge_desc=Цей запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½ÐµÐ¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ злити, оÑкільки вÑÑ– параметри об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ð²Ð¸Ð¼ÐºÐ½ÐµÐ½Ð¾. -pulls.no_merge_helper=Увімкніть параметри Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð² налаштуваннÑÑ… Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ð°Ð±Ð¾ злийте запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ€ÑƒÑ‡Ð½Ñƒ. -pulls.no_merge_wip=Цей пулл-реквеÑÑ‚ не можливо об'єднати, тому-що він вже виконуєтьÑÑ. -pulls.no_merge_not_ready=Цей запит не готовий до злиттÑ, перевірте ÑÑ‚Ð°Ñ‚ÑƒÑ Ñ€ÐµÑ†ÐµÐ½Ð·Ñ–ÑŽÐ²Ð°Ð½Ð½Ñ Ñ– ÑÑ‚Ð°Ñ‚ÑƒÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸. -pulls.no_merge_access=Ви не авторизовані, щоб виконати цей запит на злиттÑ. -pulls.merge_pull_request=Створити коміт зі злиттÑм +pulls.wrong_commit_id=ідентифікатор коміту повинен бути ідентифікатором коміту в цільовій гілці + +pulls.no_merge_desc=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½ÐµÐ¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ об'єднати, оÑкільки вÑÑ– параметри об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ñховищ вимкнено. +pulls.no_merge_helper=Увімкніть параметри об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð² налаштуваннÑÑ… Ñховища або об'єднайте запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ€ÑƒÑ‡Ð½Ñƒ. +pulls.no_merge_wip=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½ÐµÐ¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ об'єднаний, оÑкільки він позначений Ñк незавершений. +pulls.no_merge_not_ready=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ‰Ðµ не готовий до об'єднаннÑ, перевірте ÑтатуÑи Ñ€ÐµÑ†ÐµÐ½Ð·Ñ–ÑŽÐ²Ð°Ð½Ð½Ñ Ñ‚Ð° перевірки. +pulls.no_merge_access=Ви не авторизовані об'єднувати цей запит на злиттÑ. +pulls.merge_pull_request=Створити коміт об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ pulls.rebase_merge_pull_request=Перебазувати, а потім виконати Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿ÐµÑ€ÐµÐ¼Ð¾Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñм -pulls.rebase_merge_commit_pull_request=Перебазувати, а потім Ñтворити коміт Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +pulls.rebase_merge_commit_pull_request=Перебазувати, а потім Ñтворити коміт об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ pulls.squash_merge_pull_request=Створити зварений (squash) коміт pulls.merge_manually=Об’єднано вручну -pulls.merge_commit_id=ID коміту Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -pulls.require_signed_wont_sign=Гілка вимагає підпиÑаних комітів, але це Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ буде підпиÑано +pulls.merge_commit_id=Ідентифікатор коміту Ð¾Ð±â€™Ñ”Ð´Ð½Ð°Ð½Ð½Ñ +pulls.require_signed_wont_sign=Гілка вимагає підпиÑаних комітів, але це об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½Ðµ буде підпиÑано -pulls.invalid_merge_option=Цей параметр Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ можна викориÑтовувати Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Pull Request'а. -pulls.merge_conflict=Ð—Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ вдалоÑÑ: Був конфлікт при злиттÑ. Підказка: Ñпробуйте іншу Ñтратегію +pulls.invalid_merge_option=Цей параметр об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½Ðµ можна викориÑтовувати Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ запиту на злиттÑ. +pulls.merge_conflict=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½Ðµ вдалоÑÑ: Під Ñ‡Ð°Ñ Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ð¸Ð½Ð¸Ðº конфлікт. Підказка: Спробуйте іншу Ñтратегію pulls.merge_conflict_summary=Помилка -pulls.rebase_conflict=Ð—Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ вдалоÑÑ: відбувÑÑ ÐºÐ¾Ð½Ñ„Ð»Ñ–ÐºÑ‚ під Ñ‡Ð°Ñ Ð·Ð»Ð¸Ñ‚Ñ‚Ñ: %[1]s. Підказка: Ñпробуйте іншу Ñтратегію +pulls.rebase_conflict=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½Ðµ вдалоÑÑ: відбувÑÑ ÐºÐ¾Ð½Ñ„Ð»Ñ–ÐºÑ‚ під Ñ‡Ð°Ñ Ð¿ÐµÑ€ÐµÐ±Ð°Ð·ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ: %[1]s. Підказка: Ñпробуйте іншу Ñтратегію pulls.rebase_conflict_summary=Помилка -pulls.unrelated_histories=Помилка злиттÑ: head та base Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ мають Ñпільної Ñ–Ñторії. Підказка: Ñпробуйте іншу Ñтратегію -pulls.merge_out_of_date=Помилка злиттÑ: base було оновлено, поки відбувалоÑÑ Ð·Ð»Ð¸Ñ‚Ñ‚Ñ. Підказка: Ñпробуйте знову. pulls.push_rejected_summary=Повне Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ відмову -pulls.open_unmerged_pull_exists=`Ви не можете знову відкрити, оÑкільки вже Ñ–Ñнує запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ (%d) з того ж Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ð· тією ж інформацією про Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ– в очікуванні.` -pulls.status_checking=ДеÑкі перевірки знаходÑтьÑÑ Ð½Ð° розглÑді +pulls.open_unmerged_pull_exists=`Ви не можете повторно відкрити цей запит на злиттÑ, оÑкільки вже Ñ–Ñнує один (#%d) з ідентичними влаÑтивоÑÑ‚Ñми.` +pulls.status_checking=ДеÑкі перевірки ще не завершені pulls.status_checks_success=Ð’ÑÑ– перевірки були уÑпішними pulls.status_checks_warning=Декілька перевірок завершилиÑÑ Ð· попередженнÑми -pulls.status_checks_failure=Декілька перевірок не були уÑпішними +pulls.status_checks_failure=ДеÑкі перевірки не Ñпрацювали pulls.status_checks_error=Декілька перевірок завершилиÑÑ Ð· помилками pulls.status_checks_requested=Обов'Ñзково pulls.status_checks_details=Подробиці -pulls.update_branch=Оновити гілку шлÑхом Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +pulls.status_checks_hide_all=Приховати вÑÑ– перевірки +pulls.status_checks_show_all=Показати вÑÑ– перевірки +pulls.update_branch=Оновити гілку шлÑхом об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ pulls.update_branch_rebase=Оновити гілку перебазуваннÑм -pulls.update_branch_success=ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð³Ñ–Ð»ÐºÐ¸ пройшло уÑпішно +pulls.update_branch_success=ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð³Ñ–Ð»ÐºÐ¸ уÑпішне pulls.update_not_allowed=Ви не можете оновити гілку pulls.outdated_with_base_branch=Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° заÑтаріла відноÑно базової гілки +pulls.close=Закрити запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ pulls.closed_at=`закрив цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.reopened_at=`повторно відкрив цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ <a id="%[1]s" href="#%[1]s">%[2]s</a>` +pulls.cmd_instruction_hint=ПереглÑнути інÑтрукції командного Ñ€Ñдка +pulls.cmd_instruction_merge_title=Об'єднати +pulls.cmd_instruction_merge_desc=Об'єднати зміни Ñ– оновити на Gitea. +pulls.clear_merge_message=ОчиÑтити Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ +pulls.auto_merge_button_when_succeed=(Якщо перевірки уÑпішні) +pulls.auto_merge_when_succeed=Ðвтоматичне об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð¿Ñ–ÑÐ»Ñ ÑƒÑпішного Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… перевірок +pulls.auto_merge_cancel_schedule=СкаÑувати автоматичне об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ +pulls.auto_merge_not_scheduled=Цей запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ плануєтьÑÑ Ð¾Ð±'єднувати автоматично. +pulls.delete.title=Видалити цей запит на злиттÑ? +pulls.upstream_diverging_prompt_behind_1=Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° на %[1]d коміт позаду %[2]s +pulls.upstream_diverging_prompt_behind_n=Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° на %[1]d комітів позаду %[2]s +pulls.upstream_diverging_prompt_base_newer=Базова гілка %s має нові зміни +pulls.upstream_diverging_merge_confirm=Хочете об’єднати "%[1]s" з "%[2]s"? +pull.deleted_branch=(видалена):%s +pull.agit_documentation=ПереглÑнути документацію про AGit milestones.new=Ðовий етап milestones.closed=Закрито %s +milestones.update_ago=Оновлено %s milestones.no_due_date=Ðемає дати Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ milestones.open=Відкрити milestones.close=Закрити +milestones.completeness=<strong>%d%%</strong> завершено milestones.create=Створити етап milestones.title=Заголовок milestones.desc=ÐžÐ¿Ð¸Ñ -milestones.due_date=Дата Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ (опціонально) +milestones.due_date=Дата Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ (необов’Ñзково) milestones.clear=ОчиÑтити milestones.invalid_due_date_format=Дата Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¼Ð°Ñ” бути в форматі 'рррр-мм-дд'. +milestones.create_success=Етап "%s" Ñтворено. milestones.edit=Редагувати етап -milestones.edit_subheader=Створюйте етапи Ð´Ð»Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ— ваших задач. +milestones.edit_subheader=Етапи впорÑдковують задачі та відÑтежують прогреÑ. milestones.cancel=Відмінити milestones.modify=Оновити етап +milestones.edit_success=Етап '%s' оновлено. milestones.deletion=Видалити етап -milestones.deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐµÑ‚Ð°Ð¿Ñƒ призведе до його Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð· уÑÑ–Ñ… пов'Ñзаних задач. Продовжити? -milestones.deletion_success=Етап уÑпішно видалено. +milestones.deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐµÑ‚Ð°Ð¿Ñƒ видалÑÑ” його з уÑÑ–Ñ… пов'Ñзаних з ним задач. Продовжити? +milestones.deletion_success=Етап видалено. milestones.filter_sort.name=Ðазва -milestones.filter_sort.least_complete=Менш повне -milestones.filter_sort.most_complete=Більш повне -milestones.filter_sort.most_issues=Ðайбільш задач -milestones.filter_sort.least_issues=Ðайменш задач - - +milestones.filter_sort.earliest_due_data=Ðайраніший Ñтрок +milestones.filter_sort.latest_due_date=ОÑтанній Ñтрок +milestones.filter_sort.most_issues=Ðайбільше задач +milestones.filter_sort.least_issues=Ðайменше задач + +signing.will_sign=Цей коміт буде підпиÑано ключем "%s". +signing.wont_sign.error=Виникла помилка під Ñ‡Ð°Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ можливоÑті підпиÑÐ°Ð½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ. +signing.wont_sign.nokey=Ðемає ключа Ð´Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑÐ°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ коміту. +signing.wont_sign.never=Коміти ніколи не підпиÑуютьÑÑ. +signing.wont_sign.always=Коміти завжди підпиÑуютьÑÑ. +signing.wont_sign.pubkey=Коміт не буде підпиÑано, оÑкільки у Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” публічного ключа, пов'Ñзаного з вашим обліковим запиÑом. +signing.wont_sign.twofa=Ð”Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑÐ°Ð½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñ–Ð² у Ð²Ð°Ñ Ð¼Ð°Ñ” бути увімкнена двофакторна автентифікаціÑ. +signing.wont_sign.parentsigned=Цей коміт не буде підпиÑано, оÑкільки не підпиÑано батьківÑький коміт. +signing.wont_sign.basesigned=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½Ðµ буде підпиÑане, оÑкільки базовий коміт не підпиÑано. +signing.wont_sign.headsigned=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½Ðµ буде підпиÑане, оÑкільки не підпиÑано головний коміт. +signing.wont_sign.commitssigned=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½Ðµ буде підпиÑане, оÑкільки вÑÑ– пов'Ñзані з ним коміти не підпиÑані. +signing.wont_sign.approved=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½Ðµ буде підпиÑане, оÑкільки Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ðµ затверджено. +signing.wont_sign.not_signed_in=Ви не увійшли до ÑиÑтеми. + +ext_wiki=ДоÑтуп до зовнішньої вікі ext_wiki.desc=ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° зовнішню вікі. wiki=Вікі wiki.welcome=ЛаÑкаво проÑимо до Вікі. -wiki.welcome_desc=Wiki дозволÑÑ” пиÑати та ділитиÑÑ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ñ–Ñ”ÑŽ з Ñпівавторами. -wiki.desc=Пишіть та обмінюйтеÑÑ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ñ–Ñ”ÑŽ із Ñпівавторами. +wiki.welcome_desc=Wiki дозволÑÑ” пиÑати та ділитиÑÑ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ñ–Ñ”ÑŽ зі Ñпівавторами. +wiki.desc=Пишіть та обмінюйтеÑÑ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ñ–Ñ”ÑŽ зі Ñпівавторами. wiki.create_first_page=Створити першу Ñторінку wiki.page=Сторінка wiki.filter_page=Фільтр Ñторінок wiki.new_page=Сторінка +wiki.page_title=Заголовок Ñторінки +wiki.page_content=ЗміÑÑ‚ Ñторінки wiki.default_commit_message=Ðапишіть примітку про Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ†Ñ–Ñ”Ñ— Ñторінки (необов'Ñзково). wiki.save_page=Зберегти Ñторінку wiki.last_commit_info=%s редагував цю Ñторінку %s @@ -1400,12 +1968,19 @@ wiki.file_revision=Ревізії Ñторінки wiki.wiki_page_revisions=Ревізії вікі Ñторінок wiki.back_to_wiki=ПовернутиÑÑŒ на Ñторінку Вікі wiki.delete_page_button=Видалити Ñторінку -wiki.page_already_exists=Вікі-Ñторінка з таким Ñамим ім'Ñм вже Ñ–Ñнує. +wiki.delete_page_notice_1=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²Ñ–ÐºÑ–-Ñторінки "%s" не може бути ÑкаÑовано. Продовжити? +wiki.page_already_exists=Сторінка Вікі з такою назвою вже Ñ–Ñнує. +wiki.reserved_page=Ðазва Ñторінки вікі "%s" зарезервована. wiki.pages=Сторінки wiki.last_updated=ОÑтанні Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ %s wiki.page_name_desc=Введіть назву вікі-Ñторінки. ДеÑкі із Ñпеціальних імен: 'Home', '_Sidebar' та '_Footer'. +wiki.original_git_entry_tooltip=ПереглÑд оригінального файлу Git заміÑть викориÑÑ‚Ð°Ð½Ð½Ñ Ð´Ñ€ÑƒÐ¶Ð½ÑŒÐ¾Ð³Ð¾ поÑиланнÑ. activity=ÐктивніÑть +activity.navbar.pulse=ÐŸÑƒÐ»ÑŒÑ +activity.navbar.code_frequency=ЧаÑтота коду +activity.navbar.contributors=Співавтори +activity.navbar.recent_commits=Ðещодавні коміти activity.period.filter_label=Період: activity.period.daily=1 день activity.period.halfweekly=3 дні @@ -1415,25 +1990,25 @@ activity.period.quarterly=3 міÑÑці activity.period.semiyearly=6 міÑÑців activity.period.yearly=1 рік activity.overview=ОглÑд -activity.active_prs_count_1=<strong>%d</strong> Ðктивний запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +activity.active_prs_count_1=<strong>%d</strong> Ðктивний запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ activity.active_prs_count_n=<strong>%d</strong> Ðктивні запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -activity.merged_prs_count_1=Злитий запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -activity.merged_prs_count_n=Злиті запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +activity.merged_prs_count_1=Об'єднаний запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +activity.merged_prs_count_n=Об'єднані запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ activity.opened_prs_count_1=Запропонований запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -activity.opened_prs_count_n=Запропонованих запитів на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +activity.opened_prs_count_n=Запропоновані запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ activity.title.user_1=%d кориÑтувачем activity.title.user_n=%d кориÑтувачами activity.title.prs_1=%d Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ activity.title.prs_n=%d Запитів на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ activity.title.prs_merged_by=%s злито %s activity.title.prs_opened_by=%s запропоновано %s -activity.merged_prs_label=Злито +activity.merged_prs_label=Об'єднано activity.opened_prs_label=Запропоновано activity.active_issues_count_1=<strong>%d</strong> Ðктивна задача activity.active_issues_count_n=<strong>%d</strong> Ðктивні задачі activity.closed_issues_count_1=Закрита задача activity.closed_issues_count_n=Закриті задачі -activity.title.issues_1=%d Задач +activity.title.issues_1=%d Задача activity.title.issues_n=%d Задач activity.title.issues_closed_from=%s закрито %s activity.title.issues_created_by=%s Ñтворена(Ñ–) %s @@ -1443,97 +2018,131 @@ activity.new_issues_count_n=Ðові Задачі activity.new_issue_label=Відкриті activity.title.unresolved_conv_1=%d Ðезавершене Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ activity.title.unresolved_conv_n=%d Ðезавершених обговорень -activity.unresolved_conv_desc=СпиÑок вÑÑ–Ñ… Ñтарих задач Ñ– Pull Request'ів з недавньої активніÑтю, але ще не закритих або прийнÑтих. +activity.unresolved_conv_desc=Ці нещодавно змінені задачі Ñ– запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ‰Ðµ не вирішені. activity.unresolved_conv_label=Відкрити activity.title.releases_1=%d Реліз activity.title.releases_n=%d Релізів activity.title.releases_published_by=%s опубліковано %s activity.published_release_label=Опубліковано -activity.no_git_activity=У цей період не було здійÑнено жодних дій. -activity.git_stats_exclude_merges=Ðе враховуючи злиттÑ, +activity.no_git_activity=За цей період не було жодної активноÑті комітів. +activity.git_stats_exclude_merges=Ðе враховуючи об'єднаннÑ, activity.git_stats_author_1=%d автор activity.git_stats_author_n=%d автори -activity.git_stats_pushed_1=відправлено -activity.git_stats_pushed_n=відправлено +activity.git_stats_pushed_1=відправив(ла) +activity.git_stats_pushed_n=відправили activity.git_stats_commit_1=%d коміт -activity.git_stats_commit_n=%d коміти +activity.git_stats_commit_n=%d комітів activity.git_stats_push_to_branch=в %s та activity.git_stats_push_to_all_branches=до вÑÑ–Ñ… гілок. activity.git_stats_on_default_branch=Ðа %s, activity.git_stats_file_1=%d файл -activity.git_stats_file_n=%d файли -activity.git_stats_files_changed_1=змінено +activity.git_stats_file_n=%d файлів +activity.git_stats_files_changed_1=змінив(ла) activity.git_stats_files_changed_n=змінено activity.git_stats_additions=Ñ– були activity.git_stats_addition_1=%d добавка -activity.git_stats_addition_n=%d добавки +activity.git_stats_addition_n=%d додатків activity.git_stats_and_deletions=та activity.git_stats_deletion_1=%d видалений activity.git_stats_deletion_n=%d видалені +contributors.contribution_type.filter_label=Тип внеÑку: contributors.contribution_type.commits=Коміти +contributors.contribution_type.deletions=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ -settings.desc=У налаштуваннÑÑ… ви можете змінювати різні параметри цього Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ -settings.options=Репозиторій +settings.desc=У налаштуваннÑÑ… ви можете керувати параметрами Ñховища +settings.options=Сховище +settings.public_access=Публічний доÑтуп +settings.public_access.docs.everyone_read=Ð’ÑÑ– читають: вÑÑ– зареєÑтровані кориÑтувачі можуть читати розділ. Це також дозволÑÑ” кориÑтувачам Ñтворювати нові задачі/запити на злиттÑ. +settings.public_access.docs.everyone_write=Ð’ÑÑ– пишуть: вÑÑ– зареєÑтровані кориÑтувачі можуть вноÑити зміни до розділу. Тільки розділ Wiki підтримує цей дозвіл. settings.collaboration=Співавтори settings.collaboration.admin=ÐдмініÑтратор settings.collaboration.write=Ð—Ð°Ð¿Ð¸Ñ -settings.collaboration.read=Читати +settings.collaboration.read=Ð§Ð¸Ñ‚Ð°Ð½Ð½Ñ settings.collaboration.owner=ВлаÑник settings.collaboration.undefined=Ðе визначено settings.hooks=Веб-хуки settings.githooks=Git хуки settings.basic_settings=Базові Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ settings.mirror_settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð·ÐµÑ€ÐºÐ°Ð»Ð° -settings.mirror_settings.mirrored_repository=Віддзеркалений репозиторій +settings.mirror_settings.docs=Ðалаштуйте Ñховище на автоматичну Ñинхронізацію комітів, міток Ñ– гілок з іншим Ñховищем. +settings.mirror_settings.docs.disabled_pull_mirror.instructions=Ðалаштуйте ваш проєкт на автоматичне перенеÑÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñ–Ð², міток Ñ– гілок до іншого Ñховища. ÐžÑ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð·Ð¼Ñ–Ð½ з дзеркал було вимкнено адмініÑтратором вашого Ñайту. +settings.mirror_settings.docs.disabled_push_mirror.instructions=Ðалаштуйте Ñвій проєкт на автоматичне Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñ–Ð², міток Ñ– гілок з іншого Ñховища. +settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=Ðаразі це можна зробити лише в меню «Ðова міграціÑ». Ð”Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ¾Ð²Ð¾Ñ— інформації звернітьÑÑ Ð´Ð¾: +settings.mirror_settings.docs.no_new_mirrors=Ваше Ñховище віддзеркалює зміни до іншого Ñховища або з нього. Будь лаÑка, майте на увазі, що наразі ви не можете Ñтворювати нові дзеркала. +settings.mirror_settings.docs.can_still_use=Хоча ви не можете змінювати наÑвні дзеркала чи Ñтворювати нові, ви вÑе одно можете викориÑтовувати чинне дзеркало. +settings.mirror_settings.docs.doc_link_title=Як віддзеркалити Ñховища? +settings.mirror_settings.docs.doc_link_pull_section=розділ документації "Ð—Ð»Ð¸Ñ‚Ñ‚Ñ Ð· віддаленого Ñховища". +settings.mirror_settings.docs.pulling_remote_title=Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð· віддаленого Ñховища +settings.mirror_settings.mirrored_repository=Віддзеркалене Ñховища settings.mirror_settings.direction=ÐапрÑмок settings.mirror_settings.direction.pull=Pull settings.mirror_settings.direction.push=Push settings.mirror_settings.last_update=ОÑтаннє Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ -settings.mirror_settings.push_mirror.none=Ðе налаштовано дзеркало push -settings.mirror_settings.push_mirror.remote_url=URL віддаленого Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð°Ñ€Ñ–Ñ git -settings.mirror_settings.push_mirror.add=Додати Push дзеркало +settings.mirror_settings.push_mirror.remote_url=URL віддаленого Ñховища Git +settings.mirror_settings.push_mirror.edit_sync_time=Редагувати інтервал Ñинхронізації дзеркал settings.sync_mirror=Синхронізувати зараз settings.site=Веб-Ñайт settings.update_settings=Оновити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ -settings.branches.update_default_branch=Оновити гілку за замовчуваннÑм -settings.advanced_settings=Додаткові Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ -settings.wiki_desc=Увімкнути репозиторії Вікі -settings.use_internal_wiki=ВикориÑтовувати вбудовані Вікі -settings.use_external_wiki=ВикориÑтовувати зовнішні Вікі +settings.update_mirror_settings=Оновити параметри дзеркала +settings.branches.switch_default_branch=Змінити типову гілку +settings.branches.update_default_branch=Оновити типову гілку +settings.branches.add_new_rule=Додати нове правило +settings.advanced_settings=Розширені Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ +settings.wiki_desc=Увімкнути Вікі Ñховища +settings.use_internal_wiki=ВикориÑтовувати вбудовану Вікі +settings.default_wiki_branch_name=Ðазва типової гілки Вікі +settings.failed_to_change_default_wiki_branch=Ðе вдалоÑÑ Ð·Ð¼Ñ–Ð½Ð¸Ñ‚Ð¸ Ñтандартну гілку вікі. +settings.use_external_wiki=ВикориÑтовувати зовнішню Вікі settings.external_wiki_url=URL зовнішньої вікі -settings.external_wiki_url_error=Ð—Ð¾Ð²Ð½Ñ–ÑˆÐ½Ñ URL-адреÑа wiki не Ñ” допуÑтимою URL-адреÑою. -settings.external_wiki_url_desc=Відвідувачі будуть перенаправлені на URL-адреÑу, коли вони клацають по вкладці. +settings.external_wiki_url_error=Ð—Ð¾Ð²Ð½Ñ–ÑˆÐ½Ñ URL-адреÑа Вікі Ñ” недійÑною. +settings.external_wiki_url_desc=Відвідувачі перенаправлÑютьÑÑ Ð½Ð° URL-адреÑу зовнішньої вікі при натиÑканні вкладки. settings.issues_desc=Увімкнути відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ð·Ð°Ð´Ð°Ñ‡ в репозиторію settings.use_internal_issue_tracker=ВикориÑтовувати вбудовану ÑиÑтему відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ð·Ð°Ð´Ð°Ñ‡ settings.use_external_issue_tracker=ВикориÑтовувати зовнішню ÑиÑтему обліку задач settings.external_tracker_url=URL зовнішньої ÑиÑтеми відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ð·Ð°Ð´Ð°Ñ‡ -settings.external_tracker_url_error=URL зовнішнього баг-трекера не Ñ” допуÑтимою URL-адреÑою. -settings.external_tracker_url_desc=Відвідувачі перенаправлÑютьÑÑ Ð½Ð° зовнішню URL-адреÑу, коли натиÑкають вкладку 'Задачі'. +settings.external_tracker_url_error=URL-адреÑа зовнішнього трекера задач Ñ” недійÑною. +settings.external_tracker_url_desc=Відвідувачі перенаправлÑютьÑÑ Ð½Ð° URL-адреÑу зовнішнього трекера задач при натиÑканні на вкладку. settings.tracker_url_format=Формат URL зовнішнього трекера задач -settings.tracker_url_format_error=Ðеправильний формат URL-адреÑи зовнішнього баг-трекера. -settings.tracker_issue_style=Формат номеру Ð´Ð»Ñ Ð·Ð¾Ð²Ð½Ñ–ÑˆÐ½ÑŒÐ¾Ñ— ÑиÑтеми обліку задач +settings.tracker_url_format_error=Формат URL-адреÑи зовнішнього трекера проблем недійÑний. settings.tracker_issue_style.numeric=Цифровий -settings.tracker_issue_style.alphanumeric=Буквено-цифровий +settings.tracker_issue_style.alphanumeric=Ðлфавітно-цифровий +settings.tracker_issue_style.regexp=РегулÑрний вираз +settings.tracker_issue_style.regexp_pattern=Шаблон регулÑрного виразу +settings.tracker_issue_style.regexp_pattern_desc=Першу захоплену групу буде викориÑтано заміÑть <code>{index}</code>. settings.tracker_url_format_desc=ВикориÑтовуйте шаблони <code>{user}</code>, <code>{repo}</code> та <code>{index}</code> Ð´Ð»Ñ Ñ–Ð¼ÐµÐ½Ñ– кориÑтувача, репозиторію та номеру задічі. settings.enable_timetracker=Увімкнути відÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ñ‡Ð°Ñу -settings.allow_only_contributors_to_track_time=Враховувати тільки учаÑників розробки в підрахунку чаÑу +settings.allow_only_contributors_to_track_time=Лише відÑтежувати Ñ‡Ð°Ñ Ñпівавторів settings.pulls_desc=Увімкнути запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð² репозиторій settings.pulls.ignore_whitespace=Ігнорувати пробіл у конфліктах -settings.pulls.enable_autodetect_manual_merge=Увімкнути Ð°Ð²Ñ‚Ð¾Ð²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ñ€ÑƒÑ‡Ð½Ð¾Ð³Ð¾ Ð·Ð»Ð¸Ñ‚Ñ‚Ñ (Примітка: у деÑких оÑобливий випадках можуть виникнуть помилки) +settings.pulls.enable_autodetect_manual_merge=Увімкнути автоматичне Ð²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ñ€ÑƒÑ‡Ð½Ð¾Ð³Ð¾ об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ (Примітка: у деÑких оÑобливих випадках можуть виникати помилкові оцінки) settings.pulls.default_delete_branch_after_merge=ВидалÑти гілку запиту злиттÑ, коли його прийнÑто +settings.releases_desc=Увімкнути релізи Ñховища +settings.packages_desc=Увімкнути реєÑтр пакетів Ñховища +settings.projects_desc=Увімкнути проєкти +settings.projects_mode_desc=Режим проєктів (Ñкі типи проєктів показувати) +settings.projects_mode_repo=Тільки проєкти Ñховища +settings.projects_mode_owner=Тільки проєкти кориÑтувачів або організацій +settings.projects_mode_all=Ð’ÑÑ– проєкти +settings.actions_desc=Увімкнути дії Ñховища settings.admin_settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð´Ð¼Ñ–Ð½Ñ–Ñтратора -settings.admin_enable_health_check=Включити перевірки працездатноÑті репозиторію (git fsck) +settings.admin_enable_health_check=Увімкнути перевірку Ñтану Ñховища (git fsck) +settings.admin_code_indexer=ІндекÑатор коду +settings.admin_stats_indexer=ІндекÑатор ÑтатиÑтики коду +settings.admin_indexer_commit_sha=ОÑтанній індекÑований SHA +settings.admin_indexer_unindexed=Ðе індекÑовано +settings.reindex_button=Додати до черги на реіндекÑацію +settings.reindex_requested=Запит на переіндекÑацію settings.admin_enable_close_issues_via_commit_in_any_branch=Закрити задачу за допомогою коміта, зробленого не в головній гілці settings.danger_zone=Ðебезпечна зона -settings.new_owner_has_same_repo=Ðовий влаÑник вже має репозиторій з такою назвою. Будь лаÑка, виберіть інше ім'Ñ. -settings.convert=Перетворити на звичайний репозиторій -settings.convert_desc=Ви можете Ñконвертувати це дзеркало у звичайний репозиторій. Це не може бути ÑкаÑовано. +settings.new_owner_has_same_repo=Ðовий влаÑник вже має Ñховище з такою назвою. Будь лаÑка, виберіть іншу назву. +settings.convert=Перетворити на звичайне Ñховище +settings.convert_desc=Ви можете перетворити це дзеркало на звичайне Ñховище. Це неможливо ÑкаÑувати. settings.convert_notices_1=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð¿ÐµÑ€ÐµÑ‚Ð²Ð¾Ñ€Ð¸Ñ‚ÑŒ дзеркало у звичайний репозиторій Ñ– не може бути ÑкаÑована. settings.convert_confirm=Перетворити репозиторій -settings.convert_succeed=Репозиторій уÑпішно перетворений в звичайний. +settings.convert_succeed=Дзеркало було перетворено на звичайне Ñховище. settings.convert_fork=Перетворити на звичайний репозиторій settings.convert_fork_desc=Ви можете перетворити цей форк на звичайний репозиторій. Цю дію неможливо ÑкаÑувати. settings.convert_fork_notices_1=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð¿ÐµÑ€ÐµÑ‚Ð²Ð¾Ñ€Ð¸Ñ‚ÑŒ форк на звичайний репозиторій та не може бути ÑкаÑованою. @@ -1543,89 +2152,92 @@ settings.transfer=Передати новому влаÑнику settings.transfer.rejected=ПеренеÑÐµÐ½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ відхилено. settings.transfer.success=ПеренеÑÐµÐ½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ виконано. settings.transfer_abort=СкаÑувати перенеÑÐµÐ½Ð½Ñ -settings.transfer_abort_invalid=Ви не можете ÑкаÑувати неіÑнуюче перенеÑÐµÐ½Ð½Ñ Ñховища. -settings.transfer_desc=Передати репозиторій кориÑтувачеві або організації, де ви маєте права адмініÑтратора. -settings.transfer_form_title=Введіть ім'Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ñк підтвердженнÑ: -settings.transfer_in_progress=Ð’ даний Ñ‡Ð°Ñ Ð²Ñ–Ð´Ð±ÑƒÐ²Ð°Ñ”Ñ‚ÑŒÑÑ Ð¿ÐµÑ€ÐµÐ½ÐµÑеннÑ. Будь лаÑка, ÑкаÑуйте його, Ñкщо ви бажаєте перенеÑти цей репозиторій іншому кориÑтувачу. -settings.transfer_notices_1=- Ви втратите доÑтуп до репозиторіÑ, Ñкщо ви переведете його окремому кориÑтувачеві. -settings.transfer_notices_2=- Ви збережете доÑтуп, Ñкщо новим влаÑником Ñтане організаціÑ, влаÑником Ñкої ви Ñ”. -settings.transfer_notices_3=- Якщо репозиторій Ñ” приватним Ñ– передаєтьÑÑ Ð¾ÐºÑ€ÐµÐ¼Ð¾Ð¼Ñƒ кориÑтувачеві, Ñ†Ñ Ð´Ñ–Ñ Ð³Ð°Ñ€Ð°Ð½Ñ‚ÑƒÑ”, що кориÑтувач має хоча б дозвіл на Ñ‡Ð¸Ñ‚Ð°Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð°Ñ€Ñ–ÑŽ (Ñ– при необхідноÑті змінює права дозволів). +settings.transfer_abort_invalid=Ви не можете ÑкаÑувати перенеÑÐµÐ½Ð½Ñ Ð½ÐµÑ–Ñнуючого Ñховища. +settings.transfer_abort_success=ПеренеÑÐµÐ½Ð½Ñ Ñховища до %s уÑпішно ÑкаÑовано. +settings.transfer_desc=Передати це Ñховище кориÑтувачеві або організації, Ð´Ð»Ñ Ñкої ви маєте права адмініÑтратора. +settings.transfer_form_title=Введіть назву Ñховища Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ: +settings.transfer_in_progress=Ðаразі триває передача. Будь лаÑка, ÑкаÑуйте його, Ñкщо ви хочете передати це Ñховище іншому кориÑтувачеві. +settings.transfer_notices_1=- Ви втратите доÑтуп до Ñховища, Ñкщо передаÑте його окремому кориÑтувачеві. +settings.transfer_notices_2=- Ви збережете доÑтуп до Ñховища, Ñкщо передаÑте його організації, Ñкою ви (Ñпів)володієте. +settings.transfer_notices_3=- Якщо Ñховище Ñ” приватним Ñ– передаєтьÑÑ Ð¾ÐºÑ€ÐµÐ¼Ð¾Ð¼Ñƒ кориÑтувачеві, Ñ†Ñ Ð´Ñ–Ñ Ð³Ð°Ñ€Ð°Ð½Ñ‚ÑƒÑ”, що кориÑтувач має принаймні права на Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ (Ñ– змінює ці права, Ñкщо необхідно). settings.transfer_owner=Ðовий влаÑник settings.transfer_perform=ЗдіÑнити перенеÑÐµÐ½Ð½Ñ -settings.transfer_started=`Цей репозиторій чекає Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð¿ÐµÑ€ÐµÐ½ÐµÑÐµÐ½Ð½Ñ Ð²Ñ–Ð´ "%s"` -settings.transfer_succeed=Репозиторій був перенеÑений. +settings.transfer_started=Це Ñховище було позначено Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‡Ñ– та очікує на Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð²Ñ–Ð´ «%s» +settings.transfer_succeed=Сховище перенеÑено. settings.signing_settings=Параметри перевірки підпиÑу -settings.trust_model=Модель довіри Ð´Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñу -settings.trust_model.default=Модель довіри за замовчуваннÑм -settings.trust_model.default.desc=ВикориÑтовувати модель довіри репозиторію за замовчуваннÑм Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñайту. +settings.trust_model=Модель довіри до підпиÑу +settings.trust_model.default=Типова модель довіри +settings.trust_model.default.desc=ВикориÑтовувати типову модель довіри до Ñховища Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Ñайту. settings.trust_model.collaborator=Співавтор settings.trust_model.collaborator.long=Співавтор: підпиÑи довіри від Ñпівавторів -settings.trust_model.collaborator.desc=ДопуÑтимі підпиÑи Ñпівавторів цього репозиторію буде позначано Ñк "довірені" - (Ñкщо вони відповідають комітеру чи ні). Ð’ іншому випадку дійÑні підпиÑи будуть позначені Ñк «ненадійні», Ñкщо Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ñпівпадає з комітером Ñ– «невідповідні», Ñкщо ні. -settings.trust_model.committer=Коммітер -settings.trust_model.committer.long=Коммітер: ДовірÑти підпиÑам Ñкі відповідають комітерам (Так Ñк Ñ– на GitHub, Ñ– змуÑить підпиÑати коміти Gitea в ÑкоÑті коммітера) -settings.trust_model.collaboratorcommitter=Співавтор+Коммітер -settings.trust_model.collaboratorcommitter.long=Співавтор+Коммітер: ДовірÑти підпиÑам від Ñпівавторів, Ñкі відповідають комітеру -settings.trust_model.collaboratorcommitter.desc=ДопуÑтимі підпиÑи Ñпівавторів цього репозиторію будуть позначатиÑÑ Ñк "довірені", Ñкщо вони відповідають комітеру. Ð’ іншому випадку дійÑні підпиÑи будуть позначені Ñк «ненадійні», Ñкщо Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ñпівпадає з комітером Ñ– Ñк «невідповіді» в іншому випадку. Це змуÑить Gitea бути відміченим Ñк комітер піÑÐ»Ñ Ð¿Ñ–Ð´Ð¿Ð¸ÑÐ°Ð½Ð½Ñ Ñ„Ð°ÐºÑ‚Ð¸Ñ‡Ð½Ð¸Ð¼ комітером, позначеним Co-Authored-By: Ñ– Co-Committed-By: прикріпленим до комміту. Типовий ключ Gitea повинен відповідати кориÑтувачу в базі даних. -settings.wiki_delete=Видалити вікі-дані -settings.wiki_delete_desc=Будьте уважні! Як тільки ви видалите Вікі - шлÑху назад не буде. -settings.wiki_delete_notices_1=- Це назавжди знищить Ñ– відключить wiki Ð´Ð»Ñ %s. -settings.confirm_wiki_delete=Видалити Вікі-дані -settings.wiki_deletion_success=Дані wiki були видалені. +settings.trust_model.collaborator.desc=ДійÑні підпиÑи Ñпівавторів цього Ñховища будуть позначені Ñк «довірені» - (незалежно від того, чи збігаютьÑÑ Ð²Ð¾Ð½Ð¸ з підпиÑом комітера чи ні). Ð’ іншому випадку дійÑні підпиÑи будуть позначені Ñк «недійÑні», Ñкщо Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð·Ð±Ñ–Ð³Ð°Ñ”Ñ‚ÑŒÑÑ Ð· комітером Ñ– «невідповідні», Ñкщо ні. +settings.trust_model.committer=Комітер +settings.trust_model.committer.long=Комітер: ДовірÑти підпиÑам, Ñкі відповідають комітерам (це відповідає GitHub Ñ– змуÑить підпиÑані Gitea коміти мати Gitea в ÑкоÑті комітера) +settings.trust_model.collaboratorcommitter=Співавтор+Комітер +settings.trust_model.collaboratorcommitter.long=Співавтор+Комітер: ДовірÑти підпиÑам від Ñпівавторів, Ñкі відповідають комітеру +settings.trust_model.collaboratorcommitter.desc=ДійÑні підпиÑи Ñпівавторів цього Ñховища будуть позначені Ñк «довірені», Ñкщо вони збігаютьÑÑ Ð· комітером. Ð’ іншому випадку, дійÑні підпиÑи будуть позначені Ñк «недійÑні», Ñкщо Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð·Ð±Ñ–Ð³Ð°Ñ”Ñ‚ÑŒÑÑ Ð· комітером, Ñ– «невідповідні» у протилежному випадку. Це призведе до того, що Gitea буде позначено комітером у підпиÑаних комітах, а Ñправжній комітер буде позначений Ñк Co-Author-By: та Co-Committed-By: у трейлері коміта. Типовий ключ Gitea має відповідати кориÑтувачеві у базі даних. +settings.wiki_delete=Видалити дані Вікі +settings.wiki_delete_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… Вікі Ñховища Ñ” оÑтаточним Ñ– не може бути ÑкаÑоване. +settings.wiki_delete_notices_1=- Це назавжди видалить Ñ– вимкне вікі Ñховища Ð´Ð»Ñ %s. +settings.confirm_wiki_delete=Видалити дані Вікі +settings.wiki_deletion_success=Дані Вікі видалено. settings.delete=Видалити цей репозиторій -settings.delete_desc=Будьте уважні! Як тільки ви видалите репозиторій - шлÑху назад не буде. -settings.delete_notices_1=- Цю операцію <strong>ÐЕ МОЖÐÐ</strong> відмінити. -settings.delete_notices_2=- Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð¾Ñтаточно видалить <strong>%s</strong> репозиторій, включаючи код, задачі, коментарі, вікі та Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñпівавторів. +settings.delete_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñховища Ñ” оÑтаточним Ñ– не може бути ÑкаÑоване. +settings.delete_notices_1=- Цю операцію <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати. +settings.delete_notices_2=- Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð½Ð°Ð·Ð°Ð²Ð¶Ð´Ð¸ видалить Ñховище <strong>%s</strong>, включно з кодом, проблемами, коментарÑми, даними вікі та налаштуваннÑми Ñпівавторів. settings.delete_notices_fork_1=- Ð’ÑÑ– форки Ñтануть незалежними репозиторіÑми піÑÐ»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ. settings.deletion_success=Репозиторій уÑпішно видалено. -settings.update_settings_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–ÑŽ було оновлено. +settings.update_settings_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñховища оновлено. +settings.update_settings_no_unit=Сховище повинно дозволÑти хоча б ÑкуÑÑŒ взаємодію. settings.confirm_delete=Видалити репозиторій settings.add_collaborator=Додати Ñпівавтора settings.add_collaborator_success=Додано Ñпівавтора. -settings.add_collaborator_inactive_user=Ðе можливо додати неактивного кориÑтувача ÑкоÑті Ñпівавтора. +settings.add_collaborator_inactive_user=Ðеможливо додати неактивного кориÑтувача Ñк Ñпівавтора. +settings.add_collaborator_owner=Ðеможливо додати влаÑника Ñк Ñпівавтора. settings.add_collaborator_duplicate=Співавтора уже додано до цього репозиторію. settings.delete_collaborator=Видалити settings.collaborator_deletion=Видалити Ñпівавтора -settings.collaborator_deletion_desc=Цей кориÑтувач більше не матиме доÑтупу Ð´Ð»Ñ Ñпільної роботи в цьому репозиторії піÑÐ»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ. Ви хочете продовжити? -settings.remove_collaborator_success=Співавтор видалений. +settings.collaborator_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñпівавтора призведе до Ð²Ñ–Ð´ÐºÐ»Ð¸ÐºÐ°Ð½Ð½Ñ Ð¹Ð¾Ð³Ð¾ доÑтупу до цього Ñховища. Продовжити? +settings.remove_collaborator_success=Співавтора видалено. settings.org_not_allowed_to_be_collaborator=Організації не можуть бути додані Ñк Ñпівавтори. settings.change_team_access_not_allowed=Зміна доÑтупу команди до репозитарію обмежена влаÑником організації -settings.team_not_in_organization=Команда та репозитарій мають привÑзки до різних організацій +settings.team_not_in_organization=Команда не належить до тієї ж організації, що й Ñховище settings.teams=Команди -settings.add_team=Додати Команду -settings.add_team_duplicate=Команда вже має привÑзку до репозитарію -settings.add_team_success=Команда отримала доÑтуп до репозиторію. -settings.change_team_permission_tip=Дозволи команди вÑтановлюютьÑÑ Ð½Ð° Ñторінці налаштувань команди та не можуть бути заданими Ð´Ð»Ñ ÐºÐ¾Ð¶Ð½Ð¾Ð³Ð¾ з репозиторіїв окремо +settings.add_team=Додати команду +settings.add_team_duplicate=Команда вже має Ñховище +settings.add_team_success=Команда тепер має доÑтуп до Ñховища. +settings.change_team_permission_tip=Дозвіл команди вÑтановлюєтьÑÑ Ð½Ð° Ñторінці налаштувань команди Ñ– не може бути змінений Ð´Ð»Ñ ÐºÐ¾Ð¶Ð½Ð¾Ð³Ð¾ Ñховища settings.delete_team_tip=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° має доÑтуп до вÑÑ–Ñ… репозиторіїв та не може бути видалена -settings.remove_team_success=ДоÑтуп команди до репозиторію видалений. +settings.remove_team_success=ДоÑтуп команди до Ñховища видалено. settings.add_webhook=Додати веб-хук -settings.add_webhook.invalid_channel_name=Ðазва каналу Webhook не може бути порожньою Ñ– не може міÑтити лише Ñимвол #. -settings.hooks_desc=Веб-хуки автоматично робить HTTP POST-запити на Ñервер, коли відбуваютьÑÑ Ð¿ÐµÐ²Ð½Ñ– події Gitea. ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ в <a target="_blank" rel="noopener" href="%s"> інÑтрукції по викориÑтанню web-хуків </a>. +settings.add_webhook.invalid_channel_name=Ðазва каналу веб-хука не може бути порожньою Ñ– міÑтити лише Ñимвол #. +settings.hooks_desc=Веб-хуки автоматично роблÑть HTTP POST запити до Ñервера, коли відбуваютьÑÑ Ð¿ÐµÐ²Ð½Ñ– події Gitea. ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ в <a target="_blank" rel="noopener noreferrer" href="%s">інÑтрукції по викориÑтанню веб-хуків</a>. settings.webhook_deletion=Видалити веб-хук -settings.webhook_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ веб-хука призведе до Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²Ñієї пов'Ñзаної з ним інформації, включаючи Ñ–Ñторію. Бажаєте продовжити? -settings.webhook_deletion_success=Webhook видалено. +settings.webhook_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÐµÐ±-хука видалÑÑ” його Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ‚Ð° Ñ–Ñторію доÑтавки. Продовжити? +settings.webhook_deletion_success=Веб-хук видалено. settings.webhook.test_delivery=Перевірити доÑтавку -settings.webhook.test_delivery_desc=Перевірте цей веб-хук з підробленою подією. +settings.webhook.test_delivery_desc=Перевірте цей веб-хук з фальшивою подією. settings.webhook.request=Запит settings.webhook.response=Відповідь settings.webhook.headers=Заголовки settings.webhook.payload=ЗміÑÑ‚ settings.webhook.body=Тіло -settings.githook_edit_desc=Якщо хук неактивний, буде предÑтавлено зразок зміÑту. Порожнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ñƒ цьому полі призведе до Ð²Ð¸Ð¼ÐºÐ½ÐµÐ½Ð½Ñ Ñ…ÑƒÐºÑƒ. -settings.githook_name=Ім'Ñ Ñ…ÑƒÐºÑƒ +settings.webhook.replay.description=Повторити цей веб-хук. +settings.githook_edit_desc=Якщо хук неактивний, буде показано зразок вміÑту. Якщо залишити вміÑÑ‚ порожнім, хук буде вимкнено. +settings.githook_name=Ðазва хуку settings.githook_content=ЗміÑÑ‚ хука settings.update_githook=Оновити хук -settings.add_webhook_desc=Gitea буде відправлÑти <code>POST</code> запити на вказану URL адреÑу, з інформацією про події, що відбуваютьÑÑ. Подробиці на Ñторінці <a target="_blank" rel="noopener" href="%s"> інÑтрукції по викориÑтанню web-хуків </a>. +settings.add_webhook_desc=Gitea надішле запити <code>POST</code> із зазначеним типом зміÑту на цільову URL-адреÑу. ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ в <a target="_blank" rel="noopener noreferrer" href="%s">інÑтрукції по викориÑтанню веб-хуків</a>. settings.payload_url=Цільова URL-адреÑа settings.http_method=Метод HTTP settings.content_type=Тип зміÑту settings.secret=Секрет settings.slack_username=Ім'Ñ ÐºÑ€Ð¸Ñтувача -settings.slack_icon_url=URL іконки +settings.slack_icon_url=URL піктограми settings.slack_color=Колір settings.discord_username=Ім'Ñ ÐºÑ€Ð¸Ñтувача -settings.discord_icon_url=URL іконки +settings.discord_icon_url=URL піктограми settings.event_desc=Тригер: -settings.event_push_only=Push події settings.event_send_everything=Ð’ÑÑ– події settings.event_choose=ВлаÑні події… settings.event_header_repository=Події репозиторію @@ -1634,41 +2246,44 @@ settings.event_create_desc=Гілку або тег Ñтворено. settings.event_delete=Видалити settings.event_delete_desc=Гілку або мітку було видалено. settings.event_fork=Форк -settings.event_fork_desc=Репозиторій було форкнуто. settings.event_wiki=Вікі +settings.event_statuses=СтатуÑи +settings.event_statuses_desc=Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ оновлено з API. settings.event_release=Реліз -settings.event_release_desc=Реліз опублікований, оновлений або видалений з репозиторіÑ. +settings.event_release_desc=Реліз опубліковано, оновлено або видалено зі Ñховища. settings.event_push=Push -settings.event_push_desc=Git push до репозиторію. settings.event_repository=Репозиторій settings.event_repository_desc=Репозиторій Ñтворений або видалено. settings.event_header_issue=Події задачі settings.event_issues=Задачі -settings.event_issues_desc=Задача відкрита, закрита, повторно відкрита або відредагована. -settings.event_issue_assign=Задача прив'Ñзана +settings.event_issues_desc=Задачу відкрито, закрито, повторно відкрито, відредаговано або видалено. +settings.event_issue_assign=Задачу призначено settings.event_issue_assign_desc=Задачу призначено або ÑкаÑовано. -settings.event_issue_label=Задача з міткою settings.event_issue_label_desc=Мітки задачі оновлено або видалено. -settings.event_issue_milestone=Задача з етапом -settings.event_issue_milestone_desc=Задача призначена на етап або видалена з етапу. settings.event_issue_comment=Коментар задачі settings.event_issue_comment_desc=Коментар задачі Ñтворено, видалено чи відредаговано. settings.event_header_pull_request=Події запиту Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -settings.event_pull_request=Запити до Ð·Ð»Ð¸Ñ‚Ñ‚Ñ -settings.event_pull_request_desc=Запит до Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ð¾, закрито, перевідкрито або відредаговано. +settings.event_pull_request=Запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ +settings.event_pull_request_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ð¾, закрито, повторно відкрито, відредаговано або видалено. settings.event_pull_request_assign=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¾ -settings.event_pull_request_assign_desc=Запит про Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¾ або ÑкаÑовано. +settings.event_pull_request_assign_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¾ або ÑкаÑовано. settings.event_pull_request_label=Запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð° мітка settings.event_pull_request_label_desc=Мітка запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð° або очищена. -settings.event_pull_request_milestone=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ð¹ на етап -settings.event_pull_request_milestone_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ð¹ на етап або видалений з етапу. -settings.event_pull_request_comment=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¾ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð¾Ð²Ð°Ð½Ð¸Ð¹ +settings.event_pull_request_milestone=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð¾Ð´Ð°Ð½Ð¾ до етапу +settings.event_pull_request_milestone_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð¾Ð´Ð°Ð½Ð¾ до етапу або видалено з етапу. +settings.event_pull_request_comment=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¾ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð¾Ð²Ð°Ð½Ð¾ settings.event_pull_request_comment_desc=Коментар запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñтворено, відредаговано чи видалено. settings.event_pull_request_review=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ€ÐµÑ†ÐµÐ½Ð·Ð¾Ð²Ð°Ð½Ð¾ -settings.event_pull_request_review_desc=Коментар запиту до Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð¸Ð¹, відхилений або рецензований. +settings.event_pull_request_review_desc=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð¾, відхилено або прокоментовано. settings.event_pull_request_sync=Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ ÑинхронізуєтьÑÑ settings.event_pull_request_sync_desc=Запит до Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñинхронізовано. +settings.event_header_workflow=Події робочого процеÑу +settings.event_workflow_run=Запущений робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ +settings.event_workflow_run_desc=Запущений робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Gitea в черзі, в очікуванні, в процеÑÑ– Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ завершений. +settings.event_workflow_job=Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ñ€Ð¾Ð±Ð¾Ñ‡Ð¾Ð³Ð¾ процеÑу +settings.event_package=Пакет settings.branch_filter=Фільтр гілок +settings.authorization_header=Заголовок авторизації settings.active=Ðктивний settings.active_helper=Інформацію про викликані події буде надіÑлано за цією веб-хук URL-адреÑою. settings.add_hook_success=Веб-хук було додано. @@ -1680,71 +2295,109 @@ settings.hook_type=Тип хука settings.slack_token=Токен settings.slack_domain=Домен settings.slack_channel=Канал -settings.deploy_keys=Ключі Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ -settings.add_deploy_key=Додати ключ Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ -settings.deploy_key_desc=Ключі Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупні тільки Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ. Це не те ж Ñаме що Ñ– SSH-ключі аккаунта. +settings.web_hook_name_gitea=Gitea +settings.web_hook_name_gogs=Gogs +settings.web_hook_name_slack=Slack +settings.web_hook_name_discord=Discord +settings.web_hook_name_dingtalk=DingTalk +settings.web_hook_name_telegram=Telegram +settings.web_hook_name_matrix=Matrix +settings.web_hook_name_msteams=Microsoft Teams +settings.web_hook_name_feishu_or_larksuite=Feishu / Lark Suite +settings.web_hook_name_feishu=Feishu +settings.web_hook_name_larksuite=Lark Suite +settings.web_hook_name_wechatwork=WeCom (Wechat Work) +settings.web_hook_name_packagist=Packagist +settings.packagist_username=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача Packagist +settings.packagist_api_token=Токен API +settings.deploy_keys=Ключі Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ +settings.add_deploy_key=Додати ключ Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ +settings.deploy_key_desc=Ключі Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð¼Ð°ÑŽÑ‚ÑŒ доÑтуп до Ñховища лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ. settings.is_writable=Увімкнути доÑтуп Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñу -settings.is_writable_info=Чи може цей ключ бути викориÑтаний Ð´Ð»Ñ Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ <strong>push</strong> в репозиторій? Ключі Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð·Ð°Ð²Ð¶Ð´Ð¸ мають доÑтуп на pull. -settings.no_deploy_keys=Ви не додавали ключі розгортаннÑ. +settings.no_deploy_keys=Ключів Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ñ‰Ðµ немає. settings.title=Заголовок settings.deploy_key_content=ЗміÑÑ‚ -settings.key_been_used=ЗміÑÑ‚ ключа Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð²Ð¶Ðµ викориÑтовуєтьÑÑ. -settings.key_name_used=Ключ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð· таким заголовком вже Ñ–Ñнує. -settings.deploy_key_deletion=Видалити ключ Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ -settings.deploy_key_deletion_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ»ÑŽÑ‡Ð° розгортки унеможливить доÑтуп до Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ð· його допомогою. Ви впевнені? +settings.key_been_used=Ключ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð· ідентичним вміÑтом вже викориÑтовуєтьÑÑ. +settings.key_name_used=Ключ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð· такою ж назвою вже Ñ–Ñнує. +settings.deploy_key_deletion=Видалити ключ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ +settings.deploy_key_deletion_desc=Ð’Ð¸Ð»ÑƒÑ‡ÐµÐ½Ð½Ñ ÐºÐ»ÑŽÑ‡Ð° Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¸Ð·Ð²ÐµÐ´Ðµ до Ð²Ñ–Ð´ÐºÐ»Ð¸ÐºÐ°Ð½Ð½Ñ Ð¹Ð¾Ð³Ð¾ доÑтупу до цього Ñховища. Продовжити? settings.deploy_key_deletion_success=Ключі Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð±ÑƒÐ»Ð¾ видалено. settings.branches=Гілки settings.protected_branch=ЗахиÑÑ‚ гілки -settings.protected_branch_can_push=Дозволити push? -settings.protected_branch_can_push_yes=Ви можете виконувати push -settings.protect_this_branch=ЗахиÑтити цю гілку -settings.protect_this_branch_desc=Запобігає видаленню гілки та обмежує Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð² ній push та злиттÑ. +settings.protected_branch.save_rule=Зберегти правило +settings.protected_branch.delete_rule=Видалити правило +settings.branch_protection=ЗахиÑÑ‚ гілки '<b>%s</b>' +settings.protect_this_branch=Увімкнути захиÑÑ‚ гілок settings.protect_disable_push=Заборонити Push settings.protect_disable_push_desc=Ð”Ð»Ñ Ñ†Ñ–Ñ”Ñ— гілки буде заборонено Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ push. settings.protect_enable_push=Дозволити Push settings.protect_enable_push_desc=Будь-хто із правом запиÑу зможе виконувати push Ð´Ð»Ñ Ñ†Ñ–Ñ”Ñ— гілки (за виключеннÑм force push). +settings.protect_enable_merge=Увімкнути Ð¾Ð±â€™Ñ”Ð´Ð½Ð°Ð½Ð½Ñ settings.protect_check_status_contexts=Увімкнути перевірку Ñтану +settings.protect_status_check_patterns=Шаблони перевірки Ñтану: +settings.protect_status_check_patterns_desc=Введіть шаблони, щоб вказати, Ñкі перевірки Ñтану повинні пройти гілки, перш ніж Ñ—Ñ… буде об'єднано у гілку, що відповідає цьому правилу. Кожен Ñ€Ñдок визначає шаблон. Шаблони не можуть бути порожніми. settings.protect_check_status_contexts_list=Перевірки ÑтатуÑу знайдено Ð´Ð»Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð°Ñ€Ñ–ÑŽ за минулий тиждень +settings.protect_status_check_matched=Збіг +settings.protect_invalid_status_check_pattern=ÐедійÑний шаблон перевірки Ñтану: "%s". +settings.protect_no_valid_status_check_patterns=Ðемає дійÑних шаблонів перевірки Ñтану. settings.protect_required_approvals=Ðеобхідно ÑхваленнÑ: settings.dismiss_stale_approvals=Відхилити заÑтарілі Ð¿Ð¾Ð³Ð¾Ð´Ð¶ÐµÐ½Ð½Ñ settings.dismiss_stale_approvals_desc=Коли нові коміти що змінюють вміÑÑ‚ пулл-запиту відправлÑютьÑÑ Ð² гілку, Ñтарі Ð¿Ð¾Ð³Ð¾Ð´Ð¶ÐµÐ½Ð½Ñ Ð±ÑƒÐ´ÑƒÑ‚ÑŒ відхилені. +settings.ignore_stale_approvals=Ігнорувати заÑтарілі Ð·Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ settings.require_signed_commits=Потрібно підпиÑані коміти settings.require_signed_commits_desc=ВідхилÑти push до цієї гілки, Ñкщо вони не підпиÑані або Ð¿Ñ–Ð´Ð¿Ð¸Ñ Ð½ÐµÐ¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ перевірити. +settings.protect_branch_name_pattern=Шаблон назви захищених гілок +settings.protect_branch_name_pattern_desc=Шаблони назв захищених гілок. ПереглÑньте <a href="https://github.com/gobwas/glob">документацію</a> щодо ÑинтакÑиÑу шаблону. Приклади: main, release/** +settings.protect_patterns=Шаблони +settings.protect_protected_file_patterns=Шаблони захищених файлів (розділені крапками з комою ';'): +settings.protect_unprotected_file_patterns=Шаблони незахищених файлів (розділені крапкою з комою ';'): settings.add_protected_branch=Увімкнути захиÑÑ‚ settings.delete_protected_branch=Вимкнути захиÑÑ‚ +settings.update_protect_branch_success=Оновлено захиÑÑ‚ гілок Ð´Ð»Ñ Ð¿Ñ€Ð°Ð²Ð¸Ð»Ð° "%s". +settings.remove_protected_branch_success=Видалено захиÑÑ‚ гілок Ð´Ð»Ñ Ð¿Ñ€Ð°Ð²Ð¸Ð»Ð° "%s". +settings.remove_protected_branch_failed=Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ правило захиÑту гілки "%s. settings.protected_branch_deletion_desc=Будь-Ñкий кориÑтувач з дозволами на Ð·Ð°Ð¿Ð¸Ñ Ð·Ð¼Ð¾Ð¶Ðµ виконувати push в цю гілку. Ви впевнені? -settings.block_rejected_reviews=Блокувати Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸ відкидаючих рецензіÑÑ… -settings.block_rejected_reviews_desc=Ð—Ð»Ð¸Ñ‚Ñ‚Ñ Ð±ÑƒÐ´Ðµ недоÑтупним, Ñкщо Ñ” запит змін від офіційних рецензентів, навіть за наÑвноÑті доÑтатньої кількоÑті Ñхвалень. -settings.block_on_official_review_requests=Блокувати Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð¿Ñ€Ð¸ запиті на офіціальний розглÑд +settings.block_rejected_reviews=Блокувати об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ñкщо рецензії відхилено +settings.block_rejected_reviews_desc=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ неможливим, Ñкщо офіційні рецензенти вимагають внеÑÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½, навіть Ñкщо Ñ” доÑÑ‚Ð°Ñ‚Ð½Ñ ÐºÑ–Ð»ÑŒÐºÑ–Ñть Ñхвалень. +settings.block_on_official_review_requests=Блокувати об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð·Ð° офіційними запитами на Ñ€ÐµÑ†ÐµÐ½Ð·ÑƒÐ²Ð°Ð½Ð½Ñ settings.block_on_official_review_requests_desc=ÐžÐ±â€™Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð½ÐµÐ¼Ð¾Ð¶Ð»Ð¸Ð²Ðµ, коли воно має офіційні запити на розглÑд, навіть Ñкщо доÑтатньо Ñхвалень. -settings.block_outdated_branch=Блокувати злиттÑ, Ñкщо запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ñтарів -settings.block_outdated_branch_desc=Ð—Ð»Ð¸Ñ‚Ñ‚Ñ Ð±ÑƒÐ´Ðµ неможливим, коли головна гілка позаду оÑновної. -settings.default_branch_desc=Головна гілка Ñ” 'базовою' Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ репозиторіÑ, на Ñку за замовчуваннÑм ÑпрÑмовані вÑÑ– запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ– Ñка Ñ” обличчÑм вашого репозиторіÑ. Перше, що побачить відвідувач - це зміÑÑ‚ головної гілки. Виберіть Ñ—Ñ— з уже Ñ–Ñнуючих: +settings.block_outdated_branch=Блокувати об'єднаннÑ, Ñкщо запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð·Ð°Ñтарів +settings.block_outdated_branch_desc=Об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ неможливим, Ñкщо головна гілка позаду оÑновної. +settings.block_admin_merge_override=ÐдмініÑтратори повинні дотримуватиÑÑ Ð¿Ñ€Ð°Ð²Ð¸Ð» захиÑту гілки +settings.block_admin_merge_override_desc=ÐдмініÑтратори повинні дотримуватиÑÑ Ð¿Ñ€Ð°Ð²Ð¸Ð» захиÑту гілки Ñ– не можуть Ñ—Ñ… обійти. +settings.default_branch_desc=Обрати типову гілку Ñховища Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñ–Ð² на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ– комітів: +settings.merge_style_desc=Стилі об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ +settings.default_merge_style_desc=Типовий Ñтиль об'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ settings.choose_branch=Оберіть гілку… settings.no_protected_branch=Ðемає захищених гілок. settings.edit_protected_branch=Редагувати +settings.protected_branch_required_rule_name=Ðеобхідна назва правила +settings.protected_branch_duplicate_rule_name=Ðазва правила вже Ñ–Ñнує settings.protected_branch_required_approvals_min=ЧиÑло необхідних Ñхвалень не може бути від'ємним. settings.tags=Мітки settings.tags.protection=ЗахиÑÑ‚ мітки -settings.tags.protection.pattern=Шаблон тега +settings.tags.protection.pattern=Шаблон мітки settings.tags.protection.allowed=Дозволено settings.tags.protection.allowed.users=Дозволені кориÑтувачі settings.tags.protection.allowed.teams=Дозволені команди settings.tags.protection.allowed.noone=Ðіхто -settings.tags.protection.create=ЗахиÑтна мітка +settings.tags.protection.create=ЗахиÑтити мітку settings.tags.protection.none=Там не немає захищених міток. +settings.tags.protection.pattern.description=Ви можете викориÑтовувати єдине ім’Ñ, глобальний шаблон або регулÑрний вираз, щоб відповідати кільком міткам. Докладніше читайте в <a target="_blank" rel="noopener" href="https://docs.gitea.com/usage/protected-tags">поÑібнику із захищених міток</a>. settings.bot_token=Токен Ð´Ð»Ñ Ð±Ð¾Ñ‚Ð° -settings.chat_id=Чат ID +settings.chat_id=ID чату settings.matrix.homeserver_url=URL домашньої Ñторінки -settings.matrix.room_id=Ðомер кімнати +settings.matrix.room_id=ID кімнати settings.matrix.message_type=Тип Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ -settings.archive.button=Ðрхівний репозиторій +settings.visibility.public.button=Зробити публічним settings.archive.header=Відправити репозиторій в архів -settings.archive.success=Репозиторію уÑпішно приÑвоєно ÑÑ‚Ð°Ñ‚ÑƒÑ Ð°Ñ€Ñ…Ñ–Ð²Ð½Ð¾Ð³Ð¾. -settings.archive.error=СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при Ñпробі архівувати репозиторій. Докладнішу інформацію див. у журналі. -settings.archive.error_ismirror=Ðеможливо архівувати дзеркальний репозиротрій. +settings.archive.success=Сховище уÑпішно заархівовано. +settings.archive.error=СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° при Ñпробі архівувати репозиторій. Докладнішу інформацію дивітьÑÑ Ñƒ журналі. +settings.archive.error_ismirror=Ðеможливо архівувати дзеркальне Ñховище. settings.archive.branchsettings_unavailable=Параметри гілки не доÑтупні, Ñкщо репозиторій архівний. settings.archive.tagsettings_unavailable=Параметри міток недоÑтупні, Ñкщо репозиторій архівний. +settings.unarchive.header=Розархівувати це Ñховище +settings.unarchive.success=Сховище уÑпішно розархівовано. settings.update_avatar_success=Ðватар репозиторію оновлений. settings.lfs=LFS settings.lfs_filelist=Файли LFS, Ñкі зберігаютьÑÑ Ð² цьому репозиторії @@ -1756,9 +2409,9 @@ settings.lfs_delete=Видалити файл LFS з OID %s settings.lfs_delete_warning=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñƒ LFS може Ñпричинити помилки "Об'єкт не Ñ–Ñнує" під Ñ‡Ð°Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸. Ви впевнені? settings.lfs_findpointerfiles=Знайти файли-поÑÐ¸Ð»Ð°Ð½Ð½Ñ settings.lfs_locks=Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ -settings.lfs_invalid_locking_path=ÐеприпуÑтимий шлÑÑ…: %s +settings.lfs_invalid_locking_path=ÐедійÑний шлÑÑ…: %s settings.lfs_invalid_lock_directory=Ðе можливо заблокувати каталог: %s -settings.lfs_lock_already_exists=Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¶Ðµ викориÑтовуєтьÑÑ: %s +settings.lfs_lock_already_exists=Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð¶Ðµ Ñ–Ñнує: %s settings.lfs_lock=Блокувати settings.lfs_lock_path=ШлÑÑ… до файлу Ð´Ð»Ñ Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ... settings.lfs_locks_no_locks=ВідÑутнє Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ @@ -1798,7 +2451,7 @@ diff.stats_desc_file=%d змін: %d доповнень та %d видалень diff.bin=BIN diff.bin_not_shown=Бінарний файл не відображаєтьÑÑ. diff.view_file=ПереглÑнути файл -diff.file_before=Перед +diff.file_before=До diff.file_after=ПіÑÐ»Ñ diff.file_image_width=Ширина diff.file_image_height=ВиÑота @@ -1810,6 +2463,7 @@ diff.show_more=Показати більше diff.load=Завантажити різницю diff.generated=згенерований diff.vendored=Ñторонній +diff.comment.add_line_comment=Додати проÑтий коментар diff.comment.placeholder=Залишити коментар diff.comment.add_single_comment=Додати проÑтий коментар diff.comment.add_review_comment=Додати коментар @@ -1817,81 +2471,130 @@ diff.comment.start_review=Розпочати рецензію diff.comment.reply=Відповідь diff.review=Ð ÐµÑ†ÐµÐ½Ð·Ñ–Ñ diff.review.header=ÐадіÑлати рецензію -diff.review.placeholder=Рецензійований коментарій +diff.review.placeholder=Ð ÐµÑ†ÐµÐ½Ð·Ñ–Ñ ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ñ diff.review.comment=Коментар diff.review.approve=Затвердити diff.review.reject=Запит змін diff.committed_by=зафікÑовано diff.protected=Захищений -diff.image.side_by_side=Пліч-о-пліч -diff.image.swipe=Свайп -diff.image.overlay=Оверлей +diff.image.side_by_side=Поруч +diff.image.swipe=ПровеÑти пальцем +diff.image.overlay=ÐаклаÑти +diff.has_escaped=Цей Ñ€Ñдок міÑтить приховані Ñимволи Юнікоду +diff.show_file_tree=Показати дерево файлів +diff.hide_file_tree=Сховати дерево файлів +diff.submodule_added=Підмодуль %[1]s додано в %[2]s +diff.submodule_deleted=Підмодуль %[1]s видалено з %[2]s +diff.submodule_updated=Підмодуль %[1]s оновлено: %[2]s releases.desc=ВідÑлідковувати верÑÑ–Ñ— проєкту Ñ– завантаженнÑ. release.releases=Релізи release.detail=Деталі релізу -release.tags=Теги +release.tags=Мітки release.new_release=Ðовий реліз release.draft=Чернетка release.prerelease=Пре-реліз release.stable=Стабільний +release.latest=ОÑтанні release.compare=ПорівнÑти release.edit=редагувати release.ahead.commits=<strong>%d</strong> коміт(ів) release.ahead.target=до %s з моменту цього випуÑку release.source_code=Код -release.new_subheader=ÐŸÑƒÐ±Ð»Ñ–ÐºÐ°Ñ†Ñ–Ñ Ñ€ÐµÐ»Ñ–Ð·Ñ–Ð² допоможе вам організувати верÑÑ–ÑŽ проєкту. -release.edit_subheader=ÐŸÑƒÐ±Ð»Ñ–ÐºÐ°Ñ†Ñ–Ñ Ñ€ÐµÐ»Ñ–Ð·Ñ–Ð² допоможе вам організувати верÑÑ–ÑŽ проєкту. -release.tag_name=Ðазва тегу +release.new_subheader=Релізи впорÑдковують верÑÑ–Ñ— проєкту. +release.edit_subheader=Релізи впорÑдковують верÑÑ–Ñ— проєкту. +release.tag_name=Ðазва мітки release.target=Ціль -release.tag_helper=Виберіть Ñ–Ñнуючий тег або Ñтворіть новий. +release.tag_helper=Вибрати Ñ–Ñнуючу мітку або Ñтворити нову. +release.title=Ðазва релізу +release.title_empty=Заголовок не може бути порожнім. +release.message=Опишіть цей реліз release.prerelease_desc=Позначити Ñк пре-реліз -release.prerelease_helper=Позначте цей випуÑк непридатним Ð´Ð»Ñ ÐŸÐ ÐžÐ” викориÑтаннÑ. +release.prerelease_helper=Позначите випуÑк Ñк непридатний Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð´ÑƒÐºÑ‚Ð¸Ð²Ð½Ð¾Ð³Ð¾ викориÑтаннÑ. release.cancel=Відмінити release.publish=Опублікувати реліз release.save_draft=Зберегти чернетку release.edit_release=Оновити реліз release.delete_release=Видалити реліз -release.delete_tag=Видалити тег +release.delete_tag=Видалити мітку release.deletion=Видалити реліз -release.deletion_success=Реліз, було видалено. +release.deletion_success=Реліз видалено. release.deletion_tag_desc=Буде видалено цей тег із репозиторію. ВміÑÑ‚ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ñ‚Ð° Ñ–ÑÑ‚Ð¾Ñ€Ñ–Ñ Ð·Ð°Ð»Ð¸ÑˆÐ°Ñ‚ÑŒÑÑ Ð½ÐµÐ·Ð¼Ñ–Ð½Ð½Ð¸Ð¼Ð¸. Продовжити? release.deletion_tag_success=Мітка видалена. -release.tag_name_already_exist=Реліз з цим ім'Ñм мітки вже Ñ–Ñнує. -release.tag_name_invalid=ÐеприпуÑтиме ім'Ñ Ñ‚ÐµÐ³Ð°. -release.tag_name_protected=Ім'Ñ Ñ‚ÐµÐ³Ð° захищене. -release.tag_already_exist=Цей тег вже викориÑтовуєтьÑÑ. -release.downloads=Завантажити +release.tag_name_already_exist=Реліз з такою ж міткою вже Ñ–Ñнує. +release.tag_name_invalid=Ðазва мітки недійÑна. +release.tag_name_protected=Ðазва мітки захищена. +release.tag_already_exist=Ðазва мітки вже Ñ–Ñнує. +release.downloads=Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ release.download_count=ЗавантаженнÑ: %s -release.add_tag_msg=ВикориÑтовуйте заголовок Ñ– зміÑÑ‚ релізу Ñк Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñк тег повідомленнÑ. +release.add_tag_msg=ВикориÑтовуйте заголовок Ñ– зміÑÑ‚ релізу Ñк Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–Ñ‚ÐºÐ¸. release.add_tag=Створити тільки мітку +release.releases_for=Релізи Ð´Ð»Ñ %s +release.tags_for=Мітки Ð´Ð»Ñ %s -branch.name=Ім'Ñ Ð³Ñ–Ð»ÐºÐ¸ +branch.name=Ðазва гілки +branch.already_exists=Гілка з назвою "%s" вже Ñ–Ñнує. branch.delete_head=Видалити +branch.delete=`Видалити гілку "%s"` branch.delete_html=Видалити гілку +branch.delete_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð³Ñ–Ð»ÐºÐ¸ Ñ” незворотним. Хоча видалена гілка може продовжувати Ñ–Ñнувати ще деÑкий Ñ‡Ð°Ñ Ð´Ð¾ того, Ñк Ñ—Ñ— буде видалено оÑтаточно, у більшоÑті випадків це ÐЕМОЖЛИВО ÑкаÑувати. Продовжити? +branch.deletion_success=Гілку "%s" видалено. +branch.deletion_failed=Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ гілку "%s". +branch.delete_branch_has_new_commits=Гілку "%s" не можна видалити, оÑкільки піÑÐ»Ñ Ð·'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð±ÑƒÐ»Ð¾ додано нові коміти. branch.create_branch=Створити гілку %s +branch.create_from=`з "%s"` +branch.create_success=Створено гілку "%s". +branch.branch_name_conflict=Ðазва гілки "%s" конфліктує з уже Ñ–Ñнуючою гілкою "%s". +branch.tag_collision=Гілка "%s" не може бути Ñтворена, оÑкільки у Ñховищі вже Ñ–Ñнує мітка з такою назвою. branch.deleted_by=Видалено %s +branch.restore_success=Гілку "%s" відновлено. +branch.restore_failed=Ðе вдалоÑÑ Ð²Ñ–Ð´Ð½Ð¾Ð²Ð¸Ñ‚Ð¸ гілку "%s". +branch.protected_deletion_failed=Гілка "%s" захищена. Її неможливо видалити. +branch.default_deletion_failed=Гілка "%s" Ñтандартна. Її неможливо видалити. +branch.restore=`Відновити гілку "%s"` +branch.download=`Завантажити гілку "%s"` +branch.rename=`Перейменувати гілку "%s"` branch.included_desc=Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° Ñ” чаÑтиною типової гілки branch.included=Включено branch.create_new_branch=Створити гілку з гілки: branch.confirm_create_branch=Створити гілку +branch.warning_rename_default_branch=Ви перейменовуєте Ñтандартну гілку. +branch.rename_branch_to=Перейменувати "%s" на: branch.confirm_rename_branch=Перейменувати гілку branch.create_branch_operation=Створити гілку branch.new_branch=Створити нову гілку +branch.new_branch_from=`Створити нову гілку з "%s"` branch.renamed=Гілку %s перейменовано на %s. +branch.rename_default_or_protected_branch_error=Лише адмініÑтратори можуть перейменовувати типові або захищені гілки. +branch.rename_protected_branch_failed=Ð¦Ñ Ð³Ñ–Ð»ÐºÐ° захищена правилами захиÑту на оÑнові глобальних правил. tag.create_tag=Створити тег %s +tag.create_tag_operation=Створити мітку +tag.confirm_create_tag=Створити мітку +tag.create_tag_from=`Створити нову мітку з "%s"` +tag.create_success=Мітку "%s" Ñтворено. -topic.manage_topics=Керувати тематичними мітками +topic.manage_topics=Керувати темами topic.done=Готово +topic.count_prompt=Ви не можете вибрати більше ніж 25 тем +topic.format_prompt=Теми мають починатиÑÑ Ð· літери або цифри, можуть міÑтити дефіÑи ('-') Ñ– крапки ('.'), мати довжину до 35 Ñимволів. Літери повинні бути малими. +find_file.go_to_file=Перейти до файлу +find_file.no_matching=Ðе знайдено відповідного файлу error.csv.too_large=Ðе вдаєтьÑÑ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð·Ð¸Ñ‚Ð¸ цей файл, тому що він завеликий. error.csv.unexpected=Ðе вдаєтьÑÑ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð·Ð¸Ñ‚Ð¸ цей файл, тому що він міÑтить неочікуваний Ñимвол в Ñ€Ñдку %d Ñ– Ñтовпці %d. error.csv.invalid_field_count=Ðе вдаєтьÑÑ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð·Ð¸Ñ‚Ð¸ цей файл, тому що він має неправильну кількіÑть полів у Ñ€Ñдку %d. [graphs] +component_loading=Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ %s... +component_loading_failed=Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ %s +component_loading_info=Це може зайнÑти трохи чаÑу… +component_failed_to_load=СталаÑÑŒ непередбачена помилка. +code_frequency.what=чаÑтота коду +contributors.what=внеÑки +recent_commits.what=нові коміти [org] org_name_holder=Ðазва організації @@ -1901,6 +2604,7 @@ create_org=Створити організацію repo_updated=Оновлено members=УчаÑники teams=Команди +code=Код lower_members=учаÑники lower_repositories=репозиторії create_new_team=Ðова команда @@ -1910,43 +2614,62 @@ team_name=Ðазва команди team_desc=ÐžÐ¿Ð¸Ñ team_name_helper=Ðазва команди має бути проÑтою та зрозумілою. team_desc_helper=Опишіть мету або роль команди. -team_access_desc=ДоÑтуп до Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ +team_access_desc=ДоÑтуп до Ñховища team_permission_desc=Права доÑтупу team_unit_desc=Дозволити доÑтуп до розділів репозиторію team_unit_disabled=(Вимкнено) +form.name_been_taken=Ðазва організації "%s" вже зайнÑта. +form.name_reserved=Ðазву організації "%s" зарезервовано. +form.name_pattern_not_allowed=Шаблон "%s" не допуÑкаєтьÑÑ Ð² назві організації. form.create_org_not_allowed=Вам не дозволено Ñтворювати організації. settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ settings.options=ÐžÑ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ settings.full_name=Повне ім'Ñ +settings.email=Контактна адреÑа електронної пошти settings.website=Веб-Ñайт settings.location=Ð Ð¾Ð·Ñ‚Ð°ÑˆÑƒÐ²Ð°Ð½Ð½Ñ settings.permission=Дозволи settings.repoadminchangeteam=ÐдмініÑтратор репозитарію може додавати та видалÑти доÑтуп Ð´Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´ settings.visibility=ВидиміÑть -settings.visibility.public=Публічний -settings.visibility.limited_shortname=Обмежений -settings.visibility.private=Приватний (Видимий лише членам організації) +settings.visibility.public=Публічна +settings.visibility.limited=Обмежений (Видимий лише Ð´Ð»Ñ Ð°Ð²Ñ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ¾Ð²Ð°Ð½Ð¸Ñ… кориÑтувачів) +settings.visibility.limited_shortname=Обмежена +settings.visibility.private=Приватна (Видима лише членам організації) settings.visibility.private_shortname=Приватний settings.update_settings=Оновити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ settings.update_setting_success=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ— оновлені. -settings.change_orgname_redirect_prompt=Старе ім'Ñ Ð±ÑƒÐ´Ðµ перенаправлено до тих пір, поки воно не буде заброньовано. + +settings.rename=Перейменувати організацію +settings.rename_desc=Зміна назви організації також змінить URL адреÑу вашої організації Ñ– звільнить Ñтару назву. +settings.rename_success=Організацію %[1]s уÑпішно перейменована на %[2]. +settings.rename_no_change=Ðазва організації не змінилаÑÑ. +settings.rename_new_org_name=Ðазва нової організації +settings.rename_failed=Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ¹Ð¼ÐµÐ½ÑƒÐ²Ð°Ñ‚Ð¸ організацію через внутрішню помилку +settings.rename_notices_1=Цю операцію <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати. +settings.rename_notices_2=Стара назва буде перенаправлÑтиÑÑ Ð½Ð° нову, поки хтоÑÑŒ не викориÑтає Ñ—Ñ—. + settings.update_avatar_success=Ðватар організації оновлений. settings.delete=Видалити організацію settings.delete_account=Видалити цю організацію -settings.delete_prompt=ÐžÑ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð±ÑƒÐ´Ðµ оÑтаточно видалена. Це <strong>ÐЕ МОЖЛИВО</strong> відмінити! -settings.confirm_delete_account=Підтвердіть Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ -settings.delete_org_title=Видалити організацію -settings.delete_org_desc=Ð¦Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð±ÑƒÐ´Ðµ безповоротно видалена. Продовжити? -settings.hooks_desc=Додайте webhooks, Ñкий буде викликатиÑÑ Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong> Ñкими володіє Ñ†Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ. - -settings.labels_desc=Додати мітки, Ñкі можуть бути викориÑтані Ð´Ð»Ñ Ð·Ð°Ð´Ð°Ñ‡ Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong> в цій організації. +settings.delete_prompt=Організацію буде оÑтаточно видалено. Це <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати! +settings.name_confirm=Введіть назву організації Ð´Ð»Ñ Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ: +settings.delete_notices_1=Цю операцію <strong>ÐЕМОЖЛИВО</strong> ÑкаÑувати. +settings.delete_notices_2=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð½Ð°Ð·Ð°Ð²Ð¶Ð´Ð¸ видалить <strong>Ñховища</strong> <strong>%s</strong>, включно з кодом, задачами, коментарÑми, даними вікі та налаштуваннÑми Ñпівавторів. +settings.delete_notices_3=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð½Ð°Ð·Ð°Ð²Ð¶Ð´Ð¸ видалить вÑÑ– <strong>пакети</strong> <strong>%s</strong>. +settings.delete_notices_4=Ð¦Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ñ–Ñ Ð½Ð°Ð·Ð°Ð²Ð¶Ð´Ð¸ видалить вÑÑ– <strong>проєкти</strong> <strong>%s</strong>. +settings.confirm_delete_account=Підтвердити Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ +settings.delete_failed=Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ організацію через внутрішню помилку +settings.delete_successful=Організацію <b>%s</b> уÑпішно видалено. +settings.hooks_desc=Додайте веб-хуки, Ñкі Ñпрацьовуватимуть Ð´Ð»Ñ <strong>вÑÑ–Ñ… Ñховищ</strong> у цій організації. + +settings.labels_desc=Додайте мітки, Ñкі можна викориÑтовувати у задачах Ð´Ð»Ñ <strong>уÑÑ–Ñ… Ñховищ</strong> у цій організації. members.membership_visibility=ВидиміÑть учаÑника: members.public=Показувати -members.public_helper=зробити прихованим +members.public_helper=приховати members.private=Прихований members.private_helper=зробити видимим members.member_role=Роль учаÑника: @@ -1964,22 +2687,28 @@ teams.leave=Покинути teams.leave.detail=Покинути %s? teams.can_create_org_repo=Створити репозиторії teams.can_create_org_repo_helper=УчаÑники можуть Ñтворювати нові репозиторії в організації. Ðвтор отримає доÑтуп адмініÑтратора до нового репозиторію. -teams.read_access=Прочитані +teams.none_access=Ðемає доÑтупу +teams.none_access_helper=УчаÑники не можуть переглÑдати або виконувати будь-Ñкі інші дії з цією одиницею. Це не впливає на загальнодоÑтупні Ñховища. +teams.general_access=Загальний доÑтуп +teams.general_access_helper=Дозволи учаÑників будуть визначатиÑÑ Ð²Ñ–Ð´Ð¿Ð¾Ð²Ñ–Ð´Ð½Ð¾ до наведеної нижче таблиці дозволів. +teams.read_access=Ð§Ð¸Ñ‚Ð°Ð½Ð½Ñ teams.read_access_helper=УчаÑники можуть переглÑдати та клонувати репозиторії команд. +teams.write_access=Ð—Ð°Ð¿Ð¸Ñ teams.write_access_helper=УчаÑники можуть читати Ñ– виконувати push в репозиторії команд. teams.admin_access=ДоÑтуп адмініÑтратора teams.admin_access_helper=УчаÑники можуть виконувати pull, push в репозиторії команд Ñ– додавати Ñпівавторів в команду. teams.no_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° не має опиÑу teams.settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ -teams.owners_permission_desc=ВлаÑник має повний доÑтуп до <strong>уÑÑ–Ñ… репозиторіїв</strong> та має <strong>права адмініÑтратора</strong> організації. +teams.owners_permission_desc=ВлаÑники мають повний доÑтуп до <strong>уÑÑ–Ñ… репозиторіїв</strong> та <strong>права адмініÑтратора</strong> організації. teams.members=УчаÑники команди teams.update_settings=Оновити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ teams.delete_team=Видалити команду teams.add_team_member=Додати учаÑника команди +teams.invite_team_member=ЗапроÑити до %s teams.delete_team_title=Видалити команду teams.delete_team_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¸ ÑкаÑовує доÑтуп до Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ Ð´Ð»Ñ Ñ—Ñ— учаÑників. Продовжити? teams.delete_team_success=Команду було видалено. -teams.read_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° має доÑтуп Ð´Ð»Ñ <strong>читаннÑ</strong>: учаÑники можуть переглÑдати та клонувати репозиторії. +teams.read_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° має доÑтуп на <strong>читаннÑ</strong>: учаÑники можуть переглÑдати та клонувати репозиторії. teams.write_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає доÑтуп на <strong>запиÑ</strong>: учаÑники можуть отримувати й виконувати push команди до репозитрію. teams.admin_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає <strong>адмініÑтраторÑький</strong> доÑтуп: учаÑники можуть читати, виконувати push команди та додавати Ñпівробітників до репозиторію. teams.create_repo_permission_desc=Крім того, Ñ†Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає дозвіл <strong>Створити репозиторій</strong>: учаÑники можуть Ñтворювати нові репозиторії в організації. @@ -1988,9 +2717,11 @@ teams.remove_all_repos_title=Видалити вÑÑ– репозиторії ко teams.remove_all_repos_desc=Це видалить уÑÑ– репозиторії команди. teams.add_all_repos_title=Додати вÑÑ– репозиторії teams.add_all_repos_desc=Це додаÑть вÑÑ– репозиторії організації до команди. +teams.add_nonexistent_repo=Сховище, Ñке ви намагаєтеÑÑ Ð´Ð¾Ð´Ð°Ñ‚Ð¸, не Ñ–Ñнує, будь лаÑка, Ñтворіть його Ñпочатку. teams.add_duplicate_users=КориÑтувач уже Ñ” членом команди. teams.repos.none=Ð”Ð»Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð¸ немає доÑтупних репозиторіїв. teams.members.none=Ðемає членів в цій команді. +teams.members.blocked_user=Ðе вдаєтьÑÑ Ð´Ð¾Ð´Ð°Ñ‚Ð¸ кориÑтувача, оÑкільки він заблокований організацією. teams.specific_repositories=Конкретні репозиторії teams.specific_repositories_helper=УчаÑники матимуть доÑтуп лише до репозиторіїв, Ñкі були Ñвно додані до команди. Вибір цього пункту <strong>не призводить</strong> до автоматичного Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ—Ð², доданих з <i>Ð’ÑÑ– репозиторії</i>. teams.all_repositories=Ð’ÑÑ– репозиторії @@ -1998,17 +2729,35 @@ teams.all_repositories_helper=Команда має доÑтуп до вÑÑ–Ñ… Ñ teams.all_repositories_read_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає дозвіл <strong>ПереглÑд</strong> Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong>: учаÑники можуть переглÑдати та клонувати Ñ—Ñ…. teams.all_repositories_write_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає дозвіл <strong>ЗапиÑ</strong> Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong>: учаÑники можуть переглÑдати та виконувати push в репозиторіÑÑ…. teams.all_repositories_admin_permission_desc=Ð¦Ñ ÐºÐ¾Ð¼Ð°Ð½Ð´Ð° надає дозвіл <strong>ÐдмініÑтруваннÑ</strong> Ð´Ð»Ñ <strong>вÑÑ–Ñ… репозиторіїв</strong>: учаÑники можуть переглÑдати, виконувати push та додавати Ñпівробітників. - - +teams.invite.title=Ð’Ð°Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñили приєднатиÑÑ Ð´Ð¾ команди <strong>%s</strong> в організації <strong>%s</strong>. +teams.invite.by=Запрошений %s +teams.invite.description=Будь лаÑка, натиÑніть на кнопку нижче, щоб приєднатиÑÑ Ð´Ð¾ команди. + +view_as_role=ПереглÑнути Ñк: %s +view_as_public_hint=Ви переглÑдаєте README Ñк публічний кориÑтувач. +view_as_member_hint=Ви переглÑдаєте README Ñк член цієї організації. + +worktime=Ð§Ð°Ñ Ñ€Ð¾Ð±Ð¾Ñ‚Ð¸ +worktime.date_range_start=Дата початку +worktime.date_range_end=Дата Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ +worktime.query=Запит +worktime.time=Ð§Ð°Ñ +worktime.by_milestones=За етапами +worktime.by_members=За учаÑниками [admin] +maintenance=Технічне обÑÐ»ÑƒÐ³Ð¾Ð²ÑƒÐ²Ð°Ð½Ð½Ñ dashboard=Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ +self_check=Самоперевірка +identity_access=Ð†Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ Ñ‚Ð° доÑтуп users=Облікові запиÑи кориÑтувачів organizations=Організації +assets=РеÑурÑи коду repositories=Репозиторії hooks=Веб-хуки +integrations=Інтеграції authentication=Джерела автентифікації -emails=Електронні адреÑи КориÑтувача +emails=Електронна пошта кориÑтувача config=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ config_summary=ПідÑумок config_settings=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ @@ -2017,8 +2766,11 @@ monitor=Моніторинг first_page=Перша last_page=ОÑÑ‚Ð°Ð½Ð½Ñ total=Разом: %d +settings=ÐдмініÑтративні Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ +dashboard.new_version_hint=Gitea %s тепер доÑтупна, ви викориÑтовуєте %s. Перевірте <a target="_blank" rel="noreferrer" href="https://blog.gitea.io">блог</a> Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ¾Ð²Ð¾Ñ— інформації. dashboard.statistic=ПідÑумок +dashboard.maintenance_operations=Операції з технічного обÑÐ»ÑƒÐ³Ð¾Ð²ÑƒÐ²Ð°Ð½Ð½Ñ dashboard.system_status=Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÑиÑтеми dashboard.operation_name=Ðазва операції dashboard.operation_switch=Перемкнути @@ -2027,20 +2779,23 @@ dashboard.clean_unbind_oauth=ОчиÑтити ÑпиÑок незавершенРdashboard.clean_unbind_oauth_success=Ð’ÑÑ– незавершені зв'Ñзки OAuth були видалені. dashboard.task.started=Запущено завданнÑ: %[1]s dashboard.task.process=ЗавданнÑ: %[1]s +dashboard.task.cancelled=ЗавданнÑ: %[1]s ÑкаÑовано: %[3]s dashboard.task.error=Помилка у завданні: %[1]s:%[3]s dashboard.task.finished=ЗавершилоÑÑ Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ, Ñке запуÑтив %[2]s: %[1]s dashboard.task.unknown=Ðевідоме завданнÑ: %[1]s dashboard.cron.started=Запущено Cron: %[1]s dashboard.cron.process=Cron: %[1]s +dashboard.cron.cancelled=Планувальник: %[1]s ÑкаÑовано: %[3]s dashboard.cron.error=Помилка в Cron: %s: %[3]s dashboard.cron.finished=Cron: %[1]s завершено dashboard.delete_inactive_accounts=Видалити вÑÑ– неактивовані облікові запиÑи -dashboard.delete_inactive_accounts.started=Запущено Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ– неактивованих облікових запиÑів. +dashboard.delete_inactive_accounts.started=Запущено Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… неактивованих облікових запиÑів. dashboard.delete_repo_archives=Видалити вÑÑ– архіви репозиторіїв (ZIP, TAR.GZ, Ñ– Ñ‚. д..) dashboard.delete_repo_archives.started=Запущено Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… архівів репозиторіїв. -dashboard.delete_missing_repos=Видалити вÑÑ– запиÑи про репозиторії з відÑутніми файлами Git +dashboard.delete_missing_repos=Видаліть уÑÑ– Ñховища, в Ñких відÑутні файли Git dashboard.delete_missing_repos.started=Запущено Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… репозиторіїв, в Ñких відÑутні файли Git. -dashboard.delete_generated_repository_avatars=Видалити репозиторій з згенерованими аватарами +dashboard.delete_generated_repository_avatars=Видалити згенеровані аватарки Ñховища +dashboard.sync_repo_branches=Синхронізувати пропущені гілки з даних git до баз даних dashboard.update_mirrors=Оновити дзеркала dashboard.repo_health_check=Перевірка Ñтану вÑÑ–Ñ… репозиторіїв dashboard.check_repo_stats=Перевірити ÑтатиÑтику вÑÑ–Ñ… репозиторіїв @@ -2050,11 +2805,13 @@ dashboard.update_migration_poster_id=Оновити мігровані ID авт dashboard.git_gc_repos=Виконати очиÑтку ÑÐ¼Ñ–Ñ‚Ñ‚Ñ Ð´Ð»Ñ Ð²ÑÑ–Ñ… репозиторіїв dashboard.resync_all_sshkeys=Оновити файл '.ssh/authorized_keys' з SSH ключами Gitea. dashboard.resync_all_sshprincipals=Оновіть файл '.ssh/authorized_princÑ‚ipals' з SSH даними кориÑтувача Gitea. -dashboard.resync_all_hooks=ПереÑинхронізувати перед-прийнÑтні, оновлюючі та поÑÑ‚-прийнÑтні хуки в уÑÑ–Ñ… репозиторіÑÑ…. -dashboard.reinit_missing_repos=Переініціалізувати уÑÑ– репозитрії git-файли Ñких втрачено +dashboard.resync_all_hooks=Заново Ñинхронізувати хуки попереднього отриманнÑ, Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ‚Ð° поÑÑ‚-Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð²ÑÑ–Ñ… Ñховищ. +dashboard.reinit_missing_repos=Заново ініціалізувати вÑÑ– відÑутні Ñховища Git'а, Ð´Ð»Ñ Ñких Ñ–Ñнують запиÑи dashboard.sync_external_users=Синхронізувати дані зовнішніх кориÑтувачів -dashboard.cleanup_hook_task_table=ОчиÑтити hook_task таблицю -dashboard.server_uptime=Uptime Ñерверу +dashboard.cleanup_hook_task_table=ОчиÑтити таблицю hook_task +dashboard.cleanup_packages=ОчиÑтити заÑтарілі пакети +dashboard.cleanup_actions=ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ Ñ€ÐµÑурÑів проÑтрочених дій +dashboard.server_uptime=Ð§Ð°Ñ Ñ€Ð¾Ð±Ð¾Ñ‚Ð¸ Ñервера dashboard.current_goroutine=Поточна кількіÑть Goroutines dashboard.current_memory_usage=Поточне викориÑÑ‚Ð°Ð½Ð½Ñ Ð¿Ð°Ð¼'Ñті dashboard.total_memory_allocated=Виділено пам'Ñті загалом @@ -2083,6 +2840,18 @@ dashboard.total_gc_time=Загальна пауза збирача ÑÐ¼Ñ–Ñ‚Ñ‚Ñ dashboard.total_gc_pause=Загальна пауза збирача ÑÐ¼Ñ–Ñ‚Ñ‚Ñ (GC) dashboard.last_gc_pause=ОÑÑ‚Ð°Ð½Ð½Ñ Ð¿Ð°ÑƒÐ·Ð° збирача ÑÐ¼Ñ–Ñ‚Ñ‚Ñ (GC) dashboard.gc_times=КількіÑть запуÑків збирача ÑÐ¼Ñ–Ñ‚Ñ‚Ñ (GC) +dashboard.delete_old_actions=Видалити вÑÑ– Ñтарі дії з бази даних +dashboard.delete_old_actions.started=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð²ÑÑ–Ñ… Ñтарих дій з бази даних розпочато. +dashboard.update_checker=Перевірка оновлень +dashboard.delete_old_system_notices=Видалити вÑÑ– Ñтарі ÑиÑтемні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð· бази даних +dashboard.gc_lfs=Збір ÑÐ¼Ñ–Ñ‚Ñ‚Ñ Ð¼ÐµÑ‚Ð°-об'єктів LFS +dashboard.stop_endless_tasks=Зупинити неÑкінченні Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ +dashboard.cancel_abandoned_jobs=СкаÑувати покинуті Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ +dashboard.start_schedule_tasks=ЗапуÑк запланованих завдань +dashboard.sync_branch.started=Розпочато Ñинхронізацію гілок +dashboard.sync_tag.started=Розпочато Ñинхронізацію міток +dashboard.rebuild_issue_indexer=Перебудувати індекÑатор задач +dashboard.sync_repo_licenses=Синхронізувати ліцензії Ñховища users.user_manage_panel=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¸Ð¼Ð¸ запиÑами кориÑтувачів users.new_account=Створити обліковий Ð·Ð°Ð¿Ð¸Ñ @@ -2091,12 +2860,16 @@ users.full_name=Повне ім'Ñ users.activated=Ðктивовано users.admin=ÐдмініÑтратор users.restricted=Обмежено +users.reserved=Зарезервовано +users.bot=Бот +users.remote=Віддалений users.2fa=2FA users.repos=Репозиторії users.created=Створено users.last_login=ОÑтанній вхід users.never_login=Ðіколи не входив users.send_register_notify=ÐадіÑлати Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ реєÑтрацію кориÑтувача +users.new_success=Обліковий Ð·Ð°Ð¿Ð¸Ñ "%s" Ñтворено. users.edit=Редагувати users.auth_source=Джерело автентифікації users.local=Локальні @@ -2116,8 +2889,10 @@ users.allow_import_local=Може імпортувати локальні реп users.allow_create_organization=Може Ñтворювати організацій users.update_profile=Оновити обліковий Ð·Ð°Ð¿Ð¸Ñ users.delete_account=Видалити цей обліковий Ð·Ð°Ð¿Ð¸Ñ -users.still_own_repo=Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð²Ñе ще володіє одним або кількома репозиторіÑми, Ñпочатку вам потрібно видалити або передати Ñ—Ñ…. -users.still_has_org=Цей обліковий Ð·Ð°Ð¿Ð¸Ñ Ð²Ñе ще Ñ” учаÑником однієї або декількох організацій. Ð”Ð»Ñ Ð¿Ñ€Ð¾Ð´Ð¾Ð²Ð¶ÐµÐ½Ð½Ñ, покиньте або видаліть організації. +users.cannot_delete_self=Ви не можете видалити Ñебе +users.still_own_repo=Цей кориÑтувач вÑе ще володіє одним або кількома Ñховищами. Спочатку видаліть або передайте ці Ñховища. +users.still_has_org=Цей кориÑтувач Ñ” членом організації. Спочатку видаліть кориÑтувача з уÑÑ–Ñ… організацій. +users.purge=Видалити кориÑтувача users.deletion_success=Обліковий Ð·Ð°Ð¿Ð¸Ñ ÐºÐ¾Ñ€Ð¸Ñтувача було видалено. users.reset_2fa=Скинути 2FA users.list_status_filter.menu_text=Фільтр @@ -2128,13 +2903,14 @@ users.list_status_filter.is_admin=ÐдмініÑтратор users.list_status_filter.not_admin=Ðе адмініÑтратор users.list_status_filter.is_restricted=З обмеженнÑми users.list_status_filter.not_restricted=Без обмежень -users.list_status_filter.is_prohibit_login=Вхід заборонено -users.list_status_filter.not_prohibit_login=Вхід дозволено +users.list_status_filter.is_prohibit_login=Заборонити вхід +users.list_status_filter.not_prohibit_login=Дозволити вхід users.list_status_filter.is_2fa_enabled=2FA увімкнена users.list_status_filter.not_2fa_enabled=2FA вимкнена +users.details=Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ кориÑтувача emails.email_manage_panel=Ð£Ð¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ð¿Ð¾ÑˆÑ‚Ð¾ÑŽ кориÑтувача -emails.primary=Головний +emails.primary=ОÑновна emails.activated=Ðктивовано emails.filter_sort.email=Електронна пошта emails.filter_sort.email_reverse=Електронна пошта (зворотна) @@ -2142,8 +2918,13 @@ emails.filter_sort.name=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача emails.filter_sort.name_reverse=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача (зворотне) emails.updated=Електронну пошту оновлено emails.not_updated=Ðе вдалоÑÑŒ оновити адреÑу електронної пошти: %v -emails.duplicate_active=Ð¦Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð° адреÑа вже активна Ð´Ð»Ñ Ñ–Ð½ÑˆÐ¾Ð³Ð¾ кориÑтувача. +emails.duplicate_active=Ð¦Ñ Ð°Ð´Ñ€ÐµÑа електронної пошти вже активна Ð´Ð»Ñ Ñ–Ð½ÑˆÐ¾Ð³Ð¾ кориÑтувача. emails.change_email_header=Редагувати влаÑтивоÑті електронної пошти +emails.change_email_text=Ви впевнені, що хочете оновити адреÑу електронної пошти? +emails.delete=Видалити адреÑу електронної пошти +emails.delete_desc=Ви впевнені, що хочете видалити адреÑу електронної пошти? +emails.deletion_success=ÐдреÑу електронної пошти видалено. +emails.delete_primary_email_error=Ви не можете видалити оÑновну адреÑу електронної пошти. orgs.org_manage_panel=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ñ€Ð³Ð°Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñми orgs.name=Ðазва @@ -2159,12 +2940,21 @@ repos.name=Ðазва repos.private=Приватний repos.issues=Задачі repos.size=Розмір +repos.lfs_size=Розмір LFS +packages.package_manage_panel=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð°ÐºÐµÑ‚Ð°Ð¼Ð¸ +packages.total_size=Загальний розмір: %s +packages.unreferenced_size=Розмір без поÑилань: %s +packages.cleanup=ОчиÑтити проÑтрочені дані +packages.cleanup.success=ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾Ñтрочених даних завершено packages.owner=ВлаÑник +packages.creator=Ðвтор packages.name=Ðазва +packages.version=ВерÑÑ–Ñ packages.type=Тип -packages.repository=Репозиторій +packages.repository=Сховище packages.size=Розмір +packages.published=Опубліковано defaulthooks=Веб-хуки за замовчуваннÑм defaulthooks.add_webhook=Додати веб-хук за замовчуваннÑм @@ -2176,7 +2966,7 @@ systemhooks.update_webhook=Оновити ÑиÑтемний вебхук auths.auth_manage_panel=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð¶ÐµÑ€ÐµÐ»Ð¾Ð¼ автентифікації auths.new=Додати джерело автентифікації -auths.name=Ім'Ñ +auths.name=Ðазва auths.type=Тип auths.enabled=Увімкнено auths.syncenabled=Увімкнути Ñинхронізацію кориÑтувача @@ -2193,12 +2983,13 @@ auths.user_base=База пошуку кориÑтувачів auths.user_dn=DN кориÑтувача auths.attribute_username=Ðтрибут імені кориÑтувача auths.attribute_username_placeholder=Залиште порожнім, щоб викориÑтовувати ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача Ð´Ð»Ñ Ñ€ÐµÑ”Ñтрації. -auths.attribute_name=Ðтрибут імені -auths.attribute_surname=Ðтрибут Surname -auths.attribute_mail=Ðтрибут Email -auths.attribute_ssh_public_key=Ðтрибут Відкритий SSH ключ +auths.attribute_name=ВлаÑтивоÑті імені +auths.attribute_surname=ВлаÑтивоÑті прізвища +auths.attribute_mail=ВлаÑтивоÑті електронної пошти +auths.attribute_ssh_public_key=ВлаÑтивоÑті публічного ключа SSH +auths.attribute_avatar=ВлаÑтивоÑті аватару auths.attributes_in_bind=ВитÑгувати атрибути в контекÑті Bind DN -auths.allow_deactivate_all=Дозволити порожньому результату пошуку відключити вÑÑ–Ñ… кориÑтувачів +auths.allow_deactivate_all=Дозволити порожній результат пошуку, щоб деактивувати вÑÑ–Ñ… кориÑтувачів auths.use_paged_search=ВикориÑтовувати поÑторінковий пошук auths.search_page_size=Розмір Ñторінки auths.filter=КориÑтувацький фільтр @@ -2208,6 +2999,7 @@ auths.restricted_filter_helper=Залиште пуÑтим, щоб не вÑта auths.group_search_base=Пошукова база груп DN auths.group_attribute_list_users=Ðтрибут групи зі ÑпиÑком кориÑтувачів auths.user_attribute_in_group=Ðтрибути кориÑтувача в групі +auths.enable_ldap_groups=Увімкнути групи LDAP auths.ms_ad_sa=Ðтрибути пошуку MS AD auths.smtp_auth=Тип автентифікації SMTP auths.smtphost=SMTP хоÑÑ‚ @@ -2223,7 +3015,7 @@ auths.disable_helo=Вимкнути HELO auths.pam_service_name=Ім'Ñ Ñлужби PAM auths.pam_email_domain=Поштовий домен PAM (необов'Ñзково) auths.oauth2_provider=ПоÑтачальник OAuth2 -auths.oauth2_icon_url=URL іконки +auths.oauth2_icon_url=URL піктограми auths.oauth2_clientID=ID клієнта (ключ) auths.oauth2_clientSecret=Ключ клієнта auths.openIdConnectAutoDiscoveryURL=OpenID Connect URL Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ð·Ð°Ñ†Ñ–Ñ— входу @@ -2235,6 +3027,7 @@ auths.oauth2_emailURL=URL електронної пошти auths.skip_local_two_fa=ПропуÑтити локальну 2FA auths.skip_local_two_fa_helper=Якщо Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð½Ðµ вказано, локальнам кориÑтувачам, що викориÑтовують двофакторну автентифікацію, вÑе одно проходитимуть Ñ—Ñ— Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ в ÑиÑтему auths.oauth2_tenant=Tenant +auths.oauth2_map_group_to_team_removal=Видалити кориÑтувачів із Ñинхронізованих команд, Ñкщо кориÑтувач не належить до відповідної групи. auths.enable_auto_register=Увімкнути автоматичну реєÑтрацію auths.sspi_auto_create_users=Ðвтоматично Ñтворювати кориÑтувачів auths.sspi_auto_create_users_helper=Дозволити автоматичне ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð½Ð¾Ð²Ð¸Ñ… облікових запиÑів Ð´Ð»Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів, Ñкі вперше увійшли з викориÑÑ‚Ð°Ð½Ð½Ñ Ð°Ð²Ñ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ— SSPI @@ -2248,21 +3041,33 @@ auths.sspi_default_language=Типова мова кориÑтувача auths.sspi_default_language_helper=Типова мова Ð´Ð»Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів, Ñкі ÑтворюютьÑÑ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡Ð½Ð¾ при SSPI-автентифікації. Залиште не вказаним, Ñкщо надаєте перевагу автоматичному визначенню мови. auths.tips=Поради auths.tips.oauth2.general=OAuth2 Ð°Ð²Ñ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ +auths.tips.oauth2.general.tip=Під Ñ‡Ð°Ñ Ñ€ÐµÑ”Ñтрації нової автентифікації OAuth2 URL-адреÑа зворотного виклику/Ð¿ÐµÑ€ÐµÐ½Ð°Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ Ð¿Ð¾Ð²Ð¸Ð½Ð½Ð° бути такою: auths.tip.oauth2_provider=ПоÑтачальник OAuth2 +auths.tip.bitbucket=`ЗареєÑтруйте нового Ñпоживача OAuth на %s Ñ– додайте дозвіл "Обліковий запиÑ" - "ЧитаннÑ"` auths.tip.nextcloud=`ЗареєÑтруйте нового Ñпоживача OAuth у вашому екземплÑрі за допомогою наÑтупного меню "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ -> Безпека -> клієнт OAuth 2.0"` +auths.tip.dropbox=Створити новий додаток на %s +auths.tip.facebook=`ЗареєÑтруйте новий додаток на %s Ñ– додайте модуль "Facebook Login"` +auths.tip.github=ЗареєÑтруйте новий додаток OAuth на %s +auths.tip.gitlab_new=ЗареєÑтруйте новий додаток на %s +auths.tip.google_plus=Отримайте облікові дані клієнта OAuth2 з конÑолі Google API за адреÑою %s +auths.tip.twitter=Перейдіть до %s, Ñтворіть додаток Ñ– переконайтеÑÑ, що параметр «Дозволити викориÑтовувати цей додаток Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ в Twitter» увімкнено +auths.tip.discord=ЗареєÑтрувати новий додаток на %s +auths.tip.gitea=ЗареєÑтруйте новий додаток OAuth2. ПоÑібник можна знайти за поÑиланнÑм %s auths.tip.mastodon=Введіть URL Ñпеціального екземплÑра Ð´Ð»Ñ ÐµÐºÐ·ÐµÐ¼Ð¿Ð»Ñра mastodon, Ñкий ви хочете автентифікувати за допомогою (або викориÑтовувати за замовчуваннÑм) auths.edit=Редагувати джерело автентифікації -auths.activated=Ð¦Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ Ð°ÐºÑ‚Ð¸Ð²Ð¾Ð²Ð°Ð½Ð° -auths.update_success=Параметри аутентифікації оновлені. +auths.activated=Це джерело автентифікації активовано +auths.new_success=Ðвтентифікацію "%s" додано. +auths.update_success=Джерело автентифікації оновлено. auths.update=Оновити джерело автентифікації auths.delete=Видалити джерело автентифікації auths.delete_auth_title=Видалити джерело автентифікації -auths.delete_auth_desc=Це джерело аутентифікації буде видалене, ви впевнені, що ви хочете продовжити? -auths.still_in_used=Ð¦Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ° ÑправжноÑті доÑÑ– викориÑтовуєтьÑÑ Ð´ÐµÑкими кориÑтувачами. Видаліть або змініть Ð´Ð»Ñ Ñ†Ð¸Ñ… кориÑтувачів тип входу в ÑиÑтему. -auths.deletion_success=Канал аутентифікації уÑпішно знищений. -auths.login_source_of_type_exist=Джерело автентифікації такого типу вже наÑвне. +auths.delete_auth_desc=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð´Ð¶ÐµÑ€ÐµÐ»Ð° автентифікації заборонÑÑ” кориÑтувачам викориÑтовувати його Ð´Ð»Ñ Ð²Ñ…Ð¾Ð´Ñƒ. Продовжити? +auths.still_in_used=Джерело автентифікації вÑе ще викориÑтовуєтьÑÑ. Спочатку перетворіть або видаліть кориÑтувачів, Ñкі викориÑтовують це джерело автентифікації. +auths.deletion_success=Джерело автентифікації видалено. +auths.login_source_exist=Джерело автентифікації "%s" вже Ñ–Ñнує. +auths.login_source_of_type_exist=Джерело автентифікації такого типу вже Ñ–Ñнує. -config.server_config=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ Ñервера +config.server_config=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñервера config.app_name=Ðазва Ñайту config.app_ver=ВерÑÑ–Ñ Gitea config.app_url=Базова URL-адреÑа Gitea @@ -2270,13 +3075,14 @@ config.custom_conf=ШлÑÑ… до файлу конфігурації config.custom_file_root_path=ШлÑÑ… до файлу кориÑтувача config.domain=Домен Ñервера config.offline_mode=Локальний режим -config.disable_router_log=Вимкнути Ð»Ð¾Ð³ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€Ð¾ÑƒÑ‚ÐµÑ€Ñƒ +config.disable_router_log=Вимкнути журнал маршрутизатора config.run_user=ЗапуÑк від імені КориÑтувача config.run_mode=Режим Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ config.git_version=ВерÑÑ–Ñ Git +config.app_data_path=ШлÑÑ… до даних додатка config.repo_root_path=Кореневий шлÑÑ… Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñ -config.lfs_root_path=Кореневої шлÑÑ… LFS -config.log_file_root_path=ШлÑÑ… до лог файлу +config.lfs_root_path=Кореневий шлÑÑ… LFS +config.log_file_root_path=ШлÑÑ… до журналу config.script_type=Тип Ñкрипта config.reverse_auth_user=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð¸Ð·Ð°Ñ†Ñ–Ñ— на reverse proxy @@ -2286,7 +3092,7 @@ config.ssh_start_builtin_server=ВикориÑтовувати вбудованРconfig.ssh_domain=Домен SSH Ñервера config.ssh_port=Порт config.ssh_listen_port=Порт що проÑлуховуєтьÑÑ -config.ssh_root_path=ШлÑÑ… до кореню +config.ssh_root_path=ШлÑÑ… до ÐºÐ¾Ñ€ÐµÐ½Ñ config.ssh_minimum_key_size_check=Мінімальний розмір ключа перевірки config.ssh_minimum_key_sizes=Мінімальні розміри ключів @@ -2298,8 +3104,8 @@ config.lfs_http_auth_expiry=ЗаÑтаріла LFS HTTP Ð°ÑƒÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ config.db_config=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ Ð±Ð°Ð·Ð¸ даних config.db_type=Тип config.db_host=ХоÑÑ‚ -config.db_name=Ім'Ñ -config.db_user=Ім'Ñ ÐºÑ€Ð¸Ñтувача +config.db_name=Ðазва +config.db_user=Ім'Ñ ÐºÐ¾Ñ€Ð¸Ñтувача config.db_schema=Схема config.db_ssl_mode=SSL config.db_path=ШлÑÑ… @@ -2331,16 +3137,24 @@ config.queue_length=Довжина черги config.deliver_timeout=Затримка доÑтавки config.skip_tls_verify=ПропуÑтити перевірку TLS +config.mailer_config=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾ÑˆÑ‚Ð¸ config.mailer_enabled=Увімкнено -config.mailer_name=Ім'Ñ -config.mailer_smtp_port=SMTP порт +config.mailer_enable_helo=Увімкнути HELO +config.mailer_name=Ðазва +config.mailer_protocol=Протокол +config.mailer_smtp_addr=ÐдреÑа SMTP +config.mailer_smtp_port=Порт SMTP config.mailer_user=КориÑтувач config.mailer_use_sendmail=ВикориÑтовувати Sendmail config.mailer_sendmail_path=ШлÑÑ… до Sendmail config.mailer_sendmail_args=Додаткові аргументи до Sendmail config.mailer_sendmail_timeout=Тайм-аут Sendmail +config.mailer_use_dummy=Даммі config.test_email_placeholder=ÐдреÑа електронної пошти (наприклад, test@example.com) config.send_test_mail=Відправити теÑтового лиÑта +config.send_test_mail_submit=ÐадіÑлати +config.test_mail_failed=Ðе вдалоÑÑ Ð½Ð°Ð´Ñ–Ñлати теÑтовий лиÑÑ‚ "%s": %v +config.test_mail_sent=ТеÑтовий лиÑÑ‚ надіÑлано "%s". config.oauth_config=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ OAuth config.oauth_enabled=Увімкнено @@ -2350,6 +3164,10 @@ config.cache_adapter=Ðдаптер кешу config.cache_interval=Інтервал ÐºÐµÑˆÑƒÐ²Ð°Ð½Ð½Ñ config.cache_conn=ÐŸÑ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð´Ð¾ кешу config.cache_item_ttl=Ð§Ð°Ñ Ð·Ð±ÐµÑ€Ñ–Ð³Ð°Ð½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… кешу +config.cache_test=Перевірити кеш +config.cache_test_failed=Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ кеш: %v. +config.cache_test_slow=Перевірка кешу уÑпішна, але відповідь повільна: %s. +config.cache_test_succeeded=ТеÑÑ‚ кешу уÑпішно завершено, отримано відповідь за %s. config.session_config=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ ÑеÑÑ–Ñ— config.session_provider=Провайдер ÑеÑÑ–Ñ— @@ -2378,23 +3196,32 @@ config.git_pull_timeout=Тайм-аут операції Pull config.git_gc_timeout=Тайм-аут операції збирача ÑÐ¼Ñ–Ñ‚Ñ‚Ñ (GC) config.log_config=ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ Ð¶ÑƒÑ€Ð½Ð°Ð»Ñƒ +config.logger_name_fmt=Журнал: %s config.disabled_logger=Вимкнено config.access_log_mode=Режим доÑтупу до журналу +config.access_log_template=Шаблон журналу доÑтупу config.xorm_log_sql=Журнал SQL +config.set_setting_failed=Ðе вдалоÑÑ Ð²Ñтановити параметр %s +monitor.stats=СтатиÑтика monitor.cron=Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ cron -monitor.name=Ім'Ñ +monitor.name=Ðазва monitor.schedule=Розклад monitor.next=ÐаÑтупного разу monitor.previous=Попереднього разу monitor.execute_times=КількіÑть виконань monitor.process=Запущені процеÑи +monitor.performance_logs=Журнал швидкодії +monitor.processes_count=%d процеÑи(-ів) +monitor.download_diagnosis_report=Завантажити звіт діагноÑтики monitor.desc=ÐžÐ¿Ð¸Ñ monitor.start=Ð§Ð°Ñ Ð¿Ð¾Ñ‡Ð°Ñ‚ÐºÑƒ monitor.execute_time=Ð§Ð°Ñ Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ +monitor.last_execution_result=Результат monitor.process.cancel=Зупинити Ð¿Ñ€Ð¾Ñ†ÐµÑ +monitor.process.cancel_desc=СкаÑÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ñ†ÐµÑу може призвеÑти до втрати даних monitor.process.children=Дочірні процеÑи monitor.queues=Черги @@ -2404,15 +3231,19 @@ monitor.queue.type=Тип monitor.queue.exemplar=Приклад типу monitor.queue.numberworkers=КількіÑть робочих потоків monitor.queue.maxnumberworkers=МакÑимальна кількіÑть робочих потоків +monitor.queue.numberinqueue=Ðомер у черзі monitor.queue.settings.title=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿ÑƒÐ»Ñƒ monitor.queue.settings.maxnumberworkers=МакÑимальна кількіÑть робочих потоків monitor.queue.settings.maxnumberworkers.placeholder=Поточний %[1]d monitor.queue.settings.maxnumberworkers.error=МакÑимальна кількіÑть робочих потоків має бути чиÑлом monitor.queue.settings.submit=Оновити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ monitor.queue.settings.changed=ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð¾ +monitor.queue.settings.remove_all_items=Видалити вÑе +monitor.queue.settings.remove_all_items_done=УÑÑ– елементи черги видалено. notices.system_notice_list=Ð¡Ð¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ ÑиÑтеми notices.view_detail_header=ПереглÑнути деталі Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ +notices.operations=Дії notices.select_all=Вибрати вÑе notices.deselect_all=СкаÑувати Ð²Ð¸Ð´Ñ–Ð»ÐµÐ½Ð½Ñ notices.inverse_selection=Інвертувати виділене @@ -2425,11 +3256,19 @@ notices.desc=ÐžÐ¿Ð¸Ñ notices.op=Оп. notices.delete_success=Ð¡Ð¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ ÑиÑтеми були видалені. +self_check.no_problem_found=Ðаразі проблем не виÑвлено. +self_check.startup_warnings=ÐŸÐ¾Ð¿ÐµÑ€ÐµÐ´Ð¶ÐµÐ½Ð½Ñ Ð¿Ñ–Ð´ Ñ‡Ð°Ñ Ð·Ð°Ð¿ÑƒÑку: +self_check.database_collation_mismatch=ОчікуєтьÑÑ, що база даних викориÑтає зіÑтавленнÑ: %s +self_check.database_collation_case_insensitive=У базі даних викориÑтовуєтьÑÑ Ð·Ñ–ÑÑ‚Ð°Ð²Ð»ÐµÐ½Ð½Ñ %s, що Ñ” нечутливим. Хоч Gitea може працювати з ним, у деÑких рідкіÑних випадках він може працювати не так, Ñк очікуєтьÑÑ. +self_check.database_inconsistent_collation_columns=База даних викориÑтовує зіÑÑ‚Ð°Ð²Ð»ÐµÐ½Ð½Ñ %s, але ці Ñтовпці викориÑтовують невідповідні зіÑтавленнÑ. Це може Ñпричинити деÑкі неÑподівані проблеми. +self_check.database_fix_mysql=Ð”Ð»Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів MySQL/MariaDB можна викориÑтати команду "gitea doctor convert" Ð´Ð»Ñ Ñ€Ð¾Ð·Ð²'ÑÐ·Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ з ÑортуваннÑм, або ви також можете розв'Ñзати проблему SQL командою "ALTER ... COLLATE ..." вручну. +self_check.database_fix_mssql=`Ð”Ð»Ñ ÐºÐ¾Ñ€Ð¸Ñтувачів MSSQL, наразі ви можете виправити цю проблему тільки через SQL запит "ALTER ... COLATE ..."` +self_check.location_origin_mismatch=Поточна URL-адреÑа (%[1]) не відповідає URL-адреÑÑ– Gitea (%[2]). Якщо ви викориÑтовуєте зворотний прокÑÑ–, переконайтеÑÑ, що заголовки "Host" та "X-Forwarded-Proto" вÑтановлені правильно. [action] create_repo=Ñтворив(ла) репозиторій <a href="%s">%s</a> rename_repo=репозиторій перейменовано з <code>%[1]s</code> на <a href="%[2]s">%[3]s</a> -commit_repo=надіÑлав зміни (push) до <a href="%[2]s">%[3]s</a> о <a href="%[1]s">%[4]s</a> +commit_repo=надіÑлав зміни (push) до <a href="%[2]s">%[3]s</a> в <a href="%[1]s">%[4]s</a> create_issue=`відкрив задачу <a href="%[1]s">%[3]s#%[2]s</a>` close_issue=`закрив задачу <a href="%[1]s">%[3]s#%[2]s</a>` reopen_issue=`повторно відкрив задачу <a href="%[1]s">%[3]s#%[2]s</a>` @@ -2439,6 +3278,7 @@ reopen_pull_request=`повторно відкрив запит Ð·Ð»Ð¸Ñ‚Ñ‚Ñ <a comment_issue=`прокоментував задачу <a href="%[1]s">%[3]s#%[2]s</a>` comment_pull=`прокоментував запит Ð·Ð»Ð¸Ñ‚Ñ‚Ñ <a href="%[1]s">%[3]s#%[2]s</a>` merge_pull_request=`прийнÑв запит Ð·Ð»Ð¸Ñ‚Ñ‚Ñ <a href="%[1]s">%[3]s#%[2]s</a>` +auto_merge_pull_request=`автоматично об'єднано запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ <a href="%[1]s">%[3]s#%[2]s</a>` transfer_repo=перенеÑено репозиторій <code>%s</code> у <a href="%s">%s</a> push_tag=Ñтворив мітку <a href="%[2]s">%[3]s</a> в <a href="%[1]s">%[4]s</a> delete_tag=видалено мітку %[2]s з <a href="%[1]s">%[3]s</a> @@ -2471,7 +3311,7 @@ seconds=%d Ñекунди minutes=%d хвилини hours=%d години days=%d дні -weeks=%d тижднів +weeks=%d тижні(в) months=%d міÑÑці years=%d роки raw_seconds=Ñекунди @@ -2479,6 +3319,7 @@ raw_minutes=хвилини [dropzone] default_message=ПеретÑгніть файли або натиÑніть тут, щоб завантажити. +invalid_input_type=Ðеможливо завантажити файли цього типу. file_too_big=Розмір файлу ({{filesize}} MB), що більше ніж макÑимальний розмір: ({{maxFilesize}} MB). remove_file=Видалити файл @@ -2492,58 +3333,292 @@ pin=Прикріпити ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ mark_as_read=Позначити Ñк прочитане mark_as_unread=Позначити Ñк непрочитане mark_all_as_read=Позначити вÑÑ– Ñк прочитані +subscriptions=Ðбонементи +watching=Стежить +no_subscriptions=Ðемає абонементів [gpg] default_key=ПідпиÑано типовим ключем error.extract_sign=Ðе вдалоÑÑ Ð²Ð¸Ñ‚Ñгти Ð¿Ñ–Ð´Ð¿Ð¸Ñ error.generate_hash=Ðе вдалоÑÑ Ð·Ð³ÐµÐ½ÐµÑ€ÑƒÐ²Ð°Ñ‚Ð¸ хеш коміту -error.no_committer_account=Ðккаунт кориÑтувача з таким Email не знайдено -error.no_gpg_keys_found=Ðе вдалоÑÑ Ð·Ð½Ð°Ð¹Ñ‚Ð¸ GPG ключ що відповідає даному підпиÑу +error.no_committer_account=Ðемає облікового запиÑу, прив'Ñзаного до адреÑи електронної пошти комітера +error.no_gpg_keys_found=Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ підпиÑу в базі даних не знайдено жодного відомого ключа error.not_signed_commit=ÐепідпиÑаний коміт -error.failed_retrieval_gpg_keys=Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ відповідний GPG ключ кориÑтувача +error.failed_retrieval_gpg_keys=Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ жодного ключа, прив'Ñзаного до облікового запиÑу комітера error.probable_bad_signature=УВÐГÐ! Хоча ключ з таким ID Ñ– Ñ” в базі, коміт не може бути ним перевірено! Цей коміт ПІДОЗРІЛИЙ. error.probable_bad_default_signature=УВÐГÐ! Хоча типовий ключ має цей ID, коміт не може бути ним перевірено! Цей коміт ПІДОЗРІЛИЙ. [units] -error.no_unit_allowed_repo=У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу до жодного розділу цього репозиториÑ. -error.unit_not_allowed=У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу до жодного розділу цього репозиториÑ. +unit=ÐžÐ´Ð¸Ð½Ð¸Ñ†Ñ Ð²Ð¸Ð¼Ñ–Ñ€ÑŽÐ²Ð°Ð½Ð½Ñ +error.no_unit_allowed_repo=У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу до жодного розділу цього Ñховища. +error.unit_not_allowed=У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу до жодного розділу цього Ñховища. [packages] +title=Пакети +desc=Керувати пакетами Ñховища. +empty=Ðаразі пакети відÑутні. +no_metadata=Ðемає метаданих. +empty.documentation=Ð”Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ¾Ð²Ð¾Ñ— інформації про реєÑтр пакетів, дивітьÑÑ <a target="_blank" rel="noopener noreferrer" href="%s">документацію</a>. +empty.repo=Ви завантажили пакунок, але він тут не відображаєтьÑÑ? Перейдіть до <a href="%[1]s">Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð°ÐºÐµÑ‚Ñƒ</a> та під'єднайте його до цього Ñховища. +registry.documentation=Докладнішу інформацію про реєÑтр %s наведено у <a target="_blank" rel="noopener noreferrer" href="%s">документації</a>. filter.type=Тип +filter.type.all=Ð’ÑÑ– +filter.no_result=Ваш фільтр не дав результатів. +filter.container.tagged=З міткою +filter.container.untagged=Без мітки +published_by=%[1]s опубліковано <a href="%[2]s">%[3]s</a> +published_by_in=%[1]s опубліковано <a href="%[2]s">%[3]s</a> у <a href="%[4]s"><strong>%[5]s</strong></a> +installation=Ð’ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ +about=Про цей пакет +requirements=Вимоги +dependencies=ЗалежноÑті +keywords=Ключові Ñлова +details=Подробиці +details.author=Ðвтор +details.project_site=Сторінка проєкту +details.repository_site=Сторінка Ñховища +details.documentation_site=Сторінка документації +details.license=Ð›Ñ–Ñ†ÐµÐ½Ð·Ñ–Ñ +assets=РеÑурÑи +versions=ВерÑÑ–Ñ— +versions.view_all=ПереглÑнути вÑе +dependency.id=ID +dependency.version=ВерÑÑ–Ñ +search_in_external_registry=Шукати в %s +alpine.registry=Ðалаштуйте цей реєÑтр, додавши URL у ваш файл <code>/etc/apk/repositories</code>: +alpine.registry.key=Завантажте публічний ключ RSA реєÑтру в теку <code>/etc/apk/keys/</code> Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸ підпиÑу індекÑу: +alpine.registry.info=Виберіть $branch та $repository зі ÑпиÑку нижче. +alpine.install=Щоб вÑтановити пакет, виконайте наÑтупну команду: +alpine.repository=Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ Ñховище alpine.repository.branches=Гілки alpine.repository.repositories=Репозиторії +alpine.repository.architectures=Ðрхітектури +arch.registry=Додати Ñервер з відповідним Ñховищем та архітектурою до <code>/etc/pacman.conf</code>: +arch.install=Синхронізувати пакет з pacman: +arch.repository=Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ Ñховище arch.repository.repositories=Репозиторії -conan.details.repository=Репозиторій +arch.repository.architectures=Ðрхітектури +cargo.registry=Ðалаштуйте цей реєÑтр у файлі конфігурації Cargo (наприклад, <code>~/.cargo/config.toml</code>): +cargo.install=Щоб вÑтановити пакет за допомогою Cargo, виконайте наÑтупну команду: +chef.registry=Ðалаштуйте цей реєÑтр у вашому файлі <code>~/.chef/config.rb</code>: +chef.install=Щоб вÑтановити пакет, виконайте наÑтупну команду: +composer.registry=Ðалаштуйте цей реєÑтр у вашому файлі <code>~/.composer/config.json</code>: +composer.install=Щоб вÑтановити пакет за допомогою Composer, виконайте наÑтупну команду: +composer.dependencies=ЗалежноÑті +composer.dependencies.development=ЗалежноÑті розробки +conan.details.repository=Сховище +conan.registry=Ðалаштувати реєÑтр із командного Ñ€Ñдка: +conan.install=Щоб вÑтановити пакет за допомогою Conan, виконайте наÑтупну команду: +conda.registry=Ðалаштуйте цей реєÑтр Ñк Ñховище Conda у влаÑному файлі <code>.condarc</code>: +conda.install=Щоб вÑтановити пакет за допомогою Conda, виконайте наÑтупну команду: +container.details.type=Тип Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ +container.details.platform=Платформа +container.pull=Завантажити образ з командного Ñ€Ñдка: +container.images=Образи +container.multi_arch=ОС / Ðрхітектура +container.layers=Шари образів +container.labels=Мітки +container.labels.key=Ключ +container.labels.value=Ð—Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ +cran.registry=Ðалаштуйте цей реєÑтр у вашому файлі <code>Rprofile.site</code>: +cran.install=Щоб вÑтановити пакет, виконайте наÑтупну команду: +debian.registry=Ðалаштувати реєÑтр із командного Ñ€Ñдка: +debian.registry.info=Оберіть $distribution та $component зі ÑпиÑку нижче. +debian.install=Щоб вÑтановити пакет, виконайте наÑтупну команду: +debian.repository=Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ Ñховище +debian.repository.distributions=ДиÑтрибутиви +debian.repository.components=Компоненти +debian.repository.architectures=Ðрхітектури +generic.download=Завантажити пакет з командного Ñ€Ñдка: +go.install=Ð’Ñтановити пакет із командного Ñ€Ñдка: +helm.registry=Ðалаштувати реєÑтр із командного Ñ€Ñдка: +helm.install=Щоб вÑтановити пакет, виконайте наÑтупну команду: +maven.registry=Ðалаштуйте цей реєÑтр у файлі <code>pom.xml</code> вашого проєкту: +maven.install=Щоб викориÑтати пакет, додайте наÑтупне до блоку <code>dependencies</code> у файлі <code>pom.xml</code>: +maven.install2=Виконати з командного Ñ€Ñдка: +maven.download=Щоб завантажити залежніÑть, виконайте в командному Ñ€Ñдку: +nuget.registry=Ðалаштувати реєÑтр із командного Ñ€Ñдка: +nuget.install=Щоб вÑтановити пакет за допомогою NuGet, запуÑтіть наÑтупну команду: +nuget.dependency.framework=Цільовий фреймворк +npm.registry=Ðалаштувати цей реєÑтр у файлі вашого проєкту <code>.npmrc</code>: +npm.install=Щоб вÑтановити пакет за допомогою npm, виконайте наÑтупну команду: +npm.install2=або додайте до файлу package.json: +npm.dependencies=ЗалежноÑті +npm.dependencies.development=ЗалежноÑті розробки +npm.dependencies.optional=ÐеобовʼÑзкові залежноÑті +npm.details.tag=Мітка +pypi.requires=Потрібен Python +rpm.registry=Ðалаштувати реєÑтр із командного Ñ€Ñдка: +rpm.distros.redhat=на диÑтрибутивах на оÑнові RedHat +rpm.distros.suse=на диÑтрибутивах на оÑнові SUSE +rpm.install=Щоб вÑтановити пакет, виконайте наÑтупну команду: +rpm.repository=Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ Ñховище +rpm.repository.architectures=Ðрхітектури +rpm.repository.multiple_groups=Цей пакет доÑтупний у багатьох групах. +rubygems.install=Щоб вÑтановити пакет за допомогою gem, виконайте наÑтупну команду: +rubygems.install2=або додайте до Gemfile: +rubygems.dependencies.development=ЗалежноÑті розробки +rubygems.required.ruby=Вимагає верÑÑ–ÑŽ Ruby +rubygems.required.rubygems=Вимагає верÑÑ–ÑŽ RubyGem +swift.registry=Ðалаштувати реєÑтр із командного Ñ€Ñдка: +swift.install=Додайте пакет у ваш файл <code>Package.swift</code>: +swift.install2=Ñ– виконайте наÑтупну команду: +vagrant.install=Щоб додати Ð±Ð¾ÐºÑ Vagrant, виконайте наÑтупну команду: +settings.link=Прив'Ñзати пакет до Ñховища +settings.link.description=Якщо ви зв'Ñжете пакет зі Ñховищем, його буде вказано у ÑпиÑку пакетів Ñховища. +settings.link.select=Обрати Ñховище +settings.link.button=Оновити поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° Ñховище +settings.link.error=Ðе вдалоÑÑ Ð¾Ð½Ð¾Ð²Ð¸Ñ‚Ð¸ поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° Ñховище. +settings.delete=Видалити пакет +settings.delete.description=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð¿Ð°ÐºÐµÑ‚Ð° Ñ” оÑтаточним Ñ– не може бути ÑкаÑоване. +settings.delete.notice=Ви збираєтеÑÑŒ видалити %s (%s). Цю операцію неможливо ÑкаÑувати, ви впевнені? +settings.delete.success=Пакет видалено. +settings.delete.error=Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ пакет. +owner.settings.cargo.title=Ð†Ð½Ð´ÐµÐºÑ Ñ€ÐµÑ”Ñтру Cargo +owner.settings.cargo.initialize=Ініціалізувати Ñ–Ð½Ð´ÐµÐºÑ +owner.settings.cargo.initialize.description=Ð”Ð»Ñ Ð²Ð¸ÐºÐ¾Ñ€Ð¸ÑÑ‚Ð°Ð½Ð½Ñ Ñ€ÐµÑ”Ñтру Cargo потрібне Ñпеціальне Ñховище індекÑів Git. ВикориÑÑ‚Ð°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ параметра дозволить (повторно) Ñтворити та автоматично налаштувати Ñховище. +owner.settings.cargo.initialize.error=Ðе вдалоÑÑ Ñ–Ð½Ñ–Ñ†Ñ–Ð°Ð»Ñ–Ð·ÑƒÐ²Ð°Ñ‚Ð¸ Ñ–Ð½Ð´ÐµÐºÑ Cargo: %v +owner.settings.cargo.initialize.success=Ð†Ð½Ð´ÐµÐºÑ Cargo уÑпішно Ñтворено. +owner.settings.cargo.rebuild=Перебудувати Ñ–Ð½Ð´ÐµÐºÑ +owner.settings.cargo.rebuild.description=Повторна збірка може бути кориÑною, Ñкщо Ñ–Ð½Ð´ÐµÐºÑ Ð½Ðµ Ñинхронізовано зі збереженими пакетами Cargo. +owner.settings.cargo.rebuild.error=Ðе вдалоÑÑ Ð¿ÐµÑ€ÐµÐ±ÑƒÐ´ÑƒÐ²Ð°Ñ‚Ð¸ Ñ–Ð½Ð´ÐµÐºÑ Cargo: %v +owner.settings.cargo.rebuild.success=Ð†Ð½Ð´ÐµÐºÑ Cargo уÑпішно перебудовано. +owner.settings.cleanuprules.title=Керувати правилами Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ +owner.settings.cleanuprules.add=Додати правило Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ +owner.settings.cleanuprules.edit=Редагувати правило Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ +owner.settings.cleanuprules.none=Правила Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð²Ñ–Ð´Ñутні. Будь лаÑка, звернітьÑÑ Ð´Ð¾ документації. +owner.settings.cleanuprules.preview=Попередній переглÑд правила Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ +owner.settings.cleanuprules.preview.overview=Заплановано Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ %d пакетів. +owner.settings.cleanuprules.preview.none=Правило Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð½Ðµ відповідає жодному пакету. owner.settings.cleanuprules.enabled=Увімкнено +owner.settings.cleanuprules.pattern_full_match=ЗаÑтоÑувати шаблон до повної назви пакета +owner.settings.cleanuprules.keep.title=ВерÑÑ–Ñ—, Ñкі відповідають цим правилам, зберігаютьÑÑ, навіть Ñкщо вони відповідають наведеному нижче правилу видаленнÑ. +owner.settings.cleanuprules.keep.count=Залишити найновіші +owner.settings.cleanuprules.keep.count.1=1 верÑÑ–Ñ Ð½Ð° пакет +owner.settings.cleanuprules.keep.count.n=%d верÑÑ–Ñ—(-й) на пакет +owner.settings.cleanuprules.keep.pattern=Зберігати верÑÑ–Ñ—, що збігаютьÑÑ +owner.settings.cleanuprules.remove.title=ВерÑÑ–Ñ—, Ñкі відповідають цим правилам, видалÑютьÑÑ, Ñкщо правило вище не вимагає Ñ—Ñ… збереженнÑ. +owner.settings.cleanuprules.remove.days=Видалити верÑÑ–Ñ—, Ñтаріші за +owner.settings.cleanuprules.remove.pattern=Видалити верÑÑ–Ñ—, що збігаютьÑÑ +owner.settings.cleanuprules.success.update=Правило Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð¾Ð½Ð¾Ð²Ð»ÐµÐ½Ð¾. +owner.settings.cleanuprules.success.delete=Правило Ð¾Ñ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð¾. +owner.settings.chef.title=РеєÑтр Chef +owner.settings.chef.keypair=Згенерувати ключову пару +owner.settings.chef.keypair.description=Ключова пара необхідна Ð´Ð»Ñ Ð°Ð²Ñ‚ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ— у реєÑтрі Chef. Якщо ви Ñтворювали ключову пару раніше, ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð½Ð¾Ð²Ð¾Ñ— пари призведе до ÑкаÑÑƒÐ²Ð°Ð½Ð½Ñ Ñтарої. [secrets] ; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=ÐžÐ¿Ð¸Ñ +creation.name_placeholder=без ÑƒÑ€Ð°Ñ…ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€ÐµÐ³Ñ–Ñтру, тільки алфавітно-цифрові Ñимволи або підкреÑленнÑ, не можуть починатиÑÑ Ð· GITEA_ або GITHUB_. +creation.value_placeholder=Введіть довільний вміÑÑ‚. Пробіли на початку та в кінці будуть пропущені. +creation.description_placeholder=Введіть короткий Ð¾Ð¿Ð¸Ñ (необов'Ñзково). [actions] +actions=Дії +unit.desc=Керувати діÑми +status.unknown=Ðевідомий +status.waiting=ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ +status.running=ВиконуєтьÑÑ +status.success=УÑпіх +status.failure=Ðевдача +status.cancelled=СкаÑовано +status.skipped=Пропущено +status.blocked=Заблоковано +runners.status=Ð¡Ñ‚Ð°Ñ‚ÑƒÑ +runners.id=ID runners.name=Ðазва runners.owner_type=Тип runners.description=ÐžÐ¿Ð¸Ñ +runners.labels=Мітки +runners.last_online=ОÑтанній раз онлайн +runners.task_list.no_tasks=Ðаразі завдань немає. runners.task_list.run=ЗапуÑтити +runners.task_list.status=Ð¡Ñ‚Ð°Ñ‚ÑƒÑ runners.task_list.repository=Репозиторій runners.task_list.commit=Коміт +runners.task_list.done_at=Завершено о +runners.update_runner=Оновити зміни +runners.status.unspecified=Ðевідомий +runners.status.idle=ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ runners.status.active=Ðктивний +runners.version=ВерÑÑ–Ñ +runners.reset_registration_token=Скинути реєÑтраційний токен +runs.all_workflows=Ð’ÑÑ– робочі процеÑи runs.commit=Коміт - - - - +runs.scheduled=Заплановано +runs.pushed_by=завантажено +runs.invalid_workflow_helper=Файл конфігурації робочих процеÑів недійÑний. Будь лаÑка, перевірте файл конфігурації: %s +runs.no_job_without_needs=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ міÑтити принаймні одну задачу без залежноÑтей. +runs.no_job=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ міÑтити принаймні одну задачу +runs.actor=Ðктор +runs.status=Ð¡Ñ‚Ð°Ñ‚ÑƒÑ +runs.actors_no_select=УÑÑ– актори +runs.status_no_select=Ð’ÑÑ– ÑтатуÑи +runs.no_results=Збігів немає. +runs.no_workflows=Робочих процеÑів наразі немає. +runs.no_workflows.quick_start=Ðе знаєте, Ñк почати з Gitea Дії? ДивітьÑÑ <a target="_blank" rel="noopener noreferrer" href="%s">поÑібник швидкого Ñтарту</a>. +runs.no_workflows.documentation=Ð”Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ¾Ð²Ð¾Ñ— інформації про Gitea Дії, переглÑньте <a target="_blank" rel="noopener noreferrer" href="%s">документацію</a>. +runs.no_runs=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ñ‰Ðµ не виконувавÑÑ. +runs.empty_commit_message=(порожнє Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ) +runs.expire_log_message=Журнали були очищені, тому що вони були занадто Ñтарі. +runs.delete=Видалити запущений робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ +runs.delete.description=Ви впевнені, що хочете оÑтаточно видалити цей робочий процеÑ? Цю дію неможливо ÑкаÑувати. +runs.not_done=Ð’Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ робочого процеÑу не завершено. +runs.view_workflow_file=ПереглÑд файлу робочого процеÑу + +workflow.disable=Вимкнути робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ +workflow.disable_success=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ '%s' уÑпішно вимкнено. +workflow.enable=Увімкнути робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ +workflow.enable_success=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ '%s' уÑпішно ввімкнено. +workflow.disabled=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð²Ð¸Ð¼ÐºÐ½ÐµÐ½Ð¸Ð¹. +workflow.run=ЗапуÑтити робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ +workflow.not_found=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ '%s' не знайдено. +workflow.run_success=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ '%s' завершивÑÑ ÑƒÑпішно. +workflow.from_ref=ВикориÑтати робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð· +workflow.has_workflow_dispatch=Цей робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ Ð¼Ð°Ñ” тригер події workflow_dispatch. +workflow.has_no_workflow_dispatch=Робочий Ð¿Ñ€Ð¾Ñ†ÐµÑ â€œ%s†не має тригера події workflow_dispatch. + + +variables=Змінні +variables.management=ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð·Ð¼Ñ–Ð½Ð½Ð¸Ð¼Ð¸ +variables.creation=Додати змінну +variables.none=Ðаразі немає змінних. +variables.deletion=Видалити змінну +variables.deletion.description=Ð’Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½Ð½Ð¾Ñ— Ñ” оÑтаточним Ñ– не може бути ÑкаÑоване. Продовжити? +variables.description=Змінні будуть передані певним діÑм Ñ– не можуть бути прочитані інакше. +variables.id_not_exist=Змінної з ідентифікатором %d не Ñ–Ñнує. +variables.edit=Редагувати змінну +variables.deletion.failed=Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ змінну. +variables.deletion.success=Змінну видалено. +variables.creation.failed=Ðе вдалоÑÑ Ð´Ð¾Ð´Ð°Ñ‚Ð¸ змінну. +variables.creation.success=Змінну "%s" додано. +variables.update.failed=Ðе вдалоÑÑ Ð²Ñ–Ð´Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ñ‚Ð¸ змінну. +variables.update.success=Змінну відредаговано. + +logs.always_auto_scroll=Завжди автоматично прокручувати журнали +logs.always_expand_running=Завжди розгортати поточні журнали [projects] +deleted.display_name=Видалений проєкт +type-1.display_name=Індивідуальний проєкт +type-2.display_name=Проєкт Ñховища +type-3.display_name=Проєкт організації +enter_fullscreen=Повноекранний режим +exit_fullscreen=Вийти з повноекранного режиму [git.filemode] +changed_filemode=%[1]s → %[2]s ; Ordered by git filemode value, ascending. E.g. directory has "040000", normal file has "100644", … +directory=Тека +normal_file=Звичайний файл +executable_file=Виконуваний файл symbolic_link=Символічне поÑÐ¸Ð»Ð°Ð½Ð½Ñ +submodule=Підмодуль diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index d91a06d205..96a6d518e2 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -4,14 +4,14 @@ explore=探索 help=帮助 logo=å¾½æ ‡ sign_in=登录 -sign_in_with_provider=使用 %s 登录 +sign_in_with_provider=使用「%sã€ç™»å½• sign_in_or=或 sign_out=退出 sign_up=注册 link_account=链接账户 register=注册 -version=当å‰ç‰ˆæœ¬ -powered_by=Powered by %s +version=版本 +powered_by=ç”± %s 强力驱动 page=é¡µé¢ template=æ¨¡æ¿ language=è¯è¨€é€‰é¡¹ @@ -28,7 +28,7 @@ return_to_gitea=返回 Gitea more_items=更多选项 username=用户å -email=电åé‚®ä»¶åœ°å€ +email=é‚®ç®±åœ°å€ password=密ç access_token=访问令牌(Access Token) re_type=确认密ç @@ -42,7 +42,7 @@ webauthn_sign_in=按下安全密钥上的按钮。如果安全密钥没有按钮 webauthn_press_button=请按下安全密钥上的按钮… webauthn_use_twofa=使用æ¥è‡ªæ‰‹æœºä¸çš„两æ¥éªŒè¯ç webauthn_error=æ— æ³•è¯»å–安全密钥。 -webauthn_unsupported_browser=ä½ çš„æµè§ˆå™¨ç›®å‰ä¸æ”¯æŒ WebAuthn。 +webauthn_unsupported_browser=您的æµè§ˆå™¨ç›®å‰ä¸æ”¯æŒ WebAuthn。 webauthn_error_unknown=å‘生未知错误。请é‡è¯•。 webauthn_error_insecure=WebAuthn 仅支æŒå®‰å…¨è¿žæŽ¥ã€‚如果è¦åœ¨ HTTP å议上进行测试,请使用 "localhost" 或 "127.0.0.1" ä½œä¸ºè®¿é—®æ¥æº webauthn_error_unable_to_process=æœåŠ¡å™¨æ— æ³•å¤„ç†æ‚¨çš„请求。 @@ -58,11 +58,11 @@ issue_milestone=里程碑 new_repo=创建仓库 new_migrate=è¿ç§»å¤–部仓库 new_mirror=åˆ›å»ºæ–°çš„é•œåƒ -new_fork=新的仓库Fork +new_fork=派生新仓库 new_org=创建组织 new_project=创建项目 new_project_column=创建列 -manage_org=ç®¡ç†æˆ‘的组织 +manage_org=管ç†ç»„织 admin_panel=管ç†åŽå° account_settings=叿ˆ·è®¾ç½® settings=设置 @@ -78,7 +78,7 @@ forks=派生 activities=最近活动 pull_requests=åˆå¹¶è¯·æ±‚ -issues=å·¥å•ç®¡ç† +issues=å·¥å• milestones=里程碑 ok=确定 @@ -91,7 +91,7 @@ add=æ·»åŠ add_all=æ·»åŠ æ‰€æœ‰ remove=移除 remove_all=移除所有 -remove_label_str=`åˆ é™¤æ ‡ç¾ "%s"` +remove_label_str=åˆ é™¤æ ‡ç¾ã€Œ%s〠edit=编辑 view=查看 test=测试 @@ -129,9 +129,9 @@ rss_feed=RSS è®¢é˜…æº pin=固定 unpin=å–æ¶ˆç½®é¡¶ -artifacts=åˆ¶å“ +artifacts=产物 expired=已过期 -confirm_delete_artifact=您确定è¦åˆ 除制å“'%s'å—? +confirm_delete_artifact=您确定è¦åˆ 除产物「%sã€å—? archived=已归档 @@ -165,7 +165,7 @@ filter.public=公开 filter.private=ç§æœ‰ no_results_found=未找到结果 -internal_error_skipped=å‘生内部错误,但已被跳过: %s +internal_error_skipped=å‘生内部错误,但已跳过: %s [search] search=æœç´¢... @@ -184,14 +184,14 @@ org_kind=æœç´¢ç»„织... team_kind=æœç´¢å›¢é˜Ÿ... code_kind=æœç´¢ä»£ç ... code_search_unavailable=ä»£ç æœç´¢å½“å‰ä¸å¯ç”¨ã€‚请与网站管ç†å‘˜è”系。 -code_search_by_git_grep=当å‰ä»£ç æœç´¢ç»“果由“git grepâ€æä¾›ã€‚å¦‚æžœç«™ç‚¹ç®¡ç†å‘˜å¯ç”¨ä»“库索引器,å¯èƒ½ä¼šæœ‰æ›´å¥½çš„结果。 +code_search_by_git_grep=当å‰ä»£ç æœç´¢ç»“果由「git grepã€æä¾›ã€‚å¦‚æžœç«™ç‚¹ç®¡ç†å‘˜å¯ç”¨ä»“库索引器,å¯èƒ½ä¼šæœ‰æ›´å¥½çš„结果。 package_kind=æœç´¢è½¯ä»¶åŒ…... project_kind=æœç´¢é¡¹ç›®... branch_kind=æœç´¢åˆ†æ”¯... tag_kind=æœç´¢æ ‡ç¾... -tag_tooltip=æœç´¢åŒ¹é…çš„æ ‡ç¾ã€‚使用“%â€æ¥åŒ¹é…任何åºåˆ—çš„æ•°å— +tag_tooltip=æœç´¢åŒ¹é…çš„æ ‡ç¾ã€‚使用「%ã€æ¥åŒ¹é…任何åºåˆ—的数å—。 commit_kind=æœç´¢æäº¤è®°å½•... -runner_kind=æœç´¢runners... +runner_kind=æœç´¢è¿è¡Œå™¨... no_results=未找到匹é…结果 issue_kind=æœç´¢å·¥å•... pull_kind=æœç´¢åˆå¹¶è¯·æ±‚... @@ -262,20 +262,20 @@ host=æ•°æ®åº“主机 user=用户å password=æ•°æ®åº“用户密ç db_name=æ•°æ®åº“åç§° -db_schema=Schema +db_schema=æž¶æž„ db_schema_helper=留空则数æ®åº“ä¸é»˜è®¤å€¼ä¸º("public")。 ssl_mode=SSL path=æ•°æ®åº“文件路径 sqlite_helper=SQLite3 æ•°æ®åº“的文件路径。<br>如果以æœåŠ¡çš„æ–¹å¼è¿è¡Œ Gitea,请输入ç»å¯¹è·¯å¾„。 reinstall_error=您æ£åœ¨å°è¯•å®‰è£…åˆ°ä¸€ä¸ªå·²ç»æœ‰ Gitea æ•°æ®çš„æ•°æ®åº“ä¸ -reinstall_confirm_message=使用现有的 Gitea æ•°æ®åº“釿–°å®‰è£…å¯èƒ½ä¼šå¯¼è‡´å¤šä¸ªé—®é¢˜ã€‚åœ¨å¤§å¤šæ•°æƒ…å†µä¸‹ï¼Œä½ åº”è¯¥ä½¿ç”¨ä½ çŽ°æœ‰çš„ “app.ini†æ¥è¿è¡Œ Giteaã€‚å¦‚æžœä½ çŸ¥é“自己在åšä»€ä¹ˆï¼Œè¯·ç¡®è®¤ä»¥ä¸‹å†…容: +reinstall_confirm_message=使用现有的 Gitea æ•°æ®åº“釿–°å®‰è£…å¯èƒ½ä¼šå¯¼è‡´å¤šä¸ªé—®é¢˜ã€‚在大多数情况下,您应该使用您现有的「app.iniã€æ¥è¿è¡Œ Gitea。如果您知é“自己在åšä»€ä¹ˆï¼Œè¯·ç¡®è®¤ä»¥ä¸‹å†…容: reinstall_confirm_check_1=使用 app.ini ä¸ SECRET KEY åŠ å¯†çš„æ•°æ®å¯èƒ½ä¼šä¸¢å¤±ï¼šç”¨æˆ·å¯èƒ½æ— 法使用 2FA/OTP 登录,仓库镜åƒå¯èƒ½æ— 法æ£å¸¸å·¥ä½œã€‚å‹¾é€‰æ¤æ¡†ï¼Œè¡¨ç¤ºæ‚¨ç¡®è®¤å½“å‰ app.ini æ–‡ä»¶åŒ…å«æ£ç¡®çš„ SECRET KEY。 reinstall_confirm_check_2=代ç 仓库和设置å¯èƒ½éœ€è¦é‡æ–°åŒæ¥ã€‚å‹¾é€‰æ¤æ¡†ï¼Œè¡¨ç¤ºæ‚¨ç¡®è®¤å°†æ‰‹åЍ釿–°åŒæ¥ä»“库和 SSH authorized_keys 的钩å。您确认您将确ä¿ä»£ç 仓库和镜åƒè®¾ç½®æ˜¯æ£ç¡®çš„。 -reinstall_confirm_check_3=ä½ ç¡®è®¤ä½ ç»å¯¹è‚¯å®šè¿™ä¸ª Gitea 在æ£ç¡®çš„ app.ini ä½ç½®ä¸Šè¿è¡Œï¼Œè€Œä¸”ä½ ç¡®å®šä½ å¿…é¡»é‡æ–°å®‰è£…ã€‚ä½ ç¡®è®¤ä½ çŸ¥æ™“ä¸Šè¿°é£Žé™©ã€‚ +reinstall_confirm_check_3=您确认您ç»å¯¹è‚¯å®šè¿™ä¸ª Gitea 在æ£ç¡®çš„ app.ini ä½ç½®ä¸Šè¿è¡Œï¼Œè€Œä¸”æ‚¨ç¡®å®šæ‚¨å¿…é¡»é‡æ–°å®‰è£…。您确认您知晓上述风险。 err_empty_db_path=SQLite æ•°æ®åº“文件路径ä¸èƒ½ä¸ºç©ºã€‚ no_admin_and_disable_registration=您ä¸èƒ½å¤Ÿåœ¨æœªåˆ›å»ºç®¡ç†å‘˜ç”¨æˆ·çš„æƒ…å†µä¸‹ç¦æ¢æ³¨å†Œã€‚ err_empty_admin_password=管ç†å‘˜å¯†ç ä¸èƒ½ä¸ºç©ºã€‚ -err_empty_admin_email=管ç†å‘˜ç”µå邮件ä¸èƒ½ä¸ºç©ºã€‚ +err_empty_admin_email=管ç†å‘˜é‚®ç®±ä¸èƒ½ä¸ºç©ºã€‚ err_admin_name_is_reserved=管ç†å‘˜ç”¨æˆ·åæ— æ•ˆï¼Œç”¨æˆ·åæ˜¯ä¿ç•™çš„ err_admin_name_pattern_not_allowed=管ç†å‘˜ç”¨æˆ·åæ— æ•ˆï¼Œç”¨æˆ·åæ˜¯ä¿ç•™å— err_admin_name_is_invalid=管ç†å‘˜ç”¨æˆ·åæ— æ•ˆ @@ -296,7 +296,7 @@ ssh_port_helper=SSH æœåŠ¡å™¨çš„ç«¯å£å·ï¼Œä¸ºç©ºåˆ™ç¦ç”¨å®ƒã€‚ http_port=HTTP æœåŠ¡ç«¯å£ http_port_helper=Giteas web æœåС噍将侦å¬çš„端å£å·ã€‚ app_url=基础URL -app_url_helper=用于 HTTP (S) 克隆和电å邮件通知的基本地å€ã€‚ +app_url_helper=用于 HTTP (S) 克隆和邮件通知的基本地å€ã€‚ log_root_path=日志路径 log_root_path_helper=日志文件将写入æ¤ç›®å½•。 @@ -304,12 +304,12 @@ optional_title=å¯é€‰è®¾ç½® email_title=电å邮箱设置 smtp_addr=SMTP ä¸»æœºåœ°å€ smtp_port=SMTP ç«¯å£ -smtp_from=电å邮件å‘件人 -smtp_from_invalid=`"å‘é€ç”µå邮件为"åœ°å€æ— 效` -smtp_from_helper=请输入一个用于 Gitea 的电å邮件地å€ï¼Œæˆ–è€…ä½¿ç”¨å®Œæ•´æ ¼å¼ï¼š"åç§°" <email@example.com> +smtp_from=邮件å‘件人 +smtp_from_invalid=「邮件å‘件人ã€åœ°å€æ— 效 +smtp_from_helper=请输入一个用于 Gitea 的邮箱地å€ï¼Œæˆ–è€…ä½¿ç”¨å®Œæ•´æ ¼å¼ï¼šã€Œåç§°ã€<email@example.com>。 mailer_user=SMTP 用户å mailer_password=SMTP 密ç -register_confirm=需è¦å‘电å邮件确认注册 +register_confirm=需è¦é‚®ä»¶ç¡®è®¤æ³¨å†Œ mail_notify=å¯ç”¨é‚®ä»¶é€šçŸ¥æé†’ server_service_title=æœåŠ¡å™¨å’Œç¬¬ä¸‰æ–¹æœåŠ¡è®¾ç½® offline_mode=å¯ç”¨æœ¬åœ°æ¨¡å¼ @@ -334,12 +334,12 @@ admin_title=管ç†å‘˜å¸å·è®¾ç½® admin_name=管ç†å‘˜ç”¨æˆ·å admin_password=管ç†å‘˜å¯†ç confirm_password=确认密ç -admin_email=电åé‚®ä»¶åœ°å€ +admin_email=é‚®ç®±åœ°å€ install_btn_confirm=ç«‹å³å®‰è£… test_git_failed=æ— æ³•è¯†åˆ« 'git' 命令:%v sqlite3_not_available=您所使用的å‘è¡Œç‰ˆä¸æ”¯æŒ SQLite3,请从 %s ä¸‹è½½å®˜æ–¹æž„å»ºç‰ˆï¼Œè€Œä¸æ˜¯ gobuild 版本。 invalid_db_setting=æ•°æ®åº“è®¾ç½®æ— æ•ˆ: %v -invalid_db_table=æ•°æ®åº“表 '%s' æ— æ•ˆï¼š %v +invalid_db_table=æ•°æ®åº“表「%sã€æ— 效:%v invalid_repo_path=ä»“åº“æ ¹ç›®å½•è®¾ç½®æ— æ•ˆï¼š%v invalid_app_data_path=应用数æ®è·¯å¾„æ— æ•ˆï¼š %v run_user_not_match=è¿è¡Œç”¨æˆ·å䏿˜¯å½“å‰çš„用户å:%s -> %s @@ -348,14 +348,14 @@ secret_key_failed=生æˆå¯†é’¥å¤±è´¥ï¼š %v save_config_failed=应用é…ç½®ä¿å˜å¤±è´¥ï¼š%v invalid_admin_setting=管ç†å‘˜å¸æˆ·è®¾ç½®æ— 效: %v invalid_log_root_path=æ—¥å¿—è·¯å¾„æ— æ•ˆ: %v -default_keep_email_private=默认情况下éšè—电åé‚®ä»¶åœ°å€ -default_keep_email_private_popup=默认情况下, éšè—æ–°ç”¨æˆ·å¸æˆ·çš„电å邮件地å€ã€‚ +default_keep_email_private=默认情况下éšè—é‚®ç®±åœ°å€ +default_keep_email_private_popup=默认情况下,éšè—æ–°ç”¨æˆ·å¸æˆ·çš„邮箱地å€ã€‚ default_allow_create_organization=默认情况下å…许创建组织 default_allow_create_organization_popup=默认情况下, å…è®¸æ–°ç”¨æˆ·å¸æˆ·åˆ›å»ºç»„织。 default_enable_timetracking=默认情况下å¯ç”¨æ—¶é—´è·Ÿè¸ª default_enable_timetracking_popup=默认情况下å¯ç”¨æ–°ä»“库的时间跟踪。 -no_reply_address=éšè—电å邮件 -no_reply_address_helper=具有éšè—电å邮件地å€çš„用户的域å。例如, 用户å "joe" 将以 "joe@noreply.example.org" 的身份登录到 Git ä¸. 如果éšè—的电å邮件域设置为 "noreply.example.org"。 +no_reply_address=éšè—邮件域 +no_reply_address_helper=具有éšè—邮箱地å€çš„用户的域å。例如,如果éšè—邮箱域å设置为「noreply.example.orgã€ï¼Œé‚£ä¹ˆç”¨æˆ·å「joeã€åœ¨ Git ä¸å°†æ˜¾ç¤ºä¸ºã€Œjoe@noreply.example.orgã€ã€‚ password_algorithm=密ç 哈希算法 invalid_password_algorithm=æ— æ•ˆçš„å¯†ç 哈希算法 password_algorithm_helper=è®¾ç½®å¯†ç æ•£åˆ—算法。算法有ä¸åŒçš„è¦æ±‚和强度。 argon2 算法相当安全,但使用大é‡å†…å˜ï¼Œå› æ¤å¯èƒ½ä¸é€‚åˆå°åž‹ç³»ç»Ÿã€‚ @@ -378,7 +378,7 @@ my_mirrors=æˆ‘çš„é•œåƒ view_home=访问 %s filter=其他过滤器 filter_by_team_repositories=按团队仓库ç›é€‰ -feed_of=`"%s"çš„æº` +feed_of=「%sã€çš„æº show_archived=已归档 show_both_archived_unarchived=显示已归档和未归档的 @@ -414,43 +414,44 @@ create_new_account=注册å¸å· already_have_account=已有账å·ï¼Ÿ sign_in_now=ç«‹å³ç™»å½• disable_register_prompt=对ä¸èµ·ï¼Œæ³¨å†ŒåŠŸèƒ½å·²è¢«å…³é—。请è”系网站管ç†å‘˜ã€‚ -disable_register_mail=å·²ç¦ç”¨æ³¨å†Œçš„电å邮件确认。 +disable_register_mail=å·²ç¦ç”¨æ³¨å†Œé‚®ä»¶ç¡®è®¤ã€‚ manual_activation_only=请è”系您的站点管ç†å‘˜æ¥å®Œæˆæ¿€æ´»ã€‚ remember_me=è®°ä½æ¤è®¾å¤‡ remember_me.compromised=登录令牌ä¸å†æœ‰æ•ˆï¼Œå› 为它å¯èƒ½è¡¨æ˜Žå¸æˆ·å·²è¢«ç ´åã€‚è¯·æ£€æŸ¥æ‚¨çš„å¸æˆ·æ˜¯å¦æœ‰å¼‚常活动。 forgot_password_title=忘记密ç forgot_password=忘记密ç ? -need_account=需è¦ä¸€ä¸ªå¸æˆ·? -sign_up_now=还没账å·ï¼Ÿé©¬ä¸Šæ³¨å†Œã€‚ +need_account=需è¦ä¸€ä¸ªå¸æˆ·ï¼Ÿ +sign_up_tip=您æ£åœ¨ç³»ç»Ÿä¸æ³¨å†Œç¬¬ä¸€ä¸ªå¸æˆ·ï¼Œå®ƒæ‹¥æœ‰ç®¡ç†å‘˜æƒé™ã€‚è¯·ä»”ç»†è®°ä½æ‚¨çš„用户å和密ç 。 å¦‚æžœæ‚¨å¿˜è®°äº†ç”¨æˆ·åæˆ–密ç ,请å‚阅 Gitea 文档以æ¢å¤è´¦æˆ·ã€‚ +sign_up_now=ç«‹å³æ³¨å†Œã€‚ sign_up_successful=叿ˆ·åˆ›å»ºæˆåŠŸã€‚æ¬¢è¿Žï¼ -confirmation_mail_sent_prompt_ex=䏀尿–°çš„确认邮件已ç»å‘é€åˆ° <b>%s</b>请在下一个 %s 䏿£€æŸ¥æ‚¨çš„æ”¶ä»¶ç®±ä»¥å®Œæˆæ³¨å†Œè¿‡ç¨‹ã€‚ 如果您的注册电å邮件地å€ä¸æ£ç¡®ï¼Œæ‚¨å¯ä»¥é‡æ–°ç™»å½•并更改它。 +confirmation_mail_sent_prompt_ex=䏀尿–°çš„确认邮件已ç»å‘é€åˆ° <b>%s</b>。请在下一个 %s 䏿£€æŸ¥æ‚¨çš„æ”¶ä»¶ç®±ä»¥å®Œæˆæ³¨å†Œæµç¨‹ã€‚ 如果您的注册邮箱地å€ä¸æ£ç¡®ï¼Œæ‚¨å¯ä»¥é‡æ–°ç™»å½•并更改它。 must_change_password=更新您的密ç allow_password_change=è¦æ±‚用户更改密ç (推è) -reset_password_mail_sent_prompt=确认电å邮件已被å‘é€åˆ° <b>%s</b>。请您在 %s 内检查您的收件箱 ,完æˆå¯†ç é‡ç½®è¿‡ç¨‹ã€‚ +reset_password_mail_sent_prompt=确认邮件已被å‘é€åˆ° <b>%s</b>。请您在 %s 内检查您的收件箱 ,完æˆå¯†ç é‡ç½®æµç¨‹ã€‚ active_your_account=æ¿€æ´»æ‚¨çš„å¸æˆ· account_activated=叿ˆ·å·²æ¿€æ´» prohibit_login=ç¦æ¢ç™»å½• prohibit_login_desc=æ‚¨çš„å¸æˆ·è¢«ç¦æ¢ç™»å½•,请与网站管ç†å‘˜è”系。 resent_limit_prompt=您请求å‘逿¿€æ´»é‚®ä»¶è¿‡äºŽé¢‘ç¹ï¼Œè¯·ç‰å¾… 3 分钟åŽå†è¯•ï¼ has_unconfirmed_mail=%s 您好,系统检测到您有一å°å‘é€è‡³ <b>%s</b> 但未被确认的邮件。如果您未收到激活邮件,或需è¦é‡æ–°å‘é€ï¼Œè¯·å•击下方的按钮。 -change_unconfirmed_mail_address=如果您的注册电å邮件地å€ä¸æ£ç¡®ï¼Œæ‚¨å¯ä»¥åœ¨æ¤æ›´æ”¹å¹¶é‡æ–°å‘逿–°çš„确认电å邮件。 +change_unconfirmed_mail_address=如果您的注册邮箱地å€ä¸æ£ç¡®ï¼Œæ‚¨å¯ä»¥åœ¨æ¤æ›´æ”¹å¹¶é‡æ–°å‘逿–°çš„确认邮件。 resend_mail=å•击æ¤å¤„釿–°å‘é€ç¡®è®¤é‚®ä»¶ email_not_associate=æ‚¨è¾“å…¥çš„é‚®ç®±åœ°å€æœªè¢«å…³è”到任何å¸å·ï¼ send_reset_mail=å‘é€è´¦æˆ·æ¢å¤é‚®ä»¶ reset_password=账户æ¢å¤ invalid_code=æ¤ç¡®è®¤å¯†é’¥æ— 效或已过期。 -invalid_code_forgot_password=ä½ çš„ç¡®è®¤ç æ— 效或者已过期,点击 <a href="%s">这里</a> 开始新的会è¯ã€‚ +invalid_code_forgot_password=æ‚¨çš„ç¡®è®¤ç æ— 效或已过期,点击 <a href="%s">这里</a> 开始新的会è¯ã€‚ invalid_password=您的密ç 与用于创建账户的密ç ä¸åŒ¹é…。 reset_password_helper=æ¢å¤è´¦æˆ· reset_password_wrong_user=您以 %s 登录,但æ¢å¤è´¦å·é“¾æŽ¥æ˜¯ç”¨äºŽ %s。 password_too_short=密ç 长度ä¸èƒ½å°‘于 %d ä½ã€‚ -non_local_account=éžæœ¬åœ°å¸æˆ·ä¸èƒ½é€šè¿‡ Gitea çš„ web ç•Œé¢æ›´æ”¹å¯†ç 。 +non_local_account=éžæœ¬åœ°å¸æˆ·ä¸èƒ½é€šè¿‡ Gitea çš„ Web ç•Œé¢æ›´æ”¹å¯†ç 。 verify=éªŒè¯ scratch_code=验è¯å£ä»¤ use_scratch_code=使用验è¯å£ä»¤ -twofa_scratch_used=ä½ å·²ç»ä½¿ç”¨äº†ä½ 的验è¯å£ä»¤ã€‚ä½ å°†ä¼šè½¬åˆ°ä¸¤æ¥éªŒè¯è®¾ç½®é¡µé¢ä»¥ä¾¿ç§»é™¤ä½ çš„æ³¨å†Œè®¾å¤‡æˆ–è€…é‡æ–°ç”Ÿæˆæ–°çš„验è¯å£ä»¤ã€‚ -twofa_passcode_incorrect=ä½ çš„éªŒè¯ç 䏿£ç¡®ã€‚å¦‚æžœä½ ä¸¢å¤±äº†ä½ çš„è®¾å¤‡ï¼Œè¯·ä½¿ç”¨ä½ çš„éªŒè¯å£ä»¤ã€‚ -twofa_scratch_token_incorrect=ä½ çš„éªŒè¯å£ä»¤ä¸æ£ç¡®ã€‚ +twofa_scratch_used=您已ç»ä½¿ç”¨äº†æ‚¨çš„验è¯å£ä»¤ã€‚您将会转到两æ¥éªŒè¯è®¾ç½®é¡µé¢ä»¥ä¾¿ç§»é™¤æ‚¨çš„æ³¨å†Œè®¾å¤‡æˆ–è€…é‡æ–°ç”Ÿæˆæ–°çš„验è¯å£ä»¤ã€‚ +twofa_passcode_incorrect=您的验è¯ç 䏿£ç¡®ã€‚如果您丢失了您的设备,请使用您的验è¯å£ä»¤ã€‚ +twofa_scratch_token_incorrect=您的验è¯å£ä»¤ä¸æ£ç¡®ã€‚ twofa_required=您必须设置两æ¥éªŒè¯æ¥è®¿é—®ä»“库,或者å°è¯•釿–°ç™»å½•。 login_userpass=登录 login_openid=OpenID @@ -463,21 +464,21 @@ oauth_signin_submit=ç»‘å®šè´¦å· oauth.signin.error.general=å¤„ç†æŽˆæƒè¯·æ±‚时出错:%s。如果æ¤é”™è¯¯ä»ç„¶å˜åœ¨ï¼Œè¯·ä¸Žç«™ç‚¹ç®¡ç†å‘˜è”系。 oauth.signin.error.access_denied=授æƒè¯·æ±‚被拒ç»ã€‚ oauth.signin.error.temporarily_unavailable=授æƒå¤±è´¥ï¼Œå› ä¸ºè®¤è¯æœåŠ¡å™¨æš‚æ—¶ä¸å¯ç”¨ã€‚请ç¨åŽå†è¯•。 -oauth_callback_unable_auto_reg=自动注册已å¯ç”¨ï¼Œä½†OAuth2 æä¾›å•† %[1]s è¿”å›žç¼ºå¤±çš„å—æ®µï¼š%[2]sï¼Œæ— æ³•è‡ªåŠ¨åˆ›å»ºå¸æˆ·ï¼Œè¯·åˆ›å»ºæˆ–é“¾æŽ¥åˆ°ä¸€ä¸ªå¸æˆ·ï¼Œæˆ–è”系站点管ç†å‘˜ã€‚ +oauth_callback_unable_auto_reg=自动注册已å¯ç”¨ï¼Œä½† OAuth2 æä¾›å•† %[1]s è¿”å›žç¼ºå¤±çš„å—æ®µï¼š%[2]sï¼Œæ— æ³•è‡ªåŠ¨åˆ›å»ºå¸æˆ·ï¼Œè¯·åˆ›å»ºæˆ–é“¾æŽ¥åˆ°ä¸€ä¸ªå¸æˆ·ï¼Œæˆ–è”系站点管ç†å‘˜ã€‚ openid_connect_submit=连接 openid_connect_title=è¿žæŽ¥åˆ°çŽ°æœ‰çš„å¸æˆ· openid_connect_desc=所选的 OpenID URI 未知。在这里关è”ä¸€ä¸ªæ–°å¸æˆ·ã€‚ openid_register_title=åˆ›å»ºæ–°å¸æˆ· openid_register_desc=所选的 OpenID URI 未知。在这里关è”ä¸€ä¸ªæ–°å¸æˆ·ã€‚ openid_signin_desc=输入您的OpenID地å€ã€‚例如:alice.openid.example.org 或 https://openid.example.org/alice. -disable_forgot_password_mail=由于未设置电åé‚®ä»¶ï¼Œå¸æˆ·æ¢å¤è¢«ç¦ç”¨ã€‚ 请è”系您的站点管ç†å‘˜ã€‚ -disable_forgot_password_mail_admin=叿ˆ·æ¢å¤ä»…在设置电å邮件åŽå¯ç”¨ã€‚ 请设置电å邮件以å¯ç”¨å¸æˆ·æ¢å¤ã€‚ -email_domain_blacklisted=您ä¸èƒ½ä½¿ç”¨æ‚¨çš„电åé‚®ä»¶åœ°å€æ³¨å†Œã€‚ +disable_forgot_password_mail=ç”±äºŽæœªè®¾ç½®é‚®ç®±ï¼Œå¸æˆ·æ¢å¤è¢«ç¦ç”¨ã€‚ 请è”系您的站点管ç†å‘˜ã€‚ +disable_forgot_password_mail_admin=叿ˆ·æ¢å¤ä»…在设置邮箱åŽå¯ç”¨ã€‚ 请设置邮箱以å¯ç”¨å¸æˆ·æ¢å¤ã€‚ +email_domain_blacklisted=您ä¸èƒ½ä½¿ç”¨æ‚¨çš„é‚®ç®±åœ°å€æ³¨å†Œã€‚ authorize_application=åº”ç”¨æŽˆæƒ authorize_redirect_notice=å¦‚æžœæ‚¨æŽˆæƒæ¤åº”用,您将会被é‡å®šå‘到 %s。 -authorize_application_created_by=æ¤åº”用由%s创建。 +authorize_application_created_by=æ¤åº”用由 %s 创建。 authorize_application_description=如果您å…许,它将能够读å–å’Œä¿®æ”¹æ‚¨çš„æ‰€æœ‰å¸æˆ·ä¿¡æ¯ï¼ŒåŒ…括ç§äººä»“库和组织。 -authorize_application_with_scopes=范围: %s +authorize_application_with_scopes=范围:%s authorize_title=æŽˆæƒ %s è®¿é—®æ‚¨çš„å¸æˆ·ï¼Ÿ authorization_failed=授æƒå¤±è´¥ authorization_failed_desc=å› ä¸ºæ£€æµ‹åˆ°æ— æ•ˆè¯·æ±‚ï¼ŒæŽˆæƒå¤±è´¥ã€‚请å°è¯•è”系您授æƒåº”用的管ç†å‘˜ã€‚ @@ -485,7 +486,7 @@ sspi_auth_failed=SSPI 认è¯å¤±è´¥ password_pwned=æ¤å¯†ç 出现在 <a target="_blank" rel="noopener noreferrer" href="%s">被盗密ç </a> 列表上并且曾ç»è¢«å…¬å¼€ã€‚ 请使用å¦ä¸€ä¸ªå¯†ç å†è¯•一次。 password_pwned_err=æ— æ³•å®Œæˆå¯¹ HaveIBeenPwned 的请求 last_admin=您ä¸èƒ½åˆ 除最åŽä¸€ä¸ªç®¡ç†å‘˜ã€‚必须至少ä¿ç•™ä¸€ä¸ªç®¡ç†å‘˜ã€‚ -signin_passkey=使用密钥登录 +signin_passkey=使用通行密钥登录 back_to_sign_in=è¿”å›žç™»å½•é¡µé¢ [mail] @@ -501,24 +502,24 @@ activate_account.text_2=请在 <b>%s</b> 时间内,点击以下链接激活您 activate_email=è¯·éªŒè¯æ‚¨çš„é‚®ç®±åœ°å€ activate_email.title=%sï¼Œè¯·éªŒè¯æ‚¨çš„邮箱 -activate_email.text=请在 <b>%s</b> 时间内,点击以下链接,以验è¯ä½ 的电å邮件地å€ï¼š +activate_email.text=请在 <b>%s</b> æ—¶é—´å†…ï¼Œç‚¹å‡»ä»¥ä¸‹é“¾æŽ¥ï¼Œä»¥éªŒè¯æ‚¨çš„邮箱地å€ï¼š register_notify=欢迎æ¥åˆ° %s register_notify.title=%[1]s,欢迎æ¥åˆ° %[2]s -register_notify.text_1=这是您的 %s 注册确认电å邮件 ï¼ +register_notify.text_1=这是您的 %s 注册确认邮件 ï¼ register_notify.text_2=您现在å¯ä»¥ä»¥ç”¨æˆ·å %s 登录。 register_notify.text_3=如果æ¤è´¦æˆ·å·²ä¸ºæ‚¨åˆ›å»ºï¼Œè¯·å…ˆ <a href="%s">设置您的密ç </a>。 reset_password=æ¢å¤æ‚¨çš„账户 reset_password.title=%s,您已请求æ¢å¤æ‚¨çš„叿ˆ· -reset_password.text=请在 <b>%s</b> 时间内,点击以下链接,æ¢å¤ä½ 的账户: +reset_password.text=请在 <b>%s</b> 时间内,点击以下链接,以æ¢å¤æ‚¨çš„账户: register_success=注册æˆåŠŸ issue_assigned.pull=@%[1]s 已将仓库 %[3]s ä¸çš„åˆå¹¶è¯·æ±‚ %[2]s 指派给您 issue_assigned.issue=@%[1]s 已将仓库 %[3]s ä¸çš„å·¥å• %[2]s 指派给您 -issue.x_mentioned_you=<b>@%s</b> æåˆ°äº†æ‚¨ï¼š +issue.x_mentioned_you=<b>@%s</b> æåŠäº†æ‚¨ï¼š issue.action.force_push=<b>%[1]s</b> 强制从 %[3]s æŽ¨é€ <b>%[2]s</b> 至 [4]s。 issue.action.push_1=<b>@%[1]s</b> 推é€äº† %[3]d 个æäº¤åˆ° %[2]s issue.action.push_n=<b>@%[1]s</b> 推é€äº† %[3]d 个æäº¤åˆ° %[2]s @@ -533,26 +534,26 @@ issue.action.ready_for_review=<b>@%[1]s</b> æ ‡è®°æ¤åˆå¹¶è¯·æ±‚已评审通过 issue.action.new=<b>@%[1]s</b> 创建了 #%[2]d. issue.in_tree_path=在 %s ä¸ï¼š -release.new.subject=%[2]s ä¸çš„ %[1]s å‘布了 +release.new.subject=%[2]s ä¸çš„ %[1]s å·²å‘布 release.new.text=<b>@%[1]s</b> 于 %[3]s å‘布了 %[2]s -release.title=æ ‡é¢˜ï¼š %s +release.title=æ ‡é¢˜ï¼š%s release.note=注释: release.downloads=下载: -release.download.zip=æºä»£ç (ZIP) -release.download.targz=æºä»£ç (TAR.GZ) +release.download.zip=æºä»£ç (ZIP) +release.download.targz=æºä»£ç (TAR.GZ) -repo.transfer.subject_to=%s 想è¦å°† "%s" 转让给 %s -repo.transfer.subject_to_you=%s 想è¦å°† "%s" è½¬è®©ç»™ä½ -repo.transfer.to_you=ä½ +repo.transfer.subject_to=%s 想è¦å°†ã€Œ%sã€è½¬ç§»ç»™ %s +repo.transfer.subject_to_you=%s 想è¦å°†ã€Œ%sã€è½¬ç§»ç»™æ‚¨ +repo.transfer.to_you=您 repo.transfer.body=访问 %s ä»¥æŽ¥å—æˆ–æ‹’ç»è½¬ç§»ï¼Œäº¦å¯å¿½ç•¥æ¤é‚®ä»¶ã€‚ -repo.collaborator.added.subject=%s æŠŠä½ æ·»åŠ åˆ°äº† %s -repo.collaborator.added.text=æ‚¨å·²è¢«æ·»åŠ ä¸ºä»£ç 库的å作者: +repo.collaborator.added.subject=%s æŠŠæ‚¨æ·»åŠ åˆ°äº† %s +repo.collaborator.added.text=æ‚¨å·²è¢«æ·»åŠ ä¸ºä»“åº“çš„å作者: team_invite.subject=%[1]s é‚€è¯·æ‚¨åŠ å…¥ç»„ç»‡ %[2]s team_invite.text_1=%[1]s é‚€è¯·æ‚¨åŠ å…¥ç»„ç»‡ %[3]s ä¸çš„团队 %[2]s。 team_invite.text_2=请点击下é¢çš„é“¾æŽ¥åŠ å…¥å›¢é˜Ÿï¼š -team_invite.text_3=注æ„:这是å‘é€ç»™ %[1]s 的邀请。如果您未曾收到过æ¤ç±»é‚€è¯·ï¼Œè¯·å¿½ç•¥è¿™å°ç”µå邮件。 +team_invite.text_3=注æ„:æ¤é‚€è¯·æ˜¯å‘é€ç»™ %[1]s 的。如果您未预期收到æ¤é‚€è¯·ï¼Œè¯·å¿½ç•¥è¿™å°é‚®ä»¶ã€‚ [modal] yes=确认æ“作 @@ -592,9 +593,9 @@ size_error=长度必须为 %s。 min_size_error=长度最å°ä¸º %s 个å—符。 max_size_error=长度最大为 %s 个å—符。 email_error=䏿˜¯ä¸€ä¸ªæœ‰æ•ˆçš„邮箱地å€ã€‚ -url_error=`'%s' 䏿˜¯ä¸€ä¸ªæœ‰æ•ˆçš„ URL。` -include_error=`必须包å«åå—符串 "%s"。` -glob_pattern_error=`åŒ¹é…æ¨¡å¼æ— 效:%s.` +url_error=`「%sã€ä¸æ˜¯ä¸€ä¸ªæœ‰æ•ˆçš„ URL。` +include_error=`必须包å«åå—符串「%sã€ã€‚` +glob_pattern_error=`匹é…è¡¨è¾¾å¼æ— 效:%s.` regex_pattern_error=`æ£åˆ™è¡¨è¾¾å¼æ— 效:%s.` username_error=` åªèƒ½åŒ…å«å—æ¯æ•°å—å—符('0-9','a-z','A-Z'), ç ´æŠ˜å· ('-'), 下划线 ('_') 和点 ('.'). ä¸èƒ½ä»¥éžå—æ¯æ•°å—å—符开头或结尾,并且ä¸å…许连ç»çš„éžå—æ¯æ•°å—å—符。` invalid_group_team_map_error=`æ˜ å°„æ— æ•ˆï¼š %s` @@ -603,26 +604,26 @@ captcha_incorrect=验è¯ç 䏿£ç¡®ã€‚ password_not_match=密ç ä¸åŒ¹é…。 lang_select_error=从列表ä¸é€‰å‡ºè¯è¨€ -username_been_taken=用户å已被使用。 +username_been_taken=用户å已使用。 username_change_not_local_user=éžæœ¬åœ°ç”¨æˆ·ä¸å…许更改用户å。 -change_username_disabled=更改用户å已被ç¦ç”¨ã€‚ +change_username_disabled=更改用户åå·²ç¦ç”¨ã€‚ change_full_name_disabled=更改用户全åå·²ç¦ç”¨ username_has_not_been_changed=ç”¨æˆ·åæœªæ›´æ”¹ -repo_name_been_taken=仓库å称已被使用。 -repository_force_private=â€œå¼ºåˆ¶ç§æœ‰â€å·²å¯ç”¨ï¼šç§æœ‰ä»“库ä¸èƒ½è¢«å…¬å¼€ã€‚ +repo_name_been_taken=仓库å称已使用。 +repository_force_private=ã€Œå¼ºåˆ¶ç§æœ‰ã€å·²å¯ç”¨ï¼šç§æœ‰ä»“库ä¸èƒ½è¢«å…¬å¼€ã€‚ repository_files_already_exist=æ¤ä»“库已å˜åœ¨æ–‡ä»¶ã€‚请è”系系统管ç†å‘˜ã€‚ repository_files_already_exist.adopt=æ¤ä»“库已å˜åœ¨æ–‡ä»¶ï¼Œåªèƒ½è¢«æ”¶å½•。 repository_files_already_exist.delete=æ¤ä»“库已å˜åœ¨æ–‡ä»¶ï¼Œå¿…é¡»å…ˆåˆ é™¤ä»–ä»¬ã€‚ repository_files_already_exist.adopt_or_delete=æ¤ä»“库已å˜åœ¨æ–‡ä»¶ï¼Œè¦ä¹ˆåˆ 除他们,è¦ä¹ˆæ”¶å½•他们。 visit_rate_limit=远程访问达到速度é™åˆ¶ã€‚ 2fa_auth_required=远程访问需è¦åŒé‡éªŒè¯ã€‚ -org_name_been_taken=组织å称已被使用。 -team_name_been_taken=团队å称已被使用。 +org_name_been_taken=组织å称已使用。 +team_name_been_taken=团队å称已使用。 team_no_units_error=至少选择一项仓库å•元。 -email_been_used=该电å邮件地å€å·²åœ¨ä½¿ç”¨ä¸ã€‚ +email_been_used=该邮箱地å€å·²åœ¨ä½¿ç”¨ä¸ã€‚ email_invalid=æ¤é‚®ç®±åœ°å€æ— 效。 email_domain_is_not_allowed=用户 <b>%s</b> 与EMAIL_DOMAIN_ALLOWLIT 或 EMAIL_DOMAIN_BLOCKLIT 冲çªã€‚è¯·ç¡®ä¿æ‚¨çš„æ“ä½œæ˜¯é¢„æœŸçš„ã€‚ -openid_been_used=OpenID åœ°å€ "%s" 已被使用。 +openid_been_used=OpenID 地å€ã€Œ%sã€å·²è¢«ä½¿ç”¨ã€‚ username_password_incorrect=ç”¨æˆ·åæˆ–密ç 䏿£ç¡®ã€‚ password_complexity=å¯†ç æœªè¾¾åˆ°å¤æ‚ç¨‹åº¦è¦æ±‚: password_lowercase_one=至少一个å°å†™å—符 @@ -637,14 +638,14 @@ unset_password=登录用户没有设置密ç 。 unsupported_login_type=æ¤ç™»å½•ç±»åž‹ä¸æ”¯æŒæ‰‹åŠ¨åˆ é™¤å¸æˆ·ã€‚ user_not_exist=该用户ä¸å˜åœ¨ team_not_exist=团队ä¸å˜åœ¨ -last_org_owner=您ä¸èƒ½ä»Ž "所有者" 团队ä¸åˆ 除最åŽä¸€ä¸ªç”¨æˆ·ã€‚组织ä¸å¿…须至少有一个所有者。 -cannot_add_org_to_team=组织ä¸èƒ½è¢«åŠ å…¥åˆ°å›¢é˜Ÿä¸ã€‚ +last_org_owner=您ä¸èƒ½ä»Žã€Œæ‰€æœ‰è€…ã€å›¢é˜Ÿä¸åˆ 除最åŽä¸€ä¸ªç”¨æˆ·ã€‚组织ä¸å¿…须至少有一个所有者。 +cannot_add_org_to_team=组织ä¸èƒ½åŠ å…¥åˆ°å›¢é˜Ÿä¸ã€‚ duplicate_invite_to_team=æ¤ç”¨æˆ·å·²è¢«é‚€è¯·ä¸ºå›¢é˜Ÿæˆå‘˜ã€‚ organization_leave_success=您已æˆåŠŸç¦»å¼€ç»„ç»‡ %s。 -invalid_ssh_key=æ— æ³•éªŒè¯æ‚¨çš„ SSH 密钥: %s -invalid_gpg_key=æ— æ³•éªŒè¯æ‚¨çš„ GPG 密钥: %s -invalid_ssh_principal=æ— æ•ˆçš„è§„åˆ™ï¼š %s +invalid_ssh_key=æ— æ³•éªŒè¯æ‚¨çš„ SSH 密钥:%s +invalid_gpg_key=æ— æ³•éªŒè¯æ‚¨çš„ GPG 密钥:%s +invalid_ssh_principal=æ— æ•ˆçš„è§„åˆ™ï¼š%s must_use_public_key=您æä¾›çš„密钥是ç§é’¥ã€‚ä¸è¦åœ¨ä»»ä½•åœ°æ–¹ä¸Šä¼ æ‚¨çš„ç§é’¥ï¼Œè¯·æ”¹ç”¨æ‚¨çš„公钥。 unable_verify_ssh_key=æ— æ³•éªŒè¯ SSH å¯†é’¥ï¼Œè¯·ä»”ç»†æ£€æŸ¥æ˜¯å¦æœ‰é”™è¯¯ã€‚ auth_failed=授æƒéªŒè¯å¤±è´¥ï¼š%v @@ -677,27 +678,27 @@ follow=关注 unfollow=å–æ¶ˆå…³æ³¨ user_bio=简历 disabled_public_activity=该用户已éšè—活动记录。 -email_visibility.limited=所有已认è¯ç”¨æˆ·å‡å¯çœ‹åˆ°æ‚¨çš„电åé‚®ä»¶åœ°å€ -email_visibility.private=åªæœ‰ä½ 本人和管ç†å‘˜å¯ä»¥çœ‹åˆ°ä½ 的电åé‚®ä»¶åœ°å€ +email_visibility.limited=所有已认è¯ç”¨æˆ·å‡å¯çœ‹åˆ°æ‚¨çš„é‚®ç®±åœ°å€ +email_visibility.private=åªæœ‰æ‚¨æœ¬äººå’Œç®¡ç†å‘˜å¯ä»¥çœ‹åˆ°æ‚¨çš„é‚®ç®±åœ°å€ show_on_map=在地图上显示这个ä½ç½® settings=用户设置 -form.name_reserved=用户å "%s" 被ä¿ç•™ã€‚ -form.name_pattern_not_allowed=用户åä¸ä¸å…许使用 "%s" æ ¼å¼ã€‚ -form.name_chars_not_allowed=用户å "%s" åŒ…å«æ— 效å—符。 +form.name_reserved=用户å「%sã€è¢«ä¿ç•™ã€‚ +form.name_pattern_not_allowed=用户åä¸ä¸å…许使用「%sã€æ ¼å¼ã€‚ +form.name_chars_not_allowed=用户å「%sã€åŒ…嫿— 效å—符。 block.block=å±è”½ block.block.user=å±è”½ç”¨æˆ· block.block.org=å±è”½ç”¨æˆ·è®¿é—®ç»„织 -block.block.failure=å±è”½ç”¨æˆ·å¤±è´¥: %s +block.block.failure=å±è”½ç”¨æˆ·å¤±è´¥ï¼š%s block.unblock=å–æ¶ˆå±è”½ -block.unblock.failure=å±è”½ç”¨æˆ·å¤±è´¥: %s +block.unblock.failure=å–æ¶ˆå±è”½ç”¨æˆ·å¤±è´¥ï¼š%s block.blocked=您已å±è”½æ¤ç”¨æˆ·ã€‚ block.title=å±è”½ä¸€ä¸ªç”¨æˆ· block.info=å±è”½ç”¨æˆ·ä¼šé˜»æ¢ä»–们与仓库进行交互,例如打开或评论åˆå¹¶è¯·æ±‚或出现问题。了解更多关于å±è”½ç”¨æˆ·çš„ä¿¡æ¯ã€‚ block.info_1=阻æ¢ç”¨æˆ·åœ¨æ‚¨çš„叿ˆ·å’Œä»“库ä¸è¿›è¡Œä»¥ä¸‹æ“作: -block.info_2=å…³æ³¨ä½ çš„è´¦å· -block.info_3=通过@æåŠæ‚¨çš„用户å呿‚¨å‘é€é€šçŸ¥ +block.info_2=å…³æ³¨æ‚¨çš„è´¦å· +block.info_3=通过 @ æåŠæ‚¨çš„用户å呿‚¨å‘é€é€šçŸ¥ block.info_4=邀请您作为åä½œè€…åˆ°ä»–ä»¬çš„ä»“åº“ä¸ block.info_5=在仓库ä¸ç‚¹èµžã€æ´¾ç”Ÿæˆ–关注 block.info_6=æ‰“å¼€å’Œè¯„è®ºå·¥å•æˆ–åˆå¹¶è¯·æ±‚ @@ -730,18 +731,18 @@ uid=UID webauthn=两æ¥éªŒè¯ï¼ˆå®‰å…¨å¯†é’¥ï¼‰ public_profile=å…¬å¼€ä¿¡æ¯ -biography_placeholder=å‘Šè¯‰æˆ‘ä»¬ä¸€ç‚¹æ‚¨è‡ªå·±ï¼ (您å¯ä»¥ä½¿ç”¨Markdown) -location_placeholder=ä¸Žä»–äººåˆ†äº«ä½ çš„å¤§æ¦‚ä½ç½® -profile_desc=控制您的个人资料对其他用户的显示方å¼ã€‚您的主è¦ç”µå邮件地å€å°†ç”¨äºŽé€šçŸ¥ã€å¯†ç æ¢å¤å’ŒåŸºäºŽç½‘页界é¢çš„ Git æ“作 -password_username_disabled=您ä¸è¢«å…è®¸æ›´æ”¹ä½ çš„ç”¨æˆ·å。更多详情请è”系您的系统管ç†å‘˜ã€‚ -password_full_name_disabled=您ä¸è¢«å…è®¸æ›´æ”¹ä½ çš„å…¨å。请è”系您的站点管ç†å‘˜äº†è§£æ›´å¤šè¯¦æƒ…。 +biography_placeholder=å‘Šè¯‰æˆ‘ä»¬ä¸€ç‚¹æ‚¨è‡ªå·±ï¼ (您å¯ä»¥ä½¿ç”¨ Markdown) +location_placeholder=与他人分享您的大概ä½ç½® +profile_desc=控制您的个人资料对其他用户的显示方å¼ã€‚您的主邮箱地å€å°†ç”¨äºŽé€šçŸ¥ã€å¯†ç æ¢å¤å’ŒåŸºäºŽç½‘页的 Git æ“作。 +password_username_disabled=您ä¸è¢«å…许更改您的用户å。更多详情请è”系您的系统管ç†å‘˜ã€‚ +password_full_name_disabled=您ä¸è¢«å…许更改您的全å。请è”系您的站点管ç†å‘˜äº†è§£æ›´å¤šè¯¦æƒ…。 full_name=自定义åç§° website=个人网站 location=所在地区 update_theme=更新主题 update_profile=æ›´æ–°ä¿¡æ¯ update_language=æ›´æ–°è¯è¨€ -update_language_not_found=è¯è¨€ %s ä¸å¯ç”¨ã€‚ +update_language_not_found=è¯è¨€ã€Œ%sã€ä¸å¯ç”¨ã€‚ update_language_success=è¯è¨€å·²æ›´æ–°ã€‚ update_profile_success=您的资料信æ¯å·²ç»æ›´æ–° change_username=您的用户å已更改。 @@ -752,7 +753,7 @@ cancel=å–æ¶ˆæ“作 language=界é¢è¯è¨€ ui=主题 hidden_comment_types=éšè—的评论类型 -hidden_comment_types_description=æ¤å¤„选ä¸çš„æ³¨é‡Šç±»åž‹ä¸ä¼šæ˜¾ç¤ºåœ¨é—®é¢˜é¡µé¢ä¸ã€‚æ¯”å¦‚ï¼Œå‹¾é€‰â€æ ‡ç¾â€œåˆ 除所有 "<user> æ·»åŠ /åˆ é™¤çš„ <label>" 注释。 +hidden_comment_types_description=æ¤å¤„选ä¸çš„æ³¨é‡Šç±»åž‹ä¸ä¼šæ˜¾ç¤ºåœ¨å·¥å•页é¢ä¸ã€‚æ¯”å¦‚ï¼Œå‹¾é€‰ã€Œæ ‡è®°ã€åˆ 除所有「{user} æ·»åŠ /åˆ é™¤çš„ {label}ã€æ³¨é‡Šã€‚ hidden_comment_types.ref_tooltip=注释æ¤é—®é¢˜åœ¨ä½•处被æåŠè¿‡ï¼Œå¦‚å¦ä¸€ä¸ªé—®é¢˜ã€ä»£ç æäº¤ç‰ hidden_comment_types.issue_ref_tooltip=注释用户在何处更改了与æ¤é—®é¢˜ç›¸å…³è”的分支/æ ‡ç¾ comment_type_group_reference=引用 @@ -774,7 +775,7 @@ privacy=éšç§è®¾ç½® keep_activity_private=éšè—个人资料页é¢ä¸çš„æ´»åЍ keep_activity_private_popup=使活动仅对您和管ç†å‘˜å¯è§ -lookup_avatar_by_mail=从电åé‚®ç®±åœ°å€æŸ¥æ‰¾å¤´åƒ +lookup_avatar_by_mail=ä»Žé‚®ç®±åœ°å€æŸ¥æ‰¾å¤´åƒ federated_avatar_lookup=Federated Avatar 查找 enable_custom_avatar=å¯åŠ¨è‡ªå®šä¹‰å¤´åƒ choose_new_avatar=é€‰æ‹©æ–°çš„å¤´åƒ @@ -798,61 +799,61 @@ emails=é‚®ç®±åœ°å€ manage_emails=管ç†é‚®ç®±åœ°å€ manage_themes=选择默认主题 manage_openid=ç®¡ç† OpenID åœ°å€ -email_desc=您的主è¦ç”µå邮件地å€å°†ç”¨äºŽé€šçŸ¥ã€å¯†ç æ¢å¤ï¼ŒåŸºäºŽç½‘页界é¢çš„Gitæ“作(åªè¦å®ƒä¸æ˜¯è®¾ç½®ä¸ºéšè—çš„)。 +email_desc=您的主邮箱地å€å°†ç”¨äºŽé€šçŸ¥ã€å¯†ç æ¢å¤ä»¥åŠåŸºäºŽç½‘页的 Git æ“作(如果它未设为éšè—)。 theme_desc=这将是您在整个网站上的默认主题。 theme_colorblindness_help=颜色障ç¢ä¸»é¢˜æ”¯æŒ -theme_colorblindness_prompt=Gitea åªèƒ½èŽ·å¾—ä¸€äº›åŸºæœ¬çš„é¢œè‰²éšœç¢æ”¯æŒï¼Œè¿™äº›ä¸»é¢˜åªå®šä¹‰äº†å°‘数颜色。 这项工作ä»åœ¨è¿›è¡Œä¸ï¼Œå¯ä»¥é€šè¿‡åœ¨ä¸»é¢˜çš„ CSS 文件ä¸å®šä¹‰æ›´å¤šé¢œè‰²æ¥åšæ›´å¤šçš„æ”¹è¿›ã€‚ +theme_colorblindness_prompt=Gitea åªèƒ½èŽ·å¾—ä¸€äº›åŸºæœ¬çš„é¢œè‰²éšœç¢æ”¯æŒï¼Œè¿™äº›ä¸»é¢˜åªå®šä¹‰äº†å°‘数颜色。这项工作ä»åœ¨è¿›è¡Œä¸ï¼Œå¯ä»¥é€šè¿‡åœ¨ä¸»é¢˜çš„ CSS 文件ä¸å®šä¹‰æ›´å¤šé¢œè‰²æ¥åšæ›´å¤šçš„æ”¹è¿›ã€‚ primary=ä¸»è¦ activated=已激活 requires_activation=éœ€è¦æ¿€æ´» -primary_email=设为主è¦é‚®ä»¶åœ°å€ +primary_email=设为主邮箱 activate_email=å‘逿¿€æ´»é‚®ä»¶ activations_pending=ç‰å¾…激活 -can_not_add_email_activations_pending=有一个待处ç†çš„æ¿€æ´»è¯·æ±‚,请ç¨ç‰å‡ 分钟åŽå†å°è¯•æ·»åŠ æ–°çš„ç”µå邮件地å€ã€‚ +can_not_add_email_activations_pending=有一个待处ç†çš„æ¿€æ´»è¯·æ±‚,请ç¨ç‰å‡ 分钟åŽå†å°è¯•æ·»åŠ æ–°çš„é‚®ç®±åœ°å€ã€‚ delete_email=移除 -email_deletion=移除电åé‚®ä»¶åœ°å€ -email_deletion_desc=电å邮箱地å€å’Œç›¸å…³ä¿¡æ¯å°†ä¼šè¢«åˆ 除。使用æ¤ç”µå邮箱地å€å‘é€çš„Gitæäº¤å°†ä¼šä¿ç•™ï¼Œç»§ç»ï¼Ÿ -email_deletion_success=您的电å邮箱地å€å·²è¢«ç§»é™¤ã€‚ +email_deletion=ç§»é™¤é‚®ç®±åœ°å€ +email_deletion_desc=邮箱地å€å’Œç›¸å…³ä¿¡æ¯å°†ä¼šè¢«åˆ 除。使用æ¤é‚®ç®±åœ°å€å‘é€çš„Gitæäº¤å°†ä¼šä¿ç•™ï¼Œç»§ç»ï¼Ÿ +email_deletion_success=您的邮箱地å€å·²ç§»é™¤ã€‚ theme_update_success=您的主题已更新。 theme_update_error=所选主题ä¸å˜åœ¨ã€‚ openid_deletion=移除 OpenID åœ°å€ -openid_deletion_desc=åˆ é™¤æ¤ OpenID 地å€å°†ä¼šé˜»æ¢ä½ ä½¿ç”¨å®ƒè¿›è¡Œç™»å½•ã€‚ä½ ç¡®å®šè¦ç»§ç»å—? -openid_deletion_success=OpenID地å€å·²è¢«ç§»é™¤ã€‚ +openid_deletion_desc=åˆ é™¤æ¤ OpenID 地å€å°†ä¼šé˜»æ¢æ‚¨ä½¿ç”¨å®ƒè¿›è¡Œç™»å½•。您确定è¦ç»§ç»å—? +openid_deletion_success=OpenID 地å€å·²ç§»é™¤ã€‚ add_new_email=æ·»åŠ æ–°çš„é‚®ç®±åœ°å€ add_new_openid=æ·»åŠ æ–°çš„ OpenID URI -add_email=å¢žåŠ ç”µåé‚®ä»¶åœ°å€ +add_email=æ–°å¢žé‚®ç®±åœ°å€ add_openid=æ·»åŠ OpenID URI -add_email_confirmation_sent=一å°ç¡®è®¤é‚®ä»¶å·²ç»è¢«å‘é€è‡³ %s,请检查您的收件箱并在 %s 内完æˆç¡®è®¤æ³¨å†Œæ“作。 -add_email_success=新的电å邮件地å€å·²æ·»åŠ ã€‚ -email_preference_set_success=电å邮件首选项已æˆåŠŸè®¾ç½®ã€‚ +add_email_confirmation_sent=一å°ç¡®è®¤é‚®ä»¶å·²ç»å‘é€è‡³ã€Œ%sã€ï¼Œè¯·æ£€æŸ¥æ‚¨çš„æ”¶ä»¶ç®±å¹¶åœ¨ %s 内完æˆç¡®è®¤æ³¨å†Œæ“作。 +add_email_success=新邮箱地å€å·²æ·»åŠ ã€‚ +email_preference_set_success=邮件首选项已æˆåŠŸè®¾ç½®ã€‚ add_openid_success=æ–°çš„ OpenID 地å€å·²æ·»åŠ ã€‚ -keep_email_private=éšè—电åé‚®ä»¶åœ°å€ -keep_email_private_popup=这将会éšè—您的电å邮件地å€ï¼Œä¸ä»…在您的个人资料ä¸ï¼Œè¿˜åœ¨æ‚¨ä½¿ç”¨Web界é¢åˆ›å»ºåˆå¹¶è¯·æ±‚或编辑文件时。已推é€çš„æäº¤å°†ä¸ä¼šè¢«ä¿®æ”¹ã€‚在æäº¤ä¸ä½¿ç”¨ %s 以和您的账å·å…³è”。 -openid_desc=OpenID è®©ä½ å¯ä»¥å°†è®¤è¯è½¬å‘到外部æœåŠ¡ã€‚ +keep_email_private=éšè—é‚®ç®±åœ°å€ +keep_email_private_popup=这将会éšè—您的邮箱地å€ï¼Œä¸ä»…在您的个人资料ä¸ï¼Œè¿˜åœ¨æ‚¨ä½¿ç”¨ Web 界é¢åˆ›å»ºåˆå¹¶è¯·æ±‚或编辑文件时。已推é€çš„æäº¤å°†ä¸ä¼šè¢«ä¿®æ”¹ã€‚在æäº¤ä¸ä½¿ç”¨ %s 以和您的账å·å…³è”。 +openid_desc=OpenID 让您å¯ä»¥å°†è®¤è¯è½¬å‘到外部æœåŠ¡ã€‚ manage_ssh_keys=ç®¡ç† SSH 密钥 -manage_ssh_principals=管ç†SSHè¯ä¹¦è§„则 +manage_ssh_principals=ç®¡ç† SSH è¯ä¹¦è§„则 manage_gpg_keys=ç®¡ç† GPG 密钥 add_key=å¢žåŠ å¯†é’¥ -ssh_desc=这些 SSH 公钥已ç»å…³è”åˆ°ä½ çš„è´¦å·ã€‚相应的ç§é’¥æ‹¥æœ‰å®Œå…¨æ“ä½œä½ çš„ä»“åº“çš„æƒé™ã€‚ -principal_desc=这些SSHè¯ä¹¦è§„则已关è”åˆ°ä½ çš„è´¦å·å°†å…è®¸å®Œå…¨è®¿é—®ä½ çš„æ‰€æœ‰ä»“åº“ã€‚ -gpg_desc=这些 GPG 公钥已ç»å…³è”åˆ°ä½ çš„è´¦å·ã€‚请妥善ä¿ç®¡ä½ çš„ç§é’¥å› ä¸ºä»–ä»¬å°†è¢«ç”¨äºŽè®¤è¯æäº¤ã€‚ +ssh_desc=这些 SSH 公钥已ç»å…³è”到您的账å·ã€‚相应的ç§é’¥æ‹¥æœ‰å®Œå…¨æ“作您仓库的æƒé™ã€‚ +principal_desc=这些 SSH è¯ä¹¦è§„则已关è”到您的账å·å°†å…许完全访问您所有仓库。 +gpg_desc=这些 GPG 公钥已ç»å…³è”到您的账å·ã€‚请妥善ä¿ç®¡æ‚¨çš„ç§é’¥å› ä¸ºä»–ä»¬å°†è¢«ç”¨äºŽè®¤è¯æäº¤ã€‚ ssh_helper=<strong>需è¦å¸®åŠ©ï¼Ÿ</strong> 请查看有关 <a href="%s">å¦‚ä½•ç”Ÿæˆ SSH 密钥</a> 或 <a href="%s">å¸¸è§ SSH 问题</a> å¯»æ‰¾ç”æ¡ˆã€‚ -gpg_helper=<strong>需è¦å¸®åŠ©å—?</strong>看一看 GitHub <a href="%s">关于GPG</a> 的指导。 +gpg_helper=<strong>需è¦å¸®åŠ©ï¼Ÿ</strong>看一看 GitHub <a href="%s">关于 GPG</a> 的指导。 add_new_key=å¢žåŠ SSH 密钥 add_new_gpg_key=æ·»åŠ çš„ GPG 密钥 key_content_ssh_placeholder=以 'ssh-ed25519'〠'ssh-rsa'〠'ecdsa-sha2-nistp256'ã€'ecdsa-sha2-nistp384'ã€'ecdsa-sha2-nistp521'〠'sk-ecdsa-sha2-nistp256@openssh.com' 或 'sk-ssh-ed25519@openssh.com' 开头 key_content_gpg_placeholder=以 '-----BEGIN PGP PUBLIC KEY BLOCK-----' 开头 add_new_principal=æ·»åŠ è§„åˆ™ ssh_key_been_used=æ¤ SSH å¯†é’¥å·²æ·»åŠ åˆ°æœåŠ¡å™¨ã€‚ -ssh_key_name_used=使用相åŒåç§°çš„SSH公钥已ç»å˜åœ¨ï¼ +ssh_key_name_used=使用相åŒåç§°çš„ SSH 公钥已ç»å˜åœ¨ã€‚ ssh_principal_been_used=æ¤è§„则已ç»åŠ å…¥åˆ°äº†æœåŠ¡å™¨ã€‚ -gpg_key_id_used=使用相åŒåç§°çš„GPG公钥已ç»å˜åœ¨ï¼ -gpg_no_key_email_found=æ¤ GPG å¯†é’¥ä¸Žæ‚¨å¸æˆ·å…³è”的任何已激活电å邮件地å€å‡ä¸åŒ¹é…。如果您在æä¾›çš„令牌上ç¾å,它ä»ç„¶å¯ä»¥è¢«æ·»åŠ ã€‚ +gpg_key_id_used=使用相åŒåç§°çš„ GPG 公钥已ç»å˜åœ¨ã€‚ +gpg_no_key_email_found=æ¤ GPG å¯†é’¥ä¸Žæ‚¨å¸æˆ·å…³è”的任何已激活邮箱地å€å‡ä¸åŒ¹é…。如果您已对æä¾›çš„令牌进行ç¾å,ä»å¯æ·»åŠ è¯¥å¯†é’¥ã€‚ gpg_key_matched_identities=匹é…的身份: -gpg_key_matched_identities_long=æ¤å¯†é’¥ä¸åŒ…å«çš„身份信æ¯ä¸Žä¸‹é¢è¿™ä¸ªè¯¥ç”¨æˆ·å·²æ¿€æ´»ç”µåé‚®ä»¶åœ°å€æ˜¯ç›¸åŒ¹é…çš„ã€‚å› æ¤ï¼Œèƒ½ä¸Žè¿™äº›ç”µå邮件地å€ç›¸åŒ¹é…çš„æäº¤å¯ä»¥é€šè¿‡æ¤å¯†é’¥è¿›è¡ŒéªŒè¯ã€‚ +gpg_key_matched_identities_long=æ¤å¯†é’¥ä¸åŒ…å«çš„身份信æ¯ä¸Žä»¥ä¸‹æ¤ç”¨æˆ·å·²æ¿€æ´»é‚®ç®±åœ°å€åŒ¹é…。与这些邮箱地å€ç›¸åŒ¹é…çš„æäº¤å¯é€šè¿‡æ¤å¯†é’¥è¿›è¡ŒéªŒè¯ã€‚ gpg_key_verified=已验è¯çš„密钥 -gpg_key_verified_long=密钥已ç»ç”¨ä»¤ç‰Œè¿›è¡Œäº†éªŒè¯ï¼Œå¹¶ä¸”å¯ä»¥ç”¨æ¥éªŒè¯åŒ¹é…æ¤ç”¨æˆ·ä»»ä½•已激活电å邮件地å€çš„æäº¤ï¼Œä»¥åŠåŒ¹é…æ¤å¯†é’¥çš„任何身份。 +gpg_key_verified_long=密钥已通过令牌验è¯ï¼Œé™¤ä¸Žæ¤å¯†é’¥åŒ¹é…的任何身份外,还å¯ç”¨äºŽéªŒè¯ä¸Žè¯¥ç”¨æˆ·ä»»ä½•已激活邮箱地å€åŒ¹é…çš„æäº¤ã€‚ gpg_key_verify=éªŒè¯ gpg_invalid_token_signature=æä¾›çš„ GPG 密钥ã€ç¾å和令牌ä¸åŒ¹é…或过期。 gpg_token_required=您必须为下é¢çš„令牌æä¾›ç¾å @@ -860,9 +861,9 @@ gpg_token=令牌 gpg_token_help=您å¯ä»¥ä½¿ç”¨ä»¥ä¸‹æ–¹å¼ç”Ÿæˆç¾å: gpg_token_signature=GPG 增强ç¾å key_signature_gpg_placeholder=以 '-----BEGIN PGP PUBLIC KEY BLOCK-----' 开头 -verify_gpg_key_success=GPG 密钥 %s 已被验è¯ã€‚ +verify_gpg_key_success=GPG 密钥「%sã€å·²éªŒè¯ã€‚ ssh_key_verified=已验è¯çš„密钥 -ssh_key_verified_long=密钥已ç»ç”¨ä»¤ç‰Œè¿›è¡Œäº†éªŒè¯ï¼Œå¹¶ä¸”å¯ä»¥ç”¨æ¥éªŒè¯åŒ¹é…æ¤ç”¨æˆ·ä»»ä½•已激活电å邮件地å€çš„æäº¤ã€‚ +ssh_key_verified_long=密钥已通过令牌验è¯ï¼Œå¯ç”¨äºŽéªŒè¯ä¸Žè¯¥ç”¨æˆ·ä»»ä½•已激活邮箱地å€åŒ¹é…çš„æäº¤ã€‚ ssh_key_verify=éªŒè¯ ssh_invalid_token_signature=æä¾›çš„ SSH 密钥ã€ç¾å或令牌ä¸åŒ¹é…或令牌已过期。 ssh_token_required=您必须为下é¢çš„令牌æä¾›ç¾å @@ -870,24 +871,24 @@ ssh_token=令牌 ssh_token_help=您å¯ä»¥ä½¿ç”¨ä»¥ä¸‹æ–¹å¼ç”Ÿæˆç¾å: ssh_token_signature=增强 SSH ç¾å key_signature_ssh_placeholder=以 '-----BEGIN SSH SIGNATURE -----' 开头 -verify_ssh_key_success=SSH 密钥 %s 已被验è¯ã€‚ +verify_ssh_key_success=SSH 密钥「%sã€å·²éªŒè¯ã€‚ subkeys=å项 -key_id=é”®ID +key_id=密钥 ID key_name=密钥åç§° key_content=密钥内容 principal_content=内容 -add_key_success=SSH 密钥 %s æ·»åŠ æˆåŠŸã€‚ -add_gpg_key_success=GPG 密钥 %s æ·»åŠ æˆåŠŸã€‚ -add_principal_success=SSHè¯ä¹¦è§„则 %s æ·»åŠ æˆåŠŸã€‚ +add_key_success=SSH 密钥「%sã€æ·»åŠ æˆåŠŸã€‚ +add_gpg_key_success=GPG 密钥「%sã€æ·»åŠ æˆåŠŸã€‚ +add_principal_success=SSH è¯ä¹¦è§„则「%sã€æ·»åŠ æˆåŠŸã€‚ delete_key=åˆ é™¤ ssh_key_deletion=åˆ é™¤ SSH 密钥 gpg_key_deletion=åˆ é™¤ GPG 密钥 ssh_principal_deletion=åˆ é™¤ SSH è¯ä¹¦è§„则 ssh_key_deletion_desc=åˆ é™¤ SSH 公钥将喿¶ˆå¯¹åº”çš„ç§é’¥å¯¹æ‚¨çš„ Gitea 叿ˆ·çš„访问æƒé™ã€‚ç»§ç»ï¼Ÿ -gpg_key_deletion_desc=åˆ é™¤ GPG å…¬é’¥å°†æ— æ³•è®¤çŸ¥ä½¿ç”¨å¯¹åº”ç§é’¥ç¾åçš„æäº¤ï¼Œç»§ç»ï¼Ÿ +gpg_key_deletion_desc=åˆ é™¤ GPG å…¬é’¥å°†æ— æ³•è®¤è¯å¯¹åº”ç§é’¥ç¾åçš„æäº¤ï¼Œç»§ç»ï¼Ÿ ssh_principal_deletion_desc=åˆ é™¤æ¤ SSH è¯ä¹¦è§„åˆ™å°†å–æ¶ˆå®ƒå¯¹æ‚¨çš„账户的访问æƒé™ã€‚ç»§ç»ï¼Ÿ -ssh_key_deletion_success=GPG å¯†é’¥å·²è¢«åˆ é™¤ã€‚ -gpg_key_deletion_success=GPG å¯†é’¥å·²è¢«åˆ é™¤ã€‚ +ssh_key_deletion_success=SSH å¯†é’¥å·²åˆ é™¤ã€‚ +gpg_key_deletion_success=GPG å¯†é’¥å·²åˆ é™¤ã€‚ ssh_principal_deletion_success=æ¤è§„åˆ™åˆ é™¤æˆåŠŸã€‚ added_on=æ·»åŠ äºŽ %s valid_until_date=有效期至 %s @@ -901,7 +902,7 @@ token_state_desc=7 天内使用过该密钥 principal_state_desc=7 天内使用过该规则 show_openid=在个人信æ¯ä¸Šæ˜¾ç¤º hide_openid=在个人信æ¯ä¸Šéšè— -ssh_disabled=SSH 被ç¦ç”¨ +ssh_disabled=SSH å·²ç¦ç”¨ ssh_signonly=SSH ç›®å‰å·²ç¦ç”¨ï¼Œå› æ¤è¿™äº›å¯†é’¥ä»…用于æäº¤ç¾å验è¯ã€‚ ssh_externally_managed=æ¤ SSH 密钥是由外部管ç†çš„ manage_social=管ç†å…³è”ç¤¾äº¤å¸æˆ· @@ -921,10 +922,10 @@ access_token_deletion=åˆ é™¤ Access Token access_token_deletion_cancel_action=å–æ¶ˆ access_token_deletion_confirm_action=刪除 access_token_deletion_desc=åˆ é™¤ä»¤ç‰Œå°†æ’¤é”€ç¨‹åºå¯¹æ‚¨è´¦æˆ·çš„访问æƒé™ã€‚æ¤æ“ä½œæ— æ³•æ’¤æ¶ˆã€‚æ˜¯å¦ç»§ç»ï¼Ÿ -delete_token_success=令牌已ç»è¢«åˆ 除。使用该令牌的应用将ä¸å†èƒ½å¤Ÿè®¿é—®ä½ 的账å·ã€‚ +delete_token_success=令牌已ç»è¢«åˆ 除。使用该令牌的应用将ä¸å†èƒ½å¤Ÿè®¿é—®æ‚¨çš„è´¦å·ã€‚ repo_and_org_access=仓库和组织访问æƒé™ permissions_public_only=仅公开 -permissions_access_all=全部(公开ã€ç§æœ‰å’Œå—é™) +permissions_access_all=全部(公开ã€ç§æœ‰å’Œå—é™ï¼‰ permission_not_set=未设置 permission_no_access=æ— è®¿é—®æƒé™ permission_read=å¯è¯» @@ -933,7 +934,7 @@ permission_anonymous_read=匿å读 permission_everyone_read=所有人å¯è¯» permission_everyone_write=所有人å¯å†™ access_token_desc=所选令牌æƒé™ä»…é™äºŽå¯¹åº”çš„ <a %s>API</a> 路由的授æƒã€‚阅读 <a %s>文档</a> ä»¥èŽ·å–æ›´å¤šä¿¡æ¯ã€‚ -at_least_one_permission=ä½ éœ€è¦é€‰æ‹©è‡³å°‘一个æƒé™æ‰èƒ½åˆ›å»ºä»¤ç‰Œ +at_least_one_permission=您需è¦é€‰æ‹©è‡³å°‘一个æƒé™æ‰èƒ½åˆ›å»ºä»¤ç‰Œ permissions_list=æƒé™ï¼š manage_oauth2_applications=ç®¡ç† OAuth2 åº”ç”¨ç¨‹åº @@ -941,13 +942,13 @@ edit_oauth2_application=编辑 OAuth2 åº”ç”¨ç¨‹åº oauth2_applications_desc=OAuth2 应用å…许第三方应用程åºåœ¨æ¤ Gitea 实例ä¸å®‰å…¨éªŒè¯ç”¨æˆ·ã€‚ remove_oauth2_application=åˆ é™¤ OAuth2 åº”ç”¨ç¨‹åº remove_oauth2_application_desc=åˆ é™¤ OAuth2 应用将撤销所有ç¾å的访问令牌。继ç»å—? -remove_oauth2_application_success=è¯¥åº”ç”¨å·²è¢«åˆ é™¤ã€‚ +remove_oauth2_application_success=è¯¥åº”ç”¨å·²åˆ é™¤ã€‚ create_oauth2_application=创建新的 OAuth2 åº”ç”¨ç¨‹åº create_oauth2_application_button=创建应用 create_oauth2_application_success=您已æˆåŠŸåˆ›å»ºäº†ä¸€ä¸ªæ–°çš„ OAuth2 应用。 update_oauth2_application_success=您已æˆåŠŸæ›´æ–°äº†æ¤ OAuth2 应用。 oauth2_application_name=应用åç§° -oauth2_confidential_client=æœºå¯†å®¢æˆ·ç«¯ã€‚æ˜¯å¦æ˜¯èƒ½å¤Ÿç»´æŒå‡æ®æœºå¯†æ€§çš„应用,比如网页应用程åºã€‚如果是本地应用程åºè¯·ä¸è¦å‹¾é€‰ï¼ŒåŒ…括桌é¢å’Œç§»åŠ¨ç«¯åº”ç”¨ã€‚ +oauth2_confidential_client=机密客户端。对于需è¦ä¿å¯†çš„应用(例如 Web 应用),请选择æ¤é€‰é¡¹ã€‚对于包括桌é¢å’Œç§»åŠ¨åº”ç”¨åœ¨å†…çš„æœ¬æœºåº”ç”¨ï¼Œè¯·å‹¿é€‰æ‹©æ¤é€‰é¡¹ã€‚ oauth2_skip_secondary_authorization=首次授æƒåŽå…è®¸å…¬å…±å®¢æˆ·ç«¯è·³è¿‡æŽˆæƒæ¥éª¤ã€‚ <strong>å¯èƒ½ä¼šå¸¦æ¥å®‰å…¨é£Žé™©ã€‚</strong> oauth2_redirect_uris=é‡å®šå‘ URI。æ¯è¡Œä¸€ä¸ª URI。 save_application=ä¿å˜ @@ -959,7 +960,7 @@ oauth2_client_secret_hint=您离开或刷新æ¤é¡µé¢åŽå°†ä¸ä¼šå†æ˜¾ç¤ºæ¤å¯† oauth2_application_edit=编辑 oauth2_application_create_description=OAuth2 应用å…许您的第三方应用程åºè®¿é—®æ¤å®žä¾‹çš„ç”¨æˆ·å¸æˆ·ã€‚ oauth2_application_remove_description=移除一个OAuth2应用将会阻æ¢å®ƒè®¿é—®æ¤å®žä¾‹ä¸Šçš„已授æƒç”¨æˆ·è´¦æˆ·ã€‚是å¦ç»§ç»ï¼Ÿ -oauth2_application_locked=如果é…ç½®å¯ç”¨ï¼ŒGitea预注册一些OAuth2应用程åºã€‚ ä¸ºäº†é˜²æ¢æ„外的行为, 这些应用既ä¸èƒ½ç¼–辑也ä¸èƒ½åˆ 除。请å‚阅OAuth2æ–‡æ¡£ä»¥èŽ·å–æ›´å¤šä¿¡æ¯ã€‚ +oauth2_application_locked=如果é…ç½®å¯ç”¨ï¼ŒGitea 将在å¯åŠ¨æ—¶é¢„æ³¨å†Œä¸€äº› OAuth2 应用程åºã€‚ ä¸ºäº†é˜²æ¢æ„外的行为, 这些应用既ä¸èƒ½ç¼–辑也ä¸èƒ½åˆ 除。请å‚阅 OAuth2 æ–‡æ¡£ä»¥èŽ·å–æ›´å¤šä¿¡æ¯ã€‚ authorized_oauth2_applications=已授æƒçš„ OAuth2 应用 authorized_oauth2_applications_description=您已授予这些第三方应用程åºè®¿é—®æ‚¨çš„个人 Gitea 账户的æƒé™ã€‚请撤销那些您ä¸å†éœ€è¦çš„应用程åºçš„访问æƒé™ã€‚ @@ -968,10 +969,10 @@ revoke_oauth2_grant=撤回æƒé™ revoke_oauth2_grant_description=确定撤销æ¤ä¸‰æ–¹åº”用程åºçš„æŽˆæƒï¼Œå¹¶é˜»æ¢æ¤åº”用程åºè®¿é—®æ‚¨çš„æ•°æ®ï¼Ÿ revoke_oauth2_grant_success=æˆåŠŸæ’¤é”€äº†è®¿é—®æƒé™ã€‚ -twofa_desc=ä¸ºä¿æŠ¤ä½ çš„è´¦å·å¯†ç å®‰å…¨ï¼Œä½ å¯ä»¥ä½¿ç”¨æ™ºèƒ½æ‰‹æœºæˆ–å…¶å®ƒè®¾å¤‡æ¥æŽ¥æ”¶æ—¶é—´å¼ºç›¸å…³çš„ä¸€æ¬¡æ€§å¯†ç (TOTP)。 +twofa_desc=ä¸ºä¿æŠ¤æ‚¨çš„è´¦å·å¯†ç 安全,您å¯ä»¥ä½¿ç”¨æ™ºèƒ½æ‰‹æœºæˆ–å…¶å®ƒè®¾å¤‡æ¥æŽ¥æ”¶æ—¶é—´å¼ºç›¸å…³çš„ä¸€æ¬¡æ€§å¯†ç (TOTP)。 twofa_recovery_tip=如果您丢失了您的设备,您将能够使用一次性æ¢å¤å¯†é’¥æ¥é‡æ–°èŽ·å¾—å¯¹æ‚¨è´¦æˆ·çš„è®¿é—®ã€‚ -twofa_is_enrolled=ä½ çš„è´¦å·<strong>å·²å¯ç”¨</strong>了两æ¥éªŒè¯ã€‚ -twofa_not_enrolled=ä½ çš„è´¦å·æœªå¼€å¯ä¸¤æ¥éªŒè¯ã€‚ +twofa_is_enrolled=您的账å·<strong>å·²å¯ç”¨</strong>了两æ¥éªŒè¯ã€‚ +twofa_not_enrolled=æ‚¨çš„è´¦å·æœªå¼€å¯ä¸¤æ¥éªŒè¯ã€‚ twofa_disable=ç¦ç”¨ä¸¤æ¥è®¤è¯ twofa_scratch_token_regenerate=釿–°ç”Ÿæˆåˆå§‹ä»¤ç‰Œ twofa_scratch_token_regenerated=您的åˆå§‹ä»¤ç‰ŒçŽ°åœ¨æ˜¯ %sã€‚å°†å…¶å˜æ”¾åœ¨å®‰å…¨çš„地方,它将ä¸ä¼šå†æ¬¡æ˜¾ç¤ºã€‚ @@ -979,12 +980,12 @@ twofa_enroll=å¯ç”¨ä¸¤æ¥éªŒè¯ twofa_disable_note=如果需è¦, å¯ä»¥ç¦ç”¨åŒå› ç´ èº«ä»½éªŒè¯ã€‚ twofa_disable_desc=关掉两æ¥éªŒè¯ä¼šä½¿å¾—您的账å·ä¸å®‰å…¨ï¼Œç»§ç»æ‰§è¡Œï¼Ÿ regenerate_scratch_token_desc=如果您丢失了您的æ¢å¤å¯†é’¥æˆ–å·²ç»ä½¿ç”¨å®ƒç™»å½•, 您å¯ä»¥åœ¨è¿™é‡Œé‡ç½®å®ƒã€‚ -twofa_disabled=两æ¥éªŒè¯å·²è¢«ç¦ç”¨ã€‚ +twofa_disabled=两æ¥éªŒè¯å·²ç¦ç”¨ã€‚ scan_this_image=使用您的授æƒåº”用扫æè¿™å¼ 图片: or_enter_secret=或者输入密钥:%s then_enter_passcode=并输入应用程åºä¸æ˜¾ç¤ºçš„密ç : passcode_invalid=密ç 䏿£ç¡®ã€‚å†è¯•一次。 -twofa_enrolled=ä½ çš„è´¦å·å·²ç»å¯ç”¨äº†ä¸¤æ¥éªŒè¯ã€‚请ä¿å˜åˆå§‹ä»¤ç‰Œï¼ˆ%s)到一个安全的地方,æ¤ä»¤ç‰Œä»…显示一次。 +twofa_enrolled=您的账å·å·²ç»å¯ç”¨äº†ä¸¤æ¥éªŒè¯ã€‚请ä¿å˜åˆå§‹ä»¤ç‰Œï¼ˆ%s)到一个安全的地方,æ¤ä»¤ç‰Œä»…显示一次。 twofa_failed_get_secret=èŽ·å– secret 失败。 webauthn_desc=安全密钥是包å«åŠ å¯†å¯†é’¥çš„ç¡¬ä»¶è®¾å¤‡ã€‚å®ƒä»¬å¯ä»¥ç”¨äºŽåŒå› ç´ èº«ä»½éªŒè¯ã€‚å®‰å…¨å¯†é’¥å¿…é¡»æ”¯æŒ <a rel="noreferrer" target="_blank" href="%s">WebAuthn 身份验è¯å™¨</a> æ ‡å‡†ã€‚ @@ -1003,23 +1004,23 @@ remove_account_link=åˆ é™¤å·²ç»‘å®šçš„è´¦å· remove_account_link_desc=åˆ é™¤å·²ç»‘å®šå¸æˆ·å°†åŠé”€å…¶å¯¹æ‚¨çš„ Gitea 叿ˆ·çš„访问æƒé™ã€‚ç»§ç»ï¼Ÿ remove_account_link_success=已喿¶ˆç»‘å®šå¸æˆ·ã€‚ -hooks.desc=æ·»åŠ Webhooks,它们将会在您拥有的<strong>所有仓库</strong>ä¸Šè§¦å‘ +hooks.desc=æ·»åŠ Web é’©å,它们将会在您拥有的<strong>所有仓库</strong>上触å‘。 orgs_none=æ‚¨çŽ°åœ¨è¿˜ä¸æ˜¯ä»»ä½•组织的æˆå‘˜ã€‚ -repos_none=ä½ å¹¶ä¸æ‹¥æœ‰ä»»ä½•仓库。 +repos_none=æ‚¨å¹¶ä¸æ‹¥æœ‰ä»»ä½•仓库。 delete_account=åˆ é™¤å½“å‰å¸æˆ· -delete_prompt=æ¤æ“ä½œå°†æ°¸ä¹…åˆ é™¤æ‚¨çš„ç”¨æˆ·å¸æˆ·ã€‚它 <strong>ä¸èƒ½</strong> 被撤消。 -delete_with_all_comments=ä½ çš„å¸æˆ·å¹´é¾„å°äºŽ %s。为了é¿å…å¹½çµè¯„论,所有工å•/åˆå¹¶è¯·æ±‚çš„è¯„è®ºéƒ½å°†ä¸Žå®ƒä¸€èµ·è¢«åˆ é™¤ã€‚ +delete_prompt=æ¤æ“ä½œå°†æ°¸ä¹…åˆ é™¤æ‚¨çš„ç”¨æˆ·å¸æˆ·ã€‚它 <strong>æ— æ³•</strong> 被撤消。 +delete_with_all_comments=æ‚¨çš„å¸æˆ·å¹´é¾„å°äºŽ %s。为了é¿å…å¹½çµè¯„论,所有工å•/åˆå¹¶è¯·æ±‚çš„è¯„è®ºéƒ½å°†ä¸Žå®ƒä¸€èµ·è¢«åˆ é™¤ã€‚ confirm_delete_account=ç¡®è®¤åˆ é™¤å¸æˆ· delete_account_title=åˆ é™¤å½“å‰å¸æˆ· delete_account_desc=ç¡®å®žè¦æ°¸ä¹…åˆ é™¤æ¤ç”¨æˆ·å¸æˆ·å—? email_notifications.enable=å¯ç”¨é‚®ä»¶é€šçŸ¥ -email_notifications.onmention=åªåœ¨è¢«æåˆ°æ—¶é‚®ä»¶é€šçŸ¥ +email_notifications.onmention=仅被æåŠæ—¶é€šçŸ¥ email_notifications.disable=åœç”¨é‚®ä»¶é€šçŸ¥ -email_notifications.submit=邮件通知设置 -email_notifications.andyourown=和您自己的通知 +email_notifications.submit=设置邮件通知 +email_notifications.andyourown=仅与您相关的通知 visibility=用户å¯è§æ€§ visibility.public=公开 @@ -1034,9 +1035,9 @@ new_repo_helper=代ç 仓库包å«äº†æ‰€æœ‰çš„项目文件,包括版本历å²è owner=拥有者 owner_helper=由于最大仓库数é‡é™åˆ¶ï¼Œä¸€äº›ç»„织å¯èƒ½ä¸ä¼šæ˜¾ç¤ºåœ¨ä¸‹æ‹‰åˆ—表ä¸ã€‚ repo_name=仓库åç§° -repo_name_profile_public_hint=.profile 是一个特殊的å˜å‚¨åº“,您å¯ä»¥ä½¿ç”¨å®ƒå°† README.md æ·»åŠ åˆ°æ‚¨çš„å…¬å…±ç»„ç»‡èµ„æ–™ä¸ï¼Œä»»ä½•人都å¯ä»¥çœ‹åˆ°ã€‚请确ä¿å®ƒæ˜¯å…¬å¼€çš„,并使用个人资料目录ä¸çš„ README 对其进行åˆå§‹åŒ–以开始使用。 -repo_name_profile_private_hint=.profile-private 是一个特殊的å˜å‚¨åº“,您å¯ä»¥ä½¿ç”¨å®ƒå‘您的组织æˆå‘˜ä¸ªäººèµ„æ–™æ·»åŠ README.md,仅对组织æˆå‘˜å¯è§ã€‚请确ä¿å®ƒæ˜¯ç§æœ‰çš„,并使用个人资料目录ä¸çš„ README 对其进行åˆå§‹åŒ–以开始使用。 -repo_name_helper=ç†æƒ³çš„仓库å称应由简çŸã€æœ‰æ„义和独特的关键è¯ç»„æˆã€‚".profile" å’Œ ".profile-private" å¯ç”¨äºŽä¸ºç”¨æˆ·/ç»„ç»‡æ·»åŠ README.md。 +repo_name_profile_public_hint=.profile 是一个特殊的仓库,您å¯ä»¥ä½¿ç”¨å®ƒå°† README.md æ·»åŠ åˆ°æ‚¨çš„å…¬å…±ç»„ç»‡èµ„æ–™ä¸ï¼Œä»»ä½•人都å¯ä»¥çœ‹åˆ°ã€‚请确ä¿å®ƒæ˜¯å…¬å¼€çš„,并使用个人资料目录ä¸çš„ README 对其进行åˆå§‹åŒ–以开始使用。 +repo_name_profile_private_hint=.profile-private 是一个特殊的仓库,您å¯ä»¥ä½¿ç”¨å®ƒå‘您的组织æˆå‘˜ä¸ªäººèµ„æ–™æ·»åŠ README.md,仅对组织æˆå‘˜å¯è§ã€‚请确ä¿å®ƒæ˜¯ç§æœ‰çš„,并使用个人资料目录ä¸çš„ README 对其进行åˆå§‹åŒ–以开始使用。 +repo_name_helper=ç†æƒ³çš„仓库å称应由简çŸã€æœ‰æ„义和独特的关键è¯ç»„æˆã€‚「.profileã€å’Œã€Œ.profile-privateã€å¯ç”¨äºŽä¸ºç”¨æˆ·/ç»„ç»‡æ·»åŠ README.md。 repo_size=ä»“åº“å¤§å° template=æ¨¡æ¿ template_select=é€‰æ‹©æ¨¡æ¿ @@ -1050,10 +1051,10 @@ visibility_fork_helper=(修改该值将会影å“到所有派生仓库) clone_helper=ä¸çŸ¥é“如何克隆?查看<a target="_blank" rel="noopener noreferrer" href="%s">帮助</a> 。 fork_repo=派生仓库 fork_from=派生自 -already_forked=ä½ å·²ç»æ´¾ç”Ÿè¿‡ %s +already_forked=æ‚¨å·²ç»æ´¾ç”Ÿè¿‡ %s fork_to_different_account=æ´¾ç”Ÿåˆ°å…¶ä»–è´¦å· fork_visibility_helper=æ— æ³•æ›´æ”¹æ´¾ç”Ÿä»“åº“çš„å¯è§æ€§ã€‚ -fork_branch=è¦å…‹éš†åˆ° Fork 的分支 +fork_branch=è¦å…‹éš†ä¸ºæ´¾ç”Ÿçš„分支 all_branches=所有分支 view_all_branches=查看所有分支 view_all_tags=æŸ¥çœ‹æ‰€æœ‰æ ‡ç¾ @@ -1061,30 +1062,31 @@ fork_no_valid_owners=这个代ç ä»“åº“æ— æ³•è¢«æ´¾ç”Ÿï¼Œå› ä¸ºæ²¡æœ‰æœ‰æ•ˆçš„æ‰ fork.blocked_user=æ— æ³•å…‹éš†ä»“åº“ï¼Œå› ä¸ºæ‚¨è¢«ä»“åº“æ‰€æœ‰è€…å±è”½ã€‚ use_template=ä½¿ç”¨æ¤æ¨¡æ¿ open_with_editor=用 %s 打开 + download_zip=下载 ZIP download_tar=下载 TAR.GZ download_bundle=下载 BUNDLE generate_repo=生æˆä»“库 generate_from=生æˆè‡ª -repo_desc=仓库æè¿° +repo_desc=æè¿° repo_desc_helper=è¾“å…¥ç®€è¦æè¿° (å¯é€‰) repo_no_desc=æ— è¯¦ç»†ä¿¡æ¯ -repo_lang=编程è¯è¨€ -repo_gitignore_helper=选择 .gitignore 模æ¿ã€‚ +repo_lang=è¯è¨€ +repo_gitignore_helper=选择 .gitignore æ¨¡æ¿ repo_gitignore_helper_desc=从常è§è¯è¨€çš„æ¨¡æ¿åˆ—表ä¸é€‰æ‹©å¿½ç•¥è·Ÿè¸ªçš„æ–‡ä»¶ã€‚é»˜è®¤æƒ…å†µä¸‹ï¼Œç”±å¼€å‘æˆ–构建工具生æˆçš„特殊文件都包å«åœ¨ .gitignore ä¸ã€‚ issue_labels=工啿 ‡ç¾ issue_labels_helper=é€‰æ‹©ä¸€ä¸ªå·¥å•æ ‡ç¾é›† license=授æƒè®¸å¯ -license_helper=选择授æƒè®¸å¯æ–‡ä»¶ã€‚ -license_helper_desc=许å¯è¯è¯´æ˜Žäº†å…¶ä»–人å¯ä»¥å’Œä¸å¯ä»¥ç”¨æ‚¨çš„代ç åšä»€ä¹ˆã€‚ä¸ç¡®å®šå“ªä¸€ä¸ªé€‚åˆä½ çš„é¡¹ç›®ï¼Ÿè§ <a target="_blank" rel="noopener noreferrer" href="%s">选择一个许å¯è¯</a> +license_helper=选择授æƒè®¸å¯æ–‡ä»¶ +license_helper_desc=许å¯è¯è¯´æ˜Žäº†å…¶ä»–人å¯ä»¥å’Œä¸å¯ä»¥ç”¨æ‚¨çš„代ç åšä»€ä¹ˆã€‚ä¸ç¡®å®šå“ªä¸€ä¸ªé€‚åˆæ‚¨çš„é¡¹ç›®ï¼Ÿè§ <a target="_blank" rel="noopener noreferrer" href="%s">选择一个许å¯è¯</a> multiple_licenses=多许å¯è¯ object_format=å¯¹è±¡æ ¼å¼ object_format_helper=ä»“åº“çš„å¯¹è±¡æ ¼å¼ã€‚ä¹‹åŽæ— 法更改。SHA1 是最兼容的。 readme=自述 readme_helper=选择自述文件模æ¿ã€‚ readme_helper_desc=这是您å¯ä»¥ä¸ºæ‚¨çš„项目撰写完整æè¿°çš„地方。 -auto_init=åˆå§‹åŒ–仓库(æ·»åŠ . gitignoreã€è®¸å¯è¯å’Œè‡ªè¿°æ–‡ä»¶) -trust_model_helper=选择ç¾å验è¯çš„“信任模型â€ã€‚å¯èƒ½çš„选项是: +auto_init=åˆå§‹åŒ–ä»“åº“ï¼ˆæ·»åŠ .gitignoreã€è®¸å¯è¯å’Œè‡ªè¿°æ–‡ä»¶ï¼‰ +trust_model_helper=选择ç¾å验è¯çš„信任模型。å¯èƒ½çš„选项是: trust_model_helper_collaborator=å作者:信任å作者的ç¾å trust_model_helper_committer=æäº¤è€…:信任与æäº¤è€…相符的ç¾å trust_model_helper_collaborator_committer=å作者+æäº¤è€…:信任åä½œè€…åŒæ—¶æ˜¯æäº¤è€…çš„ç¾å @@ -1106,13 +1108,13 @@ mirror_address_protocol_invalid=æä¾›çš„URLæ— æ•ˆã€‚åªèƒ½ä½¿ç”¨http(s)://或gi mirror_lfs=大文件å˜å‚¨ (LFS) mirror_lfs_desc=é•œåƒ LFS æ•°æ®ã€‚ mirror_lfs_endpoint=LFS ç½‘å€ -mirror_lfs_endpoint_desc=åŒæ¥å°†å°è¯•ä½¿ç”¨å…‹éš†ç½‘å€æ¥ <a target="_blank" rel="noopener noreferrer" href="%s">确定 LFS æœåС噍</a>。如果仓库 LFS æ•°æ®å˜å‚¨åœ¨å…¶ä»–ä½ç½®ï¼Œä½ 还å¯ä»¥æŒ‡å®šè‡ªå®šä¹‰ç½‘å€ã€‚ +mirror_lfs_endpoint_desc=åŒæ¥å°†å°è¯•ä½¿ç”¨å…‹éš†ç½‘å€æ¥ <a target="_blank" rel="noopener noreferrer" href="%s">确定 LFS æœåС噍</a>。如果仓库 LFS æ•°æ®å˜å‚¨åœ¨å…¶ä»–ä½ç½®ï¼Œæ‚¨è¿˜å¯ä»¥æŒ‡å®šè‡ªå®šä¹‰ç½‘å€ã€‚ mirror_last_synced=ä¸Šæ¬¡åŒæ¥ mirror_password_placeholder=(未更改) mirror_password_blank_placeholder=(未设置) mirror_password_help=更改用户åä»¥åˆ é™¤å·²å‚¨å˜çš„密ç 。 watchers=关注者 -stargazers=称赞者 +stargazers=已赞者 stars_remove_warning=这将清除æ¤ä»“库的所有点赞数。 forks=派生仓库 stars=点赞数 @@ -1136,11 +1138,11 @@ user_search_tooltip=最多显示30å用户 tree_path_not_found=%[2]s ä¸ä¸å˜åœ¨è·¯å¾„ %[1]s transfer.accept=接å—转移 -transfer.accept_desc=`转移到 "%s"` +transfer.accept_desc=转移到「%s〠transfer.reject=æ‹’ç»è½¬ç§» -transfer.reject_desc=`å–æ¶ˆè½¬ç§»åˆ° "%s"` -transfer.no_permission_to_accept=您没有æƒé™æŽ¥å—æ¤è½¬è®©ã€‚ -transfer.no_permission_to_reject=您没有æƒé™æ‹’ç»æ¤è½¬è®©ã€‚ +transfer.reject_desc=å–æ¶ˆè½¬ç§»åˆ°ã€Œ%s〠+transfer.no_permission_to_accept=您没有æƒé™æŽ¥å—æ¤è½¬ç§»ã€‚ +transfer.no_permission_to_reject=您没有æƒé™æ‹’ç»æ¤è½¬ç§»ã€‚ desc.private=ç§æœ‰åº“ desc.public=公开 @@ -1151,9 +1153,9 @@ desc.archived=å·²å˜æ¡£ desc.sha256=SHA256 template.items=模æ¿é€‰é¡¹ -template.git_content=Gitæ•°æ®(默认分支) +template.git_content=Git æ•°æ®ï¼ˆé»˜è®¤åˆ†æ”¯ï¼‰ template.git_hooks=Git é’©å -template.git_hooks_tooltip=ä½ ç›®å‰æ— æ³•ä¿®æ”¹æˆ–åˆ é™¤è¢«æ·»åŠ è¿‡çš„ Git Hookã€‚ä»…å½“ä½ ä¿¡ä»»æ¨¡æ¿ä»“库时æ‰å¯ä»¥é€‰æ‹©æ¤é¡¹ã€‚ +template.git_hooks_tooltip=æ‚¨ç›®å‰æ— æ³•ä¿®æ”¹æˆ–åˆ é™¤è¢«æ·»åŠ è¿‡çš„ Git é’©å。仅当您信任模æ¿ä»“库时æ‰å¯ä»¥é€‰æ‹©æ¤é¡¹ã€‚ template.webhooks=Web é’©å template.topics=主题 template.avatar=å¤´åƒ @@ -1166,10 +1168,10 @@ archive.title_date=该仓库已于 %s 归档。您å¯ä»¥æŸ¥çœ‹æ–‡ä»¶æˆ–克隆它 archive.issue.nocomment=æ¤ä»“åº“å·²å˜æ¡£ï¼Œæ‚¨ä¸èƒ½åœ¨æ¤å·¥å•æ·»åŠ è¯„è®ºã€‚ archive.pull.nocomment=æ¤ä»“åº“å·²å˜æ¡£ï¼Œæ‚¨ä¸èƒ½åœ¨æ¤åˆå¹¶è¯·æ±‚æ·»åŠ è¯„è®ºã€‚ -form.reach_limit_of_creation_1=ä½ å·²ç»è¾¾åˆ°äº† %d 仓库的上é™ã€‚ -form.reach_limit_of_creation_n=ä½ å·²ç»è¾¾åˆ°äº† %d 个仓库的上é™ã€‚ -form.name_reserved=仓库åç§° %s 是被ä¿ç•™çš„。 -form.name_pattern_not_allowed=仓库åç§°ä¸ä¸å…许使用 %s æ ¼å¼ã€‚ +form.reach_limit_of_creation_1=您已ç»è¾¾åˆ°äº† %d 仓库的上é™ã€‚ +form.reach_limit_of_creation_n=您已ç»è¾¾åˆ°äº† %d 个仓库的上é™ã€‚ +form.name_reserved=仓库åç§° %s 是ä¿ç•™çš„。 +form.name_pattern_not_allowed=仓库åä¸ä¸å…许使用「%sã€æ ¼å¼ã€‚ need_auth=æŽˆæƒ migrate_options=è¿ç§»é€‰é¡¹ @@ -1177,7 +1179,7 @@ migrate_service=è¿ç§»æœåŠ¡ migrate_options_mirror_helper=è¯¥ä»“åº“å°†æ˜¯ä¸€ä¸ªé•œåƒ migrate_options_lfs=è¿ç§» LFS 文件 migrate_options_lfs_endpoint.label=LFS ç½‘å€ -migrate_options_lfs_endpoint.description=è¿ç§»å°†å°è¯•ä½¿ç”¨ä½ çš„ Git remote æ¥ <a target="_blank" rel="noopener noreferrer" href="%s">确定 LFS æœåС噍</a>。如果仓库 LFS æ•°æ®å˜å‚¨åœ¨å…¶ä»–ä½ç½®ï¼Œä½ 还å¯ä»¥æŒ‡å®šè‡ªå®šä¹‰ç½‘å€ã€‚ +migrate_options_lfs_endpoint.description=è¿ç§»å°†å°è¯•使用您的 Git remote æ¥ <a target="_blank" rel="noopener noreferrer" href="%s">确定 LFS æœåС噍</a>。如果仓库 LFS æ•°æ®å˜å‚¨åœ¨å…¶ä»–ä½ç½®ï¼Œæ‚¨è¿˜å¯ä»¥æŒ‡å®šè‡ªå®šä¹‰ç½‘å€ã€‚ migrate_options_lfs_endpoint.description.local=æ”¯æŒæœ¬åœ°æœåŠ¡å™¨è·¯å¾„ã€‚ migrate_options_lfs_endpoint.placeholder=如果留空,网å€å°†ä»Žå…‹éš† URL ä¸å¾—到 migrate_items=è¿ç§»é¡¹ç›® @@ -1187,7 +1189,7 @@ migrate_items_labels=æ ‡ç¾ migrate_items_issues=å·¥å• migrate_items_pullrequests=åˆå¹¶è¯·æ±‚ migrate_items_merge_requests=åˆå¹¶è¯·æ±‚ -migrate_items_releases=版本å‘布 +migrate_items_releases=å‘布 migrate_repo=è¿ç§»ä»“库 migrate.clone_address=从 URL è¿ç§»/克隆 migrate.clone_address_desc=现有仓库的 HTTP(s) 或 Git "clone" URL @@ -1228,6 +1230,7 @@ migrate.migrating_issues=è¿ç§»å·¥å• migrate.migrating_pulls=è¿ç§»åˆå¹¶è¯·æ±‚ migrate.cancel_migrating_title=å–æ¶ˆè¿ç§» migrate.cancel_migrating_confirm=您想è¦å–æ¶ˆæ¤æ¬¡è¿ç§»å—? +migration_status=è¿ç§»çŠ¶æ€ mirror_from=镜åƒè‡ªåœ°å€ forked_from=派生自 @@ -1260,14 +1263,14 @@ branch=分支 tree=ç›®å½•æ ‘ clear_ref=`清除当å‰å¼•用` filter_branch_and_tag=è¿‡æ»¤åˆ†æ”¯æˆ–æ ‡ç¾ -find_tag=查找Gitæ ‡ç¾ +find_tag=æŸ¥æ‰¾æ ‡ç¾ branches=分支列表 tags=æ ‡ç¾åˆ—表 issues=å·¥å• pulls=åˆå¹¶è¯·æ±‚ projects=项目 packages=软件包 -actions=Actions +actions=å·¥ä½œæµ labels=æ ‡ç¾ org_labels_desc=ç»„ç»‡çº§åˆ«çš„æ ‡ç¾ï¼Œå¯ä»¥è¢«æœ¬ç»„织下的 <strong>所有仓库</strong> 使用 org_labels_desc_manage=ç®¡ç† @@ -1276,9 +1279,9 @@ milestone=里程碑 milestones=里程碑 commits=æäº¤ commit=æäº¤ -release=版本å‘布 -releases=版本å‘布 -tag=Gitæ ‡ç¾ +release=å‘布 +releases=å‘布 +tag=æ ‡ç¾ released_this=å‘布 tagged_this=å·²æ ‡è®° file.title=%s ä½äºŽ %s @@ -1331,16 +1334,18 @@ editor.upload_file=ä¸Šä¼ æ–‡ä»¶ editor.edit_file=编辑文件 editor.preview_changes=é¢„è§ˆå˜æ›´ editor.cannot_edit_lfs_files=æ— æ³•åœ¨ web 界é¢ä¸ç¼–辑 lfs 文件。 +editor.cannot_edit_too_large_file=æ–‡ä»¶è¿‡å¤§ï¼Œæ— æ³•ç¼–è¾‘ã€‚ editor.cannot_edit_non_text_files=网页ä¸èƒ½ç¼–辑二进制文件。 +editor.file_not_editable_hint=但您ä»ç„¶å¯ä»¥é‡å‘½å或移动它。 editor.edit_this_file=编辑文件 editor.this_file_locked=文件已é”定 editor.must_be_on_a_branch=您必须在æŸä¸ªåˆ†æ”¯ä¸Šæ‰èƒ½å¯¹æ¤æ–‡ä»¶è¿›è¡Œä¿®æ”¹æ“作。 -editor.fork_before_edit=您必须在派生这个仓库æ‰èƒ½å¯¹æ¤æ–‡ä»¶è¿›è¡Œä¿®æ”¹æ“作 +editor.fork_before_edit=您必须派生这个仓库æ‰èƒ½å¯¹æ¤æ–‡ä»¶è¿›è¡Œä¿®æ”¹æ“作。 editor.delete_this_file=åˆ é™¤æ–‡ä»¶ editor.must_have_write_access=您必须具有写æƒé™æ‰èƒ½å¯¹æ¤æ–‡ä»¶è¿›è¡Œä¿®æ”¹æ“作。 -editor.file_delete_success=文件 %s å·²è¢«åˆ é™¤ã€‚ +editor.file_delete_success=文件「%sã€å·²åˆ 除。 editor.name_your_file=命忖‡ä»¶... -editor.filename_help=通过键入åç§°åŽè·Ÿæ–œçº¿ ("/") æ¥æ·»åŠ ç›®å½•ã€‚é€šè¿‡åœ¨è¾“å…¥æ¡†çš„å¼€å¤´é”®å…¥ "é€€æ ¼" æ¥åˆ 除目录。 +editor.filename_help=通过键入åç§°åŽè·Ÿæ–œçº¿ ("/") æ¥æ·»åŠ ç›®å½•ã€‚é€šè¿‡åœ¨è¾“å…¥æ¡†çš„å¼€å¤´é”®å…¥ã€Œé€€æ ¼ã€æ¥åˆ 除目录。 editor.or=或 editor.cancel_lower=å–æ¶ˆ editor.commit_signed_changes=æäº¤å·²ç¾å的更改 @@ -1351,54 +1356,60 @@ editor.update=æ›´æ–° %s editor.delete=åˆ é™¤ %s editor.patch=åº”ç”¨è¡¥ä¸ editor.patching=打补ä¸ï¼š -editor.fail_to_apply_patch=æ— æ³•åº”ç”¨è¡¥ä¸ %s +editor.fail_to_apply_patch=æ— æ³•åº”ç”¨è¡¥ä¸ editor.new_patch=æ–°è¡¥ä¸ editor.commit_message_desc=æ·»åŠ ä¸€ä¸ªå¯é€‰çš„æ‰©å±•æè¿°... editor.signoff_desc=在æäº¤æ—¥å¿—æ¶ˆæ¯æœ«å°¾æ·»åŠ ç¾ç½²äººä¿¡æ¯ã€‚ editor.commit_directly_to_this_branch=直接æäº¤è‡³ <strong class="branch-name">%s</strong> 分支。 editor.create_new_branch=ä¸ºæ¤æäº¤åˆ›å»ºä¸€ä¸ª <strong>新的分支</strong> å¹¶å‘èµ·åˆå¹¶è¯·æ±‚。 editor.create_new_branch_np=ä¸ºæ¤æäº¤åˆ›å»º <strong>新分支</strong>。 -editor.propose_file_change=æè®®æ–‡ä»¶æ›´æ”¹ +editor.propose_file_change=建议文件更改 editor.new_branch_name=为这次æäº¤çš„æ–°åˆ†æ”¯å‘½å editor.new_branch_name_desc=新的分支åç§°... editor.cancel=å–æ¶ˆ editor.filename_cannot_be_empty=文件åä¸èƒ½ä¸ºç©ºã€‚ -editor.filename_is_invalid=文件å %s æ— æ•ˆ +editor.filename_is_invalid=æ–‡ä»¶åæ— 效:「%sã€ã€‚ editor.commit_email=æäº¤é‚®ç®±åœ°å€ editor.invalid_commit_email=æäº¤çš„é‚®ç®±åœ°å€æ— 效。 -editor.branch_does_not_exist=æ¤ä»“库ä¸ä¸å˜åœ¨å为 %s 的分支。 -editor.branch_already_exists=æ¤ä»“库已å˜åœ¨å为 %s 的分支。 -editor.directory_is_a_file=%s å·²ç»ä½œä¸ºæ–‡ä»¶å在æ¤ä»“库ä¸å˜åœ¨ã€‚ -editor.file_is_a_symlink=`"%s" 是一个符å·é“¾æŽ¥ï¼Œæ— 法在 web 编辑器ä¸ç¼–辑` -editor.filename_is_a_directory=æ¤ä»“库ä¸å·²å˜åœ¨å为“%s†的目录。 -editor.file_editing_no_longer_exists=æ£åœ¨ç¼–辑的文件 %s å·²ä¸å˜åœ¨ã€‚ -editor.file_deleting_no_longer_exists=æ£åœ¨åˆ 除的文件 %s å·²ä¸å˜åœ¨ã€‚ +editor.branch_does_not_exist=æ¤ä»“库ä¸ä¸å˜åœ¨å为「%sã€çš„分支。 +editor.branch_already_exists=æ¤ä»“库已å˜åœ¨å为「%sã€çš„分支。 +editor.directory_is_a_file=目录å「%sã€å·²ä½œä¸ºæ–‡ä»¶å在æ¤ä»“库ä¸å˜åœ¨ã€‚ +editor.file_is_a_symlink=`「%sã€æ˜¯ä¸€ä¸ªç¬¦å·é“¾æŽ¥ï¼Œæ— 法在 Web 编辑器ä¸ç¼–辑` +editor.filename_is_a_directory=文件å「%sã€å·²ä½œä¸ºç›®å½•å在æ¤ä»“库ä¸å˜åœ¨ã€‚ +editor.file_modifying_no_longer_exists=æ£åœ¨ä¿®æ”¹çš„æ–‡ä»¶ã€Œ%sã€å·²ä¸å˜åœ¨äºŽæ¤ä»“库。 editor.file_changed_while_editing=文件内容在您进行编辑时已ç»å‘生å˜åŠ¨ã€‚<a target="_blank" rel="noopener noreferrer" href="%s">å•击æ¤å¤„</a> 查看å˜åŠ¨çš„å…·ä½“å†…å®¹ï¼Œæˆ–è€… <strong>冿¬¡æäº¤</strong> 覆盖已å‘生的å˜åŠ¨ã€‚ -editor.file_already_exists=æ¤ä»“库已ç»å˜åœ¨å为 %s 的文件。 -editor.commit_id_not_matching=æäº¤ID与您开始编辑时的IDä¸åŒ¹é…。请æäº¤åˆ°è¡¥ä¸åˆ†æ”¯ç„¶åŽåˆå¹¶ã€‚ +editor.file_already_exists=æ¤ä»“库已ç»å˜åœ¨å为「%sã€çš„æ–‡ä»¶ã€‚ +editor.commit_id_not_matching=æäº¤ ID 与您开始编辑时的 ID ä¸åŒ¹é…。请æäº¤åˆ°è¡¥ä¸åˆ†æ”¯ç„¶åŽåˆå¹¶ã€‚ editor.push_out_of_date=推é€ä¼¼ä¹Žå·²ç»è¿‡æ—¶ã€‚ editor.commit_empty_file_header=æäº¤ä¸€ä¸ªç©ºæ–‡ä»¶ editor.commit_empty_file_text=æ‚¨è¦æäº¤çš„æ–‡ä»¶æ˜¯ç©ºçš„ï¼Œç»§ç»å—? editor.no_changes_to_show=没有å¯ä»¥æ˜¾ç¤ºçš„å˜æ›´ã€‚ -editor.fail_to_update_file=æ›´æ–°/创建文件 %s 失败。 -editor.fail_to_update_file_summary=错误信æ¯ï¼š -editor.push_rejected_no_message=æ¤ä¿®æ”¹è¢«æœåŠ¡å™¨æ‹’ç»å¹¶ä¸”没有å馈消æ¯ã€‚请检查 Git Hook。 -editor.push_rejected=æ¤ä¿®æ”¹è¢«æœåŠ¡å™¨æ‹’ç»ã€‚请检查 Git Hook。 +editor.push_rejected_no_message=æ¤ä¿®æ”¹è¢«æœåŠ¡å™¨æ‹’ç»å¹¶ä¸”没有å馈消æ¯ã€‚请检查 Git é’©å。 +editor.push_rejected=æ¤ä¿®æ”¹è¢«æœåŠ¡å™¨æ‹’ç»ã€‚请检查 Git é’©å。 editor.push_rejected_summary=详细拒ç»ä¿¡æ¯ï¼š editor.add_subdir=æ·»åŠ ç›®å½• -editor.unable_to_upload_files=ä¸Šä¼ æ–‡ä»¶è‡³ %s æ—¶å‘生错误:%v -editor.upload_file_is_locked=文件 %s 被 %s é”定。 -editor.upload_files_to_dir=ä¸Šä¼ æ–‡ä»¶è‡³ %s -editor.cannot_commit_to_protected_branch=ä¸å¯ä»¥æäº¤åˆ°å—ä¿æŠ¤çš„åˆ†æ”¯ %s。 +editor.unable_to_upload_files=ä¸Šä¼ æ–‡ä»¶è‡³ã€Œ%sã€å¤±è´¥ï¼š%v +editor.upload_file_is_locked=文件「%sã€è¢« %s é”定。 +editor.upload_files_to_dir=ä¸Šä¼ æ–‡ä»¶è‡³ã€Œ%s〠+editor.cannot_commit_to_protected_branch=ä¸å¯ä»¥æäº¤åˆ°å—ä¿æŠ¤çš„åˆ†æ”¯ã€Œ%sã€ã€‚ editor.no_commit_to_branch=æ— æ³•ç›´æŽ¥æäº¤åˆ†æ”¯ï¼Œå› 为: editor.user_no_push_to_branch=用户ä¸èƒ½æŽ¨é€åˆ°åˆ†æ”¯ editor.require_signed_commit=分支需è¦ç¾åæäº¤ -editor.cherry_pick=Cherry-pick %s 到: +editor.cherry_pick=拣选æäº¤ %s 到: editor.revert=å°† %s 还原到: +editor.failed_to_commit=æäº¤æ›´æ”¹å¤±è´¥ã€‚ +editor.failed_to_commit_summary=错误信æ¯ï¼š + +editor.fork_create=派生仓库å‘èµ·è¯·æ±‚å˜æ›´ +editor.fork_create_description=您ä¸èƒ½ç›´æŽ¥ç¼–辑æ¤ä»“库。您å¯ä»¥ä»Žæ¤ä»“库派生,进行编辑并创建一个拉å–请求。 +editor.fork_edit_description=您ä¸èƒ½ç›´æŽ¥ç¼–辑æ¤ä»“库。 更改将写入您的派生仓库 <b>%s</b>,以便您å¯ä»¥åˆ›å»ºä¸€ä¸ªæ‹‰å–请求。 +editor.fork_not_editable=ä½ å·²ç»æ´¾ç”Ÿäº†è¿™ä¸ªä»“åº“ï¼Œä½†æ˜¯ä½ çš„åˆ†å‰æ˜¯ä¸å¯ç¼–辑的。 +editor.fork_failed_to_push_branch=推é€åˆ†æ”¯ %s 到仓库失败。 +editor.fork_branch_exists=分支 "%s" å·²å˜åœ¨äºŽæ‚¨çš„æ´¾ç”Ÿä»“库ä¸ï¼Œè¯·é€‰æ‹©ä¸€ä¸ªæ–°çš„分支å称。 commits.desc=æµè§ˆä»£ç ä¿®æ”¹åŽ†å² commits.commits=æ¬¡ä»£ç æäº¤ -commits.no_commits=没有共åŒçš„æäº¤ã€‚%s å’Œ %s 的历å²å®Œå…¨ä¸åŒã€‚ +commits.no_commits=没有共åŒçš„æäº¤ã€‚「%sã€å’Œã€Œ%sã€çš„历å²å®Œå…¨ä¸åŒã€‚ commits.nothing_to_compare=这些分支是相åŒçš„。 commits.search.tooltip=`您å¯ä»¥åœ¨å…³é”®è¯å‰åŠ ä¸Šå‰ç¼€ï¼Œå¦‚"author:", "committer:", "after:", 或"before:", 例如 "retrin author:Alice before:2019-01-13"` commits.search_branch=æ¤åˆ†æ”¯ @@ -1418,11 +1429,11 @@ commits.view_file_diff=查看æäº¤ä¸çš„æ–‡ä»¶æ›´æ”¹ commit.operations=æ“作 commit.revert=还原 -commit.revert-header=还原: %s +commit.revert-header=还原:%s commit.revert-content=选择è¦è¿˜åŽŸçš„åˆ†æ”¯ï¼š -commit.cherry-pick=Cherry-pick -commit.cherry-pick-header=Cherry-pick: %s -commit.cherry-pick-content=选择 cherry-pick çš„ç›®æ ‡åˆ†æ”¯ï¼š +commit.cherry-pick=拣选æäº¤ +commit.cherry-pick-header=拣选æäº¤ï¼š%s +commit.cherry-pick-content=é€‰æ‹©è¦æ‹£é€‰æäº¤çš„ç›®æ ‡åˆ†æ”¯ï¼š commitstatus.error=错误 commitstatus.failure=失败 @@ -1439,14 +1450,14 @@ projects.create=创建项目 projects.title=æ ‡é¢˜ projects.new=创建项目 projects.new_subheader=在一个地方åè°ƒã€è·Ÿè¸ªå’Œæ›´æ–°æ‚¨çš„å·¥ä½œï¼Œè®©é¡¹ç›®ä¿æŒé€æ˜Žå¹¶æŒ‰è®¡åˆ’进行。 -projects.create_success=项目 %s 创建æˆåŠŸã€‚ +projects.create_success=项目「%sã€åˆ›å»ºæˆåŠŸã€‚ projects.deletion=åˆ é™¤é¡¹ç›® projects.deletion_desc=åˆ é™¤é¡¹ç›®ä¼šä»Žæ‰€æœ‰ç›¸å…³çš„å·¥å•ä¸ç§»é™¤å®ƒã€‚是å¦ç»§ç»ï¼Ÿ -projects.deletion_success=è¯¥é¡¹ç›®å·²è¢«åˆ é™¤ã€‚ +projects.deletion_success=è¯¥é¡¹ç›®å·²åˆ é™¤ã€‚ projects.edit=编辑项目 projects.edit_subheader=项目用于组织工å•和跟踪进展情况。 projects.modify=更新项目 -projects.edit_success=项目 %s æ›´æ–°æˆåŠŸã€‚ +projects.edit_success=项目「%sã€æ›´æ–°æˆåŠŸã€‚ projects.type.none=æ— projects.type.basic_kanban=åŸºç¡€çœ‹æ¿ projects.type.bug_triage=Bugåˆ†ç±»çœ‹æ¿ @@ -1460,7 +1471,7 @@ projects.column.new=创建列 projects.column.set_default=设为默认 projects.column.set_default_desc=设置æ¤åˆ—为未分类问题和åˆå¹¶è¯·æ±‚的默认值 projects.column.delete=åˆ é™¤åˆ— -projects.column.deletion_desc=åˆ é™¤é¡¹ç›®åˆ—ä¼šå°†æ‰€æœ‰ç›¸å…³é—®é¢˜ç§»åˆ°â€œæœªåˆ†ç±»â€ã€‚是å¦ç»§ç»ï¼Ÿ +projects.column.deletion_desc=åˆ é™¤é¡¹ç›®åˆ—ä¼šå°†æ‰€æœ‰ç›¸å…³é—®é¢˜ç§»è‡³é»˜è®¤åˆ—ã€‚æ˜¯å¦ç»§ç»ï¼Ÿ projects.column.color=颜色 projects.open=å¼€å¯ projects.close=å…³é— @@ -1505,19 +1516,19 @@ issues.choose.blank_about=从默认模æ¿åˆ›å»ºä¸€ä¸ªå·¥å•。 issues.choose.ignore_invalid_templates=å·²å¿½ç•¥æ— æ•ˆæ¨¡æ¿ issues.choose.invalid_templates=å‘现了 %v ä¸ªæ— æ•ˆæ¨¡æ¿ issues.choose.invalid_config=问题é…置包å«é”™è¯¯ï¼š -issues.no_ref=分支/æ ‡è®°æœªæŒ‡å®š +issues.no_ref=分支/Gitæ ‡ç¾æœªæŒ‡å®š issues.create=åˆ›å»ºå·¥å• issues.new_label=åˆ›å»ºæ ‡ç¾ issues.new_label_placeholder=æ ‡ç¾åç§° issues.new_label_desc_placeholder=æè¿° issues.create_label=åˆ›å»ºæ ‡ç¾ issues.label_templates.title=åŠ è½½é¢„å®šä¹‰çš„æ ‡ç¾æ¨¡æ¿ -issues.label_templates.info=è¿˜æ²¡æœ‰ä»»ä½•æ ‡ç¾ã€‚您å¯ä»¥ä½¿ç”¨'åˆ›å»ºæ ‡ç¾'æŒ‰é’®æˆ–è€…åŠ è½½é¢„å®šä¹‰çš„æ ‡ç¾é›†åˆ›å»ºæ ‡ç¾ +issues.label_templates.info=è¿˜æ²¡æœ‰ä»»ä½•æ ‡ç¾ã€‚您å¯ä»¥ä½¿ç”¨ã€Œåˆ›å»ºæ ‡ç¾ã€æŒ‰é’®æˆ–è€…åŠ è½½é¢„å®šä¹‰çš„æ ‡ç¾é›†åˆ›å»ºæ ‡ç¾ï¼š issues.label_templates.helper=é€‰æ‹©æ ‡ç¾æ¨¡æ¿ issues.label_templates.use=ä½¿ç”¨æ ‡ç¾é›† -issues.label_templates.fail_to_load_file=åŠ è½½æ ‡ç¾æ¨¡æ¿æ–‡ä»¶ %s æ—¶å‘生错误:%v +issues.label_templates.fail_to_load_file=åŠ è½½æ ‡ç¾æ¨¡æ¿æ–‡ä»¶ã€Œ%sã€æ—¶å‘生错误:%v issues.add_label=于 %[2]s æ·»åŠ äº†æ ‡ç¾ %[1]s -issues.add_labels=于 %s æ·»åŠ %s æ ‡ç¾ +issues.add_labels=于 %[2]s æ·»åŠ äº†æ ‡ç¾ %[1]s issues.remove_label=于 %[2]s åˆ é™¤äº†æ ‡ç¾ %[1]s issues.remove_labels=于 %[2]s åˆ é™¤äº†æ ‡ç¾ %[1]s issues.add_remove_labels=于 %[3]s æ·»åŠ äº†æ ‡ç¾ %[1]s ï¼Œåˆ é™¤äº†æ ‡ç¾ %[2]s @@ -1531,7 +1542,7 @@ issues.remove_project_at=`于 %[2]s å°†æ¤å·¥å•从项目 <b>%[1]s</b> ä¸åˆ é™ issues.deleted_milestone=ï¼ˆå·²åˆ é™¤ï¼‰ issues.deleted_project=`(å·²åˆ é™¤)` issues.self_assign_at=`于 %s 指派给自己` -issues.add_assignee_at=`于 %[2]s 被 <b>%[1]s</b> 指派` +issues.add_assignee_at=`于 %[2]s ç”± <b>%[1]s</b> 指派` issues.remove_assignee_at=`<b>%s</b> å–æ¶ˆäº†æŒ‡æ´¾åœ¨ %s` issues.remove_self_assignment=`于 %s å–æ¶ˆäº†æŒ‡æ´¾` issues.change_title_at=`于 %[3]s ä¿®æ”¹æ ‡é¢˜ <b><strike>%[1]s</strike></b> 为 <b>%[2]s</b>` @@ -1559,6 +1570,7 @@ issues.filter_user_placeholder=æœç´¢ç”¨æˆ· issues.filter_user_no_select=所有用户 issues.filter_type=类型ç›é€‰ issues.filter_type.all_issues=æ‰€æœ‰å·¥å• +issues.filter_type.all_pull_requests=所有åˆå¹¶è¯·æ±‚ issues.filter_type.assigned_to_you=指派给您的 issues.filter_type.created_by_you=由您创建的 issues.filter_type.mentioning_you=æåŠæ‚¨çš„ @@ -1587,11 +1599,11 @@ issues.action_assignee_no_select=未指派 issues.action_check=选ä¸/å–æ¶ˆé€‰ä¸ issues.action_check_all=选ä¸/å–æ¶ˆé€‰ä¸æ‰€æœ‰é¡¹ç›® issues.opened_by=ç”± <a href="%[2]s">%[3]s</a> 于 %[1]s创建 -pulls.merged_by=ç”± <a href="%[2]s">%[3]s</a> 创建,被åˆå¹¶äºŽ %[1]s -pulls.merged_by_fake=ç”± %[2]s 创建,被åˆå¹¶äºŽ %[1]s -issues.closed_by=ç”± <a href="%[2]s">%[3]s</a> 创建,被关é—于 %[1]s +pulls.merged_by=ç”± <a href="%[2]s">%[3]s</a> 创建,åˆå¹¶äºŽ %[1]s +pulls.merged_by_fake=ç”± %[2]s 创建,åˆå¹¶äºŽ %[1]s +issues.closed_by=ç”± <a href="%[2]s">%[3]s</a> 创建,关é—于 %[1]s issues.opened_by_fake=ç”± %[2]s 于 %[1]s 打开 -issues.closed_by_fake=ç”± %[2]s 创建,被关é—于 %[1]s +issues.closed_by_fake=ç”± %[2]s 创建,关é—于 %[1]s issues.previous=上一页 issues.next=下一页 issues.open_title=å¼€å¯ä¸ @@ -1611,8 +1623,8 @@ issues.close=å…³é—å·¥å• issues.comment_pull_merged_at=å·²åˆå¹¶æäº¤ %[1]s 到 %[2]s %[3]s issues.comment_manually_pull_merged_at=手动åˆå¹¶æäº¤ %[1]s 到 %[2]s %[3]s issues.close_comment_issue=è¯„è®ºå¹¶å…³é— -issues.reopen_issue=釿–°å¼€å¯ -issues.reopen_comment_issue=è¯„è®ºå¹¶é‡æ–°å¼€å¯ +issues.reopen_issue=釿–°æ‰“å¼€ +issues.reopen_comment_issue=è¯„è®ºå¹¶é‡æ–°æ‰“å¼€ issues.create_comment=评论 issues.comment.blocked_user=æ— æ³•åˆ›å»ºæˆ–ç¼–è¾‘è¯„è®ºï¼Œå› ä¸ºæ‚¨å·²è¢«ä»“åº“æ‰€æœ‰è€…æˆ–å·¥å•创建者å±è”½ã€‚ issues.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闿¤å·¥å•` @@ -1648,40 +1660,41 @@ issues.edit=编辑 issues.cancel=å–æ¶ˆ issues.save=ä¿å˜ issues.label_title=æ ‡ç¾åç§° -issues.label_description=æ ‡ç¾æè¿° -issues.label_color=æ ‡ç¾é¢œè‰² -issues.label_exclusive=äº’æ–¥æ ‡ç¾ +issues.label_description=æè¿° +issues.label_color=颜色 +issues.label_color_invalid=æ— æ•ˆçš„é¢œè‰² +issues.label_exclusive=互斥 issues.label_archive=å½’æ¡£æ ‡ç¾ -issues.label_archived_filter=æ˜¾ç¤ºå˜æ¡£æ ‡ç¾ -issues.label_archive_tooltip=åœ¨æ ‡ç¾æœç´¢æ—¶ï¼Œé»˜è®¤æƒ…å†µä¸‹å˜æ¡£æ ‡ç¾å°†è¢«æŽ’除在外。 +issues.label_archived_filter=æ˜¾ç¤ºå·²å½’æ¡£æ ‡ç¾ +issues.label_archive_tooltip=åœ¨æ ‡ç¾æœç´¢æ—¶ï¼Œé»˜è®¤æƒ…å†µä¸‹å·²å½’æ¡£æ ‡ç¾å°†è¢«æŽ’除在外。 issues.label_exclusive_desc=å‘½åæ ‡ç¾ä¸º <code>scope/item</code> 以使其与其他以 <code>scope/</code> å¼€å¤´çš„æ ‡ç¾äº’斥。 issues.label_exclusive_warning=åœ¨ç¼–è¾‘å·¥å•æˆ–åˆå¹¶è¯·æ±‚çš„æ ‡ç¾æ—¶ï¼Œä»»ä½•冲çªçš„èŒƒå›´æ ‡ç¾éƒ½å°†è¢«åˆ 除。 issues.label_exclusive_order=排åºé¡ºåº issues.label_exclusive_order_tooltip=在åŒä¸€ä¸ªèŒƒå›´å†…çš„äº’æ–¥æ ‡ç¾å°†æŒ‰ç…§è¿™ä¸ªæ•°å—è¿›è¡ŒæŽ’åº issues.label_count=%d ä¸ªæ ‡ç¾ -issues.label_open_issues=%d 个开å¯çš„å·¥å• +issues.label_open_issues=%d 个已打开工å•/åˆå¹¶è¯·æ±‚ issues.label_edit=编辑 issues.label_delete=åˆ é™¤ issues.label_modify=ç¼–è¾‘æ ‡ç¾ issues.label_deletion=åˆ é™¤æ ‡ç¾ issues.label_deletion_desc=åˆ é™¤æ ‡ç¾ä¼šå°†å…¶ä»Žæ‰€æœ‰é—®é¢˜ä¸åˆ 除。继ç»ï¼Ÿ -issues.label_deletion_success=è¯¥æ ‡ç¾å·²è¢«åˆ 除。 +issues.label_deletion_success=è¯¥æ ‡ç¾å·²åˆ 除。 issues.label.filter_sort.alphabetically=æŒ‰å—æ¯é¡ºåºæŽ’åº issues.label.filter_sort.reverse_alphabetically=æŒ‰å—æ¯é€†åºæŽ’åº issues.label.filter_sort.by_size=最å°å°ºå¯¸ issues.label.filter_sort.reverse_by_size=最大尺寸 issues.num_participants=%d åå‚与者 -issues.attachment.open_tab=`åœ¨æ–°çš„æ ‡ç¾é¡µä¸æŸ¥çœ‹ '%s'` -issues.attachment.download=`点击下载 '%s'` +issues.attachment.open_tab=`åœ¨æ–°çš„æ ‡ç¾é¡µä¸æŸ¥çœ‹ã€Œ%sã€` +issues.attachment.download=`点击下载「%sã€` issues.subscribe=订阅 issues.unsubscribe=å–æ¶ˆè®¢é˜… issues.unpin=å–æ¶ˆç½®é¡¶ issues.max_pinned=您ä¸èƒ½ç½®é¡¶æ›´å¤šå·¥å• -issues.pin_comment=于 %s 被置顶 +issues.pin_comment=于 %s 置顶 issues.unpin_comment=于 %s å–æ¶ˆç½®é¡¶ issues.lock=é”å®šå¯¹è¯ issues.unlock=è§£é”å¯¹è¯ -issues.lock_duplicate=一个工å•ä¸èƒ½è¢«é”定两次。 +issues.lock_duplicate=一个工å•ä¸èƒ½é”定两次。 issues.unlock_error=æ— æ³•è§£é”一个未é”定的工å•。 issues.lock_with_reason=å› ä¸º <strong>%s</strong> 而é”定,并将对è¯é™åˆ¶ä¸ºå作者 %s issues.lock_no_reason=é”定并é™åˆ¶ä»…å作者 %s @@ -1708,13 +1721,15 @@ issues.timetracker_timer_discard=åˆ é™¤è®¡æ—¶å™¨ issues.timetracker_timer_manually_add=æ·»åŠ æ—¶é—´ issues.time_estimate_set=设置预计时间 -issues.time_estimate_display=预计: %s +issues.time_estimate_display=预估:%s issues.change_time_estimate_at=预估时间已修改为 <b>%[1]s</b> %[2]s -issues.remove_time_estimate_at=åˆ é™¤é¢„è®¡æ—¶é—´ %s +issues.remove_time_estimate_at=åˆ é™¤é¢„ä¼°æ—¶é—´ %s issues.time_estimate_invalid=é¢„è®¡æ—¶é—´æ ¼å¼æ— 效 issues.start_tracking_history=`开始工作 %s` issues.tracker_auto_close=当æ¤å·¥å•关闿—¶ï¼Œè‡ªåŠ¨åœæ¢è®¡æ—¶å™¨ -issues.tracking_already_started=`ä½ å·²ç»å¼€å§‹å¯¹ <a href="%s">å¦ä¸€ä¸ªå·¥å•</a> 进行时间跟踪ï¼` +issues.stopwatch_already_stopped=æ¤å·¥å•的计时器已ç»åœæ¢ +issues.stopwatch_already_created=æ¤å·¥å•的计时器已ç»å˜åœ¨ +issues.tracking_already_started=`您已ç»å¼€å§‹å¯¹ <a href="%s">å¦ä¸€ä¸ªå·¥å•</a> 进行时间跟踪ï¼` issues.stop_tracking=åœæ¢è®¡æ—¶å™¨ issues.stop_tracking_history=工作 <b>%[1]s</b> 于 %[2]s åœæ¢ issues.cancel_tracking=å–æ¶ˆ @@ -1730,7 +1745,7 @@ issues.time_spent_total=总用时 issues.time_spent_from_all_authors=`总花费时间:%s` issues.due_date=到期时间 -issues.invalid_due_date_format=åˆ°æœŸæ—¶é—´çš„æ ¼å¼å¿…须是 'yyyy-mm-dd' 的形å¼ã€‚ +issues.invalid_due_date_format=åˆ°æœŸæ—¶é—´çš„æ ¼å¼å¿…须是「yyyy-mm-ddã€çš„å½¢å¼ã€‚ issues.error_modifying_due_date=修改到期时间失败。 issues.error_removing_due_date=åˆ é™¤åˆ°æœŸæ—¶é—´å¤±è´¥ã€‚ issues.push_commit_1=于 %[2]s 推é€äº† %[1]d 个æäº¤ @@ -1773,7 +1788,7 @@ issues.dependency.remove_header=åˆ é™¤ä¾èµ–项 issues.dependency.issue_remove_text=æ¤æ“作将从工å•ä¸åˆ 除ä¾èµ–。是å¦è¦ç»§ç»? issues.dependency.pr_remove_text=æ¤æ“作将从åˆå¹¶è¯·æ±‚ä¸åˆ 除ä¾èµ–。是å¦è¦ç»§ç»? issues.dependency.setting=为工å•å’Œåˆå¹¶è¯·æ±‚å¯ç”¨ä¾èµ– -issues.dependency.add_error_same_issue=ä½ ä¸èƒ½è®©ä¸€ä¸ªå·¥å•ä¾èµ–于自己。 +issues.dependency.add_error_same_issue=您ä¸èƒ½è®©ä¸€ä¸ªå·¥å•ä¾èµ–于自身。 issues.dependency.add_error_dep_issue_not_exist=ä¾èµ–项ä¸å˜åœ¨ã€‚ issues.dependency.add_error_dep_not_exist=ä¾èµ–项ä¸å˜åœ¨ã€‚ issues.dependency.add_error_dep_exists=ä¾èµ–项已å˜åœ¨ã€‚ @@ -1815,9 +1830,9 @@ issues.review.stale=批准åŽå·²æ›´æ–° issues.review.unofficial=éžå®˜æ–¹å®¡æ‰¹æ•° issues.assignee.error=å› ä¸ºæœªçŸ¥åŽŸå› ï¼Œå¹¶éžæ‰€æœ‰çš„æŒ‡æ´¾éƒ½æˆåŠŸã€‚ issues.reference_issue.body=内容 -issues.content_history.deleted=åˆ é™¤äºŽ -issues.content_history.edited=最åŽç¼–辑于 -issues.content_history.created=创建于 +issues.content_history.deleted=å·²åˆ é™¤ +issues.content_history.edited=已编辑 +issues.content_history.created=已创建 issues.content_history.delete_from_history=从历å²è®°å½•ä¸åˆ 除 issues.content_history.delete_from_history_confirm=从历å²è®°å½•ä¸åˆ 除å—? issues.content_history.options=选项 @@ -1871,7 +1886,7 @@ pulls.merged=å·²åˆå¹¶ pulls.merged_success=åˆå¹¶è¯·æ±‚å·²æˆåŠŸåˆå¹¶å’Œå…³é— pulls.closed=åˆå¹¶è¯·æ±‚å·²å…³é— pulls.manually_merged=已手动åˆå¹¶ -pulls.merged_info_text=分支 %s 现在å¯ä»¥è¢«åˆ 除了。 +pulls.merged_info_text=分支 %s 现在å¯ä»¥åˆ 除了。 pulls.is_closed=åˆå¹¶è¯·æ±‚å·²ç»å…³é—。 pulls.title_wip_desc=`<a href="#">æ ‡é¢˜ä»¥ <strong>%s</strong> 开头</a>以å…åˆå¹¶è¯·æ±‚æ„外åˆå¹¶ã€‚` pulls.cannot_merge_work_in_progress=æ¤åˆå¹¶è¯·æ±‚è¢«æ ‡è®°ä¸ºæ£åœ¨è¿›è¡Œçš„工作。 @@ -1906,7 +1921,7 @@ pulls.waiting_count_1=%d 个æ£åœ¨ç‰å¾…å®¡æ ¸ pulls.waiting_count_n=%d 个æ£åœ¨ç‰å¾…å®¡æ ¸ pulls.wrong_commit_id=æäº¤ id å¿…é¡»åœ¨ç›®æ ‡åˆ†æ”¯ 上 -pulls.no_merge_desc=由于未å¯ç”¨åˆå¹¶é€‰é¡¹ï¼Œæ¤åˆå¹¶è¯·æ±‚æ— æ³•è¢«åˆå¹¶ã€‚ +pulls.no_merge_desc=由于未å¯ç”¨åˆå¹¶é€‰é¡¹ï¼Œæ¤åˆå¹¶è¯·æ±‚æ— æ³•åˆå¹¶ã€‚ pulls.no_merge_helper=在仓库设置ä¸å¯ç”¨åˆå¹¶é€‰é¡¹æˆ–者手工åˆå¹¶è¯·æ±‚。 pulls.no_merge_wip=这个åˆå¹¶è¯·æ±‚æ— æ³•åˆå¹¶ï¼Œå› ä¸ºè¢«æ ‡è®°ä¸ºå°šæœªå®Œæˆçš„工作。 pulls.no_merge_not_ready=æ¤åˆå¹¶è¯·æ±‚尚未准备好åˆå¹¶ï¼Œè¯·æ£€æŸ¥å®¡æ ¸çжæ€å’ŒçŠ¶æ€æ£€æŸ¥ã€‚ @@ -1920,7 +1935,7 @@ pulls.merge_manually=手动åˆå¹¶ pulls.merge_commit_id=åˆå¹¶æäº¤ ID pulls.require_signed_wont_sign=分支需è¦ç¾åçš„æäº¤ï¼Œä½†è¿™ä¸ªåˆå¹¶å°†ä¸ä¼šè¢«ç¾å -pulls.invalid_merge_option=ä½ å¯ä»¥åœ¨æ¤åˆå¹¶è¯·æ±‚ä¸ä½¿ç”¨åˆå¹¶é€‰é¡¹ã€‚ +pulls.invalid_merge_option=您å¯ä»¥åœ¨æ¤åˆå¹¶è¯·æ±‚ä¸ä½¿ç”¨åˆå¹¶é€‰é¡¹ã€‚ pulls.merge_conflict=åˆå¹¶å¤±è´¥ï¼šåˆå¹¶æ—¶æœ‰å†²çªå‘生。æç¤ºï¼šé‡‡ç”¨å…¶å®ƒåˆå¹¶ç–ç•¥ pulls.merge_conflict_summary=é”™è¯¯ä¿¡æ¯ pulls.rebase_conflict=åˆå¹¶å¤±è´¥ï¼šå˜åŸºæäº¤æœ‰å†²çªï¼š%[1]s。æç¤ºï¼šé‡‡ç”¨å…¶å®ƒåˆå¹¶ç–ç•¥ @@ -1928,7 +1943,7 @@ pulls.rebase_conflict_summary=é”™è¯¯ä¿¡æ¯ pulls.unrelated_histories=åˆå¹¶å¤±è´¥ï¼šä¸¤ä¸ªåˆ†æ”¯æ²¡æœ‰å…±åŒåކå²ã€‚æç¤ºï¼šå°è¯•ä¸åŒçš„ç–ç•¥ pulls.merge_out_of_date=åˆå¹¶å¤±è´¥ï¼šåœ¨ç”Ÿæˆåˆå¹¶æ—¶ï¼Œä¸»åˆ†æ”¯å·²æ›´æ–°ã€‚æç¤ºï¼šå†è¯•一次。 pulls.head_out_of_date=åˆå¹¶å¤±è´¥ï¼šåœ¨ç”Ÿæˆåˆå¹¶æ—¶ï¼Œhead 已更新。æç¤ºï¼šå†è¯•一次。 -pulls.has_merged=失败:åˆå¹¶è¯·æ±‚å·²ç»è¢«åˆå¹¶ï¼Œæ‚¨ä¸èƒ½å†æ¬¡åˆå¹¶æˆ–æ›´æ”¹ç›®æ ‡åˆ†æ”¯ã€‚ +pulls.has_merged=失败:åˆå¹¶è¯·æ±‚å·²ç»åˆå¹¶ï¼Œæ‚¨ä¸èƒ½å†æ¬¡åˆå¹¶æˆ–æ›´æ”¹ç›®æ ‡åˆ†æ”¯ã€‚ pulls.push_rejected=推é€å¤±è´¥ï¼šæŽ¨é€è¢«æ‹’ç»ã€‚审查æ¤ä»“库的 Git é’©å。 pulls.push_rejected_summary=详细拒ç»ä¿¡æ¯ pulls.push_rejected_no_message=推é€å¤±è´¥ï¼šæ¤æŽ¨é€è¢«æ‹’ç»ä½†æœªæä¾›å…¶ä»–ä¿¡æ¯ã€‚请检查æ¤ä»“库的 Git é’©å。 @@ -1952,12 +1967,12 @@ pulls.closed_at=`于 <a id="%[1]s" href="#%[1]s">%[2]s</a> 关闿¤åˆå¹¶è¯·æ±‚ pulls.reopened_at=`釿–°æ‰“å¼€æ¤åˆå¹¶è¯·æ±‚ <a id="%[1]s" href="#%[1]s">%[2]s</a>` pulls.cmd_instruction_hint=查看命令行æç¤º pulls.cmd_instruction_checkout_title=检出 -pulls.cmd_instruction_checkout_desc=ä»Žä½ çš„ä»“åº“ä¸æ£€å‡ºä¸€ä¸ªæ–°çš„åˆ†æ”¯å¹¶æµ‹è¯•å˜æ›´ã€‚ +pulls.cmd_instruction_checkout_desc=ä»Žæ‚¨çš„ä»“åº“ä¸æ£€å‡ºä¸€ä¸ªæ–°çš„åˆ†æ”¯å¹¶æµ‹è¯•å˜æ›´ã€‚ pulls.cmd_instruction_merge_title=åˆå¹¶ pulls.cmd_instruction_merge_desc=åˆå¹¶å˜æ›´å¹¶æ›´æ–°åˆ° Gitea 上 -pulls.cmd_instruction_merge_warning=è¦å‘Šï¼šæ¤æ“作ä¸èƒ½åˆå¹¶è¯¥åˆå¹¶è¯·æ±‚ï¼Œå› ä¸ºâ€œè‡ªåŠ¨æ£€æµ‹æ‰‹åŠ¨åˆå¹¶â€æœªå¯ç”¨ +pulls.cmd_instruction_merge_warning=è¦å‘Šï¼šæ¤æ“作ä¸èƒ½åˆå¹¶è¯¥åˆå¹¶è¯·æ±‚ï¼Œå› ä¸ºã€Œè‡ªåŠ¨æ£€æµ‹æ‰‹åŠ¨åˆå¹¶ã€æœªå¯ç”¨ pulls.clear_merge_message=清除åˆå¹¶ä¿¡æ¯ -pulls.clear_merge_message_hint=清除åˆå¹¶æ¶ˆæ¯åªä¼šåˆ 除æäº¤æ¶ˆæ¯å†…容,并ä¿ç•™ç”Ÿæˆçš„ git é™„åŠ å†…å®¹ï¼Œå¦‚â€œCo-Authored-By …â€ã€‚ +pulls.clear_merge_message_hint=清除åˆå¹¶æ¶ˆæ¯åªä¼šåˆ 除æäº¤æ¶ˆæ¯å†…容,并ä¿ç•™ç”Ÿæˆçš„ Git é™„åŠ å†…å®¹ï¼Œå¦‚ã€ŒCo-Authored-By…ã€ã€‚ pulls.auto_merge_button_when_succeed=(当检查æˆåŠŸæ—¶) pulls.auto_merge_when_succeed=在所有检查æˆåŠŸåŽè‡ªåЍåˆå¹¶ @@ -1972,7 +1987,7 @@ pulls.auto_merge_newly_scheduled_comment=`已于 %[1]s 设置æ¤åˆå¹¶è¯·æ±‚在æ pulls.auto_merge_canceled_schedule_comment=`已于 %[1]s å–æ¶ˆäº†è‡ªåЍåˆå¹¶è®¾ç½® ` pulls.delete.title=åˆ é™¤æ¤åˆå¹¶è¯·æ±‚? -pulls.delete.text=ä½ çœŸçš„è¦åˆ 除这个åˆå¹¶è¯·æ±‚å—? (è¿™å°†æ°¸ä¹…åˆ é™¤æ‰€æœ‰å†…å®¹ã€‚å¦‚æžœä½ æ‰“ç®—å°†å†…å®¹å˜æ¡£ï¼Œè¯·è€ƒè™‘å…³é—它) +pulls.delete.text=您真的è¦åˆ 除这个åˆå¹¶è¯·æ±‚å—ï¼Ÿï¼ˆè¿™å°†æ°¸ä¹…åˆ é™¤æ‰€æœ‰å†…å®¹ã€‚å¦‚æžœæ‚¨æ‰“ç®—å°†å†…å®¹å˜æ¡£ï¼Œè¯·è€ƒè™‘å…³é—它) pulls.recently_pushed_new_branches=您已ç»äºŽ%[2]s推é€äº†åˆ†æ”¯ <strong>%[1]s</strong> pulls.upstream_diverging_prompt_behind_1=该分支è½åŽäºŽ %[2]s %[1]d 个æäº¤ @@ -1981,7 +1996,7 @@ pulls.upstream_diverging_prompt_base_newer=基础分支 %s 有新的更改 pulls.upstream_diverging_merge=åŒæ¥æ´¾ç”Ÿ pulls.upstream_diverging_merge_confirm=è¦å°† %[1]s åˆå¹¶åˆ° %[2]s å—? -pull.deleted_branch=(å·²åˆ é™¤): %s +pull.deleted_branch=(å·²åˆ é™¤):%s pull.agit_documentation=查看有关 AGit 的文档 comments.edit.already_changed=æ— æ³•ä¿å˜å¯¹è¯„论的更改。其内容似乎已被其他用户更改。 请刷新页é¢å¹¶é‡æ–°ç¼–辑以é¿å…覆盖他们的更改 @@ -1990,7 +2005,7 @@ milestones.new=新的里程碑 milestones.closed=于 %så…³é— milestones.update_ago=已更新 %s milestones.no_due_date=æš‚æ— æˆªæ¢æ—¥æœŸ -milestones.open=å¼€å¯ +milestones.open=打开 milestones.close=å…³é— milestones.new_subheader=里程碑å¯ä»¥å¸®åŠ©æ‚¨ç»„ç»‡å·¥å•并跟踪其进度。 milestones.completeness=<strong>%d%%</strong> å·²å®Œæˆ @@ -1999,16 +2014,16 @@ milestones.title=æ ‡é¢˜ milestones.desc=æè¿° milestones.due_date=æˆªæ¢æ—¥æœŸï¼ˆå¯é€‰ï¼‰ milestones.clear=清除 -milestones.invalid_due_date_format=åˆ°æœŸæ—¶é—´çš„æ ¼å¼å¿…须是 'yyyy-mm-dd' 的形å¼ã€‚ -milestones.create_success=里程碑 %s 创建æˆåŠŸã€‚ +milestones.invalid_due_date_format=åˆ°æœŸæ—¶é—´çš„æ ¼å¼å¿…须是「yyyy-mm-ddã€çš„å½¢å¼ã€‚ +milestones.create_success=里程碑「%sã€åˆ›å»ºæˆåŠŸã€‚ milestones.edit=编辑里程碑 milestones.edit_subheader=里程碑组织工å•,åˆå¹¶è¯·æ±‚和跟踪进度。 milestones.cancel=å–æ¶ˆ milestones.modify=更新里程碑 -milestones.edit_success=里程碑 %s å·²ç»æ›´æ–°ã€‚ +milestones.edit_success=里程碑「%sã€æ›´æ–°æˆåŠŸã€‚ milestones.deletion=åˆ é™¤é‡Œç¨‹ç¢‘ milestones.deletion_desc=åˆ é™¤è¯¥é‡Œç¨‹ç¢‘å°†ä¼šç§»é™¤æ‰€æœ‰å·¥å•ä¸ç›¸å…³çš„ä¿¡æ¯ã€‚是å¦ç»§ç»ï¼Ÿ -milestones.deletion_success=é‡Œç¨‹ç¢‘å·²è¢«åˆ é™¤ã€‚ +milestones.deletion_success=é‡Œç¨‹ç¢‘å·²åˆ é™¤ã€‚ milestones.filter_sort.name=åç§° milestones.filter_sort.earliest_due_data=到期日从远到近 milestones.filter_sort.latest_due_date=到期日从近到远 @@ -2017,7 +2032,7 @@ milestones.filter_sort.most_complete=完æˆåº¦ä»Žé«˜åˆ°ä½Ž milestones.filter_sort.most_issues=å·¥å•从多到少 milestones.filter_sort.least_issues=å·¥å•从少到多 -signing.will_sign=这个æäº¤å°†ç”¨å¯†é’¥ "%s" ç¾å。 +signing.will_sign=这个æäº¤å°†ç”¨å¯†é’¥ã€Œ%sã€ç¾å。 signing.wont_sign.error=在检查æäº¤æ˜¯å¦å¯ä»¥è¢«ç¾å时出错。 signing.wont_sign.nokey=没有å¯ç”¨çš„密钥æ¥ç¾ç½²è¿™ä¸ªæäº¤ã€‚ signing.wont_sign.never=æäº¤ä»Žæœªç¾å。 @@ -2032,11 +2047,11 @@ signing.wont_sign.approved=åˆå¹¶å°†ä¸ä¼šè¢«ç¾åï¼Œå› ä¸ºåˆå¹¶è¯·æ±‚æœªè¢«æ‰ signing.wont_sign.not_signed_in=您还没有登录。 ext_wiki=访问外部百科 -ext_wiki.desc=链接到外部 wiki。 +ext_wiki.desc=链接到外部百科。 wiki=百科 -wiki.welcome=欢迎æ¥åˆ°ç™¾ç§‘! -wiki.welcome_desc=百科å…è®¸ä½ æ’°å†™å’Œä¸Žå作者分享文档 +wiki.welcome=欢迎æ¥åˆ°ç™¾ç§‘。 +wiki.welcome_desc=百科å…许您撰写和与å作者分享文档。 wiki.desc=撰写和与å作者分享文档 wiki.create_first_page=åˆ›å»ºç¬¬ä¸€ä¸ªé¡µé¢ wiki.page=é¡µé¢ @@ -2053,95 +2068,95 @@ wiki.file_revision=页é¢åŽ†å² wiki.wiki_page_revisions=页é¢åŽ†å² wiki.back_to_wiki=返回百科 wiki.delete_page_button=åˆ é™¤é¡µé¢ -wiki.delete_page_notice_1=ç™¾ç§‘é¡µé¢ %s åˆ é™¤åŽæ— 法æ¢å¤ï¼Œæ˜¯å¦ç»§ç»ï¼Ÿ +wiki.delete_page_notice_1=百科页é¢ã€Œ%sã€åˆ é™¤åŽæ— 法æ¢å¤ï¼Œæ˜¯å¦ç»§ç»ï¼Ÿ wiki.page_already_exists=相åŒåç§°çš„ Wiki 页é¢å·²ç»å˜åœ¨ã€‚ -wiki.reserved_page=百科页é¢åç§° %s 是被ä¿ç•™çš„。 +wiki.reserved_page=百科页é¢å称「%sã€æ˜¯ä¿ç•™çš„。 wiki.pages=æ‰€æœ‰é¡µé¢ wiki.last_updated=æœ€åŽæ›´æ–°äºŽ %s -wiki.page_name_desc=è¾“å…¥æ¤ Wiki 页é¢çš„å称。特殊å称有:'Home', '_Sidebar' å’Œ '_Footer'。 +wiki.page_name_desc=输入æ¤ç™¾ç§‘页é¢çš„å称。特殊å称有:「Homeã€ï¼Œã€Œ_Sidebarã€å’Œã€Œ_Footerã€ã€‚ wiki.original_git_entry_tooltip=查看原始的 Git æ–‡ä»¶è€Œä¸æ˜¯ä½¿ç”¨å‹å¥½é“¾æŽ¥ã€‚ -activity=åŠ¨æ€ +activity=活动 activity.navbar.pulse=活动 activity.navbar.code_frequency=代ç 频率 activity.navbar.contributors=贡献者 -activity.navbar.recent_commits=最近的æäº¤ +activity.navbar.recent_commits=最近æäº¤ activity.period.filter_label=周期: activity.period.daily=1 天 activity.period.halfweekly=3 天 -activity.period.weekly=1周 -activity.period.monthly=1 个月 -activity.period.quarterly=3个月 -activity.period.semiyearly=6 个月 -activity.period.yearly=1å¹´ +activity.period.weekly=1 周 +activity.period.monthly=1 月 +activity.period.quarterly=3 月 +activity.period.semiyearly=6 月 +activity.period.yearly=1 å¹´ activity.overview=概览 -activity.active_prs_count_1=<strong>%d</strong> åˆå¹¶è¯·æ±‚ -activity.active_prs_count_n=<strong>%d</strong> åˆå¹¶è¯·æ±‚ -activity.merged_prs_count_1=åˆå¹¶è¯·æ±‚ -activity.merged_prs_count_n=åˆå¹¶è¯·æ±‚ -activity.opened_prs_count_1=æ–°åˆå¹¶è¯·æ±‚ -activity.opened_prs_count_n=æ–°åˆå¹¶è¯·æ±‚ -activity.title.user_1=%d 用户 -activity.title.user_n=%d 用户 -activity.title.prs_1=%d åˆå¹¶è¯·æ±‚ -activity.title.prs_n=%d åˆå¹¶è¯·æ±‚ +activity.active_prs_count_1=<strong>%d</strong> 个åˆå¹¶è¯·æ±‚ +activity.active_prs_count_n=<strong>%d</strong> 个åˆå¹¶è¯·æ±‚ +activity.merged_prs_count_1=å·²åˆå¹¶çš„åˆå¹¶è¯·æ±‚ +activity.merged_prs_count_n=å·²åˆå¹¶çš„åˆå¹¶è¯·æ±‚ +activity.opened_prs_count_1=创建的åˆå¹¶è¯·æ±‚ +activity.opened_prs_count_n=创建的åˆå¹¶è¯·æ±‚ +activity.title.user_1=%d ä½ç”¨æˆ· +activity.title.user_n=%d ä½ç”¨æˆ· +activity.title.prs_1=%d 个åˆå¹¶è¯·æ±‚ +activity.title.prs_n=%d 个åˆå¹¶è¯·æ±‚ activity.title.prs_merged_by=%[2]s ç”± %[1]s åˆå¹¶ activity.title.prs_opened_by=%[2]s 创建了 %[1]s activity.merged_prs_label=å·²åˆå¹¶ activity.opened_prs_label=已创建 -activity.active_issues_count_1=<strong>%d</strong> å·¥å• -activity.active_issues_count_n=<strong>%d</strong> å·¥å• +activity.active_issues_count_1=<strong>%d</strong> å¼ å·¥å• +activity.active_issues_count_n=<strong>%d</strong> å¼ å·¥å• activity.closed_issues_count_1=已关é—çš„å·¥å• activity.closed_issues_count_n=已关é—çš„å·¥å• -activity.title.issues_1=%d å·¥å• -activity.title.issues_n=%d å·¥å• -activity.title.issues_closed_from=%s 从 %s è¢«å…³é— +activity.title.issues_1=%d å¼ å·¥å• +activity.title.issues_n=%d å¼ å·¥å• +activity.title.issues_closed_from=%s 从 %s å…³é— activity.title.issues_created_by=%[2]s 创建了 %[1]s activity.closed_issue_label=å·²å…³é— -activity.new_issues_count_1=åˆ›å»ºå·¥å• -activity.new_issues_count_n=åˆ›å»ºå·¥å• +activity.new_issues_count_1=å·²æ‰“å¼€çš„å·¥å• +activity.new_issues_count_n=å·²æ‰“å¼€çš„å·¥å• activity.new_issue_label=打开的 activity.title.unresolved_conv_1=%d æœªè§£å†³çš„ä¼šè¯ activity.title.unresolved_conv_n=%d æœªè§£å†³çš„ä¼šè¯ activity.unresolved_conv_desc=这些最近更新的工å•å’Œåˆå¹¶è¯·æ±‚还没有解决。 activity.unresolved_conv_label=打开 -activity.title.releases_1=%d 版本å‘布 -activity.title.releases_n=%d 版本å‘布 +activity.title.releases_1=%d 个å‘布 +activity.title.releases_n=%d 个å‘布 activity.title.releases_published_by=%[2]s å‘布了 %[1]s activity.published_release_label=å·²å‘布 activity.no_git_activity=åœ¨æ¤æœŸé—´æ²¡æœ‰ä»»ä½•æäº¤æ´»åŠ¨ã€‚ -activity.git_stats_exclude_merges=排除åˆå¹¶ï¼Œ -activity.git_stats_author_1=%d 作者 -activity.git_stats_author_n=%d 作者 -activity.git_stats_pushed_1=å·²ç»æŽ¨é€ -activity.git_stats_pushed_n=å·²ç»æŽ¨é€ -activity.git_stats_commit_1=%d æäº¤ -activity.git_stats_commit_n=%d æäº¤ +activity.git_stats_exclude_merges=排除åˆå¹¶åŽï¼Œ +activity.git_stats_author_1=%d ä½ä½œè€… +activity.git_stats_author_n=%d ä½ä½œè€… +activity.git_stats_pushed_1=å·²æŽ¨é€ +activity.git_stats_pushed_n=å·²æŽ¨é€ +activity.git_stats_commit_1=%d 次æäº¤ +activity.git_stats_commit_n=%d 次æäº¤ activity.git_stats_push_to_branch=到 %s å’Œ activity.git_stats_push_to_all_branches=到所有分支。 activity.git_stats_on_default_branch=在 %s 上, -activity.git_stats_file_1=%d 文件 -activity.git_stats_file_n=%d 文件 -activity.git_stats_files_changed_1=å·²ç»æ”¹å˜ -activity.git_stats_files_changed_n=å·²ç»æ”¹å˜ -activity.git_stats_additions=而且 -activity.git_stats_addition_1=新增 %d 行 -activity.git_stats_addition_n=新增 %d 行 +activity.git_stats_file_1=%d 个文件 +activity.git_stats_file_n=%d 个文件 +activity.git_stats_files_changed_1=已修改 +activity.git_stats_files_changed_n=已修改 +activity.git_stats_additions=并且已有 +activity.git_stats_addition_1=%d 行新增 +activity.git_stats_addition_n=%d 行新增 activity.git_stats_and_deletions=å’Œ -activity.git_stats_deletion_1=åˆ é™¤ %d 行 -activity.git_stats_deletion_n=åˆ é™¤ %d 行 +activity.git_stats_deletion_1=%d è¡Œåˆ é™¤ +activity.git_stats_deletion_n=%d è¡Œåˆ é™¤ contributors.contribution_type.filter_label=贡献类型: contributors.contribution_type.commits=æäº¤ -contributors.contribution_type.additions=更多 +contributors.contribution_type.additions=新增 contributors.contribution_type.deletions=åˆ é™¤ settings=设置 -settings.desc=è®¾ç½®æ˜¯ä½ å¯ä»¥ç®¡ç†ä»“库设置的地方 +settings.desc=设置是您å¯ä»¥ç®¡ç†ä»“库设置的地方 settings.options=仓库 settings.public_access=公开访问 -settings.public_access_desc=é…置公共访客访问æƒé™ä»¥è¦†ç›–æ¤å˜å‚¨åº“的默认值。 -settings.public_access.docs.not_set=未设置:没有é¢å¤–的公共访问æƒé™ã€‚访客æƒé™éµå¾ªå˜å‚¨åº“çš„å¯è§æ€§å’Œæˆå‘˜æƒé™ã€‚ +settings.public_access_desc=é…置公共访客访问æƒé™ä»¥è¦†ç›–æ¤ä»“库的默认值。 +settings.public_access.docs.not_set=未设置:没有é¢å¤–的公共访问æƒé™ã€‚访客æƒé™éµå¾ªä»“库的å¯è§æ€§å’Œæˆå‘˜æƒé™ã€‚ settings.public_access.docs.anonymous_read=匿åå¯è¯»ï¼šæœªç™»å½•的用户å¯ä»¥é€šè¿‡è¯»å–æƒé™è®¿é—®å•元。 settings.public_access.docs.everyone_read=所有人å¯è¯»ï¼šæ‰€æœ‰ç™»å½•用户都å¯ä»¥é€šè¿‡è¯»å–æƒé™è®¿é—®å•元。读å–问题/拉å–请求å•元的æƒé™ä¹Ÿæ„味ç€ç”¨æˆ·å¯ä»¥åˆ›å»ºæ–°çš„问题/拉å–请求。 settings.public_access.docs.everyone_write=所有人å¯å†™ï¼šæ‰€æœ‰ç™»å½•用户都有写入æƒé™ã€‚åªæœ‰ç™¾ç§‘æ”¯æŒæ¤æƒé™ã€‚ @@ -2151,6 +2166,7 @@ settings.collaboration.write=å¯å†™æƒé™ settings.collaboration.read=å¯è¯»æƒé™ settings.collaboration.owner=所有者 settings.collaboration.undefined=未定义 +settings.collaboration.per_unit=å•å…ƒæƒé™ settings.hooks=Web é’©å settings.githooks=ç®¡ç† Git é’©å settings.basic_settings=基本设置 @@ -2158,14 +2174,14 @@ settings.mirror_settings=镜åƒè®¾ç½® settings.mirror_settings.docs=è®¾ç½®æ‚¨çš„ä»“åº“ä»¥è‡ªåŠ¨åŒæ¥å¦ä¸€ä¸ªä»“库的æäº¤ã€æ ‡ç¾å’Œåˆ†æ”¯ã€‚ settings.mirror_settings.docs.disabled_pull_mirror.instructions=设置您的项目以自动将æäº¤ã€æ ‡ç¾å’Œåˆ†æ”¯æŽ¨é€åˆ°å¦ä¸€ä¸ªä»“库。您的站点管ç†å‘˜å·²ç¦ç”¨äº†æ‹‰å–镜åƒã€‚ settings.mirror_settings.docs.disabled_push_mirror.instructions=å°†æ‚¨çš„é¡¹ç›®è®¾ç½®ä¸ºè‡ªåŠ¨ä»Žä¸€ä¸ªä»“åº“æ‹‰å–æäº¤ã€æ ‡ç¾å’Œåˆ†æ”¯ã€‚ -settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=现在,这åªèƒ½åœ¨â€œè¿ç§»å¤–部仓库â€èœå•ä¸å®Œæˆã€‚欲了解更多信æ¯ï¼Œè¯·å’¨è¯¢ï¼š +settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=现在,这åªèƒ½åœ¨ã€Œè¿ç§»å¤–部仓库ã€èœå•ä¸å®Œæˆã€‚欲了解更多信æ¯ï¼Œè¯·å‚考: settings.mirror_settings.docs.disabled_push_mirror.info=您的站点管ç†å‘˜å·²ç¦ç”¨æŽ¨é€é•œåƒã€‚ settings.mirror_settings.docs.no_new_mirrors=您的仓库将镜åƒåŒæ¥å¦ä¸€ä¸ªä»“库的更改。请注æ„,您现在ä¸èƒ½åˆ›å»ºä»»ä½•新的镜åƒã€‚ settings.mirror_settings.docs.can_still_use=虽然您ä¸èƒ½ä¿®æ”¹çŽ°æœ‰é•œåƒæˆ–创建新镜åƒï¼Œä½†æ‚¨ä»ç„¶å¯ä»¥ä½¿ç”¨å·²å˜åœ¨çš„镜åƒã€‚ settings.mirror_settings.docs.pull_mirror_instructions=è¦åˆ›å»ºä¸€ä¸ªæ‹‰å–镜åƒï¼Œè¯·å‚阅: settings.mirror_settings.docs.more_information_if_disabled=您å¯ä»¥åœ¨è¿™é‡Œæ‰¾åˆ°æ›´å¤šå…³äºŽæŽ¨é€å’Œæ‹‰å–镜åƒçš„ä¿¡æ¯ï¼š settings.mirror_settings.docs.doc_link_title=如何镜åƒä»“库? -settings.mirror_settings.docs.doc_link_pull_section=文档ä¸çš„ “从远程仓库拉å–†部分。 +settings.mirror_settings.docs.doc_link_pull_section=文档ä¸çš„「从远程仓库拉å–ã€éƒ¨åˆ†ã€‚ settings.mirror_settings.docs.pulling_remote_title=从远程仓库拉å–代ç settings.mirror_settings.mirrored_repository=镜åƒåº“ settings.mirror_settings.pushed_repository=推é€ä»“库 @@ -2193,22 +2209,22 @@ settings.use_internal_wiki=使用内置百科 settings.default_wiki_branch_name=默认百科分支åç§° settings.failed_to_change_default_wiki_branch=更改百科默认分支失败。 settings.use_external_wiki=使用外部百科 -settings.external_wiki_url=外部 Wiki 链接 +settings.external_wiki_url=外部百科链接 settings.external_wiki_url_error=å¤–éƒ¨ç™¾ç§‘é“¾æŽ¥æ— æ•ˆ -settings.external_wiki_url_desc=å½“ç‚¹å‡»ç™¾ç§‘æ ‡ç¾æ—¶ï¼Œè®¿é—®è€…将被é‡å®šå‘到外部百科系统的URL。 +settings.external_wiki_url_desc=å½“ç‚¹å‡»ç™¾ç§‘æ ‡ç¾æ—¶ï¼Œè®¿é—®è€…将被é‡å®šå‘到外部百科系统的 URL。 settings.issues_desc=å¯ç”¨å·¥å•系统 -settings.use_internal_issue_tracker=使用内置的轻é‡çº§å·¥å•管ç†ç³»ç»Ÿ -settings.use_external_issue_tracker=使用外部的工å•管ç†ç³»ç»Ÿ +settings.use_internal_issue_tracker=使用内置工å•系统 +settings.use_external_issue_tracker=使用外部工å•系统 settings.external_tracker_url=外部工å•系统 URL settings.external_tracker_url_error=å¤–éƒ¨ç™¾ç§‘é“¾æŽ¥æ— æ•ˆ -settings.external_tracker_url_desc=å½“ç‚¹å‡»å·¥å•æ ‡ç¾æ—¶ï¼Œè®¿é—®è€…将被é‡å®šå‘到外部工å•系统的URL。 +settings.external_tracker_url_desc=å½“ç‚¹å‡»å·¥å•æ ‡ç¾æ—¶ï¼Œè®¿é—®è€…将被é‡å®šå‘到外部工å•系统的 URL。 settings.tracker_url_format=外部工å•管ç†ç³»ç»Ÿçš„ URL æ ¼å¼ settings.tracker_url_format_error=外部工å•é“¾æŽ¥æ— æ•ˆ settings.tracker_issue_style=外部工å•管ç†ç³»ç»Ÿçš„ç¼–å·æ ¼å¼ settings.tracker_issue_style.numeric=纯数å—å½¢å¼ settings.tracker_issue_style.alphanumeric=è‹±æ–‡å—æ¯æ•°å—组åˆå½¢å¼ settings.tracker_issue_style.regexp=æ£åˆ™è¡¨è¾¾å¼ -settings.tracker_issue_style.regexp_pattern=æ£åˆ™è¡¨è¾¾å¼æ¨¡å¼ +settings.tracker_issue_style.regexp_pattern=æ£åˆ™è¡¨è¾¾å¼ settings.tracker_issue_style.regexp_pattern_desc=第一个被æ•获的组将å–代 <code>{index}</code>。 settings.tracker_url_format_desc=使用å ä½ç¬¦ <code>{user}</code>, <code>{repo}</code> å’Œ <code>{index}</code> 作为用户åã€ä»“库å和工å•索引。 settings.enable_timetracker=å¯ç”¨æ—¶é—´è·Ÿè¸ª @@ -2218,15 +2234,15 @@ settings.pulls.ignore_whitespace=å¿½ç•¥ç©ºç™½å†²çª settings.pulls.enable_autodetect_manual_merge=å¯ç”¨è‡ªåŠ¨æ£€æµ‹æ‰‹åŠ¨åˆå¹¶ (注æ„:在æŸäº›ç‰¹æ®Šæƒ…况下å¯èƒ½å‘生错误判æ–) settings.pulls.allow_rebase_update=å…许通过å˜åŸºæ›´æ–°åˆå¹¶è¯·æ±‚分支 settings.pulls.default_delete_branch_after_merge=默认åˆå¹¶åŽåˆ 除åˆå¹¶è¯·æ±‚分支 -settings.pulls.default_allow_edits_from_maintainers=默认开å¯å…许维护者编辑 -settings.releases_desc=å¯ç”¨å‘布 +settings.pulls.default_allow_edits_from_maintainers=默认å…许维护者编辑 +settings.releases_desc=å¯ç”¨ä»“库å‘布 settings.packages_desc=å¯ç”¨ä»“库软件包注册ä¸å¿ƒ settings.projects_desc=å¯ç”¨é¡¹ç›® settings.projects_mode_desc=é¡¹ç›®æ¨¡å¼ (è¦æ˜¾ç¤ºçš„项目类型) settings.projects_mode_repo=仅仓库项目 settings.projects_mode_owner=ä»…é™ç”¨æˆ·æˆ–组织项目 settings.projects_mode_all=所有项目 -settings.actions_desc=å¯ç”¨ Actions +settings.actions_desc=å¯ç”¨ä»“åº“å·¥ä½œæµ settings.admin_settings=管ç†å‘˜è®¾ç½® settings.admin_enable_health_check=å¯ç”¨ä»“库å¥åº·æ£€æŸ¥ (git fsck) settings.admin_code_indexer=代ç 索引器 @@ -2249,23 +2265,23 @@ settings.convert_fork_notices_1=该æ“作会将派生仓库转æ¢ä¸ºæ™®é€šä»“库 settings.convert_fork_confirm=转æ¢ä»“库 settings.convert_fork_succeed=æ¤æ´¾ç”Ÿä»“库已ç»è½¬æ¢ä¸ºæ™®é€šä»“库。 settings.transfer=è½¬ç§»ä»“åº“æ‰€æœ‰æƒ -settings.transfer.rejected=代ç 库转移被拒ç»ã€‚ -settings.transfer.success=代ç 库转移æˆåŠŸã€‚ -settings.transfer.blocked_user=æ— æ³•ä¼ è¾“ä»“åº“ï¼Œå› ä¸ºæ‚¨è¢«æ–°çš„æ‰€æœ‰è€…å±è”½ã€‚ +settings.transfer.rejected=仓库转移被拒ç»ã€‚ +settings.transfer.success=仓库转移æˆåŠŸã€‚ +settings.transfer.blocked_user=æ— æ³•è½¬ç§»ä»“åº“ï¼Œå› ä¸ºæ‚¨å·²è¢«æ–°æ‰€æœ‰è€…å±è”½ã€‚ settings.transfer_abort=å–æ¶ˆè½¬ç§» -settings.transfer_abort_invalid=ä½ ä¸èƒ½å–消ä¸å˜åœ¨çš„代ç 库转移。 -settings.transfer_abort_success=æˆåŠŸå–æ¶ˆäº†å°†ä»£ç 库转让给 %s。 +settings.transfer_abort_invalid=您ä¸èƒ½å–消ä¸å˜åœ¨çš„仓库转移。 +settings.transfer_abort_success=æˆåŠŸå–æ¶ˆå°†ä»“库转移给 %s。 settings.transfer_desc=您å¯ä»¥å°†ä»“库转移至您拥有管ç†å‘˜æƒé™çš„叿ˆ·æˆ–组织。 -settings.transfer_form_title=输入仓库å称以åšç¡®è®¤: -settings.transfer_in_progress=当剿£åœ¨è¿›è¡Œè½¬è®©ã€‚ å¦‚æžœä½ æƒ³å°†æ¤ä»£ç 库转让给å¦ä¸€ä¸ªç”¨æˆ·ï¼Œè¯·å–消它。 -settings.transfer_notices_1=- 如果将æ¤ä»“库转移给其他用户, 您将失去对æ¤ä»“库的访问æƒé™ã€‚ -settings.transfer_notices_2=-如果将其转移到您 (å…±åŒ) 拥有的组织,您å¯ä»¥ç»§ç»è®¿é—®è¯¥ä»“库。 -settings.transfer_notices_3=- å¦‚æžœä»“åº“æ˜¯ç§æœ‰çš„并且被转移给æŸä¸ªç”¨æˆ·ï¼Œé‚£ä¹ˆæ¤æ“作å¯ä»¥ç¡®ä¿è¯¥ç”¨æˆ·è‡³å°‘具有读æƒé™(以åŠå¿…è¦æ—¶çš„æ›´æ”¹æƒé™)。 -settings.transfer_notices_4=- 如果å˜å‚¨åº“属于æŸä¸ªç»„织,而您将其转移给å¦ä¸€ä¸ªç»„织或个人,那么您将失去å˜å‚¨åº“å·¥å•与其组织项目系统之间的链接。 +settings.transfer_form_title=输入仓库å称以确认: +settings.transfer_in_progress=当剿£åœ¨è¿›è¡Œè½¬ç§»ã€‚ 如果您想将æ¤ä»“库转移给å¦ä¸€ä¸ªç”¨æˆ·ï¼Œè¯·å–消它。 +settings.transfer_notices_1=- 如果将æ¤ä»“库转移给其他用户,您将失去对æ¤ä»“库的访问æƒé™ã€‚ +settings.transfer_notices_2=- 如果将其转移到您(共åŒï¼‰æ‹¥æœ‰çš„组织,您å¯ä»¥ç»§ç»è®¿é—®è¯¥ä»“库。 +settings.transfer_notices_3=- å¦‚æžœä»“åº“æ˜¯ç§æœ‰çš„并且被转移给æŸä¸ªç”¨æˆ·ï¼Œé‚£ä¹ˆæ¤æ“作å¯ä»¥ç¡®ä¿è¯¥ç”¨æˆ·è‡³å°‘具有读æƒé™ï¼ˆä»¥åŠå¿…è¦æ—¶çš„æ›´æ”¹æƒé™ï¼‰ã€‚ +settings.transfer_notices_4=- 如果仓库属于æŸä¸ªç»„织,而您将其转移给å¦ä¸€ä¸ªç»„织或个人,那么您将失去仓库工å•与其组织项目系统之间的链接。 settings.transfer_owner=新拥有者 -settings.transfer_perform=执行转让 -settings.transfer_started=该代ç åº“å·²è¢«æ ‡è®°ä¸ºè½¬è®©å¹¶ç‰å¾…æ¥è‡ª %s 的确认 -settings.transfer_succeed=仓库已被转移。 +settings.transfer_perform=执行转移 +settings.transfer_started=è¯¥ä»“åº“å·²æ ‡è®°ä¸ºè½¬ç§»å¹¶ç‰å¾…æ¥è‡ª %s 的确认 +settings.transfer_succeed=仓库已转移。 settings.signing_settings=ç¾å验è¯è®¾ç½® settings.trust_model=ç¾å信任模型 settings.trust_model.default=默认信任模型 @@ -2275,7 +2291,7 @@ settings.trust_model.collaborator.long=å作者:信任å作者的ç¾å settings.trust_model.collaborator.desc=æ¤ä»“库ä¸å作者的有效ç¾åå°†è¢«æ ‡è®°ä¸ºã€Œå¯ä¿¡ã€(æ— è®ºå®ƒä»¬æ˜¯å¦æ˜¯æäº¤è€…),ç¾ååªç¬¦åˆæäº¤è€…æ—¶å°†æ ‡è®°ä¸ºã€Œä¸å¯ä¿¡ã€ï¼Œéƒ½ä¸åŒ¹é…æ—¶æ ‡è®°ä¸ºã€Œä¸åŒ¹é…ã€ã€‚ settings.trust_model.committer=æäº¤è€… settings.trust_model.committer.long=æäº¤è€…: 信任与æäº¤è€…相符的ç¾å (æ¤ç‰¹æ€§ç±»ä¼¼ GitHub,这会强制采用 Gitea 作为æäº¤è€…å’Œç¾å者) -settings.trust_model.committer.desc=有效ç¾ååªæœ‰å’Œæäº¤è€…ç›¸åŒ¹é…æ‰ä¼šè¢«æ ‡è®°ä¸ºâ€œå—ä¿¡ä»»â€ï¼Œå¦åˆ™å®ƒä»¬å°†è¢«æ ‡è®°ä¸ºâ€œä¸åŒ¹é…â€ã€‚这强制 Gitea æˆä¸ºç¾åæäº¤çš„æäº¤è€…,而实际æäº¤è€…è¢«åŠ ä¸Š Co-authored-by: å’Œ Co-committed-by: çš„æ ‡è®°ã€‚ 默认的 Gitea å¯†é’¥å¿…é¡»åŒ¹é…æ•°æ®åº“ä¸çš„一å用户。 +settings.trust_model.committer.desc=有效ç¾ååªæœ‰å’Œæäº¤è€…ç›¸åŒ¹é…æ‰ä¼šè¢«æ ‡è®°ä¸ºã€Œå—ä¿¡ä»»ã€ï¼Œå¦åˆ™å®ƒä»¬å°†è¢«æ ‡è®°ä¸ºã€Œä¸åŒ¹é…ã€ã€‚这强制 Gitea æˆä¸ºç¾åæäº¤çš„æäº¤è€…,而实际æäº¤è€…è¢«åŠ ä¸Š Co-authored-by: å’Œ Co-committed-by: çš„æ ‡è®°ã€‚ 默认的 Gitea å¯†é’¥å¿…é¡»åŒ¹é…æ•°æ®åº“ä¸çš„一å用户。 settings.trust_model.collaboratorcommitter=å作者+æäº¤è€… settings.trust_model.collaboratorcommitter.long=å作者+æäº¤è€…:信任åä½œè€…åŒæ—¶æ˜¯æäº¤è€…çš„ç¾å settings.trust_model.collaboratorcommitter.desc=æ¤ä»“库ä¸å作者的有效ç¾ååœ¨ä»–åŒæ—¶æ˜¯æäº¤è€…æ—¶å°†è¢«æ ‡è®°ä¸ºã€Œå¯ä¿¡ã€ï¼Œç¾ååªåŒ¹é…了æäº¤è€…æ—¶å°†æ ‡è®°ä¸ºã€Œä¸å¯ä¿¡ã€ï¼Œéƒ½ä¸åŒ¹é…æ—¶æ ‡è®°ä¸ºã€Œä¸åŒ¹é…ã€ã€‚这会强制 Gitea æˆä¸ºç¾å者和æäº¤è€…,实际的æäº¤è€…å°†è¢«æ ‡è®°äºŽæäº¤æ¶ˆæ¯ç»“尾处的「Co-Authored-By:ã€å’Œã€ŒCo-Committed-By:ã€ã€‚默认的 Gitea ç¾åå¯†é’¥å¿…é¡»åŒ¹é…æ•°æ®åº“ä¸çš„一个用户密钥。 @@ -2286,19 +2302,19 @@ settings.confirm_wiki_delete=åˆ é™¤ç™¾ç§‘æ•°æ® settings.wiki_deletion_success=仓库百科数æ®åˆ 除æˆåŠŸï¼ settings.delete=åˆ é™¤æœ¬ä»“åº“ settings.delete_desc=åˆ é™¤ä»“åº“æ˜¯æ°¸ä¹…æ€§çš„, æ— æ³•æ’¤æ¶ˆã€‚ -settings.delete_notices_1=- æ¤æ“作 <strong>ä¸å¯ä»¥</strong> 被回滚。 +settings.delete_notices_1=- æ¤æ“作 <strong>æ— æ³•</strong> 被回滚。 settings.delete_notices_2=- æ¤æ“ä½œå°†æ°¸ä¹…åˆ é™¤ä»“åº“ <strong>%s</strong>,包括 Git æ•°æ®ã€ å·¥å•ã€è¯„论ã€ç™¾ç§‘å’Œå作者的æ“作æƒé™ã€‚ settings.delete_notices_fork_1=- 在æ¤ä»“åº“åˆ é™¤åŽï¼Œå®ƒçš„æ´¾ç”Ÿä»“åº“å°†å˜æˆç‹¬ç«‹ä»“库。 -settings.deletion_success=ä»“åº“å·²è¢«åˆ é™¤ã€‚ +settings.deletion_success=ä»“åº“å·²åˆ é™¤ã€‚ settings.update_settings_success=仓库设置已更新。 -settings.update_settings_no_unit=该代ç 库应该至少å…许æŸç§å½¢å¼çš„交互。 +settings.update_settings_no_unit=该仓库应至少å…许æŸç§å½¢å¼çš„交互。 settings.confirm_delete=åˆ é™¤æœ¬ä»“åº“ settings.add_collaborator=å¢žåŠ å作者 settings.add_collaborator_success=åä½œè€…æ·»åŠ æˆåŠŸï¼ settings.add_collaborator_inactive_user=æ— æ³•æ·»åŠ æœªæ¿€æ´»çš„ç”¨æˆ·ä½œä¸ºåˆä½œè€…。 settings.add_collaborator_owner=ä¸èƒ½å°†æ‰€æœ‰è€…æ·»åŠ ä¸ºå作者。 -settings.add_collaborator_duplicate=åˆä½œè€…å·²ç»è¢«æ·»åŠ åˆ°æœ¬ä»“åº“ã€‚ -settings.add_collaborator.blocked_user=æ¤å†™ä½œè€…被仓库所有者å±è”½ï¼Œå之亦然。 +settings.add_collaborator_duplicate=åä½œè€…å·²ç»æ·»åŠ åˆ°æœ¬ä»“åº“ã€‚ +settings.add_collaborator.blocked_user=æ¤å作者被仓库所有者å±è”½ï¼Œå之亦然。 settings.delete_collaborator=åˆ é™¤ settings.collaborator_deletion=åˆ é™¤å作者 settings.collaborator_deletion_desc=åˆ é™¤å作者åŽä»–å°†æ— æ³•å†å¯¹æ¤ä»“库的访问。继ç»ï¼Ÿ @@ -2312,25 +2328,25 @@ settings.add_team_duplicate=å›¢é˜Ÿå·²ç»æ‹¥æœ‰ä»“库 settings.add_team_success=团队现在å¯ä»¥è®¿é—®ä»“库。 settings.change_team_permission_tip=团队æƒé™è®¾ç½®äºŽå›¢é˜Ÿè®¾ç½®é¡µé¢ï¼Œä¸èƒ½æ ¹æ®ä»“库更改 settings.delete_team_tip=è¯¥å›¢é˜Ÿä»æœ‰ä»“库, æ— æ³•åˆ é™¤ -settings.remove_team_success=团队访问仓库的æƒé™å·²è¢«åˆ 除。 +settings.remove_team_success=团队访问仓库的æƒé™å·²åˆ 除。 settings.add_webhook=æ·»åŠ Web é’©å -settings.add_webhook.invalid_channel_name=Webhook 通é“åç§°ä¸èƒ½ä¸ºç©ºä¸”ä¸èƒ½ä»…包å«ä¸€ä¸ª # å—符。 -settings.hooks_desc=当Gitea事件å‘生时,Webé’©å自动å‘出HTTP POST请求。在 <a target="_blank" rel="noopener noreferrer" href="%s"> 指å—</a> ä¸é˜…读更多内容。 +settings.add_webhook.invalid_channel_name=Web é’©å通é“åç§°ä¸èƒ½ä¸ºç©ºä¸”ä¸èƒ½ä»…包å«ä¸€ä¸ª # å—符。 +settings.hooks_desc=当 Gitea 事件å‘生时,Web é’©å自动å‘出 HTTP POST 请求。在 <a target="_blank" rel="noopener noreferrer" href="%s"> 指å—</a> ä¸é˜…读更多内容。 settings.webhook_deletion=åˆ é™¤ Web é’©å -settings.webhook_deletion_desc=åˆ é™¤ webé’©å å°†åˆ é™¤å…¶è®¾ç½®å’ŒåŽ†å²è®°å½•。继ç»ï¼Ÿ +settings.webhook_deletion_desc=åˆ é™¤ Web é’©åå°†åˆ é™¤å…¶è®¾ç½®å’ŒåŽ†å²è®°å½•。继ç»ï¼Ÿ settings.webhook_deletion_success=Web é’©ååˆ é™¤æˆåŠŸï¼ settings.webhook.test_delivery=æµ‹è¯•æŽ¨é€ -settings.webhook.test_delivery_desc=用å‡äº‹ä»¶æµ‹è¯•这个 webé’©å。 -settings.webhook.test_delivery_desc_disabled=è¦ç”¨ 虚å‡äº‹ä»¶ 测试这个Webhook,请激活它。 +settings.webhook.test_delivery_desc=用å‡äº‹ä»¶æµ‹è¯•这个 Web é’©å。 +settings.webhook.test_delivery_desc_disabled=è¦ç”¨å‡äº‹ä»¶æµ‹è¯•这个 Webé’©å,请激活它。 settings.webhook.request=请求内容 settings.webhook.response=å“应内容 settings.webhook.headers=å¤´ä¿¡æ¯ settings.webhook.payload=内容 settings.webhook.body=å“应体 -settings.webhook.replay.description=釿”¾æ¤ webhook。 -settings.webhook.replay.description_disabled=è‹¥è¦é‡æ’æ¤ WebHook,请激活它。 -settings.webhook.delivery.success=ä¸€ä¸ªäº‹ä»¶å·²è¢«æ·»åŠ åˆ°æŽ¨é€é˜Ÿåˆ—。å¯èƒ½éœ€è¦è¿‡å‡ ç§’é’Ÿæ‰ä¼šæ˜¾ç¤ºåœ¨æŽ¨é€è®°å½•ä¸ã€‚ -settings.githooks_desc=Git Hook 是 Git 本身æä¾›çš„功能。您å¯ä»¥åœ¨ä¸‹æ–¹ç¼–辑 hook 文件以设置自定义æ“作。 +settings.webhook.replay.description=釿”¾æ¤ Web é’©å。 +settings.webhook.replay.description_disabled=è‹¥è¦é‡æ–°è¿è¡Œæ¤ Web é’©å,请激活它。 +settings.webhook.delivery.success=ä¸€ä¸ªäº‹ä»¶å·²æ·»åŠ åˆ°æŽ¨é€é˜Ÿåˆ—。å¯èƒ½éœ€è¦è¿‡å‡ ç§’é’Ÿæ‰ä¼šæ˜¾ç¤ºåœ¨æŽ¨é€è®°å½•ä¸ã€‚ +settings.githooks_desc=Git é’©åæ˜¯ Git 本身æä¾›çš„功能。您å¯ä»¥åœ¨ä¸‹æ–¹ç¼–辑 hook 文件以设置自定义æ“作。 settings.githook_edit_desc=å¦‚æžœé’©åæœªå¯åŠ¨ï¼Œåˆ™ä¼šæ˜¾ç¤ºæ ·ä¾‹æ–‡ä»¶ä¸çš„内容。如果想è¦åˆ 除æŸä¸ªé’©å,则æäº¤ç©ºç™½æ–‡æœ¬å³å¯ã€‚ settings.githook_name=é’©ååç§° settings.githook_content=é’©åæ–‡æœ¬ @@ -2338,8 +2354,8 @@ settings.update_githook=æ›´æ–°é’©å设置 settings.add_webhook_desc=Gitea å°†å‘ç›®æ ‡ URL å‘é€å…·æœ‰æŒ‡å®šå†…容类型的 <code>POST</code> 请求。在 <a target="_blank" rel="noopener noreferrer" href="%s">webhooks 指å—</a> ä¸é˜…读更多内容。 settings.payload_url=ç›®æ ‡ URL settings.http_method=HTTP 方法 -settings.content_type=POST Content Type -settings.secret=密钥文本 +settings.content_type=POST 内容类型 +settings.secret=密钥 settings.slack_username=æœåŠ¡åç§° settings.slack_icon_url=å›¾æ ‡ URL settings.slack_color=颜色 @@ -2355,12 +2371,12 @@ settings.event_create_desc=åˆ›å»ºåˆ†æ”¯æˆ–æ ‡ç¾ settings.event_delete=刪除 settings.event_delete_desc=åˆ†æ”¯æˆ–æ ‡ç¾å·²åˆ 除。 settings.event_fork=派生 -settings.event_fork_desc=仓库被派生。 +settings.event_fork_desc=仓库已派生。 settings.event_wiki=百科 settings.event_wiki_desc=创建ã€é‡å‘½åã€ç¼–è¾‘æˆ–åˆ é™¤äº†ç™¾ç§‘é¡µé¢ã€‚ settings.event_statuses=çŠ¶æ€ settings.event_statuses_desc=已从 API æ›´æ–°æäº¤çжæ€ã€‚ -settings.event_release=版本å‘布 +settings.event_release=å‘布 settings.event_release_desc=å‘å¸ƒã€æ›´æ–°æˆ–åˆ é™¤ç‰ˆæœ¬æ—¶ã€‚ settings.event_push=æŽ¨é€ settings.event_force_push=å¼ºåˆ¶æŽ¨é€ @@ -2371,43 +2387,45 @@ settings.event_header_issue=å·¥å•事件 settings.event_issues=å·¥å• settings.event_issues_desc=å·¥å•已打开ã€å·²å…³é—ã€å·²é‡æ–°æ‰“开或已编辑。 settings.event_issue_assign=å·¥å•已指派 -settings.event_issue_assign_desc=å·¥å•å·²è¢«æŒ‡æ´¾æˆ–å–æ¶ˆæŒ‡æ´¾ã€‚ -settings.event_issue_label=å·²æ ‡è®°å·¥å• -settings.event_issue_label_desc=工啿 ‡ç¾è¢«æ›´æ–°æˆ–清除。 -settings.event_issue_milestone=å·¥å•è¢«æ”¶å…¥é‡Œç¨‹ç¢‘ä¸ -settings.event_issue_milestone_desc=å·¥å•è¢«æ”¶å…¥æˆ–å–æ¶ˆæ”¶å…¥é‡Œç¨‹ç¢‘ä¸ã€‚ +settings.event_issue_assign_desc=å·¥å•å·²æŒ‡æ´¾æˆ–å–æ¶ˆæŒ‡æ´¾ã€‚ +settings.event_issue_label=å·¥å•å¢žåˆ æ ‡ç¾ +settings.event_issue_label_desc=工啿 ‡ç¾å·²æ›´æ–°æˆ–清除。 +settings.event_issue_milestone=å·¥å•å·²æ”¶å…¥é‡Œç¨‹ç¢‘ä¸ +settings.event_issue_milestone_desc=å·¥å•å·²æ”¶å…¥æˆ–å–æ¶ˆæ”¶å…¥é‡Œç¨‹ç¢‘ä¸ã€‚ settings.event_issue_comment=å·¥å•评论 -settings.event_issue_comment_desc=å·¥å•评论被创建ã€ç¼–è¾‘æˆ–åˆ é™¤ +settings.event_issue_comment_desc=å·¥å•评论已创建ã€ç¼–è¾‘æˆ–åˆ é™¤ã€‚ settings.event_header_pull_request=åˆå¹¶è¯·æ±‚事件 settings.event_pull_request=åˆå¹¶è¯·æ±‚ -settings.event_pull_request_desc=åˆå¹¶è¯·æ±‚被打开ã€è¢«å…³é—ã€è¢«é‡æ–°æ‰“开或被编辑。 -settings.event_pull_request_assign=åˆå¹¶è¯·æ±‚被指派 -settings.event_pull_request_assign_desc=åˆå¹¶è¯·æ±‚è¢«æŒ‡æ´¾æˆ–å–æ¶ˆæŒ‡æ´¾ã€‚ -settings.event_pull_request_label=åˆå¹¶è¯·æ±‚è¢«è´´ä¸Šæ ‡ç¾ -settings.event_pull_request_label_desc=åˆå¹¶è¯·æ±‚çš„æ ‡ç¾è¢«æ›´æ–°æˆ–清除。 -settings.event_pull_request_milestone=åˆå¹¶è¯·æ±‚è¢«è®°å½•äºŽé‡Œç¨‹ç¢‘ä¸ -settings.event_pull_request_milestone_desc=åˆå¹¶è¯·æ±‚è¢«è®°å½•æˆ–å–æ¶ˆè®°å½•于里程碑ä¸ã€‚ -settings.event_pull_request_comment=åˆå¹¶è¯·æ±‚被评论 -settings.event_pull_request_comment_desc=åˆå¹¶è¯·æ±‚评论被创建ã€ç¼–è¾‘æˆ–åˆ é™¤ã€‚ +settings.event_pull_request_desc=åˆå¹¶è¯·æ±‚已打开ã€å…³é—ã€é‡æ–°æ‰“开或编辑。 +settings.event_pull_request_assign=åˆå¹¶è¯·æ±‚已指派 +settings.event_pull_request_assign_desc=åˆå¹¶è¯·æ±‚å·²æŒ‡æ´¾æˆ–å–æ¶ˆæŒ‡æ´¾ã€‚ +settings.event_pull_request_label=åˆå¹¶è¯·æ±‚å¢žåˆ æ ‡ç¾ +settings.event_pull_request_label_desc=åˆå¹¶è¯·æ±‚çš„æ ‡ç¾å·²æ›´æ–°æˆ–清除。 +settings.event_pull_request_milestone=åˆå¹¶è¯·æ±‚å·²è®°å½•äºŽé‡Œç¨‹ç¢‘ä¸ +settings.event_pull_request_milestone_desc=åˆå¹¶è¯·æ±‚å·²è®°å½•æˆ–å–æ¶ˆè®°å½•于里程碑ä¸ã€‚ +settings.event_pull_request_comment=åˆå¹¶è¯·æ±‚已评论 +settings.event_pull_request_comment_desc=åˆå¹¶è¯·æ±‚评论已创建ã€ç¼–è¾‘æˆ–åˆ é™¤ã€‚ settings.event_pull_request_review=å·²å®¡æ ¸çš„åˆå¹¶è¯·æ±‚ -settings.event_pull_request_review_desc=åˆå¹¶è¯·æ±‚è¢«æ‰¹å‡†ã€æ‹’ç»æˆ–æå‡ºå®¡æŸ¥æ„è§ -settings.event_pull_request_sync=åˆå¹¶è¯·æ±‚è¢«åŒæ¥ -settings.event_pull_request_sync_desc=åˆå¹¶è¯·æ±‚è¢«åŒæ¥ã€‚ +settings.event_pull_request_review_desc=åˆå¹¶è¯·æ±‚å·²æ‰¹å‡†ã€æ‹’ç»æˆ–æå‡ºå®¡æŸ¥æ„è§ã€‚ +settings.event_pull_request_sync=åˆå¹¶è¯·æ±‚å·²åŒæ¥ +settings.event_pull_request_sync_desc=åˆå¹¶è¯·æ±‚å·²åŒæ¥ã€‚ settings.event_pull_request_review_request=å‘èµ·åˆå¹¶è¯·æ±‚评审 settings.event_pull_request_review_request_desc=åˆå¹¶è¯·æ±‚è¯„å®¡å·²è¯·æ±‚æˆ–å·²å–æ¶ˆ settings.event_pull_request_approvals=åˆå¹¶è¯·æ±‚批准 settings.event_pull_request_merge=åˆå¹¶è¯·æ±‚åˆå¹¶ settings.event_header_workflow=工作æµç¨‹äº‹ä»¶ +settings.event_workflow_run=工作æµè¿è¡Œ +settings.event_workflow_run_desc=Gitea 工作æµé˜Ÿåˆ—ä¸ã€ç‰å¾…ä¸ã€æ£åœ¨è¿›è¡Œæˆ–已完æˆçš„任务。 settings.event_workflow_job=工作æµä»»åŠ¡ -settings.event_workflow_job_desc=Gitea Actions 工作æµé˜Ÿåˆ—ä¸ã€ç‰å¾…ä¸ã€æ£åœ¨è¿›è¡Œæˆ–已完æˆä»»åŠ¡ã€‚ +settings.event_workflow_job_desc=Gitea 工作æµé˜Ÿåˆ—ä¸ã€ç‰å¾…ä¸ã€æ£åœ¨è¿›è¡Œæˆ–已完æˆçš„任务。 settings.event_package=软件包 -settings.event_package_desc=软件包已在仓库ä¸è¢«åˆ›å»ºæˆ–åˆ é™¤ã€‚ +settings.event_package_desc=软件包在仓库ä¸å·²åˆ›å»ºæˆ–åˆ é™¤ã€‚ settings.branch_filter=分支过滤 -settings.branch_filter_desc=推é€ã€åˆ›å»ºï¼Œåˆ 除分支事件的分支白åå•,使用 glob 模å¼åŒ¹é…指定。若为空或 <code>*</code>ï¼Œåˆ™å°†æŠ¥å‘Šæ‰€æœ‰åˆ†æ”¯çš„äº‹ä»¶ã€‚è¯æ³•æ–‡æ¡£è§ <a href="%[1]s">%[2]s</a>。示例:<code>master</code>,<code>{master,release*}</code>。 +settings.branch_filter_desc=推é€ã€åˆ›å»ºï¼Œåˆ 除分支事件的分支白åå•,使用 glob 表达å¼åŒ¹é…指定。若为空或 <code>*</code>ï¼Œåˆ™ä¼šæŠ¥å‘Šæ‰€æœ‰åˆ†æ”¯çš„äº‹ä»¶ã€‚è¯æ³•æ–‡æ¡£è§ <a href="%[1]s">%[2]s</a>。示例:<code>master</code>,<code>{master,release*}</code>。 settings.authorization_header=æŽˆæƒæ ‡å¤´ settings.authorization_header_desc=当å˜åœ¨æ—¶å°†è¢«ä½œä¸ºæŽˆæƒæ ‡å¤´åŒ…å«åœ¨å†…。例如: %s。 settings.active=激活 -settings.active_helper=触å‘事件的信æ¯å°†å‘é€åˆ°æ¤ webhook 网å€ã€‚ +settings.active_helper=触å‘事件的信æ¯å°†å‘é€åˆ°æ¤ Web é’©å URL。 settings.add_hook_success=Web é’©åæ·»åŠ æˆåŠŸï¼ settings.update_webhook=æ›´æ–° Web é’©å settings.update_hook_success=Web é’©åæ›´æ–°æˆåŠŸï¼ @@ -2417,7 +2435,7 @@ settings.hook_type=é’©å类型 settings.slack_token=令牌 settings.slack_domain=域å settings.slack_channel=é¢‘é“ -settings.add_web_hook_desc=å°† <a target="_blank" rel="noreferrer" href="%s">%s</a>集æˆåˆ°æ‚¨çš„代ç 库。 +settings.add_web_hook_desc=å°† <a target="_blank" rel="noreferrer" href="%s">%s</a> 集æˆåˆ°æ‚¨çš„仓库。 settings.web_hook_name_gitea=Gitea settings.web_hook_name_gogs=Gogs settings.web_hook_name_slack=Slack @@ -2444,7 +2462,7 @@ settings.title=æ ‡é¢˜ settings.deploy_key_content=密钥文本 settings.key_been_used=具有相åŒå†…容的部署密钥已在使用ä¸ã€‚ settings.key_name_used=使用相åŒå称的部署密钥已ç»å˜åœ¨ï¼ -settings.add_key_success=部署密钥 %s æ·»åŠ æˆåŠŸã€‚ +settings.add_key_success=部署密钥「%sã€æ·»åŠ æˆåŠŸã€‚ settings.deploy_key_deletion=åˆ é™¤éƒ¨ç½²å¯†é’¥ settings.deploy_key_deletion_desc=åˆ é™¤éƒ¨ç½²å¯†é’¥å°†å–æ¶ˆæ¤å¯†é’¥å¯¹æ¤ä»“库的访问æƒé™ã€‚ç»§ç»ï¼Ÿ settings.deploy_key_deletion_success=éƒ¨ç½²å¯†é’¥å·²åˆ é™¤ã€‚ @@ -2453,11 +2471,11 @@ settings.protected_branch=åˆ†æ”¯ä¿æŠ¤ settings.protected_branch.save_rule=ä¿å˜è§„则 settings.protected_branch.delete_rule=åˆ é™¤è§„åˆ™ settings.protected_branch_can_push=是å¦å…许推é€ï¼Ÿ -settings.protected_branch_can_push_yes=ä½ å¯ä»¥æŽ¨ -settings.protected_branch_can_push_no=ä½ ä¸èƒ½æŽ¨é€ -settings.branch_protection=分支 '<b>%s</b>' çš„ä¿æŠ¤è§„åˆ™ +settings.protected_branch_can_push_yes=您å¯ä»¥æŽ¨é€ +settings.protected_branch_can_push_no=您ä¸èƒ½æŽ¨é€ +settings.branch_protection=分支「<b>%s</b>ã€çš„ä¿æŠ¤è§„åˆ™ settings.protect_this_branch=å¯ç”¨åˆ†æ”¯ä¿æŠ¤ -settings.protect_this_branch_desc=阻æ¢åˆ 除并é™åˆ¶Git推é€å’Œåˆå¹¶åˆ°åˆ†æ”¯ã€‚ +settings.protect_this_branch_desc=阻æ¢åˆ 除并é™åˆ¶ Git 推é€å’Œåˆå¹¶åˆ°åˆ†æ”¯ã€‚ settings.protect_disable_push=ç¦ç”¨æŽ¨é€ settings.protect_disable_push_desc=æ¤åˆ†æ”¯ä¸å…许推é€ã€‚ settings.protect_disable_force_push=ç¦ç”¨å¼ºåˆ¶æŽ¨é€ @@ -2483,13 +2501,13 @@ settings.protect_merge_whitelist_committers_desc=ä»…å…许白åå•用户或团é settings.protect_merge_whitelist_users=åˆå¹¶ç™½åå•用户: settings.protect_merge_whitelist_teams=åˆå¹¶ç™½åå•团队: settings.protect_check_status_contexts=å¯ç”¨çŠ¶æ€æ£€æŸ¥ -settings.protect_status_check_patterns=çŠ¶æ€æ£€æŸ¥æ¨¡å¼ï¼š -settings.protect_status_check_patterns_desc=输入模å¼ï¼ŒæŒ‡å®šå“ªäº›çŠ¶æ€æ£€æŸ¥å¿…须通过,æ‰èƒ½å°†åˆ†æ”¯åˆå¹¶åˆ°ç¬¦åˆæ¤è§„则的分支ä¸åŽ»ã€‚æ¯ä¸€è¡ŒæŒ‡å®šä¸€ä¸ªæ¨¡å¼ï¼Œæ¨¡å¼ä¸èƒ½ä¸ºç©ºã€‚ +settings.protect_status_check_patterns=çŠ¶æ€æ£€æŸ¥è¡¨è¾¾å¼ï¼š +settings.protect_status_check_patterns_desc=输入表达å¼ä»¥æŒ‡å®šåœ¨åˆ†æ”¯åˆå¹¶åˆ°åŒ¹é…æ¤è§„则的分支之å‰å¿…é¡»é€šè¿‡å“ªäº›çŠ¶æ€æ£€æŸ¥ã€‚æ¯ä¸€è¡ŒæŒ‡å®šä¸€ä¸ªè¡¨è¾¾å¼ä¸”表达å¼ä¸èƒ½ä¸ºç©ºã€‚ settings.protect_check_status_contexts_desc=è¦æ±‚çŠ¶æ€æ£€æŸ¥é€šè¿‡æ‰èƒ½åˆå¹¶ã€‚如果å¯ç”¨ï¼Œæäº¤å¿…须先推é€åˆ°å¦ä¸€ä¸ªåˆ†æ”¯ï¼Œç„¶åŽå†åˆå¹¶æˆ–推é€åˆ°åŒ¹é…è¿™äº›ä¿æŠ¤è§„åˆ™çš„åˆ†æ”¯ã€‚å¦‚æžœæ²¡æœ‰é€‰æ‹©å…·ä½“çš„çŠ¶æ€æ£€æŸ¥ä¸Šä¸‹æ–‡ï¼Œåˆ™æ‰€æœ‰çš„çŠ¶æ€æ£€æŸ¥éƒ½é€šè¿‡æ‰èƒ½åˆå¹¶ã€‚ settings.protect_check_status_contexts_list=æ¤ä»“åº“ä¸Šå‘¨è¿›è¡Œè¿‡çš„çŠ¶æ€æ£€æŸ¥ settings.protect_status_check_matched=åŒ¹é… -settings.protect_invalid_status_check_pattern=æ— æ•ˆçš„çŠ¶æ€æ£€æŸ¥è§„则:“%sâ€ã€‚ -settings.protect_no_valid_status_check_patterns=æ²¡æœ‰æœ‰æ•ˆçš„çŠ¶æ€æ£€æŸ¥è§„则。 +settings.protect_invalid_status_check_pattern=æ— æ•ˆçš„çŠ¶æ€æ£€æŸ¥è¡¨è¾¾å¼ï¼šã€Œ%sã€ã€‚ +settings.protect_no_valid_status_check_patterns=æ²¡æœ‰æœ‰æ•ˆçš„çŠ¶æ€æ£€æŸ¥è¡¨è¾¾å¼ã€‚ settings.protect_required_approvals=所需的批准: settings.protect_required_approvals_desc=åªå…许åˆå¹¶æœ‰è¶³å¤Ÿå®¡æ ¸çš„åˆå¹¶è¯·æ±‚ã€‚è¦æ±‚çš„å®¡æ ¸å¿…é¡»æ¥è‡ªç™½å啿ˆ–者有æƒé™çš„用户或团队。 settings.protect_approvals_whitelist_enabled=仅列入白åå•的用户或团队æ‰å¯æ‰¹å‡† @@ -2502,18 +2520,18 @@ settings.ignore_stale_approvals=忽略过期批准 settings.ignore_stale_approvals_desc=对旧æäº¤ï¼ˆè¿‡æœŸå®¡æ ¸ï¼‰çš„æ‰¹å‡†å°†ä¸è®¡å…¥ PR çš„æ‰¹å‡†æ•°ã€‚å¦‚æžœè¿‡æœŸå®¡æŸ¥å·²è¢«é©³å›žï¼Œåˆ™ä¸Žæ¤æ— 关。 settings.require_signed_commits=需è¦ç¾åæäº¤ settings.require_signed_commits_desc=æ‹’ç»æŽ¨é€æœªç¾åæˆ–æ— æ³•éªŒè¯çš„æäº¤åˆ°åˆ†æ”¯ -settings.protect_branch_name_pattern=å—ä¿æŠ¤çš„åˆ†æ”¯åç§°æ¨¡å¼ -settings.protect_branch_name_pattern_desc=åˆ†æ”¯ä¿æŠ¤çš„å称匹é…è§„åˆ™ã€‚è¯æ³•请å‚阅 <a href="%s">文档</a> 。如:main, release/** -settings.protect_patterns=规则 -settings.protect_protected_file_patterns=å—ä¿æŠ¤çš„æ–‡ä»¶æ¨¡å¼(ä½¿ç”¨åˆ†å· ';' 分隔): -settings.protect_protected_file_patterns_desc=å³ä½¿ç”¨æˆ·æœ‰æƒæ·»åŠ ã€ç¼–è¾‘æˆ–åˆ é™¤æ¤åˆ†æ”¯ä¸çš„æ–‡ä»¶ï¼Œä¹Ÿä¸å…许直接更改å—ä¿æŠ¤çš„æ–‡ä»¶ã€‚ å¯ä»¥ä½¿ç”¨åˆ†å· (';') 分隔多个模å¼ã€‚ è§<a href='%[1]s'>%[2]s</a>文档了解模å¼è¯æ³•。例如: <code>.drone.yml</code>, <code>/docs/**/*.txt</code> -settings.protect_unprotected_file_patterns=ä¸å—ä¿æŠ¤çš„æ–‡ä»¶æ¨¡å¼(ä½¿ç”¨åˆ†å· ';' 分隔): -settings.protect_unprotected_file_patterns_desc=如果用户有写æƒé™ï¼Œåˆ™å…许直接更改的ä¸å—ä¿æŠ¤çš„æ–‡ä»¶ï¼Œä»¥ç»•è¿‡æŽ¨é€é™åˆ¶ã€‚å¯ä»¥ä½¿ç”¨åˆ†å·åˆ†éš”å¤šä¸ªæ¨¡å¼ (';')。 è§ <a href='%[1]s'>%[2]s</a> 文档了解模å¼è¯æ³•。例如: <code>.drone.yml</code>, <code>/docs/**/*.txt</code> +settings.protect_branch_name_pattern=å—ä¿æŠ¤çš„åˆ†æ”¯åç§°è¡¨è¾¾å¼ +settings.protect_branch_name_pattern_desc=åˆ†æ”¯ä¿æŠ¤çš„å称匹é…表达å¼ã€‚è¯æ³•请å‚阅 <a href="%s">文档</a> 。如:main, release/** +settings.protect_patterns=è¡¨è¾¾å¼ +settings.protect_protected_file_patterns=å—ä¿æŠ¤çš„æ–‡ä»¶è¡¨è¾¾å¼ï¼ˆä½¿ç”¨åˆ†å·ã€Œ;ã€åˆ†éš”): +settings.protect_protected_file_patterns_desc=å³ä½¿ç”¨æˆ·æœ‰æƒæ·»åŠ ã€ç¼–è¾‘æˆ–åˆ é™¤æ¤åˆ†æ”¯ä¸çš„æ–‡ä»¶ï¼Œä¹Ÿä¸å…许直接更改å—ä¿æŠ¤çš„æ–‡ä»¶ã€‚ å¯ä»¥ä½¿ç”¨åˆ†å·ã€Œ;ã€åˆ†éš”多个表达å¼ã€‚ è§<a href='%[1]s'>%[2]s</a>文档了解表达å¼è¯æ³•。例如: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>。 +settings.protect_unprotected_file_patterns=ä¸å—ä¿æŠ¤çš„æ–‡ä»¶è¡¨è¾¾å¼ï¼ˆä½¿ç”¨åˆ†å·ã€Œ;ã€åˆ†éš”): +settings.protect_unprotected_file_patterns_desc=如果用户有写æƒé™ï¼Œåˆ™å…许直接更改的ä¸å—ä¿æŠ¤çš„æ–‡ä»¶ï¼Œä»¥ç»•è¿‡æŽ¨é€é™åˆ¶ã€‚å¯ä»¥ä½¿ç”¨åˆ†å·åˆ†éš”多个表达å¼ã€Œ;ã€ã€‚ è§ <a href='%[1]s'>%[2]s</a> 文档了解表达å¼è¯æ³•。例如: <code>.drone.yml</code>, <code>/docs/**/*.txt</code>。 settings.add_protected_branch=å¯ç”¨ä¿æŠ¤ settings.delete_protected_branch=ç¦ç”¨ä¿æŠ¤ -settings.update_protect_branch_success=åˆ†æ”¯ä¿æŠ¤è§„åˆ™ %s æ›´æ–°æˆåŠŸã€‚ -settings.remove_protected_branch_success=ç§»é™¤åˆ†æ”¯ä¿æŠ¤è§„åˆ™"%s"æˆåŠŸã€‚ -settings.remove_protected_branch_failed=ç§»é™¤åˆ†æ”¯ä¿æŠ¤è§„åˆ™"%s"失败。 +settings.update_protect_branch_success=åˆ†æ”¯ä¿æŠ¤è§„åˆ™ã€Œ%sã€æ›´æ–°æˆåŠŸã€‚ +settings.remove_protected_branch_success=åˆ†æ”¯ä¿æŠ¤è§„åˆ™ã€Œ%sã€ç§»é™¤æˆåŠŸã€‚ +settings.remove_protected_branch_failed=åˆ†æ”¯ä¿æŠ¤è§„åˆ™ã€Œ%sã€ç§»é™¤å¤±è´¥ã€‚ settings.protected_branch_deletion=åˆ é™¤åˆ†æ”¯ä¿æŠ¤ settings.protected_branch_deletion_desc=ç¦ç”¨åˆ†æ”¯ä¿æŠ¤å…许具有写入æƒé™çš„ç”¨æˆ·æŽ¨é€æäº¤åˆ°æ¤åˆ†æ”¯ã€‚ç»§ç»ï¼Ÿ settings.block_rejected_reviews=æ‹’ç»å®¡æ ¸é˜»æ¢äº†æ¤åˆå¹¶ @@ -2534,15 +2552,15 @@ settings.protected_branch_required_rule_name=必须填写规则åç§° settings.protected_branch_duplicate_rule_name=规则åç§°å·²å˜åœ¨ settings.protected_branch_required_approvals_min=所需的审批数ä¸èƒ½ä¸ºè´Ÿæ•°ã€‚ settings.tags=æ ‡ç¾ -settings.tags.protection=Gitæ ‡ç¾ä¿æŠ¤ -settings.tags.protection.pattern=Gitæ ‡ç¾æ¨¡å¼ +settings.tags.protection=æ ‡ç¾ä¿æŠ¤ +settings.tags.protection.pattern=æ ‡ç¾è¡¨è¾¾å¼ settings.tags.protection.allowed=å…许列表 settings.tags.protection.allowed.users=å…è®¸çš„è´¦å· settings.tags.protection.allowed.teams=å…许的团队 settings.tags.protection.allowed.noone=æ— -settings.tags.protection.create=ä¿æŠ¤Gitæ ‡ç¾ -settings.tags.protection.none=没有å—ä¿æŠ¤çš„Gitæ ‡ç¾ -settings.tags.protection.pattern.description=ä½ å¯ä»¥ä½¿ç”¨å•个å称或 glob 模å¼åŒ¹é…或æ£åˆ™è¡¨è¾¾å¼æ¥åŒ¹é…å¤šä¸ªæ ‡ç¾ã€‚了解详情请访问 <a target="_blank" rel="noopener" href="%s">ä¿æŠ¤Gitæ ‡ç¾æŒ‡å—</a>。 +settings.tags.protection.create=ä¿æŠ¤æ ‡ç¾ +settings.tags.protection.none=没有å—ä¿æŠ¤çš„æ ‡ç¾ã€‚ +settings.tags.protection.pattern.description=您å¯ä»¥ä½¿ç”¨å•个å称或 glob 表达å¼åŒ¹é…或æ£åˆ™è¡¨è¾¾å¼æ¥åŒ¹é…å¤šä¸ªæ ‡ç¾ã€‚了解详情请访问 <a target="_blank" rel="noopener" href="%s">ä¿æŠ¤æ ‡ç¾æŒ‡å—</a>。 settings.bot_token=Bot 令牌 settings.chat_id=èŠå¤© ID settings.thread_id=线程 ID @@ -2563,13 +2581,13 @@ settings.visibility.error=试图更改仓库å¯è§æ€§æ—¶å‡ºé”™ã€‚ settings.visibility.fork_error=æ— æ³•æ›´æ”¹æ´¾ç”Ÿä»“åº“çš„å¯è§æ€§ã€‚ settings.archive.button=归档仓库 settings.archive.header=å½’æ¡£æ¤ä»“库 -settings.archive.text=归档仓库将使其完全åªè¯»ã€‚它将在首页éšè—ã€‚æ²¡æœ‰äººï¼ˆç”šè‡³ä½ ï¼ï¼‰èƒ½å¤Ÿè¿›è¡Œæ–°çš„æäº¤ï¼Œæˆ–打开工å•åŠåˆå¹¶è¯·æ±‚。 +settings.archive.text=归档仓库将使其完全åªè¯»ã€‚它将在首页éšè—。没有人(包括您)能够进行新的æäº¤ï¼Œæˆ–打开工å•åŠåˆå¹¶è¯·æ±‚。 settings.archive.success=仓库已æˆåŠŸå½’æ¡£ã€‚ settings.archive.error=仓库在归档时出现异常。请通过日志获å–详细信æ¯ã€‚ settings.archive.error_ismirror=请ä¸è¦å¯¹é•œåƒä»“åº“å½’æ¡£ï¼Œè°¢è°¢ï¼ settings.archive.branchsettings_unavailable=å·²å½’æ¡£ä»“åº“æ— æ³•è¿›è¡Œåˆ†æ”¯è®¾ç½®ã€‚ -settings.archive.tagsettings_unavailable=已归档仓库的Gitæ ‡ç¾è®¾ç½®ä¸å¯ç”¨ã€‚ -settings.archive.mirrors_unavailable=如果仓库已被归档,镜åƒå°†ä¸å¯ç”¨ã€‚ +settings.archive.tagsettings_unavailable=å·²å½’æ¡£ä»“åº“çš„æ ‡ç¾è®¾ç½®ä¸å¯ç”¨ã€‚ +settings.archive.mirrors_unavailable=如果仓库已归档,镜åƒå°†ä¸å¯ç”¨ã€‚ settings.unarchive.button=撤销仓库归档 settings.unarchive.header=撤销æ¤ä»“库归档 settings.unarchive.text=撤销归档将æ¢å¤ä»“库接收æäº¤ã€æŽ¨é€ï¼Œä»¥åŠæ–°å·¥å•å’Œåˆå¹¶è¯·æ±‚的能力。 @@ -2623,7 +2641,7 @@ diff.whitespace_show_everything=显示所有更改 diff.whitespace_ignore_all_whitespace=æ¯”è¾ƒè¡Œæ—¶å¿½ç•¥ç©ºç™½ç¬¦å· diff.whitespace_ignore_amount_changes=å¿½ç•¥ç©ºç™½ç¬¦å·æ•°é‡çš„å˜åŒ– diff.whitespace_ignore_at_eol=忽略行末空白符å·çš„æ›´æ”¹ -diff.stats_desc=共有 <strong> %d 个文件被更改</strong>,包括 <strong>%d 次æ’å…¥</strong> å’Œ <strong>%d æ¬¡åˆ é™¤</strong> +diff.stats_desc=å…±<strong>修改 %d 个文件</strong>ï¼ŒåŒ…å« <strong>%d 行新增</strong>å’Œ <strong>%d è¡Œåˆ é™¤</strong> diff.stats_desc_file=å˜æ›´ %d 行:新增 %d è¡Œï¼Œåˆ é™¤ %d 行 diff.bin=二进制 diff.bin_not_shown=二进制文件未显示。 @@ -2667,22 +2685,22 @@ diff.submodule_deleted=忍¡å— %[1]s 已从 %[2]s ä¸åˆ 除 diff.submodule_updated=忍¡å— %[1]s 已更新:%[2]s releases.desc=跟踪项目版本和下载。 -release.releases=版本å‘布 +release.releases=å‘布 release.detail=å‘布详情 -release.tags=Gitæ ‡ç¾ +release.tags=æ ‡ç¾ release.new_release=å‘布新版 release.draft=è‰ç¨¿ -release.prerelease=预å‘行 +release.prerelease=预å‘布 release.stable=稳定 -release.latest=最新版本 +release.latest=最新 release.compare=比较 release.edit=编辑 release.ahead.commits=<strong>%d</strong> 次æäº¤ -release.ahead.target=在æ¤ç‰ˆæœ¬å‘布åŽè¢«åŠ å…¥åˆ° %s +release.ahead.target=在æ¤ç‰ˆæœ¬å‘布åŽå·²åŠ å…¥åˆ° %s tag.ahead.target=è‡ªæ¤æ ‡ç¾åˆ° %s release.source_code=æºä»£ç -release.new_subheader=版本å‘布组织项目的版本。 -release.edit_subheader=版本å‘布组织项目的版本。 +release.new_subheader=å‘布组织项目的版本。 +release.edit_subheader=å‘布组织项目的版本。 release.tag_name=æ ‡ç¾åç§° release.target=ç›®æ ‡åˆ†æ”¯ release.tag_helper=选择一个å˜åœ¨çš„æ ‡ç¾æˆ–è€…åˆ›å»ºæ–°æ ‡ç¾ã€‚ @@ -2691,74 +2709,74 @@ release.tag_helper_existing=çŽ°æœ‰æ ‡ç¾ã€‚ release.title=å‘å¸ƒæ ‡é¢˜ release.title_empty=æ ‡é¢˜ä¸èƒ½ä¸ºç©ºã€‚ release.message=æè¿°è¿™ä¸ªç‰ˆæœ¬ -release.prerelease_desc=æ ‡è®°ä¸ºé¢„å‘行 +release.prerelease_desc=æ ‡è®°ä¸ºé¢„å‘布 release.prerelease_helper=æ ‡è®°æ¤ç‰ˆæœ¬ä¸é€‚åˆç”Ÿäº§ä½¿ç”¨ã€‚ release.cancel=å–æ¶ˆ release.publish=å‘布版本 release.save_draft=ä¿å˜è‰ç¨¿ release.edit_release=ä¿å˜æ¤æ¬¡å‘布 release.delete_release=åˆ é™¤å‘布 -release.delete_tag=åˆ é™¤ Gitæ ‡ç¾ +release.delete_tag=åˆ é™¤æ ‡ç¾ release.deletion=åˆ é™¤å‘布 -release.deletion_desc=åˆ é™¤ç‰ˆæœ¬å‘布åªä¼šä»Ž Gitea ä¸ç§»é™¤ã€‚è¿™ä¸ä¼šå½±å“ Git çš„æ ‡ç¾ä»¥åŠæ‚¨ä»“库的内容和历å²ã€‚是å¦ç»§ç»ï¼Ÿ -release.deletion_success=Releaseå·²è¢«åˆ é™¤ã€‚ -release.deletion_tag_desc=将从仓库ä¸åˆ é™¤æ¤ Gitæ ‡ç¾ã€‚仓库内容和历å²è®°å½•ä¿æŒä¸å˜ã€‚ç»§ç»å—? -release.deletion_tag_success=该 Gitæ ‡ç¾ å·²ç»è¢«åˆ 除 -release.tag_name_already_exist=ä½¿ç”¨æ¤æ ‡ç¾åç§°çš„å‘布版本已ç»å˜åœ¨ã€‚ +release.deletion_desc=åˆ é™¤å‘布åªä¼šä»Ž Gitea ä¸ç§»é™¤å‘布。这ä¸ä¼šå½±å“ Git çš„æ ‡ç¾ä»¥åŠæ‚¨ä»“库的内容和历å²ã€‚是å¦ç»§ç»ï¼Ÿ +release.deletion_success=该å‘å¸ƒå·²åˆ é™¤ã€‚ +release.deletion_tag_desc=将从仓库ä¸åˆ é™¤æ¤æ ‡ç¾ã€‚仓库内容和历å²è®°å½•ä¿æŒä¸å˜ã€‚ç»§ç»å—? +release.deletion_tag_success=è¯¥æ ‡ç¾å·²åˆ 除。 +release.tag_name_already_exist=ä½¿ç”¨æ¤æ ‡ç¾åç§°çš„å‘布已ç»å˜åœ¨ã€‚ release.tag_name_invalid=æ ‡ç¾åç§°æ— æ•ˆã€‚ -release.tag_name_protected=Gitæ ‡ç¾åç§°å·²å—ä¿æŠ¤ã€‚ -release.tag_already_exist=æ¤ Gitæ ‡ç¾ åç§°å·²å˜åœ¨ +release.tag_name_protected=æ ‡ç¾åå·²å—ä¿æŠ¤ã€‚ +release.tag_already_exist=æ¤æ ‡ç¾åå·²å˜åœ¨ã€‚ release.downloads=下载附件 release.download_count=下载:%s release.add_tag_msg=使用å‘å¸ƒçš„æ ‡é¢˜å’Œå†…å®¹ä½œä¸ºæ ‡ç¾æ¶ˆæ¯ã€‚ release.add_tag=ä»…åˆ›å»ºæ ‡ç¾ -release.releases_for=%s 的版本å‘布 +release.releases_for=%s çš„å‘布 release.tags_for=%s çš„æ ‡ç¾ branch.name=分支åç§° -branch.already_exists=å为 %s 的分支已å˜åœ¨ã€‚ +branch.already_exists=å为「%sã€çš„分支已å˜åœ¨ã€‚ branch.delete_head=刪除 -branch.delete=åˆ é™¤åˆ†æ”¯ %s +branch.delete=åˆ é™¤åˆ†æ”¯ã€Œ%s〠branch.delete_html=åˆ é™¤åˆ†æ”¯ branch.delete_desc=åˆ é™¤åˆ†æ”¯æ˜¯æ°¸ä¹…çš„ã€‚è™½ç„¶å·²åˆ é™¤çš„åˆ†æ”¯åœ¨å®žé™…è¢«åˆ é™¤å‰æœ‰å¯èƒ½ä¼šçŸæ—¶é—´å˜åœ¨ï¼Œä½†è¿™åœ¨å¤§å¤šæ•°æƒ…å†µä¸‹æ— æ³•æ’¤é”€ã€‚æ˜¯å¦ç»§ç»ï¼Ÿ -branch.deletion_success=分支 %s å·²è¢«åˆ é™¤ã€‚ -branch.deletion_failed=åˆ é™¤åˆ†æ”¯ %s 失败。 -branch.delete_branch_has_new_commits=å› ä¸ºåˆå¹¶ä¹‹åŽæœ‰æ–°çš„æäº¤ï¼Œåˆ†æ”¯ %s æ— æ³•è¢«åˆ é™¤ã€‚ +branch.deletion_success=分支「%sã€åˆ 除æˆåŠŸã€‚ +branch.deletion_failed=分支「%sã€åˆ 除失败。 +branch.delete_branch_has_new_commits=å› ä¸ºåˆå¹¶ä¹‹åŽæœ‰æ–°çš„æäº¤ï¼Œåˆ†æ”¯ã€Œ%sã€æ— æ³•åˆ é™¤ã€‚ branch.create_branch=创建分支 %s -branch.create_from=从 %s -branch.create_success=分支 '%s' 已创建。 -branch.branch_already_exists=æ¤ä»“库已å˜åœ¨å为 %s 的分支。 -branch.branch_name_conflict=分支åç§°"%s"与已å˜åœ¨çš„分支"%s"冲çªã€‚ -branch.tag_collision=分支 %s ä¸èƒ½è¢«åˆ›å»ºå› 为åŒåçš„æ ‡ç¾å·²ç»å˜åœ¨ã€‚ +branch.create_from=从「%s〠+branch.create_success=分支「%sã€å·²åˆ›å»ºã€‚ +branch.branch_already_exists=æ¤ä»“库已å˜åœ¨å为「%sã€çš„分支。 +branch.branch_name_conflict=分支å称「%sã€ä¸Žå·²å˜åœ¨çš„分支「%sã€å†²çªã€‚ +branch.tag_collision=分支「%sã€æ— æ³•åˆ›å»ºï¼Œå› ä¸ºåŒåçš„æ ‡ç¾å·²ç»å˜åœ¨ã€‚ branch.deleted_by=åˆ é™¤äººï¼š%s -branch.restore_success=分支 "%s"已还原。 -branch.restore_failed=还原分支 "%s"失败。 -branch.protected_deletion_failed=ä¸èƒ½åˆ 除å—ä¿æŠ¤çš„åˆ†æ”¯ "%s"。 -branch.default_deletion_failed=ä¸èƒ½åˆ 除默认分支"%s"。 -branch.default_branch_not_exist=默认分支 %s ä¸å˜åœ¨ã€‚ -branch.restore=`还原分支 "%s"` -branch.download=`下载分支 "%s"` -branch.rename=`é‡å‘½å分支 "%s"` +branch.restore_success=分支「%sã€å·²è¿˜åŽŸã€‚ +branch.restore_failed=分支「%sã€è¿˜åŽŸå¤±è´¥ã€‚ +branch.protected_deletion_failed=ä¸èƒ½åˆ 除å—ä¿æŠ¤çš„åˆ†æ”¯ã€Œ%sã€ã€‚ +branch.default_deletion_failed=ä¸èƒ½åˆ 除默认分支「%sã€ã€‚ +branch.default_branch_not_exist=默认分支「%sã€ä¸å˜åœ¨ã€‚ +branch.restore=还原分支「%s〠+branch.download=下载分支「%s〠+branch.rename=é‡å‘½å分支「%s〠branch.included_desc=æ¤åˆ†æ”¯æ˜¯é»˜è®¤åˆ†æ”¯çš„一部分 branch.included=å·²åŒ…å« branch.create_new_branch=从下列分支创建分支: branch.confirm_create_branch=创建分支 branch.warning_rename_default_branch=您æ£åœ¨é‡å‘½å默认分支。 -branch.rename_branch_to=é‡å‘½å %s 为: +branch.rename_branch_to=é‡å‘½å「%sã€ä¸ºï¼š branch.confirm_rename_branch=é‡å‘½å分支 branch.create_branch_operation=创建分支 branch.new_branch=创建新分支 -branch.new_branch_from=基于"%s"创建新分支 -branch.renamed=分支 %s 被é‡å‘½å为 %s。 +branch.new_branch_from=基于「%sã€åˆ›å»ºæ–°åˆ†æ”¯ +branch.renamed=分支 %s å·²é‡å‘½å为 %s。 branch.rename_default_or_protected_branch_error=åªæœ‰ç®¡ç†å‘˜èƒ½é‡å‘½å默认分支和å—ä¿æŠ¤çš„åˆ†æ”¯ã€‚ branch.rename_protected_branch_failed=æ¤åˆ†æ”¯å—到 glob è¯æ³•è§„åˆ™çš„ä¿æŠ¤ã€‚ tag.create_tag=åˆ›å»ºæ ‡ç¾ %s tag.create_tag_operation=åˆ›å»ºæ ‡ç¾ tag.confirm_create_tag=åˆ›å»ºæ ‡ç¾ -tag.create_tag_from=基于"%s"åˆ›å»ºæ–°æ ‡ç¾ +tag.create_tag_from=基于「%sã€åˆ›å»ºæ–°æ ‡ç¾ -tag.create_success=æ ‡ç¾"%s"å·²å˜åœ¨ +tag.create_success=æ ‡ç¾ã€Œ%sã€å·²å˜åœ¨ã€‚ topic.manage_topics=管ç†ä¸»é¢˜ topic.done=ä¿å˜ @@ -2776,7 +2794,7 @@ error.broken_git_hook=æ¤ä»“库的 Git é’©å似乎已æŸå。 请按照 <a tar [graphs] component_loading=æ£åœ¨åŠ è½½ %s... component_loading_failed=æ— æ³•åŠ è½½ %s -component_loading_info=è¿™å¯èƒ½éœ€è¦ä¸€ç‚¹â€¦ +component_loading_info=è¿™å¯èƒ½éœ€è¦ä¸€ç‚¹æ—¶é—´â€¦ component_failed_to_load=æ„外的错误å‘生了。 code_frequency.what=代ç 频率 contributors.what=贡献 @@ -2805,14 +2823,15 @@ team_permission_desc=æƒé™ team_unit_desc=å…许访问仓库å•å…ƒ team_unit_disabled=(å·²ç¦ç”¨) -form.name_reserved=组织åç§° '%s' 是被ä¿ç•™çš„。 -form.name_pattern_not_allowed=仓库åç§°ä¸ä¸å…许使用 "%s"。 +form.name_been_taken=组织å称「%sã€å·²ç»è¢«å 用。 +form.name_reserved=组织å称「%sã€æ˜¯ä¿ç•™çš„。 +form.name_pattern_not_allowed=组织åä¸ä¸å…许使用「%sã€æ ¼å¼ã€‚ form.create_org_not_allowed=æ¤è´¦å·ç¦æ¢åˆ›å»ºç»„织 settings=组织设置 settings.options=组织 settings.full_name=组织全å -settings.email=è”系电å邮件 +settings.email=è”系邮箱 settings.website=网站 settings.location=所在地区 settings.permission=æƒé™ @@ -2826,15 +2845,28 @@ settings.visibility.private_shortname=ç§æœ‰ settings.update_settings=更新组织设置 settings.update_setting_success=组织设置已更新。 -settings.change_orgname_prompt=注æ„:更改组织åç§°åŒæ—¶ä¼šæ›´æ”¹ç»„织的 URL 地å€å¹¶é‡Šæ”¾æ—§çš„å称。 -settings.change_orgname_redirect_prompt=在被人使用å‰ï¼Œæ—§ç”¨æˆ·å将会被é‡å®šå‘。 + +settings.rename=修改组织åç§° +settings.rename_desc=更改组织åç§°åŒæ—¶ä¼šæ›´æ”¹ç»„织的 URL 地å€å¹¶é‡Šæ”¾æ—§çš„å称。 +settings.rename_success=组织 %[1]s å·²æˆåŠŸé‡å‘½å为 %[2]s。 +settings.rename_no_change=组织å称没有å˜åŒ–。 +settings.rename_new_org_name=新组织åç§° +settings.rename_failed=由于内部错误,é‡å‘½å组织失败 +settings.rename_notices_1=æ¤æ“作 <strong>æ— æ³•</strong> 被回滚。 +settings.rename_notices_2=在被人使用å‰ï¼Œæ—§å称将会被é‡å®šå‘。 + settings.update_avatar_success=组织头åƒå·²ç»æ›´æ–°ã€‚ settings.delete=åˆ é™¤ç»„ç»‡ settings.delete_account=åˆ é™¤å½“å‰ç»„织 -settings.delete_prompt=åˆ é™¤æ“作会永久清除该组织的信æ¯ï¼Œå¹¶ä¸” <strong>ä¸å¯æ¢å¤</strong>ï¼ +settings.delete_prompt=åˆ é™¤æ“作会永久清除该组织的信æ¯ï¼Œå¹¶ä¸” <strong>æ— æ³•</strong> æ¢å¤ï¼ +settings.name_confirm=输入组织å称以确认: +settings.delete_notices_1=æ¤æ“作 <strong>æ— æ³•</strong> 被回滚。 +settings.delete_notices_2=æ¤æ“ä½œå°†æ°¸ä¹…åˆ é™¤ <strong>%s</strong> 的所有<strong>仓库</strong>,包括 Git æ•°æ®ã€ å·¥å•ã€è¯„论ã€ç™¾ç§‘å’Œå作者的æ“作æƒé™ã€‚ +settings.delete_notices_3=æ¤æ“ä½œå°†æ°¸ä¹…åˆ é™¤ <strong>%s</strong> 的所有 <strong>软件包</strong>。 +settings.delete_notices_4=æ¤æ“ä½œå°†æ°¸ä¹…åˆ é™¤ <strong>%s</strong> 的所有 <strong>项目</strong>。 settings.confirm_delete_account=ç¡®è®¤åˆ é™¤ç»„ç»‡ -settings.delete_org_title=åˆ é™¤ç»„ç»‡ -settings.delete_org_desc=æ¤ç»„ç»‡å°†ä¼šè¢«æ°¸ä¹…åˆ é™¤ï¼Œç¡®è®¤ç»§ç»å—? +settings.delete_failed=ç”±äºŽå†…éƒ¨é”™è¯¯ï¼Œåˆ é™¤ç»„ç»‡å¤±è´¥ +settings.delete_successful=组织 <b>%s</b> å·²æˆåŠŸåˆ é™¤ã€‚ settings.hooks_desc=在æ¤å¤„æ·»åŠ çš„ Web é’©å将会应用到该组织下的 <strong>所有仓库</strong>。 settings.labels_desc=æ·»åŠ èƒ½å¤Ÿè¢«è¯¥ç»„ç»‡ä¸‹çš„ <strong>所有仓库</strong> 的工å•ä½¿ç”¨çš„æ ‡ç¾ã€‚ @@ -2880,7 +2912,7 @@ teams.invite_team_member=é‚€è¯·åŠ å…¥ %s teams.invite_team_member.list=待处ç†çš„邀请 teams.delete_team_title=åˆ é™¤å›¢é˜Ÿ teams.delete_team_desc=åˆ é™¤ä¸€ä¸ªå›¢é˜Ÿå°†åˆ é™¤å›¢é˜Ÿæˆå‘˜çš„访问æƒé™ï¼Œç»§ç»ï¼Ÿ -teams.delete_team_success=è¯¥å›¢é˜Ÿå·²è¢«åˆ é™¤ã€‚ +teams.delete_team_success=è¯¥å›¢é˜Ÿå·²åˆ é™¤ã€‚ teams.read_permission_desc=该团队拥有对所属仓库的 <strong>读å–</strong> æƒé™ï¼Œå›¢é˜Ÿæˆå‘˜å¯ä»¥è¿›è¡ŒæŸ¥çœ‹å’Œå…‹éš†ç‰åªè¯»æ“作。 teams.write_permission_desc=该团队拥有对所属仓库的 <strong>读å–</strong> å’Œ <strong>写入</strong> çš„æƒé™ã€‚ teams.admin_permission_desc=该团队拥有一定的 <strong>管ç†</strong> æƒé™ï¼Œå›¢é˜Ÿæˆå‘˜å¯ä»¥è¯»å–ã€å…‹éš†ã€æŽ¨é€ä»¥åŠæ·»åŠ å…¶å®ƒä»“åº“å作者。 @@ -2931,7 +2963,7 @@ repositories=ä»“åº“ç®¡ç† hooks=Web é’©å integrations=é›†æˆ authentication=è®¤è¯æº -emails=用户邮件 +emails=用户邮箱 config=应用é…ç½® config_summary=æ‘˜è¦ config_settings=设置 @@ -2950,27 +2982,27 @@ dashboard.operation_name=æ“作åç§° dashboard.operation_switch=开关 dashboard.operation_run=执行 dashboard.clean_unbind_oauth=æ¸…ç†æœªç»‘定的 OAuth 连接 -dashboard.clean_unbind_oauth_success=所有未绑定的 OAuth è¿žæŽ¥å·²è¢«åˆ é™¤ã€‚ +dashboard.clean_unbind_oauth_success=所有未绑定的 OAuth è¿žæŽ¥å·²åˆ é™¤ã€‚ dashboard.task.started=已开始任务:%[1]s dashboard.task.process=任务: %[1]s dashboard.task.cancelled=任务: %[1]s 已喿¶ˆ: %[3]s dashboard.task.error=任务ä¸çš„错误: %[1]s: %[3]s dashboard.task.finished=任务: %[2]s å¯åŠ¨çš„ %[1]s å·²å®Œæˆ dashboard.task.unknown=未知任务: %[1]s -dashboard.cron.started=已开始计划任务:%[1]s +dashboard.cron.started=计划任务:%[1]s å·²å¯åЍ dashboard.cron.process=计划任务:%[1]s -dashboard.cron.cancelled=定时任务: %[1]s 已喿¶ˆ: %[3]s -dashboard.cron.error=任务ä¸çš„错误: %s: %[3]s -dashboard.cron.finished=任务:%[1]s å·²ç»å®Œæˆ +dashboard.cron.cancelled=计划任务:%[1]s 已喿¶ˆï¼š%[3]s +dashboard.cron.error=计划任务错误: %s: %[3]s +dashboard.cron.finished=计划任务:%[1]s å·²å®Œæˆ dashboard.delete_inactive_accounts=åˆ é™¤æ‰€æœ‰æœªæ¿€æ´»çš„å¸æˆ· dashboard.delete_inactive_accounts.started=åˆ é™¤æ‰€æœ‰æœªæ¿€æ´»çš„è´¦æˆ·ä»»åŠ¡å·²å¯åŠ¨ã€‚ -dashboard.delete_repo_archives=åˆ é™¤æ‰€æœ‰ä»£ç åº“çš„å˜æ¡£ (ZIP〠TARã€GZ, ç‰...) +dashboard.delete_repo_archives=åˆ é™¤æ‰€æœ‰ä»“åº“çš„å˜æ¡£ï¼ˆZIPã€TARã€GZç‰ï¼‰ dashboard.delete_repo_archives.started=åˆ é™¤æ‰€æœ‰ä»“åº“å˜æ¡£ä»»åС已å¯åŠ¨ã€‚ dashboard.delete_missing_repos=åˆ é™¤æ‰€æœ‰ä¸¢å¤± Git 文件的仓库 dashboard.delete_missing_repos.started=åˆ é™¤æ‰€æœ‰ä¸¢å¤± Git 文件的仓库任务已å¯åŠ¨ã€‚ dashboard.delete_generated_repository_avatars=åˆ é™¤ç”Ÿæˆçš„ä»“åº“å¤´åƒ -dashboard.sync_repo_branches=将缺少的分支从 git æ•°æ®åŒæ¥åˆ°æ•°æ®åº“ -dashboard.sync_repo_tags=从 git æ•°æ®åŒæ¥æ ‡ç¾åˆ°æ•°æ®åº“ +dashboard.sync_repo_branches=将缺少的分支从 Git æ•°æ®åŒæ¥åˆ°æ•°æ®åº“ +dashboard.sync_repo_tags=从 Git æ•°æ®åŒæ¥æ ‡ç¾åˆ°æ•°æ®åº“ dashboard.update_mirrors=更新镜åƒä»“库 dashboard.repo_health_check=å¥åº·æ£€æŸ¥æ‰€æœ‰ä»“库 dashboard.check_repo_stats=检查所有仓库统计 @@ -2985,11 +3017,11 @@ dashboard.reinit_missing_repos=釿–°åˆå§‹åŒ–所有丢失的 Git 仓库å˜åœ¨çš dashboard.sync_external_users=åŒæ¥å¤–éƒ¨ç”¨æˆ·æ•°æ® dashboard.cleanup_hook_task_table=æ¸…ç† hook_task 表 dashboard.cleanup_packages=清ç†è¿‡æœŸçš„软件包 -dashboard.cleanup_actions=清ç†è¿‡æœŸçš„ Actions èµ„æº +dashboard.cleanup_actions=清ç†è¿‡æœŸçš„工作æµèµ„æº dashboard.server_uptime=æœåŠ¡è¿è¡Œæ—¶é—´ dashboard.current_goroutine=å½“å‰ Goroutines æ•°é‡ dashboard.current_memory_usage=当å‰å†…å˜ä½¿ç”¨é‡ -dashboard.total_memory_allocated=所有被分é…çš„å†…å˜ +dashboard.total_memory_allocated=所有已分é…çš„å†…å˜ dashboard.memory_obtained=内å˜å ç”¨é‡ dashboard.pointer_lookup_times=指针查找次数 dashboard.memory_allocate_times=内å˜åˆ†é…次数 @@ -2998,36 +3030,36 @@ dashboard.current_heap_usage=å½“å‰ Heap 内å˜ä½¿ç”¨é‡ dashboard.heap_memory_obtained=Heap 内å˜å ç”¨é‡ dashboard.heap_memory_idle=Heap 内å˜ç©ºé—²é‡ dashboard.heap_memory_in_use=æ£åœ¨ä½¿ç”¨çš„ Heap å†…å˜ -dashboard.heap_memory_released=被释放的 Heap å†…å˜ +dashboard.heap_memory_released=已释放的 Heap å†…å˜ dashboard.heap_objects=Heap å¯¹è±¡æ•°é‡ dashboard.bootstrap_stack_usage=å¯åЍ Stack ä½¿ç”¨é‡ -dashboard.stack_memory_obtained=被分é…çš„ Stack å†…å˜ +dashboard.stack_memory_obtained=已分é…çš„ Stack å†…å˜ dashboard.mspan_structures_usage=MSpan 结构内å˜ä½¿ç”¨é‡ -dashboard.mspan_structures_obtained=被分é…çš„ MSpan ç»“æž„å†…å˜ +dashboard.mspan_structures_obtained=已分é…çš„ MSpan ç»“æž„å†…å˜ dashboard.mcache_structures_usage=MCache 结构内å˜ä½¿ç”¨é‡ -dashboard.mcache_structures_obtained=被分é…çš„ MCache ç»“æž„å†…å˜ -dashboard.profiling_bucket_hash_table_obtained=被分é…的剖æžå“ˆå¸Œè¡¨å†…å˜ -dashboard.gc_metadata_obtained=被分é…çš„ GC 元数æ®å†…å˜ -dashboard.other_system_allocation_obtained=其它被分é…çš„ç³»ç»Ÿå†…å˜ +dashboard.mcache_structures_obtained=已分é…çš„ MCache ç»“æž„å†…å˜ +dashboard.profiling_bucket_hash_table_obtained=已分é…的剖æžå“ˆå¸Œè¡¨å†…å˜ +dashboard.gc_metadata_obtained=已分é…çš„ GC 元数æ®å†…å˜ +dashboard.other_system_allocation_obtained=其它已分é…çš„ç³»ç»Ÿå†…å˜ dashboard.next_gc_recycle=下次 GC 内å˜å›žæ”¶é‡ dashboard.last_gc_time=è·ç¦»ä¸Šæ¬¡ GC æ—¶é—´ dashboard.total_gc_time=GC æš‚åœæ—¶é—´æ€»é‡ dashboard.total_gc_pause=GC æš‚åœæ—¶é—´æ€»é‡ dashboard.last_gc_pause=上次 GC æš‚åœæ—¶é—´ dashboard.gc_times=GC 执行次数 -dashboard.delete_old_actions=从数æ®åº“ä¸åˆ 除所有旧æ“作记录 -dashboard.delete_old_actions.started=已开始从数æ®åº“ä¸åˆ 除所有旧æ“作记录。 +dashboard.delete_old_actions=从数æ®åº“ä¸åˆ 除所有旧工作æµè®°å½• +dashboard.delete_old_actions.started=已开始从数æ®åº“ä¸åˆ 除所有旧工作æµè®°å½•。 dashboard.update_checker=更新检查器 dashboard.delete_old_system_notices=从数æ®åº“ä¸åˆ 除所有旧系统通知 dashboard.gc_lfs=垃圾回收 LFS å…ƒæ•°æ® -dashboard.stop_zombie_tasks=åœæ¢åƒµå°¸ä»»åŠ¡ -dashboard.stop_endless_tasks=åœæ¢æ— æ³•åœæ¢çš„任务 -dashboard.cancel_abandoned_jobs=å–æ¶ˆä¸¢å¼ƒçš„任务 -dashboard.start_schedule_tasks=开始Actions调度任务 +dashboard.stop_zombie_tasks=åœæ¢åƒµå°¸å·¥ä½œæµä»»åŠ¡ +dashboard.stop_endless_tasks=åœæ¢æ— é™å¾ªçŽ¯çš„å·¥ä½œæµä»»åŠ¡ +dashboard.cancel_abandoned_jobs=å–æ¶ˆå·²æ”¾å¼ƒçš„工作æµä»»åŠ¡ +dashboard.start_schedule_tasks=å¯åŠ¨å·¥ä½œæµè®¡åˆ’任务 dashboard.sync_branch.started=åˆ†æ”¯åŒæ¥å·²å¼€å§‹ dashboard.sync_tag.started=æ ‡ç¾åŒæ¥å·²å¼€å§‹ dashboard.rebuild_issue_indexer=é‡å»ºå·¥å•索引 -dashboard.sync_repo_licenses=釿–°ä»“库许å¯è¯æŽ¢æµ‹ +dashboard.sync_repo_licenses=釿–°æŽ¢æµ‹ä»“库许å¯è¯ users.user_manage_panel=ç”¨æˆ·å¸æˆ·ç®¡ç† users.new_account=åˆ›å»ºæ–°å¸æˆ· @@ -3045,33 +3077,33 @@ users.created=创建时间 users.last_login=上次登录 users.never_login=从未登录 users.send_register_notify=å‘逿³¨å†Œé€šçŸ¥ -users.new_success=用户账户 '%s' 已被创建。 +users.new_success=用户账户「%sã€å·²åˆ›å»ºã€‚ users.edit=修改 users.auth_source=è®¤è¯æº users.local=本地 users.auth_login_name=认è¯ç™»å½•åç§° users.password_helper=ä¿æŒå¯†ç ä¸ºç©ºå°†ä¸æ›´æ”¹å¯†ç 。 -users.update_profile_success=è¯¥å¸æˆ·å·²è¢«æ›´æ–°ã€‚ +users.update_profile_success=è¯¥å¸æˆ·å·²æ›´æ–°ã€‚ users.edit_account=编辑å¸å· users.max_repo_creation=最大仓库数 users.max_repo_creation_desc=(设置为 -1 表示使用全局默认值) -users.is_activated=该用户已被激活 +users.is_activated=该用户已激活 users.prohibit_login=ç¦ç”¨ç™»å½• users.is_admin=是管ç†å‘˜ users.is_restricted=å—é™ users.allow_git_hook=å…许创建 Git é’©å -users.allow_git_hook_tooltip=Git é’©å将会被以æ“作系统用户è¿è¡Œï¼Œå°†ä¼šæ‹¥æœ‰åŒæ ·çš„主机访问æƒé™ã€‚å› æ¤ï¼Œæ‹¥æœ‰æ¤ç‰¹æ®Šçš„Git é’©åæƒé™å°†èƒ½å¤Ÿè®¿é—®åˆä¿®æ”¹æ‰€æœ‰çš„ Gitea 仓库或者Gitea的数æ®åº“ã€‚åŒæ—¶ä¹Ÿèƒ½èŽ·å¾—Gitea的管ç†å‘˜æƒé™ã€‚ +users.allow_git_hook_tooltip=Git é’©å将会以æ“作系统用户è¿è¡Œï¼Œæ‹¥æœ‰åŒæ ·çš„主机访问æƒé™ã€‚å› æ¤ï¼Œæ‹¥æœ‰æ¤ç‰¹æ®Šçš„ Git é’©åæƒé™å°†èƒ½å¤Ÿè®¿é—®åˆä¿®æ”¹æ‰€æœ‰çš„ Gitea 仓库或者 Gitea 的数æ®åº“ã€‚åŒæ—¶ä¹Ÿèƒ½èŽ·å¾— Gitea 的管ç†å‘˜æƒé™ã€‚ users.allow_import_local=å…许导入本地仓库 users.allow_create_organization=å…许创建组织 users.update_profile=æ›´æ–°å¸æˆ· users.delete_account=åˆ é™¤å¸æˆ· -users.cannot_delete_self=ä½ ä¸èƒ½åˆ 除自己 -users.still_own_repo=æ¤ç”¨æˆ·ä»ç„¶æ‹¥æœ‰ä¸€ä¸ªæˆ–å¤šä¸ªä»“åº“ã€‚å¿…é¡»é¦–å…ˆåˆ é™¤æˆ–è½¬è®©è¿™äº›ä»“åº“ã€‚ +users.cannot_delete_self=您ä¸èƒ½åˆ 除自己 +users.still_own_repo=æ¤ç”¨æˆ·ä»ç„¶æ‹¥æœ‰ä¸€ä¸ªæˆ–å¤šä¸ªä»“åº“ã€‚å¿…é¡»é¦–å…ˆåˆ é™¤æˆ–è½¬ç§»è¿™äº›ä»“åº“ã€‚ users.still_has_org=æ¤ç”¨æˆ·æ˜¯ç»„织的æˆå‘˜ã€‚必须先从组织ä¸åˆ 除用户。 users.purge=清ç†ç”¨æˆ· -users.purge_help=å¼ºåˆ¶åˆ é™¤ç”¨æˆ·å’Œç”¨æˆ·æ‹¥æœ‰çš„ä»»ä½•ä»“åº“ã€ç»„ç»‡å’Œè½¯ä»¶åŒ…ã€‚æ‰€æœ‰è¯„è®ºä¹Ÿå°†è¢«åˆ é™¤ã€‚ +users.purge_help=å¼ºåˆ¶åˆ é™¤ç”¨æˆ·å’Œç”¨æˆ·æ‹¥æœ‰çš„ä»»ä½•ä»“åº“ã€ç»„ç»‡å’Œè½¯ä»¶åŒ…ã€‚æ‰€æœ‰è¯„è®ºä¹Ÿå°†åˆ é™¤ã€‚ users.still_own_packages=æ¤ç”¨æˆ·ä»ç„¶æ‹¥æœ‰ä¸€ä¸ªæˆ–å¤šä¸ªè½¯ä»¶åŒ…ï¼Œè¯·å…ˆåˆ é™¤è¿™äº›è½¯ä»¶åŒ…ã€‚ -users.deletion_success=ç”¨æˆ·å¸æˆ·å·²è¢«åˆ 除。 +users.deletion_success=ç”¨æˆ·å¸æˆ·å·²åˆ 除。 users.reset_2fa=é‡ç½®ä¸¤æ¥éªŒè¯ users.list_status_filter.menu_text=过滤 users.list_status_filter.reset=é‡ç½® @@ -3090,19 +3122,19 @@ users.details=ç”¨æˆ·è¯¦ç»†ä¿¡æ¯ emails.email_manage_panel=é‚®ä»¶ç®¡ç† emails.primary=主è¦çš„ emails.activated=已激活 -emails.filter_sort.email=电å邮件 -emails.filter_sort.email_reverse=电å邮件(逆åº) +emails.filter_sort.email=邮箱 +emails.filter_sort.email_reverse=邮箱(倒åºï¼‰ emails.filter_sort.name=用户å emails.filter_sort.name_reverse=用户å(倒åº) -emails.updated=电å邮件已更新 -emails.not_updated=æ— æ³•æ›´æ–°è¯·æ±‚çš„ç”µå邮件地å€ï¼š %v -emails.duplicate_active=æ¤ç”µå邮件地å€å·²è¢«å¦ä¸€ä¸ªç”¨æˆ·æ¿€æ´»ä½¿ç”¨ã€‚ -emails.change_email_header=更新电å邮件属性 -emails.change_email_text=æ‚¨ç¡®å®šè¦æ›´æ–°è¯¥ç”µå邮件地å€å—? -emails.delete=åˆ é™¤ç”µå邮件 -emails.delete_desc=您确定è¦åˆ 除该电å邮件地å€ï¼Ÿ -emails.deletion_success=电å邮件地å€å·²è¢«åˆ 除。 -emails.delete_primary_email_error=您ä¸èƒ½åˆ 除主电å邮件。 +emails.updated=邮箱已更新 +emails.not_updated=æ— æ³•æ›´æ–°è¯·æ±‚çš„é‚®ç®±åœ°å€ï¼š%v +emails.duplicate_active=æ¤é‚®ç®±åœ°å€å·²è¢«å¦ä¸€ä¸ªç”¨æˆ·æ¿€æ´»ä½¿ç”¨ã€‚ +emails.change_email_header=更新邮箱属性 +emails.change_email_text=æ‚¨ç¡®å®šè¦æ›´æ–°è¯¥é‚®ç®±åœ°å€å—? +emails.delete=åˆ é™¤é‚®ç®± +emails.delete_desc=您确定è¦åˆ 除该邮箱地å€ï¼Ÿ +emails.deletion_success=邮箱地å€å·²åˆ 除。 +emails.delete_primary_email_error=您ä¸èƒ½åˆ 除主邮箱。 orgs.org_manage_panel=ç»„ç»‡ç®¡ç† orgs.name=åç§° @@ -3122,7 +3154,7 @@ repos.lfs_size=LFS å¤§å° packages.package_manage_panel=è½¯ä»¶åŒ…ç®¡ç† packages.total_size=总大å°ï¼š%s -packages.unreferenced_size=未引用大å°ï¼š %s +packages.unreferenced_size=未引用大å°ï¼š%s packages.cleanup=清ç†è¿‡æœŸæ•°æ® packages.cleanup.success=清ç†è¿‡æœŸæ•°æ®æˆåŠŸ packages.owner=所有者 @@ -3134,13 +3166,13 @@ packages.repository=仓库 packages.size=å¤§å° packages.published=å·²å‘布 -defaulthooks=默认Webé’©å +defaulthooks=默认 Web é’©å defaulthooks.desc=当æŸäº› Gitea äº‹ä»¶è§¦å‘æ—¶ï¼ŒWeb é’©åè‡ªåŠ¨å‘æœåС噍å‘出 HTTP POST 请求。这里定义的 Web é’©åæ˜¯é»˜è®¤é…置,将被å¤åˆ¶åˆ°æ‰€æœ‰æ–°çš„仓库ä¸ã€‚详情请访问 <a target="_blank" rel="noopener" href="%s">Web é’©åæŒ‡å—</a>。 -defaulthooks.add_webhook=æ·»åŠ é»˜è®¤Web é’©å +defaulthooks.add_webhook=æ·»åŠ é»˜è®¤ Web é’©å defaulthooks.update_webhook=更新默认 Web 钩å systemhooks=系统 Web é’©å -systemhooks.desc=当æŸäº› Gitea äº‹ä»¶è§¦å‘æ—¶ï¼ŒWeb é’©åè‡ªåŠ¨å‘æœåС噍å‘出HTTP POST请求。这里定义的 Web é’©å将作用于系统上的所有仓库,所以请考虑这å¯èƒ½å¸¦æ¥çš„任何性能影å“。了解详情请访问 <a target="_blank" rel="noopener" href="%s">Web é’©åæŒ‡å—</a>。 +systemhooks.desc=当æŸäº› Gitea äº‹ä»¶è§¦å‘æ—¶ï¼ŒWeb é’©åè‡ªåŠ¨å‘æœåС噍å‘出 HTTP POST 请求。这里定义的 Web é’©å将作用于系统上的所有仓库,所以请考虑这å¯èƒ½å¸¦æ¥çš„任何性能影å“。了解详情请访问 <a target="_blank" rel="noopener" href="%s">Web é’©åæŒ‡å—</a>。 systemhooks.add_webhook=æ·»åŠ ç³»ç»Ÿ Web é’©å systemhooks.update_webhook=更新系统 Web é’©å @@ -3162,7 +3194,7 @@ auths.bind_password=绑定密ç auths.user_base=用户æœç´¢åŸºå‡† auths.user_dn=用户 DN auths.attribute_username=用户å属性 -auths.attribute_username_placeholder=置空将使用Gitea的用户å。 +auths.attribute_username_placeholder=置空将使用 Gitea 的用户å。 auths.attribute_name=åå—属性 auths.attribute_surname=å§“æ°å±žæ€§ auths.attribute_mail=电å邮箱属性 @@ -3196,7 +3228,7 @@ auths.helo_hostname=HELO 主机å auths.helo_hostname_helper=用 HELO å‘é€çš„主机å。 留空å‘é€å½“å‰ä¸»æœºå。 auths.disable_helo=ç¦ç”¨ HELO auths.pam_service_name=PAM æœåŠ¡åç§° -auths.pam_email_domain=PAM 电å邮件域(å¯é€‰) +auths.pam_email_domain=PAM 邮件域(å¯é€‰ï¼‰ auths.oauth2_provider=OAuth2 æä¾›ç¨‹åº auths.oauth2_icon_url=å›¾æ ‡ URL auths.oauth2_clientID=客户端 ID (键) @@ -3205,8 +3237,8 @@ auths.openIdConnectAutoDiscoveryURL=OpenID 连接自动å‘现 URL auths.oauth2_use_custom_url=使用自定义的 URL è€Œä¸æ˜¯é»˜è®¤çš„ URL auths.oauth2_tokenURL=令牌 URL auths.oauth2_authURL=æŽˆæƒ URL -auths.oauth2_profileURL=Profile URL -auths.oauth2_emailURL=电å邮件 URL +auths.oauth2_profileURL=个人资料 URL +auths.oauth2_emailURL=邮箱 URL auths.skip_local_two_fa=跳过本地两æ¥éªŒè¯ auths.skip_local_two_fa_helper=ä¸è®¾ç½®æ„味ç€è®¾ç½®äº†ä¸¤æ¥éªŒè¯çš„æœ¬åœ°ç”¨æˆ·ä»ç„¶éœ€è¦é€šè¿‡ä¸¤æ¥éªŒè¯æ‰èƒ½ç™»å½• auths.oauth2_tenant=租户 @@ -3226,31 +3258,31 @@ auths.sspi_auto_create_users_helper=å…许 SSPI 认è¯åœ¨ç”¨æˆ·ç¬¬ä¸€æ¬¡ç™»å½•æ— auths.sspi_auto_activate_users=自动激活用户 auths.sspi_auto_activate_users_helper=å…许 SSPI 认è¯è‡ªåŠ¨æ¿€æ´»æ–°ç”¨æˆ· auths.sspi_strip_domain_names=从用户åä¸åˆ 除域å部分 -auths.sspi_strip_domain_names_helper=å¦‚æžœé€‰ä¸æ¤é¡¹ï¼ŒåŸŸå将从登录åä¸åˆ 除(例如,"DOMAIN\user"å’Œ"user@example.org"ï¼Œä¸¤è€…éƒ½å°†å˜æˆåªæ˜¯â€œç”¨æˆ·â€)。 +auths.sspi_strip_domain_names_helper=å¦‚æžœé€‰ä¸æ¤é¡¹ï¼ŒåŸŸå将从登录åä¸åˆ 除(例如,「DOMAIN\userã€å’Œã€Œuser@example.orgã€ï¼Œä¸¤è€…éƒ½å°†å˜æˆåªæ˜¯ã€Œç”¨æˆ·ã€ï¼‰ã€‚ auths.sspi_separator_replacement=è¦ä½¿ç”¨çš„分隔符代替\, / å’Œ @ -auths.sspi_separator_replacement_helper=用于替æ¢ä¸‹çº§ç™»å½•å称分隔符的å—符 (例如 "DOMAIN\user") ä¸çš„ \ 和用户主åå—(如"user@example.org"ä¸çš„ @)。 +auths.sspi_separator_replacement_helper=用于替æ¢ä¸‹çº§ç™»å½•å称分隔符的å—符(例如「DOMAIN\userã€ï¼‰ä¸çš„ \ 和用户主åå—(如「user@example.orgã€ä¸çš„ @)。 auths.sspi_default_language=默认è¯è¨€ auths.sspi_default_language_helper=SSPI è®¤è¯æ–¹æ³•为用户自动创建的默认è¯è¨€ã€‚如果您想è¦è‡ªåŠ¨æ£€æµ‹åˆ°è¯è¨€ï¼Œè¯·ç•™ç©ºã€‚ auths.tips=帮助æç¤º auths.tips.oauth2.general=OAuth2 è®¤è¯ auths.tips.oauth2.general.tip=当注册新的 OAuth2 èº«ä»½éªŒè¯æ—¶ï¼Œå›žè°ƒ/é‡å®šå‘ URL 应该是: auths.tip.oauth2_provider=OAuth2 æä¾›ç¨‹åº -auths.tip.bitbucket=在 %s 注册新的 OAuth ä½¿ç”¨è€…åŒæ—¶æ·»åŠ æƒé™â€œè´¦å·â€-“读å–†-auths.tip.nextcloud=使用下é¢çš„èœå•“设置(Settings) -> 安全(Security) -> OAuth 2.0 clientâ€åœ¨æ‚¨çš„实例上注册一个新的 OAuth 客户端。 +auths.tip.bitbucket=在 %s 注册新的 OAuth ä½¿ç”¨è€…åŒæ—¶æ·»åŠ æƒé™ã€Œè´¦å·ã€-「读å–〠+auths.tip.nextcloud=使用下é¢çš„èœå•「设置 -> 安全 -> OAuth 2.0 客户端ã€åœ¨æ‚¨çš„实例上注册一个新的 OAuth 客户端 auths.tip.dropbox=在 %s ä¸Šåˆ›å»ºä¸€ä¸ªæ–°çš„åº”ç”¨ç¨‹åº -auths.tip.facebook=`在 %s æ³¨å†Œä¸€ä¸ªæ–°çš„åº”ç”¨ï¼Œå¹¶æ·»åŠ äº§å“"Facebook 登录"` +auths.tip.facebook=在 %s æ³¨å†Œä¸€ä¸ªæ–°çš„åº”ç”¨ï¼Œå¹¶æ·»åŠ äº§å“「Facebook 登录〠auths.tip.github=在 %s 注册一个 OAuth åº”ç”¨ç¨‹åº auths.tip.gitlab_new=在 %s 注册一个新的应用 auths.tip.google_plus=ä»Žè°·æŒ API æŽ§åˆ¶å° %s 获得 OAuth2 å®¢æˆ·ç«¯å‡æ® auths.tip.openid_connect=使用 OpenID 连接å‘现 URL ({server}/.well-known/openid-configuration) æ¥æŒ‡å®šç»ˆç‚¹ -auths.tip.twitter=访问 %s,创建应用并确ä¿å¯ç”¨äº†"å…许æ¤åº”用程åºç”¨äºŽç™»å½• Twitter"的选项。 +auths.tip.twitter=访问 %s,创建应用并确ä¿å¯ç”¨äº†ã€Œå…许æ¤åº”用程åºä½¿ç”¨ Twitter 登录ã€çš„选项 auths.tip.discord=在 %s ä¸Šæ³¨å†Œæ–°åº”ç”¨ç¨‹åº auths.tip.gitea=注册一个新的 OAuth2 应用程åºã€‚å¯ä»¥è®¿é—® %s 查看帮助 -auths.tip.yandex=在 %s 上创建一个新的应用程åºã€‚在“ Yandex.Passport APIâ€è¿™éƒ¨åˆ†ä¸é€‰æ‹©ä»¥ä¸‹æƒé™ï¼šâ€œè®¿é—®ç”µå邮件地å€ï¼ˆAccess to email address)â€ï¼Œâ€œè®¿é—®ç”¨æˆ·å¤´åƒï¼ˆAccess to user avatar)â€å’Œâ€œè®¿é—®ç”¨æˆ·å,åå—和姓æ°ï¼Œæ€§åˆ«ï¼ˆAccess to username, first name and surname, genderAccess to username, first name and surname, gender)†+auths.tip.yandex=在 %s 上创建一个新的应用。在「Yandex.Passport APIã€è¿™éƒ¨åˆ†ä¸é€‰æ‹©ä»¥ä¸‹æƒé™ï¼šã€Œè®¿é—®é‚®ç®±åœ°å€ã€ï¼Œã€Œè®¿é—®ç”¨æˆ·å¤´åƒã€å’Œã€Œè®¿é—®ç”¨æˆ·å,åå—和姓æ°ï¼Œæ€§åˆ«ã€ auths.tip.mastodon=输入您想è¦è®¤è¯çš„ mastodon 实例的自定义 URL (或使用默认值) auths.edit=ä¿®æ”¹è®¤è¯æº auths.activated=è¯¥è®¤è¯æºå·²ç»å¯ç”¨ -auths.new_success=å·²æ·»åŠ èº«ä»½éªŒè¯ '%s'。 +auths.new_success=å·²æ·»åŠ èº«ä»½éªŒè¯ã€Œ%sã€ã€‚ auths.update_success=è®¤è¯æºå·²ç»æ›´æ–°ã€‚ auths.update=æ›´æ–°è®¤è¯æº auths.delete=åˆ é™¤è®¤è¯æº @@ -3258,10 +3290,10 @@ auths.delete_auth_title=åˆ é™¤èº«ä»½éªŒè¯æº auths.delete_auth_desc=åˆ é™¤ä¸€ä¸ªè®¤è¯æºå°†é˜»æ¢ä½¿ç”¨å®ƒè¿›è¡Œç™»å½•。确认? auths.still_in_used=è®¤è¯æºä»åœ¨ä½¿ç”¨ã€‚è¯·å…ˆè§£é™¤æˆ–è€…åˆ é™¤ä½¿ç”¨æ¤è®¤è¯æºçš„用户。 auths.deletion_success=è®¤è¯æºå·²ç»æ›´æ–°ã€‚ -auths.login_source_exist=è®¤è¯æº '%s' å·²ç»å˜åœ¨ã€‚ +auths.login_source_exist=è®¤è¯æºã€Œ%sã€å·²ç»å˜åœ¨ã€‚ auths.login_source_of_type_exist=æ¤ç±»åž‹çš„è®¤è¯æºå·²å˜åœ¨ã€‚ -auths.unable_to_initialize_openid=æ— æ³•åˆå§‹åŒ– OpenID Connect æä¾›å•†ï¼š %s -auths.invalid_openIdConnectAutoDiscoveryURL=æ— æ•ˆçš„ Auto Discovery URL (这必须是一个以 http:// 或 https://开头的有效的 URL) +auths.unable_to_initialize_openid=æ— æ³•åˆå§‹åŒ– OpenID Connect æä¾›å•†ï¼š%s +auths.invalid_openIdConnectAutoDiscoveryURL=æ— æ•ˆçš„è‡ªåŠ¨å‘现 URL(这必须是一个以 http:// 或 https:// 开头的有效的 URL) config.server_config=æœåС噍é…ç½® config.app_name=站点åç§° @@ -3302,16 +3334,16 @@ config.db_type=类型 config.db_host=主机 config.db_name=æ•°æ®åº“åç§° config.db_user=用户å -config.db_schema=æž¶æž„æ¨¡å¼ +config.db_schema=æž¶æž„ config.db_ssl_mode=SSL config.db_path=æ•°æ®åº“路径 config.service_config=æœåŠ¡é…ç½® -config.register_email_confirm=需è¦ç”µå邮件确认注册 +config.register_email_confirm=需è¦é‚®ä»¶ç¡®è®¤æ³¨å†Œ config.disable_register=ç¦æ¢ç”¨æˆ·æ³¨å†Œ -config.allow_only_internal_registration=åªå…许通过 Gitea 进行注册 +config.allow_only_internal_registration=ä»…å…许通过 Gitea 进行注册 config.allow_only_external_registration=ä»…å…许通过外部æœåŠ¡æ³¨å†Œ -config.enable_openid_signup=å¯ç”¨ OpenID 自注册 +config.enable_openid_signup=å¯ç”¨ OpenID 自助注册 config.enable_openid_signin=å¯ç”¨ OpenID 登录 config.show_registration_button=显示注册按钮 config.require_sign_in_view=å¯ç”¨ç™»å½•访问é™åˆ¶ @@ -3319,12 +3351,12 @@ config.mail_notify=å¯ç”¨é‚®ä»¶é€šçŸ¥ config.enable_captcha=å¯ç”¨ç™»å½•验è¯ç config.active_code_lives=激活用户链接有效期 config.reset_password_code_lives=æ¢å¤è´¦æˆ·éªŒè¯ç 过期时间 -config.default_keep_email_private=默认情况下éšè—电åé‚®ä»¶åœ°å€ +config.default_keep_email_private=默认éšè—é‚®ç®±åœ°å€ config.default_allow_create_organization=默认情况下å…许创建组织 config.enable_timetracking=å¯ç”¨æ—¶é—´è·Ÿè¸ª config.default_enable_timetracking=默认情况下å¯ç”¨æ—¶é—´è·Ÿè¸ª config.default_allow_only_contributors_to_track_time=ä»…å…许æˆå‘˜è·Ÿè¸ªæ—¶é—´ -config.no_reply_address=éšè—电å邮件域 +config.no_reply_address=éšè—邮件域 config.default_visibility_organization=新组织的默认å¯è§æ€§ config.default_enable_dependencies=默认情况下å¯ç”¨å·¥å•ä¾èµ– @@ -3335,7 +3367,7 @@ config.skip_tls_verify=跳过 TLS éªŒè¯ config.mailer_config=Mailer é…ç½® config.mailer_enabled=å¯ç”¨æœåŠ¡ -config.mailer_enable_helo=å¯ç”¨HELO +config.mailer_enable_helo=å¯ç”¨ HELO config.mailer_name=任务åç§° config.mailer_protocol=åè®® config.mailer_smtp_addr=SMTP åœ°å€ @@ -3349,8 +3381,8 @@ config.mailer_use_dummy=Dummy config.test_email_placeholder=电åé‚®å€ (例如,test@example.com) config.send_test_mail=å‘逿µ‹è¯•邮件 config.send_test_mail_submit=å‘é€ -config.test_mail_failed=å‘逿µ‹è¯•邮件至 '%s' 时失败:%v -config.test_mail_sent=测试邮件已ç»å‘é€è‡³ '%s'。 +config.test_mail_failed=å‘逿µ‹è¯•邮件至「%sã€å¤±è´¥ï¼š%v +config.test_mail_sent=测试邮件已ç»å‘é€è‡³ã€Œ%sã€ã€‚ config.oauth_config=OAuth é…ç½® config.oauth_enabled=å¯ç”¨ @@ -3362,7 +3394,7 @@ config.cache_conn=Cache 连接å—符串 config.cache_item_ttl=缓å˜é¡¹ç›® TTL config.cache_test=æµ‹è¯•ç¼“å˜ config.cache_test_failed=ç¼“å˜æµ‹è¯•失败: %v。 -config.cache_test_slow=ç¼“å˜æµ‹è¯•æˆåŠŸï¼Œä½†å“应缓慢: %s。 +config.cache_test_slow=ç¼“å˜æµ‹è¯•æˆåŠŸï¼Œä½†å“应缓慢:%s。 config.cache_test_succeeded=ç¼“å˜æµ‹è¯•æˆåŠŸï¼Œåœ¨ %s 时间内得到å“应。 config.session_config=Session é…ç½® @@ -3403,7 +3435,7 @@ config.set_setting_failed=设置 %s 失败 monitor.stats=统计 -monitor.cron=Cron 任务 +monitor.cron=计划任务 monitor.name=任务åç§° monitor.schedule=任务安排 monitor.next=下次执行时间 @@ -3424,7 +3456,7 @@ monitor.process.cancel_desc=䏿¢ä¸€ä¸ªè¿›ç¨‹å¯èƒ½å¯¼è‡´æ•°æ®ä¸¢å¤± monitor.process.children=å进程 monitor.queues=队列 -monitor.queue=队列: %s +monitor.queue=队列:%s monitor.queue.name=åç§° monitor.queue.type=类型 monitor.queue.exemplar=æ•°æ®ç±»åž‹ @@ -3441,7 +3473,7 @@ monitor.queue.settings.maxnumberworkers.error=æœ€å¤§å·¥ä½œè€…æ•°å¿…é¡»æ˜¯æ•°å— monitor.queue.settings.submit=更新设置 monitor.queue.settings.changed=设置已更新 monitor.queue.settings.remove_all_items=移除全部 -monitor.queue.settings.remove_all_items_done=队列ä¸çš„æ‰€æœ‰é¡¹ç›®å·²è¢«ç§»é™¤ã€‚ +monitor.queue.settings.remove_all_items_done=队列ä¸çš„æ‰€æœ‰é¡¹ç›®å·²ç§»é™¤ã€‚ notices.system_notice_list=系统æç¤ºç®¡ç† notices.view_detail_header=查看æç¤ºè¯¦æƒ… @@ -3456,16 +3488,16 @@ notices.type_1=仓库 notices.type_2=任务 notices.desc=æç¤ºæè¿° notices.op=æ“作 -notices.delete_success=ç³»ç»Ÿé€šçŸ¥å·²è¢«åˆ é™¤ã€‚ +notices.delete_success=ç³»ç»Ÿé€šçŸ¥å·²åˆ é™¤ã€‚ self_check.no_problem_found=尚未å‘现问题。 self_check.startup_warnings=å¯åЍè¦å‘Šï¼š self_check.database_collation_mismatch=期望数æ®åº“ä½¿ç”¨çš„æ ¡éªŒæ–¹å¼ï¼š%s -self_check.database_collation_case_insensitive=æ•°æ®åº“æ£åœ¨ä½¿ç”¨ä¸€ä¸ªæ ¡éªŒ %s, è¿™æ˜¯ä¸€ä¸ªä¸æ•æ„Ÿçš„æ ¡éªŒ. 虽然Giteaå¯ä»¥ä¸Žå®ƒåˆä½œï¼Œä½†å¯èƒ½æœ‰ä¸€äº›ç½•è§çš„æƒ…况ä¸å¦‚é¢„æœŸçš„é‚£æ ·èµ·ä½œç”¨ã€‚ -self_check.database_inconsistent_collation_columns=æ•°æ®åº“æ£åœ¨ä½¿ç”¨%s的排åºè§„则,但是这些列使用了ä¸åŒ¹é…的排åºè§„则。这å¯èƒ½ä¼šé€ æˆä¸€äº›æ„外问题。 -self_check.database_fix_mysql=对于MySQL/MariaDB用户,您å¯ä»¥ä½¿ç”¨â€œgitea doctor convertâ€å‘½ä»¤æ¥è§£å†³æ ¡éªŒé—®é¢˜ã€‚ 或者您也å¯ä»¥é€šè¿‡ "ALTER ... COLLATE ..." è¿™æ ·çš„SQL æ¥æ‰‹åŠ¨è§£å†³è¿™ä¸ªé—®é¢˜ã€‚ -self_check.database_fix_mssql=对于MSSQL用户,您现在åªèƒ½é€šè¿‡"ALTER ... COLLATE ..."SQLs手动解决这个问题。 -self_check.location_origin_mismatch=å½“å‰ URL (%[1]s) 与 Gitea çš„ URL (%[2]s) ä¸åŒ¹é… 。 如果您æ£åœ¨ä½¿ç”¨åå‘代ç†ï¼Œè¯·ç¡®ä¿è®¾ç½®æ£ç¡®çš„“主机â€å’Œâ€œX-转å‘-åŽŸå§‹â€æ ‡é¢˜ã€‚ +self_check.database_collation_case_insensitive=æ•°æ®åº“æ£åœ¨ä½¿ç”¨ä¸€ä¸ªæ ¡éªŒ %sï¼Œè¿™æ˜¯ä¸€ä¸ªä¸æ•æ„Ÿçš„æ ¡éªŒã€‚è™½ç„¶ Gitea å¯ä»¥ä¸Žå®ƒåˆä½œï¼Œä½†å¯èƒ½æœ‰ä¸€äº›ç½•è§çš„æƒ…况ä¸å¦‚é¢„æœŸçš„é‚£æ ·èµ·ä½œç”¨ã€‚ +self_check.database_inconsistent_collation_columns=æ•°æ®åº“æ£åœ¨ä½¿ç”¨ %s 的排åºè§„则,但是这些列使用了ä¸åŒ¹é…的排åºè§„则。这å¯èƒ½ä¼šé€ æˆä¸€äº›æ„外问题。 +self_check.database_fix_mysql=对于 MySQL/MariaDB 用户,您å¯ä»¥ä½¿ç”¨ã€Œgitea doctor convertã€å‘½ä»¤æ¥è§£å†³æ ¡éªŒé—®é¢˜ã€‚ 或者您也å¯ä»¥é€šè¿‡ã€ŒALTER ... COLLATE ...ã€è¿™æ ·çš„ SQL æ¥æ‰‹åŠ¨è§£å†³è¿™ä¸ªé—®é¢˜ã€‚ +self_check.database_fix_mssql=对于 MSSQL 用户,您现在åªèƒ½é€šè¿‡ã€ŒALTER ... COLLATE ...ã€SQL 手动解决这个问题。 +self_check.location_origin_mismatch=å½“å‰ URL(%[1]s)与 Gitea çš„ URL(%[2]s)ä¸åŒ¹é… 。 如果您æ£åœ¨ä½¿ç”¨åå‘代ç†ï¼Œè¯·ç¡®ä¿è®¾ç½®æ£ç¡®çš„「Hostã€å’Œã€ŒX-Forwarded-Protoã€æ ‡å¤´ã€‚ [action] create_repo=创建了仓库 <a href="%s">%s</a> @@ -3473,10 +3505,10 @@ rename_repo=é‡å‘½å仓库 <code>%[1]s</code> 为 <a href="%[2]s">%[3]s</a> commit_repo=推é€åˆ°äº†ä»“库 <a href="%[1]s">%[4]s</a> çš„ <a href="%[2]s">%[3]s</a> 分支 create_issue=`åˆ›å»ºäº†å·¥å• <a href="%[1]s">%[3]s#%[2]s</a>` close_issue=`å…³é—äº†å·¥å• <a href="%[1]s">%[3]s#%[2]s</a>` -reopen_issue=`釿–°å¼€å¯äº†å·¥å• <a href="%[1]s">%[3]s#%[2]s</a>` +reopen_issue=`釿–°æ‰“å¼€äº†å·¥å• <a href="%[1]s">%[3]s#%[2]s</a>` create_pull_request=`创建了åˆå¹¶è¯·æ±‚ <a href="%[1]s">%[3]s#%[2]s</a>` close_pull_request=`å…³é—了åˆå¹¶è¯·æ±‚ <a href="%[1]s">%[3]s#%[2]s</a>` -reopen_pull_request=`釿–°å¼€å¯äº†åˆå¹¶è¯·æ±‚ <a href="%[1]s">%[3]s#%[2]s</a>` +reopen_pull_request=`釿–°æ‰“开了åˆå¹¶è¯·æ±‚ <a href="%[1]s">%[3]s#%[2]s</a>` comment_issue=`è¯„è®ºäº†å·¥å• <a href="%[1]s">%[3]s#%[2]s</a>` comment_pull=`评论了åˆå¹¶è¯·æ±‚ <a href="%[1]s">%[3]s#%[2]s</a>` merge_pull_request=`åˆå¹¶äº†åˆå¹¶è¯·æ±‚ <a href="%[1]s">%[3]s#%[2]s</a>` @@ -3544,7 +3576,7 @@ no_subscriptions=æ— è®¢é˜… default_key=使用默认密钥ç¾å error.extract_sign=æ— æ³•æå–ç¾å error.generate_hash=æ— æ³•ç”Ÿæˆæäº¤çš„å“ˆå¸Œ -error.no_committer_account=æ²¡æœ‰å¸æˆ·é“¾æŽ¥åˆ°æäº¤è€…的电å邮件 +error.no_committer_account=æ²¡æœ‰å¸æˆ·é“¾æŽ¥åˆ°æäº¤è€…的邮箱 error.no_gpg_keys_found=找ä¸åˆ°æ¤ç¾å对应的密钥 error.not_signed_commit=未ç¾åçš„æäº¤ error.failed_retrieval_gpg_keys=找ä¸åˆ°ä»»ä½•与该æäº¤è€…è´¦å·ç›¸å…³çš„密钥 @@ -3670,11 +3702,11 @@ rubygems.dependencies.development=å¼€å‘ä¾èµ– rubygems.required.ruby=éœ€è¦ Ruby 版本 rubygems.required.rubygems=éœ€è¦ RubyGem 版本 swift.registry=ä»Žå‘½ä»¤è¡Œè®¾ç½®æ¤æ³¨å†Œä¸å¿ƒï¼š -swift.install=åœ¨ä½ çš„ <code>Package.swift</code>æ–‡ä»¶ä¸æ·»åŠ è¯¥åŒ…ï¼š +swift.install=在您的 <code>Package.swift</code>æ–‡ä»¶ä¸æ·»åŠ è¯¥åŒ…ï¼š swift.install2=å¹¶è¿è¡Œä»¥ä¸‹å‘½ä»¤ï¼š vagrant.install=è‹¥è¦æ·»åŠ ä¸€ä¸ª Vagrant box,请è¿è¡Œä»¥ä¸‹å‘½ä»¤ï¼š settings.link=å°†æ¤è½¯ä»¶åŒ…链接到仓库 -settings.link.description=如果您将一个软件包与一个代ç 库链接起æ¥ï¼Œè½¯ä»¶åŒ…将显示在代ç 库的软件包列表ä¸ã€‚ +settings.link.description=如果您将一个软件包与一个仓库链接起æ¥ï¼Œè½¯ä»¶åŒ…将显示在仓库的软件包列表ä¸ã€‚ settings.link.select=选择仓库 settings.link.button=更新仓库链接 settings.link.success=仓库链接已æˆåŠŸæ›´æ–°ã€‚ @@ -3682,13 +3714,13 @@ settings.link.error=更新仓库链接失败。 settings.delete=åˆ é™¤è½¯ä»¶åŒ… settings.delete.description=åˆ é™¤è½¯ä»¶åŒ…æ˜¯æ°¸ä¹…æ€§çš„ï¼Œæ— æ³•æ’¤æ¶ˆã€‚ settings.delete.notice=您将è¦åˆ 除 %s (%s)ã€‚æ¤æ“作是ä¸å¯é€†çš„,您确定å—? -settings.delete.success=è½¯ä»¶åŒ…å·²è¢«åˆ é™¤ã€‚ +settings.delete.success=è½¯ä»¶åŒ…å·²åˆ é™¤ã€‚ settings.delete.error=åˆ é™¤è½¯ä»¶åŒ…å¤±è´¥ã€‚ owner.settings.cargo.title=Cargo 注册ä¸å¿ƒç´¢å¼• owner.settings.cargo.initialize=åˆå§‹åŒ–索引 -owner.settings.cargo.initialize.description=使用 Cargo 注册ä¸å¿ƒæ—¶éœ€è¦ä¸€ä¸ªç‰¹æ®Šç´¢å¼•çš„ Git 仓库。使用æ¤é€‰é¡¹å°†ï¼ˆé‡æ–°ï¼‰åˆ›å»ºå˜å‚¨åº“并自动é…置它。 -owner.settings.cargo.initialize.error=åˆå§‹åŒ–Cargo索引失败: %v -owner.settings.cargo.initialize.success=Cargoç´¢å¼•å·²ç»æˆåŠŸåˆ›å»ºã€‚ +owner.settings.cargo.initialize.description=使用 Cargo 注册ä¸å¿ƒæ—¶éœ€è¦ä¸€ä¸ªç‰¹æ®Šç´¢å¼•çš„ Git 仓库。使用æ¤é€‰é¡¹å°†ï¼ˆé‡æ–°ï¼‰åˆ›å»ºä»“库并自动é…置它。 +owner.settings.cargo.initialize.error=åˆå§‹åŒ– Cargo 索引失败: %v +owner.settings.cargo.initialize.success=Cargo ç´¢å¼•å·²ç»æˆåŠŸåˆ›å»ºã€‚ owner.settings.cargo.rebuild=é‡å»ºç´¢å¼• owner.settings.cargo.rebuild.description=如果索引与å˜å‚¨çš„ Cargo 包ä¸åŒæ¥ï¼Œé‡å»ºå¯èƒ½ä¼šæœ‰ç”¨ã€‚ owner.settings.cargo.rebuild.error=æ— æ³•é‡å»º Cargo 索引: %v @@ -3701,7 +3733,7 @@ owner.settings.cleanuprules.preview=清ç†è§„则预览 owner.settings.cleanuprules.preview.overview=%d ä¸ªè½¯ä»¶åŒ…è®¡åˆ’è¢«åˆ é™¤ã€‚ owner.settings.cleanuprules.preview.none=清ç†è§„则与任何软件包都ä¸åŒ¹é…。 owner.settings.cleanuprules.enabled=å¯ç”¨ -owner.settings.cleanuprules.pattern_full_match=应用规则到完整软件包åç§° +owner.settings.cleanuprules.pattern_full_match=应用表达å¼åˆ°å®Œæ•´è½¯ä»¶åŒ…åç§° owner.settings.cleanuprules.keep.title=与这些规则相匹é…的版本å³ä½¿ä¸Žä¸‹é¢çš„åˆ é™¤è§„åˆ™ç›¸åŒ¹é…,也将予以ä¿ç•™ã€‚ owner.settings.cleanuprules.keep.count=ä¿ç•™æœ€æ–°çš„ owner.settings.cleanuprules.keep.count.1=æ¯ä¸ªè½¯ä»¶åŒ…1个版本 @@ -3719,30 +3751,30 @@ owner.settings.chef.keypair.description=需è¦å¯†é’¥å¯¹æ‰èƒ½å‘ Chef 注册ä¸å [secrets] secrets=密钥 -description=Secrets å°†è¢«ä¼ ç»™ç‰¹å®šçš„ Actions,其它情况将ä¸èƒ½è¯»å– +description=å¯†é’¥å°†è¢«ä¼ ç»™ç‰¹å®šçš„å·¥ä½œæµï¼Œå…¶å®ƒæƒ…å†µæ— æ³•è¯»å–。 none=还没有密钥。 ; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=组织æè¿° -creation.name_placeholder=ä¸åŒºåˆ†å¤§å°å†™ï¼Œå—æ¯æ•°å—或下划线ä¸èƒ½ä»¥GITEA_ 或 GITHUB_ 开头。 -creation.value_placeholder=输入任何内容,开头和结尾的空白都会被çœç•¥ -creation.description_placeholder=è¾“å…¥ç®€çŸæè¿°(å¯é€‰)。 +creation.name_placeholder=ä¸åŒºåˆ†å¤§å°å†™ï¼Œä»…é™å—æ¯æ•°å—或下划线且ä¸èƒ½ä»¥ GITEA_ 或 GITHUB_ 开头 +creation.value_placeholder=输入任何内容,开头和结尾的空白将会被忽略 +creation.description_placeholder=è¾“å…¥ç®€çŸæè¿°ï¼ˆå¯é€‰ï¼‰ -save_success=密钥 '%s' ä¿å˜æˆåŠŸã€‚ +save_success=密钥「%sã€ä¿å˜æˆåŠŸã€‚ save_failed=密钥ä¿å˜å¤±è´¥ã€‚ add_secret=æ·»åŠ å¯†é’¥ edit_secret=编辑密钥 deletion=åˆ é™¤å¯†é’¥ -deletion.description=åˆ é™¤å¯†é’¥æ˜¯æ°¸ä¹…æ€§çš„ï¼Œæ— æ³•æ’¤æ¶ˆã€‚ç»§ç»å—? -deletion.success=æ¤Secretå·²è¢«åˆ é™¤ã€‚ +deletion.description=åˆ é™¤å¯†é’¥æ˜¯æ°¸ä¹…æ€§çš„ä¸”æ— æ³•æ’¤æ¶ˆã€‚ç»§ç»ï¼Ÿ +deletion.success=æ¤å¯†é’¥å·²åˆ 除。 deletion.failed=åˆ é™¤å¯†é’¥å¤±è´¥ã€‚ management=å¯†é’¥ç®¡ç† [actions] -actions=Actions +actions=å·¥ä½œæµ -unit.desc=管ç†Actions +unit.desc=管ç†å·¥ä½œæµ status.unknown=未知 status.waiting=ç‰å¾…ä¸ @@ -3753,9 +3785,9 @@ status.cancelled=已喿¶ˆ status.skipped=已忽略 status.blocked=é˜»å¡žä¸ -runners=Runners -runners.runner_manage_panel=Runnersç®¡ç† -runners.new=创建 Runner +runners=è¿è¡Œå™¨ +runners.runner_manage_panel=è¿è¡Œå™¨ç®¡ç† +runners.new=创建新è¿è¡Œå™¨ runners.new_notice=如何å¯åŠ¨ä¸€ä¸ªè¿è¡Œå™¨ runners.status=çŠ¶æ€ runners.id=ID @@ -3764,9 +3796,9 @@ runners.owner_type=类型 runners.description=æè¿° runners.labels=æ ‡ç¾ runners.last_online=上次在线时间 -runners.runner_title=Runner -runners.task_list=最近在æ¤runner上的任务 -runners.task_list.no_tasks=还没有任务。 +runners.runner_title=è¿è¡Œå™¨ +runners.task_list=最近在æ¤è¿è¡Œå™¨ä¸Šçš„任务 +runners.task_list.no_tasks=ç›®å‰è¿˜æ²¡æœ‰ä»»åŠ¡ã€‚ runners.task_list.run=执行 runners.task_list.status=çŠ¶æ€ runners.task_list.repository=仓库 @@ -3775,13 +3807,13 @@ runners.task_list.done_at=完æˆäºŽ runners.edit_runner=编辑è¿è¡Œå™¨ runners.update_runner=更新更改 runners.update_runner_success=è¿è¡Œå™¨æ›´æ–°æˆåŠŸ -runners.update_runner_failed=æ›´æ–°è¿è¡Œå™¨å¤±è´¥ -runners.delete_runner=åˆ é™¤è¿è¡Œå™¨ +runners.update_runner_failed=è¿è¡Œå™¨æ›´æ–°å¤±è´¥ +runners.delete_runner=åˆ é™¤æ¤è¿è¡Œå™¨ runners.delete_runner_success=è¿è¡Œå™¨åˆ 除æˆåŠŸ -runners.delete_runner_failed=åˆ é™¤è¿è¡Œå™¨å¤±è´¥ +runners.delete_runner_failed=è¿è¡Œå™¨åˆ 除失败 runners.delete_runner_header=确认è¦åˆ 除æ¤è¿è¡Œå™¨ runners.delete_runner_notice=如果一个任务æ£åœ¨è¿è¡Œåœ¨æ¤è¿è¡Œå™¨ä¸Šï¼Œå®ƒå°†è¢«ç»ˆæ¢å¹¶æ ‡è®°ä¸ºå¤±è´¥ã€‚它å¯èƒ½ä¼šæ‰“æ–æ£åœ¨æž„建的工作æµã€‚ -runners.none=æ— å¯ç”¨çš„ Runner +runners.none=æ— å¯ç”¨è¿è¡Œå™¨ runners.status.unspecified=未知 runners.status.idle=空闲 runners.status.active=å¯ç”¨ @@ -3795,8 +3827,8 @@ runs.all_workflows=æ‰€æœ‰å·¥ä½œæµ runs.commit=æäº¤ runs.scheduled=已计划的 runs.pushed_by=推é€è€… -runs.invalid_workflow_helper=工作æµé…ç½®æ–‡ä»¶æ— æ•ˆã€‚è¯·æ£€æŸ¥æ‚¨çš„é…置文件: %s -runs.no_matching_online_runner_helper=æ²¡æœ‰åŒ¹é…æ ‡ç¾çš„在线 runner: %s +runs.invalid_workflow_helper=工作æµé…ç½®æ–‡ä»¶æ— æ•ˆã€‚è¯·æ£€æŸ¥æ‚¨çš„é…置文件:%s +runs.no_matching_online_runner_helper=æ²¡æœ‰åŒ¹é… %s æ ‡ç¾çš„在线è¿è¡Œå™¨ runs.no_job_without_needs=工作æµå¿…须包å«è‡³å°‘一个没有ä¾èµ–关系的作业。 runs.no_job=工作æµå¿…须包å«è‡³å°‘一个作业 runs.actor=æ“作者 @@ -3805,23 +3837,28 @@ runs.actors_no_select=所有æ“作者 runs.status_no_select=æ‰€æœ‰çŠ¶æ€ runs.no_results=没有匹é…的结果。 runs.no_workflows=ç›®å‰è¿˜æ²¡æœ‰å·¥ä½œæµã€‚ -runs.no_workflows.quick_start=ä¸çŸ¥é“如何使用 Gitea Actionså—?请查看 <a target="_blank" rel="noopener noreferrer" href="%s">快速å¯åŠ¨æŒ‡å—</a>。 -runs.no_workflows.documentation=关于Gitea Actions的更多信æ¯ï¼Œè¯·å‚阅 <a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。 +runs.no_workflows.quick_start=ä¸çŸ¥é“如何使用 Gitea 工作æµå—?请查看<a target="_blank" rel="noopener noreferrer" href="%s">快速开始指å—</a>。 +runs.no_workflows.documentation=关于 Gitea 工作æµçš„æ›´å¤šä¿¡æ¯ï¼Œè¯·å‚阅<a target="_blank" rel="noopener noreferrer" href="%s">文档</a>。 runs.no_runs=工作æµå°šæœªè¿è¡Œè¿‡ã€‚ -runs.empty_commit_message=(空白的æäº¤æ¶ˆæ¯) -runs.expire_log_message=旧的日志已被清除 +runs.empty_commit_message=(空白的æäº¤æ¶ˆæ¯ï¼‰ +runs.expire_log_message=旧的日志已清除。 +runs.delete=åˆ é™¤å·¥ä½œæµè¿è¡Œ +runs.cancel=å–æ¶ˆå·¥ä½œæµè¿è¡Œ +runs.delete.description=æ‚¨ç¡®å®šè¦æ°¸ä¹…åˆ é™¤æ¤å·¥ä½œæµè¿è¡Œå—ï¼Ÿæ¤æ“ä½œæ— æ³•æ’¤æ¶ˆã€‚ +runs.not_done=æ¤å·¥ä½œæµè¿è¡Œå°šæœªå®Œæˆã€‚ +runs.view_workflow_file=æŸ¥çœ‹å·¥ä½œæµæ–‡ä»¶ workflow.disable=ç¦ç”¨å·¥ä½œæµ -workflow.disable_success=å·¥ä½œæµ '%s' å·²æˆåŠŸç¦ç”¨ã€‚ +workflow.disable_success=工作æµã€Œ%sã€å·²æˆåŠŸç¦ç”¨ã€‚ workflow.enable=å¯ç”¨å·¥ä½œæµ -workflow.enable_success=å·¥ä½œæµ '%s' å·²æˆåŠŸå¯ç”¨ã€‚ +workflow.enable_success=工作æµã€Œ%sã€å·²æˆåŠŸå¯ç”¨ã€‚ workflow.disabled=工作æµå·²ç¦ç”¨ã€‚ workflow.run=è¿è¡Œå·¥ä½œæµ -workflow.not_found=å·¥ä½œæµ %s 未找到。 -workflow.run_success=å·¥ä½œæµ %s å·²æˆåŠŸè¿è¡Œã€‚ +workflow.not_found=工作æµã€Œ%sã€æœªæ‰¾åˆ°ã€‚ +workflow.run_success=工作æµã€Œ%sã€å·²æˆåŠŸè¿è¡Œã€‚ workflow.from_ref=使用工作æµä»Ž -workflow.has_workflow_dispatch=æ¤ Workflow 有一个 Workflow_dispatch 事件触å‘器。 -workflow.has_no_workflow_dispatch=å·¥ä½œæµ %s 没有 workflow_dispatch 事件的触å‘器。 +workflow.has_workflow_dispatch=æ¤å·¥ä½œæµæœ‰ä¸€ä¸ª workflow_dispatch 事件触å‘器。 +workflow.has_no_workflow_dispatch=工作æµã€Œ%sã€æ²¡æœ‰ workflow_dispatch 事件触å‘器。 need_approval_desc=该工作æµç”±æ´¾ç”Ÿä»“库的åˆå¹¶è¯·æ±‚所触å‘ï¼Œéœ€è¦æ‰¹å‡†æ–¹å¯è¿è¡Œã€‚ @@ -3831,15 +3868,15 @@ variables.creation=æ·»åŠ å˜é‡ variables.none=ç›®å‰è¿˜æ²¡æœ‰å˜é‡ã€‚ variables.deletion=åˆ é™¤å˜é‡ variables.deletion.description=åˆ é™¤å˜é‡æ˜¯æ°¸ä¹…æ€§çš„ï¼Œæ— æ³•æ’¤æ¶ˆã€‚ç»§ç»å—? -variables.description=å˜é‡å°†è¢«ä¼ 给特定的 Actions,其它情况将ä¸èƒ½è¯»å– +variables.description=å˜é‡å°†è¢«ä¼ 给特定的工作æµï¼Œå…¶å®ƒæƒ…å†µä¸‹æ— æ³•è¯»å–。 variables.id_not_exist=ID为 %d çš„å˜é‡ä¸å˜åœ¨ã€‚ variables.edit=编辑å˜é‡ -variables.deletion.failed=åˆ é™¤å˜é‡å¤±è´¥ã€‚ -variables.deletion.success=å˜é‡å·²è¢«åˆ 除。 -variables.creation.failed=æ·»åŠ å˜é‡å¤±è´¥ã€‚ -variables.creation.success=å˜é‡ “%sâ€ æ·»åŠ æˆåŠŸã€‚ -variables.update.failed=编辑å˜é‡å¤±è´¥ã€‚ -variables.update.success=该å˜é‡å·²è¢«ç¼–辑。 +variables.deletion.failed=å˜é‡åˆ 除失败。 +variables.deletion.success=å˜é‡å·²åˆ 除。 +variables.creation.failed=å˜é‡æ·»åŠ å¤±è´¥ã€‚ +variables.creation.success=å˜é‡ã€Œ%sã€æ·»åŠ æˆåŠŸã€‚ +variables.update.failed=å˜é‡ç¼–辑失败。 +variables.update.success=å˜é‡å·²ç¼–辑。 logs.always_auto_scroll=总是自动滚动日志 logs.always_expand_running=总是展开è¿è¡Œæ—¥å¿— diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 2874da3170..77a5c0abec 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -374,6 +374,7 @@ editor.create_new_branch=建立 <strong>新的分支</strong> ç‚ºæ¤æäº¤å’Œé–‹ editor.cancel=å–æ¶ˆ editor.no_changes_to_show=沒有å¯ä»¥é¡¯ç¤ºçš„變更。 + commits.commits=次程å¼ç¢¼æäº¤ commits.author=作者 commits.message=備註 @@ -656,10 +657,11 @@ settings.visibility.private_shortname=ç§æœ‰åº« settings.update_settings=更新組織è¨å®š settings.update_setting_success=組織è¨å®šå·²æ›´æ–°ã€‚ + + settings.delete=刪除組織 settings.delete_account=刪除當å‰çµ„ç¹” settings.confirm_delete_account=確èªåˆªé™¤çµ„ç¹” -settings.delete_org_title=刪除組織 settings.hooks_desc=新增 webhooks 將觸發在這個組織下 <strong>全部的儲å˜åº«</strong> 。 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index a52e147415..85c796a3e4 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1318,7 +1318,6 @@ editor.update=æ›´æ–° %s editor.delete=刪除 %s editor.patch=套用 Patch editor.patching=æ£åœ¨ Patch: -editor.fail_to_apply_patch=無法套用 Patch「%s〠editor.new_patch=新增 Patch editor.commit_message_desc=(é¸ç”¨) åŠ å…¥è©³ç´°èªªæ˜Ž... editor.signoff_desc=在æäº¤è¨Šæ¯åº•éƒ¨åŠ å…¥æäº¤è€…的「Signed-off-byã€è³‡è¨Šã€‚ @@ -1336,8 +1335,6 @@ editor.branch_already_exists=æ¤å„²å˜åº«å·²æœ‰å為「%sã€çš„分支。 editor.directory_is_a_file=目錄å稱「%sã€å·²è¢«æ¤å„²å˜åº«çš„æª”案使用。 editor.file_is_a_symlink=`"%s" 是一個符號連çµã€‚符號連çµç„¡æ³•在網é 編輯器ä¸ç·¨è¼¯` editor.filename_is_a_directory=檔å「%sã€å·²è¢«æ¤å„²å˜åº«çš„目錄å稱使用。 -editor.file_editing_no_longer_exists=æ£è¦ç·¨è¼¯çš„æª”案「%sã€å·²ä¸å˜åœ¨æ¤å„²å˜åº«ä¸ã€‚ -editor.file_deleting_no_longer_exists=æ£è¦åˆªé™¤çš„æª”案「%sã€å·²ä¸å˜åœ¨æ¤å„²å˜åº«ä¸ã€‚ editor.file_changed_while_editing=檔案內容在您編輯的途ä¸å·²è¢«è®Šæ›´ã€‚<a target="_blank" rel="noopener noreferrer" href="%s">按一下æ¤è™•</a>查看更動的地方或<strong>冿¬¡æäº¤</strong>以覆蓋這些變更。 editor.file_already_exists=æ¤å„²å˜åº«å·²æœ‰å為「%sã€çš„æª”案。 editor.commit_id_not_matching=æäº¤ ID 與您開始編輯時的 ID ä¸åŒ¹é…。請æäº¤åˆ°ä¸€å€‹è£œä¸åˆ†æ”¯ç„¶å¾Œåˆä½µã€‚ @@ -1345,8 +1342,6 @@ editor.push_out_of_date=推é€ä¼¼ä¹Žå·²éŽæ™‚。 editor.commit_empty_file_header=æäº¤ç©ºç™½æª”案 editor.commit_empty_file_text=ä½ æº–å‚™æäº¤çš„æª”案是空白的,是å¦ç¹¼çºŒï¼Ÿ editor.no_changes_to_show=沒有å¯ä»¥é¡¯ç¤ºçš„變更。 -editor.fail_to_update_file=æ›´æ–°/建立檔案「%sã€å¤±æ•—。 -editor.fail_to_update_file_summary=錯誤訊æ¯: editor.push_rejected_no_message=該變更被伺æœå™¨æ‹’絕但未æä¾›å…¶ä»–資訊。請檢查 Git Hook。 editor.push_rejected=該變更被伺æœå™¨æ‹’絕。請檢查 Git Hook。 editor.push_rejected_summary=完整的拒絕訊æ¯: @@ -1361,6 +1356,7 @@ editor.require_signed_commit=分支僅接å—經簽署的æäº¤ editor.cherry_pick=Cherry-pick %s 到: editor.revert=還原 %s 到: + commits.desc=ç€è¦½åŽŸå§‹ç¢¼ä¿®æ”¹æ·ç¨‹ã€‚ commits.commits=次程å¼ç¢¼æäº¤ commits.no_commits=沒有共åŒçš„æäº¤ã€‚「%sã€å’Œã€Œ%sã€çš„æ·å²å®Œå…¨ä¸åŒã€‚ @@ -1905,7 +1901,6 @@ pulls.cmd_instruction_checkout_title=檢出 pulls.cmd_instruction_checkout_desc=從您的專案儲å˜åº«ä¸ï¼Œæª¢å‡ºä¸€å€‹æ–°åˆ†æ”¯ä¸¦æ¸¬è©¦è®Šæ›´ã€‚ pulls.cmd_instruction_merge_title=åˆä½µ pulls.cmd_instruction_merge_desc=åˆä½µè®Šæ›´ä¸¦åœ¨ Gitea 上更新。 -pulls.cmd_instruction_merge_warning=è¦å‘Šï¼šæ¤æ“作無法åˆä½µåˆä½µè«‹æ±‚ï¼Œå› ç‚ºæœªå•Ÿç”¨ã€Œè‡ªå‹•æª¢æ¸¬æ‰‹å‹•åˆä½µã€ pulls.clear_merge_message=清除åˆä½µè¨Šæ¯ pulls.clear_merge_message_hint=清除åˆä½µè¨Šæ¯å°‡åƒ…移除æäº¤è¨Šæ¯å…§å®¹ï¼Œç•™ä¸‹ç”¢ç”Ÿçš„ git çµå°¾ï¼Œå¦‚「Co-Authored-By …ã€ã€‚ @@ -2308,7 +2303,6 @@ settings.event_repository=儲å˜åº« settings.event_repository_desc=建立或刪除儲å˜åº«ã€‚ settings.event_header_issue=å•題事件 settings.event_issues=å•題 -settings.event_issues_desc=建立ã€ç·¨è¼¯ã€é—œé–‰åŠé‡æ–°é–‹æ”¾å•題。 settings.event_issue_assign=指派å•題 settings.event_issue_assign_desc=æŒ‡æ´¾æˆ–å–æ¶ˆæŒ‡æ´¾å•題。 settings.event_issue_label=標籤 @@ -2319,7 +2313,6 @@ settings.event_issue_comment=å•題留言 settings.event_issue_comment_desc=已經建立ã€ç·¨è¼¯æˆ–刪除的å•題留言。 settings.event_header_pull_request=åˆä½µè«‹æ±‚事件 settings.event_pull_request=åˆä½µè«‹æ±‚ -settings.event_pull_request_desc=建立ã€ç·¨è¼¯ã€é—œé–‰åŠé‡æ–°é–‹æ”¾åˆä½µè«‹æ±‚。 settings.event_pull_request_assign=指派åˆä½µè«‹æ±‚ settings.event_pull_request_assign_desc=æŒ‡æ´¾æˆ–å–æ¶ˆæŒ‡æ´¾åˆä½µè«‹æ±‚。 settings.event_pull_request_label=åˆä½µè«‹æ±‚標籤 @@ -2756,15 +2749,13 @@ settings.visibility.private_shortname=ç§æœ‰ settings.update_settings=æ›´æ–°è¨å®š settings.update_setting_success=組織è¨å®šå·²æ›´æ–°ã€‚ -settings.change_orgname_prompt=注æ„:更改組織åç¨±å°‡åŒæ™‚更改組織的 URL 並釋放舊å稱。 -settings.change_orgname_redirect_prompt=舊的åç¨±è¢«é ˜ç”¨å‰ï¼Œæœƒé‡æ–°å°Žå‘æ–°å稱。 + + settings.update_avatar_success=已更新組織的大é 貼。 settings.delete=刪除組織 settings.delete_account=刪除這個組織 settings.delete_prompt=該組織將被永久刪除。æ¤å‹•作<strong>ä¸å¯</strong>é‚„åŽŸï¼ settings.confirm_delete_account=確èªåˆªé™¤çµ„ç¹” -settings.delete_org_title=刪除組織 -settings.delete_org_desc=å³å°‡æ°¸ä¹…刪除這個組織,是å¦ç¹¼çºŒï¼Ÿ settings.hooks_desc=æ¤çµ„織下的<strong>所有å˜å„²åº«</strong>éƒ½æœƒè§¸ç™¼åœ¨æ¤æ–°å¢žçš„ Webhook。 settings.labels_desc=在æ¤è™•新增的標籤å¯ç”¨æ–¼æ¤çµ„織下的<strong>所有儲å˜åº«</strong>。 diff --git a/package-lock.json b/package-lock.json index e61fe3472d..8361199086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,42 +11,42 @@ "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", "@github/relative-time-element": "4.4.8", - "@github/text-expander-element": "2.9.1", + "@github/text-expander-element": "2.9.2", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.15.1", + "@primer/octicons": "19.15.2", "@silverwind/vue3-calendar-heatmap": "2.0.6", "add-asset-webpack-plugin": "3.0.0", - "ansi_up": "6.0.5", - "asciinema-player": "3.9.0", + "ansi_up": "6.0.6", + "asciinema-player": "3.10.0", "chart.js": "4.4.9", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", - "clippie": "4.1.6", + "clippie": "4.1.7", "cropperjs": "1.6.2", "css-loader": "7.1.2", "dayjs": "1.11.13", "dropzone": "6.0.0-beta.2", "easymde": "2.20.0", "esbuild-loader": "4.3.0", - "escape-goat": "4.0.0", "fast-glob": "3.3.3", - "htmx.org": "2.0.4", + "htmx.org": "2.0.6", "idiomorph": "0.7.3", "jquery": "3.7.1", "katex": "0.16.22", "license-checker-webpack-plugin": "0.2.1", "mermaid": "11.6.0", "mini-css-extract-plugin": "2.9.2", - "minimatch": "10.0.1", + "minimatch": "10.0.2", "monaco-editor": "0.52.2", "monaco-editor-webpack-plugin": "7.1.0", + "online-3d-viewer": "0.16.0", "pdfobject": "2.3.1", "perfect-debounce": "1.0.0", - "postcss": "8.5.3", + "postcss": "8.5.5", "postcss-loader": "8.1.1", - "postcss-nesting": "13.0.1", + "postcss-nesting": "13.0.2", "sortablejs": "1.15.6", - "swagger-ui-dist": "5.21.0", + "swagger-ui-dist": "5.24.1", "tailwindcss": "3.4.17", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", @@ -54,19 +54,19 @@ "toastify-js": "1.12.0", "tributejs": "5.1.3", "typescript": "5.8.3", - "uint8-to-base64": "0.2.0", + "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vue": "3.5.13", + "vue": "3.5.16", "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.2", "vue-loader": "17.4.2", - "webpack": "5.99.8", + "webpack": "5.99.9", "webpack-cli": "6.0.1", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", - "@playwright/test": "1.52.0", + "@playwright/test": "1.53.0", "@stoplight/spectral-cli": "6.15.0", "@stylistic/eslint-plugin-js": "3.1.0", "@stylistic/stylelint-plugin": "3.1.2", @@ -80,30 +80,30 @@ "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.4", - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@vitejs/plugin-vue": "5.2.3", - "@vitest/eslint-plugin": "1.1.44", + "@typescript-eslint/eslint-plugin": "8.34.0", + "@typescript-eslint/parser": "8.34.0", + "@vitejs/plugin-vue": "5.2.4", + "@vitest/eslint-plugin": "1.2.2", "eslint": "8.57.0", - "eslint-import-resolver-typescript": "4.3.4", + "eslint-import-resolver-typescript": "4.4.3", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.0.2", - "eslint-plugin-import-x": "4.11.0", + "eslint-plugin-import-x": "4.15.2", "eslint-plugin-no-jquery": "3.1.1", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-playwright": "2.2.0", - "eslint-plugin-regexp": "2.7.0", + "eslint-plugin-regexp": "2.9.0", "eslint-plugin-sonarjs": "3.0.2", "eslint-plugin-unicorn": "56.0.1", - "eslint-plugin-vue": "10.1.0", - "eslint-plugin-vue-scoped-css": "2.9.0", + "eslint-plugin-vue": "10.2.0", + "eslint-plugin-vue-scoped-css": "2.10.0", "eslint-plugin-wc": "3.0.1", - "happy-dom": "17.4.6", - "markdownlint-cli": "0.44.0", - "material-icon-theme": "5.22.0", + "happy-dom": "18.0.1", + "markdownlint-cli": "0.45.0", + "material-icon-theme": "5.23.0", "nolyfill": "1.0.44", "postcss-html": "1.8.0", - "stylelint": "16.19.1", + "stylelint": "16.20.0", "stylelint-config-recommended": "16.0.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.11", @@ -113,11 +113,11 @@ "type-fest": "4.41.0", "updates": "16.4.2", "vite-string-plugin": "1.4.4", - "vitest": "3.1.3", + "vitest": "3.2.3", "vue-tsc": "2.2.10" }, "engines": { - "node": ">= 18.0.0" + "node": ">= 20.0.0" } }, "node_modules/@alloc/quick-lru": { @@ -203,12 +203,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -218,18 +218,18 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -431,9 +431,9 @@ } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, "funding": [ { @@ -450,13 +450,13 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, "funding": [ { @@ -552,9 +552,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], @@ -568,9 +568,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], @@ -584,9 +584,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], @@ -600,9 +600,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], @@ -616,9 +616,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], @@ -632,9 +632,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], @@ -648,9 +648,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], @@ -664,9 +664,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], @@ -680,9 +680,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], @@ -696,9 +696,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], @@ -712,9 +712,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], @@ -728,9 +728,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], @@ -744,9 +744,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], @@ -760,9 +760,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], @@ -776,9 +776,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], @@ -792,9 +792,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], @@ -808,9 +808,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], @@ -824,9 +824,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", "cpu": [ "arm64" ], @@ -840,9 +840,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], @@ -856,9 +856,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", "cpu": [ "arm64" ], @@ -872,9 +872,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], @@ -888,9 +888,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], @@ -904,9 +904,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], @@ -920,9 +920,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], @@ -936,9 +936,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], @@ -1054,10 +1054,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1152,13 +1159,13 @@ "license": "MIT" }, "node_modules/@github/text-expander-element": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.9.1.tgz", - "integrity": "sha512-T/pCDjB/diMaarmcdc01hP026v0b9lidluyZD5z/EPOExXRdNDqb11kOXevoMZY42WiI3Yhoqsj3nbM+HthLgQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.9.2.tgz", + "integrity": "sha512-XY8EUMqM4GAloNxXNA1Py1ny+engWwYntbgsnpstQN4piaTI9rIlfYldyd0nnPXhxjGCVqHPmP6yg17Q0/n9Vg==", "license": "MIT", "dependencies": { "@github/combobox-nav": "^2.0.2", - "dom-input-range": "^1.2.0" + "dom-input-range": "^2.0.0" } }, "node_modules/@humanwhocodes/config-array": { @@ -1177,10 +1184,17 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1546,15 +1560,15 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz", - "integrity": "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.11.tgz", + "integrity": "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.4.0", - "@emnapi/runtime": "^1.4.0", + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, @@ -1611,9 +1625,9 @@ } }, "node_modules/@pkgr/core": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", - "integrity": "sha512-ROFF39F6ZrnzSUEmQQZUar0Jt4xVoP9WnDRdWwF4NNcXs3xBTLgBUDoOwW141y1jP+S8nahIbdxbFC7IShw9Iw==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", "dev": true, "license": "MIT", "engines": { @@ -1624,13 +1638,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", - "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz", + "integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.52.0" + "playwright": "1.53.0" }, "bin": { "playwright": "cli.js" @@ -1650,9 +1664,9 @@ } }, "node_modules/@primer/octicons": { - "version": "19.15.1", - "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.15.1.tgz", - "integrity": "sha512-2cFLIrfbNvfeAPxmJTr/k7/b5QvIxqM7MeroGrXojDodA2yB1nXyt91Xhu9+2e0CL5ZAe3Vd2/fEf9N9EUiX9A==", + "version": "19.15.2", + "resolved": "https://registry.npmjs.org/@primer/octicons/-/octicons-19.15.2.tgz", + "integrity": "sha512-B80KFaujnmYTB339nPsQm3epqVHz4NaaoD6PoOvPSEsJ5vdfCpM94NX99eq2La84aBm44BKE0uVTzQ8qjk34Kg==", "license": "MIT", "dependencies": { "object-assign": "^4.1.1" @@ -1706,9 +1720,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", + "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", "cpu": [ "arm" ], @@ -1720,9 +1734,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", + "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", "cpu": [ "arm64" ], @@ -1734,9 +1748,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", + "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", "cpu": [ "arm64" ], @@ -1748,9 +1762,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", + "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", "cpu": [ "x64" ], @@ -1762,9 +1776,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", + "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", "cpu": [ "arm64" ], @@ -1776,9 +1790,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", + "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", "cpu": [ "x64" ], @@ -1790,9 +1804,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", + "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", "cpu": [ "arm" ], @@ -1804,9 +1818,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", + "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", "cpu": [ "arm" ], @@ -1818,9 +1832,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", + "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", "cpu": [ "arm64" ], @@ -1832,9 +1846,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", + "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", "cpu": [ "arm64" ], @@ -1846,9 +1860,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", + "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", "cpu": [ "loong64" ], @@ -1860,9 +1874,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", + "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", "cpu": [ "ppc64" ], @@ -1874,9 +1888,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", + "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", "cpu": [ "riscv64" ], @@ -1888,9 +1902,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", + "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", "cpu": [ "riscv64" ], @@ -1902,9 +1916,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", + "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", "cpu": [ "s390x" ], @@ -1916,9 +1930,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", + "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", "cpu": [ "x64" ], @@ -1930,9 +1944,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", + "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", "cpu": [ "x64" ], @@ -1944,9 +1958,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", + "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", "cpu": [ "arm64" ], @@ -1958,9 +1972,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", + "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", "cpu": [ "ia32" ], @@ -1972,9 +1986,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", + "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", "cpu": [ "x64" ], @@ -2012,6 +2026,16 @@ "vue": "^3.2.29" } }, + "node_modules/@simonwep/pickr": { + "version": "1.9.0", + "resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.9.0.tgz", + "integrity": "sha512-oEYvv15PyfZzjoAzvXYt3UyNGwzsrpFxLaZKzkOSd0WYBVwLd19iJerePDONxC1iF6+DpcswPdLIM2KzCJuYFg==", + "license": "MIT", + "dependencies": { + "core-js": "3.32.2", + "nanopop": "2.3.0" + } + }, "node_modules/@stoplight/better-ajv-errors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@stoplight/better-ajv-errors/-/better-ajv-errors-1.0.3.tgz", @@ -2221,10 +2245,17 @@ "node": "^12.20 || >=14.13" } }, + "node_modules/@stoplight/spectral-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/@stoplight/spectral-core/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2597,10 +2628,20 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/codemirror": { - "version": "5.60.15", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz", - "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==", + "version": "5.60.16", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.16.tgz", + "integrity": "sha512-V/yHdamffSS075jit+fDxaOAmdP2liok8NSNJnAZfDJErzOheuygHZEhAJrfmk5TEyM32MhkZjwo/idX791yxw==", "license": "MIT", "dependencies": { "@types/tern": "*" @@ -2869,6 +2910,13 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/dropzone": { "version": "5.7.9", "resolved": "https://registry.npmjs.org/@types/dropzone/-/dropzone-5.7.9.tgz", @@ -2988,12 +3036,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.14.tgz", - "integrity": "sha512-BL1eyu/XWsFGTtDWOYULQEs4KR0qdtYfCxYAUYRoB7JP7h9ETYLgQTww6kH8Sj2C0pFGgrpM0XKv6/kbIzYJ1g==", + "version": "24.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", + "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/normalize-package-data": { @@ -3150,20 +3198,27 @@ "node": ">= 8" } }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.0.tgz", - "integrity": "sha512-/jU9ettcntkBFmWUzzGgsClEi2ZFiikMX5eEQsmxIAWMOn4H3D4rvHssstmAHGVvrYnaMqdWWWg0b5M6IN/MTQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", + "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/type-utils": "8.32.0", - "@typescript-eslint/utils": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/type-utils": "8.34.0", + "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, @@ -3175,22 +3230,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "@typescript-eslint/parser": "^8.34.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/@typescript-eslint/parser": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.0.tgz", - "integrity": "sha512-B2MdzyWxCE2+SqiZHAjPphft+/2x2FlO9YBx7eKE1BCb+rqBlQdhtAEhzIEdozHd55DXPmxBdpMygFJjfjjA9A==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", + "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "engines": { @@ -3205,15 +3270,37 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", + "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.34.0", + "@typescript-eslint/types": "^8.34.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.0.tgz", - "integrity": "sha512-jc/4IxGNedXkmG4mx4nJTILb6TMjL66D41vyeaPWvDUmeYQzF3lKtN15WsAeTr65ce4mPxwopPSo1yUUAWw0hQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", + "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0" + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3223,15 +3310,32 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", + "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.0.tgz", - "integrity": "sha512-t2vouuYQKEKSLtJaa5bB4jHeha2HJczQ6E5IXPDPgIty9EqcJxpr1QHQ86YyIPwDwxvUmLfP2YADQ5ZY4qddZg==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", + "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.32.0", - "@typescript-eslint/utils": "8.32.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -3248,9 +3352,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.0.tgz", - "integrity": "sha512-O5Id6tGadAZEMThM6L9HmVf5hQUXNSxLVKeGJYWNhhVseps/0LddMkp7//VDkzwJ69lPL0UmZdcZwggj9akJaA==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", + "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", "dev": true, "license": "MIT", "engines": { @@ -3262,14 +3366,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.0.tgz", - "integrity": "sha512-pU9VD7anSCOIoBFnhTGfOzlVFQIA1XXiQpH/CezqOBaDppRwTglJzCC6fUQGpfwey4T183NKhF1/mfatYmjRqQ==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", + "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/visitor-keys": "8.32.0", + "@typescript-eslint/project-service": "8.34.0", + "@typescript-eslint/tsconfig-utils": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3288,6 +3394,23 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3305,16 +3428,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.0.tgz", - "integrity": "sha512-8S9hXau6nQ/sYVtC3D6ISIDoJzS1NsCK+gluVhLN2YkBPX+/1wkwyUiDKnxRh15579WoOIyVWnoyIf3yGI9REw==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.32.0", - "@typescript-eslint/types": "8.32.0", - "@typescript-eslint/typescript-estree": "8.32.0" + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3329,13 +3452,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.32.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.0.tgz", - "integrity": "sha512-1rYQTCLFFzOI5Nl0c8LUpJT8HxpwVRn9E4CkMsYfuN6ctmQqExjSTzzSk0Tz2apmXy7WU6/6fyaZVVA/thPN+w==", + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", + "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.32.0", + "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -3353,10 +3476,38 @@ "dev": true, "license": "ISC" }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.9.0.tgz", + "integrity": "sha512-h1T2c2Di49ekF2TE8ZCoJkb+jwETKUIPDJ/nO3tJBKlLFPu+fyd93f0rGP/BvArKx2k2HlRM4kqkNarj3dvZlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.9.0.tgz", + "integrity": "sha512-sG1NHtgXtX8owEkJ11yn34vt0Xqzi3k9TJ8zppDmyG8GZV4kVWw44FHwKwHeEFl07uKPeC4ZoyuQaGh5ruJYPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", - "integrity": "sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.9.0.tgz", + "integrity": "sha512-nJ9z47kfFnCxN1z/oYZS7HSNsFh43y2asePzTEZpEvK7kGyuShSl3RRXnm/1QaqFL+iP+BjMwuB+DYUymOkA5A==", "cpu": [ "arm64" ], @@ -3368,9 +3519,9 @@ ] }, "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.2.tgz", - "integrity": "sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.9.0.tgz", + "integrity": "sha512-TK+UA1TTa0qS53rjWn7cVlEKVGz2B6JYe0C++TdQjvWYIyx83ruwh0wd4LRxYBM5HeuAzXcylA9BH2trARXJTw==", "cpu": [ "x64" ], @@ -3382,9 +3533,9 @@ ] }, "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.2.tgz", - "integrity": "sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.9.0.tgz", + "integrity": "sha512-6uZwzMRFcD7CcCd0vz3Hp+9qIL2jseE/bx3ZjaLwn8t714nYGwiE84WpaMCYjU+IQET8Vu/+BNAGtYD7BG/0yA==", "cpu": [ "x64" ], @@ -3396,9 +3547,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.2.tgz", - "integrity": "sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.9.0.tgz", + "integrity": "sha512-bPUBksQfrgcfv2+mm+AZinaKq8LCFvt5PThYqRotqSuuZK1TVKkhbVMS/jvSRfYl7jr3AoZLYbDkItxgqMKRkg==", "cpu": [ "arm" ], @@ -3410,9 +3561,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.2.tgz", - "integrity": "sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.9.0.tgz", + "integrity": "sha512-uT6E7UBIrTdCsFQ+y0tQd3g5oudmrS/hds5pbU3h4s2t/1vsGWbbSKhBSCD9mcqaqkBwoqlECpUrRJCmldl8PA==", "cpu": [ "arm" ], @@ -3424,9 +3575,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.2.tgz", - "integrity": "sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.9.0.tgz", + "integrity": "sha512-vdqBh911wc5awE2bX2zx3eflbyv8U9xbE/jVKAm425eRoOVv/VseGZsqi3A3SykckSpF4wSROkbQPvbQFn8EsA==", "cpu": [ "arm64" ], @@ -3438,9 +3589,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.2.tgz", - "integrity": "sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.9.0.tgz", + "integrity": "sha512-/8JFZ/SnuDr1lLEVsxsuVwrsGquTvT51RZGvyDB/dOK3oYK2UqeXzgeyq6Otp8FZXQcEYqJwxb9v+gtdXn03eQ==", "cpu": [ "arm64" ], @@ -3452,9 +3603,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.2.tgz", - "integrity": "sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.9.0.tgz", + "integrity": "sha512-FkJjybtrl+rajTw4loI3L6YqSOpeZfDls4SstL/5lsP2bka9TiHUjgMBjygeZEis1oC8LfJTS8FSgpKPaQx2tQ==", "cpu": [ "ppc64" ], @@ -3466,9 +3617,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.2.tgz", - "integrity": "sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.9.0.tgz", + "integrity": "sha512-w/NZfHNeDusbqSZ8r/hp8iL4S39h4+vQMc9/vvzuIKMWKppyUGKm3IST0Qv0aOZ1rzIbl9SrDeIqK86ZpUK37w==", "cpu": [ "riscv64" ], @@ -3480,9 +3631,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.2.tgz", - "integrity": "sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.9.0.tgz", + "integrity": "sha512-bEPBosut8/8KQbUixPry8zg/fOzVOWyvwzOfz0C0Rw6dp+wIBseyiHKjkcSyZKv/98edrbMknBaMNJfA/UEdqw==", "cpu": [ "riscv64" ], @@ -3494,9 +3645,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.2.tgz", - "integrity": "sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.9.0.tgz", + "integrity": "sha512-LDtMT7moE3gK753gG4pc31AAqGUC86j3AplaFusc717EUGF9ZFJ356sdQzzZzkBk1XzMdxFyZ4f/i35NKM/lFA==", "cpu": [ "s390x" ], @@ -3508,9 +3659,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz", - "integrity": "sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.9.0.tgz", + "integrity": "sha512-WmFd5KINHIXj8o1mPaT8QRjA9HgSXhN1gl9Da4IZihARihEnOylu4co7i/yeaIpcfsI6sYs33cNZKyHYDh0lrA==", "cpu": [ "x64" ], @@ -3522,9 +3673,9 @@ ] }, "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.2.tgz", - "integrity": "sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.9.0.tgz", + "integrity": "sha512-CYuXbANW+WgzVRIl8/QvZmDaZxrqvOldOwlbUjIM4pQ46FJ0W5cinJ/Ghwa/Ng1ZPMJMk1VFdsD/XwmCGIXBWg==", "cpu": [ "x64" ], @@ -3536,9 +3687,9 @@ ] }, "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.2.tgz", - "integrity": "sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.9.0.tgz", + "integrity": "sha512-6Rp2WH0OoitMYR57Z6VE8Y6corX8C6QEMWLgOV6qXiJIeZ1F9WGXY/yQ8yDC4iTraotyLOeJ2Asea0urWj2fKQ==", "cpu": [ "wasm32" ], @@ -3546,16 +3697,16 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.9" + "@napi-rs/wasm-runtime": "^0.2.11" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.2.tgz", - "integrity": "sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.9.0.tgz", + "integrity": "sha512-rknkrTRuvujprrbPmGeHi8wYWxmNVlBoNW8+4XF2hXUnASOjmuC9FNF1tGbDiRQWn264q9U/oGtixyO3BT8adQ==", "cpu": [ "arm64" ], @@ -3567,9 +3718,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", - "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.9.0.tgz", + "integrity": "sha512-Ceymm+iBl+bgAICtgiHyMLz6hjxmLJKqBim8tDzpX61wpZOx2bPK6Gjuor7I2RiUynVjvvkoRIkrPyMwzBzF3A==", "cpu": [ "ia32" ], @@ -3581,9 +3732,9 @@ ] }, "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", - "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.9.0.tgz", + "integrity": "sha512-k59o9ZyeyS0hAlcaKFezYSH2agQeRFEB7KoQLXl3Nb3rgkqT1NY9Vwy+SqODiLmYnEjxWJVRE/yq2jFVqdIxZw==", "cpu": [ "x64" ], @@ -3595,9 +3746,9 @@ ] }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz", - "integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "dev": true, "license": "MIT", "engines": { @@ -3609,13 +3760,15 @@ } }, "node_modules/@vitest/eslint-plugin": { - "version": "1.1.44", - "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.44.tgz", - "integrity": "sha512-m4XeohMT+Dj2RZfxnbiFR+Cv5dEC0H7C6TlxRQT7GK2556solm99kxgzJp/trKrZvanZcOFyw7aABykUTfWyrg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.2.2.tgz", + "integrity": "sha512-R8NwW+VxyKqVGcMfYsUbdThQyMbtNcoeg+jJeTgMHqWdFdcS0nrODAQXhkplvWzgd7jIJ+GQeydGqFLibsxMxg==", "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.24.0" + }, "peerDependencies": { - "@typescript-eslint/utils": ">= 8.24.0", "eslint": ">= 8.57.0", "typescript": ">= 5.0.0", "vitest": "*" @@ -3630,14 +3783,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.3.tgz", + "integrity": "sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.3", + "@vitest/utils": "3.2.3", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -3646,13 +3800,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.3.tgz", + "integrity": "sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.2.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -3661,7 +3815,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "peerDependenciesMeta": { "msw": { @@ -3673,9 +3827,9 @@ } }, "node_modules/@vitest/mocker/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -3700,9 +3854,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.3.tgz", + "integrity": "sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==", "dev": true, "license": "MIT", "dependencies": { @@ -3713,27 +3867,28 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.3.tgz", + "integrity": "sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", - "pathe": "^2.0.3" + "@vitest/utils": "3.2.3", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.3.tgz", + "integrity": "sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.2.3", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -3752,26 +3907,26 @@ } }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.3.tgz", + "integrity": "sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==", "dev": true, "license": "MIT", "dependencies": { - "tinyspy": "^3.0.2" + "tinyspy": "^4.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.3.tgz", + "integrity": "sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.2.3", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -3780,72 +3935,72 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.13.tgz", - "integrity": "sha512-MnQJ7eKchJx5Oz+YdbqyFUk8BN6jasdJv31n/7r6/WwlOOv7qzvot6B66887l2ST3bUW4Mewml54euzpJWA6bg==", + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.14.tgz", + "integrity": "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.13" + "@volar/source-map": "2.4.14" } }, "node_modules/@volar/source-map": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.13.tgz", - "integrity": "sha512-l/EBcc2FkvHgz2ZxV+OZK3kMSroMr7nN3sZLF2/f6kWW66q8+tEL4giiYyFjt0BcubqJhBt6soYIrAPhg/Yr+Q==", + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.14.tgz", + "integrity": "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.13.tgz", - "integrity": "sha512-Ukz4xv84swJPupZeoFsQoeJEOm7U9pqsEnaGGgt5ni3SCTa22m8oJP5Nng3Wed7Uw5RBELdLxxORX8YhJPyOgQ==", + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.14.tgz", + "integrity": "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.13", + "@volar/language-core": "2.4.14", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", - "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.16.tgz", + "integrity": "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.27.2", + "@vue/shared": "3.5.16", "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.0" + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", - "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz", + "integrity": "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-core": "3.5.16", + "@vue/shared": "3.5.16" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", - "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz", + "integrity": "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.13", - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13", + "@babel/parser": "^7.27.2", + "@vue/compiler-core": "3.5.16", + "@vue/compiler-dom": "3.5.16", + "@vue/compiler-ssr": "3.5.16", + "@vue/shared": "3.5.16", "estree-walker": "^2.0.2", - "magic-string": "^0.30.11", - "postcss": "^8.4.48", - "source-map-js": "^1.2.0" + "magic-string": "^0.30.17", + "postcss": "^8.5.3", + "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-sfc/node_modules/magic-string": { @@ -3858,13 +4013,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", - "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz", + "integrity": "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.16", + "@vue/shared": "3.5.16" } }, "node_modules/@vue/compiler-vue2": { @@ -3903,6 +4058,23 @@ } } }, + "node_modules/@vue/language-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/@vue/language-core/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -3920,53 +4092,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", - "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz", + "integrity": "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.13" + "@vue/shared": "3.5.16" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", - "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.16.tgz", + "integrity": "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/reactivity": "3.5.16", + "@vue/shared": "3.5.16" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", - "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz", + "integrity": "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.13", - "@vue/runtime-core": "3.5.13", - "@vue/shared": "3.5.13", + "@vue/reactivity": "3.5.16", + "@vue/runtime-core": "3.5.16", + "@vue/shared": "3.5.16", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", - "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.16.tgz", + "integrity": "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-ssr": "3.5.16", + "@vue/shared": "3.5.16" }, "peerDependencies": { - "vue": "3.5.13" + "vue": "3.5.16" } }, "node_modules/@vue/shared": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", - "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.16.tgz", + "integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==", "license": "MIT" }, "node_modules/@webassemblyjs/ast": { @@ -4185,9 +4357,9 @@ } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -4299,9 +4471,9 @@ "license": "MIT" }, "node_modules/ansi_up": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-6.0.5.tgz", - "integrity": "sha512-bo4K8S5usgFivfgvgQozTC2EfusPf76o7w0LUVdAOkpISvVmQqtwCdF5c6okokrgIN13KhFIVB/0BhnNXueQeA==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-6.0.6.tgz", + "integrity": "sha512-yIa1x3Ecf8jWP4UWEunNjqNX6gzE4vg2gGz+xqRGY+TBSucnYp6RRdPV4brmtg6bQ1ljD48mZ5iGSEj7QEpRKA==", "license": "MIT", "engines": { "node": "*" @@ -4458,9 +4630,9 @@ } }, "node_modules/asciinema-player": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.9.0.tgz", - "integrity": "sha512-SXVFImVzeNr8ZUdNIHABGuzlbnGWTKy245AquAjODsAnv+Lp6vxjYGN0LfA8ns30tnx/ag/bMrTbLq13TpHE6w==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.10.0.tgz", + "integrity": "sha512-shoOK6F606nDKZxDVM7JuGSCAyWLePoGRFNlV+FqiP5Sqvyn0BlE7wlbjZyd2X4P1iRhv/HKfVNtnQIxmgphRA==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.21.0", @@ -4551,10 +4723,13 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-3.0.1.tgz", + "integrity": "sha512-vjtV3hiLqYDNRoiAv0zC4QaGAMPomEoq83PRmYIofPswwZurCeWR5LByXm7SyoL0Zh5+2z0+HC7jG8gSZJUh0w==", + "license": "MIT", + "engines": { + "node": ">= 16" + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -4605,12 +4780,15 @@ "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-4.0.1.tgz", + "integrity": "sha512-YClrbvTCXGe70pU2JiEiPLYXO9gQkyxYeKpJIQHVS/gOs6EWMQP2RYBwjFLNT322Ji8TOC3IMPfsYCedNpzKfA==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^3.0.0" + }, + "engines": { + "node": ">= 18" } }, "node_modules/braces": { @@ -4626,9 +4804,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", "funding": [ { "type": "opencollective", @@ -4645,8 +4823,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -4728,9 +4906,9 @@ } }, "node_modules/cacheable": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.9.0.tgz", - "integrity": "sha512-8D5htMCxPDUULux9gFzv30f04Xo3wCnik0oOxKoRTPIBoqA7HtOcJ87uBhQTs3jCfZZTrUBGsYIZOgE0ZRgMAg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.0.tgz", + "integrity": "sha512-SSgQTAnhd7WlJXnGlIi4jJJOiHzgnM5wRMEPaXAU4kECTAMpBoYKoZ9i5zHmclIEZbxcu3j7yY/CF8DTmwIsHg==", "dev": true, "license": "MIT", "dependencies": { @@ -4739,9 +4917,9 @@ } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz", - "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.4.tgz", + "integrity": "sha512-ypEvQvInNpUe+u+w8BIcPkQvEqXquyyibWE/1NB5T2BTzIpS5cGEV1LZskDzPSTvNAaT4+5FutvzlvnkxOSKlw==", "dev": true, "license": "MIT", "dependencies": { @@ -4767,9 +4945,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001717", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz", - "integrity": "sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==", + "version": "1.0.30001722", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001722.tgz", + "integrity": "sha512-DCQHBBZtiK6JVkAGw7drvAMK0Q0POD/xZvEmDp6baiMMP6QXXk9HpD6mNYBZWhOPG6LvIDb82ITqtWjhDckHCA==", "funding": [ { "type": "opencollective", @@ -5024,9 +5202,9 @@ } }, "node_modules/clippie": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.1.6.tgz", - "integrity": "sha512-1M3xZRNWcVwRkh3i2XcVYFjVtfC6FCLmIpk5s54uT3+jkBa25KRE3dB0Fgkt/YLoeR7AABWkVTlb0WziQYGgaw==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/clippie/-/clippie-4.1.7.tgz", + "integrity": "sha512-l8BmUmWqOt4mpxeSflcfODJJOU+DsE5atWj82k1zsxd2X82haz2+g12PJPMOt6e1Cs4/ZnOWdRAV+nY9n2o1Rg==", "license": "BSD-2-Clause" }, "node_modules/cliui": { @@ -5169,14 +5347,25 @@ "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "license": "MIT" }, + "node_modules/core-js": { + "version": "3.32.2", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.32.2.tgz", + "integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.42.0.tgz", - "integrity": "sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ==", + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.43.0.tgz", + "integrity": "sha512-2GML2ZsCc5LR7hZYz4AXmjQw8zuy2T//2QntwdnpuYI7jteT6GVYJL7F6C2C57R7gSYrcqVW3lAALefdbhBLDA==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.25.0" }, "funding": { "type": "opencollective", @@ -5929,9 +6118,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -6105,9 +6294,9 @@ } }, "node_modules/dom-input-range": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/dom-input-range/-/dom-input-range-1.2.0.tgz", - "integrity": "sha512-8HVA5Oy5Vt872S7IXsjjp6/5Hqsm5YZLhurxwwQXp80T9qVsj8/mEUH3sQlFujLLUoWfxiaThHHuJ3/q1MHVuA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/dom-input-range/-/dom-input-range-2.0.1.tgz", + "integrity": "sha512-UMGnuQlEepIPCju5NrPe+8y52B+pRvSjIaSPW92KpukoDGSEVkI2iaCR4HxK7K6C7zmVsWE8PEjCYZaJMr3vpg==", "license": "MIT", "workspaces": [ "demos" @@ -6158,9 +6347,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz", - "integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -6211,9 +6400,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.150", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.150.tgz", - "integrity": "sha512-rOOkP2ZUMx1yL4fCxXQKDHQ8ZXwisb2OycOQVKHgvB3ZI4CvehOd4y2tfnnLDieJ3Zs1RL1Dlp3cMkyIn7nnXA==", + "version": "1.5.166", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", + "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -6307,9 +6496,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -6319,31 +6508,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/esbuild-loader": { @@ -6373,18 +6562,6 @@ "node": ">=6" } }, - "node_modules/escape-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6472,18 +6649,46 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.3.tgz", - "integrity": "sha512-vDo4d9yQE+cS2tdIT4J02H/16veRvkHgiLDRpej+WL67oCfbOb97itZXn8wMPJ/GsiEBVjrjs//AVNw2Cp1EcA==", + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } }, + "node_modules/eslint-import-context": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.8.tgz", + "integrity": "sha512-bq+F7nyc65sKpZGT09dY0S0QrOnQtuDVIfyTGQ8uuvtMIF7oHp6CEP3mouN0rrnYF3Jqo6Ke0BfU/5wASZue1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.1.1" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -6507,18 +6712,19 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.4.tgz", - "integrity": "sha512-buzw5z5VtiQMysYLH9iW9BV04YyZebsw+gPi+c4FCjfS9i6COYOrEWw9t3m3wA9PFBfqcBCqWf32qrXLbwafDw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.3.tgz", + "integrity": "sha512-elVDn1eWKFrWlzxlWl9xMt8LltjKl161Ix50JFC50tHXI5/TRP32SNEqlJ/bo/HV+g7Rou/tlPQU2AcRtIhrOg==", "dev": true, "license": "ISC", "dependencies": { - "debug": "^4.4.0", - "get-tsconfig": "^4.10.0", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", "is-bun-module": "^2.0.0", - "stable-hash": "^0.0.5", - "tinyglobby": "^0.2.13", - "unrs-resolver": "^1.6.3" + "stable-hash-x": "^0.1.1", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" }, "engines": { "node": "^16.17.0 || >=18.6.0" @@ -6717,23 +6923,21 @@ } }, "node_modules/eslint-plugin-import-x": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.11.0.tgz", - "integrity": "sha512-NAaYY49342gj09QGvwnFFl5KcD5aLzjAz97Lo+upnN8MzjEGSIlmL5sxCYGqtIeMjw8fSRDFZIp2xjRLT+yl4Q==", + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.15.2.tgz", + "integrity": "sha512-J5gx7sN6DTm0LRT//eP3rVVQ2Yi4hrX0B+DbWxa5er8PZ6JjLo9GUBwogIFvEDdwJaSqZplpQT+haK/cXhb7VQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^8.31.0", + "@typescript-eslint/types": "^8.34.0", "comment-parser": "^1.4.1", - "debug": "^4.4.0", - "eslint-import-resolver-node": "^0.3.9", - "get-tsconfig": "^4.10.0", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", "is-glob": "^4.0.3", "minimatch": "^9.0.3 || ^10.0.1", - "semver": "^7.7.1", - "stable-hash": "^0.0.5", - "tslib": "^2.8.1", - "unrs-resolver": "^1.7.0" + "semver": "^7.7.2", + "stable-hash-x": "^0.1.1", + "unrs-resolver": "^1.9.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6742,13 +6946,30 @@ "url": "https://opencollective.com/eslint-plugin-import-x" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "@typescript-eslint/utils": "^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "eslint-import-resolver-node": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/utils": { + "optional": true + }, + "eslint-import-resolver-node": { + "optional": true + } } }, + "node_modules/eslint-plugin-import/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -6832,10 +7053,17 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -6912,14 +7140,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz", - "integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz", + "integrity": "sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.0" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -6943,9 +7171,9 @@ } }, "node_modules/eslint-plugin-regexp": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.7.0.tgz", - "integrity": "sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.9.0.tgz", + "integrity": "sha512-9WqJMnOq8VlE/cK+YAo9C9YHhkOtcEtEk9d12a+H7OSZFwlpI6stiHmYPGa2VE0QhTzodJyhlyprUaXDZLgHBw==", "dev": true, "license": "MIT", "dependencies": { @@ -6985,6 +7213,23 @@ "eslint": "^8.0.0 || ^9.0.0" } }, + "node_modules/eslint-plugin-sonarjs/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-sonarjs/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/eslint-plugin-sonarjs/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -7001,6 +7246,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/eslint-plugin-sonarjs/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-unicorn": { "version": "56.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz", @@ -7049,9 +7307,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.1.0.tgz", - "integrity": "sha512-/VTiJ1eSfNLw6lvG9ENySbGmcVvz6wZ9nA7ZqXlLBY2RkaF15iViYKxglWiIch12KiLAj0j1iXPYU6W4wTROFA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.2.0.tgz", + "integrity": "sha512-tl9s+KN3z0hN2b8fV2xSs5ytGl7Esk1oSCxULLwFcdaElhZ8btYYZFrWxvh4En+czrSDtuLCeCOGa8HhEZuBdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -7071,9 +7329,9 @@ } }, "node_modules/eslint-plugin-vue-scoped-css": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.9.0.tgz", - "integrity": "sha512-zXeKtEUpfk3PlsgKnr9/2U8K2xcsCV1M9hXWRhKbl3wipVowGXfHrhqUzHFVWNAHzEQv0DCDXGFWrmsGFqhGGA==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue-scoped-css/-/eslint-plugin-vue-scoped-css-2.10.0.tgz", + "integrity": "sha512-oH2NY7XFHF3EGOotvuPdnhB0x4uOmjoRoWZVfMnJ2PILDKVgZgM8WZ0rhDlh+fsr9jO9P8CAXO5/9s9v/GZNhg==", "dev": true, "license": "MIT", "dependencies": { @@ -7139,9 +7397,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7168,10 +7426,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -7231,15 +7496,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7465,6 +7730,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -7640,9 +7911,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -7690,10 +7961,16 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7846,19 +8123,37 @@ } }, "node_modules/happy-dom": { - "version": "17.4.6", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-17.4.6.tgz", - "integrity": "sha512-OEV1hDe9i2rFr66+WZNiwy1S8rAJy6bRXmXql68YJDjdfHBRbN76om+qVh68vQACf6y5Bcr90e/oK53RQxsDdg==", + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-18.0.1.tgz", + "integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==", "dev": true, "license": "MIT", "dependencies": { - "webidl-conversions": "^7.0.0", + "@types/node": "^20.0.0", + "@types/whatwg-mimetype": "^3.0.2", "whatwg-mimetype": "^3.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, + "node_modules/happy-dom/node_modules/@types/node": { + "version": "20.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", + "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/happy-dom/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7896,9 +8191,9 @@ } }, "node_modules/hookified": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.9.0.tgz", - "integrity": "sha512-2yEEGqphImtKIe1NXWEhu6yD3hlFR4Mxk4Mtp3XEyScpSt4pQ4ymmXA1zzxZpj99QkFK+nN0nzjeb2+RUi/6CQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.9.1.tgz", + "integrity": "sha512-u3pxtGhKjcSXnGm1CX6aXS9xew535j3lkOCegbA6jdyh0BaAjTbXI4aslKstCr6zUNtoCxFGFKwjbSHdGrMB8g==", "dev": true, "license": "MIT" }, @@ -7953,9 +8248,9 @@ } }, "node_modules/htmx.org": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz", - "integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.6.tgz", + "integrity": "sha512-7ythjYneGSk3yCHgtCnQeaoF+D+o7U2LF37WU3O0JYv3gTZSicdEFiI/Ai/NJyC5ZpYJWMpUb11OC5Lr6AfAqA==", "license": "0BSD" }, "node_modules/iconv-lite": { @@ -8383,18 +8678,19 @@ } }, "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jest-worker": { @@ -8758,10 +9054,16 @@ "webpack": "^4.4.0 || ^5.4.0" } }, + "node_modules/license-checker-webpack-plugin/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/license-checker-webpack-plugin/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9000,10 +9302,14 @@ } }, "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } }, "node_modules/magic-string": { "version": "0.25.9", @@ -9041,52 +9347,52 @@ } }, "node_modules/markdownlint": { - "version": "0.37.4", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.37.4.tgz", - "integrity": "sha512-u00joA/syf3VhWh6/ybVFkib5Zpj2e5KB/cfCei8fkSRuums6nyisTWGqjTWIOFoFwuXoTBQQiqlB4qFKp8ncQ==", + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.38.0.tgz", + "integrity": "sha512-xaSxkaU7wY/0852zGApM8LdlIfGCW8ETZ0Rr62IQtAnUMlMuifsg09vWJcNYeL4f0anvr8Vo4ZQar8jGpV0btQ==", "dev": true, "license": "MIT", "dependencies": { - "markdown-it": "14.1.0", - "micromark": "4.0.1", - "micromark-core-commonmark": "2.0.2", - "micromark-extension-directive": "3.0.2", + "micromark": "4.0.2", + "micromark-core-commonmark": "2.0.3", + "micromark-extension-directive": "4.0.0", "micromark-extension-gfm-autolink-literal": "2.1.0", "micromark-extension-gfm-footnote": "2.1.0", - "micromark-extension-gfm-table": "2.1.0", + "micromark-extension-gfm-table": "2.1.1", "micromark-extension-math": "3.1.0", - "micromark-util-types": "2.0.1" + "micromark-util-types": "2.0.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/DavidAnson" } }, "node_modules/markdownlint-cli": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.44.0.tgz", - "integrity": "sha512-ZJTAONlvF9NkrIBltCdW15DxN9UTbPiKMEqAh2EU2gwIFlrCMavyCEPPO121cqfYOrLUJWW8/XKWongstmmTeQ==", + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.45.0.tgz", + "integrity": "sha512-GiWr7GfJLVfcopL3t3pLumXCYs8sgWppjIA1F/Cc3zIMgD3tmkpyZ1xkm1Tej8mw53B93JsDjgA3KOftuYcfOw==", "dev": true, "license": "MIT", "dependencies": { "commander": "~13.1.0", - "glob": "~10.4.5", - "ignore": "~7.0.3", + "glob": "~11.0.2", + "ignore": "~7.0.4", "js-yaml": "~4.1.0", "jsonc-parser": "~3.3.1", "jsonpointer": "~5.0.1", - "markdownlint": "~0.37.4", - "minimatch": "~9.0.5", + "markdown-it": "~14.1.0", + "markdownlint": "~0.38.0", + "minimatch": "~10.0.1", "run-con": "~1.3.2", - "smol-toml": "~1.3.1" + "smol-toml": "~1.3.4" }, "bin": { "markdownlint": "markdownlint.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/markdownlint-cli/node_modules/commander": { @@ -9100,30 +9406,33 @@ } }, "node_modules/markdownlint-cli/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/markdownlint-cli/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -9137,22 +9446,6 @@ "dev": true, "license": "MIT" }, - "node_modules/markdownlint-cli/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -9166,9 +9459,9 @@ } }, "node_modules/material-icon-theme": { - "version": "5.22.0", - "resolved": "https://registry.npmjs.org/material-icon-theme/-/material-icon-theme-5.22.0.tgz", - "integrity": "sha512-hJyUSrb1ak8pJHcgw83ItXyLcj2dRH5lb3H1QWekkqTEQ4xPqUBSHWnBLioT5/sf5GFrut83p9FenC0KoYnjZw==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/material-icon-theme/-/material-icon-theme-5.23.0.tgz", + "integrity": "sha512-coDbg7MzBzQoLfv501w63pP1QjyCbP5mICm40WxyHIxa+aA4yS9QMOKGzaZDsq7oUKjJ0NfjjkIMG4ngGO9erQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9266,9 +9559,9 @@ } }, "node_modules/mermaid/node_modules/marked": { - "version": "15.0.11", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.11.tgz", - "integrity": "sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==", + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -9278,9 +9571,9 @@ } }, "node_modules/micromark": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", - "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", "dev": true, "funding": [ { @@ -9314,9 +9607,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz", - "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", "dev": true, "funding": [ { @@ -9349,9 +9642,9 @@ } }, "node_modules/micromark-extension-directive": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", - "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", "dev": true, "license": "MIT", "dependencies": { @@ -9407,9 +9700,9 @@ } }, "node_modules/micromark-extension-gfm-table": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", - "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -9797,9 +10090,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", - "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", "dev": true, "funding": [ { @@ -9878,12 +10171,12 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.2.tgz", + "integrity": "sha512-+9TJCIYXgZ2Dm5LxVCFsa8jOm+evMwXHFI0JM1XROmkfkpz8/iLLDh+TwSmyIBrs6C6Xu9294/fq8cBA+P6AqA==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^4.0.1" }, "engines": { "node": "20 || >=22" @@ -10007,10 +10300,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanopop": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.3.0.tgz", + "integrity": "sha512-fzN+T2K7/Ah25XU02MJkPZ5q4Tj5FpjmIYq4rvoHX4yb16HzFdCO6JxFFn5Y/oBhQ8no8fUZavnyIv9/+xkBBw==", + "license": "MIT" + }, "node_modules/napi-postinstall": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.3.tgz", - "integrity": "sha512-Mi7JISo/4Ij2tDZ2xBE2WH+/KvVlkhA6juEjpEeRAVPNCpN3nxJo/5FhDNKgBcdmcmhaH6JjgST4xY/23ZYK0w==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.2.4.tgz", + "integrity": "sha512-ZEzHJwBhZ8qQSbknHqYcdtQVr8zUgGyM/q6h6qAyhtyVMNrSgDhrC4disf03dYW0e+czXyLnZINnCTEkWy0eJg==", "dev": true, "license": "MIT", "bin": { @@ -10247,6 +10546,17 @@ "wrappy": "1" } }, + "node_modules/online-3d-viewer": { + "version": "0.16.0", + "resolved": "https://registry.npmmirror.com/online-3d-viewer/-/online-3d-viewer-0.16.0.tgz", + "integrity": "sha512-Mcmo41TM3K+svlMDRH8ySKSY2e8s7Sssdb5U9LV3gkFKVWGGuS304Vk5gqxopAJbE72DpsC67Ve3YNtcAuROwQ==", + "license": "MIT", + "dependencies": { + "@simonwep/pickr": "1.9.0", + "fflate": "0.8.2", + "three": "0.176.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10415,16 +10725,17 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -10580,13 +10891,13 @@ } }, "node_modules/playwright": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", - "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz", + "integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.52.0" + "playwright-core": "1.53.0" }, "bin": { "playwright": "cli.js" @@ -10599,9 +10910,9 @@ } }, "node_modules/playwright-core": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", - "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz", + "integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -10648,9 +10959,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", "funding": [ { "type": "opencollective", @@ -10667,7 +10978,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -10869,9 +11180,9 @@ } }, "node_modules/postcss-nesting": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz", - "integrity": "sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", "funding": [ { "type": "github", @@ -10884,7 +11195,7 @@ ], "license": "MIT-0", "dependencies": { - "@csstools/selector-resolve-nested": "^3.0.0", + "@csstools/selector-resolve-nested": "^3.1.0", "@csstools/selector-specificity": "^5.0.0", "postcss-selector-parser": "^7.0.0" }, @@ -10896,9 +11207,9 @@ } }, "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz", - "integrity": "sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", "funding": [ { "type": "github", @@ -11648,9 +11959,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11669,18 +11980,18 @@ } }, "node_modules/seroval": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.0.tgz", - "integrity": "sha512-4tYQDy3HVM0JjJ1CfDK3K8FhBKIDDri27oc2AyabuuHfQw6/yTDPp2Abt1h2cNtf1R0T+7AQYAzPhUgqXztaXw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/seroval-plugins": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.0.tgz", - "integrity": "sha512-FFu/UE3uA8L1vj0CXXZo2Nlh10MtYoOs0G//ptwlQMjfPFSeIVYUNy0zewfV8iM0CrOebAfHEG6J3xA9c+lsaQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.2.tgz", + "integrity": "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==", "license": "MIT", "engines": { "node": ">=10" @@ -11796,14 +12107,14 @@ } }, "node_modules/solid-js": { - "version": "1.9.6", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.6.tgz", - "integrity": "sha512-PoasAJvLk60hRtOTe9ulvALOdLjjqxuxcGZRolBQqxOnXrBXHGzqMT4ijNhGsDAYdOgEa8ZYaAE94PSldrFSkA==", + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz", + "integrity": "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==", "license": "MIT", "dependencies": { "csstype": "^3.1.0", - "seroval": "^1.1.0", - "seroval-plugins": "^1.1.0" + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.0" } }, "node_modules/sortablejs": { @@ -11936,12 +12247,15 @@ "spdx-ranges": "^2.0.0" } }, - "node_modules/stable-hash": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", - "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "node_modules/stable-hash-x": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.1.1.tgz", + "integrity": "sha512-l0x1D6vhnsNUGPFVDx45eif0y6eedVC8nm5uACTrVFJFtl2mLRW17aWtVyxFCpn5t94VUPkjU8vSLwIuwwqtJQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } }, "node_modules/stackback": { "version": "0.0.2", @@ -12098,6 +12412,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/style-search": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", @@ -12106,9 +12433,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.19.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.19.1.tgz", - "integrity": "sha512-C1SlPZNMKl+d/C867ZdCRthrS+6KuZ3AoGW113RZCOL0M8xOGpgx7G70wq7lFvqvm4dcfdGFVLB/mNaLFChRKw==", + "version": "16.20.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.20.0.tgz", + "integrity": "sha512-B5Myu9WRxrgKuLs3YyUXLP2H0mrbejwNxPmyADlACWwFsrL8Bmor/nTSh4OMae5sHjOz6gkSeccQH34gM4/nAw==", "dev": true, "funding": [ { @@ -12132,15 +12459,15 @@ "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", "css-tree": "^3.1.0", - "debug": "^4.3.7", + "debug": "^4.4.1", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.0.8", + "file-entry-cache": "^10.1.0", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", - "ignore": "^7.0.3", + "ignore": "^7.0.4", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.36.0", @@ -12253,9 +12580,9 @@ } }, "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz", - "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", "dev": true, "funding": [ { @@ -12272,8 +12599,8 @@ "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/stylelint/node_modules/@csstools/selector-specificity": { @@ -12307,31 +12634,31 @@ "license": "MIT" }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.0.tgz", - "integrity": "sha512-Et/ex6smi3wOOB+n5mek+Grf7P2AxZR5ueqRUvAAn4qkyatXi3cUC1cuQXVkX0VlzBVsN4BkWJFmY/fYiRTdww==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.1.tgz", + "integrity": "sha512-zcmsHjg2B2zjuBgjdnB+9q0+cWcgWfykIcsDkWDB4GTPtl1eXUA+gTI6sO0u01AqK3cliHryTU55/b2Ow1hfZg==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.9" + "flat-cache": "^6.1.10" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.9", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.9.tgz", - "integrity": "sha512-DUqiKkTlAfhtl7g78IuwqYM+YqvT+as0mY+EVk6mfimy19U79pJCzDZQsnqk3Ou/T6hFXWLGbwbADzD/c8Tydg==", + "version": "6.1.10", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.10.tgz", + "integrity": "sha512-B6/v1f0NwjxzmeOhzfXPGWpKBVA207LS7lehaVKQnFrVktcFRfkzjZZ2gwj2i1TkEUMQht7ZMJbABUT5N+V1Nw==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^1.9.0", + "cacheable": "^1.10.0", "flatted": "^3.3.3", - "hookified": "^1.8.2" + "hookified": "^1.9.1" } }, "node_modules/stylelint/node_modules/ignore": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz", - "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", "engines": { @@ -12448,6 +12775,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/sucrase/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/sucrase/node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -12477,6 +12819,27 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sucrase/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/sucrase/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/sucrase/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -12492,6 +12855,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sucrase/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/superstruct": { "version": "0.10.13", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.10.13.tgz", @@ -12625,9 +13004,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", - "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.24.1.tgz", + "integrity": "sha512-ITeWc7CCAfK53u8jnV39UNqStQZjSt+bVYtJHsOEL3vVj/WV9/8HmsF8Ej4oD8r+Xk1HpWyeW/t59r1QNeAcUQ==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -12647,14 +13026,13 @@ } }, "node_modules/synckit": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.4.tgz", - "integrity": "sha512-Q/XQKRaJiLiFIBNN+mndW7S/RHxvwzuZS6ZwmRzUBqJBv/5QIKCEwkBC8GBf8EQJKYnaFs0wOZbKTXBPj8L9oQ==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.3", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -12753,22 +13131,22 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.42.0.tgz", + "integrity": "sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==", "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -12847,6 +13225,12 @@ "node": ">=0.8" } }, + "node_modules/three": { + "version": "0.176.0", + "resolved": "https://registry.npmmirror.com/three/-/three-0.176.0.tgz", + "integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==", + "license": "MIT" + }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -12876,9 +13260,9 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12893,9 +13277,9 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -12921,9 +13305,9 @@ } }, "node_modules/tinypool": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.0.tgz", + "integrity": "sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==", "dev": true, "license": "MIT", "engines": { @@ -12941,9 +13325,9 @@ } }, "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", "dev": true, "license": "MIT", "engines": { @@ -13109,15 +13493,15 @@ "license": "MIT" }, "node_modules/uint8-to-base64": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/uint8-to-base64/-/uint8-to-base64-0.2.0.tgz", - "integrity": "sha512-r13jrghEYZAN99GeYpEjM107DOxqB65enskpwce8rRHVAGEtaWmsF5GqoGdPMf8DIXc9XyAJTdvlvRZi4LsszA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/uint8-to-base64/-/uint8-to-base64-0.2.1.tgz", + "integrity": "sha512-uO/84GaoDUfiAxpa8EksjVLE77A9Kc7ZTziN4zRpq4de9yLaLcZn3jx1/sVjyupsywcVX6RKWbqLe7gUNyzH+Q==", "license": "ISC" }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, "node_modules/universalify": { @@ -13131,9 +13515,9 @@ } }, "node_modules/unrs-resolver": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.7.2.tgz", - "integrity": "sha512-BBKpaylOW8KbHsu378Zky/dGh4ckT/4NW/0SHRABdqRLcQJ2dAOjDo9g97p04sWflm0kqPqpUatxReNV/dqI5A==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.9.0.tgz", + "integrity": "sha512-wqaRu4UnzBD2ABTC1kLfBjAqIDZ5YUTr/MLGa7By47JV1bJDSW7jq/ZSLigB7enLe7ubNaJhtnBXgrc/50cEhg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -13141,26 +13525,28 @@ "napi-postinstall": "^0.2.2" }, "funding": { - "url": "https://github.com/sponsors/JounQin" + "url": "https://opencollective.com/unrs-resolver" }, "optionalDependencies": { - "@unrs/resolver-binding-darwin-arm64": "1.7.2", - "@unrs/resolver-binding-darwin-x64": "1.7.2", - "@unrs/resolver-binding-freebsd-x64": "1.7.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.7.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.7.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.7.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-riscv64-musl": "1.7.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.7.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.7.2", - "@unrs/resolver-binding-linux-x64-musl": "1.7.2", - "@unrs/resolver-binding-wasm32-wasi": "1.7.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.7.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.7.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.7.2" + "@unrs/resolver-binding-android-arm-eabi": "1.9.0", + "@unrs/resolver-binding-android-arm64": "1.9.0", + "@unrs/resolver-binding-darwin-arm64": "1.9.0", + "@unrs/resolver-binding-darwin-x64": "1.9.0", + "@unrs/resolver-binding-freebsd-x64": "1.9.0", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.0", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.0", + "@unrs/resolver-binding-linux-arm64-gnu": "1.9.0", + "@unrs/resolver-binding-linux-arm64-musl": "1.9.0", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.0", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.0", + "@unrs/resolver-binding-linux-riscv64-musl": "1.9.0", + "@unrs/resolver-binding-linux-s390x-gnu": "1.9.0", + "@unrs/resolver-binding-linux-x64-gnu": "1.9.0", + "@unrs/resolver-binding-linux-x64-musl": "1.9.0", + "@unrs/resolver-binding-wasm32-wasi": "1.9.0", + "@unrs/resolver-binding-win32-arm64-msvc": "1.9.0", + "@unrs/resolver-binding-win32-ia32-msvc": "1.9.0", + "@unrs/resolver-binding-win32-x64-msvc": "1.9.0" } }, "node_modules/update-browserslist-db": { @@ -13355,17 +13741,17 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.3.tgz", + "integrity": "sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", - "debug": "^4.4.0", + "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0" + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" @@ -13392,9 +13778,9 @@ "license": "MIT" }, "node_modules/vite/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -13435,9 +13821,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", + "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", "dev": true, "license": "MIT", "dependencies": { @@ -13451,56 +13837,58 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.43.0", + "@rollup/rollup-android-arm64": "4.43.0", + "@rollup/rollup-darwin-arm64": "4.43.0", + "@rollup/rollup-darwin-x64": "4.43.0", + "@rollup/rollup-freebsd-arm64": "4.43.0", + "@rollup/rollup-freebsd-x64": "4.43.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", + "@rollup/rollup-linux-arm-musleabihf": "4.43.0", + "@rollup/rollup-linux-arm64-gnu": "4.43.0", + "@rollup/rollup-linux-arm64-musl": "4.43.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-musl": "4.43.0", + "@rollup/rollup-linux-s390x-gnu": "4.43.0", + "@rollup/rollup-linux-x64-gnu": "4.43.0", + "@rollup/rollup-linux-x64-musl": "4.43.0", + "@rollup/rollup-win32-arm64-msvc": "4.43.0", + "@rollup/rollup-win32-ia32-msvc": "4.43.0", + "@rollup/rollup-win32-x64-msvc": "4.43.0", "fsevents": "~2.3.2" } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.3.tgz", + "integrity": "sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.3", + "@vitest/mocker": "3.2.3", + "@vitest/pretty-format": "^3.2.3", + "@vitest/runner": "3.2.3", + "@vitest/snapshot": "3.2.3", + "@vitest/spy": "3.2.3", + "@vitest/utils": "3.2.3", "chai": "^5.2.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", + "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.13", - "tinypool": "^1.0.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.0", "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.3", "why-is-node-running": "^2.3.0" }, "bin": { @@ -13516,8 +13904,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.2.3", + "@vitest/ui": "3.2.3", "happy-dom": "*", "jsdom": "*" }, @@ -13555,6 +13943,19 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest/node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -13612,16 +14013,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", - "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.16.tgz", + "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.13", - "@vue/compiler-sfc": "3.5.13", - "@vue/runtime-dom": "3.5.13", - "@vue/server-renderer": "3.5.13", - "@vue/shared": "3.5.13" + "@vue/compiler-dom": "3.5.16", + "@vue/compiler-sfc": "3.5.16", + "@vue/runtime-dom": "3.5.16", + "@vue/server-renderer": "3.5.16", + "@vue/shared": "3.5.16" }, "peerDependencies": { "typescript": "*" @@ -13678,9 +14079,9 @@ } }, "node_modules/vue-eslint-parser/node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "peer": true, @@ -13735,9 +14136,9 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", @@ -13748,19 +14149,15 @@ } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.99.8", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", - "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", @@ -13880,9 +14277,9 @@ } }, "node_modules/webpack/node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/webpack/node_modules/eslint-scope": { @@ -13908,9 +14305,9 @@ } }, "node_modules/webpack/node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.2.tgz", + "integrity": "sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA==", "license": "MIT", "engines": { "node": ">=10.13.0" @@ -13936,12 +14333,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/whatwg-url/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -14149,15 +14540,15 @@ } }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index bc2c0c87f3..fc620bc986 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "type": "module", "engines": { - "node": ">= 18.0.0" + "node": ">= 20.0.0" }, "dependencies": { "@citation-js/core": "0.7.18", @@ -10,42 +10,42 @@ "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", "@github/relative-time-element": "4.4.8", - "@github/text-expander-element": "2.9.1", + "@github/text-expander-element": "2.9.2", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", - "@primer/octicons": "19.15.1", + "@primer/octicons": "19.15.2", "@silverwind/vue3-calendar-heatmap": "2.0.6", "add-asset-webpack-plugin": "3.0.0", - "ansi_up": "6.0.5", - "asciinema-player": "3.9.0", + "ansi_up": "6.0.6", + "asciinema-player": "3.10.0", "chart.js": "4.4.9", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", - "clippie": "4.1.6", + "clippie": "4.1.7", "cropperjs": "1.6.2", "css-loader": "7.1.2", "dayjs": "1.11.13", "dropzone": "6.0.0-beta.2", "easymde": "2.20.0", "esbuild-loader": "4.3.0", - "escape-goat": "4.0.0", "fast-glob": "3.3.3", - "htmx.org": "2.0.4", + "htmx.org": "2.0.6", "idiomorph": "0.7.3", "jquery": "3.7.1", "katex": "0.16.22", "license-checker-webpack-plugin": "0.2.1", "mermaid": "11.6.0", "mini-css-extract-plugin": "2.9.2", - "minimatch": "10.0.1", + "minimatch": "10.0.2", "monaco-editor": "0.52.2", "monaco-editor-webpack-plugin": "7.1.0", + "online-3d-viewer": "0.16.0", "pdfobject": "2.3.1", "perfect-debounce": "1.0.0", - "postcss": "8.5.3", + "postcss": "8.5.5", "postcss-loader": "8.1.1", - "postcss-nesting": "13.0.1", + "postcss-nesting": "13.0.2", "sortablejs": "1.15.6", - "swagger-ui-dist": "5.21.0", + "swagger-ui-dist": "5.24.1", "tailwindcss": "3.4.17", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", @@ -53,19 +53,19 @@ "toastify-js": "1.12.0", "tributejs": "5.1.3", "typescript": "5.8.3", - "uint8-to-base64": "0.2.0", + "uint8-to-base64": "0.2.1", "vanilla-colorful": "0.7.2", - "vue": "3.5.13", + "vue": "3.5.16", "vue-bar-graph": "2.2.0", "vue-chartjs": "5.3.2", "vue-loader": "17.4.2", - "webpack": "5.99.8", + "webpack": "5.99.9", "webpack-cli": "6.0.1", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.5.0", - "@playwright/test": "1.52.0", + "@playwright/test": "1.53.0", "@stoplight/spectral-cli": "6.15.0", "@stylistic/eslint-plugin-js": "3.1.0", "@stylistic/stylelint-plugin": "3.1.2", @@ -79,30 +79,30 @@ "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.4", - "@typescript-eslint/eslint-plugin": "8.32.0", - "@typescript-eslint/parser": "8.32.0", - "@vitejs/plugin-vue": "5.2.3", - "@vitest/eslint-plugin": "1.1.44", + "@typescript-eslint/eslint-plugin": "8.34.0", + "@typescript-eslint/parser": "8.34.0", + "@vitejs/plugin-vue": "5.2.4", + "@vitest/eslint-plugin": "1.2.2", "eslint": "8.57.0", - "eslint-import-resolver-typescript": "4.3.4", + "eslint-import-resolver-typescript": "4.4.3", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.0.2", - "eslint-plugin-import-x": "4.11.0", + "eslint-plugin-import-x": "4.15.2", "eslint-plugin-no-jquery": "3.1.1", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-playwright": "2.2.0", - "eslint-plugin-regexp": "2.7.0", + "eslint-plugin-regexp": "2.9.0", "eslint-plugin-sonarjs": "3.0.2", "eslint-plugin-unicorn": "56.0.1", - "eslint-plugin-vue": "10.1.0", - "eslint-plugin-vue-scoped-css": "2.9.0", + "eslint-plugin-vue": "10.2.0", + "eslint-plugin-vue-scoped-css": "2.10.0", "eslint-plugin-wc": "3.0.1", - "happy-dom": "17.4.6", - "markdownlint-cli": "0.44.0", - "material-icon-theme": "5.22.0", + "happy-dom": "18.0.1", + "markdownlint-cli": "0.45.0", + "material-icon-theme": "5.23.0", "nolyfill": "1.0.44", "postcss-html": "1.8.0", - "stylelint": "16.19.1", + "stylelint": "16.20.0", "stylelint-config-recommended": "16.0.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.11", @@ -112,7 +112,7 @@ "type-fest": "4.41.0", "updates": "16.4.2", "vite-string-plugin": "1.4.4", - "vitest": "3.1.3", + "vitest": "3.2.3", "vue-tsc": "2.2.10" }, "browserslist": [ diff --git a/public/assets/img/svg/octicon-pause.svg b/public/assets/img/svg/octicon-pause.svg new file mode 100644 index 0000000000..008de5afdf --- /dev/null +++ b/public/assets/img/svg/octicon-pause.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-pause" width="16" height="16" aria-hidden="true"><path d="M5 2h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1m5 0h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1"/></svg>
\ No newline at end of file diff --git a/routers/api/actions/artifacts_utils.go b/routers/api/actions/artifacts_utils.go index 77ce765098..35868c290e 100644 --- a/routers/api/actions/artifacts_utils.go +++ b/routers/api/actions/artifacts_utils.go @@ -43,7 +43,7 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) { return task, runID, true } -func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { //nolint:unparam +func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) { //nolint:unparam // ActionTask is never used task := ctx.ActionTask runID, err := strconv.ParseInt(rawRunID, 10, 64) if err != nil || task.Job.RunID != runID { diff --git a/routers/api/packages/alpine/alpine.go b/routers/api/packages/alpine/alpine.go index f35cff3df2..ba4a4f23ce 100644 --- a/routers/api/packages/alpine/alpine.go +++ b/routers/api/packages/alpine/alpine.go @@ -68,7 +68,7 @@ func GetRepositoryFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pv, &packages_service.PackageFileInfo{ @@ -216,7 +216,7 @@ func DownloadPackageFile(ctx *context.Context) { } } - s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index ae4ea7ea87..878e0f9945 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -5,8 +5,6 @@ package packages import ( "net/http" - "regexp" - "strings" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/perm" @@ -282,42 +280,10 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/conda", func() { - var ( - downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`) - uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`) - ) - - r.Get("/*", func(ctx *context.Context) { - m := downloadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("architecture", m[2]) - ctx.SetPathParam("filename", m[3]) - - switch m[3] { - case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": - conda.EnumeratePackages(ctx) - default: - conda.DownloadPackageFile(ctx) - } - }) - r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) { - m := uploadPattern.FindStringSubmatch(ctx.PathParam("*")) - if len(m) == 0 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("channel", strings.TrimSuffix(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - - conda.UploadPackageFile(ctx) - }) + r.PathGroup("/conda/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("GET", "/<channel:*>/<architecture>/<filename>", conda.ListOrGetPackages) + g.MatchPath("PUT", "/<channel:*>/<filename>", reqPackageAccess(perm.AccessModeWrite), conda.UploadPackageFile) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/cran", func() { r.Group("/src", func() { @@ -358,60 +324,15 @@ func CommonRoutes() *web.Router { }, reqPackageAccess(perm.AccessModeRead)) r.Group("/go", func() { r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), goproxy.UploadPackage) - r.Get("/sumdb/sum.golang.org/supported", func(ctx *context.Context) { - ctx.Status(http.StatusNotFound) - }) + r.Get("/sumdb/sum.golang.org/supported", http.NotFound) - // Manual mapping of routes because the package name contains slashes which chi does not support // https://go.dev/ref/mod#goproxy-protocol - r.Get("/*", func(ctx *context.Context) { - path := ctx.PathParam("*") - - if strings.HasSuffix(path, "/@latest") { - ctx.SetPathParam("name", path[:len(path)-len("/@latest")]) - ctx.SetPathParam("version", "latest") - - goproxy.PackageVersionMetadata(ctx) - return - } - - parts := strings.SplitN(path, "/@v/", 2) - if len(parts) != 2 { - ctx.Status(http.StatusNotFound) - return - } - - ctx.SetPathParam("name", parts[0]) - - // <package/name>/@v/list - if parts[1] == "list" { - goproxy.EnumeratePackageVersions(ctx) - return - } - - // <package/name>/@v/<version>.zip - if strings.HasSuffix(parts[1], ".zip") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".zip")]) - - goproxy.DownloadPackageFile(ctx) - return - } - // <package/name>/@v/<version>.info - if strings.HasSuffix(parts[1], ".info") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".info")]) - - goproxy.PackageVersionMetadata(ctx) - return - } - // <package/name>/@v/<version>.mod - if strings.HasSuffix(parts[1], ".mod") { - ctx.SetPathParam("version", parts[1][:len(parts[1])-len(".mod")]) - - goproxy.PackageVersionGoModContent(ctx) - return - } - - ctx.Status(http.StatusNotFound) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<name:*>/@<version:latest>", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/list", goproxy.EnumeratePackageVersions) + g.MatchPath("GET", "/<name:*>/@v/<version>.zip", goproxy.DownloadPackageFile) + g.MatchPath("GET", "/<name:*>/@v/<version>.info", goproxy.PackageVersionMetadata) + g.MatchPath("GET", "/<name:*>/@v/<version>.mod", goproxy.PackageVersionGoModContent) }) }, reqPackageAccess(perm.AccessModeRead)) r.Group("/generic", func() { @@ -532,82 +453,26 @@ func CommonRoutes() *web.Router { }) }) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/pypi", func() { r.Post("/", reqPackageAccess(perm.AccessModeWrite), pypi.UploadPackageFile) r.Get("/files/{id}/{version}/{filename}", pypi.DownloadPackageFile) r.Get("/simple/{id}", pypi.PackageMetadata) }, reqPackageAccess(perm.AccessModeRead)) - r.Group("/rpm", func() { - r.Group("/repository.key", func() { - r.Head("", rpm.GetRepositoryKey) - r.Get("", rpm.GetRepositoryKey) - }) - - var ( - repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`) - uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`) - filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`) - repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`) - ) - - r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) { - path := ctx.PathParam("*") - isHead := ctx.Req.Method == http.MethodHead - isGetHead := ctx.Req.Method == http.MethodHead || ctx.Req.Method == http.MethodGet - isPut := ctx.Req.Method == http.MethodPut - isDelete := ctx.Req.Method == http.MethodDelete - - m := repoPattern.FindStringSubmatch(path) - if len(m) == 2 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.GetRepositoryConfig(ctx) - return - } - m = repoFilePattern.FindStringSubmatch(path) - if len(m) == 3 && isGetHead { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("filename", m[2]) - if isHead { - rpm.CheckRepositoryFileExistence(ctx) - } else { - rpm.GetRepositoryFile(ctx) - } - return - } - - m = uploadPattern.FindStringSubmatch(path) - if len(m) == 2 && isPut { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - rpm.UploadPackageFile(ctx) - return - } - - m = filePattern.FindStringSubmatch(path) - if len(m) == 6 && (isGetHead || isDelete) { - ctx.SetPathParam("group", strings.Trim(m[1], "/")) - ctx.SetPathParam("name", m[2]) - ctx.SetPathParam("version", m[3]) - ctx.SetPathParam("architecture", m[4]) - if isGetHead { - rpm.DownloadPackageFile(ctx) - } else { - reqPackageAccess(perm.AccessModeWrite)(ctx) - if ctx.Written() { - return - } - rpm.DeletePackageFile(ctx) - } - return - } - - ctx.Status(http.StatusNotFound) - }) + r.Methods("HEAD,GET", "/rpm.repo", reqPackageAccess(perm.AccessModeRead), rpm.GetRepositoryConfig) + r.PathGroup("/rpm/*", func(g *web.RouterPathGroup) { + g.MatchPath("HEAD,GET", "/repository.key", rpm.GetRepositoryKey) + g.MatchPath("HEAD,GET", "/<group:*>.repo", rpm.GetRepositoryConfig) + g.MatchPath("HEAD", "/<group:*>/repodata/<filename>", rpm.CheckRepositoryFileExistence) + g.MatchPath("GET", "/<group:*>/repodata/<filename>", rpm.GetRepositoryFile) + g.MatchPath("PUT", "/<group:*>/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile) + // this URL pattern is only used internally in the RPM index, it is generated by us, the filename part is not really used (can be anything) + g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>", rpm.DownloadPackageFile) + g.MatchPath("HEAD,GET", "/<group:*>/package/<name>/<version>/<architecture>/<filename>", rpm.DownloadPackageFile) + g.MatchPath("DELETE", "/<group:*>/package/<name>/<version>/<architecture>", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/rubygems", func() { r.Get("/specs.4.8.gz", rubygems.EnumeratePackages) r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) @@ -621,6 +486,7 @@ func CommonRoutes() *web.Router { r.Delete("/yank", rubygems.DeletePackage) }, reqPackageAccess(perm.AccessModeWrite)) }, reqPackageAccess(perm.AccessModeRead)) + r.Group("/swift", func() { r.Group("", func() { // Needs to be unauthenticated. r.Post("", swift.CheckAuthenticate) @@ -632,31 +498,12 @@ func CommonRoutes() *web.Router { r.Get("", swift.EnumeratePackageVersions) r.Get(".json", swift.EnumeratePackageVersions) }, swift.CheckAcceptMediaType(swift.AcceptJSON)) - r.Group("/{version}", func() { - r.Get("/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) - r.Put("", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) - r.Get("", func(ctx *context.Context) { - // Can't use normal routes here: https://github.com/go-chi/chi/issues/781 - - version := ctx.PathParam("version") - if strings.HasSuffix(version, ".zip") { - swift.CheckAcceptMediaType(swift.AcceptZip)(ctx) - if ctx.Written() { - return - } - ctx.SetPathParam("version", version[:len(version)-4]) - swift.DownloadPackageFile(ctx) - } else { - swift.CheckAcceptMediaType(swift.AcceptJSON)(ctx) - if ctx.Written() { - return - } - if strings.HasSuffix(version, ".json") { - ctx.SetPathParam("version", version[:len(version)-5]) - } - swift.PackageVersionMetadata(ctx) - } - }) + r.PathGroup("/*", func(g *web.RouterPathGroup) { + g.MatchPath("GET", "/<version>.json", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("GET", "/<version>.zip", swift.CheckAcceptMediaType(swift.AcceptZip), swift.DownloadPackageFile) + g.MatchPath("GET", "/<version>/Package.swift", swift.CheckAcceptMediaType(swift.AcceptSwift), swift.DownloadManifest) + g.MatchPath("GET", "/<version>", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.PackageVersionMetadata) + g.MatchPath("PUT", "/<version>", reqPackageAccess(perm.AccessModeWrite), swift.CheckAcceptMediaType(swift.AcceptJSON), swift.UploadPackageFile) }) }) r.Get("/identifiers", swift.CheckAcceptMediaType(swift.AcceptJSON), swift.LookupPackageIdentifiers) @@ -693,6 +540,8 @@ func ContainerRoutes() *web.Router { &container.Auth{}, }) + // TODO: Content Discovery / References (not implemented yet) + r.Get("", container.ReqContainerAccess, container.DetermineSupport) r.Group("/token", func() { r.Get("", container.Authenticate) @@ -701,27 +550,22 @@ func ContainerRoutes() *web.Router { r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList) r.Group("/{username}", func() { r.PathGroup("/*", func(g *web.RouterPathGroup) { - g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.InitiateUploadBlob) - g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagList) - g.MatchPath("GET,PATCH,PUT,DELETE", `/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, func(ctx *context.Context) { - switch ctx.Req.Method { - case http.MethodGet: - container.GetUploadBlob(ctx) - case http.MethodPatch: - container.UploadBlob(ctx) - case http.MethodPut: - container.EndUploadBlob(ctx) - default: /* DELETE */ - container.CancelUploadBlob(ctx) - } - }) + g.MatchPath("POST", "/<image:*>/blobs/uploads", reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName, container.PostBlobsUploads) + g.MatchPath("GET", "/<image:*>/tags/list", container.VerifyImageName, container.GetTagsList) + + patternBlobsUploadsUUID := g.PatternRegexp(`/<image:*>/blobs/uploads/<uuid:[-.=\w]+>`, reqPackageAccess(perm.AccessModeWrite), container.VerifyImageName) + g.MatchPattern("GET", patternBlobsUploadsUUID, container.GetBlobsUpload) + g.MatchPattern("PATCH", patternBlobsUploadsUUID, container.PatchBlobsUpload) + g.MatchPattern("PUT", patternBlobsUploadsUUID, container.PutBlobsUpload) + g.MatchPattern("DELETE", patternBlobsUploadsUUID, container.DeleteBlobsUpload) + g.MatchPath("HEAD", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.HeadBlob) g.MatchPath("GET", `/<image:*>/blobs/<digest>`, container.VerifyImageName, container.GetBlob) g.MatchPath("DELETE", `/<image:*>/blobs/<digest>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteBlob) g.MatchPath("HEAD", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.HeadManifest) g.MatchPath("GET", `/<image:*>/manifests/<reference>`, container.VerifyImageName, container.GetManifest) - g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.UploadManifest) + g.MatchPath("PUT", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.PutManifest) g.MatchPath("DELETE", `/<image:*>/manifests/<reference>`, container.VerifyImageName, reqPackageAccess(perm.AccessModeWrite), container.DeleteManifest) }) }, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead)) diff --git a/routers/api/packages/arch/arch.go b/routers/api/packages/arch/arch.go index f5dc6c1d01..bf9cc3f1b8 100644 --- a/routers/api/packages/arch/arch.go +++ b/routers/api/packages/arch/arch.go @@ -239,7 +239,7 @@ func GetPackageOrRepositoryFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/cargo/cargo.go b/routers/api/packages/cargo/cargo.go index 710c614c6e..cfcf79244f 100644 --- a/routers/api/packages/cargo/cargo.go +++ b/routers/api/packages/cargo/cargo.go @@ -95,10 +95,7 @@ type SearchResultMeta struct { // https://doc.rust-lang.org/cargo/reference/registries.html#search func SearchPackages(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) perPage := ctx.FormInt("per_page") paginator := db.ListOptions{ Page: page, @@ -168,7 +165,7 @@ func ListOwners(ctx *context.Context) { // DownloadPackageFile serves the content of a package func DownloadPackageFile(ctx *context.Context) { - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/chef/chef.go b/routers/api/packages/chef/chef.go index a0c8c5696c..1f11afe548 100644 --- a/routers/api/packages/chef/chef.go +++ b/routers/api/packages/chef/chef.go @@ -343,7 +343,7 @@ func DownloadPackage(ctx *context.Context) { pf := pd.Files[0].File - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/composer/composer.go b/routers/api/packages/composer/composer.go index c6c14e5cf4..9daf0ffeff 100644 --- a/routers/api/packages/composer/composer.go +++ b/routers/api/packages/composer/composer.go @@ -53,10 +53,7 @@ func ServiceIndex(ctx *context.Context) { // SearchPackages searches packages, only "q" is supported // https://packagist.org/apidoc#search-packages func SearchPackages(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) perPage := ctx.FormInt("per_page") paginator := db.ListOptions{ Page: page, @@ -163,7 +160,7 @@ func PackageMetadata(ctx *context.Context) { // DownloadPackageFile serves the content of a package func DownloadPackageFile(ctx *context.Context) { - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/conan/conan.go b/routers/api/packages/conan/conan.go index 8019eee9f7..fe70e02cd6 100644 --- a/routers/api/packages/conan/conan.go +++ b/routers/api/packages/conan/conan.go @@ -480,7 +480,7 @@ func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKe return } - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/conda/conda.go b/routers/api/packages/conda/conda.go index 7a46681235..cfe069d6db 100644 --- a/routers/api/packages/conda/conda.go +++ b/routers/api/packages/conda/conda.go @@ -36,6 +36,24 @@ func apiError(ctx *context.Context, status int, obj any) { }) } +func isCondaPackageFileName(filename string) bool { + return strings.HasSuffix(filename, ".tar.bz2") || strings.HasSuffix(filename, ".conda") +} + +func ListOrGetPackages(ctx *context.Context) { + filename := ctx.PathParam("filename") + switch filename { + case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2": + EnumeratePackages(ctx) + return + } + if isCondaPackageFileName(filename) { + DownloadPackageFile(ctx) + return + } + ctx.NotFound(nil) +} + func EnumeratePackages(ctx *context.Context) { type Info struct { Subdir string `json:"subdir"` @@ -174,6 +192,12 @@ func EnumeratePackages(ctx *context.Context) { } func UploadPackageFile(ctx *context.Context) { + filename := ctx.PathParam("filename") + if !isCondaPackageFileName(filename) { + apiError(ctx, http.StatusBadRequest, nil) + return + } + upload, needToClose, err := ctx.UploadStream() if err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -191,7 +215,7 @@ func UploadPackageFile(ctx *context.Context) { defer buf.Close() var pck *conda_module.Package - if strings.HasSuffix(strings.ToLower(ctx.PathParam("filename")), ".tar.bz2") { + if strings.HasSuffix(filename, ".tar.bz2") { pck, err = conda_module.ParsePackageBZ2(buf) } else { pck, err = conda_module.ParsePackageConda(buf, buf.Size()) @@ -293,7 +317,7 @@ func DownloadPackageFile(ctx *context.Context) { pf := pfs[0] - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/container/auth.go b/routers/api/packages/container/auth.go index 1d8ae6af7d..1e1b87eb79 100644 --- a/routers/api/packages/container/auth.go +++ b/routers/api/packages/container/auth.go @@ -21,7 +21,7 @@ func (a *Auth) Name() string { } // Verify extracts the user from the Bearer token -// If it's an anonymous session a ghost user is returned +// If it's an anonymous session, a ghost user is returned func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { packageMeta, err := packages.ParseAuthorizationRequest(req) if err != nil { diff --git a/routers/api/packages/container/blob.go b/routers/api/packages/container/blob.go index 4a2320ab76..4b7bcee9d0 100644 --- a/routers/api/packages/container/blob.go +++ b/routers/api/packages/container/blob.go @@ -20,11 +20,13 @@ import ( container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/util" packages_service "code.gitea.io/gitea/services/packages" + + "github.com/opencontainers/go-digest" ) // saveAsPackageBlob creates a package blob from an upload // The uploaded blob gets stored in a special upload version to link them to the package/image -func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam +func saveAsPackageBlob(ctx context.Context, hsr packages_module.HashedSizeReader, pci *packages_service.PackageCreationInfo) (*packages_model.PackageBlob, error) { //nolint:unparam // PackageBlob is never used pb := packages_service.NewPackageBlob(hsr) exists := false @@ -88,20 +90,18 @@ func mountBlob(ctx context.Context, pi *packages_service.PackageInfo, pb *packag }) } -func containerPkgName(piOwnerID int64, piName string) string { - return fmt.Sprintf("pkg_%d_container_%s", piOwnerID, strings.ToLower(piName)) +func containerGlobalLockKey(piOwnerID int64, piName, usage string) string { + return fmt.Sprintf("pkg_%d_container_%s_%s", piOwnerID, strings.ToLower(piName), usage) } func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageInfo) (*packages_model.PackageVersion, error) { - var uploadVersion *packages_model.PackageVersion - - releaser, err := globallock.Lock(ctx, containerPkgName(pi.Owner.ID, pi.Name)) + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(pi.Owner.ID, pi.Name, "package")) if err != nil { return nil, err } defer releaser() - err = db.WithTx(ctx, func(ctx context.Context) error { + return db.WithTx2(ctx, func(ctx context.Context) (*packages_model.PackageVersion, error) { created := true p := &packages_model.Package{ OwnerID: pi.Owner.ID, @@ -113,7 +113,7 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI if p, err = packages_model.TryInsertPackage(ctx, p); err != nil { if !errors.Is(err, packages_model.ErrDuplicatePackage) { log.Error("Error inserting package: %v", err) - return err + return nil, err } created = false } @@ -121,31 +121,26 @@ func getOrCreateUploadVersion(ctx context.Context, pi *packages_service.PackageI if created { if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository, strings.ToLower(pi.Owner.LowerName+"/"+pi.Name)); err != nil { log.Error("Error setting package property: %v", err) - return err + return nil, err } } pv := &packages_model.PackageVersion{ PackageID: p.ID, CreatorID: pi.Owner.ID, - Version: container_model.UploadVersion, - LowerVersion: container_model.UploadVersion, + Version: container_module.UploadVersion, + LowerVersion: container_module.UploadVersion, IsInternal: true, MetadataJSON: "null", } if pv, err = packages_model.GetOrInsertVersion(ctx, pv); err != nil { if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) - return err + return nil, err } } - - uploadVersion = pv - - return nil + return pv, nil }) - - return uploadVersion, err } func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, pb *packages_model.PackageBlob) error { @@ -175,8 +170,8 @@ func createFileForBlob(ctx context.Context, pv *packages_model.PackageVersion, p return nil } -func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error { - releaser, err := globallock.Lock(ctx, containerPkgName(ownerID, image)) +func deleteBlob(ctx context.Context, ownerID int64, image string, digest digest.Digest) error { + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(ownerID, image, "blob")) if err != nil { return err } @@ -186,7 +181,7 @@ func deleteBlob(ctx context.Context, ownerID int64, image, digest string) error pfds, err := container_model.GetContainerBlobs(ctx, &container_model.BlobSearchOptions{ OwnerID: ownerID, Image: image, - Digest: digest, + Digest: string(digest), }) if err != nil { return err diff --git a/routers/api/packages/container/container.go b/routers/api/packages/container/container.go index 6ef1655235..d532f698ad 100644 --- a/routers/api/packages/container/container.go +++ b/routers/api/packages/container/container.go @@ -13,6 +13,7 @@ import ( "regexp" "strconv" "strings" + "sync" auth_model "code.gitea.io/gitea/models/auth" packages_model "code.gitea.io/gitea/models/packages" @@ -21,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/setting" @@ -31,17 +33,21 @@ import ( packages_service "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ) // maximum size of a container manifest // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests const maxManifestSize = 10 * 1024 * 1024 -var ( - imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`) - referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) -) +var globalVars = sync.OnceValue(func() (ret struct { + imageNamePattern, referencePattern *regexp.Regexp +}, +) { + ret.imageNamePattern = regexp.MustCompile(`\A[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*\z`) + ret.referencePattern = regexp.MustCompile(`\A[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}\z`) + return ret +}) type containerHeaders struct { Status int @@ -50,7 +56,7 @@ type containerHeaders struct { Range string Location string ContentType string - ContentLength int64 + ContentLength optional.Option[int64] } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#legacy-docker-support-http-headers @@ -64,8 +70,8 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) { if h.ContentType != "" { resp.Header().Set("Content-Type", h.ContentType) } - if h.ContentLength != 0 { - resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10)) + if h.ContentLength.Has() { + resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength.Value(), 10)) } if h.UploadUUID != "" { resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID) @@ -83,9 +89,7 @@ func jsonResponse(ctx *context.Context, status int, obj any) { Status: status, ContentType: "application/json", }) - if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil { - log.Error("JSON encode: %v", err) - } + _ = json.NewEncoder(ctx.Resp).Encode(obj) // ignore network errors } func apiError(ctx *context.Context, status int, err error) { @@ -133,7 +137,7 @@ func ReqContainerAccess(ctx *context.Context) { // VerifyImageName is a middleware which checks if the image name is allowed func VerifyImageName(ctx *context.Context) { - if !imageNamePattern.MatchString(ctx.PathParam("image")) { + if !globalVars().imageNamePattern.MatchString(ctx.PathParam("image")) { apiErrorDefined(ctx, errNameInvalid) } } @@ -215,7 +219,7 @@ func GetRepositoryList(ctx *context.Context) { if len(repositories) == n { v := url.Values{} if n > 0 { - v.Add("n", strconv.Itoa(n)) + v.Add("n", strconv.Itoa(n)) // FIXME: "n" can't be zero here, the logic is inconsistent with GetTagsList } v.Add("last", repositories[len(repositories)-1]) @@ -230,7 +234,7 @@ func GetRepositoryList(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#mounting-a-blob-from-another-repository // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func InitiateUploadBlob(ctx *context.Context) { +func PostBlobsUploads(ctx *context.Context) { image := ctx.PathParam("image") mount := ctx.FormTrim("mount") @@ -312,14 +316,14 @@ func InitiateUploadBlob(ctx *context.Context) { setResponseHeaders(ctx.Resp, &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID), - Range: "0-0", UploadUUID: upload.ID, Status: http.StatusAccepted, }) } -// https://docs.docker.com/registry/spec/api/#get-blob-upload -func GetUploadBlob(ctx *context.Context) { +// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks +func GetBlobsUpload(ctx *context.Context) { + image := ctx.PathParam("image") uuid := ctx.PathParam("uuid") upload, err := packages_model.GetBlobUploadByID(ctx, uuid) @@ -332,15 +336,21 @@ func GetUploadBlob(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ - Range: fmt.Sprintf("0-%d", upload.BytesReceived), + // FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578 + respHeaders := &containerHeaders{ + Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, upload.ID), UploadUUID: upload.ID, Status: http.StatusNoContent, - }) + } + if upload.BytesReceived > 0 { + respHeaders.Range = fmt.Sprintf("0-%d", upload.BytesReceived-1) + } + setResponseHeaders(ctx.Resp, respHeaders) } +// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#single-post // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func UploadBlob(ctx *context.Context) { +func PatchBlobsUpload(ctx *context.Context) { image := ctx.PathParam("image") uploader, err := container_service.NewBlobUploader(ctx, ctx.PathParam("uuid")) @@ -376,16 +386,19 @@ func UploadBlob(ctx *context.Context) { return } - setResponseHeaders(ctx.Resp, &containerHeaders{ + respHeaders := &containerHeaders{ Location: fmt.Sprintf("/v2/%s/%s/blobs/uploads/%s", ctx.Package.Owner.LowerName, image, uploader.ID), - Range: fmt.Sprintf("0-%d", uploader.Size()-1), UploadUUID: uploader.ID, Status: http.StatusAccepted, - }) + } + if uploader.Size() > 0 { + respHeaders.Range = fmt.Sprintf("0-%d", uploader.Size()-1) + } + setResponseHeaders(ctx.Resp, respHeaders) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-a-blob-in-chunks -func EndUploadBlob(ctx *context.Context) { +func PutBlobsUpload(ctx *context.Context) { image := ctx.PathParam("image") digest := ctx.FormTrim("digest") @@ -403,12 +416,7 @@ func EndUploadBlob(ctx *context.Context) { } return } - doClose := true - defer func() { - if doClose { - uploader.Close() - } - }() + defer uploader.Close() if ctx.Req.Body != nil { if err := uploader.Append(ctx, ctx.Req.Body); err != nil { @@ -441,11 +449,10 @@ func EndUploadBlob(ctx *context.Context) { return } - if err := uploader.Close(); err != nil { - apiError(ctx, http.StatusInternalServerError, err) - return - } - doClose = false + // There was a strange bug: the "Close" fails with error "close .../tmp/package-upload/....: file already closed" + // AFAIK there should be no other "Close" call to the uploader between NewBlobUploader and this line. + // At least it's safe to call Close twice, so ignore the error. + _ = uploader.Close() if err := container_service.RemoveBlobUploadByID(ctx, uploader.ID); err != nil { apiError(ctx, http.StatusInternalServerError, err) @@ -460,7 +467,7 @@ func EndUploadBlob(ctx *context.Context) { } // https://docs.docker.com/registry/spec/api/#delete-blob-upload -func CancelUploadBlob(ctx *context.Context) { +func DeleteBlobsUpload(ctx *context.Context) { uuid := ctx.PathParam("uuid") _, err := packages_model.GetBlobUploadByID(ctx, uuid) @@ -484,16 +491,15 @@ func CancelUploadBlob(ctx *context.Context) { } func getBlobFromContext(ctx *context.Context) (*packages_model.PackageFileDescriptor, error) { - d := ctx.PathParam("digest") - - if digest.Digest(d).Validate() != nil { + d := digest.Digest(ctx.PathParam("digest")) + if d.Validate() != nil { return nil, container_model.ErrContainerBlobNotExist } return workaroundGetContainerBlob(ctx, &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.PathParam("image"), - Digest: d, + Digest: string(d), }) } @@ -511,7 +517,7 @@ func HeadBlob(ctx *context.Context) { setResponseHeaders(ctx.Resp, &containerHeaders{ ContentDigest: blob.Properties.GetByName(container_module.PropertyDigest), - ContentLength: blob.Blob.Size, + ContentLength: optional.Some(blob.Blob.Size), Status: http.StatusOK, }) } @@ -533,9 +539,8 @@ func GetBlob(ctx *context.Context) { // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#deleting-blobs func DeleteBlob(ctx *context.Context) { - d := ctx.PathParam("digest") - - if digest.Digest(d).Validate() != nil { + d := digest.Digest(ctx.PathParam("digest")) + if d.Validate() != nil { apiErrorDefined(ctx, errBlobUnknown) return } @@ -551,7 +556,7 @@ func DeleteBlob(ctx *context.Context) { } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests -func UploadManifest(ctx *context.Context) { +func PutManifest(ctx *context.Context) { reference := ctx.PathParam("reference") mci := &manifestCreationInfo{ @@ -563,7 +568,7 @@ func UploadManifest(ctx *context.Context) { IsTagged: digest.Digest(reference).Validate() != nil, } - if mci.IsTagged && !referencePattern.MatchString(reference) { + if mci.IsTagged && !globalVars().referencePattern.MatchString(reference) { apiErrorDefined(ctx, errManifestInvalid.WithMessage("Tag is invalid")) return } @@ -607,18 +612,18 @@ func UploadManifest(ctx *context.Context) { } func getBlobSearchOptionsFromContext(ctx *context.Context) (*container_model.BlobSearchOptions, error) { - reference := ctx.PathParam("reference") - opts := &container_model.BlobSearchOptions{ OwnerID: ctx.Package.Owner.ID, Image: ctx.PathParam("image"), IsManifest: true, } - if digest.Digest(reference).Validate() == nil { - opts.Digest = reference - } else if referencePattern.MatchString(reference) { + reference := ctx.PathParam("reference") + if d := digest.Digest(reference); d.Validate() == nil { + opts.Digest = string(d) + } else if globalVars().referencePattern.MatchString(reference) { opts.Tag = reference + opts.OnlyLead = true } else { return nil, container_model.ErrContainerBlobNotExist } @@ -650,7 +655,7 @@ func HeadManifest(ctx *context.Context) { setResponseHeaders(ctx.Resp, &containerHeaders{ ContentDigest: manifest.Properties.GetByName(container_module.PropertyDigest), ContentType: manifest.Properties.GetByName(container_module.PropertyMediaType), - ContentLength: manifest.Blob.Size, + ContentLength: optional.Some(manifest.Blob.Size), Status: http.StatusOK, }) } @@ -705,7 +710,7 @@ func DeleteManifest(ctx *context.Context) { func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) { serveDirectReqParams := make(url.Values) serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType)) - s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams) + s, u, _, err := packages_service.OpenBlobForDownload(ctx, pfd.File, pfd.Blob, serveDirectReqParams) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -714,14 +719,14 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) headers := &containerHeaders{ ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest), ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType), - ContentLength: pfd.Blob.Size, + ContentLength: optional.Some(pfd.Blob.Size), Status: http.StatusOK, } if u != nil { headers.Status = http.StatusTemporaryRedirect headers.Location = u.String() - + headers.ContentLength = optional.None[int64]() // do not set Content-Length for redirect responses setResponseHeaders(ctx.Resp, headers) return } @@ -735,7 +740,7 @@ func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) } // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery -func GetTagList(ctx *context.Context) { +func GetTagsList(ctx *context.Context) { image := ctx.PathParam("image") if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil { @@ -780,7 +785,8 @@ func GetTagList(ctx *context.Context) { }) } -// FIXME: Workaround to be removed in v1.20 +// FIXME: Workaround to be removed in v1.20. +// Update maybe we should never really remote it, as long as there is legacy data? // https://github.com/go-gitea/gitea/issues/19586 func workaroundGetContainerBlob(ctx *context.Context, opts *container_model.BlobSearchOptions) (*packages_model.PackageFileDescriptor, error) { blob, err := container_model.GetContainerBlob(ctx, opts) diff --git a/routers/api/packages/container/manifest.go b/routers/api/packages/container/manifest.go index 26faa7b024..de40215aa7 100644 --- a/routers/api/packages/container/manifest.go +++ b/routers/api/packages/container/manifest.go @@ -10,11 +10,13 @@ import ( "io" "os" "strings" + "time" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" container_model "code.gitea.io/gitea/models/packages/container" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/globallock" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" packages_module "code.gitea.io/gitea/modules/packages" @@ -22,23 +24,12 @@ import ( "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" packages_service "code.gitea.io/gitea/services/packages" + container_service "code.gitea.io/gitea/services/packages/container" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" oci "github.com/opencontainers/image-spec/specs-go/v1" ) -func isValidMediaType(mt string) bool { - return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.") -} - -func isImageManifestMediaType(mt string) bool { - return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json") -} - -func isImageIndexMediaType(mt string) bool { - return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json") -} - // manifestCreationInfo describes a manifest to create type manifestCreationInfo struct { MediaType string @@ -55,71 +46,71 @@ func processManifest(ctx context.Context, mci *manifestCreationInfo, buf *packag if err := json.NewDecoder(buf).Decode(&index); err != nil { return "", err } - if index.SchemaVersion != 2 { return "", errUnsupported.WithMessage("Schema version is not supported") } - if _, err := buf.Seek(0, io.SeekStart); err != nil { return "", err } - if !isValidMediaType(mci.MediaType) { + if !container_module.IsMediaTypeValid(mci.MediaType) { mci.MediaType = index.MediaType - if !isValidMediaType(mci.MediaType) { + if !container_module.IsMediaTypeValid(mci.MediaType) { return "", errManifestInvalid.WithMessage("MediaType not recognized") } } - if isImageManifestMediaType(mci.MediaType) { - return processImageManifest(ctx, mci, buf) - } else if isImageIndexMediaType(mci.MediaType) { - return processImageManifestIndex(ctx, mci, buf) + // .../container/manifest.go:453:createManifestBlob() [E] Error inserting package blob: Error 1062 (23000): Duplicate entry '..........' for key 'package_blob.UQE_package_blob_md5' + releaser, err := globallock.Lock(ctx, containerGlobalLockKey(mci.Owner.ID, mci.Image, "manifest")) + if err != nil { + return "", err + } + defer releaser() + + if container_module.IsMediaTypeImageManifest(mci.MediaType) { + return processOciImageManifest(ctx, mci, buf) + } else if container_module.IsMediaTypeImageIndex(mci.MediaType) { + return processOciImageIndex(ctx, mci, buf) } return "", errManifestInvalid } -func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { - manifestDigest := "" - - err := func() error { - var manifest oci.Manifest - if err := json.NewDecoder(buf).Decode(&manifest); err != nil { - return err - } - - if _, err := buf.Seek(0, io.SeekStart); err != nil { - return err - } - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - configDescriptor, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ - OwnerID: mci.Owner.ID, - Image: mci.Image, - Digest: string(manifest.Config.Digest), - }) - if err != nil { - return err - } +type processManifestTxRet struct { + pv *packages_model.PackageVersion + pb *packages_model.PackageBlob + created bool + digest string +} - configReader, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(configDescriptor.Blob.HashSHA256)) - if err != nil { - return err +func handleCreateManifestResult(ctx context.Context, err error, mci *manifestCreationInfo, contentStore *packages_module.ContentStore, txRet *processManifestTxRet) (string, error) { + if err != nil && txRet.created && txRet.pb != nil { + if err := contentStore.Delete(packages_module.BlobHash256Key(txRet.pb.HashSHA256)); err != nil { + log.Error("Error deleting package blob from content store: %v", err) } - defer configReader.Close() + return "", err + } + pd, err := packages_model.GetPackageDescriptor(ctx, txRet.pv) + if err != nil { + log.Error("Error getting package descriptor: %v", err) // ignore this error + } else { + notify_service.PackageCreate(ctx, mci.Creator, pd) + } + return txRet.digest, nil +} - metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader) - if err != nil { - return err - } +func processOciImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (manifestDigest string, errRet error) { + manifest, configDescriptor, metadata, err := container_service.ParseManifestMetadata(ctx, buf, mci.Owner.ID, mci.Image) + if err != nil { + return "", err + } + if _, err = buf.Seek(0, io.SeekStart); err != nil { + return "", err + } + contentStore := packages_module.NewContentStore() + var txRet processManifestTxRet + err = db.WithTx(ctx, func(ctx context.Context) (err error) { blobReferences := make([]*blobReference, 0, 1+len(manifest.Layers)) - blobReferences = append(blobReferences, &blobReference{ Digest: manifest.Config.Digest, MediaType: manifest.Config.MediaType, @@ -150,78 +141,43 @@ func processImageManifest(ctx context.Context, mci *manifestCreationInfo, buf *p return err } - uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_model.UploadVersion) - if err != nil && err != packages_model.ErrPackageNotExist { + uploadVersion, err := packages_model.GetInternalVersionByNameAndVersion(ctx, mci.Owner.ID, packages_model.TypeContainer, mci.Image, container_module.UploadVersion) + if err != nil && !errors.Is(err, packages_model.ErrPackageNotExist) { return err } for _, ref := range blobReferences { - if err := createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil { + if _, err = createFileFromBlobReference(ctx, pv, uploadVersion, ref); err != nil { return err } } + txRet.pv = pv + txRet.pb, txRet.created, txRet.digest, err = createManifestBlob(ctx, contentStore, mci, pv, buf) + return err + }) - pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf) - removeBlob := false - defer func() { - if removeBlob { - contentStore := packages_module.NewContentStore() - if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil { - log.Error("Error deleting package blob from content store: %v", err) - } - } - }() - if err != nil { - removeBlob = created - return err - } - - if err := committer.Commit(); err != nil { - removeBlob = created - return err - } - - if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil { - return err - } - - manifestDigest = digest + return handleCreateManifestResult(ctx, err, mci, contentStore, &txRet) +} - return nil - }() - if err != nil { +func processOciImageIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (manifestDigest string, errRet error) { + var index oci.Index + if err := json.NewDecoder(buf).Decode(&index); err != nil { + return "", err + } + if _, err := buf.Seek(0, io.SeekStart); err != nil { return "", err } - return manifestDigest, nil -} - -func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, buf *packages_module.HashedBuffer) (string, error) { - manifestDigest := "" - - err := func() error { - var index oci.Index - if err := json.NewDecoder(buf).Decode(&index); err != nil { - return err - } - - if _, err := buf.Seek(0, io.SeekStart); err != nil { - return err - } - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - + contentStore := packages_module.NewContentStore() + var txRet processManifestTxRet + err := db.WithTx(ctx, func(ctx context.Context) (err error) { metadata := &container_module.Metadata{ Type: container_module.TypeOCI, Manifests: make([]*container_module.Manifest, 0, len(index.Manifests)), } for _, manifest := range index.Manifests { - if !isImageManifestMediaType(manifest.MediaType) { + if !container_module.IsMediaTypeImageManifest(manifest.MediaType) { return errManifestInvalid } @@ -265,50 +221,12 @@ func processImageManifestIndex(ctx context.Context, mci *manifestCreationInfo, b return err } - pb, created, digest, err := createManifestBlob(ctx, mci, pv, buf) - removeBlob := false - defer func() { - if removeBlob { - contentStore := packages_module.NewContentStore() - if err := contentStore.Delete(packages_module.BlobHash256Key(pb.HashSHA256)); err != nil { - log.Error("Error deleting package blob from content store: %v", err) - } - } - }() - if err != nil { - removeBlob = created - return err - } - - if err := committer.Commit(); err != nil { - removeBlob = created - return err - } - - if err := notifyPackageCreate(ctx, mci.Creator, pv); err != nil { - return err - } - - manifestDigest = digest - - return nil - }() - if err != nil { - return "", err - } - - return manifestDigest, nil -} - -func notifyPackageCreate(ctx context.Context, doer *user_model.User, pv *packages_model.PackageVersion) error { - pd, err := packages_model.GetPackageDescriptor(ctx, pv) - if err != nil { + txRet.pv = pv + txRet.pb, txRet.created, txRet.digest, err = createManifestBlob(ctx, contentStore, mci, pv, buf) return err - } - - notify_service.PackageCreate(ctx, doer, pd) + }) - return nil + return handleCreateManifestResult(ctx, err, mci, contentStore, &txRet) } func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, metadata *container_module.Metadata) (*packages_model.PackageVersion, error) { @@ -349,24 +267,31 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met LowerVersion: strings.ToLower(mci.Reference), MetadataJSON: string(metadataJSON), } - var pv *packages_model.PackageVersion - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + pv, err := packages_model.GetOrInsertVersion(ctx, _pv) + if err != nil { if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { log.Error("Error inserting package: %v", err) return nil, err } - if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return nil, err - } - - // keep download count on overwrite - _pv.DownloadCount = pv.DownloadCount - - if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { - if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { - log.Error("Error inserting package: %v", err) - return nil, err + if container_module.IsMediaTypeImageIndex(mci.MediaType) { + if pv.CreatedUnix.AsTime().Before(time.Now().Add(-24 * time.Hour)) { + if err = packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + return nil, err + } + // keep download count on overwriting + _pv.DownloadCount = pv.DownloadCount + if pv, err = packages_model.GetOrInsertVersion(ctx, _pv); err != nil { + if !errors.Is(err, packages_model.ErrDuplicatePackageVersion) { + log.Error("Error inserting package: %v", err) + return nil, err + } + } + } else { + err = packages_model.UpdateVersion(ctx, &packages_model.PackageVersion{ID: pv.ID, MetadataJSON: _pv.MetadataJSON}) + if err != nil { + return nil, err + } } } } @@ -376,14 +301,20 @@ func createPackageAndVersion(ctx context.Context, mci *manifestCreationInfo, met } if mci.IsTagged { - if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil { - log.Error("Error setting package version property: %v", err) + if err = packages_model.InsertOrUpdateProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged, ""); err != nil { + return nil, err + } + } else { + if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestTagged); err != nil { return nil, err } } + + if err = packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference); err != nil { + return nil, err + } for _, manifest := range metadata.Manifests { - if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { - log.Error("Error setting package version property: %v", err) + if _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypeVersion, pv.ID, container_module.PropertyManifestReference, manifest.Digest); err != nil { return nil, err } } @@ -400,9 +331,9 @@ type blobReference struct { IsLead bool } -func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) error { +func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *packages_model.PackageVersion, ref *blobReference) (*packages_model.PackageFile, error) { if ref.File.Blob.Size != ref.ExpectedSize { - return errSizeInvalid + return nil, errSizeInvalid } if ref.Name == "" { @@ -410,20 +341,21 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package } pf := &packages_model.PackageFile{ - VersionID: pv.ID, - BlobID: ref.File.Blob.ID, - Name: ref.Name, - LowerName: ref.Name, - IsLead: ref.IsLead, + VersionID: pv.ID, + BlobID: ref.File.Blob.ID, + Name: ref.Name, + LowerName: ref.Name, + CompositeKey: string(ref.Digest), + IsLead: ref.IsLead, } var err error if pf, err = packages_model.TryInsertFile(ctx, pf); err != nil { if errors.Is(err, packages_model.ErrDuplicatePackageFile) { // Skip this blob because the manifest contains the same filesystem layer multiple times. - return nil + return pf, nil } log.Error("Error inserting package file: %v", err) - return err + return nil, err } props := map[string]string{ @@ -433,21 +365,21 @@ func createFileFromBlobReference(ctx context.Context, pv, uploadVersion *package for name, value := range props { if _, err := packages_model.InsertProperty(ctx, packages_model.PropertyTypeFile, pf.ID, name, value); err != nil { log.Error("Error setting package file property: %v", err) - return err + return nil, err } } - // Remove the file from the blob upload version + // Remove the ref file (old file) from the blob upload version if uploadVersion != nil && ref.File.File != nil && uploadVersion.ID == ref.File.File.VersionID { if err := packages_service.DeletePackageFile(ctx, ref.File.File); err != nil { - return err + return nil, err } } - return nil + return pf, nil } -func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (*packages_model.PackageBlob, bool, string, error) { +func createManifestBlob(ctx context.Context, contentStore *packages_module.ContentStore, mci *manifestCreationInfo, pv *packages_model.PackageVersion, buf *packages_module.HashedBuffer) (_ *packages_model.PackageBlob, created bool, manifestDigest string, _ error) { pb, exists, err := packages_model.GetOrInsertBlob(ctx, packages_service.NewPackageBlob(buf)) if err != nil { log.Error("Error inserting package blob: %v", err) @@ -456,29 +388,48 @@ func createManifestBlob(ctx context.Context, mci *manifestCreationInfo, pv *pack // FIXME: Workaround to be removed in v1.20 // https://github.com/go-gitea/gitea/issues/19586 if exists { - err = packages_module.NewContentStore().Has(packages_module.BlobHash256Key(pb.HashSHA256)) + err = contentStore.Has(packages_module.BlobHash256Key(pb.HashSHA256)) if err != nil && (errors.Is(err, util.ErrNotExist) || errors.Is(err, os.ErrNotExist)) { log.Debug("Package registry inconsistent: blob %s does not exist on file system", pb.HashSHA256) exists = false } } if !exists { - contentStore := packages_module.NewContentStore() if err := contentStore.Save(packages_module.BlobHash256Key(pb.HashSHA256), buf, buf.Size()); err != nil { log.Error("Error saving package blob in content store: %v", err) return nil, false, "", err } } - manifestDigest := digestFromHashSummer(buf) - err = createFileFromBlobReference(ctx, pv, nil, &blobReference{ + manifestDigest = digestFromHashSummer(buf) + pf, err := createFileFromBlobReference(ctx, pv, nil, &blobReference{ Digest: digest.Digest(manifestDigest), MediaType: mci.MediaType, - Name: container_model.ManifestFilename, + Name: container_module.ManifestFilename, File: &packages_model.PackageFileDescriptor{Blob: pb}, ExpectedSize: pb.Size, IsLead: true, }) + if err != nil { + return nil, false, "", err + } + oldManifestFiles, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{ + OwnerID: mci.Owner.ID, + PackageType: packages_model.TypeContainer, + VersionID: pv.ID, + Query: container_module.ManifestFilename, + }) + if err != nil { + return nil, false, "", err + } + for _, oldManifestFile := range oldManifestFiles { + if oldManifestFile.ID != pf.ID && oldManifestFile.IsLead { + err = packages_model.UpdateFile(ctx, &packages_model.PackageFile{ID: oldManifestFile.ID, IsLead: false}, []string{"is_lead"}) + if err != nil { + return nil, false, "", err + } + } + } return pb, !exists, manifestDigest, err } diff --git a/routers/api/packages/cran/cran.go b/routers/api/packages/cran/cran.go index 8a20072cb6..732acd215f 100644 --- a/routers/api/packages/cran/cran.go +++ b/routers/api/packages/cran/cran.go @@ -250,7 +250,7 @@ func downloadPackageFile(ctx *context.Context, opts *cran_model.SearchOptions) { return } - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/debian/debian.go b/routers/api/packages/debian/debian.go index fec34c91a6..346f71fa5d 100644 --- a/routers/api/packages/debian/debian.go +++ b/routers/api/packages/debian/debian.go @@ -59,7 +59,7 @@ func GetRepositoryFile(ctx *context.Context) { key += "|" + component + "|" + architecture } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pv, &packages_service.PackageFileInfo{ @@ -106,7 +106,7 @@ func GetRepositoryFileByHash(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) @@ -210,7 +210,7 @@ func DownloadPackageFile(ctx *context.Context) { name := ctx.PathParam("name") version := ctx.PathParam("version") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/generic/generic.go b/routers/api/packages/generic/generic.go index 0b5daa7334..db7aeace50 100644 --- a/routers/api/packages/generic/generic.go +++ b/routers/api/packages/generic/generic.go @@ -31,7 +31,7 @@ func apiError(ctx *context.Context, status int, obj any) { // DownloadPackageFile serves the specific generic package. func DownloadPackageFile(ctx *context.Context) { - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/goproxy/goproxy.go b/routers/api/packages/goproxy/goproxy.go index bde29df739..89ec86bce9 100644 --- a/routers/api/packages/goproxy/goproxy.go +++ b/routers/api/packages/goproxy/goproxy.go @@ -106,7 +106,7 @@ func DownloadPackageFile(ctx *context.Context) { return } - s, u, _, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, util.ErrNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/helm/helm.go b/routers/api/packages/helm/helm.go index fb12daaa46..39c34f4da4 100644 --- a/routers/api/packages/helm/helm.go +++ b/routers/api/packages/helm/helm.go @@ -122,7 +122,7 @@ func DownloadPackageFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pvs[0], &packages_service.PackageFileInfo{ diff --git a/routers/api/packages/maven/maven.go b/routers/api/packages/maven/maven.go index 9089c2eccf..40a8ff8242 100644 --- a/routers/api/packages/maven/maven.go +++ b/routers/api/packages/maven/maven.go @@ -223,7 +223,7 @@ func servePackageFile(ctx *context.Context, params parameters, serveContent bool return } - s, u, _, err := packages_service.GetPackageBlobStream(ctx, pf, pb, nil) + s, u, _, err := packages_service.OpenBlobForDownload(ctx, pf, pb, nil) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/npm/npm.go b/routers/api/packages/npm/npm.go index 6ec46bcb36..1f09816d32 100644 --- a/routers/api/packages/npm/npm.go +++ b/routers/api/packages/npm/npm.go @@ -85,7 +85,7 @@ func DownloadPackageFile(ctx *context.Context) { packageVersion := ctx.PathParam("version") filename := ctx.PathParam("filename") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, @@ -132,7 +132,7 @@ func DownloadPackageFileByName(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pvs[0], &packages_service.PackageFileInfo{ diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go index a726065ad0..801c60af13 100644 --- a/routers/api/packages/nuget/api_v2.go +++ b/routers/api/packages/nuget/api_v2.go @@ -246,21 +246,30 @@ type TypedValue[T any] struct { } type FeedEntryProperties struct { - Version string `xml:"d:Version"` - NormalizedVersion string `xml:"d:NormalizedVersion"` Authors string `xml:"d:Authors"` + Copyright string `xml:"d:Copyright,omitempty"` + Created TypedValue[time.Time] `xml:"d:Created"` Dependencies string `xml:"d:Dependencies"` Description string `xml:"d:Description"` - VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"` + DevelopmentDependency TypedValue[bool] `xml:"d:DevelopmentDependency"` DownloadCount TypedValue[int64] `xml:"d:DownloadCount"` - PackageSize TypedValue[int64] `xml:"d:PackageSize"` - Created TypedValue[time.Time] `xml:"d:Created"` + ID string `xml:"d:Id"` + IconURL string `xml:"d:IconUrl,omitempty"` + Language string `xml:"d:Language,omitempty"` LastUpdated TypedValue[time.Time] `xml:"d:LastUpdated"` - Published TypedValue[time.Time] `xml:"d:Published"` + LicenseURL string `xml:"d:LicenseUrl,omitempty"` + MinClientVersion string `xml:"d:MinClientVersion,omitempty"` + NormalizedVersion string `xml:"d:NormalizedVersion"` + Owners string `xml:"d:Owners,omitempty"` + PackageSize TypedValue[int64] `xml:"d:PackageSize"` ProjectURL string `xml:"d:ProjectUrl,omitempty"` + Published TypedValue[time.Time] `xml:"d:Published"` ReleaseNotes string `xml:"d:ReleaseNotes,omitempty"` RequireLicenseAcceptance TypedValue[bool] `xml:"d:RequireLicenseAcceptance"` - Title string `xml:"d:Title"` + Tags string `xml:"d:Tags,omitempty"` + Title string `xml:"d:Title,omitempty"` + Version string `xml:"d:Version"` + VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"` } type FeedEntry struct { @@ -353,21 +362,30 @@ func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNames Author: metadata.Authors, Content: content, Properties: &FeedEntryProperties{ - Version: pd.Version.Version, - NormalizedVersion: pd.Version.Version, Authors: metadata.Authors, + Copyright: metadata.Copyright, + Created: createdValue, Dependencies: buildDependencyString(metadata), Description: metadata.Description, - VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, + DevelopmentDependency: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.DevelopmentDependency}, DownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, - PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()}, - Created: createdValue, + ID: pd.Package.Name, + IconURL: metadata.IconURL, + Language: metadata.Language, LastUpdated: createdValue, - Published: createdValue, + LicenseURL: metadata.LicenseURL, + MinClientVersion: metadata.MinClientVersion, + NormalizedVersion: pd.Version.Version, + Owners: metadata.Owners, + PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()}, ProjectURL: metadata.ProjectURL, + Published: createdValue, ReleaseNotes: metadata.ReleaseNotes, RequireLicenseAcceptance: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.RequireLicenseAcceptance}, - Title: pd.Package.Name, + Tags: metadata.Tags, + Title: metadata.Title, + Version: pd.Version.Version, + VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount}, }, } diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go index fa5067a278..92d62d90b1 100644 --- a/routers/api/packages/nuget/nuget.go +++ b/routers/api/packages/nuget/nuget.go @@ -36,7 +36,7 @@ func apiError(ctx *context.Context, status int, obj any) { }) } -func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam +func xmlResponse(ctx *context.Context, status int, obj any) { //nolint:unparam // status is always StatusOK ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8") ctx.Resp.WriteHeader(status) if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil { @@ -405,7 +405,7 @@ func DownloadPackageFile(ctx *context.Context) { packageVersion := ctx.PathParam("version") filename := ctx.PathParam("filename") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, @@ -669,7 +669,7 @@ func DownloadSymbolFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0]) + s, u, pf, err := packages_service.OpenFileForDownload(ctx, pfs[0]) if err != nil { if errors.Is(err, packages_model.ErrPackageNotExist) || errors.Is(err, packages_model.ErrPackageFileNotExist) { apiError(ctx, http.StatusNotFound, err) diff --git a/routers/api/packages/pub/pub.go b/routers/api/packages/pub/pub.go index e7b07aefd0..4bd36e94b6 100644 --- a/routers/api/packages/pub/pub.go +++ b/routers/api/packages/pub/pub.go @@ -274,7 +274,7 @@ func DownloadPackageFile(ctx *context.Context) { pf := pd.Files[0].File - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/pypi/pypi.go b/routers/api/packages/pypi/pypi.go index 199f4e7478..9b5ae6c89d 100644 --- a/routers/api/packages/pypi/pypi.go +++ b/routers/api/packages/pypi/pypi.go @@ -82,7 +82,7 @@ func DownloadPackageFile(ctx *context.Context) { packageVersion := ctx.PathParam("version") filename := ctx.PathParam("filename") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/rpm/rpm.go b/routers/api/packages/rpm/rpm.go index a00a61c079..938c35341d 100644 --- a/routers/api/packages/rpm/rpm.go +++ b/routers/api/packages/rpm/rpm.go @@ -96,7 +96,7 @@ func GetRepositoryFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pv, &packages_service.PackageFileInfo{ @@ -220,7 +220,7 @@ func DownloadPackageFile(ctx *context.Context) { name := ctx.PathParam("name") version := ctx.PathParam("version") - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index de8c7ef3ed..774d5520fd 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -14,6 +14,7 @@ import ( "strings" packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/optional" packages_module "code.gitea.io/gitea/modules/packages" rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" @@ -177,7 +178,7 @@ func DownloadPackageFile(ctx *context.Context) { return } - s, u, pf, err := packages_service.GetFileStreamByPackageVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageVersion( ctx, pvs[0], &packages_service.PackageFileInfo{ @@ -309,7 +310,7 @@ func GetPackageInfo(ctx *context.Context) { apiError(ctx, http.StatusNotFound, nil) return } - infoContent, err := makePackageInfo(ctx, versions) + infoContent, err := makePackageInfo(ctx, versions, cache.NewEphemeralCache()) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -317,7 +318,7 @@ func GetPackageInfo(ctx *context.Context) { ctx.PlainText(http.StatusOK, infoContent) } -// GetAllPackagesVersions returns a custom text based format containing information about all versions of all rubygems. +// GetAllPackagesVersions returns a custom text-based format containing information about all versions of all rubygems. // ref: https://guides.rubygems.org/rubygems-org-compact-index-api/ func GetAllPackagesVersions(ctx *context.Context) { packages, err := packages_model.GetPackagesByType(ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems) @@ -326,6 +327,7 @@ func GetAllPackagesVersions(ctx *context.Context) { return } + ephemeralCache := cache.NewEphemeralCache() out := &strings.Builder{} out.WriteString("---\n") for _, pkg := range packages { @@ -338,7 +340,7 @@ func GetAllPackagesVersions(ctx *context.Context) { continue } - info, err := makePackageInfo(ctx, versions) + info, err := makePackageInfo(ctx, versions, ephemeralCache) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return @@ -348,7 +350,14 @@ func GetAllPackagesVersions(ctx *context.Context) { _, _ = fmt.Fprintf(out, "%s ", pkg.Name) for i, v := range versions { sep := util.Iif(i == len(versions)-1, "", ",") - _, _ = fmt.Fprintf(out, "%s%s", v.Version, sep) + pd, err := packages_model.GetPackageDescriptorWithCache(ctx, v, ephemeralCache) + if errors.Is(err, util.ErrNotExist) { + continue + } else if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + writePackageVersionForList(pd.Metadata, v.Version, sep, out) } _, _ = fmt.Fprintf(out, " %x\n", md5.Sum([]byte(info))) } @@ -356,6 +365,16 @@ func GetAllPackagesVersions(ctx *context.Context) { ctx.PlainText(http.StatusOK, out.String()) } +func writePackageVersionForList(metadata any, version, sep string, out *strings.Builder) { + if metadata, _ := metadata.(*rubygems_module.Metadata); metadata != nil && metadata.Platform != "" && metadata.Platform != "ruby" { + // VERSION_PLATFORM (see comment above in GetAllPackagesVersions) + _, _ = fmt.Fprintf(out, "%s_%s%s", version, metadata.Platform, sep) + } else { + // VERSION only + _, _ = fmt.Fprintf(out, "%s%s", version, sep) + } +} + func writePackageVersionRequirements(prefix string, reqs []rubygems_module.VersionRequirement, out *strings.Builder) { out.WriteString(prefix) if len(reqs) == 0 { @@ -367,11 +386,21 @@ func writePackageVersionRequirements(prefix string, reqs []rubygems_module.Versi } } -func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion) (string, error) { +func writePackageVersionForDependency(version, platform string, out *strings.Builder) { + if platform != "" && platform != "ruby" { + // VERSION-PLATFORM (see comment below in makePackageVersionDependency) + _, _ = fmt.Fprintf(out, "%s-%s ", version, platform) + } else { + // VERSION only + _, _ = fmt.Fprintf(out, "%s ", version) + } +} + +func makePackageVersionDependency(ctx *context.Context, version *packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) { // format: VERSION[-PLATFORM] [DEPENDENCY[,DEPENDENCY,...]]|REQUIREMENT[,REQUIREMENT,...] // DEPENDENCY: GEM:CONSTRAINT[&CONSTRAINT] // REQUIREMENT: KEY:VALUE (always contains "checksum") - pd, err := packages_model.GetPackageDescriptor(ctx, version) + pd, err := packages_model.GetPackageDescriptorWithCache(ctx, version, c) if err != nil { return "", err } @@ -388,8 +417,7 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model. } buf := &strings.Builder{} - buf.WriteString(version.Version) - buf.WriteByte(' ') + writePackageVersionForDependency(version.Version, metadata.Platform, buf) for i, dep := range metadata.RuntimeDependencies { sep := util.Iif(i == 0, "", ",") writePackageVersionRequirements(fmt.Sprintf("%s%s:", sep, dep.Name), dep.Version, buf) @@ -404,10 +432,10 @@ func makePackageVersionDependency(ctx *context.Context, version *packages_model. return buf.String(), nil } -func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion) (string, error) { +func makePackageInfo(ctx *context.Context, versions []*packages_model.PackageVersion, c *cache.EphemeralCache) (string, error) { ret := "---\n" for _, v := range versions { - dep, err := makePackageVersionDependency(ctx, v) + dep, err := makePackageVersionDependency(ctx, v, c) if err != nil { return "", err } diff --git a/routers/api/packages/rubygems/rubygems_test.go b/routers/api/packages/rubygems/rubygems_test.go new file mode 100644 index 0000000000..a07e12a7d3 --- /dev/null +++ b/routers/api/packages/rubygems/rubygems_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package rubygems + +import ( + "strings" + "testing" + + rubygems_module "code.gitea.io/gitea/modules/packages/rubygems" + + "github.com/stretchr/testify/assert" +) + +func TestWritePackageVersion(t *testing.T) { + buf := &strings.Builder{} + + writePackageVersionForList(nil, "1.0", " ", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForList(&rubygems_module.Metadata{Platform: "ruby"}, "1.0", " ", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForList(&rubygems_module.Metadata{Platform: "linux"}, "1.0", " ", buf) + assert.Equal(t, "1.0_linux ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "ruby", buf) + assert.Equal(t, "1.0 ", buf.String()) + buf.Reset() + + writePackageVersionForDependency("1.0", "os", buf) + assert.Equal(t, "1.0-os ", buf.String()) + buf.Reset() +} diff --git a/routers/api/packages/swift/swift.go b/routers/api/packages/swift/swift.go index 47439c4c3b..bf542f33a7 100644 --- a/routers/api/packages/swift/swift.go +++ b/routers/api/packages/swift/swift.go @@ -429,7 +429,7 @@ func DownloadPackageFile(ctx *context.Context) { pf := pd.Files[0].File - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { apiError(ctx, http.StatusInternalServerError, err) return diff --git a/routers/api/packages/vagrant/vagrant.go b/routers/api/packages/vagrant/vagrant.go index 3afaa5de1f..9eb67e5397 100644 --- a/routers/api/packages/vagrant/vagrant.go +++ b/routers/api/packages/vagrant/vagrant.go @@ -218,7 +218,7 @@ func UploadPackageFile(ctx *context.Context) { } func DownloadPackageFile(ctx *context.Context) { - s, u, pf, err := packages_service.GetFileStreamByPackageNameAndVersion( + s, u, pf, err := packages_service.OpenFileForDownloadByPackageNameAndVersion( ctx, &packages_service.PackageInfo{ Owner: ctx.Package.Owner, diff --git a/routers/api/v1/admin/action.go b/routers/api/v1/admin/action.go new file mode 100644 index 0000000000..2fbb8e1a95 --- /dev/null +++ b/routers/api/v1/admin/action.go @@ -0,0 +1,93 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package admin + +import ( + "code.gitea.io/gitea/routers/api/v1/shared" + "code.gitea.io/gitea/services/context" +) + +// ListWorkflowJobs Lists all jobs +func ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/jobs admin listAdminWorkflowJobs + // --- + // summary: Lists all jobs + // produces: + // - application/json + // parameters: + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListJobs(ctx, 0, 0, 0) +} + +// ListWorkflowRuns Lists all runs +func ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /admin/actions/runs admin listAdminWorkflowRuns + // --- + // summary: Lists all runs + // produces: + // - application/json + // parameters: + // - name: event + // in: query + // description: workflow event name + // type: string + // required: false + // - name: branch + // in: query + // description: workflow branch + // type: string + // required: false + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowRunsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListRuns(ctx, 0, 0) +} diff --git a/routers/api/v1/admin/org.go b/routers/api/v1/admin/org.go index 8808a1587d..c3473372f2 100644 --- a/routers/api/v1/admin/org.go +++ b/routers/api/v1/admin/org.go @@ -29,7 +29,7 @@ func CreateOrg(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of the user that will own the created organization + // description: username of the user who will own the created organization // type: string // required: true // - name: organization @@ -101,7 +101,7 @@ func GetAllOrgs(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Type: user_model.UserTypeOrganization, OrderBy: db.SearchOrderByAlphabetically, diff --git a/routers/api/v1/admin/repo.go b/routers/api/v1/admin/repo.go index c119d5390a..12a78c9c4b 100644 --- a/routers/api/v1/admin/repo.go +++ b/routers/api/v1/admin/repo.go @@ -22,7 +22,7 @@ func CreateRepo(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of the user. This user will own the created repository + // description: username of the user who will own the created repository // type: string // required: true // - name: repository diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 3ba77604ec..8a267cc418 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -175,7 +175,7 @@ func EditUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to edit + // description: username of the user whose data is to be edited // type: string // required: true // - name: body @@ -239,7 +239,7 @@ func EditUser(ctx *context.APIContext) { Location: optional.FromPtr(form.Location), Description: optional.FromPtr(form.Description), IsActive: optional.FromPtr(form.Active), - IsAdmin: optional.FromPtr(form.Admin), + IsAdmin: user_service.UpdateOptionFieldFromPtr(form.Admin), Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]), AllowGitHook: optional.FromPtr(form.AllowGitHook), AllowImportLocal: optional.FromPtr(form.AllowImportLocal), @@ -272,7 +272,7 @@ func DeleteUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to delete + // description: username of the user to delete // type: string // required: true // - name: purge @@ -328,7 +328,7 @@ func CreatePublicKey(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of the user + // description: username of the user who is to receive a public key // type: string // required: true // - name: key @@ -358,7 +358,7 @@ func DeleteUserPublicKey(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose public key is to be deleted // type: string // required: true // - name: id @@ -405,7 +405,7 @@ func SearchUsers(ctx *context.APIContext) { // format: int64 // - name: login_name // in: query - // description: user's login name to search for + // description: identifier of the user, provided by the external authenticator // type: string // - name: page // in: query @@ -423,7 +423,7 @@ func SearchUsers(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Type: user_model.UserTypeIndividual, LoginName: ctx.FormTrim("login_name"), @@ -456,7 +456,7 @@ func RenameUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: existing username of user + // description: current username of the user // type: string // required: true // - name: body diff --git a/routers/api/v1/admin/user_badge.go b/routers/api/v1/admin/user_badge.go index 6d9665a72b..ce32f455b0 100644 --- a/routers/api/v1/admin/user_badge.go +++ b/routers/api/v1/admin/user_badge.go @@ -22,7 +22,7 @@ func ListUserBadges(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose badges are to be listed // type: string // required: true // responses: @@ -53,7 +53,7 @@ func AddUserBadges(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user to whom a badge is to be added // type: string // required: true // - name: body @@ -87,7 +87,7 @@ func DeleteUserBadges(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose badge is to be deleted // type: string // required: true // - name: body diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 95512cb9b6..4a4bf12657 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -228,7 +228,7 @@ func repoAssignment() func(ctx *context.APIContext) { } } - if !ctx.Repo.Permission.HasAnyUnitAccess() { + if !ctx.Repo.Permission.HasAnyUnitAccessOrPublicAccess() { ctx.APIErrorNotFound() return } @@ -455,15 +455,6 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { } } -// reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin -func reqRepoBranchWriter(ctx *context.APIContext) { - options, ok := web.GetForm(ctx).(api.FileOptionInterface) - if !ok || (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) { - ctx.APIError(http.StatusForbidden, "user should have a permission to write to this branch") - return - } -} - // reqRepoReader user should have specific read permission or be a repo admin or a site admin func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { @@ -744,9 +735,17 @@ func mustEnableWiki(ctx *context.APIContext) { } } +// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor func mustNotBeArchived(ctx *context.APIContext) { if ctx.Repo.Repository.IsArchived { - ctx.APIError(http.StatusLocked, fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString())) + ctx.APIError(http.StatusLocked, fmt.Errorf("%s is archived", ctx.Repo.Repository.FullName())) + return + } +} + +func mustEnableEditor(ctx *context.APIContext) { + if !ctx.Repo.Repository.CanEnableEditor() { + ctx.APIError(http.StatusLocked, fmt.Errorf("%s is not allowed to edit", ctx.Repo.Repository.FullName())) return } } @@ -942,6 +941,8 @@ func Routes() *web.Router { m.Get("/{runner_id}", reqToken(), reqChecker, act.GetRunner) m.Delete("/{runner_id}", reqToken(), reqChecker, act.DeleteRunner) }) + m.Get("/runs", reqToken(), reqChecker, act.ListWorkflowRuns) + m.Get("/jobs", reqToken(), reqChecker, act.ListWorkflowJobs) }) } @@ -971,7 +972,8 @@ func Routes() *web.Router { // Misc (public accessible) m.Group("", func() { m.Get("/version", misc.Version) - m.Get("/signing-key.gpg", misc.SigningKey) + m.Get("/signing-key.gpg", misc.SigningKeyGPG) + m.Get("/signing-key.pub", misc.SigningKeySSH) m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw) @@ -1077,6 +1079,9 @@ func Routes() *web.Router { m.Get("/{runner_id}", reqToken(), user.GetRunner) m.Delete("/{runner_id}", reqToken(), user.DeleteRunner) }) + + m.Get("/runs", reqToken(), user.ListWorkflowRuns) + m.Get("/jobs", reqToken(), user.ListWorkflowJobs) }) m.Get("/followers", user.ListMyFollowers) @@ -1201,6 +1206,7 @@ func Routes() *web.Router { }, context.ReferencesGitRepo(), reqToken(), reqRepoReader(unit.TypeActions)) m.Group("/actions/jobs", func() { + m.Get("/{job_id}", repo.GetWorkflowJob) m.Get("/{job_id}/logs", repo.DownloadActionsRunJobLogs) }, reqToken(), reqRepoReader(unit.TypeActions)) @@ -1241,7 +1247,7 @@ func Routes() *web.Router { }, reqToken()) m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile) m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS) - m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) + m.Methods("HEAD,GET", "/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) m.Combo("/forks").Get(repo.ListForks). Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) m.Post("/merge-upstream", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeCode), bind(api.MergeUpstreamRequest{}), repo.MergeUpstream) @@ -1279,9 +1285,13 @@ func Routes() *web.Router { }, reqToken(), reqAdmin()) m.Group("/actions", func() { m.Get("/tasks", repo.ListActionTasks) - m.Group("/runs/{run}", func() { - m.Get("/artifacts", repo.GetArtifactsOfRun) - m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun) + m.Group("/runs", func() { + m.Group("/{run}", func() { + m.Get("", repo.GetWorkflowRun) + m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun) + m.Get("/jobs", repo.ListWorkflowRunJobs) + m.Get("/artifacts", repo.GetArtifactsOfRun) + }) }) m.Get("/artifacts", repo.GetArtifacts) m.Group("/artifacts/{artifact_id}", func() { @@ -1413,21 +1423,29 @@ func Routes() *web.Router { m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/notes/{sha}", repo.GetNote) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) - m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, repo.ApplyDiffPatch) m.Group("/contents", func() { m.Get("", repo.GetContentsList) m.Get("/*", repo.GetContents) - m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.ChangeFiles) - m.Group("/*", func() { - m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.CreateFile) - m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.UpdateFile) - m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile) - }, reqToken()) + m.Group("", func() { + // "change file" operations, need permission to write to the target branch provided by the form + m.Post("", bind(api.ChangeFilesOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ChangeFiles) + m.Group("/*", func() { + m.Post("", bind(api.CreateFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.CreateFile) + m.Put("", bind(api.UpdateFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.UpdateFile) + m.Delete("", bind(api.DeleteFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.DeleteFile) + }) + m.Post("/diffpatch", bind(api.ApplyDiffPatchFileOptions{}), repo.ReqChangeRepoFileOptionsAndCheck, repo.ApplyDiffPatch) + }, mustEnableEditor, reqToken()) + }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) + m.Group("/contents-ext", func() { + m.Get("", repo.GetContentsExt) + m.Get("/*", repo.GetContentsExt) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()) m.Combo("/file-contents", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()). Get(repo.GetFileContentsGet). - Post(bind(api.GetFilesOptions{}), repo.GetFileContentsPost) // POST method requires "write" permission, so we also support "GET" method above - m.Get("/signing-key.gpg", misc.SigningKey) + Post(bind(api.GetFilesOptions{}), repo.GetFileContentsPost) // the POST method requires "write" permission, so we also support "GET" method above + m.Get("/signing-key.gpg", misc.SigningKeyGPG) + m.Get("/signing-key.pub", misc.SigningKeySSH) m.Group("/topics", func() { m.Combo("").Get(repo.ListTopics). Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics) @@ -1448,7 +1466,7 @@ func Routes() *web.Router { m.Delete("", repo.DeleteAvatar) }, reqAdmin(), reqToken()) - m.Get("/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive) + m.Methods("HEAD,GET", "/{ball_type:tarball|zipball|bundle}/*", reqRepoReader(unit.TypeCode), repo.DownloadArchive) }, repoAssignment(), checkTokenPublicOnly()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) @@ -1732,11 +1750,15 @@ func Routes() *web.Router { Patch(bind(api.EditHookOption{}), admin.EditHook). Delete(admin.DeleteHook) }) - m.Group("/actions/runners", func() { - m.Get("", admin.ListRunners) - m.Post("/registration-token", admin.CreateRegistrationToken) - m.Get("/{runner_id}", admin.GetRunner) - m.Delete("/{runner_id}", admin.DeleteRunner) + m.Group("/actions", func() { + m.Group("/runners", func() { + m.Get("", admin.ListRunners) + m.Post("/registration-token", admin.CreateRegistrationToken) + m.Get("/{runner_id}", admin.GetRunner) + m.Delete("/{runner_id}", admin.DeleteRunner) + }) + m.Get("/runs", admin.ListWorkflowRuns) + m.Get("/jobs", admin.ListWorkflowJobs) }) m.Group("/runners", func() { m.Get("/registration-token", admin.GetRegistrationToken) diff --git a/routers/api/v1/misc/markup.go b/routers/api/v1/misc/markup.go index 0cd4b8c5c5..909310b4c8 100644 --- a/routers/api/v1/misc/markup.go +++ b/routers/api/v1/misc/markup.go @@ -42,7 +42,7 @@ func Markup(ctx *context.APIContext) { return } - mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck + mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath) } @@ -73,7 +73,7 @@ func Markdown(ctx *context.APIContext) { return } - mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck + mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, "") } diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index 667396e39c..db70e04b8f 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -4,14 +4,35 @@ package misc import ( - "fmt" - + "code.gitea.io/gitea/modules/git" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/context" ) -// SigningKey returns the public key of the default signing key if it exists -func SigningKey(ctx *context.APIContext) { +func getSigningKey(ctx *context.APIContext, expectedFormat string) { + // if the handler is in the repo's route group, get the repo's signing key + // otherwise, get the global signing key + path := "" + if ctx.Repo != nil && ctx.Repo.Repository != nil { + path = ctx.Repo.Repository.RepoPath() + } + content, format, err := asymkey_service.PublicSigningKey(ctx, path) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if format == "" { + ctx.APIErrorNotFound("no signing key") + return + } else if format != expectedFormat { + ctx.APIErrorNotFound("signing key format is " + format) + return + } + _, _ = ctx.Write([]byte(content)) +} + +// SigningKeyGPG returns the public key of the default signing key if it exists +func SigningKeyGPG(ctx *context.APIContext) { // swagger:operation GET /signing-key.gpg miscellaneous getSigningKey // --- // summary: Get default signing-key.gpg @@ -44,19 +65,42 @@ func SigningKey(ctx *context.APIContext) { // description: "GPG armored public key" // schema: // type: string + getSigningKey(ctx, git.SigningKeyFormatOpenPGP) +} - path := "" - if ctx.Repo != nil && ctx.Repo.Repository != nil { - path = ctx.Repo.Repository.RepoPath() - } +// SigningKeySSH returns the public key of the default signing key if it exists +func SigningKeySSH(ctx *context.APIContext) { + // swagger:operation GET /signing-key.pub miscellaneous getSigningKeySSH + // --- + // summary: Get default signing-key.pub + // produces: + // - text/plain + // responses: + // "200": + // description: "ssh public key" + // schema: + // type: string - content, err := asymkey_service.PublicSigningKey(ctx, path) - if err != nil { - ctx.APIErrorInternal(err) - return - } - _, err = ctx.Write([]byte(content)) - if err != nil { - ctx.APIErrorInternal(fmt.Errorf("Error writing key content %w", err)) - } + // swagger:operation GET /repos/{owner}/{repo}/signing-key.pub repository repoSigningKeySSH + // --- + // summary: Get signing-key.pub for given repository + // produces: + // - text/plain + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // description: "ssh public key" + // schema: + // type: string + getSigningKey(ctx, git.SigningKeyFormatSSH) } diff --git a/routers/api/v1/org/action.go b/routers/api/v1/org/action.go index 700a5ef8ea..3ae5e60585 100644 --- a/routers/api/v1/org/action.go +++ b/routers/api/v1/org/action.go @@ -384,13 +384,13 @@ func (Action) CreateVariable(ctx *context.APIContext) { // "$ref": "#/definitions/CreateVariableOption" // responses: // "201": - // description: response when creating an org-level variable - // "204": - // description: response when creating an org-level variable + // description: successfully created the org-level variable // "400": // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" + // "409": + // description: variable name already exists. + // "500": + // "$ref": "#/responses/error" opt := web.GetForm(ctx).(*api.CreateVariableOption) @@ -419,7 +419,7 @@ func (Action) CreateVariable(ctx *context.APIContext) { return } - ctx.Status(http.StatusNoContent) + ctx.Status(http.StatusCreated) } // UpdateVariable update an org-level variable @@ -570,6 +570,96 @@ func (Action) DeleteRunner(ctx *context.APIContext) { shared.DeleteRunner(ctx, ctx.Org.Organization.ID, 0, ctx.PathParamInt64("runner_id")) } +func (Action) ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/jobs organization getOrgWorkflowJobs + // --- + // summary: Get org-level workflow jobs + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListJobs(ctx, ctx.Org.Organization.ID, 0, 0) +} + +func (Action) ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/actions/runs organization getOrgWorkflowRuns + // --- + // summary: Get org-level workflow runs + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: name of the organization + // type: string + // required: true + // - name: event + // in: query + // description: workflow event name + // type: string + // required: false + // - name: branch + // in: query + // description: workflow branch + // type: string + // required: false + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowRunsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRuns(ctx, ctx.Org.Organization.ID, 0) +} + var _ actions_service.API = new(Action) // Action implements actions_service.API diff --git a/routers/api/v1/org/block.go b/routers/api/v1/org/block.go index 69a5222a20..6b2f3dc615 100644 --- a/routers/api/v1/org/block.go +++ b/routers/api/v1/org/block.go @@ -47,7 +47,7 @@ func CheckUserBlock(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: user to check + // description: username of the user to check // type: string // required: true // responses: @@ -71,7 +71,7 @@ func BlockUser(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: user to block + // description: username of the user to block // type: string // required: true // - name: note @@ -101,7 +101,7 @@ func UnblockUser(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: user to unblock + // description: username of the user to unblock // type: string // required: true // responses: diff --git a/routers/api/v1/org/member.go b/routers/api/v1/org/member.go index a1875a7886..1c12b0cc94 100644 --- a/routers/api/v1/org/member.go +++ b/routers/api/v1/org/member.go @@ -133,7 +133,7 @@ func IsMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user to check for an organization membership // type: string // required: true // responses: @@ -186,7 +186,7 @@ func IsPublicMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user to check for a public organization membership // type: string // required: true // responses: @@ -240,7 +240,7 @@ func PublicizeMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user whose membership is to be publicized // type: string // required: true // responses: @@ -282,7 +282,7 @@ func ConcealMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user whose membership is to be concealed // type: string // required: true // responses: @@ -324,7 +324,7 @@ func DeleteMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user + // description: username of the user to remove from the organization // type: string // required: true // responses: diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index c9208f4757..05744ba155 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -26,12 +26,10 @@ import ( func listUserOrgs(ctx *context.APIContext, u *user_model.User) { listOptions := utils.GetListOptions(ctx) - showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == u.ID) - opts := organization.FindOrgOptions{ - ListOptions: listOptions, - UserID: u.ID, - IncludePrivate: showPrivate, + ListOptions: listOptions, + UserID: u.ID, + IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, u), } orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts) if err != nil { @@ -84,7 +82,7 @@ func ListUserOrgs(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose organizations are to be listed // type: string // required: true // - name: page @@ -114,7 +112,7 @@ func GetUserOrgsPermissions(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose permissions are to be obtained // type: string // required: true // - name: org @@ -201,7 +199,7 @@ func GetAll(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - publicOrgs, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + publicOrgs, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, ListOptions: listOptions, Type: user_model.UserTypeOrganization, diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 71c21f2dde..1a1710750a 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -426,7 +426,7 @@ func GetTeamMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the member to list + // description: username of the user whose data is to be listed // type: string // required: true // responses: @@ -467,7 +467,7 @@ func AddTeamMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user to add + // description: username of the user to add to a team // type: string // required: true // responses: @@ -509,7 +509,7 @@ func RemoveTeamMember(ctx *context.APIContext) { // required: true // - name: username // in: path - // description: username of the user to remove + // description: username of the user to remove from a team // type: string // required: true // responses: diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index 237250b2c5..a57db015f0 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -339,12 +339,12 @@ func (Action) CreateVariable(ctx *context.APIContext) { // responses: // "201": // description: response when creating a repo-level variable - // "204": - // description: response when creating a repo-level variable // "400": // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" + // "409": + // description: variable name already exists. + // "500": + // "$ref": "#/responses/error" opt := web.GetForm(ctx).(*api.CreateVariableOption) @@ -373,7 +373,7 @@ func (Action) CreateVariable(ctx *context.APIContext) { return } - ctx.Status(http.StatusNoContent) + ctx.Status(http.StatusCreated) } // UpdateVariable update a repo-level variable @@ -650,6 +650,114 @@ func (Action) DeleteRunner(ctx *context.APIContext) { shared.DeleteRunner(ctx, 0, ctx.Repo.Repository.ID, ctx.PathParamInt64("runner_id")) } +// GetWorkflowRunJobs Lists all jobs for a workflow run. +func (Action) ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/jobs repository listWorkflowJobs + // --- + // summary: Lists all jobs for a repository + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + shared.ListJobs(ctx, 0, repoID, 0) +} + +// ListWorkflowRuns Lists all runs for a repository run. +func (Action) ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs repository getWorkflowRuns + // --- + // summary: Lists all runs for a repository run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: event + // in: query + // description: workflow event name + // type: string + // required: false + // - name: branch + // in: query + // description: workflow branch + // type: string + // required: false + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ArtifactsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + shared.ListRuns(ctx, 0, repoID) +} + var _ actions_service.API = new(Action) // Action implements actions_service.API @@ -756,7 +864,7 @@ func ActionsListRepositoryWorkflows(ctx *context.APIContext) { // "500": // "$ref": "#/responses/error" - workflows, err := actions_service.ListActionWorkflows(ctx) + workflows, err := convert.ListActionWorkflows(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository) if err != nil { ctx.APIErrorInternal(err) return @@ -802,7 +910,7 @@ func ActionsGetWorkflow(ctx *context.APIContext) { // "$ref": "#/responses/error" workflowID := ctx.PathParam("workflow_id") - workflow, err := actions_service.GetActionWorkflow(ctx, workflowID) + workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.APIError(http.StatusNotFound, err) @@ -992,6 +1100,157 @@ func ActionsEnableWorkflow(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } +// GetWorkflowRun Gets a specific workflow run. +func GetWorkflowRun(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun + // --- + // summary: Gets a specific workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: id of the run + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/WorkflowRun" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + runID := ctx.PathParamInt64("run") + job, _, err := db.GetByID[actions_model.ActionRun](ctx, runID) + + if err != nil || job.RepoID != ctx.Repo.Repository.ID { + ctx.APIError(http.StatusNotFound, util.ErrNotExist) + } + + convertedArtifact, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedArtifact) +} + +// ListWorkflowRunJobs Lists all jobs for a workflow run. +func ListWorkflowRunJobs(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs + // --- + // summary: Lists all jobs for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: runid of the workflow run + // type: integer + // required: true + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.Repo.Repository.ID + + runID := ctx.PathParamInt64("run") + + // Avoid the list all jobs functionality for this api route to be used with a runID == 0. + if runID <= 0 { + ctx.APIError(http.StatusBadRequest, util.NewInvalidArgumentErrorf("runID must be a positive integer")) + return + } + + // runID is used as an additional filter next to repoID to ensure that we only list jobs for the specified repoID and runID. + // no additional checks for runID are needed here + shared.ListJobs(ctx, 0, repoID, runID) +} + +// GetWorkflowJob Gets a specific workflow job for a workflow run. +func GetWorkflowJob(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/actions/jobs/{job_id} repository getWorkflowJob + // --- + // summary: Gets a specific workflow job for a workflow run + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: name of the owner + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: job_id + // in: path + // description: id of the job + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/WorkflowJob" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + jobID := ctx.PathParamInt64("job_id") + job, _, err := db.GetByID[actions_model.ActionRunJob](ctx, jobID) + + if err != nil || job.RepoID != ctx.Repo.Repository.ID { + ctx.APIError(http.StatusNotFound, util.ErrNotExist) + } + + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, job) + if err != nil { + ctx.APIErrorInternal(err) + return + } + ctx.JSON(http.StatusOK, convertedWorkflowJob) +} + // GetArtifacts Lists all artifacts for a repository. func GetArtifactsOfRun(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/artifacts repository getArtifactsOfRun diff --git a/routers/api/v1/repo/blob.go b/routers/api/v1/repo/blob.go index d1cb72f5f1..9a17fc1bbf 100644 --- a/routers/api/v1/repo/blob.go +++ b/routers/api/v1/repo/blob.go @@ -47,7 +47,7 @@ func GetBlob(ctx *context.APIContext) { return } - if blob, err := files_service.GetBlobBySHA(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil { + if blob, err := files_service.GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, sha); err != nil { ctx.APIError(http.StatusBadRequest, err) } else { ctx.JSON(http.StatusOK, blob) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index fe82550fdd..9af958a5b7 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -224,9 +224,9 @@ func CreateBranch(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - } else if len(opt.OldBranchName) > 0 { //nolint - if gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, opt.OldBranchName) { //nolint - oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint + } else if len(opt.OldBranchName) > 0 { //nolint:staticcheck // deprecated field + if gitrepo.IsBranchExist(ctx, ctx.Repo.Repository, opt.OldBranchName) { //nolint:staticcheck // deprecated field + oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint:staticcheck // deprecated field if err != nil { ctx.APIErrorInternal(err) return @@ -579,7 +579,7 @@ func CreateBranchProtection(ctx *context.APIContext) { ruleName := form.RuleName if ruleName == "" { - ruleName = form.BranchName //nolint + ruleName = form.BranchName //nolint:staticcheck // deprecated field } if len(ruleName) == 0 { ctx.APIError(http.StatusBadRequest, "both rule_name and branch_name are empty") @@ -1181,7 +1181,7 @@ func MergeUpstream(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.MergeUpstreamRequest) - mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch) + mergeStyle, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, form.Branch, form.FfOnly) if err != nil { if errors.Is(err, util.ErrInvalidArgument) { ctx.APIError(http.StatusBadRequest, err) diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index d1652c1d51..c2c10cc695 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -93,7 +93,7 @@ func IsCollaborator(ctx *context.APIContext) { // required: true // - name: collaborator // in: path - // description: username of the collaborator + // description: username of the user to check for being a collaborator // type: string // required: true // responses: @@ -145,7 +145,7 @@ func AddOrUpdateCollaborator(ctx *context.APIContext) { // required: true // - name: collaborator // in: path - // description: username of the collaborator to add + // description: username of the user to add or update as a collaborator // type: string // required: true // - name: body @@ -264,7 +264,7 @@ func GetRepoPermissions(ctx *context.APIContext) { // required: true // - name: collaborator // in: path - // description: username of the collaborator + // description: username of the collaborator whose permissions are to be obtained // type: string // required: true // responses: diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index f40d39a251..69b5996222 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -62,7 +62,7 @@ func GetRawFile(ctx *context.APIContext) { // required: true // - name: ref // in: query - // description: "The name of the commit/branch/tag. Default the repository’s default branch" + // description: "The name of the commit/branch/tag. Default to the repository’s default branch" // type: string // required: false // responses: @@ -115,7 +115,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { // required: true // - name: ref // in: query - // description: "The name of the commit/branch/tag. Default the repository’s default branch" + // description: "The name of the commit/branch/tag. Default to the repository’s default branch" // type: string // required: false // responses: @@ -139,27 +139,27 @@ func GetRawFileOrLFS(ctx *context.APIContext) { ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry))) // LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file - if blob.Size() > 1024 { + if blob.Size() > lfs.MetaFileMaxSize { // First handle caching for the blob if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { return } - // OK not cached - serve! + // If not cached - serve! if err := common.ServeBlob(ctx.Base, ctx.Repo.Repository, ctx.Repo.TreePath, blob, lastModified); err != nil { ctx.APIErrorInternal(err) } return } - // OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice) + // OK, now the blob is known to have at most 1024 (lfs pointer max size) bytes, + // we can simply read this in one go (This saves reading it twice) dataRc, err := blob.DataAsync() if err != nil { ctx.APIErrorInternal(err) return } - // FIXME: code from #19689, what if the file is large ... OOM ... buf, err := io.ReadAll(dataRc) if err != nil { _ = dataRc.Close() @@ -181,7 +181,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) { return } - // OK not cached - serve! + // If not cached - serve! common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf)) return } @@ -405,13 +405,6 @@ func GetEditorconfig(ctx *context.APIContext) { ctx.JSON(http.StatusOK, def) } -// canWriteFiles returns true if repository is editable and user has proper access level. -func canWriteFiles(ctx *context.APIContext, branch string) bool { - return ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, branch) && - !ctx.Repo.Repository.IsMirror && - !ctx.Repo.Repository.IsArchived -} - func base64Reader(s string) (io.ReadSeeker, error) { b, err := base64.StdEncoding.DecodeString(s) if err != nil { @@ -420,6 +413,45 @@ func base64Reader(s string) (io.ReadSeeker, error) { return bytes.NewReader(b), nil } +func ReqChangeRepoFileOptionsAndCheck(ctx *context.APIContext) { + commonOpts := web.GetForm(ctx).(api.FileOptionsInterface).GetFileOptions() + commonOpts.BranchName = util.IfZero(commonOpts.BranchName, ctx.Repo.Repository.DefaultBranch) + commonOpts.NewBranchName = util.IfZero(commonOpts.NewBranchName, commonOpts.BranchName) + if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, commonOpts.NewBranchName) && !ctx.IsUserSiteAdmin() { + ctx.APIError(http.StatusForbidden, "user should have a permission to write to the target branch") + return + } + changeFileOpts := &files_service.ChangeRepoFilesOptions{ + Message: commonOpts.Message, + OldBranch: commonOpts.BranchName, + NewBranch: commonOpts.NewBranchName, + Committer: &files_service.IdentityOptions{ + GitUserName: commonOpts.Committer.Name, + GitUserEmail: commonOpts.Committer.Email, + }, + Author: &files_service.IdentityOptions{ + GitUserName: commonOpts.Author.Name, + GitUserEmail: commonOpts.Author.Email, + }, + Dates: &files_service.CommitDateOptions{ + Author: commonOpts.Dates.Author, + Committer: commonOpts.Dates.Committer, + }, + Signoff: commonOpts.Signoff, + } + if commonOpts.Dates.Author.IsZero() { + commonOpts.Dates.Author = time.Now() + } + if commonOpts.Dates.Committer.IsZero() { + commonOpts.Dates.Committer = time.Now() + } + ctx.Data["__APIChangeRepoFilesOptions"] = changeFileOpts +} + +func getAPIChangeRepoFileOptions[T api.FileOptionsInterface](ctx *context.APIContext) (apiOpts T, opts *files_service.ChangeRepoFilesOptions) { + return web.GetForm(ctx).(T), ctx.Data["__APIChangeRepoFilesOptions"].(*files_service.ChangeRepoFilesOptions) +} + // ChangeFiles handles API call for modifying multiple files func ChangeFiles(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles @@ -456,20 +488,18 @@ func ChangeFiles(ctx *context.APIContext) { // "$ref": "#/responses/error" // "423": // "$ref": "#/responses/repoArchivedError" - - apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions) - - if apiOpts.BranchName == "" { - apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch + apiOpts, opts := getAPIChangeRepoFileOptions[*api.ChangeFilesOptions](ctx) + if ctx.Written() { + return } - - var files []*files_service.ChangeRepoFile for _, file := range apiOpts.Files { contentReader, err := base64Reader(file.ContentBase64) if err != nil { ctx.APIError(http.StatusUnprocessableEntity, err) return } + // FIXME: ChangeFileOperation.SHA is NOT required for update or delete if last commit is provided in the options + // But the LastCommitID is not provided in the API options, need to fully fix them in API changeRepoFile := &files_service.ChangeRepoFile{ Operation: file.Operation, TreePath: file.Path, @@ -477,41 +507,15 @@ func ChangeFiles(ctx *context.APIContext) { ContentReader: contentReader, SHA: file.SHA, } - files = append(files, changeRepoFile) - } - - opts := &files_service.ChangeRepoFilesOptions{ - Files: files, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files_service.IdentityOptions{ - GitUserName: apiOpts.Committer.Name, - GitUserEmail: apiOpts.Committer.Email, - }, - Author: &files_service.IdentityOptions{ - GitUserName: apiOpts.Author.Name, - GitUserEmail: apiOpts.Author.Email, - }, - Dates: &files_service.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() + opts.Files = append(opts.Files, changeRepoFile) } if opts.Message == "" { - opts.Message = changeFilesCommitMessage(ctx, files) + opts.Message = changeFilesCommitMessage(ctx, opts.Files) } - if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil { - handleCreateOrUpdateFileError(ctx, err) + if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { + handleChangeRepoFilesError(ctx, err) } else { ctx.JSON(http.StatusCreated, filesResponse) } @@ -559,56 +563,27 @@ func CreateFile(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" - apiOpts := web.GetForm(ctx).(*api.CreateFileOptions) - - if apiOpts.BranchName == "" { - apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch + apiOpts, opts := getAPIChangeRepoFileOptions[*api.CreateFileOptions](ctx) + if ctx.Written() { + return } - contentReader, err := base64Reader(apiOpts.ContentBase64) if err != nil { ctx.APIError(http.StatusUnprocessableEntity, err) return } - opts := &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "create", - TreePath: ctx.PathParam("*"), - ContentReader: contentReader, - }, - }, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files_service.IdentityOptions{ - GitUserName: apiOpts.Committer.Name, - GitUserEmail: apiOpts.Committer.Email, - }, - Author: &files_service.IdentityOptions{ - GitUserName: apiOpts.Author.Name, - GitUserEmail: apiOpts.Author.Email, - }, - Dates: &files_service.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() - } - + opts.Files = append(opts.Files, &files_service.ChangeRepoFile{ + Operation: "create", + TreePath: ctx.PathParam("*"), + ContentReader: contentReader, + }) if opts.Message == "" { opts.Message = changeFilesCommitMessage(ctx, opts.Files) } - if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil { - handleCreateOrUpdateFileError(ctx, err) + if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { + handleChangeRepoFilesError(ctx, err) } else { fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) ctx.JSON(http.StatusCreated, fileResponse) @@ -656,96 +631,55 @@ func UpdateFile(ctx *context.APIContext) { // "$ref": "#/responses/error" // "423": // "$ref": "#/responses/repoArchivedError" - apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions) - if ctx.Repo.Repository.IsEmpty { - ctx.APIError(http.StatusUnprocessableEntity, errors.New("repo is empty")) - return - } - if apiOpts.BranchName == "" { - apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch + apiOpts, opts := getAPIChangeRepoFileOptions[*api.UpdateFileOptions](ctx) + if ctx.Written() { + return } - contentReader, err := base64Reader(apiOpts.ContentBase64) if err != nil { ctx.APIError(http.StatusUnprocessableEntity, err) return } - - opts := &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "update", - ContentReader: contentReader, - SHA: apiOpts.SHA, - FromTreePath: apiOpts.FromPath, - TreePath: ctx.PathParam("*"), - }, - }, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files_service.IdentityOptions{ - GitUserName: apiOpts.Committer.Name, - GitUserEmail: apiOpts.Committer.Email, - }, - Author: &files_service.IdentityOptions{ - GitUserName: apiOpts.Author.Name, - GitUserEmail: apiOpts.Author.Email, - }, - Dates: &files_service.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() - } - + opts.Files = append(opts.Files, &files_service.ChangeRepoFile{ + Operation: "update", + ContentReader: contentReader, + SHA: apiOpts.SHA, + FromTreePath: apiOpts.FromPath, + TreePath: ctx.PathParam("*"), + }) if opts.Message == "" { opts.Message = changeFilesCommitMessage(ctx, opts.Files) } - if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil { - handleCreateOrUpdateFileError(ctx, err) + if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { + handleChangeRepoFilesError(ctx, err) } else { fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) ctx.JSON(http.StatusOK, fileResponse) } } -func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) { +func handleChangeRepoFilesError(ctx *context.APIContext, err error) { if files_service.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) { ctx.APIError(http.StatusForbidden, err) return } if git_model.IsErrBranchAlreadyExists(err) || files_service.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) || - files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) { + files_service.IsErrFilePathInvalid(err) || files_service.IsErrRepoFileAlreadyExists(err) || + files_service.IsErrCommitIDDoesNotMatch(err) || files_service.IsErrSHAOrCommitIDNotProvided(err) { ctx.APIError(http.StatusUnprocessableEntity, err) return } - if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) { + if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { ctx.APIError(http.StatusNotFound, err) return } - - ctx.APIErrorInternal(err) -} - -// Called from both CreateFile or UpdateFile to handle both -func createOrUpdateFiles(ctx *context.APIContext, opts *files_service.ChangeRepoFilesOptions) (*api.FilesResponse, error) { - if !canWriteFiles(ctx, opts.OldBranch) { - return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{ - UserID: ctx.Doer.ID, - RepoName: ctx.Repo.Repository.LowerName, - } + if errors.Is(err, util.ErrNotExist) { + ctx.APIError(http.StatusNotFound, err) + return } - - return files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts) + ctx.APIErrorInternal(err) } // format commit message if empty @@ -759,7 +693,7 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch switch file.Operation { case "create": createFiles = append(createFiles, file.TreePath) - case "update": + case "update", "upload", "rename": // upload and rename works like "update", there is no translation for them at the moment updateFiles = append(updateFiles, file.TreePath) case "delete": deleteFiles = append(deleteFiles, file.TreePath) @@ -817,74 +751,27 @@ func DeleteFile(ctx *context.APIContext) { // "$ref": "#/responses/error" // "404": // "$ref": "#/responses/error" + // "422": + // "$ref": "#/responses/error" // "423": // "$ref": "#/responses/repoArchivedError" - apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions) - if !canWriteFiles(ctx, apiOpts.BranchName) { - ctx.APIError(http.StatusForbidden, repo_model.ErrUserDoesNotHaveAccessToRepo{ - UserID: ctx.Doer.ID, - RepoName: ctx.Repo.Repository.LowerName, - }) + apiOpts, opts := getAPIChangeRepoFileOptions[*api.DeleteFileOptions](ctx) + if ctx.Written() { return } - if apiOpts.BranchName == "" { - apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch - } - - opts := &files_service.ChangeRepoFilesOptions{ - Files: []*files_service.ChangeRepoFile{ - { - Operation: "delete", - SHA: apiOpts.SHA, - TreePath: ctx.PathParam("*"), - }, - }, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files_service.IdentityOptions{ - GitUserName: apiOpts.Committer.Name, - GitUserEmail: apiOpts.Committer.Email, - }, - Author: &files_service.IdentityOptions{ - GitUserName: apiOpts.Author.Name, - GitUserEmail: apiOpts.Author.Email, - }, - Dates: &files_service.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() - } - + opts.Files = append(opts.Files, &files_service.ChangeRepoFile{ + Operation: "delete", + SHA: apiOpts.SHA, + TreePath: ctx.PathParam("*"), + }) if opts.Message == "" { opts.Message = changeFilesCommitMessage(ctx, opts.Files) } if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { - if git.IsErrBranchNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) { - ctx.APIError(http.StatusNotFound, err) - return - } else if git_model.IsErrBranchAlreadyExists(err) || - files_service.IsErrFilenameInvalid(err) || - pull_service.IsErrSHADoesNotMatch(err) || - files_service.IsErrCommitIDDoesNotMatch(err) || - files_service.IsErrSHAOrCommitIDNotProvided(err) { - ctx.APIError(http.StatusBadRequest, err) - return - } else if files_service.IsErrUserCannotCommit(err) { - ctx.APIError(http.StatusForbidden, err) - return - } - ctx.APIErrorInternal(err) + handleChangeRepoFilesError(ctx, err) } else { fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0) ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent @@ -902,11 +789,81 @@ func resolveRefCommit(ctx *context.APIContext, ref string, minCommitIDLen ...int return refCommit } -// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir +func GetContentsExt(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/contents-ext/{filepath} repository repoGetContentsExt + // --- + // summary: The extended "contents" API, to get file metadata and/or content, or list a directory. + // description: It guarantees that only one of the response fields is set if the request succeeds. + // Users can pass "includes=file_content" or "includes=lfs_metadata" to retrieve more fields. + // "includes=file_content" only works for single file, if you need to retrieve file contents in batch, + // use "file-contents" API after listing the directory. + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: filepath + // in: path + // description: path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be "required", + // you can leave it empty or pass a single dot (".") to get the root directory. + // type: string + // required: true + // - name: ref + // in: query + // description: the name of the commit/branch/tag, default to the repository’s default branch. + // type: string + // required: false + // - name: includes + // in: query + // description: By default this API's response only contains file's metadata. Use comma-separated "includes" options to retrieve more fields. + // Option "file_content" will try to retrieve the file content, "lfs_metadata" will try to retrieve LFS metadata, + // "commit_metadata" will try to retrieve commit metadata, and "commit_message" will try to retrieve commit message. + // type: string + // required: false + // responses: + // "200": + // "$ref": "#/responses/ContentsExtResponse" + // "404": + // "$ref": "#/responses/notFound" + + if treePath := ctx.PathParam("*"); treePath == "." || treePath == "/" { + ctx.SetPathParam("*", "") // workaround for swagger, it requires path parameter to be "required", but we need to list root directory + } + opts := files_service.GetContentsOrListOptions{TreePath: ctx.PathParam("*")} + for includeOpt := range strings.SplitSeq(ctx.FormString("includes"), ",") { + if includeOpt == "" { + continue + } + switch includeOpt { + case "file_content": + opts.IncludeSingleFileContent = true + case "lfs_metadata": + opts.IncludeLfsMetadata = true + case "commit_metadata": + opts.IncludeCommitMetadata = true + case "commit_message": + opts.IncludeCommitMessage = true + default: + ctx.APIError(http.StatusBadRequest, fmt.Sprintf("unknown include option %q", includeOpt)) + return + } + } + ctx.JSON(http.StatusOK, getRepoContents(ctx, opts)) +} + func GetContents(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents // --- - // summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir + // summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir. + // description: This API follows GitHub's design, and it is not easy to use. Recommend users to use the "contents-ext" API instead. // produces: // - application/json // parameters: @@ -935,29 +892,38 @@ func GetContents(ctx *context.APIContext) { // "$ref": "#/responses/ContentsResponse" // "404": // "$ref": "#/responses/notFound" - - treePath := ctx.PathParam("*") - refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref")) + ret := getRepoContents(ctx, files_service.GetContentsOrListOptions{ + TreePath: ctx.PathParam("*"), + IncludeSingleFileContent: true, + IncludeCommitMetadata: true, + }) if ctx.Written() { return } + ctx.JSON(http.StatusOK, util.Iif[any](ret.FileContents != nil, ret.FileContents, ret.DirContents)) +} - if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, refCommit, treePath); err != nil { +func getRepoContents(ctx *context.APIContext, opts files_service.GetContentsOrListOptions) *api.ContentsExtResponse { + refCommit := resolveRefCommit(ctx, ctx.FormTrim("ref")) + if ctx.Written() { + return nil + } + ret, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts) + if err != nil { if git.IsErrNotExist(err) { ctx.APIErrorNotFound("GetContentsOrList", err) - return + return nil } ctx.APIErrorInternal(err) - } else { - ctx.JSON(http.StatusOK, fileList) } + return &ret } -// GetContentsList Get the metadata of all the entries of the root dir func GetContentsList(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList // --- - // summary: Gets the metadata of all the entries of the root dir + // summary: Gets the metadata of all the entries of the root dir. + // description: This API follows GitHub's design, and it is not easy to use. Recommend users to use our "contents-ext" API instead. // produces: // - application/json // parameters: @@ -990,7 +956,7 @@ func GetFileContentsGet(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/file-contents repository repoGetFileContents // --- // summary: Get the metadata and contents of requested files - // description: See the POST method. This GET method supports to use JSON encoded request body in query parameter. + // description: See the POST method. This GET method supports using JSON encoded request body in query parameter. // produces: // - application/json // parameters: @@ -1020,7 +986,7 @@ func GetFileContentsGet(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - // POST method requires "write" permission, so we also support this "GET" method + // The POST method requires "write" permission, so we also support this "GET" method handleGetFileContents(ctx) } @@ -1064,7 +1030,7 @@ func GetFileContentsPost(ctx *context.APIContext) { // This is actually a "read" request, but we need to accept a "files" list, then POST method seems easy to use. // But the permission system requires that the caller must have "write" permission to use POST method. - // At the moment there is no other way to get around the permission check, so there is a "GET" workaround method above. + // At the moment, there is no other way to get around the permission check, so there is a "GET" workaround method above. handleGetFileContents(ctx) } @@ -1081,6 +1047,6 @@ func handleGetFileContents(ctx *context.APIContext) { if ctx.Written() { return } - filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, refCommit, opts.Files) + filesResponse := files_service.GetContentsListFromTreePaths(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, refCommit, opts.Files) ctx.JSON(http.StatusOK, util.SliceNilAsEmpty(filesResponse)) } diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index b9a71982d0..d4a5872fd1 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -152,7 +152,7 @@ func SearchIssues(ctx *context.APIContext) { ) { // find repos user can access (for issue search) - opts := &repo_model.SearchRepoOptions{ + opts := repo_model.SearchRepoOptions{ Private: false, AllPublic: true, TopicOnly: false, diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 2048c76ea0..1b58beb7b6 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -77,10 +77,7 @@ func GetIssueDependencies(ctx *context.APIContext) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) limit := ctx.FormInt("limit") if limit == 0 { limit = setting.API.DefaultPagingNum @@ -328,10 +325,7 @@ func GetIssueBlocks(ctx *context.APIContext) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) limit := ctx.FormInt("limit") if limit <= 1 { limit = setting.API.DefaultPagingNum diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go index b18e172b37..0f28b9757d 100644 --- a/routers/api/v1/repo/issue_stopwatch.go +++ b/routers/api/v1/repo/issue_stopwatch.go @@ -4,7 +4,6 @@ package repo import ( - "errors" "net/http" issues_model "code.gitea.io/gitea/models/issues" @@ -49,14 +48,17 @@ func StartIssueStopwatch(ctx *context.APIContext) { // "409": // description: Cannot start a stopwatch again if it already exists - issue, err := prepareIssueStopwatch(ctx, false) - if err != nil { + issue := prepareIssueForStopwatch(ctx) + if ctx.Written() { return } - if err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil { + if ok, err := issues_model.CreateIssueStopwatch(ctx, ctx.Doer, issue); err != nil { ctx.APIErrorInternal(err) return + } else if !ok { + ctx.APIError(http.StatusConflict, "cannot start a stopwatch again if it already exists") + return } ctx.Status(http.StatusCreated) @@ -96,18 +98,20 @@ func StopIssueStopwatch(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" // "409": - // description: Cannot stop a non existent stopwatch + // description: Cannot stop a non-existent stopwatch - issue, err := prepareIssueStopwatch(ctx, true) - if err != nil { + issue := prepareIssueForStopwatch(ctx) + if ctx.Written() { return } - if err := issues_model.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil { + if ok, err := issues_model.FinishIssueStopwatch(ctx, ctx.Doer, issue); err != nil { ctx.APIErrorInternal(err) return + } else if !ok { + ctx.APIError(http.StatusConflict, "cannot stop a non-existent stopwatch") + return } - ctx.Status(http.StatusCreated) } @@ -145,22 +149,25 @@ func DeleteIssueStopwatch(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" // "409": - // description: Cannot cancel a non existent stopwatch + // description: Cannot cancel a non-existent stopwatch - issue, err := prepareIssueStopwatch(ctx, true) - if err != nil { + issue := prepareIssueForStopwatch(ctx) + if ctx.Written() { return } - if err := issues_model.CancelStopwatch(ctx, ctx.Doer, issue); err != nil { + if ok, err := issues_model.CancelStopwatch(ctx, ctx.Doer, issue); err != nil { ctx.APIErrorInternal(err) return + } else if !ok { + ctx.APIError(http.StatusConflict, "cannot cancel a non-existent stopwatch") + return } ctx.Status(http.StatusNoContent) } -func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_model.Issue, error) { +func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue { issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index")) if err != nil { if issues_model.IsErrIssueNotExist(err) { @@ -168,32 +175,19 @@ func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*issues_m } else { ctx.APIErrorInternal(err) } - - return nil, err + return nil } if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { ctx.Status(http.StatusForbidden) - return nil, errors.New("Unable to write to PRs") + return nil } if !ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) { ctx.Status(http.StatusForbidden) - return nil, errors.New("Cannot use time tracker") - } - - if issues_model.StopwatchExists(ctx, ctx.Doer.ID, issue.ID) != shouldExist { - if shouldExist { - ctx.APIError(http.StatusConflict, "cannot stop/cancel a non existent stopwatch") - err = errors.New("cannot stop/cancel a non existent stopwatch") - } else { - ctx.APIError(http.StatusConflict, "cannot start a stopwatch again if it already exists") - err = errors.New("cannot start a stopwatch again if it already exists") - } - return nil, err + return nil } - - return issue, nil + return issue } // GetStopwatches get all stopwatches diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go index 21e549496d..c89f228a06 100644 --- a/routers/api/v1/repo/issue_subscription.go +++ b/routers/api/v1/repo/issue_subscription.go @@ -43,7 +43,7 @@ func AddIssueSubscription(ctx *context.APIContext) { // required: true // - name: user // in: path - // description: user to subscribe + // description: username of the user to subscribe the issue to // type: string // required: true // responses: @@ -87,7 +87,7 @@ func DelIssueSubscription(ctx *context.APIContext) { // required: true // - name: user // in: path - // description: user witch unsubscribe + // description: username of the user to unsubscribe from an issue // type: string // required: true // responses: diff --git a/routers/api/v1/repo/issue_tracked_time.go b/routers/api/v1/repo/issue_tracked_time.go index dd6abf94c6..171da272cc 100644 --- a/routers/api/v1/repo/issue_tracked_time.go +++ b/routers/api/v1/repo/issue_tracked_time.go @@ -405,7 +405,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) { // required: true // - name: user // in: path - // description: username of user + // description: username of the user whose tracked times are to be listed // type: string // required: true // responses: diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index f2e0cad86c..c1e0b47d33 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -203,7 +203,7 @@ func Migrate(ctx *context.APIContext) { } if repo != nil { - if errDelete := repo_service.DeleteRepositoryDirectly(ctx, ctx.Doer, repo.ID); errDelete != nil { + if errDelete := repo_service.DeleteRepositoryDirectly(ctx, repo.ID); errDelete != nil { log.Error("DeleteRepository: %v", errDelete) } } diff --git a/routers/api/v1/repo/patch.go b/routers/api/v1/repo/patch.go index bcf498bf7e..e9f5cf5d90 100644 --- a/routers/api/v1/repo/patch.go +++ b/routers/api/v1/repo/patch.go @@ -5,15 +5,10 @@ package repo import ( "net/http" - "time" - git_model "code.gitea.io/gitea/models/git" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" - pull_service "code.gitea.io/gitea/services/pull" "code.gitea.io/gitea/services/repository/files" ) @@ -49,63 +44,22 @@ func ApplyDiffPatch(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // "423": // "$ref": "#/responses/repoArchivedError" - apiOpts := web.GetForm(ctx).(*api.ApplyDiffPatchFileOptions) - + apiOpts, changeRepoFileOpts := getAPIChangeRepoFileOptions[*api.ApplyDiffPatchFileOptions](ctx) opts := &files.ApplyDiffPatchOptions{ - Content: apiOpts.Content, - SHA: apiOpts.SHA, - Message: apiOpts.Message, - OldBranch: apiOpts.BranchName, - NewBranch: apiOpts.NewBranchName, - Committer: &files.IdentityOptions{ - GitUserName: apiOpts.Committer.Name, - GitUserEmail: apiOpts.Committer.Email, - }, - Author: &files.IdentityOptions{ - GitUserName: apiOpts.Author.Name, - GitUserEmail: apiOpts.Author.Email, - }, - Dates: &files.CommitDateOptions{ - Author: apiOpts.Dates.Author, - Committer: apiOpts.Dates.Committer, - }, - Signoff: apiOpts.Signoff, - } - if opts.Dates.Author.IsZero() { - opts.Dates.Author = time.Now() - } - if opts.Dates.Committer.IsZero() { - opts.Dates.Committer = time.Now() - } - - if opts.Message == "" { - opts.Message = "apply-patch" - } + Content: apiOpts.Content, + Message: util.IfZero(apiOpts.Message, "apply-patch"), - if !canWriteFiles(ctx, apiOpts.BranchName) { - ctx.APIErrorInternal(repo_model.ErrUserDoesNotHaveAccessToRepo{ - UserID: ctx.Doer.ID, - RepoName: ctx.Repo.Repository.LowerName, - }) - return + OldBranch: changeRepoFileOpts.OldBranch, + NewBranch: changeRepoFileOpts.NewBranch, + Committer: changeRepoFileOpts.Committer, + Author: changeRepoFileOpts.Author, + Dates: changeRepoFileOpts.Dates, + Signoff: changeRepoFileOpts.Signoff, } fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts) if err != nil { - if files.IsErrUserCannotCommit(err) || pull_service.IsErrFilePathProtected(err) { - ctx.APIError(http.StatusForbidden, err) - return - } - if git_model.IsErrBranchAlreadyExists(err) || files.IsErrFilenameInvalid(err) || pull_service.IsErrSHADoesNotMatch(err) || - files.IsErrFilePathInvalid(err) || files.IsErrRepoFileAlreadyExists(err) { - ctx.APIError(http.StatusUnprocessableEntity, err) - return - } - if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) { - ctx.APIError(http.StatusNotFound, err) - return - } - ctx.APIErrorInternal(err) + handleChangeRepoFilesError(ctx, err) } else { ctx.JSON(http.StatusCreated, fileResponse) } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index f1ba06dd4a..2c194f9253 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -73,7 +74,7 @@ func ListPullRequests(ctx *context.APIContext) { // in: query // description: Type of sort // type: string - // enum: [oldest, recentupdate, leastupdate, mostcomment, leastcomment, priority] + // enum: [oldest, recentupdate, recentclose, leastupdate, mostcomment, leastcomment, priority] // - name: milestone // in: query // description: ID of the milestone @@ -1301,7 +1302,7 @@ func UpdatePullRequest(ctx *context.APIContext) { // default merge commit message message := fmt.Sprintf("Merge branch '%s' into %s", pr.BaseBranch, pr.HeadBranch) - if err = pull_service.Update(ctx, pr, ctx.Doer, message, rebase); err != nil { + if err = pull_service.Update(graceful.GetManager().ShutdownContext(), pr, ctx.Doer, message, rebase); err != nil { if pull_service.IsErrMergeConflicts(err) { ctx.APIError(http.StatusConflict, "merge failed because of conflict") return diff --git a/routers/api/v1/repo/release.go b/routers/api/v1/repo/release.go index b6f5a3ac9e..272b395dfb 100644 --- a/routers/api/v1/repo/release.go +++ b/routers/api/v1/repo/release.go @@ -247,7 +247,9 @@ func CreateRelease(ctx *context.APIContext) { IsTag: false, Repo: ctx.Repo.Repository, } - if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, ""); err != nil { + // GitHub doesn't have "tag_message", GitLab has: https://docs.gitlab.com/api/releases/#create-a-release + // It doesn't need to be the same as the "release note" + if err := release_service.CreateRelease(ctx.Repo.GitRepo, rel, nil, form.TagMessage); err != nil { if repo_model.IsErrReleaseAlreadyExist(err) { ctx.APIError(http.StatusConflict, err) } else if release_service.IsErrProtectedTagName(err) { diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 7caf004b4a..8acc912796 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -134,7 +134,7 @@ func Search(ctx *context.APIContext) { private = false } - opts := &repo_model.SearchRepoOptions{ + opts := repo_model.SearchRepoOptions{ ListOptions: utils.GetListOptions(ctx), Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), diff --git a/routers/api/v1/repo/status.go b/routers/api/v1/repo/status.go index 756adcf3a3..40007ea1e5 100644 --- a/routers/api/v1/repo/status.go +++ b/routers/api/v1/repo/status.go @@ -258,19 +258,24 @@ func GetCombinedCommitStatusByRef(ctx *context.APIContext) { repo := ctx.Repo.Repository - statuses, count, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), utils.GetListOptions(ctx)) + statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String(), utils.GetListOptions(ctx)) if err != nil { ctx.APIErrorInternal(fmt.Errorf("GetLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err)) return } + count, err := git_model.CountLatestCommitStatus(ctx, repo.ID, refCommit.Commit.ID.String()) + if err != nil { + ctx.APIErrorInternal(fmt.Errorf("CountLatestCommitStatus[%s, %s]: %w", repo.FullName(), refCommit.CommitID, err)) + return + } + ctx.SetTotalCountHeader(count) + if len(statuses) == 0 { ctx.JSON(http.StatusOK, &api.CombinedStatus{}) return } combiStatus := convert.ToCombinedStatus(ctx, statuses, convert.ToRepo(ctx, repo, ctx.Repo.Permission)) - - ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, combiStatus) } diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index d5840b4149..8e24ffa465 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -298,10 +298,7 @@ func ListWikiPages(ctx *context.APIContext) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) limit := ctx.FormInt("limit") if limit <= 1 { limit = setting.API.DefaultPagingNum @@ -434,10 +431,7 @@ func ListPageRevisions(ctx *context.APIContext) { // get commit count - wiki revisions commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) // get Commit Count commitsHistory, err := wikiRepo.CommitsByFileAndRange( @@ -505,7 +499,7 @@ func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { if blob.Size() > setting.API.DefaultMaxBlobSize { return "" } - content, err := blob.GetBlobContentBase64() + content, err := blob.GetBlobContentBase64(nil) if err != nil { ctx.APIErrorInternal(err) return "" diff --git a/routers/api/v1/shared/action.go b/routers/api/v1/shared/action.go new file mode 100644 index 0000000000..c97e9419fd --- /dev/null +++ b/routers/api/v1/shared/action.go @@ -0,0 +1,187 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package shared + +import ( + "fmt" + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" + "code.gitea.io/gitea/routers/api/v1/utils" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/convert" +) + +// ListJobs lists jobs for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means all jobs +// ownerID == 0 and repoID != 0 means all jobs for the given repo +// ownerID != 0 and repoID == 0 means all jobs for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// runID == 0 means all jobs +// runID is used as an additional filter together with ownerID and repoID to only return jobs for the given run +// Access rights are checked at the API route level +func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + opts := actions_model.FindRunJobOptions{ + OwnerID: ownerID, + RepoID: repoID, + RunID: runID, + ListOptions: utils.GetListOptions(ctx), + } + for _, status := range ctx.FormStrings("status") { + values, err := convertToInternal(status) + if err != nil { + ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + return + } + opts.Statuses = append(opts.Statuses, values...) + } + + jobs, total, err := db.FindAndCount[actions_model.ActionRunJob](ctx, opts) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionWorkflowJobsResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionWorkflowJob, len(jobs)) + + isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID + for i := range jobs { + var repository *repo_model.Repository + if isRepoLevel { + repository = ctx.Repo.Repository + } else { + repository, err = repo_model.GetRepositoryByID(ctx, jobs[i].RepoID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + + convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedWorkflowJob + } + + ctx.JSON(http.StatusOK, &res) +} + +func convertToInternal(s string) ([]actions_model.Status, error) { + switch s { + case "pending", "waiting", "requested", "action_required": + return []actions_model.Status{actions_model.StatusBlocked}, nil + case "queued": + return []actions_model.Status{actions_model.StatusWaiting}, nil + case "in_progress": + return []actions_model.Status{actions_model.StatusRunning}, nil + case "completed": + return []actions_model.Status{ + actions_model.StatusSuccess, + actions_model.StatusFailure, + actions_model.StatusSkipped, + actions_model.StatusCancelled, + }, nil + case "failure": + return []actions_model.Status{actions_model.StatusFailure}, nil + case "success": + return []actions_model.Status{actions_model.StatusSuccess}, nil + case "skipped", "neutral": + return []actions_model.Status{actions_model.StatusSkipped}, nil + case "cancelled", "timed_out": + return []actions_model.Status{actions_model.StatusCancelled}, nil + default: + return nil, fmt.Errorf("invalid status %s", s) + } +} + +// ListRuns lists jobs for api route validated ownerID and repoID +// ownerID == 0 and repoID == 0 means all runs +// ownerID == 0 and repoID != 0 means all runs for the given repo +// ownerID != 0 and repoID == 0 means all runs for the given user/org +// ownerID != 0 and repoID != 0 undefined behavior +// Access rights are checked at the API route level +func ListRuns(ctx *context.APIContext, ownerID, repoID int64) { + if ownerID != 0 && repoID != 0 { + setting.PanicInDevOrTesting("ownerID and repoID should not be both set") + } + opts := actions_model.FindRunOptions{ + OwnerID: ownerID, + RepoID: repoID, + ListOptions: utils.GetListOptions(ctx), + } + + if event := ctx.FormString("event"); event != "" { + opts.TriggerEvent = webhook.HookEventType(event) + } + if branch := ctx.FormString("branch"); branch != "" { + opts.Ref = string(git.RefNameFromBranch(branch)) + } + for _, status := range ctx.FormStrings("status") { + values, err := convertToInternal(status) + if err != nil { + ctx.APIError(http.StatusBadRequest, fmt.Errorf("Invalid status %s", status)) + return + } + opts.Status = append(opts.Status, values...) + } + if actor := ctx.FormString("actor"); actor != "" { + user, err := user_model.GetUserByName(ctx, actor) + if err != nil { + ctx.APIErrorInternal(err) + return + } + opts.TriggerUserID = user.ID + } + if headSHA := ctx.FormString("head_sha"); headSHA != "" { + opts.CommitSHA = headSHA + } + + runs, total, err := db.FindAndCount[actions_model.ActionRun](ctx, opts) + if err != nil { + ctx.APIErrorInternal(err) + return + } + + res := new(api.ActionWorkflowRunsResponse) + res.TotalCount = total + + res.Entries = make([]*api.ActionWorkflowRun, len(runs)) + isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID + for i := range runs { + var repository *repo_model.Repository + if isRepoLevel { + repository = ctx.Repo.Repository + } else { + repository, err = repo_model.GetRepositoryByID(ctx, runs[i].RepoID) + if err != nil { + ctx.APIErrorInternal(err) + return + } + } + + convertedRun, err := convert.ToActionWorkflowRun(ctx, repository, runs[i]) + if err != nil { + ctx.APIErrorInternal(err) + return + } + res.Entries[i] = convertedRun + } + + ctx.JSON(http.StatusOK, &res) +} diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go index df0c8a805a..9e20c0533b 100644 --- a/routers/api/v1/swagger/repo.go +++ b/routers/api/v1/swagger/repo.go @@ -331,6 +331,12 @@ type swaggerContentsListResponse struct { Body []api.ContentsResponse `json:"body"` } +// swagger:response ContentsExtResponse +type swaggerContentsExtResponse struct { + // in:body + Body api.ContentsExtResponse `json:"body"` +} + // FileDeleteResponse // swagger:response FileDeleteResponse type swaggerFileDeleteResponse struct { @@ -443,6 +449,34 @@ type swaggerRepoTasksList struct { Body api.ActionTaskResponse `json:"body"` } +// WorkflowRunsList +// swagger:response WorkflowRunsList +type swaggerActionWorkflowRunsResponse struct { + // in:body + Body api.ActionWorkflowRunsResponse `json:"body"` +} + +// WorkflowRun +// swagger:response WorkflowRun +type swaggerWorkflowRun struct { + // in:body + Body api.ActionWorkflowRun `json:"body"` +} + +// WorkflowJobsList +// swagger:response WorkflowJobsList +type swaggerActionWorkflowJobsResponse struct { + // in:body + Body api.ActionWorkflowJobsResponse `json:"body"` +} + +// WorkflowJob +// swagger:response WorkflowJob +type swaggerWorkflowJob struct { + // in:body + Body api.ActionWorkflowJob `json:"body"` +} + // ArtifactsList // swagger:response ArtifactsList type swaggerRepoArtifactsList struct { diff --git a/routers/api/v1/user/action.go b/routers/api/v1/user/action.go index 04097fcc95..e934d02aa7 100644 --- a/routers/api/v1/user/action.go +++ b/routers/api/v1/user/action.go @@ -12,6 +12,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/shared" "code.gitea.io/gitea/routers/api/v1/utils" actions_service "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/context" @@ -127,13 +128,11 @@ func CreateVariable(ctx *context.APIContext) { // "$ref": "#/definitions/CreateVariableOption" // responses: // "201": - // description: response when creating a variable - // "204": - // description: response when creating a variable + // description: successfully created the user-level variable // "400": // "$ref": "#/responses/error" - // "404": - // "$ref": "#/responses/notFound" + // "409": + // description: variable name already exists. opt := web.GetForm(ctx).(*api.CreateVariableOption) @@ -162,7 +161,7 @@ func CreateVariable(ctx *context.APIContext) { return } - ctx.Status(http.StatusNoContent) + ctx.Status(http.StatusCreated) } // UpdateVariable update a user-level variable which is created by current doer @@ -358,3 +357,86 @@ func ListVariables(ctx *context.APIContext) { ctx.SetTotalCountHeader(count) ctx.JSON(http.StatusOK, variables) } + +// ListWorkflowRuns lists workflow runs +func ListWorkflowRuns(ctx *context.APIContext) { + // swagger:operation GET /user/actions/runs user getUserWorkflowRuns + // --- + // summary: Get workflow runs + // parameters: + // - name: event + // in: query + // description: workflow event name + // type: string + // required: false + // - name: branch + // in: query + // description: workflow branch + // type: string + // required: false + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: actor + // in: query + // description: triggered by user + // type: string + // required: false + // - name: head_sha + // in: query + // description: triggering sha of the workflow run + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/WorkflowRunsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + shared.ListRuns(ctx, ctx.Doer.ID, 0) +} + +// ListWorkflowJobs lists workflow jobs +func ListWorkflowJobs(ctx *context.APIContext) { + // swagger:operation GET /user/actions/jobs user getUserWorkflowJobs + // --- + // summary: Get workflow jobs + // parameters: + // - name: status + // in: query + // description: workflow status (pending, queued, in_progress, failure, success, skipped) + // type: string + // required: false + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // produces: + // - application/json + // responses: + // "200": + // "$ref": "#/responses/WorkflowJobsList" + // "400": + // "$ref": "#/responses/error" + // "404": + // "$ref": "#/responses/notFound" + + shared.ListJobs(ctx, ctx.Doer.ID, 0, 0) +} diff --git a/routers/api/v1/user/app.go b/routers/api/v1/user/app.go index 7201010161..6f1053e7ac 100644 --- a/routers/api/v1/user/app.go +++ b/routers/api/v1/user/app.go @@ -30,7 +30,7 @@ func ListAccessTokens(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of to user whose access tokens are to be listed // type: string // required: true // - name: page @@ -83,7 +83,7 @@ func CreateAccessToken(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose token is to be created // required: true // type: string // - name: body @@ -149,7 +149,7 @@ func DeleteAccessToken(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose token is to be deleted // type: string // required: true // - name: token diff --git a/routers/api/v1/user/block.go b/routers/api/v1/user/block.go index 7231e9add7..8365188f60 100644 --- a/routers/api/v1/user/block.go +++ b/routers/api/v1/user/block.go @@ -37,7 +37,7 @@ func CheckUserBlock(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: user to check + // description: username of the user to check // type: string // required: true // responses: @@ -56,7 +56,7 @@ func BlockUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: user to block + // description: username of the user to block // type: string // required: true // - name: note @@ -81,7 +81,7 @@ func UnblockUser(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: user to unblock + // description: username of the user to unblock // type: string // required: true // responses: diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go index 0d0c0be7e0..339b994af4 100644 --- a/routers/api/v1/user/follower.go +++ b/routers/api/v1/user/follower.go @@ -67,7 +67,7 @@ func ListFollowers(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose followers are to be listed // type: string // required: true // - name: page @@ -131,7 +131,7 @@ func ListFollowing(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose followed users are to be listed // type: string // required: true // - name: page @@ -167,7 +167,7 @@ func CheckMyFollowing(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of followed user + // description: username of the user to check for authenticated followers // type: string // required: true // responses: @@ -187,12 +187,12 @@ func CheckFollowing(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of following user + // description: username of the following user // type: string // required: true // - name: target // in: path - // description: username of followed user + // description: username of the followed user // type: string // required: true // responses: @@ -216,7 +216,7 @@ func Follow(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to follow + // description: username of the user to follow // type: string // required: true // responses: @@ -246,7 +246,7 @@ func Unfollow(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to unfollow + // description: username of the user to unfollow // type: string // required: true // responses: diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index b76bd8a1ee..9ec4d2c938 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -53,7 +53,7 @@ func ListGPGKeys(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose GPG key list is to be obtained // type: string // required: true // - name: page diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go index 628f5d6cac..aa69245e49 100644 --- a/routers/api/v1/user/key.go +++ b/routers/api/v1/user/key.go @@ -136,7 +136,7 @@ func ListPublicKeys(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose public keys are to be listed // type: string // required: true // - name: fingerprint diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go index 6aabc4fb90..6d0129681e 100644 --- a/routers/api/v1/user/repo.go +++ b/routers/api/v1/user/repo.go @@ -19,7 +19,7 @@ import ( func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) { opts := utils.GetListOptions(ctx) - repos, count, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{ + repos, count, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ Actor: u, Private: private, ListOptions: opts, @@ -62,7 +62,7 @@ func ListUserRepos(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose owned repos are to be listed // type: string // required: true // - name: page @@ -103,7 +103,7 @@ func ListMyRepos(ctx *context.APIContext) { // "200": // "$ref": "#/responses/RepositoryList" - opts := &repo_model.SearchRepoOptions{ + opts := repo_model.SearchRepoOptions{ ListOptions: utils.GetListOptions(ctx), Actor: ctx.Doer, OwnerID: ctx.Doer.ID, diff --git a/routers/api/v1/user/star.go b/routers/api/v1/user/star.go index 4b0cb45d67..ee5d63063b 100644 --- a/routers/api/v1/user/star.go +++ b/routers/api/v1/user/star.go @@ -50,7 +50,7 @@ func GetStarredRepos(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose starred repos are to be listed // type: string // required: true // - name: page diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 757a548518..6de1125c40 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -73,7 +73,7 @@ func Search(ctx *context.APIContext) { if ctx.PublicOnly { visible = []structs.VisibleType{structs.VisibleTypePublic} } - users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + users, maxResults, err = user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), UID: uid, @@ -110,7 +110,7 @@ func GetInfo(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to get + // description: username of the user whose data is to be listed // type: string // required: true // responses: @@ -151,7 +151,7 @@ func GetUserHeatmapData(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user to get + // description: username of the user whose heatmap is to be obtained // type: string // required: true // responses: @@ -177,7 +177,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) { // parameters: // - name: username // in: path - // description: username of user + // description: username of the user whose activity feeds are to be listed // type: string // required: true // - name: only-performed-by diff --git a/routers/api/v1/user/watch.go b/routers/api/v1/user/watch.go index 76d7c81793..844eac2c67 100644 --- a/routers/api/v1/user/watch.go +++ b/routers/api/v1/user/watch.go @@ -49,7 +49,7 @@ func GetWatchedRepos(ctx *context.APIContext) { // - name: username // type: string // in: path - // description: username of the user + // description: username of the user whose watched repos are to be listed // required: true // - name: page // in: query diff --git a/routers/api/v1/utils/hook.go b/routers/api/v1/utils/hook.go index 3bbd292718..6f598f14c8 100644 --- a/routers/api/v1/utils/hook.go +++ b/routers/api/v1/utils/hook.go @@ -173,6 +173,7 @@ func updateHookEvents(events []string) webhook_module.HookEvents { hookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(events, string(webhook_module.HookEventRelease), true) hookEvents[webhook_module.HookEventPackage] = util.SliceContainsString(events, string(webhook_module.HookEventPackage), true) hookEvents[webhook_module.HookEventStatus] = util.SliceContainsString(events, string(webhook_module.HookEventStatus), true) + hookEvents[webhook_module.HookEventWorkflowRun] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowRun), true) hookEvents[webhook_module.HookEventWorkflowJob] = util.SliceContainsString(events, string(webhook_module.HookEventWorkflowJob), true) // Issues diff --git a/routers/common/markup.go b/routers/common/markup.go index 4c77ff33ed..00b2dd07c6 100644 --- a/routers/common/markup.go +++ b/routers/common/markup.go @@ -76,7 +76,11 @@ func RenderMarkup(ctx *context.Base, ctxRepo *context.Repository, mode, text, ur }) rctx = rctx.WithMarkupType(markdown.MarkupName) case "comment": - rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName}) + rctx = renderhelper.NewRenderContextRepoComment(ctx, repoModel, renderhelper.RepoCommentOptions{ + DeprecatedOwnerName: repoOwnerName, + DeprecatedRepoName: repoName, + FootnoteContextID: "preview", + }) rctx = rctx.WithMarkupType(markdown.MarkupName) case "wiki": rctx = renderhelper.NewRenderContextRepoWiki(ctx, repoModel, renderhelper.RepoWikiOptions{DeprecatedOwnerName: repoOwnerName, DeprecatedRepoName: repoName}) diff --git a/routers/install/install.go b/routers/install/install.go index 2962f3948f..dc8f209f3b 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -10,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strconv" "strings" "time" @@ -98,14 +99,7 @@ func Install(ctx *context.Context) { form.SSLMode = setting.Database.SSLMode curDBType := setting.Database.Type.String() - var isCurDBTypeSupported bool - for _, dbType := range setting.SupportedDatabaseTypes { - if dbType == curDBType { - isCurDBTypeSupported = true - break - } - } - if !isCurDBTypeSupported { + if !slices.Contains(setting.SupportedDatabaseTypes, curDBType) { curDBType = "mysql" } ctx.Data["CurDbType"] = curDBType @@ -606,6 +600,8 @@ func SubmitInstall(ctx *context.Context) { // InstallDone shows the "post-install" page, makes it easier to develop the page. // The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install" -func InstallDone(ctx *context.Context) { //nolint +func InstallDone(ctx *context.Context) { //nolint:revive // export stutter + hasUsers, _ := user_model.HasUsers(ctx) + ctx.Data["IsAccountCreated"] = hasUsers.HasAnyUser ctx.HTML(http.StatusOK, tplPostInstall) } diff --git a/routers/private/hook_post_receive.go b/routers/private/hook_post_receive.go index a391e572b3..e8bef7d6c1 100644 --- a/routers/private/hook_post_receive.go +++ b/routers/private/hook_post_receive.go @@ -207,25 +207,19 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { return } - cols := make([]string, 0, 2) - - if isPrivate.Has() { + // FIXME: these options are not quite right, for example: changing visibility should do more works than just setting the is_private flag + // These options should only be used for "push-to-create" + if isPrivate.Has() && repo.IsPrivate != isPrivate.Value() { + // TODO: it needs to do more work repo.IsPrivate = isPrivate.Value() - cols = append(cols, "is_private") + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil { + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change visibility"}) + } } - - if isTemplate.Has() { + if isTemplate.Has() && repo.IsTemplate != isTemplate.Value() { repo.IsTemplate = isTemplate.Value() - cols = append(cols, "is_template") - } - - if len(cols) > 0 { - if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, cols...); err != nil { - log.Error("Failed to Update: %s/%s Error: %v", ownerName, repoName, err) - ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ - Err: fmt.Sprintf("Failed to Update: %s/%s Error: %v", ownerName, repoName, err), - }) - return + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_template"); err != nil { + ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{Err: "Failed to change template status"}) } } } diff --git a/routers/private/hook_verification_test.go b/routers/private/hook_verification_test.go index f6c2e1087f..8653e34daa 100644 --- a/routers/private/hook_verification_test.go +++ b/routers/private/hook_verification_test.go @@ -18,7 +18,9 @@ func TestVerifyCommits(t *testing.T) { unittest.PrepareTestEnv(t) gitRepo, err := git.OpenRepository(t.Context(), testReposDir+"repo1_hook_verification") - defer gitRepo.Close() + if err != nil { + defer gitRepo.Close() + } assert.NoError(t, err) objectFormat, err := gitRepo.GetObjectFormat() diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 80d554b6e3..0f6f31b884 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -177,7 +177,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { customURLMapping = nil } var scopes []string - for _, s := range strings.Split(form.Oauth2Scopes, ",") { + for s := range strings.SplitSeq(form.Oauth2Scopes, ",") { s = strings.TrimSpace(s) if s != "" { scopes = append(scopes, s) diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index 520f14e89f..0e5b23db6d 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -61,7 +61,7 @@ func TestCache(ctx *context.Context) { func shadowPasswordKV(cfgItem, splitter string) string { fields := strings.Split(cfgItem, splitter) - for i := 0; i < len(fields); i++ { + for i := range fields { if strings.HasPrefix(fields[i], "password=") { fields[i] = "password=******" break @@ -200,7 +200,7 @@ func ChangeConfig(ctx *context.Context) { value := ctx.FormString("value") cfg := setting.Config() - marshalBool := func(v string) (string, error) { //nolint:unparam + marshalBool := func(v string) (string, error) { //nolint:unparam // error is always nil if b, _ := strconv.ParseBool(v); b { return "true", nil } diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go index d040dbe0ba..5395529d66 100644 --- a/routers/web/admin/diagnosis.go +++ b/routers/web/admin/diagnosis.go @@ -16,13 +16,7 @@ import ( ) func MonitorDiagnosis(ctx *context.Context) { - seconds := ctx.FormInt64("seconds") - if seconds <= 1 { - seconds = 1 - } - if seconds > 300 { - seconds = 300 - } + seconds := min(max(ctx.FormInt64("seconds"), 1), 300) httplib.ServeSetHeaders(ctx.Resp, &httplib.ServeHeaderOptions{ ContentType: "application/zip", diff --git a/routers/web/admin/notice.go b/routers/web/admin/notice.go index 21a8ab0d17..e9d6abbe92 100644 --- a/routers/web/admin/notice.go +++ b/routers/web/admin/notice.go @@ -26,10 +26,7 @@ func Notices(ctx *context.Context) { ctx.Data["PageIsAdminNotices"] = true total := system_model.CountNotices(ctx) - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) notices, err := system_model.Notices(ctx, page, setting.UI.Admin.NoticePagingNum) if err != nil { diff --git a/routers/web/admin/orgs.go b/routers/web/admin/orgs.go index 35e61efa17..e34f203aaf 100644 --- a/routers/web/admin/orgs.go +++ b/routers/web/admin/orgs.go @@ -27,7 +27,7 @@ func Organizations(ctx *context.Context) { ctx.SetFormString("sort", UserSearchDefaultAdminSort) } - explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{ + explore.RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Type: user_model.UserTypeOrganization, IncludeReserved: true, // administrator needs to list all accounts include reserved diff --git a/routers/web/admin/packages.go b/routers/web/admin/packages.go index 5122342259..1904bfee11 100644 --- a/routers/web/admin/packages.go +++ b/routers/web/admin/packages.go @@ -24,10 +24,7 @@ const ( // Packages shows all packages func Packages(ctx *context.Context) { - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) query := ctx.FormTrim("q") packageType := ctx.FormTrim("type") sort := ctx.FormTrim("sort") diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index e42cbb316c..27577cd35b 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/web/explore" @@ -64,7 +65,7 @@ func Users(ctx *context.Context) { "SortType": sortType, } - explore.RenderUserSearch(ctx, &user_model.SearchUserOptions{ + explore.RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Type: user_model.UserTypeIndividual, ListOptions: db.ListOptions{ @@ -268,7 +269,7 @@ func ViewUser(ctx *context.Context) { return } - repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptionsAll, OwnerID: u.ID, OrderBy: db.SearchOrderByAlphabetically, @@ -292,9 +293,9 @@ func ViewUser(ctx *context.Context) { ctx.Data["EmailsTotal"] = len(emails) orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{ - ListOptions: db.ListOptionsAll, - UserID: u.ID, - IncludePrivate: true, + ListOptions: db.ListOptionsAll, + UserID: u.ID, + IncludeVisibility: structs.VisibleTypePrivate, }) if err != nil { ctx.ServerError("FindOrgs", err) @@ -431,7 +432,7 @@ func EditUserPost(ctx *context.Context) { Website: optional.Some(form.Website), Location: optional.Some(form.Location), IsActive: optional.Some(form.Active), - IsAdmin: optional.Some(form.Admin), + IsAdmin: user_service.UpdateOptionFieldFromValue(form.Admin), AllowGitHook: optional.Some(form.AllowGitHook), AllowImportLocal: optional.Some(form.AllowImportLocal), MaxRepoCreation: optional.Some(form.MaxRepoCreation), diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go index 69b9d285b7..94f75f69ff 100644 --- a/routers/web/auth/auth.go +++ b/routers/web/auth/auth.go @@ -421,9 +421,11 @@ func SignOut(ctx *context.Context) { // SignUp render the register page func SignUp(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("sign_up") - ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/sign_up" + hasUsers, _ := user_model.HasUsers(ctx) + ctx.Data["IsFirstTimeRegistration"] = !hasUsers.HasAnyUser + oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true)) if err != nil { ctx.ServerError("UserSignUp", err) @@ -610,10 +612,16 @@ func createUserInContext(ctx *context.Context, tpl templates.TplName, form any, // sends a confirmation email if required. func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.User) (ok bool) { // Auto-set admin for the only user. - if user_model.CountUsers(ctx, nil) == 1 { + hasUsers, err := user_model.HasUsers(ctx) + if err != nil { + ctx.ServerError("HasUsers", err) + return false + } + if hasUsers.HasOnlyOneUser { + // the only user is the one just created, will set it as admin opts := &user_service.UpdateOptions{ IsActive: optional.Some(true), - IsAdmin: optional.Some(true), + IsAdmin: user_service.UpdateOptionFieldFromValue(true), SetLastLogin: true, } if err := user_service.UpdateUser(ctx, u, opts); err != nil { diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 96c1dcf358..a13b987aab 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -193,8 +193,8 @@ func SignInOAuthCallback(ctx *context.Context) { source := authSource.Cfg.(*oauth2.Source) isAdmin, isRestricted := getUserAdminAndRestrictedFromGroupClaims(source, &gothUser) - u.IsAdmin = isAdmin.ValueOrDefault(false) - u.IsRestricted = isRestricted.ValueOrDefault(false) + u.IsAdmin = isAdmin.ValueOrDefault(user_service.UpdateOptionField[bool]{FieldValue: false}).FieldValue + u.IsRestricted = isRestricted.ValueOrDefault(setting.Service.DefaultUserIsRestricted) if !createAndHandleCreatedUser(ctx, templates.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) { // error already handled @@ -258,11 +258,11 @@ func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[ return claimValueToStringSet(groupClaims) } -func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin, isRestricted optional.Option[bool]) { +func getUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, gothUser *goth.User) (isAdmin optional.Option[user_service.UpdateOptionField[bool]], isRestricted optional.Option[bool]) { groups := getClaimedGroups(source, gothUser) if source.AdminGroup != "" { - isAdmin = optional.Some(groups.Contains(source.AdminGroup)) + isAdmin = user_service.UpdateOptionFieldFromSync(groups.Contains(source.AdminGroup)) } if source.RestrictedGroup != "" { isRestricted = optional.Some(groups.Contains(source.RestrictedGroup)) diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index 8c2d3276a8..2ef4a86022 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -349,10 +349,7 @@ func RegisterOpenIDPost(ctx *context.Context) { context.VerifyCaptcha(ctx, tplSignUpOID, form) } - length := setting.MinPasswordLength - if length < 256 { - length = 256 - } + length := max(setting.MinPasswordLength, 256) password, err := util.CryptoRandomString(int64(length)) if err != nil { ctx.RenderWithErr(err.Error(), tplSignUpOID, form) diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go index 765931a730..a22d376579 100644 --- a/routers/web/devtest/devtest.go +++ b/routers/web/devtest/devtest.go @@ -132,7 +132,7 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) { selectedStyle := ctx.FormString("style", badge.DefaultStyle) var badges []badge.Badge badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green")) - for r := rune(0); r < 256; r++ { + for r := range rune(256) { if unicode.IsPrint(r) { s := strings.Repeat(string(r), 15) badges = append(badges, badge.GenerateBadge(s, util.TruncateRunes(s, 7), "green")) diff --git a/routers/web/explore/code.go b/routers/web/explore/code.go index 8f6518a4fc..3bb50ef397 100644 --- a/routers/web/explore/code.go +++ b/routers/web/explore/code.go @@ -5,6 +5,7 @@ package explore import ( "net/http" + "slices" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -93,14 +94,7 @@ func Code(ctx *context.Context) { loadRepoIDs := make([]int64, 0, len(searchResults)) for _, result := range searchResults { - var find bool - for _, id := range loadRepoIDs { - if id == result.RepoID { - find = true - break - } - } - if !find { + if !slices.Contains(loadRepoIDs, result.RepoID) { loadRepoIDs = append(loadRepoIDs, result.RepoID) } } diff --git a/routers/web/explore/org.go b/routers/web/explore/org.go index 7bb71acfd7..f8f7f5c18c 100644 --- a/routers/web/explore/org.go +++ b/routers/web/explore/org.go @@ -44,7 +44,7 @@ func Organizations(ctx *context.Context) { ctx.SetFormString("sort", sortOrder) } - RenderUserSearch(ctx, &user_model.SearchUserOptions{ + RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Type: user_model.UserTypeOrganization, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index cf3128314b..f0d7d0ce7d 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -94,7 +94,7 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { private := ctx.FormOptionalBool("private") ctx.Data["IsPrivate"] = private - repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: page, PageSize: opts.PageSize, @@ -151,6 +151,7 @@ func Repos(ctx *context.Context) { ctx.Data["CodePageIsDisabled"] = setting.Service.Explore.DisableCodePage ctx.Data["Title"] = ctx.Tr("explore") ctx.Data["PageIsExplore"] = true + ctx.Data["ShowRepoOwnerOnList"] = true ctx.Data["PageIsExploreRepositories"] = true ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index e1e1ec1cfd..af48e6fb79 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -32,7 +32,7 @@ func isKeywordValid(keyword string) bool { } // RenderUserSearch render user search page -func RenderUserSearch(ctx *context.Context, opts *user_model.SearchUserOptions, tplName templates.TplName) { +func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, tplName templates.TplName) { // Sitemap index for sitemap paths opts.Page = int(ctx.PathParamInt64("idx")) isSitemap := ctx.PathParam("idx") != "" @@ -151,7 +151,7 @@ func Users(ctx *context.Context) { ctx.SetFormString("sort", sortOrder) } - RenderUserSearch(ctx, &user_model.SearchUserOptions{ + RenderUserSearch(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Type: user_model.UserTypeIndividual, ListOptions: db.ListOptions{PageSize: setting.UI.ExplorePagingNum}, diff --git a/routers/web/home.go b/routers/web/home.go index 208cc36dfb..4b15ee83c2 100644 --- a/routers/web/home.go +++ b/routers/web/home.go @@ -68,7 +68,7 @@ func Home(ctx *context.Context) { func HomeSitemap(ctx *context.Context) { m := sitemap.NewSitemapIndex() if !setting.Service.Explore.DisableUsersPage { - _, cnt, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + _, cnt, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Type: user_model.UserTypeIndividual, ListOptions: db.ListOptions{PageSize: 1}, IsActive: optional.Some(true), @@ -86,7 +86,7 @@ func HomeSitemap(ctx *context.Context) { } } - _, cnt, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + _, cnt, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: 1, }, diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go index 0c7ec6c2eb..f90cf3ffed 100644 --- a/routers/web/misc/markup.go +++ b/routers/web/misc/markup.go @@ -15,6 +15,6 @@ import ( // Markup render markup document to HTML func Markup(ctx *context.Context) { form := web.GetForm(ctx).(*api.MarkupOption) - mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck + mode := util.Iif(form.Wiki, "wiki", form.Mode) //nolint:staticcheck // form.Wiki is deprecated common.RenderMarkup(ctx.Base, ctx.Repo, mode, form.Text, form.Context, form.FilePath) } diff --git a/routers/web/misc/misc.go b/routers/web/misc/misc.go index d42afafe9e..59b97c1717 100644 --- a/routers/web/misc/misc.go +++ b/routers/web/misc/misc.go @@ -20,7 +20,7 @@ func SSHInfo(rw http.ResponseWriter, req *http.Request) { return } rw.Header().Set("content-type", "text/json;charset=UTF-8") - _, err := rw.Write([]byte(`{"type":"gitea","version":1}`)) + _, err := rw.Write([]byte(`{"type":"agit","version":1}`)) if err != nil { log.Error("fail to write result: err: %v", err) rw.WriteHeader(http.StatusInternalServerError) diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 8981af1691..63ae6c683b 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -115,7 +115,7 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.Data["PageIsViewOverview"] = isViewOverview ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil - repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, diff --git a/routers/web/org/members.go b/routers/web/org/members.go index 2cbe75989a..61022d3f09 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -28,10 +28,7 @@ func Members(ctx *context.Context) { ctx.Data["Title"] = org.FullName ctx.Data["PageIsOrgMembers"] = true - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) opts := &organization.FindOrgMembersOpts{ Doer: ctx.Doer, diff --git a/routers/web/org/org_labels.go b/routers/web/org/org_labels.go index 456ed3f01e..2a4aa7f557 100644 --- a/routers/web/org/org_labels.go +++ b/routers/web/org/org_labels.go @@ -4,13 +4,15 @@ package org import ( - "net/http" + "errors" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/modules/label" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + shared_label "code.gitea.io/gitea/routers/web/shared/label" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" ) @@ -32,14 +34,8 @@ func RetrieveLabels(ctx *context.Context) { // NewLabel create new label for organization func NewLabel(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CreateLabelForm) - ctx.Data["Title"] = ctx.Tr("repo.labels") - ctx.Data["PageIsLabels"] = true - ctx.Data["PageIsOrgSettings"] = true - - if ctx.HasError() { - ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) - ctx.Redirect(ctx.Org.OrgLink + "/settings/labels") + form := shared_label.GetLabelEditForm(ctx) + if ctx.Written() { return } @@ -55,20 +51,22 @@ func NewLabel(ctx *context.Context) { ctx.ServerError("NewLabel", err) return } - ctx.Redirect(ctx.Org.OrgLink + "/settings/labels") + ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels") } // UpdateLabel update a label's name and color func UpdateLabel(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CreateLabelForm) + form := shared_label.GetLabelEditForm(ctx) + if ctx.Written() { + return + } + l, err := issues_model.GetLabelInOrgByID(ctx, ctx.Org.Organization.ID, form.ID) - if err != nil { - switch { - case issues_model.IsErrOrgLabelNotExist(err): - ctx.HTTPError(http.StatusNotFound) - default: - ctx.ServerError("UpdateLabel", err) - } + if errors.Is(err, util.ErrNotExist) { + ctx.JSONErrorNotFound() + return + } else if err != nil { + ctx.ServerError("GetLabelInOrgByID", err) return } @@ -82,7 +80,7 @@ func UpdateLabel(ctx *context.Context) { ctx.ServerError("UpdateLabel", err) return } - ctx.Redirect(ctx.Org.OrgLink + "/settings/labels") + ctx.JSONRedirect(ctx.Org.OrgLink + "/settings/labels") } // DeleteLabel delete a label diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index f423e9cb36..059cce8281 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -53,10 +53,7 @@ func Projects(ctx *context.Context) { isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" keyword := ctx.FormTrim("q") - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) var projectType project_model.Type if ctx.ContextUser.IsOrganization() { diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 82c3bce722..2bc1e8bc43 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -18,6 +18,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" @@ -31,8 +32,6 @@ import ( const ( // tplSettingsOptions template path for render settings tplSettingsOptions templates.TplName = "org/settings/options" - // tplSettingsDelete template path for render delete repository - tplSettingsDelete templates.TplName = "org/settings/delete" // tplSettingsHooks template path for render hook settings tplSettingsHooks templates.TplName = "org/settings/hooks" // tplSettingsLabels template path for render labels settings @@ -71,26 +70,6 @@ func SettingsPost(ctx *context.Context) { org := ctx.Org.Organization - if org.Name != form.Name { - if err := user_service.RenameUser(ctx, org.AsUser(), form.Name); err != nil { - if user_model.IsErrUserAlreadyExist(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form) - } else if db.IsErrNameReserved(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) - } else if db.IsErrNamePatternNotAllowed(err) { - ctx.Data["Err_Name"] = true - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) - } else { - ctx.ServerError("RenameUser", err) - } - return - } - - ctx.Org.OrgLink = setting.AppSubURL + "/org/" + url.PathEscape(org.Name) - } - if form.Email != "" { if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil { ctx.Data["Err_Email"] = true @@ -120,7 +99,7 @@ func SettingsPost(ctx *context.Context) { // update forks visibility if visibilityChanged { - repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{ + repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ Actor: org.AsUser(), Private: true, ListOptions: db.ListOptions{Page: 1, PageSize: org.NumRepos}, }) if err != nil { @@ -163,42 +142,27 @@ func SettingsDeleteAvatar(ctx *context.Context) { ctx.JSONRedirect(ctx.Org.OrgLink + "/settings") } -// SettingsDelete response for deleting an organization -func SettingsDelete(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("org.settings") - ctx.Data["PageIsOrgSettings"] = true - ctx.Data["PageIsSettingsDelete"] = true - - if ctx.Req.Method == http.MethodPost { - if ctx.Org.Organization.Name != ctx.FormString("org_name") { - ctx.Data["Err_OrgName"] = true - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_org_name"), tplSettingsDelete, nil) - return - } - - if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil { - if repo_model.IsErrUserOwnRepos(err) { - ctx.Flash.Error(ctx.Tr("form.org_still_own_repo")) - ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") - } else if packages_model.IsErrUserOwnPackages(err) { - ctx.Flash.Error(ctx.Tr("form.org_still_own_packages")) - ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") - } else { - ctx.ServerError("DeleteOrganization", err) - } - } else { - log.Trace("Organization deleted: %s", ctx.Org.Organization.Name) - ctx.Redirect(setting.AppSubURL + "/") - } +// SettingsDeleteOrgPost response for deleting an organization +func SettingsDeleteOrgPost(ctx *context.Context) { + if ctx.Org.Organization.Name != ctx.FormString("org_name") { + ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name")) return } - if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { - ctx.ServerError("RenderUserOrgHeader", err) + if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false /* no purge */); err != nil { + if repo_model.IsErrUserOwnRepos(err) { + ctx.JSONError(ctx.Tr("form.org_still_own_repo")) + } else if packages_model.IsErrUserOwnPackages(err) { + ctx.JSONError(ctx.Tr("form.org_still_own_packages")) + } else { + log.Error("DeleteOrganization: %v", err) + ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.delete_failed")))) + } return } - ctx.HTML(http.StatusOK, tplSettingsDelete) + ctx.Flash.Success(ctx.Tr("org.settings.delete_successful", ctx.Org.Organization.Name)) + ctx.JSONRedirect(setting.AppSubURL + "/") } // Webhooks render webhook list page @@ -250,3 +214,40 @@ func Labels(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSettingsLabels) } + +// SettingsRenamePost response for renaming organization +func SettingsRenamePost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.RenameOrgForm) + if ctx.HasError() { + ctx.JSONError(ctx.GetErrMsg()) + return + } + + oldOrgName, newOrgName := ctx.Org.Organization.Name, form.NewOrgName + + if form.OrgName != oldOrgName { + ctx.JSONError(ctx.Tr("form.enterred_invalid_org_name")) + return + } + if newOrgName == oldOrgName { + ctx.JSONError(ctx.Tr("org.settings.rename_no_change")) + return + } + + if err := user_service.RenameUser(ctx, ctx.Org.Organization.AsUser(), newOrgName); err != nil { + if user_model.IsErrUserAlreadyExist(err) { + ctx.JSONError(ctx.Tr("org.form.name_been_taken", newOrgName)) + } else if db.IsErrNameReserved(err) { + ctx.JSONError(ctx.Tr("org.form.name_reserved", newOrgName)) + } else if db.IsErrNamePatternNotAllowed(err) { + ctx.JSONError(ctx.Tr("org.form.name_pattern_not_allowed", newOrgName)) + } else { + log.Error("RenameOrganization: %v", err) + ctx.JSONError(util.Iif(ctx.Doer.IsAdmin, err.Error(), string(ctx.Tr("org.settings.rename_failed")))) + } + return + } + + ctx.Flash.Success(ctx.Tr("org.settings.rename_success", oldOrgName, newOrgName)) + ctx.JSONRedirect(setting.AppSubURL + "/org/" + url.PathEscape(newOrgName) + "/settings") +} diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 676c6d0c63..0ec7cfddc5 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -283,11 +283,22 @@ func NewTeam(ctx *context.Context) { } // FIXME: TEAM-UNIT-PERMISSION: this design is not right, when a new unit is added in the future, -// admin team won't inherit the correct admin permission for the new unit. +// The existing teams won't inherit the correct admin permission for the new unit. +// The full history is like this: +// 1. There was only "team", no "team unit", so "team.authorize" was used to determine the team permission. +// 2. Later, "team unit" was introduced, then the usage of "team.authorize" became inconsistent, and causes various bugs. +// - Sometimes, "team.authorize" is used to determine the team permission, e.g. admin, owner +// - Sometimes, "team unit" is used not really used and "team unit" is used. +// - Some functions like `GetTeamsWithAccessToAnyRepoUnit` use both. +// +// 3. After introducing "team unit" and more unclear changes, it becomes difficult to maintain team permissions. +// - Org owner need to click the permission for each unit, but can't just set a common "write" permission for all units. +// +// Ideally, "team.authorize=write" should mean the team has write access to all units including newly (future) added ones. func getUnitPerms(forms url.Values, teamPermission perm.AccessMode) map[unit_model.Type]perm.AccessMode { unitPerms := make(map[unit_model.Type]perm.AccessMode) for _, ut := range unit_model.AllRepoUnitTypes { - // Default accessmode is none + // Default access mode is none unitPerms[ut] = perm.AccessModeNone v, ok := forms[fmt.Sprintf("unit_%d", ut)] diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index f466a184c3..202da407d2 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -126,7 +126,7 @@ func prepareWorkflowDispatchTemplate(ctx *context.Context, commit *git.Commit) ( var curWorkflow *model.Workflow - entries, err := actions.ListWorkflows(commit) + _, entries, err := actions.ListWorkflows(commit) if err != nil { ctx.ServerError("ListWorkflows", err) return nil @@ -318,7 +318,7 @@ func prepareWorkflowList(ctx *context.Context, workflows []Workflow) { ctx.Data["Page"] = pager ctx.Data["HasWorkflowsOrRuns"] = len(workflows) > 0 || len(runs) > 0 - ctx.Data["AllowDeleteWorkflowRuns"] = ctx.Repo.CanWrite(unit.TypeActions) + ctx.Data["CanWriteRepoUnitActions"] = ctx.Repo.CanWrite(unit.TypeActions) } // loadIsRefDeleted loads the IsRefDeleted field for each run in the list. @@ -377,10 +377,8 @@ func workflowDispatchConfig(w *model.Workflow) *WorkflowDispatch { if !decodeNode(w.RawOn, &val) { return nil } - for _, v := range val { - if v == "workflow_dispatch" { - return &WorkflowDispatch{} - } + if slices.Contains(val, "workflow_dispatch") { + return &WorkflowDispatch{} } case yaml.MappingNode: var val map[string]yaml.Node diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 13b19862ff..7e1b923fa4 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -64,6 +64,36 @@ func View(ctx *context_module.Context) { ctx.HTML(http.StatusOK, tplViewActions) } +func ViewWorkflowFile(ctx *context_module.Context) { + runIndex := getRunIndex(ctx) + run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) + if err != nil { + ctx.NotFoundOrServerError("GetRunByIndex", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return + } + commit, err := ctx.Repo.GitRepo.GetCommit(run.CommitSHA) + if err != nil { + ctx.NotFoundOrServerError("GetCommit", func(err error) bool { + return errors.Is(err, util.ErrNotExist) + }, err) + return + } + rpath, entries, err := actions.ListWorkflows(commit) + if err != nil { + ctx.ServerError("ListWorkflows", err) + return + } + for _, entry := range entries { + if entry.Name() == run.WorkflowID { + ctx.Redirect(fmt.Sprintf("%s/src/commit/%s/%s/%s", ctx.Repo.RepoLink, url.PathEscape(run.CommitSHA), util.PathEscapeSegments(rpath), util.PathEscapeSegments(run.WorkflowID))) + return + } + } + ctx.NotFound(nil) +} + type LogCursor struct { Step int `json:"step"` Cursor int64 `json:"cursor"` @@ -274,7 +304,7 @@ func ViewPost(ctx *context_module.Context) { if task != nil { steps, logs, err := convertToViewModel(ctx, req.LogCursors, task) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("convertToViewModel", err) return } resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, steps...) @@ -378,7 +408,7 @@ func Rerun(ctx *context_module.Context) { run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("GetRunByIndex", err) return } @@ -396,7 +426,7 @@ func Rerun(ctx *context_module.Context) { run.Started = 0 run.Stopped = 0 if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("UpdateRun", err) return } } @@ -411,7 +441,7 @@ func Rerun(ctx *context_module.Context) { // if the job has needs, it should be set to "blocked" status to wait for other jobs shouldBlock := len(j.Needs) > 0 if err := rerunJob(ctx, j, shouldBlock); err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("RerunJob", err) return } } @@ -425,7 +455,7 @@ func Rerun(ctx *context_module.Context) { // jobs other than the specified one should be set to "blocked" status shouldBlock := j.JobID != job.JobID if err := rerunJob(ctx, j, shouldBlock); err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("RerunJob", err) return } } @@ -455,7 +485,7 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou } actions_service.CreateCommitStatus(ctx, job) - _ = job.LoadAttributes(ctx) + actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) return nil @@ -517,7 +547,7 @@ func Cancel(ctx *context_module.Context) { } return nil }); err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("StopTask", err) return } @@ -527,7 +557,11 @@ func Cancel(ctx *context_module.Context) { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } - + if len(updatedjobs) > 0 { + job := updatedjobs[0] + actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } ctx.JSON(http.StatusOK, struct{}{}) } @@ -563,12 +597,18 @@ func Approve(ctx *context_module.Context) { } return nil }); err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("UpdateRunJob", err) return } actions_service.CreateCommitStatus(ctx, jobs...) + if len(updatedjobs) > 0 { + job := updatedjobs[0] + actions_service.NotifyWorkflowRunStatusUpdateWithReload(ctx, job) + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } + for _, job := range updatedjobs { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) @@ -650,7 +690,7 @@ func ArtifactsDeleteView(ctx *context_module.Context) { return } if err = actions_model.SetArtifactNeedDelete(ctx, run.ID, artifactName); err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("SetArtifactNeedDelete", err) return } ctx.JSON(http.StatusOK, struct{}{}) @@ -666,7 +706,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { ctx.HTTPError(http.StatusNotFound, err.Error()) return } - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("GetRunByIndex", err) return } @@ -675,7 +715,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { ArtifactName: artifactName, }) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("FindArtifacts", err) return } if len(artifacts) == 0 { @@ -696,7 +736,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { if len(artifacts) == 1 && actions.IsArtifactV4(artifacts[0]) { err := actions.DownloadArtifactV4(ctx.Base, artifacts[0]) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("DownloadArtifactV4", err) return } return @@ -709,7 +749,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { for _, art := range artifacts { f, err := storage.ActionsArtifacts.Open(art.StoragePath) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("ActionsArtifacts.Open", err) return } @@ -717,7 +757,7 @@ func ArtifactsDownloadView(ctx *context_module.Context) { if art.ContentEncoding == "gzip" { r, err = gzip.NewReader(f) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("gzip.NewReader", err) return } } else { @@ -727,11 +767,11 @@ func ArtifactsDownloadView(ctx *context_module.Context) { w, err := writer.Create(art.ArtifactPath) if err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("writer.Create", err) return } if _, err := io.Copy(w, r); err != nil { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) + ctx.ServerError("io.Copy", err) return } } diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 5d382ebd71..96d1d87836 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -45,10 +45,7 @@ func Branches(ctx *context.Context) { ctx.Data["PageIsViewCode"] = true ctx.Data["PageIsBranches"] = true - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) pageSize := setting.Git.BranchesRangeSize kw := ctx.FormString("q") @@ -261,7 +258,7 @@ func CreateBranch(ctx *context.Context) { func MergeUpstream(ctx *context.Context) { branchName := ctx.FormString("branch") - _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName) + _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName, false) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.JSONErrorNotFound() diff --git a/routers/web/repo/cherry_pick.go b/routers/web/repo/cherry_pick.go deleted file mode 100644 index 690b830bc2..0000000000 --- a/routers/web/repo/cherry_pick.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "bytes" - "errors" - "net/http" - "strings" - - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/forms" - "code.gitea.io/gitea/services/repository/files" -) - -var tplCherryPick templates.TplName = "repo/editor/cherry_pick" - -// CherryPick handles cherrypick GETs -func CherryPick(ctx *context.Context) { - ctx.Data["SHA"] = ctx.PathParam("sha") - cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(ctx.PathParam("sha")) - if err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(err) - return - } - ctx.ServerError("GetCommit", err) - return - } - - if ctx.FormString("cherry-pick-type") == "revert" { - ctx.Data["CherryPickType"] = "revert" - ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") - ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() - } else { - ctx.Data["CherryPickType"] = "cherry-pick" - splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) - ctx.Data["commit_summary"] = splits[0] - ctx.Data["commit_message"] = splits[1] - } - - canCommit := renderCommitRights(ctx) - ctx.Data["TreePath"] = "" - - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - - ctx.HTML(http.StatusOK, tplCherryPick) -} - -// CherryPickPost handles cherrypick POSTs -func CherryPickPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CherryPickForm) - - sha := ctx.PathParam("sha") - ctx.Data["SHA"] = sha - if form.Revert { - ctx.Data["CherryPickType"] = "revert" - } else { - ctx.Data["CherryPickType"] = "cherry-pick" - } - - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplCherryPick) - return - } - - // Cannot commit to a an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplCherryPick, &form) - return - } - - message := strings.TrimSpace(form.CommitSummary) - if message == "" { - if form.Revert { - message = ctx.Locale.TrString("repo.commit.revert-header", sha) - } else { - message = ctx.Locale.TrString("repo.commit.cherry-pick-header", sha) - } - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplCherryPick, &form) - return - } - opts := &files.ApplyDiffPatchOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, - Author: gitCommitter, - Committer: gitCommitter, - } - - // First lets try the simple plain read-tree -m approach - opts.Content = sha - if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, form.Revert, opts); err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - // Drop through to the apply technique - - buf := &bytes.Buffer{} - if form.Revert { - if err := git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), sha, buf); err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist.")) - return - } - ctx.ServerError("GetRawDiff", err) - return - } - } else { - if err := git.GetRawDiff(ctx.Repo.GitRepo, sha, git.RawDiffType("patch"), buf); err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(errors.New("commit " + ctx.PathParam("sha") + " does not exist.")) - return - } - ctx.ServerError("GetRawDiff", err) - return - } - } - - opts.Content = buf.String() - ctx.Data["FileContent"] = opts.Content - - if _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplCherryPick, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) - return - } - } - - if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { - ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) - } else { - ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName)) - } -} diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index 01a6cbc319..b3af138461 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -67,10 +67,7 @@ func Commits(ctx *context.Context) { commitsCount := ctx.Repo.CommitsCount - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) pageSize := ctx.FormInt("limit") if pageSize <= 0 { @@ -230,10 +227,7 @@ func FileHistory(ctx *context.Context) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) commits, err := ctx.Repo.GitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ @@ -377,7 +371,7 @@ func Diff(ctx *context.Context) { ctx.Data["FileIconPoolHTML"] = renderedIconPool.RenderToHTML() } - statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) + statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, commitID, db.ListOptionsAll) if err != nil { log.Error("GetLatestCommitStatus: %v", err) } @@ -457,6 +451,9 @@ func processGitCommits(ctx *context.Context, gitCommits []*git.Commit) ([]*git_m } if !ctx.Repo.CanRead(unit_model.TypeActions) { for _, commit := range commits { + if commit.Status == nil { + continue + } commit.Status.HideActionsURL(ctx) git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses) } diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go index 8b99dd95da..de34a9375c 100644 --- a/routers/web/repo/compare.go +++ b/routers/web/repo/compare.go @@ -575,7 +575,13 @@ func PrepareCompareDiff( ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link() ctx.Data["AfterCommitID"] = headCommitID - ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") + + // follow GitHub's behavior: autofill the form and expand + newPrFormTitle := ctx.FormTrim("title") + newPrFormBody := ctx.FormTrim("body") + ctx.Data["ExpandNewPrForm"] = ctx.FormBool("expand") || ctx.FormBool("quick_pull") || newPrFormTitle != "" || newPrFormBody != "" + ctx.Data["TitleQuery"] = newPrFormTitle + ctx.Data["BodyQuery"] = newPrFormBody if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) || headCommitID == ci.CompareInfo.BaseCommitID { diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go index cbcb3a3b21..2a5ac10282 100644 --- a/routers/web/repo/editor.go +++ b/routers/web/repo/editor.go @@ -4,6 +4,7 @@ package repo import ( + "bytes" "fmt" "io" "net/http" @@ -11,18 +12,17 @@ import ( "strings" git_model "code.gitea.io/gitea/models/git" - repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" @@ -34,863 +34,422 @@ const ( tplEditDiffPreview templates.TplName = "repo/editor/diff_preview" tplDeleteFile templates.TplName = "repo/editor/delete" tplUploadFile templates.TplName = "repo/editor/upload" + tplPatchFile templates.TplName = "repo/editor/patch" + tplCherryPick templates.TplName = "repo/editor/cherry_pick" - frmCommitChoiceDirect string = "direct" - frmCommitChoiceNewBranch string = "commit-to-new-branch" + editorCommitChoiceDirect string = "direct" + editorCommitChoiceNewBranch string = "commit-to-new-branch" ) -func canCreateBasePullRequest(ctx *context.Context) bool { - baseRepo := ctx.Repo.Repository.BaseRepo - return baseRepo != nil && baseRepo.UnitEnabled(ctx, unit.TypePullRequests) -} - -func renderCommitRights(ctx *context.Context) bool { - canCommitToBranch, err := ctx.Repo.CanCommitToBranch(ctx, ctx.Doer) - if err != nil { - log.Error("CanCommitToBranch: %v", err) - } - ctx.Data["CanCommitToBranch"] = canCommitToBranch - ctx.Data["CanCreatePullRequest"] = ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) || canCreateBasePullRequest(ctx) - - return canCommitToBranch.CanCommitToBranch -} - -// redirectForCommitChoice redirects after committing the edit to a branch -func redirectForCommitChoice(ctx *context.Context, commitChoice, newBranchName, treePath string) { - if commitChoice == frmCommitChoiceNewBranch { - // Redirect to a pull request when possible - redirectToPullRequest := false - repo := ctx.Repo.Repository - baseBranch := ctx.Repo.BranchName - headBranch := newBranchName - if repo.UnitEnabled(ctx, unit.TypePullRequests) { - redirectToPullRequest = true - } else if canCreateBasePullRequest(ctx) { - redirectToPullRequest = true - baseBranch = repo.BaseRepo.DefaultBranch - headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch - repo = repo.BaseRepo - } - - if redirectToPullRequest { - ctx.Redirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) - return +func prepareEditorCommitFormOptions(ctx *context.Context, editorAction string) *context.CommitFormOptions { + cleanedTreePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) + if cleanedTreePath != ctx.Repo.TreePath { + redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(cleanedTreePath)) + if ctx.Req.URL.RawQuery != "" { + redirectTo += "?" + ctx.Req.URL.RawQuery } + ctx.Redirect(redirectTo) + return nil } - returnURI := ctx.FormString("return_uri") - - ctx.RedirectToCurrentSite( - returnURI, - ctx.Repo.RepoLink+"/src/branch/"+util.PathEscapeSegments(newBranchName)+"/"+util.PathEscapeSegments(treePath), - ) -} + commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName) + if err != nil { + ctx.ServerError("PrepareCommitFormOptions", err) + return nil + } -// getParentTreeFields returns list of parent tree names and corresponding tree paths -// based on given tree path. -func getParentTreeFields(treePath string) (treeNames, treePaths []string) { - if len(treePath) == 0 { - return treeNames, treePaths + if commitFormOptions.NeedFork { + ForkToEdit(ctx) + return nil } - treeNames = strings.Split(treePath, "/") - treePaths = make([]string, len(treeNames)) - for i := range treeNames { - treePaths[i] = strings.Join(treeNames[:i+1], "/") + if commitFormOptions.WillSubmitToFork && !commitFormOptions.TargetRepo.CanEnableEditor() { + ctx.Data["NotFoundPrompt"] = ctx.Locale.Tr("repo.editor.fork_not_editable") + ctx.NotFound(nil) } - return treeNames, treePaths -} -func editFileCommon(ctx *context.Context, isNewFile bool) { - ctx.Data["PageIsEdit"] = true - ctx.Data["IsNewFile"] = isNewFile ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + ctx.Data["TreePath"] = ctx.Repo.TreePath + ctx.Data["CommitFormOptions"] = commitFormOptions + + // for online editor ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["IsEditingFileOnly"] = ctx.FormString("return_uri") != "" ctx.Data["ReturnURI"] = ctx.FormString("return_uri") -} - -func editFile(ctx *context.Context, isNewFile bool) { - editFileCommon(ctx, isNewFile) - canCommit := renderCommitRights(ctx) - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if treePath != ctx.Repo.TreePath { - if isNewFile { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_new", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) - } else { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_edit", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) - } - return - } - - // Check if the filename (and additional path) is specified in the querystring - // (filename is a misnomer, but kept for compatibility with GitHub) - filePath, fileName := path.Split(ctx.Req.URL.Query().Get("filename")) - filePath = strings.Trim(filePath, "/") - treeNames, treePaths := getParentTreeFields(path.Join(ctx.Repo.TreePath, filePath)) - - if !isNewFile { - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) - if err != nil { - HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) - return - } - - // No way to edit a directory online. - if entry.IsDir() { - ctx.NotFound(nil) - return - } - - blob := entry.Blob() - if blob.Size() >= setting.UI.MaxDisplayFileSize { - ctx.NotFound(err) - return - } - - buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) - if err != nil { - if git.IsErrNotExist(err) { - ctx.NotFound(err) - } else { - ctx.ServerError("getFileReader", err) - } - return - } - - defer dataRc.Close() - - ctx.Data["FileSize"] = blob.Size() - - // Only some file types are editable online as text. - if !fInfo.st.IsRepresentableAsText() || fInfo.isLFSFile { - ctx.NotFound(nil) - return - } - - d, _ := io.ReadAll(dataRc) - - buf = append(buf, d...) - if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { - log.Error("ToUTF8: %v", err) - ctx.Data["FileContent"] = string(buf) - } else { - ctx.Data["FileContent"] = content - } - } else { - // Append filename from query, or empty string to allow username the new file. - treeNames = append(treeNames, fileName) - } - - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths + // form fields ctx.Data["commit_summary"] = "" ctx.Data["commit_message"] = "" - ctx.Data["commit_choice"] = util.Iif(canCommit, frmCommitChoiceDirect, frmCommitChoiceNewBranch) - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) + ctx.Data["commit_choice"] = util.Iif(commitFormOptions.CanCommitToBranch, editorCommitChoiceDirect, editorCommitChoiceNewBranch) + ctx.Data["new_branch_name"] = getUniquePatchBranchName(ctx, ctx.Doer.LowerName, commitFormOptions.TargetRepo) ctx.Data["last_commit"] = ctx.Repo.CommitID - - ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) - - ctx.HTML(http.StatusOK, tplEditFile) + return commitFormOptions } -// GetEditorConfig returns a editorconfig JSON string for given treePath or "null" -func GetEditorConfig(ctx *context.Context, treePath string) string { - ec, _, err := ctx.Repo.GetEditorconfig() - if err == nil { - def, err := ec.GetDefinitionForFilename(treePath) - if err == nil { - jsonStr, _ := json.Marshal(def) - return string(jsonStr) - } - } - return "null" +func prepareTreePathFieldsAndPaths(ctx *context.Context, treePath string) { + // show the tree path fields in the "breadcrumb" and help users to edit the target tree path + ctx.Data["TreeNames"], ctx.Data["TreePaths"] = getParentTreeFields(strings.TrimPrefix(treePath, "/")) } -// EditFile render edit file page -func EditFile(ctx *context.Context) { - editFile(ctx, false) +type preparedEditorCommitForm[T any] struct { + form T + commonForm *forms.CommitCommonForm + CommitFormOptions *context.CommitFormOptions + OldBranchName string + NewBranchName string + GitCommitter *files_service.IdentityOptions } -// NewFile render create file page -func NewFile(ctx *context.Context) { - editFile(ctx, true) +func (f *preparedEditorCommitForm[T]) GetCommitMessage(defaultCommitMessage string) string { + commitMessage := util.IfZero(strings.TrimSpace(f.commonForm.CommitSummary), defaultCommitMessage) + if body := strings.TrimSpace(f.commonForm.CommitMessage); body != "" { + commitMessage += "\n\n" + body + } + return commitMessage } -func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile bool) { - editFileCommon(ctx, isNewFile) - ctx.Data["PageHasPosted"] = true - - canCommit := renderCommitRights(ctx) - treeNames, treePaths := getParentTreeFields(form.TreePath) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - ctx.Data["TreePath"] = form.TreePath - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["FileContent"] = form.Content - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath) - +func prepareEditorCommitSubmittedForm[T forms.CommitCommonFormInterface](ctx *context.Context) *preparedEditorCommitForm[T] { + form := web.GetForm(ctx).(T) if ctx.HasError() { - ctx.HTML(http.StatusOK, tplEditFile) - return + ctx.JSONError(ctx.GetErrMsg()) + return nil } - // Cannot commit to an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) - return - } + commonForm := form.GetCommitCommonForm() + commonForm.TreePath = files_service.CleanGitTreePath(commonForm.TreePath) - // CommitSummary is optional in the web form, if empty, give it a default message based on add or update - // `message` will be both the summary and message combined - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - if isNewFile { - message = ctx.Locale.TrString("repo.editor.add", form.TreePath) - } else { - message = ctx.Locale.TrString("repo.editor.update", form.TreePath) - } + commitFormOptions, err := context.PrepareCommitFormOptions(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.Permission, ctx.Repo.RefFullName) + if err != nil { + ctx.ServerError("PrepareCommitFormOptions", err) + return nil } - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage + if commitFormOptions.NeedFork { + // It shouldn't happen, because we should have done the checks in the "GET" request. But just in case. + ctx.JSONError(ctx.Locale.TrString("error.not_found")) + return nil } - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplEditFile, &form) - return + // check commit behavior + fromBaseBranch := ctx.FormString("from_base_branch") + commitToNewBranch := commonForm.CommitChoice == editorCommitChoiceNewBranch || fromBaseBranch != "" + targetBranchName := util.Iif(commitToNewBranch, commonForm.NewBranchName, ctx.Repo.BranchName) + if targetBranchName == ctx.Repo.BranchName && !commitFormOptions.CanCommitToBranch { + ctx.JSONError(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", targetBranchName)) + return nil } - operation := "update" - if isNewFile { - operation = "create" + if !issues.CanMaintainerWriteToBranch(ctx, ctx.Repo.Permission, targetBranchName, ctx.Doer) { + ctx.NotFound(nil) + return nil } - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, - Files: []*files_service.ChangeRepoFile{ - { - Operation: operation, - FromTreePath: ctx.Repo.TreePath, - TreePath: form.TreePath, - ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")), - }, - }, - Signoff: form.Signoff, - Author: gitCommitter, - Committer: gitCommitter, - }); err != nil { - // This is where we handle all the errors thrown by files_service.ChangeRepoFiles - if git.IsErrNotExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form) - } else if git_model.IsErrLFSFileLocked(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplEditFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form) - default: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", fileErr.Path), tplEditFile, &form) - } - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrRepoFileAlreadyExists(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form) - } else if git.IsErrBranchNotExist(err) { - // For when a user adds/updates a file to a branch that no longer exists - if branchErr, ok := err.(git.ErrBranchNotExist); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - ctx.Data["Err_NewBranchName"] = true - if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.commit_id_not_matching"), tplEditFile, &form) - } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_out_of_date"), tplEditFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplEditFile, &form) + // Committer user info + gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, commonForm.CommitEmail) + if !valid { + ctx.JSONError(ctx.Tr("repo.editor.invalid_commit_email")) + return nil + } + + if commitToNewBranch { + // if target branch exists, we should stop + targetBranchExists, err := git_model.IsBranchExist(ctx, commitFormOptions.TargetRepo.ID, targetBranchName) + if err != nil { + ctx.ServerError("IsBranchExist", err) + return nil + } else if targetBranchExists { + if fromBaseBranch != "" { + ctx.JSONError(ctx.Tr("repo.editor.fork_branch_exists", targetBranchName)) } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("editFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplEditFile, &form) + ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", targetBranchName)) } - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.fail_to_update_file", form.TreePath), - "Summary": ctx.Tr("repo.editor.fail_to_update_file_summary"), - "Details": utils.SanitizeFlashErrorString(err.Error()), - }) - if err != nil { - ctx.ServerError("editFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplEditFile, &form) + return nil } } - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) -} - -// EditFilePost response for editing file -func EditFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - editFilePost(ctx, *form, false) -} - -// NewFilePost response for creating file -func NewFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - editFilePost(ctx, *form, true) -} - -// DiffPreviewPost render preview diff page -func DiffPreviewPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditPreviewDiffForm) - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if len(treePath) == 0 { - ctx.HTTPError(http.StatusInternalServerError, "file name to diff is invalid") - return - } - - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetTreeEntryByPath: "+err.Error()) - return - } else if entry.IsDir() { - ctx.HTTPError(http.StatusUnprocessableEntity) - return - } - - diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, "GetDiffPreview: "+err.Error()) - return + oldBranchName := ctx.Repo.BranchName + if fromBaseBranch != "" { + err = editorPushBranchToForkedRepository(ctx, ctx.Doer, ctx.Repo.Repository.BaseRepo, fromBaseBranch, commitFormOptions.TargetRepo, targetBranchName) + if err != nil { + log.Error("Unable to editorPushBranchToForkedRepository: %v", err) + ctx.JSONError(ctx.Tr("repo.editor.fork_failed_to_push_branch", targetBranchName)) + return nil + } + // we have pushed the base branch as the new branch, now we need to commit the changes directly to the new branch + oldBranchName = targetBranchName } - if len(diff.Files) != 0 { - ctx.Data["File"] = diff.Files[0] + return &preparedEditorCommitForm[T]{ + form: form, + commonForm: commonForm, + CommitFormOptions: commitFormOptions, + OldBranchName: oldBranchName, + NewBranchName: targetBranchName, + GitCommitter: gitCommitter, } - - ctx.HTML(http.StatusOK, tplEditDiffPreview) } -// DeleteFile render delete file page -func DeleteFile(ctx *context.Context) { - ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - treePath := cleanUploadFileName(ctx.Repo.TreePath) - - if treePath != ctx.Repo.TreePath { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_delete", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) +// redirectForCommitChoice redirects after committing the edit to a branch +func redirectForCommitChoice[T any](ctx *context.Context, parsed *preparedEditorCommitForm[T], treePath string) { + // when editing a file in a PR, it should return to the origin location + if returnURI := ctx.FormString("return_uri"); returnURI != "" && httplib.IsCurrentGiteaSiteURL(ctx, returnURI) { + ctx.JSONRedirect(returnURI) return } - ctx.Data["TreePath"] = treePath - canCommit := renderCommitRights(ctx) - - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - ctx.Data["last_commit"] = ctx.Repo.CommitID - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch + if parsed.commonForm.CommitChoice == editorCommitChoiceNewBranch { + // Redirect to a pull request when possible + redirectToPullRequest := false + repo, baseBranch, headBranch := ctx.Repo.Repository, parsed.OldBranchName, parsed.NewBranchName + if ctx.Repo.Repository.IsFork && parsed.CommitFormOptions.CanCreateBasePullRequest { + redirectToPullRequest = true + baseBranch = repo.BaseRepo.DefaultBranch + headBranch = repo.Owner.Name + "/" + repo.Name + ":" + headBranch + repo = repo.BaseRepo + } else if repo.UnitEnabled(ctx, unit.TypePullRequests) { + redirectToPullRequest = true + } + if redirectToPullRequest { + ctx.JSONRedirect(repo.Link() + "/compare/" + util.PathEscapeSegments(baseBranch) + "..." + util.PathEscapeSegments(headBranch)) + return + } } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.HTML(http.StatusOK, tplDeleteFile) + // redirect to the newly updated file + redirectTo := util.URLJoin(ctx.Repo.RepoLink, "src/branch", util.PathEscapeSegments(parsed.NewBranchName), util.PathEscapeSegments(treePath)) + ctx.JSONRedirect(redirectTo) } -// DeleteFilePost response for deleting file -func DeleteFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.DeleteRepoFileForm) - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - ctx.Data["PageIsDelete"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["TreePath"] = ctx.Repo.TreePath - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplDeleteFile) - return - } - - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form) - return - } - - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - message = ctx.Locale.TrString("repo.editor.delete", ctx.Repo.TreePath) - } - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage +func editFileOpenExisting(ctx *context.Context) (prefetch []byte, dataRc io.ReadCloser, fInfo *fileInfo) { + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + if err != nil { + HandleGitError(ctx, "GetTreeEntryByPath", err) + return nil, nil, nil } - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplDeleteFile, &form) - return + // No way to edit a directory online. + if entry.IsDir() { + ctx.NotFound(nil) + return nil, nil, nil } - if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Files: []*files_service.ChangeRepoFile{ - { - Operation: "delete", - TreePath: ctx.Repo.TreePath, - }, - }, - Message: message, - Signoff: form.Signoff, - Author: gitCommitter, - Committer: gitCommitter, - }); err != nil { - // This is where we handle all the errors thrown by repofiles.DeleteRepoFile - if git.IsErrNotExist(err) || files_service.IsErrRepoFileDoesNotExist(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplDeleteFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - if fileErr, ok := err.(files_service.ErrFilePathInvalid); ok { - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplDeleteFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplDeleteFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplDeleteFile, &form) - default: - ctx.ServerError("DeleteRepoFile", err) - } - } else { - ctx.ServerError("DeleteRepoFile", err) - } - } else if git.IsErrBranchNotExist(err) { - // For when a user deletes a file to a branch that no longer exists - if branchErr, ok := err.(git.ErrBranchNotExist); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplDeleteFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - if branchErr, ok := err.(git_model.ErrBranchAlreadyExists); ok { - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplDeleteFile, &form) - } else { - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_deleting", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(form.LastCommit)+"..."+util.PathEscapeSegments(ctx.Repo.CommitID)), tplDeleteFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplDeleteFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("DeleteFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplDeleteFile, &form) - } + blob := entry.Blob() + buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound(err) } else { - ctx.ServerError("DeleteRepoFile", err) + ctx.ServerError("getFileReader", err) } - return + return nil, nil, nil } - ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath)) - treePath := path.Dir(ctx.Repo.TreePath) - if treePath == "." { - treePath = "" // the file deleted was in the root, so we return the user to the root directory - } - if len(treePath) > 0 { - // Need to get the latest commit since it changed - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) - if err == nil && commit != nil { - // We have the comment, now find what directory we can return the user to - // (must have entries) - treePath = GetClosestParentWithFiles(treePath, commit) - } else { - treePath = "" // otherwise return them to the root of the repo + if fInfo.isLFSFile() { + lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) + if err != nil { + _ = dataRc.Close() + ctx.ServerError("GetTreePathLock", err) + return nil, nil, nil + } else if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + _ = dataRc.Close() + ctx.NotFound(nil) + return nil, nil, nil } } - redirectForCommitChoice(ctx, form.CommitChoice, branchName, treePath) + return buf, dataRc, fInfo } -// UploadFile render upload file page -func UploadFile(ctx *context.Context) { - ctx.Data["PageIsUpload"] = true - upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) - treePath := cleanUploadFileName(ctx.Repo.TreePath) - if treePath != ctx.Repo.TreePath { - ctx.Redirect(path.Join(ctx.Repo.RepoLink, "_upload", util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(treePath))) - return - } - ctx.Repo.TreePath = treePath - - treeNames, treePaths := getParentTreeFields(ctx.Repo.TreePath) - if len(treeNames) == 0 { - // We must at least have one element for user to input. - treeNames = []string{""} - } - - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - - ctx.HTML(http.StatusOK, tplUploadFile) -} - -// UploadFilePost response for uploading file -func UploadFilePost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.UploadRepoFileForm) - ctx.Data["PageIsUpload"] = true - upload.AddUploadContext(ctx, "repo") - canCommit := renderCommitRights(ctx) - - oldBranchName := ctx.Repo.BranchName - branchName := oldBranchName - - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - - form.TreePath = cleanUploadFileName(form.TreePath) +func EditFile(ctx *context.Context) { + editorAction := ctx.PathParam("editor_action") + isNewFile := editorAction == "_new" + ctx.Data["IsNewFile"] = isNewFile - treeNames, treePaths := getParentTreeFields(form.TreePath) - if len(treeNames) == 0 { - // We must at least have one element for user to input. - treeNames = []string{""} + // Check if the filename (and additional path) is specified in the querystring + // (filename is a misnomer, but kept for compatibility with GitHub) + urlQuery := ctx.Req.URL.Query() + queryFilename := urlQuery.Get("filename") + if queryFilename != "" { + newTreePath := path.Join(ctx.Repo.TreePath, queryFilename) + redirectTo := fmt.Sprintf("%s/%s/%s/%s", ctx.Repo.RepoLink, editorAction, util.PathEscapeSegments(ctx.Repo.BranchName), util.PathEscapeSegments(newTreePath)) + urlQuery.Del("filename") + if newQueryParams := urlQuery.Encode(); newQueryParams != "" { + redirectTo += "?" + newQueryParams + } + ctx.Redirect(redirectTo) + return } - ctx.Data["TreePath"] = form.TreePath - ctx.Data["TreeNames"] = treeNames - ctx.Data["TreePaths"] = treePaths - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = branchName + // on the "New File" page, we should add an empty path field to make end users could input a new name + prepareTreePathFieldsAndPaths(ctx, util.Iif(isNewFile, ctx.Repo.TreePath+"/", ctx.Repo.TreePath)) - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplUploadFile) + prepareEditorCommitFormOptions(ctx, editorAction) + if ctx.Written() { return } - if oldBranchName != branchName { - if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err == nil && exist { - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplUploadFile, &form) + if !isNewFile { + prefetch, dataRc, fInfo := editFileOpenExisting(ctx) + if ctx.Written() { return } - } else if !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplUploadFile, &form) - return - } + defer dataRc.Close() - if !ctx.Repo.Repository.IsEmpty { - var newTreePath string - for _, part := range treeNames { - newTreePath = path.Join(newTreePath, part) - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath) + ctx.Data["FileSize"] = fInfo.fileSize + + // Only some file types are editable online as text. + if fInfo.isLFSFile() { + ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") + } else if !fInfo.st.IsRepresentableAsText() { + ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") + } else if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { + ctx.Data["NotEditableReason"] = ctx.Tr("repo.editor.cannot_edit_too_large_file") + } + + if ctx.Data["NotEditableReason"] == nil { + buf, err := io.ReadAll(io.MultiReader(bytes.NewReader(prefetch), dataRc)) if err != nil { - if git.IsErrNotExist(err) { - break // Means there is no item with that name, so we're good - } - ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err) + ctx.ServerError("ReadAll", err) return } - - // User can only upload files to a directory, the directory name shouldn't be an existing file. - if !entry.IsDir() { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplUploadFile, &form) - return + if content, err := charset.ToUTF8(buf, charset.ConvertOpts{KeepBOM: true}); err != nil { + ctx.Data["FileContent"] = string(buf) + } else { + ctx.Data["FileContent"] = content } } } - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - dir := form.TreePath - if dir == "" { - dir = "/" - } - message = ctx.Locale.TrString("repo.editor.upload_files_to_dir", dir) - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } + ctx.Data["EditorconfigJson"] = getContextRepoEditorConfig(ctx, ctx.Repo.TreePath) + ctx.HTML(http.StatusOK, tplEditFile) +} - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplUploadFile, &form) +func EditFilePost(ctx *context.Context) { + editorAction := ctx.PathParam("editor_action") + isNewFile := editorAction == "_new" + parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) + if ctx.Written() { return } - if err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ - LastCommitID: ctx.Repo.CommitID, - OldBranch: oldBranchName, - NewBranch: branchName, - TreePath: form.TreePath, - Message: message, - Files: form.Files, - Signoff: form.Signoff, - Author: gitCommitter, - Committer: gitCommitter, - }); err != nil { - if git_model.IsErrLFSFileLocked(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(git_model.ErrLFSFileLocked).Path, err.(git_model.ErrLFSFileLocked).UserName), tplUploadFile, &form) - } else if files_service.IsErrFilenameInvalid(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplUploadFile, &form) - } else if files_service.IsErrFilePathInvalid(err) { - ctx.Data["Err_TreePath"] = true - fileErr := err.(files_service.ErrFilePathInvalid) - switch fileErr.Type { - case git.EntryModeSymlink: - ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplUploadFile, &form) - case git.EntryModeTree: - ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplUploadFile, &form) - case git.EntryModeBlob: - ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplUploadFile, &form) - default: - ctx.HTTPError(http.StatusInternalServerError, err.Error()) - } - } else if files_service.IsErrRepoFileAlreadyExists(err) { - ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplUploadFile, &form) - } else if git.IsErrBranchNotExist(err) { - branchErr := err.(git.ErrBranchNotExist) - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplUploadFile, &form) - } else if git_model.IsErrBranchAlreadyExists(err) { - // For when a user specifies a new branch that already exists - ctx.Data["Err_NewBranchName"] = true - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplUploadFile, &form) - } else if git.IsErrPushOutOfDate(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(form.NewBranchName)), tplUploadFile, &form) - } else if git.IsErrPushRejected(err) { - errPushRej := err.(*git.ErrPushRejected) - if len(errPushRej.Message) == 0 { - ctx.RenderWithErr(ctx.Tr("repo.editor.push_rejected_no_message"), tplUploadFile, &form) - } else { - flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ - "Message": ctx.Tr("repo.editor.push_rejected"), - "Summary": ctx.Tr("repo.editor.push_rejected_summary"), - "Details": utils.SanitizeFlashErrorString(errPushRej.Message), - }) - if err != nil { - ctx.ServerError("UploadFilePost.HTMLString", err) - return - } - ctx.RenderWithErr(flashError, tplUploadFile, &form) - } - } else { - // os.ErrNotExist - upload file missing in the intervening time?! - log.Error("Error during upload to repo: %-v to filepath: %s on %s from %s: %v", ctx.Repo.Repository, form.TreePath, oldBranchName, form.NewBranchName, err) - ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form) - } + defaultCommitMessage := util.Iif(isNewFile, ctx.Locale.TrString("repo.editor.add", parsed.form.TreePath), ctx.Locale.TrString("repo.editor.update", parsed.form.TreePath)) + + var operation string + if isNewFile { + operation = "create" + } else if parsed.form.Content.Has() { + // The form content only has data if the file is representable as text, is not too large and not in lfs. + operation = "update" + } else if ctx.Repo.TreePath != parsed.form.TreePath { + // If it doesn't have data, the only possible operation is a "rename" + operation = "rename" + } else { + // It should never happen, just in case + ctx.JSONError(ctx.Tr("error.occurred")) return } - if ctx.Repo.Repository.IsEmpty { - if isEmpty, err := ctx.Repo.GitRepo.IsEmpty(); err == nil && !isEmpty { - _ = repo_model.UpdateRepositoryColsWithAutoTime(ctx, &repo_model.Repository{ID: ctx.Repo.Repository.ID, IsEmpty: false}, "is_empty") - } + _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Files: []*files_service.ChangeRepoFile{ + { + Operation: operation, + FromTreePath: ctx.Repo.TreePath, + TreePath: parsed.form.TreePath, + ContentReader: strings.NewReader(strings.ReplaceAll(parsed.form.Content.Value(), "\r", "")), + }, + }, + Signoff: parsed.form.Signoff, + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) + return } - redirectForCommitChoice(ctx, form.CommitChoice, branchName, form.TreePath) + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) } -func cleanUploadFileName(name string) string { - // Rebase the filename - name = util.PathJoinRel(name) - // Git disallows any filenames to have a .git directory in them. - for _, part := range strings.Split(name, "/") { - if strings.ToLower(part) == ".git" { - return "" - } +// DeleteFile render delete file page +func DeleteFile(ctx *context.Context) { + prepareEditorCommitFormOptions(ctx, "_delete") + if ctx.Written() { + return } - return name + ctx.Data["PageIsDelete"] = true + ctx.HTML(http.StatusOK, tplDeleteFile) } -// UploadFileToServer upload file to server file dir not git -func UploadFileToServer(ctx *context.Context) { - file, header, err := ctx.Req.FormFile("file") - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("FormFile: %v", err)) +// DeleteFilePost response for deleting file +func DeleteFilePost(ctx *context.Context) { + parsed := prepareEditorCommitSubmittedForm[*forms.DeleteRepoFileForm](ctx) + if ctx.Written() { return } - defer file.Close() - - buf := make([]byte, 1024) - n, _ := util.ReadAtMost(file, buf) - if n > 0 { - buf = buf[:n] - } - err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes) + treePath := ctx.Repo.TreePath + _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Files: []*files_service.ChangeRepoFile{ + { + Operation: "delete", + TreePath: treePath, + }, + }, + Message: parsed.GetCommitMessage(ctx.Locale.TrString("repo.editor.delete", treePath)), + Signoff: parsed.form.Signoff, + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) if err != nil { - ctx.HTTPError(http.StatusBadRequest, err.Error()) + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } - name := cleanUploadFileName(header.Filename) - if len(name) == 0 { - ctx.HTTPError(http.StatusInternalServerError, "Upload file name is invalid") - return - } + ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", treePath)) + redirectTreePath := getClosestParentWithFiles(ctx.Repo.GitRepo, parsed.NewBranchName, treePath) + redirectForCommitChoice(ctx, parsed, redirectTreePath) +} - upload, err := repo_model.NewUpload(ctx, name, buf, file) - if err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("NewUpload: %v", err)) +func UploadFile(ctx *context.Context) { + ctx.Data["PageIsUpload"] = true + prepareTreePathFieldsAndPaths(ctx, ctx.Repo.TreePath) + opts := prepareEditorCommitFormOptions(ctx, "_upload") + if ctx.Written() { return } + upload.AddUploadContextForRepo(ctx, opts.TargetRepo) - log.Trace("New file uploaded: %s", upload.UUID) - ctx.JSON(http.StatusOK, map[string]string{ - "uuid": upload.UUID, - }) + ctx.HTML(http.StatusOK, tplUploadFile) } -// RemoveUploadFileFromServer remove file from server file dir -func RemoveUploadFileFromServer(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.RemoveUploadFileForm) - if len(form.File) == 0 { - ctx.Status(http.StatusNoContent) +func UploadFilePost(ctx *context.Context) { + parsed := prepareEditorCommitSubmittedForm[*forms.UploadRepoFileForm](ctx) + if ctx.Written() { return } - if err := repo_model.DeleteUploadByUUID(ctx, form.File); err != nil { - ctx.HTTPError(http.StatusInternalServerError, fmt.Sprintf("DeleteUploadByUUID: %v", err)) + defaultCommitMessage := ctx.Locale.TrString("repo.editor.upload_files_to_dir", util.IfZero(parsed.form.TreePath, "/")) + err := files_service.UploadRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.UploadRepoFileOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + TreePath: parsed.form.TreePath, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Files: parsed.form.Files, + Signoff: parsed.form.Signoff, + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) return } - - log.Trace("Upload file removed: %s", form.File) - ctx.Status(http.StatusNoContent) -} - -// GetUniquePatchBranchName Gets a unique branch name for a new patch branch -// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format -// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to -// type in the branch name themselves (will be an empty field) -func GetUniquePatchBranchName(ctx *context.Context) string { - prefix := ctx.Doer.LowerName + "-patch-" - for i := 1; i <= 1000; i++ { - branchName := fmt.Sprintf("%s%d", prefix, i) - if exist, err := git_model.IsBranchExist(ctx, ctx.Repo.Repository.ID, branchName); err != nil { - log.Error("GetUniquePatchBranchName: %v", err) - return "" - } else if !exist { - return branchName - } - } - return "" -} - -// GetClosestParentWithFiles Recursively gets the path of parent in a tree that has files (used when file in a tree is -// deleted). Returns "" for the root if no parents other than the root have files. If the given treePath isn't a -// SubTree or it has no entries, we go up one dir and see if we can return the user to that listing. -func GetClosestParentWithFiles(treePath string, commit *git.Commit) string { - if len(treePath) == 0 || treePath == "." { - return "" - } - // see if the tree has entries - if tree, err := commit.SubTree(treePath); err != nil { - // failed to get tree, going up a dir - return GetClosestParentWithFiles(path.Dir(treePath), commit) - } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 { - // no files in this dir, going up a dir - return GetClosestParentWithFiles(path.Dir(treePath), commit) - } - return treePath + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) } diff --git a/routers/web/repo/editor_apply_patch.go b/routers/web/repo/editor_apply_patch.go new file mode 100644 index 0000000000..bd2811cc5f --- /dev/null +++ b/routers/web/repo/editor_apply_patch.go @@ -0,0 +1,51 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + "strings" + + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/repository/files" +) + +func NewDiffPatch(ctx *context.Context) { + prepareEditorCommitFormOptions(ctx, "_diffpatch") + if ctx.Written() { + return + } + + ctx.Data["PageIsPatch"] = true + ctx.HTML(http.StatusOK, tplPatchFile) +} + +// NewDiffPatchPost response for sending patch page +func NewDiffPatchPost(ctx *context.Context) { + parsed := prepareEditorCommitSubmittedForm[*forms.EditRepoFileForm](ctx) + if ctx.Written() { + return + } + + defaultCommitMessage := ctx.Locale.TrString("repo.editor.patch") + _, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Content: strings.ReplaceAll(parsed.form.Content.Value(), "\r\n", "\n"), + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + }) + if err != nil { + err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch") + } + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) + return + } + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) +} diff --git a/routers/web/repo/editor_cherry_pick.go b/routers/web/repo/editor_cherry_pick.go new file mode 100644 index 0000000000..10c2741b1c --- /dev/null +++ b/routers/web/repo/editor_cherry_pick.go @@ -0,0 +1,86 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "bytes" + "net/http" + "strings" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" + "code.gitea.io/gitea/services/repository/files" +) + +func CherryPick(ctx *context.Context) { + prepareEditorCommitFormOptions(ctx, "_cherrypick") + if ctx.Written() { + return + } + + fromCommitID := ctx.PathParam("sha") + ctx.Data["FromCommitID"] = fromCommitID + cherryPickCommit, err := ctx.Repo.GitRepo.GetCommit(fromCommitID) + if err != nil { + HandleGitError(ctx, "GetCommit", err) + return + } + + if ctx.FormString("cherry-pick-type") == "revert" { + ctx.Data["CherryPickType"] = "revert" + ctx.Data["commit_summary"] = "revert " + ctx.PathParam("sha") + ctx.Data["commit_message"] = "revert " + cherryPickCommit.Message() + } else { + ctx.Data["CherryPickType"] = "cherry-pick" + splits := strings.SplitN(cherryPickCommit.Message(), "\n", 2) + ctx.Data["commit_summary"] = splits[0] + ctx.Data["commit_message"] = splits[1] + } + + ctx.HTML(http.StatusOK, tplCherryPick) +} + +func CherryPickPost(ctx *context.Context) { + fromCommitID := ctx.PathParam("sha") + parsed := prepareEditorCommitSubmittedForm[*forms.CherryPickForm](ctx) + if ctx.Written() { + return + } + + defaultCommitMessage := util.Iif(parsed.form.Revert, ctx.Locale.TrString("repo.commit.revert-header", fromCommitID), ctx.Locale.TrString("repo.commit.cherry-pick-header", fromCommitID)) + opts := &files.ApplyDiffPatchOptions{ + LastCommitID: parsed.form.LastCommit, + OldBranch: parsed.OldBranchName, + NewBranch: parsed.NewBranchName, + Message: parsed.GetCommitMessage(defaultCommitMessage), + Author: parsed.GitCommitter, + Committer: parsed.GitCommitter, + } + + // First try the simple plain read-tree -m approach + opts.Content = fromCommitID + if _, err := files.CherryPick(ctx, ctx.Repo.Repository, ctx.Doer, parsed.form.Revert, opts); err != nil { + // Drop through to the "apply" method + buf := &bytes.Buffer{} + if parsed.form.Revert { + err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf) + } else { + err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf) + } + if err == nil { + opts.Content = buf.String() + _, err = files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, opts) + if err != nil { + err = util.ErrorWrapLocale(err, "repo.editor.fail_to_apply_patch") + } + } + if err != nil { + editorHandleFileOperationError(ctx, parsed.NewBranchName, err) + return + } + } + redirectForCommitChoice(ctx, parsed, parsed.form.TreePath) +} diff --git a/routers/web/repo/editor_error.go b/routers/web/repo/editor_error.go new file mode 100644 index 0000000000..245226a039 --- /dev/null +++ b/routers/web/repo/editor_error.go @@ -0,0 +1,82 @@ +// Copyright 2025 Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + + git_model "code.gitea.io/gitea/models/git" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/utils" + context_service "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" +) + +func errorAs[T error](v error) (e T, ok bool) { + if errors.As(v, &e) { + return e, true + } + return e, false +} + +func editorHandleFileOperationErrorRender(ctx *context_service.Context, message, summary, details string) { + flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ + "Message": message, + "Summary": summary, + "Details": utils.SanitizeFlashErrorString(details), + }) + if err == nil { + ctx.JSONError(flashError) + } else { + log.Error("RenderToHTML: %v", err) + ctx.JSONError(message + "\n" + summary + "\n" + utils.SanitizeFlashErrorString(details)) + } +} + +func editorHandleFileOperationError(ctx *context_service.Context, targetBranchName string, err error) { + if errAs := util.ErrorAsLocale(err); errAs != nil { + ctx.JSONError(ctx.Tr(errAs.TrKey, errAs.TrArgs...)) + } else if errAs, ok := errorAs[git.ErrNotExist](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.file_modifying_no_longer_exists", errAs.RelPath)) + } else if errAs, ok := errorAs[git_model.ErrLFSFileLocked](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.upload_file_is_locked", errAs.Path, errAs.UserName)) + } else if errAs, ok := errorAs[files_service.ErrFilenameInvalid](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path)) + } else if errAs, ok := errorAs[files_service.ErrFilePathInvalid](err); ok { + switch errAs.Type { + case git.EntryModeSymlink: + ctx.JSONError(ctx.Tr("repo.editor.file_is_a_symlink", errAs.Path)) + case git.EntryModeTree: + ctx.JSONError(ctx.Tr("repo.editor.filename_is_a_directory", errAs.Path)) + case git.EntryModeBlob: + ctx.JSONError(ctx.Tr("repo.editor.directory_is_a_file", errAs.Path)) + default: + ctx.JSONError(ctx.Tr("repo.editor.filename_is_invalid", errAs.Path)) + } + } else if errAs, ok := errorAs[files_service.ErrRepoFileAlreadyExists](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.file_already_exists", errAs.Path)) + } else if errAs, ok := errorAs[git.ErrBranchNotExist](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.branch_does_not_exist", errAs.Name)) + } else if errAs, ok := errorAs[git_model.ErrBranchAlreadyExists](err); ok { + ctx.JSONError(ctx.Tr("repo.editor.branch_already_exists", errAs.BranchName)) + } else if files_service.IsErrCommitIDDoesNotMatch(err) { + ctx.JSONError(ctx.Tr("repo.editor.commit_id_not_matching")) + } else if files_service.IsErrCommitIDDoesNotMatch(err) || git.IsErrPushOutOfDate(err) { + ctx.JSONError(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+util.PathEscapeSegments(ctx.Repo.CommitID)+"..."+util.PathEscapeSegments(targetBranchName))) + } else if errAs, ok := errorAs[*git.ErrPushRejected](err); ok { + if errAs.Message == "" { + ctx.JSONError(ctx.Tr("repo.editor.push_rejected_no_message")) + } else { + editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.push_rejected"), ctx.Locale.TrString("repo.editor.push_rejected_summary"), errAs.Message) + } + } else if errors.Is(err, util.ErrNotExist) { + ctx.JSONError(ctx.Tr("error.not_found")) + } else { + setting.PanicInDevOrTesting("unclear err %T: %v", err, err) + editorHandleFileOperationErrorRender(ctx, ctx.Locale.TrString("repo.editor.failed_to_commit"), ctx.Locale.TrString("repo.editor.failed_to_commit_summary"), err.Error()) + } +} diff --git a/routers/web/repo/editor_fork.go b/routers/web/repo/editor_fork.go new file mode 100644 index 0000000000..b78a634c00 --- /dev/null +++ b/routers/web/repo/editor_fork.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +const tplEditorFork templates.TplName = "repo/editor/fork" + +func ForkToEdit(ctx *context.Context) { + ctx.HTML(http.StatusOK, tplEditorFork) +} + +func ForkToEditPost(ctx *context.Context) { + ForkRepoTo(ctx, ctx.Doer, repo_service.ForkRepoOptions{ + BaseRepo: ctx.Repo.Repository, + Name: getUniqueRepositoryName(ctx, ctx.Doer.ID, ctx.Repo.Repository.Name), + Description: ctx.Repo.Repository.Description, + SingleBranch: ctx.Repo.Repository.DefaultBranch, // maybe we only need the default branch in the fork? + }) + if ctx.Written() { + return + } + ctx.JSONRedirect("") // reload the page, the new fork should be editable now +} diff --git a/routers/web/repo/editor_preview.go b/routers/web/repo/editor_preview.go new file mode 100644 index 0000000000..14be5b72b6 --- /dev/null +++ b/routers/web/repo/editor_preview.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "code.gitea.io/gitea/services/context" + files_service "code.gitea.io/gitea/services/repository/files" +) + +func DiffPreviewPost(ctx *context.Context) { + content := ctx.FormString("content") + treePath := files_service.CleanGitTreePath(ctx.Repo.TreePath) + if treePath == "" { + ctx.HTTPError(http.StatusBadRequest, "file name to diff is invalid") + return + } + + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(treePath) + if err != nil { + ctx.ServerError("GetTreeEntryByPath", err) + return + } else if entry.IsDir() { + ctx.HTTPError(http.StatusUnprocessableEntity) + return + } + + diff, err := files_service.GetDiffPreview(ctx, ctx.Repo.Repository, ctx.Repo.BranchName, treePath, content) + if err != nil { + ctx.ServerError("GetDiffPreview", err) + return + } + + if len(diff.Files) != 0 { + ctx.Data["File"] = diff.Files[0] + } + + ctx.HTML(http.StatusOK, tplEditDiffPreview) +} diff --git a/routers/web/repo/editor_test.go b/routers/web/repo/editor_test.go index 89bf8f309c..6e2c1d6219 100644 --- a/routers/web/repo/editor_test.go +++ b/routers/web/repo/editor_test.go @@ -6,76 +6,27 @@ package repo import ( "testing" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" - "code.gitea.io/gitea/services/contexttest" "github.com/stretchr/testify/assert" ) -func TestCleanUploadName(t *testing.T) { +func TestEditorUtils(t *testing.T) { unittest.PrepareTestEnv(t) - - kases := map[string]string{ - ".git/refs/master": "", - "/root/abc": "root/abc", - "./../../abc": "abc", - "a/../.git": "", - "a/../../../abc": "abc", - "../../../acd": "acd", - "../../.git/abc": "", - "..\\..\\.git/abc": "..\\..\\.git/abc", - "..\\../.git/abc": "", - "..\\../.git": "", - "abc/../def": "def", - ".drone.yml": ".drone.yml", - ".abc/def/.drone.yml": ".abc/def/.drone.yml", - "..drone.yml.": "..drone.yml.", - "..a.dotty...name...": "..a.dotty...name...", - "..a.dotty../.folder../.name...": "..a.dotty../.folder../.name...", - } - for k, v := range kases { - assert.Equal(t, cleanUploadFileName(k), v) - } -} - -func TestGetUniquePatchBranchName(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - expectedBranchName := "user2-patch-1" - branchName := GetUniquePatchBranchName(ctx) - assert.Equal(t, expectedBranchName, branchName) -} - -func TestGetClosestParentWithFiles(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - repo := ctx.Repo.Repository - branch := repo.DefaultBranch - gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) - defer gitRepo.Close() - commit, _ := gitRepo.GetBranchCommit(branch) - var expectedTreePath string // Should return the root dir, empty string, since there are no subdirs in this repo - for _, deletedFile := range []string{ - "dir1/dir2/dir3/file.txt", - "file.txt", - } { - treePath := GetClosestParentWithFiles(deletedFile, commit) - assert.Equal(t, expectedTreePath, treePath) - } + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + t.Run("getUniquePatchBranchName", func(t *testing.T) { + branchName := getUniquePatchBranchName(t.Context(), "user2", repo) + assert.Equal(t, "user2-patch-1", branchName) + }) + t.Run("getClosestParentWithFiles", func(t *testing.T) { + gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) + defer gitRepo.Close() + treePath := getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "docs/foo/bar") + assert.Equal(t, "docs", treePath) + treePath = getClosestParentWithFiles(gitRepo, "sub-home-md-img-check", "any/other") + assert.Empty(t, treePath) + }) } diff --git a/routers/web/repo/editor_uploader.go b/routers/web/repo/editor_uploader.go new file mode 100644 index 0000000000..1ce9a1aca4 --- /dev/null +++ b/routers/web/repo/editor_uploader.go @@ -0,0 +1,61 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/context/upload" + files_service "code.gitea.io/gitea/services/repository/files" +) + +// UploadFileToServer upload file to server file dir not git +func UploadFileToServer(ctx *context.Context) { + file, header, err := ctx.Req.FormFile("file") + if err != nil { + ctx.ServerError("FormFile", err) + return + } + defer file.Close() + + buf := make([]byte, 1024) + n, _ := util.ReadAtMost(file, buf) + if n > 0 { + buf = buf[:n] + } + + err = upload.Verify(buf, header.Filename, setting.Repository.Upload.AllowedTypes) + if err != nil { + ctx.HTTPError(http.StatusBadRequest, err.Error()) + return + } + + name := files_service.CleanGitTreePath(header.Filename) + if len(name) == 0 { + ctx.HTTPError(http.StatusBadRequest, "Upload file name is invalid") + return + } + + uploaded, err := repo_model.NewUpload(ctx, name, buf, file) + if err != nil { + ctx.ServerError("NewUpload", err) + return + } + + ctx.JSON(http.StatusOK, map[string]string{"uuid": uploaded.UUID}) +} + +// RemoveUploadFileFromServer remove file from server file dir +func RemoveUploadFileFromServer(ctx *context.Context) { + fileUUID := ctx.FormString("file") + if err := repo_model.DeleteUploadByUUID(ctx, fileUUID); err != nil { + ctx.ServerError("DeleteUploadByUUID", err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/routers/web/repo/editor_util.go b/routers/web/repo/editor_util.go new file mode 100644 index 0000000000..f910f0bd40 --- /dev/null +++ b/routers/web/repo/editor_util.go @@ -0,0 +1,110 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + "fmt" + "path" + "strings" + + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + context_service "code.gitea.io/gitea/services/context" +) + +// getUniquePatchBranchName Gets a unique branch name for a new patch branch +// It will be in the form of <username>-patch-<num> where <num> is the first branch of this format +// that doesn't already exist. If we exceed 1000 tries or an error is thrown, we just return "" so the user has to +// type in the branch name themselves (will be an empty field) +func getUniquePatchBranchName(ctx context.Context, prefixName string, repo *repo_model.Repository) string { + prefix := prefixName + "-patch-" + for i := 1; i <= 1000; i++ { + branchName := fmt.Sprintf("%s%d", prefix, i) + if exist, err := git_model.IsBranchExist(ctx, repo.ID, branchName); err != nil { + log.Error("getUniquePatchBranchName: %v", err) + return "" + } else if !exist { + return branchName + } + } + return "" +} + +// getClosestParentWithFiles Recursively gets the closest path of parent in a tree that has files when a file in a tree is +// deleted. It returns "" for the tree root if no parents other than the root have files. +func getClosestParentWithFiles(gitRepo *git.Repository, branchName, originTreePath string) string { + var f func(treePath string, commit *git.Commit) string + f = func(treePath string, commit *git.Commit) string { + if treePath == "" || treePath == "." { + return "" + } + // see if the tree has entries + if tree, err := commit.SubTree(treePath); err != nil { + return f(path.Dir(treePath), commit) // failed to get the tree, going up a dir + } else if entries, err := tree.ListEntries(); err != nil || len(entries) == 0 { + return f(path.Dir(treePath), commit) // no files in this dir, going up a dir + } + return treePath + } + commit, err := gitRepo.GetBranchCommit(branchName) // must get the commit again to get the latest change + if err != nil { + log.Error("GetBranchCommit: %v", err) + return "" + } + return f(originTreePath, commit) +} + +// getContextRepoEditorConfig returns the editorconfig JSON string for given treePath or "null" +func getContextRepoEditorConfig(ctx *context_service.Context, treePath string) string { + ec, _, err := ctx.Repo.GetEditorconfig() + if err == nil { + def, err := ec.GetDefinitionForFilename(treePath) + if err == nil { + jsonStr, _ := json.Marshal(def) + return string(jsonStr) + } + } + return "null" +} + +// getParentTreeFields returns list of parent tree names and corresponding tree paths based on given treePath. +// eg: []{"a", "b", "c"}, []{"a", "a/b", "a/b/c"} +// or: []{""}, []{""} for the root treePath +func getParentTreeFields(treePath string) (treeNames, treePaths []string) { + treeNames = strings.Split(treePath, "/") + treePaths = make([]string, len(treeNames)) + for i := range treeNames { + treePaths[i] = strings.Join(treeNames[:i+1], "/") + } + return treeNames, treePaths +} + +// getUniqueRepositoryName Gets a unique repository name for a user +// It will append a -<num> postfix if the name is already taken +func getUniqueRepositoryName(ctx context.Context, ownerID int64, name string) string { + uniqueName := name + for i := 1; i < 1000; i++ { + _, err := repo_model.GetRepositoryByName(ctx, ownerID, uniqueName) + if err != nil || repo_model.IsErrRepoNotExist(err) { + return uniqueName + } + uniqueName = fmt.Sprintf("%s-%d", name, i) + i++ + } + return "" +} + +func editorPushBranchToForkedRepository(ctx context.Context, doer *user_model.User, baseRepo *repo_model.Repository, baseBranchName string, targetRepo *repo_model.Repository, targetBranchName string) error { + return git.Push(ctx, baseRepo.RepoPath(), git.PushOptions{ + Remote: targetRepo.RepoPath(), + Branch: baseBranchName + ":" + targetBranchName, + Env: repo_module.PushingEnvironment(doer, targetRepo), + }) +} diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 79f033659b..c2694e540f 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -151,7 +151,7 @@ func ForkPost(ctx *context.Context) { ctx.Data["ContextUser"] = ctxUser if ctx.HasError() { - ctx.HTML(http.StatusOK, tplFork) + ctx.JSONError(ctx.GetErrMsg()) return } @@ -159,12 +159,12 @@ func ForkPost(ctx *context.Context) { traverseParentRepo := forkRepo for { if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) { - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo")) return } repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID) if repo != nil { - ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) return } if !traverseParentRepo.IsFork { @@ -189,44 +189,50 @@ func ForkPost(ctx *context.Context) { } } - repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{ + repo := ForkRepoTo(ctx, ctxUser, repo_service.ForkRepoOptions{ BaseRepo: forkRepo, Name: form.RepoName, Description: form.Description, SingleBranch: form.ForkSingleBranch, }) + if ctx.Written() { + return + } + ctx.JSONRedirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) +} + +func ForkRepoTo(ctx *context.Context, owner *user_model.User, forkOpts repo_service.ForkRepoOptions) *repo_model.Repository { + repo, err := repo_service.ForkRepository(ctx, ctx.Doer, owner, forkOpts) if err != nil { ctx.Data["Err_RepoName"] = true switch { case repo_model.IsErrReachLimitOfRepo(err): - maxCreationLimit := ctxUser.MaxCreationLimit() + maxCreationLimit := owner.MaxCreationLimit() msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit) - ctx.RenderWithErr(msg, tplFork, &form) + ctx.JSONError(msg) case repo_model.IsErrRepoAlreadyExist(err): - ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.settings.new_owner_has_same_repo")) case repo_model.IsErrRepoFilesAlreadyExist(err): switch { case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories): - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt_or_delete")) case setting.Repository.AllowAdoptionOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist.adopt")) case setting.Repository.AllowDeleteOfUnadoptedRepositories: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist.delete")) default: - ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form) + ctx.JSONError(ctx.Tr("form.repository_files_already_exist")) } case db.IsErrNameReserved(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name)) case db.IsErrNamePatternNotAllowed(err): - ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form) + ctx.JSONError(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern)) case errors.Is(err, user_model.ErrBlockedUser): - ctx.RenderWithErr(ctx.Tr("repo.fork.blocked_user"), tplFork, form) + ctx.JSONError(ctx.Tr("repo.fork.blocked_user")) default: ctx.ServerError("ForkPost", err) } - return + return nil } - - log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name) - ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name)) + return repo } diff --git a/routers/web/repo/githttp.go b/routers/web/repo/githttp.go index 61606f8c5f..deb3ae4f3a 100644 --- a/routers/web/repo/githttp.go +++ b/routers/web/repo/githttp.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strconv" "strings" "sync" @@ -363,12 +364,7 @@ func containsParentDirectorySeparator(v string) bool { if !strings.Contains(v, "..") { return false } - for _, ent := range strings.FieldsFunc(v, isSlashRune) { - if ent == ".." { - return true - } - } - return false + return slices.Contains(strings.FieldsFunc(v, isSlashRune), "..") } func isSlashRune(r rune) bool { return r == '/' || r == '\\' } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index e70e8fdd7b..54b7e5df2a 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -212,7 +212,7 @@ func getActionIssues(ctx *context.Context) issues_model.IssueList { return nil } issueIDs := make([]int64, 0, 10) - for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") { + for stringIssueID := range strings.SplitSeq(commaSeparatedIssueIDs, ",") { issueID, err := strconv.ParseInt(stringIssueID, 10, 64) if err != nil { ctx.ServerError("ParseInt", err) @@ -364,7 +364,9 @@ func UpdateIssueContent(ctx *context.Context) { } } - rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) + rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{ + FootnoteContextID: "0", + }) content, err := markdown.RenderString(rctx, issue.Content) if err != nil { ctx.ServerError("RenderString", err) diff --git a/routers/web/repo/issue_comment.go b/routers/web/repo/issue_comment.go index 9b51999fbd..c2a7f6b682 100644 --- a/routers/web/repo/issue_comment.go +++ b/routers/web/repo/issue_comment.go @@ -8,6 +8,7 @@ import ( "fmt" "html/template" "net/http" + "strconv" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/renderhelper" @@ -278,7 +279,9 @@ func UpdateCommentContent(ctx *context.Context) { var renderedContent template.HTML if comment.Content != "" { - rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) + rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{ + FootnoteContextID: strconv.FormatInt(comment.ID, 10), + }) renderedContent, err = markdown.RenderString(rctx, comment.Content) if err != nil { ctx.ServerError("RenderString", err) diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go index f9c41adbcf..72a316e98d 100644 --- a/routers/web/repo/issue_label.go +++ b/routers/web/repo/issue_label.go @@ -4,6 +4,7 @@ package repo import ( + "errors" "net/http" "code.gitea.io/gitea/models/db" @@ -13,7 +14,9 @@ import ( "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/templates" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + shared_label "code.gitea.io/gitea/routers/web/shared/label" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" issue_service "code.gitea.io/gitea/services/issue" @@ -100,13 +103,8 @@ func RetrieveLabelsForList(ctx *context.Context) { // NewLabel create new label for repository func NewLabel(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CreateLabelForm) - ctx.Data["Title"] = ctx.Tr("repo.labels") - ctx.Data["PageIsLabels"] = true - - if ctx.HasError() { - ctx.Flash.Error(ctx.Data["ErrorMsg"].(string)) - ctx.Redirect(ctx.Repo.RepoLink + "/labels") + form := shared_label.GetLabelEditForm(ctx) + if ctx.Written() { return } @@ -122,34 +120,36 @@ func NewLabel(ctx *context.Context) { ctx.ServerError("NewLabel", err) return } - ctx.Redirect(ctx.Repo.RepoLink + "/labels") + ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels") } // UpdateLabel update a label's name and color func UpdateLabel(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.CreateLabelForm) + form := shared_label.GetLabelEditForm(ctx) + if ctx.Written() { + return + } + l, err := issues_model.GetLabelInRepoByID(ctx, ctx.Repo.Repository.ID, form.ID) - if err != nil { - switch { - case issues_model.IsErrRepoLabelNotExist(err): - ctx.HTTPError(http.StatusNotFound) - default: - ctx.ServerError("UpdateLabel", err) - } + if errors.Is(err, util.ErrNotExist) { + ctx.JSONErrorNotFound() + return + } else if err != nil { + ctx.ServerError("GetLabelInRepoByID", err) return } + l.Name = form.Title l.Exclusive = form.Exclusive l.ExclusiveOrder = form.ExclusiveOrder l.Description = form.Description l.Color = form.Color - l.SetArchived(form.IsArchived) if err := issues_model.UpdateLabel(ctx, l); err != nil { ctx.ServerError("UpdateLabel", err) return } - ctx.Redirect(ctx.Repo.RepoLink + "/labels") + ctx.JSONRedirect(ctx.Repo.RepoLink + "/labels") } // DeleteLabel delete a label diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index c3fba07034..f4eca26f8e 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -6,10 +6,12 @@ package repo import ( "net/http" "strconv" + "strings" "testing" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/web" @@ -19,19 +21,20 @@ import ( "github.com/stretchr/testify/assert" ) -func int64SliceToCommaSeparated(a []int64) string { - s := "" - for i, n := range a { - if i > 0 { - s += "," - } - s += strconv.Itoa(int(n)) - } - return s +func TestIssueLabel(t *testing.T) { + unittest.PrepareTestEnv(t) + t.Run("RetrieveLabels", testRetrieveLabels) + t.Run("NewLabel", testNewLabel) + t.Run("NewLabelInvalidColor", testNewLabelInvalidColor) + t.Run("UpdateLabel", testUpdateLabel) + t.Run("UpdateLabelInvalidColor", testUpdateLabelInvalidColor) + t.Run("UpdateIssueLabelClear", testUpdateIssueLabelClear) + t.Run("UpdateIssueLabelToggle", testUpdateIssueLabelToggle) + t.Run("InitializeLabels", testInitializeLabels) + t.Run("DeleteLabel", testDeleteLabel) } -func TestInitializeLabels(t *testing.T) { - unittest.PrepareTestEnv(t) +func testInitializeLabels(t *testing.T) { assert.NoError(t, repository.LoadRepoConfig()) ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/initialize") contexttest.LoadUser(t, ctx, 2) @@ -47,8 +50,7 @@ func TestInitializeLabels(t *testing.T) { assert.Equal(t, "/user2/repo2/labels", test.RedirectURL(ctx.Resp)) } -func TestRetrieveLabels(t *testing.T) { - unittest.PrepareTestEnv(t) +func testRetrieveLabels(t *testing.T) { for _, testCase := range []struct { RepoID int64 Sort string @@ -74,9 +76,8 @@ func TestRetrieveLabels(t *testing.T) { } } -func TestNewLabel(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/edit") +func testNewLabel(t *testing.T) { + ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.CreateLabelForm{ @@ -84,17 +85,32 @@ func TestNewLabel(t *testing.T) { Color: "#abcdef", }) NewLabel(ctx) - assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) + assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ Name: "newlabel", Color: "#abcdef", }) - assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp)) + assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(respWriter)) } -func TestUpdateLabel(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/edit") +func testNewLabelInvalidColor(t *testing.T) { + ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit") + contexttest.LoadUser(t, ctx, 2) + contexttest.LoadRepo(t, ctx, 1) + web.SetForm(ctx, &forms.CreateLabelForm{ + Title: "newlabel-x", + Color: "bad-label-code", + }) + NewLabel(ctx) + assert.Equal(t, http.StatusBadRequest, ctx.Resp.WrittenStatus()) + assert.Equal(t, "repo.issues.label_color_invalid", test.ParseJSONError(respWriter.Body.Bytes()).ErrorMessage) + unittest.AssertNotExistsBean(t, &issues_model.Label{ + Name: "newlabel-x", + }) +} + +func testUpdateLabel(t *testing.T) { + ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) web.SetForm(ctx, &forms.CreateLabelForm{ @@ -104,17 +120,37 @@ func TestUpdateLabel(t *testing.T) { IsArchived: true, }) UpdateLabel(ctx) - assert.Equal(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) + assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ ID: 2, Name: "newnameforlabel", Color: "#abcdef", }) - assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(ctx.Resp)) + assert.Equal(t, "/user2/repo1/labels", test.RedirectURL(respWriter)) } -func TestDeleteLabel(t *testing.T) { - unittest.PrepareTestEnv(t) +func testUpdateLabelInvalidColor(t *testing.T) { + ctx, respWriter := contexttest.MockContext(t, "user2/repo1/labels/edit") + contexttest.LoadUser(t, ctx, 2) + contexttest.LoadRepo(t, ctx, 1) + web.SetForm(ctx, &forms.CreateLabelForm{ + ID: 1, + Title: "label1", + Color: "bad-label-code", + }) + + UpdateLabel(ctx) + + assert.Equal(t, http.StatusBadRequest, ctx.Resp.WrittenStatus()) + assert.Equal(t, "repo.issues.label_color_invalid", test.ParseJSONError(respWriter.Body.Bytes()).ErrorMessage) + unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ + ID: 1, + Name: "label1", + Color: "#abcdef", + }) +} + +func testDeleteLabel(t *testing.T) { ctx, _ := contexttest.MockContext(t, "user2/repo1/labels/delete") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) @@ -126,8 +162,7 @@ func TestDeleteLabel(t *testing.T) { assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg) } -func TestUpdateIssueLabel_Clear(t *testing.T) { - unittest.PrepareTestEnv(t) +func testUpdateIssueLabelClear(t *testing.T) { ctx, _ := contexttest.MockContext(t, "user2/repo1/issues/labels") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) @@ -140,7 +175,7 @@ func TestUpdateIssueLabel_Clear(t *testing.T) { unittest.CheckConsistencyFor(t, &issues_model.Label{}) } -func TestUpdateIssueLabel_Toggle(t *testing.T) { +func testUpdateIssueLabelToggle(t *testing.T) { for _, testCase := range []struct { Action string IssueIDs []int64 @@ -156,7 +191,8 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) { ctx, _ := contexttest.MockContext(t, "user2/repo1/issues/labels") contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) - ctx.Req.Form.Set("issue_ids", int64SliceToCommaSeparated(testCase.IssueIDs)) + + ctx.Req.Form.Set("issue_ids", strings.Join(base.Int64sToStrings(testCase.IssueIDs), ",")) ctx.Req.Form.Set("action", testCase.Action) ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID))) UpdateIssueLabel(ctx) diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 35107bc585..b55f4bcc90 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -61,7 +61,7 @@ func SearchIssues(ctx *context.Context) { ) { // find repos user can access (for issue search) - opts := &repo_model.SearchRepoOptions{ + opts := repo_model.SearchRepoOptions{ Private: false, AllPublic: true, TopicOnly: false, diff --git a/routers/web/repo/issue_new.go b/routers/web/repo/issue_new.go index d8863961ff..887019b146 100644 --- a/routers/web/repo/issue_new.go +++ b/routers/web/repo/issue_new.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "html/template" + "maps" "net/http" "slices" "sort" @@ -136,9 +137,7 @@ func NewIssue(ctx *context.Context) { ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo) templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, pageMetaData) - for k, v := range errs { - ret.TemplateErrors[k] = v - } + maps.Copy(ret.TemplateErrors, errs) if ctx.Written() { return } diff --git a/routers/web/repo/issue_stopwatch.go b/routers/web/repo/issue_stopwatch.go index 5a8d203771..2de3a7cfec 100644 --- a/routers/web/repo/issue_stopwatch.go +++ b/routers/web/repo/issue_stopwatch.go @@ -10,33 +10,47 @@ import ( "code.gitea.io/gitea/services/context" ) -// IssueStopwatch creates or stops a stopwatch for the given issue. -func IssueStopwatch(c *context.Context) { +// IssueStartStopwatch creates a stopwatch for the given issue. +func IssueStartStopwatch(c *context.Context) { issue := GetActionIssue(c) if c.Written() { return } - var showSuccessMessage bool - - if !issues_model.StopwatchExists(c, c.Doer.ID, issue.ID) { - showSuccessMessage = true - } - if !c.Repo.CanUseTimetracker(c, issue, c.Doer) { c.NotFound(nil) return } - if err := issues_model.CreateOrStopIssueStopwatch(c, c.Doer, issue); err != nil { - c.ServerError("CreateOrStopIssueStopwatch", err) + if ok, err := issues_model.CreateIssueStopwatch(c, c.Doer, issue); err != nil { + c.ServerError("CreateIssueStopwatch", err) return + } else if !ok { + c.Flash.Warning(c.Tr("repo.issues.stopwatch_already_created")) + } else { + c.Flash.Success(c.Tr("repo.issues.tracker_auto_close")) } + c.JSONRedirect("") +} - if showSuccessMessage { - c.Flash.Success(c.Tr("repo.issues.tracker_auto_close")) +// IssueStopStopwatch stops a stopwatch for the given issue. +func IssueStopStopwatch(c *context.Context) { + issue := GetActionIssue(c) + if c.Written() { + return + } + + if !c.Repo.CanUseTimetracker(c, issue, c.Doer) { + c.NotFound(nil) + return } + if ok, err := issues_model.FinishIssueStopwatch(c, c.Doer, issue); err != nil { + c.ServerError("FinishIssueStopwatch", err) + return + } else if !ok { + c.Flash.Warning(c.Tr("repo.issues.stopwatch_already_stopped")) + } c.JSONRedirect("") } @@ -51,7 +65,7 @@ func CancelStopwatch(c *context.Context) { return } - if err := issues_model.CancelStopwatch(c, c.Doer, issue); err != nil { + if _, err := issues_model.CancelStopwatch(c, c.Doer, issue); err != nil { c.ServerError("CancelStopwatch", err) return } diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 13b9d83da4..d4458ed19e 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "sort" + "strconv" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" @@ -442,6 +443,10 @@ func ViewPullMergeBox(ctx *context.Context) { preparePullViewPullInfo(ctx, issue) preparePullViewReviewAndMerge(ctx, issue) ctx.Data["PullMergeBoxReloading"] = issue.PullRequest.IsChecking() + + // TODO: it should use a dedicated struct to render the pull merge box, to make sure all data is prepared correctly + ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) + ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) ctx.HTML(http.StatusOK, tplPullMergeBox) } @@ -624,7 +629,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue comment.Issue = issue if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview { - rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo) + rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{ + FootnoteContextID: strconv.FormatInt(comment.ID, 10), + }) comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content) if err != nil { ctx.ServerError("RenderString", err) @@ -700,7 +707,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue } } } else if comment.Type.HasContentSupport() { - rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo) + rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{ + FootnoteContextID: strconv.FormatInt(comment.ID, 10), + }) comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content) if err != nil { ctx.ServerError("RenderString", err) @@ -757,6 +766,9 @@ func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue } if !ctx.Repo.CanRead(unit.TypeActions) { for _, commit := range comment.Commits { + if commit.Status == nil { + continue + } commit.Status.HideActionsURL(ctx) git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses) } @@ -981,7 +993,9 @@ func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Iss func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { var err error - rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) + rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{ + FootnoteContextID: "0", // Set footnote context ID to 0 for the issue content + }) issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content) if err != nil { ctx.ServerError("RenderString", err) diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 8a26a0dcc3..dd53b1d3f1 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -38,10 +38,7 @@ func Milestones(ctx *context.Context) { isShowClosed := ctx.FormString("state") == "closed" sortType := ctx.FormString("sort") keyword := ctx.FormTrim("q") - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) miles, total, err := db.FindAndCount[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ ListOptions: db.ListOptions{ diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 65a340a799..d09a57c03f 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -21,10 +21,7 @@ const ( // Packages displays a list of all packages in the repository func Packages(ctx *context.Context) { - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) query := ctx.FormTrim("q") packageType := ctx.FormTrim("type") diff --git a/routers/web/repo/patch.go b/routers/web/repo/patch.go deleted file mode 100644 index ca346b7e6c..0000000000 --- a/routers/web/repo/patch.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package repo - -import ( - "net/http" - "strings" - - git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/web" - "code.gitea.io/gitea/services/context" - "code.gitea.io/gitea/services/forms" - "code.gitea.io/gitea/services/repository/files" -) - -const ( - tplPatchFile templates.TplName = "repo/editor/patch" -) - -// NewDiffPatch render create patch page -func NewDiffPatch(ctx *context.Context) { - canCommit := renderCommitRights(ctx) - - ctx.Data["PageIsPatch"] = true - - ctx.Data["commit_summary"] = "" - ctx.Data["commit_message"] = "" - if canCommit { - ctx.Data["commit_choice"] = frmCommitChoiceDirect - } else { - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - } - ctx.Data["new_branch_name"] = GetUniquePatchBranchName(ctx) - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - - ctx.HTML(http.StatusOK, tplPatchFile) -} - -// NewDiffPatchPost response for sending patch page -func NewDiffPatchPost(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.EditRepoFileForm) - - canCommit := renderCommitRights(ctx) - branchName := ctx.Repo.BranchName - if form.CommitChoice == frmCommitChoiceNewBranch { - branchName = form.NewBranchName - } - ctx.Data["PageIsPatch"] = true - ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() - ctx.Data["FileContent"] = form.Content - ctx.Data["commit_summary"] = form.CommitSummary - ctx.Data["commit_message"] = form.CommitMessage - ctx.Data["commit_choice"] = form.CommitChoice - ctx.Data["new_branch_name"] = form.NewBranchName - ctx.Data["last_commit"] = ctx.Repo.CommitID - ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") - - if ctx.HasError() { - ctx.HTML(http.StatusOK, tplPatchFile) - return - } - - // Cannot commit to an existing branch if user doesn't have rights - if branchName == ctx.Repo.BranchName && !canCommit { - ctx.Data["Err_NewBranchName"] = true - ctx.Data["commit_choice"] = frmCommitChoiceNewBranch - ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form) - return - } - - // CommitSummary is optional in the web form, if empty, give it a default message based on add or update - // `message` will be both the summary and message combined - message := strings.TrimSpace(form.CommitSummary) - if len(message) == 0 { - message = ctx.Locale.TrString("repo.editor.patch") - } - - form.CommitMessage = strings.TrimSpace(form.CommitMessage) - if len(form.CommitMessage) > 0 { - message += "\n\n" + form.CommitMessage - } - - gitCommitter, valid := WebGitOperationGetCommitChosenEmailIdentity(ctx, form.CommitEmail) - if !valid { - ctx.Data["Err_CommitEmail"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_email"), tplPatchFile, &form) - return - } - - fileResponse, err := files.ApplyDiffPatch(ctx, ctx.Repo.Repository, ctx.Doer, &files.ApplyDiffPatchOptions{ - LastCommitID: form.LastCommit, - OldBranch: ctx.Repo.BranchName, - NewBranch: branchName, - Message: message, - Content: strings.ReplaceAll(form.Content, "\r", ""), - Author: gitCommitter, - Committer: gitCommitter, - }) - if err != nil { - if git_model.IsErrBranchAlreadyExists(err) { - // User has specified a branch that already exists - branchErr := err.(git_model.ErrBranchAlreadyExists) - ctx.Data["Err_NewBranchName"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form) - return - } else if files.IsErrCommitIDDoesNotMatch(err) { - ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplPatchFile, &form) - return - } - ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_apply_patch", err), tplPatchFile, &form) - return - } - - if form.CommitChoice == frmCommitChoiceNewBranch && ctx.Repo.Repository.UnitEnabled(ctx, unit.TypePullRequests) { - ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ctx.Repo.BranchName) + "..." + util.PathEscapeSegments(form.NewBranchName)) - } else { - ctx.Redirect(ctx.Repo.RepoLink + "/commit/" + fileResponse.Commit.SHA) - } -} diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 0bf1f64d09..a57976b4ca 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -61,10 +61,7 @@ func Projects(ctx *context.Context) { isShowClosed := strings.ToLower(ctx.FormTrim("state")) == "closed" keyword := ctx.FormTrim("q") repo := ctx.Repo.Repository - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) ctx.Data["OpenCount"] = repo.NumOpenProjects ctx.Data["ClosedCount"] = repo.NumClosedProjects diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 43ddc265cf..f662152e2e 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -27,6 +27,7 @@ import ( "code.gitea.io/gitea/modules/fileicon" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/graceful" issue_template "code.gitea.io/gitea/modules/issue/template" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -291,7 +292,7 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) if len(compareInfo.Commits) != 0 { sha := compareInfo.Commits[0].ID.String() - commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll) + commitStatuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptionsAll) if err != nil { ctx.ServerError("GetLatestCommitStatus", err) return nil @@ -358,7 +359,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err) return nil } - commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll) + commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll) if err != nil { ctx.ServerError("GetLatestCommitStatus", err) return nil @@ -454,7 +455,7 @@ func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C return nil } - commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll) + commitStatuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll) if err != nil { ctx.ServerError("GetLatestCommitStatus", err) return nil @@ -986,7 +987,9 @@ func UpdatePullRequest(ctx *context.Context) { // default merge commit message message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch) - if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil { + // The update process should not be cancelled by the user + // so we set the context to be a background context + if err = pull_service.Update(graceful.GetManager().ShutdownContext(), issue.PullRequest, ctx.Doer, message, rebase); err != nil { if pull_service.IsErrMergeConflicts(err) { conflictError := err.(pull_service.ErrMergeConflicts) flashError, err := ctx.RenderToHTML(tplAlertDetails, map[string]any{ @@ -1255,13 +1258,8 @@ func CancelAutoMergePullRequest(ctx *context.Context) { } func stopTimerIfAvailable(ctx *context.Context, user *user_model.User, issue *issues_model.Issue) error { - if issues_model.StopwatchExists(ctx, user.ID, issue.ID) { - if err := issues_model.CreateOrStopIssueStopwatch(ctx, user, issue); err != nil { - return err - } - } - - return nil + _, err := issues_model.FinishIssueStopwatch(ctx, user, issue) + return err } func PullsNewRedirect(ctx *context.Context) { diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 553bdbf6e5..36ea20c23e 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "strconv" "strings" "code.gitea.io/gitea/models/db" @@ -102,7 +103,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) releaseInfos := make([]*ReleaseInfo, 0, len(releases)) for _, r := range releases { if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok { - r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) + r.Publisher, err = user_model.GetPossibleUserByID(ctx, r.PublisherID) if err != nil { if user_model.IsErrUserNotExist(err) { r.Publisher = user_model.NewGhostUser() @@ -113,7 +114,9 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) cacheUsers[r.PublisherID] = r.Publisher } - rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo) + rctx := renderhelper.NewRenderContextRepoComment(ctx, r.Repo, renderhelper.RepoCommentOptions{ + FootnoteContextID: strconv.FormatInt(r.ID, 10), + }) r.RenderedNote, err = markdown.RenderString(rctx, r.Note) if err != nil { return nil, err @@ -130,7 +133,7 @@ func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) } if canReadActions { - statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll) + statuses, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptionsAll) if err != nil { return nil, err } @@ -378,7 +381,7 @@ func NewRelease(ctx *context.Context) { ctx.Data["ShowCreateTagOnlyButton"] = false ctx.Data["tag_name"] = rel.TagName - ctx.Data["tag_target"] = rel.Target + ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch) ctx.Data["title"] = rel.Title ctx.Data["content"] = rel.Note ctx.Data["attachments"] = rel.Attachments @@ -534,7 +537,7 @@ func EditRelease(ctx *context.Context) { } ctx.Data["ID"] = rel.ID ctx.Data["tag_name"] = rel.TagName - ctx.Data["tag_target"] = rel.Target + ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch) ctx.Data["title"] = rel.Title ctx.Data["content"] = rel.Note ctx.Data["prerelease"] = rel.IsPrerelease @@ -580,7 +583,7 @@ func EditReleasePost(ctx *context.Context) { return } ctx.Data["tag_name"] = rel.TagName - ctx.Data["tag_target"] = rel.Target + ctx.Data["tag_target"] = util.IfZero(rel.Target, ctx.Repo.Repository.DefaultBranch) ctx.Data["title"] = rel.Title ctx.Data["content"] = rel.Note ctx.Data["prerelease"] = rel.IsPrerelease diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index ee112b83f2..828ec08a8a 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -88,7 +88,7 @@ func checkContextUser(ctx *context.Context, uid int64) *user_model.User { } var orgsAvailable []*organization.Organization - for i := 0; i < len(orgs); i++ { + for i := range orgs { if ctx.Doer.CanCreateRepoIn(orgs[i].AsUser()) { orgsAvailable = append(orgsAvailable, orgs[i]) } @@ -461,7 +461,7 @@ func SearchRepo(ctx *context.Context) { if page <= 0 { page = 1 } - opts := &repo_model.SearchRepoOptions{ + opts := repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ Page: page, PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")), diff --git a/routers/web/repo/setting/lfs.go b/routers/web/repo/setting/lfs.go index a065620b2b..af6708e841 100644 --- a/routers/web/repo/setting/lfs.go +++ b/routers/web/repo/setting/lfs.go @@ -45,10 +45,7 @@ func LFSFiles(ctx *context.Context) { ctx.NotFound(nil) return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) total, err := git_model.CountLFSMetaObjects(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("LFSFiles", err) @@ -77,10 +74,7 @@ func LFSLocks(ctx *context.Context) { } ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs" - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) total, err := git_model.CountLFSLockByRepoID(ctx, ctx.Repo.Repository.ID) if err != nil { ctx.ServerError("LFSLocks", err) @@ -273,8 +267,10 @@ func LFSFileGet(ctx *context.Context) { buf = buf[:n] st := typesniffer.DetectContentType(buf) + // FIXME: there is no IsPlainText set, but template uses it ctx.Data["IsTextFile"] = st.IsText() ctx.Data["FileSize"] = meta.Size + // FIXME: the last field is the URL-base64-encoded filename, it should not be "direct" ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s/%s.git/info/lfs/objects/%s/%s", setting.AppURL, url.PathEscape(ctx.Repo.Repository.OwnerName), url.PathEscape(ctx.Repo.Repository.Name), url.PathEscape(meta.Oid), "direct") switch { case st.IsRepresentableAsText(): @@ -315,8 +311,6 @@ func LFSFileGet(ctx *context.Context) { } ctx.Data["LineNums"] = gotemplate.HTML(output.String()) - case st.IsPDF(): - ctx.Data["IsPDFFile"] = true case st.IsVideo(): ctx.Data["IsVideoFile"] = true case st.IsAudio(): diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index f241242f02..0eea5e3f34 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" @@ -89,7 +90,7 @@ func SettingsProtectedBranch(c *context.Context) { c.Data["recent_status_checks"] = contexts if c.Repo.Owner.IsOrganization() { - teams, err := organization.OrgFromUser(c.Repo.Owner).TeamsWithAccessToRepo(c, c.Repo.Repository.ID, perm.AccessModeRead) + teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(c, c.Repo.Owner.ID, c.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests) if err != nil { c.ServerError("Repo.Owner.TeamsWithAccessToRepo", err) return diff --git a/routers/web/repo/setting/protected_tag.go b/routers/web/repo/setting/protected_tag.go index 33692778d5..50f5a28c4c 100644 --- a/routers/web/repo/setting/protected_tag.go +++ b/routers/web/repo/setting/protected_tag.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" @@ -156,7 +157,7 @@ func setTagsContext(ctx *context.Context) error { ctx.Data["Users"] = users if ctx.Repo.Owner.IsOrganization() { - teams, err := organization.OrgFromUser(ctx.Repo.Owner).TeamsWithAccessToRepo(ctx, ctx.Repo.Repository.ID, perm.AccessModeRead) + teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, ctx.Repo.Owner.ID, ctx.Repo.Repository.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests) if err != nil { ctx.ServerError("Repo.Owner.TeamsWithAccessToRepo", err) return err diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 5552a8726c..6e16ead183 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -62,7 +62,7 @@ func SettingsCtxData(ctx *context.Context) { ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner) signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath()) - ctx.Data["SigningKeyAvailable"] = len(signing) > 0 + ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled @@ -105,7 +105,7 @@ func SettingsPost(ctx *context.Context) { ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath()) - ctx.Data["SigningKeyAvailable"] = len(signing) > 0 + ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled @@ -663,44 +663,37 @@ func handleSettingsPostAdvanced(ctx *context.Context) { func handleSettingsPostSigning(ctx *context.Context) { form := web.GetForm(ctx).(*forms.RepoSettingForm) repo := ctx.Repo.Repository - changed := false trustModel := repo_model.ToTrustModel(form.TrustModel) if trustModel != repo.TrustModel { repo.TrustModel = trustModel - changed = true - } - - if changed { - if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { - ctx.ServerError("UpdateRepository", err) + if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "trust_model"); err != nil { + ctx.ServerError("UpdateRepositoryColsNoAutoTime", err) return } + log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) } - log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings") } func handleSettingsPostAdmin(ctx *context.Context) { - form := web.GetForm(ctx).(*forms.RepoSettingForm) - repo := ctx.Repo.Repository if !ctx.Doer.IsAdmin { ctx.HTTPError(http.StatusForbidden) return } + repo := ctx.Repo.Repository + form := web.GetForm(ctx).(*forms.RepoSettingForm) if repo.IsFsckEnabled != form.EnableHealthCheck { repo.IsFsckEnabled = form.EnableHealthCheck + if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_fsck_enabled"); err != nil { + ctx.ServerError("UpdateRepositoryColsNoAutoTime", err) + return + } + log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) } - if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { - ctx.ServerError("UpdateRepository", err) - return - } - - log.Trace("Repository admin settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) - ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings") } diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go index ba6b0d1efc..15ebea888c 100644 --- a/routers/web/repo/setting/settings_test.go +++ b/routers/web/repo/setting/settings_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/contexttest" @@ -24,23 +25,8 @@ import ( "github.com/stretchr/testify/assert" ) -func createSSHAuthorizedKeysTmpPath(t *testing.T) func() { - tmpDir := t.TempDir() - - oldPath := setting.SSH.RootPath - setting.SSH.RootPath = tmpDir - - return func() { - setting.SSH.RootPath = oldPath - } -} - func TestAddReadOnlyDeployKey(t *testing.T) { - if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil { - defer deferable() - } else { - return - } + defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())() unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1/settings/keys") @@ -64,11 +50,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) { } func TestAddReadWriteOnlyDeployKey(t *testing.T) { - if deferable := createSSHAuthorizedKeysTmpPath(t); deferable != nil { - defer deferable() - } else { - return - } + defer test.MockVariableValue(&setting.SSH.RootPath, t.TempDir())() unittest.PrepareTestEnv(t) diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index d3151a86a2..006abafe57 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -185,6 +185,7 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent { webhook_module.HookEventRepository: form.Repository, webhook_module.HookEventPackage: form.Package, webhook_module.HookEventStatus: form.Status, + webhook_module.HookEventWorkflowRun: form.WorkflowRun, webhook_module.HookEventWorkflowJob: form.WorkflowJob, }, BranchFilter: form.BranchFilter, diff --git a/routers/web/repo/treelist.go b/routers/web/repo/treelist.go index 0248a0627b..7d7f5a1473 100644 --- a/routers/web/repo/treelist.go +++ b/routers/web/repo/treelist.go @@ -6,6 +6,7 @@ package repo import ( "html/template" "net/http" + "path" "strings" pull_model "code.gitea.io/gitea/models/pull" @@ -111,7 +112,7 @@ func transformDiffTreeForWeb(renderedIconPool *fileicon.RenderedIconPool, diffTr item := &WebDiffFileItem{FullName: file.HeadPath, DiffStatus: file.Status} item.IsViewed = filesViewedState[item.FullName] == pull_model.Viewed item.NameHash = git.HashFilePathForWebUI(item.FullName) - item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{FullName: file.HeadPath, EntryMode: file.HeadMode}) + item.FileIcon = fileicon.RenderEntryIconHTML(renderedIconPool, &fileicon.EntryInfo{BaseName: path.Base(file.HeadPath), EntryMode: file.HeadMode}) switch file.HeadMode { case git.EntryModeTree: diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 2f01434684..773919c054 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -12,6 +12,7 @@ import ( "io" "net/http" "net/url" + "path" "strings" "time" @@ -59,60 +60,63 @@ const ( ) type fileInfo struct { - isTextFile bool - isLFSFile bool - fileSize int64 - lfsMeta *lfs.Pointer - st typesniffer.SniffedType + fileSize int64 + lfsMeta *lfs.Pointer + st typesniffer.SniffedType } -func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, io.ReadCloser, *fileInfo, error) { - dataRc, err := blob.DataAsync() +func (fi *fileInfo) isLFSFile() bool { + return fi.lfsMeta != nil && fi.lfsMeta.Oid != "" +} + +func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) (buf []byte, dataRc io.ReadCloser, fi *fileInfo, err error) { + dataRc, err = blob.DataAsync() if err != nil { return nil, nil, nil, err } - buf := make([]byte, 1024) + const prefetchSize = lfs.MetaFileMaxSize + + buf = make([]byte, prefetchSize) n, _ := util.ReadAtMost(dataRc, buf) buf = buf[:n] - st := typesniffer.DetectContentType(buf) - isTextFile := st.IsText() + fi = &fileInfo{fileSize: blob.Size(), st: typesniffer.DetectContentType(buf)} // FIXME: what happens when README file is an image? - if !isTextFile || !setting.LFS.StartServer { - return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil + if !fi.st.IsText() || !setting.LFS.StartServer { + return buf, dataRc, fi, nil } pointer, _ := lfs.ReadPointerFromBuffer(buf) - if !pointer.IsValid() { // fallback to plain file - return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil + if !pointer.IsValid() { // fallback to a plain file + return buf, dataRc, fi, nil } meta, err := git_model.GetLFSMetaObjectByOid(ctx, repoID, pointer.Oid) - if err != nil { // fallback to plain file + if err != nil { // fallback to a plain file log.Warn("Unable to access LFS pointer %s in repo %d: %v", pointer.Oid, repoID, err) - return buf, dataRc, &fileInfo{isTextFile, false, blob.Size(), nil, st}, nil + return buf, dataRc, fi, nil } - dataRc.Close() - + // close the old dataRc and open the real LFS target + _ = dataRc.Close() dataRc, err = lfs.ReadMetaObject(pointer) if err != nil { return nil, nil, nil, err } - buf = make([]byte, 1024) + buf = make([]byte, prefetchSize) n, err = util.ReadAtMost(dataRc, buf) if err != nil { - dataRc.Close() - return nil, nil, nil, err + _ = dataRc.Close() + return nil, nil, fi, err } buf = buf[:n] - - st = typesniffer.DetectContentType(buf) - - return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil + fi.st = typesniffer.DetectContentType(buf) + fi.fileSize = blob.Size() + fi.lfsMeta = &meta.Pointer + return buf, dataRc, fi, nil } func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { @@ -131,7 +135,7 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { ctx.Data["LatestCommitVerification"] = verification ctx.Data["LatestCommitUser"] = user_model.ValidateCommitWithEmail(ctx, latestCommit) - statuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll) + statuses, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, latestCommit.ID.String(), db.ListOptionsAll) if err != nil { log.Error("GetLatestCommitStatus: %v", err) } @@ -257,7 +261,9 @@ func prepareDirectoryFileIcons(ctx *context.Context, files []git.CommitInfo) { renderedIconPool := fileicon.NewRenderedIconPool() fileIcons := map[string]template.HTML{} for _, f := range files { - fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFromGitTreeEntry(f.Entry)) + fullPath := path.Join(ctx.Repo.TreePath, f.Entry.Name()) + entryInfo := fileicon.EntryInfoFromGitTreeEntry(ctx.Repo.Commit, fullPath, f.Entry) + fileIcons[f.Entry.Name()] = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo) } fileIcons[".."] = fileicon.RenderEntryIconHTML(renderedIconPool, fileicon.EntryInfoFolder()) ctx.Data["FileIcons"] = fileIcons @@ -394,9 +400,10 @@ func Forks(ctx *context.Context) { } pager := context.NewPagination(int(total), pageSize, page, 5) + ctx.Data["ShowRepoOwnerAvatar"] = true + ctx.Data["ShowRepoOwnerOnList"] = true ctx.Data["Page"] = pager - - ctx.Data["Forks"] = forks + ctx.Data["Repos"] = forks ctx.HTML(http.StatusOK, tplForks) } diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go index 3df6051975..2d5bddd939 100644 --- a/routers/web/repo/view_file.go +++ b/routers/web/repo/view_file.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" issue_service "code.gitea.io/gitea/services/issue" @@ -40,7 +41,128 @@ func prepareLatestCommitInfo(ctx *context.Context) bool { return loadLatestCommitData(ctx, commit) } -func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { +func prepareFileViewLfsAttrs(ctx *context.Context) (*attribute.Attributes, bool) { + attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{ + Filenames: []string{ctx.Repo.TreePath}, + Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage}, + }) + if err != nil { + ctx.ServerError("attribute.CheckAttributes", err) + return nil, false + } + attrs := attrsMap[ctx.Repo.TreePath] + if attrs == nil { + // this case shouldn't happen, just in case. + setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath) + attrs = attribute.NewAttributes() + } + ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value() + return attrs, true +} + +func handleFileViewRenderMarkup(ctx *context.Context, filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte, utf8Reader io.Reader) bool { + markupType := markup.DetectMarkupTypeByFileName(filename) + if markupType == "" { + markupType = markup.DetectRendererType(filename, sniffedType, prefetchBuf) + } + if markupType == "" { + return false + } + + ctx.Data["HasSourceRenderedToggle"] = true + + if ctx.FormString("display") == "source" { + return false + } + + ctx.Data["MarkupType"] = markupType + metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx) + metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL() + rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ + CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), + CurrentTreePath: path.Dir(ctx.Repo.TreePath), + }). + WithMarkupType(markupType). + WithRelativePath(ctx.Repo.TreePath). + WithMetas(metas) + + var err error + ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, utf8Reader) + if err != nil { + ctx.ServerError("Render", err) + return true + } + // to prevent iframe from loading third-party url + ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") + return true +} + +func handleFileViewRenderSource(ctx *context.Context, filename string, attrs *attribute.Attributes, fInfo *fileInfo, utf8Reader io.Reader) bool { + if ctx.FormString("display") == "rendered" || !fInfo.st.IsRepresentableAsText() { + return false + } + + if !fInfo.st.IsText() { + if ctx.FormString("display") == "" { + // not text but representable as text, e.g. SVG + // since there is no "display" is specified, let other renders to handle + return false + } + ctx.Data["HasSourceRenderedToggle"] = true + } + + buf, _ := io.ReadAll(utf8Reader) + // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html + // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line; + // Gitea uses the definition (like most modern editors): + // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines; + // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL. + // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines. + // This NumLines is only used for the display on the UI: "xxx lines" + if len(buf) == 0 { + ctx.Data["NumLines"] = 0 + } else { + ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1 + } + + language := attrs.GetLanguage().Value() + fileContent, lexerName, err := highlight.File(filename, language, buf) + ctx.Data["LexerName"] = lexerName + if err != nil { + log.Error("highlight.File failed, fallback to plain text: %v", err) + fileContent = highlight.PlainText(buf) + } + status := &charset.EscapeStatus{} + statuses := make([]*charset.EscapeStatus, len(fileContent)) + for i, line := range fileContent { + statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale) + status = status.Or(statuses[i]) + } + ctx.Data["EscapeStatus"] = status + ctx.Data["FileContent"] = fileContent + ctx.Data["LineEscapeStatus"] = statuses + return true +} + +func handleFileViewRenderImage(ctx *context.Context, fInfo *fileInfo, prefetchBuf []byte) bool { + if !fInfo.st.IsImage() { + return false + } + if fInfo.st.IsSvgImage() && !setting.UI.SVG.Enabled { + return false + } + if fInfo.st.IsSvgImage() { + ctx.Data["HasSourceRenderedToggle"] = true + } else { + img, _, err := image.DecodeConfig(bytes.NewReader(prefetchBuf)) + if err == nil { // ignore the error for the formats that are not supported by image.DecodeConfig + ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height) + } + } + return true +} + +func prepareFileView(ctx *context.Context, entry *git.TreeEntry) { ctx.Data["IsViewFile"] = true ctx.Data["HideRepoInfo"] = true @@ -86,11 +208,8 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } } - isDisplayingSource := ctx.FormString("display") == "source" - isDisplayingRendered := !isDisplayingSource - // Don't call any other repository functions depends on git.Repository until the dataRc closed to - // avoid create unnecessary temporary cat file. + // avoid creating an unnecessary temporary cat file. buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) if err != nil { ctx.ServerError("getFileReader", err) @@ -98,226 +217,94 @@ func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { } defer dataRc.Close() - if fInfo.isLFSFile { + if fInfo.isLFSFile() { ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) } - isRepresentableAsText := fInfo.st.IsRepresentableAsText() - if !isRepresentableAsText { - // If we can't show plain text, always try to render. - isDisplayingSource = false - isDisplayingRendered = true + if !prepareFileViewEditorButtons(ctx) { + return } - ctx.Data["IsLFSFile"] = fInfo.isLFSFile + + ctx.Data["IsLFSFile"] = fInfo.isLFSFile() ctx.Data["FileSize"] = fInfo.fileSize - ctx.Data["IsTextFile"] = fInfo.isTextFile - ctx.Data["IsRepresentableAsText"] = isRepresentableAsText - ctx.Data["IsDisplayingSource"] = isDisplayingSource - ctx.Data["IsDisplayingRendered"] = isDisplayingRendered + ctx.Data["IsRepresentableAsText"] = fInfo.st.IsRepresentableAsText() ctx.Data["IsExecutable"] = entry.IsExecutable() + ctx.Data["CanCopyContent"] = fInfo.st.IsRepresentableAsText() || fInfo.st.IsImage() - isTextSource := fInfo.isTextFile || isDisplayingSource - ctx.Data["IsTextSource"] = isTextSource - if isTextSource { - ctx.Data["CanCopyContent"] = true - } - - // Check LFS Lock - lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) - ctx.Data["LFSLock"] = lfsLock - if err != nil { - ctx.ServerError("GetTreePathLock", err) + attrs, ok := prepareFileViewLfsAttrs(ctx) + if !ok { return } - if lfsLock != nil { - u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID) - if err != nil { - ctx.ServerError("GetTreePathLock", err) - return - } - ctx.Data["LFSLockOwner"] = u.Name - ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink() - ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked") - } - // Assume file is not editable first. - if fInfo.isLFSFile { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") - } else if !isRepresentableAsText { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") - } - - // read all needed attributes which will be used later - // there should be no performance different between reading 2 or 4 here - attrsMap, err := attribute.CheckAttributes(ctx, ctx.Repo.GitRepo, ctx.Repo.CommitID, attribute.CheckAttributeOpts{ - Filenames: []string{ctx.Repo.TreePath}, - Attributes: []string{attribute.LinguistGenerated, attribute.LinguistVendored, attribute.LinguistLanguage, attribute.GitlabLanguage}, - }) - if err != nil { - ctx.ServerError("attribute.CheckAttributes", err) - return - } - attrs := attrsMap[ctx.Repo.TreePath] - if attrs == nil { - // this case shouldn't happen, just in case. - setting.PanicInDevOrTesting("no attributes found for %s", ctx.Repo.TreePath) - attrs = attribute.NewAttributes() - } + // TODO: in the future maybe we need more accurate flags, for example: + // * IsRepresentableAsText: some files are text, some are not + // * IsRenderableXxx: some files are rendered by backend "markup" engine, some are rendered by frontend (pdf, 3d) + // * DefaultViewMode: when there is no "display" query parameter, which view mode should be used by default, source or rendered + utf8Reader := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) switch { - case isRepresentableAsText: - if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { - ctx.Data["IsFileTooLarge"] = true - break - } - - if fInfo.st.IsSvgImage() { - ctx.Data["IsImageFile"] = true - ctx.Data["CanCopyContent"] = true - ctx.Data["HasSourceRenderedToggle"] = true - } - - rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) - - shouldRenderSource := ctx.FormString("display") == "source" - readmeExist := util.IsReadmeFileName(blob.Name()) - ctx.Data["ReadmeExist"] = readmeExist - - markupType := markup.DetectMarkupTypeByFileName(blob.Name()) - if markupType == "" { - markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf)) - } - if markupType != "" { - ctx.Data["HasSourceRenderedToggle"] = true - } - if markupType != "" && !shouldRenderSource { - ctx.Data["IsMarkup"] = true - ctx.Data["MarkupType"] = markupType - metas := ctx.Repo.Repository.ComposeRepoFileMetas(ctx) - metas["RefTypeNameSubURL"] = ctx.Repo.RefTypeNameSubURL() - rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), - CurrentTreePath: path.Dir(ctx.Repo.TreePath), - }). - WithMarkupType(markupType). - WithRelativePath(ctx.Repo.TreePath). - WithMetas(metas) - - ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) - if err != nil { - ctx.ServerError("Render", err) - return - } - // to prevent iframe load third-party url - ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") - } else { - buf, _ := io.ReadAll(rd) - - // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html - // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line; - // Gitea uses the definition (like most modern editors): - // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines; - // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL. - // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines. - // This NumLines is only used for the display on the UI: "xxx lines" - if len(buf) == 0 { - ctx.Data["NumLines"] = 0 - } else { - ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1 - } - - language := attrs.GetLanguage().Value() - fileContent, lexerName, err := highlight.File(blob.Name(), language, buf) - ctx.Data["LexerName"] = lexerName - if err != nil { - log.Error("highlight.File failed, fallback to plain text: %v", err) - fileContent = highlight.PlainText(buf) - } - status := &charset.EscapeStatus{} - statuses := make([]*charset.EscapeStatus, len(fileContent)) - for i, line := range fileContent { - statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale) - status = status.Or(statuses[i]) - } - ctx.Data["EscapeStatus"] = status - ctx.Data["FileContent"] = fileContent - ctx.Data["LineEscapeStatus"] = statuses - } - if !fInfo.isLFSFile { - if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.Data["CanEditFile"] = false - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") - } else { - ctx.Data["CanEditFile"] = true - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") - } - } else if !ctx.Repo.RefFullName.IsBranch() { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") - } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") - } - } - - case fInfo.st.IsPDF(): - ctx.Data["IsPDFFile"] = true + case fInfo.fileSize >= setting.UI.MaxDisplayFileSize: + ctx.Data["IsFileTooLarge"] = true + case handleFileViewRenderMarkup(ctx, entry.Name(), fInfo.st, buf, utf8Reader): + // it also sets ctx.Data["FileContent"] and more + ctx.Data["IsMarkup"] = true + case handleFileViewRenderSource(ctx, entry.Name(), attrs, fInfo, utf8Reader): + // it also sets ctx.Data["FileContent"] and more + ctx.Data["IsDisplayingSource"] = true + case handleFileViewRenderImage(ctx, fInfo, buf): + ctx.Data["IsImageFile"] = true case fInfo.st.IsVideo(): ctx.Data["IsVideoFile"] = true case fInfo.st.IsAudio(): ctx.Data["IsAudioFile"] = true - case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()): - ctx.Data["IsImageFile"] = true - ctx.Data["CanCopyContent"] = true default: - if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { - ctx.Data["IsFileTooLarge"] = true - break - } + // unable to render anything, show the "view raw" or let frontend handle it + } +} - // TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go" - // It is used by "external renders", markupRender will execute external programs to get rendered content. - if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" { - rd := io.MultiReader(bytes.NewReader(buf), dataRc) - ctx.Data["IsMarkup"] = true - ctx.Data["MarkupType"] = markupType - - rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), - CurrentTreePath: path.Dir(ctx.Repo.TreePath), - }). - WithMarkupType(markupType). - WithRelativePath(ctx.Repo.TreePath) - - ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) - if err != nil { - ctx.ServerError("Render", err) - return - } - } +func prepareFileViewEditorButtons(ctx *context.Context) bool { + // archived or mirror repository, the buttons should not be shown + if !ctx.Repo.Repository.CanEnableEditor() { + return true } - ctx.Data["IsVendored"], ctx.Data["IsGenerated"] = attrs.GetVendored().Value(), attrs.GetGenerated().Value() + // The buttons should not be shown if it's not a branch + if !ctx.Repo.RefFullName.IsBranch() { + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") + return true + } - if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() { - img, _, err := image.DecodeConfig(bytes.NewReader(buf)) - if err == nil { - // There are Image formats go can't decode - // Instead of throwing an error in that case, we show the size only when we can decode - ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height) - } + if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + ctx.Data["CanEditFile"] = true + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") + ctx.Data["CanDeleteFile"] = true + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") + return true } - if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.Data["CanDeleteFile"] = false - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") - } else { - ctx.Data["CanDeleteFile"] = true - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") + lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) + ctx.Data["LFSLock"] = lfsLock + if err != nil { + ctx.ServerError("GetTreePathLock", err) + return false + } + if lfsLock != nil { + u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID) + if err != nil { + ctx.ServerError("GetTreePathLock", err) + return false } - } else if !ctx.Repo.RefFullName.IsBranch() { - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") - } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") + ctx.Data["LFSLockOwner"] = u.Name + ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink() + ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked") } + + // it's a lfs file and the user is not the owner of the lock + isLFSLocked := lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID + ctx.Data["CanEditFile"] = !isLFSLocked + ctx.Data["EditFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.edit_this_file")) + ctx.Data["CanDeleteFile"] = !isLFSLocked + ctx.Data["DeleteFileTooltip"] = util.Iif(isLFSLocked, ctx.Tr("repo.editor.this_file_locked"), ctx.Tr("repo.editor.delete_this_file")) + return true } diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index 48fa47d738..c7396d44e3 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -143,7 +143,7 @@ func prepareToRenderDirectory(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefFullName.ShortName()) } - subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) + subfolder, readmeFile, err := findReadmeFileInEntries(ctx, ctx.Repo.TreePath, entries, true) if err != nil { ctx.ServerError("findReadmeFileInEntries", err) return @@ -339,7 +339,7 @@ func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) { if entry.IsDir() { prepareToRenderDirectory(ctx) } else { - prepareToRenderFile(ctx, entry) + prepareFileView(ctx, entry) } } } @@ -377,8 +377,8 @@ func prepareHomeTreeSideBarSwitch(ctx *context.Context) { func redirectSrcToRaw(ctx *context.Context) bool { // GitHub redirects a tree path with "?raw=1" to the raw path - // It is useful to embed some raw contents into markdown files, - // then viewing the markdown in "src" path could embed the raw content correctly. + // It is useful to embed some raw contents into Markdown files, + // then viewing the Markdown in "src" path could embed the raw content correctly. if ctx.Repo.TreePath != "" && ctx.FormBool("raw") { ctx.Redirect(ctx.Repo.RepoLink + "/raw/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)) return true @@ -386,6 +386,20 @@ func redirectSrcToRaw(ctx *context.Context) bool { return false } +func redirectFollowSymlink(ctx *context.Context, treePathEntry *git.TreeEntry) bool { + if ctx.Repo.TreePath == "" || !ctx.FormBool("follow_symlink") { + return false + } + if treePathEntry.IsLink() { + if res, err := git.EntryFollowLinks(ctx.Repo.Commit, ctx.Repo.TreePath, treePathEntry); err == nil { + redirect := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() + "/" + util.PathEscapeSegments(res.TargetFullPath) + "?" + ctx.Req.URL.RawQuery + ctx.Redirect(redirect) + return true + } // else: don't handle the links we cannot resolve, so ignore the error + } + return false +} + // Home render repository home page func Home(ctx *context.Context) { if handleRepoHomeFeed(ctx) { @@ -394,6 +408,7 @@ func Home(ctx *context.Context) { if redirectSrcToRaw(ctx) { return } + // Check whether the repo is viewable: not in migration, and the code unit should be enabled // Ideally the "feed" logic should be after this, but old code did so, so keep it as-is. checkHomeCodeViewable(ctx) @@ -424,6 +439,10 @@ func Home(ctx *context.Context) { return } + if redirectFollowSymlink(ctx, entry) { + return + } + // prepare the tree path var treeNames, paths []string branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.RefTypeNameSubURL() diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go index 459cf0a616..ba03febff3 100644 --- a/routers/web/repo/view_readme.go +++ b/routers/web/repo/view_readme.go @@ -32,15 +32,7 @@ import ( // entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries() // // FIXME: There has to be a more efficient way of doing this -func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) { - // Create a list of extensions in priority order - // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md - // 2. Txt files - e.g. README.txt - // 3. No extension - e.g. README - exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority - extCount := len(exts) - readmeFiles := make([]*git.TreeEntry, extCount+1) - +func findReadmeFileInEntries(ctx *context.Context, parentDir string, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) { docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/) for _, entry := range entries { if tryWellKnownDirs && entry.IsDir() { @@ -62,16 +54,23 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try docsEntries[2] = entry } } - continue } + } + + // Create a list of extensions in priority order + // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md + // 2. Txt files - e.g. README.txt + // 3. No extension - e.g. README + exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority + extCount := len(exts) + readmeFiles := make([]*git.TreeEntry, extCount+1) + for _, entry := range entries { if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok { - log.Debug("Potential readme file: %s", entry.Name()) + fullPath := path.Join(parentDir, entry.Name()) if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { if entry.IsLink() { - target, err := entry.FollowLinks() - if err != nil && !git.IsErrSymlinkUnresolved(err) { - return "", nil, err - } else if target != nil && (target.IsExecutable() || target.IsRegular()) { + res, err := git.EntryFollowLinks(ctx.Repo.Commit, fullPath, entry) + if err == nil && (res.TargetEntry.IsExecutable() || res.TargetEntry.IsRegular()) { readmeFiles[i] = entry } } else { @@ -80,6 +79,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try } } } + var readmeFile *git.TreeEntry for _, f := range readmeFiles { if f != nil { @@ -103,7 +103,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try return "", nil, err } - subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false) + subfolder, readmeFile, err := findReadmeFileInEntries(ctx, parentDir, childEntries, false) if err != nil && !git.IsErrNotExist(err) { return "", nil, err } @@ -139,46 +139,52 @@ func localizedExtensions(ext, languageCode string) (localizedExts []string) { } func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) { - target := readmeFile - if readmeFile != nil && readmeFile.IsLink() { - target, _ = readmeFile.FollowLinks() - } - if target == nil { - // if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't) - // simply skip rendering the README + if readmeFile == nil { return } + readmeFullPath := path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name()) + readmeTargetEntry := readmeFile + if readmeFile.IsLink() { + if res, err := git.EntryFollowLinks(ctx.Repo.Commit, readmeFullPath, readmeFile); err == nil { + readmeTargetEntry = res.TargetEntry + } else { + readmeTargetEntry = nil // if we cannot resolve the symlink, we cannot render the readme, ignore the error + } + } + if readmeTargetEntry == nil { + return // if no valid README entry found, skip rendering the README + } + ctx.Data["RawFileLink"] = "" - ctx.Data["ReadmeInList"] = true + ctx.Data["ReadmeInList"] = path.Join(subfolder, readmeFile.Name()) // the relative path to the readme file to the current tree path ctx.Data["ReadmeExist"] = true ctx.Data["FileIsSymlink"] = readmeFile.IsLink() - buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob()) + buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, readmeTargetEntry.Blob()) if err != nil { ctx.ServerError("getFileReader", err) return } defer dataRc.Close() - ctx.Data["FileIsText"] = fInfo.isTextFile - ctx.Data["FileTreePath"] = path.Join(subfolder, readmeFile.Name()) + ctx.Data["FileIsText"] = fInfo.st.IsText() + ctx.Data["FileTreePath"] = readmeFullPath ctx.Data["FileSize"] = fInfo.fileSize - ctx.Data["IsLFSFile"] = fInfo.isLFSFile + ctx.Data["IsLFSFile"] = fInfo.isLFSFile() - if fInfo.isLFSFile { + if fInfo.isLFSFile() { filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name())) ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64)) } - if !fInfo.isTextFile { + if !fInfo.st.IsText() { return } if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { // Pretend that this is a normal text file to display 'This file is too large to be shown' ctx.Data["IsFileTooLarge"] = true - ctx.Data["IsTextFile"] = true return } @@ -190,10 +196,10 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ CurrentRefPath: ctx.Repo.RefTypeNameSubURL(), - CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder), + CurrentTreePath: path.Dir(readmeFullPath), }). WithMarkupType(markupType). - WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). + WithRelativePath(readmeFullPath) ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) if err != nil { @@ -212,7 +218,7 @@ func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFil ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) } - if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + if !fInfo.isLFSFile() && ctx.Repo.Repository.CanEnableEditor() { ctx.Data["CanEditReadmeFile"] = true } } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 41bf9f5adb..69858c9692 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -7,6 +7,7 @@ package repo import ( "bytes" gocontext "context" + "html/template" "io" "net/http" "net/url" @@ -61,9 +62,9 @@ func MustEnableWiki(ctx *context.Context) { return } - unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki) + repoUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalWiki) if err == nil { - ctx.Redirect(unit.ExternalWikiConfig().ExternalWikiURL) + ctx.Redirect(repoUnit.ExternalWikiConfig().ExternalWikiURL) return } } @@ -95,7 +96,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) } func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) { - wikiGitRepo, errGitRepo := gitrepo.OpenRepository(ctx, ctx.Repo.Repository.WikiStorageRepo()) + wikiGitRepo, errGitRepo := gitrepo.RepositoryFromRequestContextOrOpen(ctx, ctx.Repo.Repository.WikiStorageRepo()) if errGitRepo != nil { ctx.ServerError("OpenRepository", errGitRepo) return nil, nil, errGitRepo @@ -178,23 +179,17 @@ func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_ } func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return nil, nil } - // Get page list. + // get the wiki pages list. entries, err := commit.ListEntries() if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("ListEntries", err) return nil, nil } @@ -208,9 +203,6 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { if repo_model.IsErrWikiInvalidFileName(err) { continue } - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("WikiFilenameToName", err) return nil, nil } else if wikiName == "_Sidebar" || wikiName == "_Footer" { @@ -249,58 +241,26 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { ctx.Redirect(util.URLJoin(ctx.Repo.RepoLink, "wiki/raw", string(pageName))) } if entry == nil || ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - // get filecontent + // get page content data := wikiContentsByEntry(ctx, entry) if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - var sidebarContent []byte - if !isSideBar { - sidebarContent, _, _, _ = wikiContentsByName(ctx, commit, "_Sidebar") - if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } - return nil, nil - } - } else { - sidebarContent = data - } - - var footerContent []byte - if !isFooter { - footerContent, _, _, _ = wikiContentsByName(ctx, commit, "_Footer") - if ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } - return nil, nil - } - } else { - footerContent = data - } - rctx := renderhelper.NewRenderContextRepoWiki(ctx, ctx.Repo.Repository) - buf := &strings.Builder{} - renderFn := func(data []byte) (escaped *charset.EscapeStatus, output string, err error) { + renderFn := func(data []byte) (escaped *charset.EscapeStatus, output template.HTML, err error) { + buf := &strings.Builder{} markupRd, markupWr := io.Pipe() defer markupWr.Close() done := make(chan struct{}) go func() { // We allow NBSP here this is rendered escaped, _ = charset.EscapeControlReader(markupRd, buf, ctx.Locale, charset.RuneNBSP) - output = buf.String() + output = template.HTML(buf.String()) buf.Reset() close(done) }() @@ -311,75 +271,61 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { return escaped, output, err } - ctx.Data["EscapeStatus"], ctx.Data["content"], err = renderFn(data) + ctx.Data["EscapeStatus"], ctx.Data["WikiContentHTML"], err = renderFn(data) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } if rctx.SidebarTocNode != nil { - sb := &strings.Builder{} - err = markdown.SpecializedMarkdown(rctx).Renderer().Render(sb, nil, rctx.SidebarTocNode) - if err != nil { + sb := strings.Builder{} + if err = markdown.SpecializedMarkdown(rctx).Renderer().Render(&sb, nil, rctx.SidebarTocNode); err != nil { log.Error("Failed to render wiki sidebar TOC: %v", err) - } else { - ctx.Data["sidebarTocContent"] = sb.String() } + ctx.Data["WikiSidebarTocHTML"] = templates.SanitizeHTML(sb.String()) } if !isSideBar { - buf.Reset() - ctx.Data["sidebarEscapeStatus"], ctx.Data["sidebarContent"], err = renderFn(sidebarContent) + sidebarContent, _, _, _ := wikiContentsByName(ctx, commit, "_Sidebar") + if ctx.Written() { + return nil, nil + } + ctx.Data["WikiSidebarEscapeStatus"], ctx.Data["WikiSidebarHTML"], err = renderFn(sidebarContent) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } - ctx.Data["sidebarPresent"] = sidebarContent != nil - } else { - ctx.Data["sidebarPresent"] = false } if !isFooter { - buf.Reset() - ctx.Data["footerEscapeStatus"], ctx.Data["footerContent"], err = renderFn(footerContent) + footerContent, _, _, _ := wikiContentsByName(ctx, commit, "_Footer") + if ctx.Written() { + return nil, nil + } + ctx.Data["WikiFooterEscapeStatus"], ctx.Data["WikiFooterHTML"], err = renderFn(footerContent) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("Render", err) return nil, nil } - ctx.Data["footerPresent"] = footerContent != nil - } else { - ctx.Data["footerPresent"] = false } // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount - return wikiRepo, entry + return wikiGitRepo, entry } func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) + wikiGitRepo, commit, err := findWikiRepoCommit(ctx) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) } return nil, nil } - // get requested pagename + // get requested page name pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*")) if len(pageName) == 0 { pageName = "Home" @@ -394,53 +340,35 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name - // lookup filename in wiki - get filecontent, gitTree entry , real filename - data, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) + // lookup filename in wiki - get page content, gitTree entry , real filename + _, entry, pageFilename, noEntry := wikiContentsByName(ctx, commit, pageName) if noEntry { ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages") } if entry == nil || ctx.Written() { - if wikiRepo != nil { - wikiRepo.Close() - } return nil, nil } - ctx.Data["content"] = string(data) - ctx.Data["sidebarPresent"] = false - ctx.Data["sidebarContent"] = "" - ctx.Data["footerPresent"] = false - ctx.Data["footerContent"] = "" - // get commit count - wiki revisions - commitsCount, _ := wikiRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) + commitsCount, _ := wikiGitRepo.FileCommitsCount(ctx.Repo.Repository.DefaultWikiBranch, pageFilename) ctx.Data["CommitCount"] = commitsCount // get page - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) // get Commit Count - commitsHistory, err := wikiRepo.CommitsByFileAndRange( + commitsHistory, err := wikiGitRepo.CommitsByFileAndRange( git.CommitsByFileAndRangeOptions{ Revision: ctx.Repo.Repository.DefaultWikiBranch, File: pageFilename, Page: page, }) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("CommitsByFileAndRange", err) return nil, nil } ctx.Data["Commits"], err = git_service.ConvertFromGitCommit(ctx, commitsHistory, ctx.Repo.Repository) if err != nil { - if wikiRepo != nil { - wikiRepo.Close() - } ctx.ServerError("ConvertFromGitCommit", err) return nil, nil } @@ -449,16 +377,11 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager - return wikiRepo, entry + return wikiGitRepo, entry } func renderEditPage(ctx *context.Context) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - _ = wikiRepo.Close() - } - }() + _, commit, err := findWikiRepoCommit(ctx) if err != nil { if !git.IsErrNotExist(err) { ctx.ServerError("GetBranchCommit", err) @@ -466,7 +389,7 @@ func renderEditPage(ctx *context.Context) { return } - // get requested pagename + // get requested page name pageName := wiki_service.WebPathFromRequest(ctx.PathParamRaw("*")) if len(pageName) == 0 { pageName = "Home" @@ -490,17 +413,13 @@ func renderEditPage(ctx *context.Context) { return } - // get filecontent + // get wiki page content data := wikiContentsByEntry(ctx, entry) if ctx.Written() { return } - ctx.Data["content"] = string(data) - ctx.Data["sidebarPresent"] = false - ctx.Data["sidebarContent"] = "" - ctx.Data["footerPresent"] = false - ctx.Data["footerContent"] = "" + ctx.Data["WikiEditContent"] = string(data) } // WikiPost renders post of wiki page @@ -562,12 +481,7 @@ func Wiki(ctx *context.Context) { return } - wikiRepo, entry := renderViewPage(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() + wikiGitRepo, entry := renderViewPage(ctx) if ctx.Written() { return } @@ -583,7 +497,7 @@ func Wiki(ctx *context.Context) { ctx.Data["FormatWarning"] = ext + " rendering is not supported at the moment. Rendered as Markdown." } // Get last change information. - lastCommit, err := wikiRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -603,13 +517,7 @@ func WikiRevision(ctx *context.Context) { return } - wikiRepo, entry := renderRevisionPage(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() - + wikiGitRepo, entry := renderRevisionPage(ctx) if ctx.Written() { return } @@ -621,7 +529,7 @@ func WikiRevision(ctx *context.Context) { // Get last change information. wikiPath := entry.Name() - lastCommit, err := wikiRepo.GetCommitByPath(wikiPath) + lastCommit, err := wikiGitRepo.GetCommitByPath(wikiPath) if err != nil { ctx.ServerError("GetCommitByPath", err) return @@ -641,12 +549,7 @@ func WikiPages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.wiki.pages") ctx.Data["CanWriteWiki"] = ctx.Repo.CanWrite(unit.TypeWiki) && !ctx.Repo.Repository.IsArchived - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - _ = wikiRepo.Close() - } - }() + _, commit, err := findWikiRepoCommit(ctx) if err != nil { ctx.Redirect(ctx.Repo.RepoLink + "/wiki") return @@ -700,13 +603,7 @@ func WikiPages(ctx *context.Context) { // WikiRaw outputs raw blob requested by user (image for example) func WikiRaw(ctx *context.Context) { - wikiRepo, commit, err := findWikiRepoCommit(ctx) - defer func() { - if wikiRepo != nil { - wikiRepo.Close() - } - }() - + _, commit, err := findWikiRepoCommit(ctx) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound(nil) diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index d0139f6613..59bf6ed79b 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -164,7 +164,7 @@ func TestEditWiki(t *testing.T) { EditWiki(ctx) assert.Equal(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, "Home", ctx.Data["Title"]) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"]) + assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["WikiEditContent"]) ctx, _ = contexttest.MockContext(t, "user2/repo1/wiki/jpeg.jpg?action=_edit") ctx.SetPathParam("*", "jpeg.jpg") @@ -245,7 +245,12 @@ func TestDefaultWikiBranch(t *testing.T) { assert.NoError(t, wiki_service.ChangeDefaultWikiBranch(db.DefaultContext, repoWithNoWiki, "main")) // repo with wiki - assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime(db.DefaultContext, &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"})) + assert.NoError(t, repo_model.UpdateRepositoryColsNoAutoTime( + db.DefaultContext, + &repo_model.Repository{ID: 1, DefaultWikiBranch: "wrong-branch"}, + "default_wiki_branch", + ), + ) ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki") ctx.SetPathParam("*", "Home") diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index a642cfd66d..648f8046a4 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -108,10 +108,7 @@ func Runners(ctx *context.Context) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) opts := actions_model.FindRunnerOptions{ ListOptions: db.ListOptions{ @@ -179,10 +176,7 @@ func RunnersEdit(ctx *context.Context) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) runnerID := ctx.PathParamInt64("runnerid") ownerID := rCtx.OwnerID diff --git a/routers/web/shared/label/label.go b/routers/web/shared/label/label.go new file mode 100644 index 0000000000..6968a318c4 --- /dev/null +++ b/routers/web/shared/label/label.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package label + +import ( + "code.gitea.io/gitea/modules/label" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/forms" +) + +func GetLabelEditForm(ctx *context.Context) *forms.CreateLabelForm { + form := web.GetForm(ctx).(*forms.CreateLabelForm) + if ctx.HasError() { + ctx.JSONError(ctx.Data["ErrorMsg"].(string)) + return nil + } + var err error + form.Color, err = label.NormalizeColor(form.Color) + if err != nil { + ctx.JSONError(ctx.Tr("repo.issues.label_color_invalid")) + return nil + } + return form +} diff --git a/routers/web/shared/packages/packages.go b/routers/web/shared/packages/packages.go index 3d1795b42c..a18dedf89c 100644 --- a/routers/web/shared/packages/packages.go +++ b/routers/web/shared/packages/packages.go @@ -8,7 +8,6 @@ import ( "net/http" "time" - "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" @@ -159,12 +158,18 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) { PackageID: p.ID, IsInternal: optional.Some(false), Sort: packages_model.SortCreatedDesc, - Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200), }) if err != nil { ctx.ServerError("SearchVersions", err) return } + if pcr.KeepCount > 0 { + if pcr.KeepCount < len(pvs) { + pvs = pvs[pcr.KeepCount:] + } else { + pvs = nil + } + } for _, pv := range pvs { if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil { ctx.ServerError("ShouldBeSkipped", err) @@ -177,7 +182,6 @@ func SetRulePreviewContext(ctx *context.Context, owner *user_model.User) { if pcr.MatchFullName { toMatch = p.LowerName + "/" + pv.LowerVersion } - if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) { continue } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 48a5d58ea4..2bd0abc4c0 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -47,13 +47,12 @@ func prepareContextForProfileBigAvatar(ctx *context.Context) { ctx.Data["RenderedDescription"] = content } - showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) orgs, err := db.Find[organization.Organization](ctx, organization.FindOrgOptions{ - UserID: ctx.ContextUser.ID, - IncludePrivate: showPrivate, + UserID: ctx.ContextUser.ID, + IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.ContextUser), ListOptions: db.ListOptions{ Page: 1, - // query one more results (without a separate counting) to see whether we need to add the "show more orgs" link + // query one more result (without a separate counting) to see whether we need to add the "show more orgs" link PageSize: setting.UI.User.OrgPagingNum + 1, }, }) @@ -165,7 +164,7 @@ func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult } func loadHeaderCount(ctx *context.Context) error { - repoCount, err := repo_model.CountRepository(ctx, &repo_model.SearchRepoOptions{ + repoCount, err := repo_model.CountRepository(ctx, repo_model.SearchRepoOptions{ Actor: ctx.Doer, OwnerID: ctx.ContextUser.ID, Private: ctx.IsSigned, diff --git a/routers/web/user/code.go b/routers/web/user/code.go index f2153c6d54..11579c40a6 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -5,6 +5,7 @@ package user import ( "net/http" + "slices" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" @@ -86,14 +87,7 @@ func CodeSearch(ctx *context.Context) { loadRepoIDs := make([]int64, 0, len(searchResults)) for _, result := range searchResults { - var find bool - for _, id := range loadRepoIDs { - if id == result.RepoID { - find = true - break - } - } - if !find { + if !slices.Contains(loadRepoIDs, result.RepoID) { loadRepoIDs = append(loadRepoIDs, result.RepoID) } } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 77f9cb8cca..b53a3daedb 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -176,7 +176,7 @@ func Milestones(ctx *context.Context) { } var ( - userRepoCond = repo_model.SearchRepositoryCondition(&repoOpts) // all repo condition user could visit + userRepoCond = repo_model.SearchRepositoryCondition(repoOpts) // all repo condition user could visit repoCond = userRepoCond repoIDs []int64 @@ -197,7 +197,7 @@ func Milestones(ctx *context.Context) { reposQuery = reposQuery[1 : len(reposQuery)-1] // for each ID (delimiter ",") add to int to repoIDs - for _, rID := range strings.Split(reposQuery, ",") { + for rID := range strings.SplitSeq(reposQuery, ",") { // Ensure nonempty string entries if rID != "" && rID != "0" { rIDint64, err := strconv.ParseInt(rID, 10, 64) @@ -242,7 +242,7 @@ func Milestones(ctx *context.Context) { return } - showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, &repoOpts, userRepoCond, false) + showRepos, _, err := repo_model.SearchRepositoryByCondition(ctx, repoOpts, userRepoCond, false) if err != nil { ctx.ServerError("SearchRepositoryByCondition", err) return @@ -461,7 +461,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // As team: // - Team org's owns the repository. // - Team has read permission to repository. - repoOpts := &repo_model.SearchRepoOptions{ + repoOpts := repo_model.SearchRepoOptions{ Actor: ctx.Doer, OwnerID: ctxUser.ID, Private: true, @@ -520,10 +520,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { opts.IsClosed = optional.Some(isShowClosed) // Make sure page number is at least 1. Will be posted to ctx.Data. - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) opts.Paginator = &db.ListOptions{ Page: page, PageSize: setting.UI.IssuePagingNum, diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index 2a8a8812e3..68ad79b11e 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -28,7 +28,7 @@ func TestArchivedIssues(t *testing.T) { ctx.Req.Form.Set("state", "open") // Assume: User 30 has access to two Repos with Issues, one of the Repos being archived. - repos, _, _ := repo_model.GetUserRepositories(db.DefaultContext, &repo_model.SearchRepoOptions{Actor: ctx.Doer}) + repos, _, _ := repo_model.GetUserRepositories(db.DefaultContext, repo_model.SearchRepoOptions{Actor: ctx.Doer}) assert.Len(t, repos, 3) IsArchived := make(map[int64]bool) NumIssues := make(map[int64]int) diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go index 89f3c6956f..610a9b8076 100644 --- a/routers/web/user/notification.go +++ b/routers/web/user/notification.go @@ -203,10 +203,7 @@ func NotificationPurgePost(ctx *context.Context) { // NotificationSubscriptions returns the list of subscribed issues func NotificationSubscriptions(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) sortType := ctx.FormString("sort") ctx.Data["SortType"] = sortType @@ -287,16 +284,8 @@ func NotificationSubscriptions(ctx *context.Context) { ctx.Data["CommitLastStatus"] = lastStatus ctx.Data["CommitStatuses"] = commitStatuses ctx.Data["Issues"] = issues - ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "") - commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues) - if err != nil { - ctx.ServerError("GetIssuesLastCommitStatus", err) - return - } - ctx.Data["CommitStatus"] = commitStatus - approvalCounts, err := issues.GetApprovalCounts(ctx) if err != nil { ctx.ServerError("ApprovalCounts", err) @@ -339,10 +328,7 @@ func NotificationSubscriptions(ctx *context.Context) { // NotificationWatching returns the list of watching repos func NotificationWatching(ctx *context.Context) { - page := ctx.FormInt("page") - if page < 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) keyword := ctx.FormTrim("q") ctx.Data["Keyword"] = keyword @@ -390,7 +376,7 @@ func NotificationWatching(ctx *context.Context) { private := ctx.FormOptionalBool("private") ctx.Data["IsPrivate"] = private - repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + repos, count, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: setting.UI.User.RepoPagingNum, Page: page, diff --git a/routers/web/user/package.go b/routers/web/user/package.go index e96f5a04ea..216acdf927 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -4,6 +4,8 @@ package user import ( + gocontext "context" + "errors" "net/http" "net/url" @@ -20,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/optional" alpine_module "code.gitea.io/gitea/modules/packages/alpine" arch_module "code.gitea.io/gitea/modules/packages/arch" + container_module "code.gitea.io/gitea/modules/packages/container" debian_module "code.gitea.io/gitea/modules/packages/debian" rpm_module "code.gitea.io/gitea/modules/packages/rpm" "code.gitea.io/gitea/modules/setting" @@ -31,6 +34,7 @@ import ( "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" packages_service "code.gitea.io/gitea/services/packages" + container_service "code.gitea.io/gitea/services/packages/container" ) const ( @@ -46,10 +50,7 @@ func ListPackages(ctx *context.Context) { ctx.ServerError("RenderUserOrgHeader", err) return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) query := ctx.FormTrim("q") packageType := ctx.FormTrim("type") @@ -165,6 +166,24 @@ func RedirectToLastVersion(ctx *context.Context) { ctx.Redirect(pd.VersionWebLink()) } +func viewPackageContainerImage(ctx gocontext.Context, pd *packages_model.PackageDescriptor, digest string) (*container_module.Metadata, error) { + manifestBlob, err := container_model.GetContainerBlob(ctx, &container_model.BlobSearchOptions{ + OwnerID: pd.Owner.ID, + Image: pd.Package.LowerName, + Digest: digest, + }) + if err != nil { + return nil, err + } + manifestReader, err := packages_service.OpenBlobStream(manifestBlob.Blob) + if err != nil { + return nil, err + } + defer manifestReader.Close() + _, _, metadata, err := container_service.ParseManifestMetadata(ctx, manifestReader, pd.Owner.ID, pd.Package.LowerName) + return metadata, err +} + // ViewPackageVersion displays a single package version func ViewPackageVersion(ctx *context.Context) { if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil { @@ -172,6 +191,7 @@ func ViewPackageVersion(ctx *context.Context) { return } + versionSub := ctx.PathParam("version_sub") pd := ctx.Package.Descriptor ctx.Data["Title"] = pd.Package.Name ctx.Data["IsPackagesPage"] = true @@ -260,21 +280,30 @@ func ViewPackageVersion(ctx *context.Context) { ctx.Data["Groups"] = util.Sorted(groups.Values()) ctx.Data["Architectures"] = util.Sorted(architectures.Values()) - } - - var ( - total int64 - pvs []*packages_model.PackageVersion - ) - switch pd.Package.Type { case packages_model.TypeContainer: - pvs, total, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{ + imageMetadata := pd.Metadata + if versionSub != "" { + imageMetadata, err = viewPackageContainerImage(ctx, pd, versionSub) + if errors.Is(err, util.ErrNotExist) { + ctx.NotFound(nil) + return + } else if err != nil { + ctx.ServerError("viewPackageContainerImage", err) + return + } + } + ctx.Data["ContainerImageMetadata"] = imageMetadata + } + var pvs []*packages_model.PackageVersion + var pvsTotal int64 + if pd.Package.Type == packages_model.TypeContainer { + pvs, pvsTotal, err = container_model.SearchImageTags(ctx, &container_model.ImageTagsSearchOptions{ Paginator: db.NewAbsoluteListOptions(0, 5), PackageID: pd.Package.ID, IsTagged: true, }) - default: - pvs, total, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + } else { + pvs, pvsTotal, err = packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ Paginator: db.NewAbsoluteListOptions(0, 5), PackageID: pd.Package.ID, IsInternal: optional.Some(false), @@ -284,9 +313,8 @@ func ViewPackageVersion(ctx *context.Context) { ctx.ServerError("", err) return } - ctx.Data["LatestVersions"] = pvs - ctx.Data["TotalVersionCount"] = total + ctx.Data["TotalVersionCount"] = pvsTotal ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() @@ -320,10 +348,7 @@ func ListPackageVersions(ctx *context.Context) { return } - page := ctx.FormInt("page") - if page <= 1 { - page = 1 - } + page := max(ctx.FormInt("page"), 1) pagination := &db.ListOptions{ PageSize: setting.UI.PackagesPagingNum, Page: page, @@ -407,7 +432,7 @@ func PackageSettings(ctx *context.Context) { ctx.Data["IsPackagesPage"] = true ctx.Data["PackageDescriptor"] = pd - repos, _, _ := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{ + repos, _, _ := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ Actor: pd.Owner, Private: true, }) @@ -488,9 +513,9 @@ func DownloadPackageFile(ctx *context.Context) { return } - s, u, _, err := packages_service.GetPackageFileStream(ctx, pf) + s, u, _, err := packages_service.OpenFileForDownload(ctx, pf) if err != nil { - ctx.ServerError("GetPackageFileStream", err) + ctx.ServerError("OpenFileForDownload", err) return } diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go index ee19665109..d7052914b6 100644 --- a/routers/web/user/profile.go +++ b/routers/web/user/profile.go @@ -63,21 +63,9 @@ func userProfile(ctx *context.Context) { ctx.Data["Title"] = ctx.ContextUser.DisplayName() ctx.Data["PageIsUserProfile"] = true - // prepare heatmap data - if setting.Service.EnableUserHeatmap { - data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserHeatmapDataByUser", err) - return - } - ctx.Data["HeatmapData"] = data - ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) - } - profileDbRepo, profileReadmeBlob := shared_user.FindOwnerProfileReadme(ctx, ctx.Doer) - showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) - prepareUserProfileTabData(ctx, showPrivate, profileDbRepo, profileReadmeBlob) + prepareUserProfileTabData(ctx, profileDbRepo, profileReadmeBlob) // prepare the user nav header data after "prepareUserProfileTabData" to avoid re-querying the NumFollowers & NumFollowing // because ctx.Data["NumFollowers"] and "NumFollowing" logic duplicates in both of them @@ -90,7 +78,7 @@ func userProfile(ctx *context.Context) { ctx.HTML(http.StatusOK, tplProfile) } -func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) { +func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) { // if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page // if there is not a profile readme, the overview tab should be treated as the repositories tab tab := ctx.FormString("tab") @@ -173,8 +161,20 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb ctx.Data["Cards"] = following total = int(numFollowing) case "activity": + // prepare heatmap data + if setting.Service.EnableUserHeatmap { + data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserHeatmapDataByUser", err) + return + } + ctx.Data["HeatmapData"] = data + ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data) + } + date := ctx.FormString("date") pagingNum = setting.UI.FeedPagingNum + showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) items, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ RequestedUser: ctx.ContextUser, Actor: ctx.Doer, @@ -197,7 +197,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb total = int(count) case "stars": ctx.Data["PageIsProfileStarList"] = true - repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + ctx.Data["ShowRepoOwnerOnList"] = true + repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: pagingNum, Page: page, @@ -224,7 +225,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb total = int(count) case "watching": - repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: pagingNum, Page: page, @@ -265,8 +266,8 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb } case "organizations": orgs, count, err := db.FindAndCount[organization.Organization](ctx, organization.FindOrgOptions{ - UserID: ctx.ContextUser.ID, - IncludePrivate: showPrivate, + UserID: ctx.ContextUser.ID, + IncludeVisibility: organization.DoerViewOtherVisibility(ctx.Doer, ctx.ContextUser), ListOptions: db.ListOptions{ Page: page, PageSize: pagingNum, @@ -279,7 +280,7 @@ func prepareUserProfileTabData(ctx *context.Context, showPrivate bool, profileDb ctx.Data["Cards"] = orgs total = int(count) default: // default to "repositories" - repos, count, err = repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + repos, count, err = repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: pagingNum, Page: page, diff --git a/routers/web/user/search.go b/routers/web/user/search.go index be5eee90a9..9acb9694d7 100644 --- a/routers/web/user/search.go +++ b/routers/web/user/search.go @@ -16,7 +16,7 @@ import ( // SearchCandidates searches candidate users for dropdown list func SearchCandidates(ctx *context.Context) { - users, _, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{ + users, _, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{ Actor: ctx.Doer, Keyword: ctx.FormTrim("q"), Type: user_model.UserTypeIndividual, diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 7577036a55..98995cd69c 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/typesniffer" @@ -206,8 +207,8 @@ func Organization(ctx *context.Context) { PageSize: setting.UI.Admin.UserPagingNum, Page: ctx.FormInt("page"), }, - UserID: ctx.Doer.ID, - IncludePrivate: ctx.IsSigned, + UserID: ctx.Doer.ID, + IncludeVisibility: structs.VisibleTypePrivate, } if opts.Page <= 0 { @@ -284,7 +285,7 @@ func Repos(ctx *context.Context) { return } - userRepos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{ + userRepos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ Actor: ctxUser, Private: true, ListOptions: db.ListOptions{ @@ -309,7 +310,7 @@ func Repos(ctx *context.Context) { ctx.Data["Dirs"] = repoNames ctx.Data["ReposMap"] = repos } else { - repos, count64, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) + repos, count64, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{Actor: ctxUser, Private: true, ListOptions: opts}) if err != nil { ctx.ServerError("GetUserRepositories", err) return diff --git a/routers/web/web.go b/routers/web/web.go index 866401252d..1039f9e739 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -964,7 +964,8 @@ func registerWebRoutes(m *web.Router) { addSettingsVariablesRoutes() }, actions.MustEnableActions) - m.Methods("GET,POST", "/delete", org.SettingsDelete) + m.Post("/rename", web.Bind(forms.RenameOrgForm{}), org.SettingsRenamePost) + m.Post("/delete", org.SettingsDeleteOrgPost) m.Group("/packages", func() { m.Get("", org.Packages) @@ -1012,6 +1013,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/versions", user.ListPackageVersions) m.Group("/{version}", func() { m.Get("", user.ViewPackageVersion) + m.Get("/{version_sub}", user.ViewPackageVersion) m.Get("/files/{fileid}", user.DownloadPackageFile) m.Group("/settings", func() { m.Get("", user.PackageSettings) @@ -1029,7 +1031,7 @@ func registerWebRoutes(m *web.Router) { m.Get("", org.Projects) m.Get("/{id}", org.ViewProject) }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) - m.Group("", func() { //nolint:dupl + m.Group("", func() { //nolint:dupl // duplicates lines 1421-1441 m.Get("/new", org.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) m.Group("/{id}", func() { @@ -1251,7 +1253,8 @@ func registerWebRoutes(m *web.Router) { m.Post("/add", web.Bind(forms.AddTimeManuallyForm{}), repo.AddTimeManually) m.Post("/{timeid}/delete", repo.DeleteTime) m.Group("/stopwatch", func() { - m.Post("/toggle", repo.IssueStopwatch) + m.Post("/start", repo.IssueStartStopwatch) + m.Post("/stop", repo.IssueStopStopwatch) m.Post("/cancel", repo.CancelStopwatch) }) }) @@ -1311,26 +1314,38 @@ func registerWebRoutes(m *web.Router) { }, reqSignIn, context.RepoAssignment, context.RepoMustNotBeArchived()) // end "/{username}/{reponame}": create or edit issues, pulls, labels, milestones - m.Group("/{username}/{reponame}", func() { // repo code + m.Group("/{username}/{reponame}", func() { // repo code (at least "code reader") m.Group("", func() { m.Group("", func() { - m.Post("/_preview/*", web.Bind(forms.EditPreviewDiffForm{}), repo.DiffPreviewPost) - m.Combo("/_edit/*").Get(repo.EditFile). - Post(web.Bind(forms.EditRepoFileForm{}), repo.EditFilePost) - m.Combo("/_new/*").Get(repo.NewFile). - Post(web.Bind(forms.EditRepoFileForm{}), repo.NewFilePost) - m.Combo("/_delete/*").Get(repo.DeleteFile). - Post(web.Bind(forms.DeleteRepoFileForm{}), repo.DeleteFilePost) - m.Combo("/_upload/*", repo.MustBeAbleToUpload).Get(repo.UploadFile). - Post(web.Bind(forms.UploadRepoFileForm{}), repo.UploadFilePost) - m.Combo("/_diffpatch/*").Get(repo.NewDiffPatch). - Post(web.Bind(forms.EditRepoFileForm{}), repo.NewDiffPatchPost) - m.Combo("/_cherrypick/{sha:([a-f0-9]{7,64})}/*").Get(repo.CherryPick). - Post(web.Bind(forms.CherryPickForm{}), repo.CherryPickPost) - }, context.RepoRefByType(git.RefTypeBranch), context.CanWriteToBranch(), repo.WebGitOperationCommonData) + // "GET" requests only need "code reader" permission, "POST" requests need "code writer" permission. + // Because reader can "fork and edit" + canWriteToBranch := context.CanWriteToBranch() + m.Post("/_preview/*", repo.DiffPreviewPost) // read-only, fine with "code reader" + m.Post("/_fork/*", repo.ForkToEditPost) // read-only, fork to own repo, fine with "code reader" + + // the path params are used in PrepareCommitFormOptions to construct the correct form action URL + m.Combo("/{editor_action:_edit}/*"). + Get(repo.EditFile). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost) + m.Combo("/{editor_action:_new}/*"). + Get(repo.EditFile). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.EditFilePost) + m.Combo("/{editor_action:_delete}/*"). + Get(repo.DeleteFile). + Post(web.Bind(forms.DeleteRepoFileForm{}), canWriteToBranch, repo.DeleteFilePost) + m.Combo("/{editor_action:_upload}/*", repo.MustBeAbleToUpload). + Get(repo.UploadFile). + Post(web.Bind(forms.UploadRepoFileForm{}), canWriteToBranch, repo.UploadFilePost) + m.Combo("/{editor_action:_diffpatch}/*"). + Get(repo.NewDiffPatch). + Post(web.Bind(forms.EditRepoFileForm{}), canWriteToBranch, repo.NewDiffPatchPost) + m.Combo("/{editor_action:_cherrypick}/{sha:([a-f0-9]{7,64})}/*"). + Get(repo.CherryPick). + Post(web.Bind(forms.CherryPickForm{}), canWriteToBranch, repo.CherryPickPost) + }, context.RepoRefByType(git.RefTypeBranch), repo.WebGitOperationCommonData) m.Group("", func() { m.Post("/upload-file", repo.UploadFileToServer) - m.Post("/upload-remove", web.Bind(forms.RemoveUploadFileForm{}), repo.RemoveUploadFileFromServer) + m.Post("/upload-remove", repo.RemoveUploadFileFromServer) }, repo.MustBeAbleToUpload, reqRepoCodeWriter) }, repo.MustBeEditable, context.RepoMustNotBeArchived()) @@ -1403,7 +1418,7 @@ func registerWebRoutes(m *web.Router) { m.Group("/{username}/{reponame}/projects", func() { m.Get("", repo.Projects) m.Get("/{id}", repo.ViewProject) - m.Group("", func() { //nolint:dupl + m.Group("", func() { //nolint:dupl // duplicates lines 1034-1054 m.Get("/new", repo.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), repo.NewProjectPost) m.Group("/{id}", func() { @@ -1445,6 +1460,7 @@ func registerWebRoutes(m *web.Router) { m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) m.Get("/logs", actions.Logs) }) + m.Get("/workflow", actions.ViewWorkflowFile) m.Post("/cancel", reqRepoActionsWriter, actions.Cancel) m.Post("/approve", reqRepoActionsWriter, actions.Approve) m.Post("/delete", reqRepoActionsWriter, actions.Delete) diff --git a/services/actions/clear_tasks.go b/services/actions/clear_tasks.go index 2aeb0e8c96..274c04aa57 100644 --- a/services/actions/clear_tasks.go +++ b/services/actions/clear_tasks.go @@ -42,6 +42,10 @@ func notifyWorkflowJobStatusUpdate(ctx context.Context, jobs []*actions_model.Ac _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } + if len(jobs) > 0 { + job := jobs[0] + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } } } @@ -123,7 +127,7 @@ func CancelAbandonedJobs(ctx context.Context) error { } CreateCommitStatus(ctx, job) if updated { - _ = job.LoadAttributes(ctx) + NotifyWorkflowRunStatusUpdateWithReload(ctx, job) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } } diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index eb15d16061..ef241e5091 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -14,9 +14,9 @@ import ( git_model "code.gitea.io/gitea/models/git" user_model "code.gitea.io/gitea/models/user" actions_module "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/commitstatus" git "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - api "code.gitea.io/gitea/modules/structs" webhook_module "code.gitea.io/gitea/modules/webhook" commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus" @@ -92,7 +92,7 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er } ctxname := fmt.Sprintf("%s / %s (%s)", runName, job.Name, event) state := toCommitStatus(job.Status) - if statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil { + if statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptionsAll); err == nil { for _, v := range statuses { if v.Context == ctxname { if v.State == state { @@ -147,16 +147,18 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er return commitstatus_service.CreateCommitStatus(ctx, repo, creator, commitID.String(), &status) } -func toCommitStatus(status actions_model.Status) api.CommitStatusState { +func toCommitStatus(status actions_model.Status) commitstatus.CommitStatusState { switch status { - case actions_model.StatusSuccess, actions_model.StatusSkipped: - return api.CommitStatusSuccess + case actions_model.StatusSuccess: + return commitstatus.CommitStatusSuccess case actions_model.StatusFailure, actions_model.StatusCancelled: - return api.CommitStatusFailure + return commitstatus.CommitStatusFailure case actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusRunning: - return api.CommitStatusPending + return commitstatus.CommitStatusPending + case actions_model.StatusSkipped: + return commitstatus.CommitStatusSkipped default: - return api.CommitStatusError + return commitstatus.CommitStatusError } } diff --git a/services/actions/interface.go b/services/actions/interface.go index b407f5c6c8..a054c38e4f 100644 --- a/services/actions/interface.go +++ b/services/actions/interface.go @@ -33,4 +33,8 @@ type API interface { GetRunner(*context.APIContext) // DeleteRunner delete runner DeleteRunner(*context.APIContext) + // ListWorkflowJobs list jobs + ListWorkflowJobs(*context.APIContext) + // ListWorkflowRuns list runs + ListWorkflowRuns(*context.APIContext) } diff --git a/services/actions/job_emitter.go b/services/actions/job_emitter.go index c11bb5875f..47c9f59094 100644 --- a/services/actions/job_emitter.go +++ b/services/actions/job_emitter.go @@ -11,6 +11,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/queue" notify_service "code.gitea.io/gitea/services/notify" @@ -78,9 +79,30 @@ func checkJobsOfRun(ctx context.Context, runID int64) error { _ = job.LoadAttributes(ctx) notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil) } + if len(jobs) > 0 { + runUpdated := true + for _, job := range jobs { + if !job.Status.IsDone() { + runUpdated = false + break + } + } + if runUpdated { + NotifyWorkflowRunStatusUpdateWithReload(ctx, jobs[0]) + } + } return nil } +func NotifyWorkflowRunStatusUpdateWithReload(ctx context.Context, job *actions_model.ActionRunJob) { + job.Run = nil + if err := job.LoadAttributes(ctx); err != nil { + log.Error("LoadAttributes: %v", err) + return + } + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) +} + type jobStatusResolver struct { statuses map[int64]actions_model.Status needs map[int64][]int64 diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 831cde3523..d10cc0ab34 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -6,13 +6,16 @@ package actions import ( "context" + actions_model "code.gitea.io/gitea/models/actions" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" packages_model "code.gitea.io/gitea/models/packages" perm_model "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" @@ -762,3 +765,41 @@ func (n *actionsNotifier) MigrateRepository(ctx context.Context, doer, u *user_m Sender: convert.ToUser(ctx, doer, nil), }).Notify(ctx) } + +func (n *actionsNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { + ctx = withMethod(ctx, "WorkflowRunStatusUpdate") + + var org *api.Organization + if repo.Owner.IsOrganization() { + org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) + } + + status := convert.ToWorkflowRunAction(run.Status) + + gitRepo, err := gitrepo.OpenRepository(ctx, repo) + if err != nil { + log.Error("OpenRepository: %v", err) + return + } + defer gitRepo.Close() + + convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + if err != nil { + log.Error("GetActionWorkflow: %v", err) + return + } + convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) + if err != nil { + log.Error("ToActionWorkflowRun: %v", err) + return + } + + newNotifyInput(repo, sender, webhook_module.HookEventWorkflowRun).WithPayload(&api.WorkflowRunPayload{ + Action: status, + Workflow: convertedWorkflow, + WorkflowRun: convertedRun, + Organization: org, + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm_model.AccessModeOwner}), + Sender: convert.ToUser(ctx, sender, nil), + }).Notify(ctx) +} diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index 54f4346a66..8010f51a86 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -33,7 +33,9 @@ import ( "github.com/nektos/act/pkg/model" ) -var methodCtxKey struct{} +type methodCtxKeyType struct{} + +var methodCtxKey methodCtxKeyType // withMethod sets the notification method that this context currently executes. // Used for debugging/ troubleshooting purposes. @@ -44,8 +46,7 @@ func withMethod(ctx context.Context, method string) context.Context { return ctx } } - // FIXME: review the use of this nolint directive - return context.WithValue(ctx, methodCtxKey, method) //nolint:staticcheck + return context.WithValue(ctx, methodCtxKey, method) } // getMethod gets the notification method that this context currently executes. @@ -178,7 +179,7 @@ func notify(ctx context.Context, input *notifyInput) error { return fmt.Errorf("gitRepo.GetCommit: %w", err) } - if skipWorkflows(input, commit) { + if skipWorkflows(ctx, input, commit) { return nil } @@ -243,7 +244,7 @@ func notify(ctx context.Context, input *notifyInput) error { return handleWorkflows(ctx, detectedWorkflows, commit, input, ref.String()) } -func skipWorkflows(input *notifyInput, commit *git.Commit) bool { +func skipWorkflows(ctx context.Context, input *notifyInput, commit *git.Commit) bool { // skip workflow runs with a configured skip-ci string in commit message or pr title if the event is push or pull_request(_sync) // https://docs.github.com/en/actions/managing-workflow-runs/skipping-workflow-runs skipWorkflowEvents := []webhook_module.HookEventType{ @@ -263,6 +264,27 @@ func skipWorkflows(input *notifyInput, commit *git.Commit) bool { } } } + if input.Event == webhook_module.HookEventWorkflowRun { + wrun, ok := input.Payload.(*api.WorkflowRunPayload) + for i := 0; i < 5 && ok && wrun.WorkflowRun != nil; i++ { + if wrun.WorkflowRun.Event != "workflow_run" { + return false + } + r, err := actions_model.GetRunByRepoAndID(ctx, input.Repo.ID, wrun.WorkflowRun.ID) + if err != nil { + log.Error("GetRunByRepoAndID: %v", err) + return true + } + wrun, err = r.GetWorkflowRunEventPayload() + if err != nil { + log.Error("GetWorkflowRunEventPayload: %v", err) + return true + } + } + // skip workflow runs events exceeding the maxiumum of 5 recursive events + log.Debug("repo %s: skipped workflow_run because of recursive event of 5", input.Repo.RepoPath()) + return true + } return false } @@ -372,6 +394,15 @@ func handleWorkflows( continue } CreateCommitStatus(ctx, alljobs...) + if len(alljobs) > 0 { + job := alljobs[0] + err := job.LoadRun(ctx) + if err != nil { + log.Error("LoadRun: %v", err) + continue + } + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } for _, job := range alljobs { notify_service.WorkflowJobStatusUpdate(ctx, input.Repo, input.Doer, job, nil) } diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index a30b166063..c029c5a1a2 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -157,6 +157,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) if err != nil { log.Error("LoadAttributes: %v", err) } + notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) for _, job := range allJobs { notify_service.WorkflowJobStatusUpdate(ctx, run.Repo, run.TriggerUser, job, nil) } diff --git a/services/actions/workflow.go b/services/actions/workflow.go index 15cebf6847..233e22b5dd 100644 --- a/services/actions/workflow.go +++ b/services/actions/workflow.go @@ -5,9 +5,6 @@ package actions import ( "fmt" - "net/http" - "net/url" - "path" "strings" actions_model "code.gitea.io/gitea/models/actions" @@ -31,61 +28,8 @@ import ( "github.com/nektos/act/pkg/model" ) -func getActionWorkflowPath(commit *git.Commit) string { - paths := []string{".gitea/workflows", ".github/workflows"} - for _, treePath := range paths { - if _, err := commit.SubTree(treePath); err == nil { - return treePath - } - } - return "" -} - -func getActionWorkflowEntry(ctx *context.APIContext, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow { - cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) - cfg := cfgUnit.ActionsConfig() - - defaultBranch, _ := commit.GetBranchName() - - workflowURL := fmt.Sprintf("%s/actions/workflows/%s", ctx.Repo.Repository.APIURL(), url.PathEscape(entry.Name())) - workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", ctx.Repo.Repository.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), url.PathEscape(entry.Name())) - badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", ctx.Repo.Repository.HTMLURL(ctx), url.PathEscape(entry.Name()), url.QueryEscape(ctx.Repo.Repository.DefaultBranch)) - - // See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow - // State types: - // - active - // - deleted - // - disabled_fork - // - disabled_inactivity - // - disabled_manually - state := "active" - if cfg.IsWorkflowDisabled(entry.Name()) { - state = "disabled_manually" - } - - // The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined - // by retrieving the first and last commits for the file history. The first commit would indicate the creation date, - // while the last commit would represent the modification date. The DeletedAt could be determined by identifying - // the last commit where the file existed. However, this implementation has not been done here yet, as it would likely - // cause a significant performance degradation. - createdAt := commit.Author.When - updatedAt := commit.Author.When - - return &api.ActionWorkflow{ - ID: entry.Name(), - Name: entry.Name(), - Path: path.Join(folder, entry.Name()), - State: state, - CreatedAt: createdAt, - UpdatedAt: updatedAt, - URL: workflowURL, - HTMLURL: workflowRepoURL, - BadgeURL: badgeURL, - } -} - func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnable bool) error { - workflow, err := GetActionWorkflow(ctx, workflowID) + workflow, err := convert.GetActionWorkflow(ctx, ctx.Repo.GitRepo, ctx.Repo.Repository, workflowID) if err != nil { return err } @@ -102,44 +46,6 @@ func EnableOrDisableWorkflow(ctx *context.APIContext, workflowID string, isEnabl return repo_model.UpdateRepoUnit(ctx, cfgUnit) } -func ListActionWorkflows(ctx *context.APIContext) ([]*api.ActionWorkflow, error) { - defaultBranchCommit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch) - if err != nil { - ctx.APIErrorInternal(err) - return nil, err - } - - entries, err := actions.ListWorkflows(defaultBranchCommit) - if err != nil { - ctx.APIError(http.StatusNotFound, err.Error()) - return nil, err - } - - folder := getActionWorkflowPath(defaultBranchCommit) - - workflows := make([]*api.ActionWorkflow, len(entries)) - for i, entry := range entries { - workflows[i] = getActionWorkflowEntry(ctx, defaultBranchCommit, folder, entry) - } - - return workflows, nil -} - -func GetActionWorkflow(ctx *context.APIContext, workflowID string) (*api.ActionWorkflow, error) { - entries, err := ListActionWorkflows(ctx) - if err != nil { - return nil, err - } - - for _, entry := range entries { - if entry.Name == workflowID { - return entry, nil - } - } - - return nil, util.NewNotExistErrorf("workflow %q not found", workflowID) -} - func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, gitRepo *git.Repository, workflowID, ref string, processInputs func(model *model.WorkflowDispatch, inputs map[string]any) error) error { if workflowID == "" { return util.ErrorWrapLocale( @@ -185,7 +91,7 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re } // get workflow entry from runTargetCommit - entries, err := actions.ListWorkflows(runTargetCommit) + _, entries, err := actions.ListWorkflows(runTargetCommit) if err != nil { return err } @@ -297,6 +203,15 @@ func DispatchActionWorkflow(ctx reqctx.RequestContext, doer *user_model.User, re log.Error("FindRunJobs: %v", err) } CreateCommitStatus(ctx, allJobs...) + if len(allJobs) > 0 { + job := allJobs[0] + err := job.LoadRun(ctx) + if err != nil { + log.Error("LoadRun: %v", err) + } else { + notify_service.WorkflowRunStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job.Run) + } + } for _, job := range allJobs { notify_service.WorkflowJobStatusUpdate(ctx, repo, doer, job, nil) } diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 105782a93a..148f51fd10 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -6,6 +6,7 @@ package asymkey import ( "context" "fmt" + "slices" "strings" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -359,24 +360,39 @@ func VerifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, si return nil } +func verifySSHCommitVerificationByInstanceKey(c *git.Commit, committerUser, signerUser *user_model.User, committerGitEmail, publicKeyContent string) *asymkey_model.CommitVerification { + fingerprint, err := asymkey_model.CalcFingerprint(publicKeyContent) + if err != nil { + log.Error("Error calculating the fingerprint public key %q, err: %v", publicKeyContent, err) + return nil + } + sshPubKey := &asymkey_model.PublicKey{ + Verified: true, + Content: publicKeyContent, + Fingerprint: fingerprint, + HasUsed: true, + } + return verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, sshPubKey, committerUser, signerUser, committerGitEmail) +} + // ParseCommitWithSSHSignature check if signature is good against keystore. -func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification { +func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUser *user_model.User) *asymkey_model.CommitVerification { // Now try to associate the signature with the committer, if present - if committer.ID != 0 { + if committerUser.ID != 0 { keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ - OwnerID: committer.ID, + OwnerID: committerUser.ID, NotKeytype: asymkey_model.KeyTypePrincipal, }) if err != nil { // Skipping failed to get ssh keys of user log.Error("ListPublicKeys: %v", err) return &asymkey_model.CommitVerification{ - CommittingUser: committer, + CommittingUser: committerUser, Verified: false, Reason: "gpg.error.failed_retrieval_gpg_keys", } } - committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committer.ID, user_model.GetEmailAddresses) + committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committerUser.ID, user_model.GetEmailAddresses) if err != nil { log.Error("GetEmailAddresses: %v", err) } @@ -391,7 +407,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * for _, k := range keys { if k.Verified && activated { - commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email) + commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committerUser, committerUser, c.Committer.Email) if commitVerification != nil { return commitVerification } @@ -399,8 +415,45 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * } } + // Try the pre-set trusted keys (for key-rotation purpose) + // At the moment, we still use the SigningName&SigningEmail for the rotated keys. + // Maybe in the future we can extend the key format to "ssh-xxx .... old-user@example.com" to support different signer emails. + for _, k := range setting.Repository.Signing.TrustedSSHKeys { + signerUser := &user_model.User{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } + commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, c.Committer.Email, k) + if commitVerification != nil && commitVerification.Verified { + return commitVerification + } + } + + // Try the configured instance-wide SSH public key + if setting.Repository.Signing.SigningFormat == git.SigningKeyFormatSSH && !slices.Contains([]string{"", "default", "none"}, setting.Repository.Signing.SigningKey) { + gpgSettings := git.GPGSettings{ + Sign: true, + KeyID: setting.Repository.Signing.SigningKey, + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + Format: setting.Repository.Signing.SigningFormat, + } + signerUser := &user_model.User{ + Name: gpgSettings.Name, + Email: gpgSettings.Email, + } + if err := gpgSettings.LoadPublicKeyContent(); err != nil { + log.Error("Error getting instance-wide SSH signing key %q, err: %v", gpgSettings.KeyID, err) + } else { + commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, gpgSettings.Email, gpgSettings.PublicKeyContent) + if commitVerification != nil && commitVerification.Verified { + return commitVerification + } + } + } + return &asymkey_model.CommitVerification{ - CommittingUser: committer, + CommittingUser: committerUser, Verified: false, Reason: asymkey_model.NoKeyFound, } diff --git a/services/asymkey/commit_test.go b/services/asymkey/commit_test.go new file mode 100644 index 0000000000..0438209a61 --- /dev/null +++ b/services/asymkey/commit_test.go @@ -0,0 +1,54 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package asymkey + +import ( + "strings" + "testing" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseCommitWithSSHSignature(t *testing.T) { + // Here we only test the TrustedSSHKeys. The complete signing test is in tests/integration/gpg_ssh_git_test.go + t.Run("TrustedSSHKey", func(t *testing.T) { + defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")() + defer test.MockVariableValue(&setting.Repository.Signing.TrustedSSHKeys, []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH6Y4idVaW3E+bLw1uqoAfJD7o5Siu+HqS51E9oQLPE9"})() + + commit, err := git.CommitFromReader(nil, git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree 9a93ffa76e8b72bdb6431910b3a506fa2b39f42e +author User Two <user2@example.com> 1749230009 +0200 +committer User Two <user2@example.com> 1749230009 +0200 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgfpjiJ1VpbcT5svDW6qgB8kPujl + KK74epLnUT2hAs8T0AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 + AAAAQDX2t2iHuuLxEWHLJetYXKsgayv3c43r0pJNfAzdLN55Q65pC5M7rG6++gT2bxcpOu + Y6EXbpLqia9sunEF3+LQY= + -----END SSH SIGNATURE----- + +Initial commit with signed file +`)) + require.NoError(t, err) + committingUser := &user_model.User{ + ID: 2, + Name: "User Two", + Email: "user2@example.com", + } + ret := ParseCommitWithSSHSignature(t.Context(), commit, committingUser) + require.NotNil(t, ret) + assert.True(t, ret.Verified) + assert.False(t, ret.Warning) + assert.Equal(t, committingUser, ret.CommittingUser) + if assert.NotNil(t, ret.SigningUser) { + assert.Equal(t, "gitea", ret.SigningUser.Name) + assert.Equal(t, "gitea@fake.local", ret.SigningUser.Email) + } + }) +} diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index 2216bca54a..f94462ea46 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -6,6 +6,7 @@ package asymkey import ( "context" "fmt" + "os" "strings" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -85,9 +86,9 @@ func IsErrWontSign(err error) bool { } // SigningKey returns the KeyID and git Signature for the repo -func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) { +func SigningKey(ctx context.Context, repoPath string) (*git.SigningKey, *git.Signature) { if setting.Repository.Signing.SigningKey == "none" { - return "", nil + return nil, nil } if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { @@ -95,53 +96,77 @@ func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) { value, _, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) sign, valid := git.ParseBool(strings.TrimSpace(value)) if !sign || !valid { - return "", nil + return nil, nil } + format, _, _ := git.NewCommand("config", "--default", git.SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) signingKey, _, _ := git.NewCommand("config", "--get", "user.signingkey").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) signingName, _, _ := git.NewCommand("config", "--get", "user.name").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) signingEmail, _, _ := git.NewCommand("config", "--get", "user.email").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) - return strings.TrimSpace(signingKey), &git.Signature{ - Name: strings.TrimSpace(signingName), - Email: strings.TrimSpace(signingEmail), + + if strings.TrimSpace(signingKey) == "" { + return nil, nil } + + return &git.SigningKey{ + KeyID: strings.TrimSpace(signingKey), + Format: strings.TrimSpace(format), + }, &git.Signature{ + Name: strings.TrimSpace(signingName), + Email: strings.TrimSpace(signingEmail), + } } - return setting.Repository.Signing.SigningKey, &git.Signature{ - Name: setting.Repository.Signing.SigningName, - Email: setting.Repository.Signing.SigningEmail, + if setting.Repository.Signing.SigningKey == "" { + return nil, nil } + + return &git.SigningKey{ + KeyID: setting.Repository.Signing.SigningKey, + Format: setting.Repository.Signing.SigningFormat, + }, &git.Signature{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } } // PublicSigningKey gets the public signing key within a provided repository directory -func PublicSigningKey(ctx context.Context, repoPath string) (string, error) { +func PublicSigningKey(ctx context.Context, repoPath string) (content, format string, err error) { signingKey, _ := SigningKey(ctx, repoPath) - if signingKey == "" { - return "", nil + if signingKey == nil { + return "", "", nil + } + if signingKey.Format == git.SigningKeyFormatSSH { + content, err := os.ReadFile(signingKey.KeyID) + if err != nil { + log.Error("Unable to read SSH public key file in %s: %s, %v", repoPath, signingKey, err) + return "", signingKey.Format, err + } + return string(content), signingKey.Format, nil } content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath, - "gpg --export -a", "gpg", "--export", "-a", signingKey) + "gpg --export -a", "gpg", "--export", "-a", signingKey.KeyID) if err != nil { log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err) - return "", err + return "", signingKey.Format, err } - return content, nil + return content, signingKey.Format, nil } // SignInitialCommit determines if we should sign the initial commit to this repository -func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, string, *git.Signature, error) { +func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit) signingKey, sig := SigningKey(ctx, repoPath) - if signingKey == "" { - return false, "", nil, &ErrWontSign{noKey} + if signingKey == nil { + return false, nil, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, "", nil, &ErrWontSign{never} + return false, nil, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -150,18 +175,18 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, "", nil, err + return false, nil, nil, err } if len(keys) == 0 { - return false, "", nil, &ErrWontSign{pubkey} + return false, nil, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, "", nil, err + return false, nil, nil, err } if twofaModel == nil { - return false, "", nil, &ErrWontSign{twofa} + return false, nil, nil, &ErrWontSign{twofa} } } } @@ -169,19 +194,19 @@ Loop: } // SignWikiCommit determines if we should sign the commits to this repository wiki -func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, string, *git.Signature, error) { +func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) { repoWikiPath := repo.WikiPath() rules := signingModeFromStrings(setting.Repository.Signing.Wiki) signingKey, sig := SigningKey(ctx, repoWikiPath) - if signingKey == "" { - return false, "", nil, &ErrWontSign{noKey} + if signingKey == nil { + return false, nil, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, "", nil, &ErrWontSign{never} + return false, nil, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -190,35 +215,35 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, "", nil, err + return false, nil, nil, err } if len(keys) == 0 { - return false, "", nil, &ErrWontSign{pubkey} + return false, nil, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, "", nil, err + return false, nil, nil, err } if twofaModel == nil { - return false, "", nil, &ErrWontSign{twofa} + return false, nil, nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo()) if err != nil { - return false, "", nil, err + return false, nil, nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit("HEAD") if err != nil { - return false, "", nil, err + return false, nil, nil, err } if commit.Signature == nil { - return false, "", nil, &ErrWontSign{parentSigned} + return false, nil, nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{parentSigned} + return false, nil, nil, &ErrWontSign{parentSigned} } } } @@ -226,18 +251,18 @@ Loop: } // SignCRUDAction determines if we should sign a CRUD commit to this repository -func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) { +func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) signingKey, sig := SigningKey(ctx, repoPath) - if signingKey == "" { - return false, "", nil, &ErrWontSign{noKey} + if signingKey == nil { + return false, nil, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, "", nil, &ErrWontSign{never} + return false, nil, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -246,35 +271,35 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, "", nil, err + return false, nil, nil, err } if len(keys) == 0 { - return false, "", nil, &ErrWontSign{pubkey} + return false, nil, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, "", nil, err + return false, nil, nil, err } if twofaModel == nil { - return false, "", nil, &ErrWontSign{twofa} + return false, nil, nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, "", nil, err + return false, nil, nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit(parentCommit) if err != nil { - return false, "", nil, err + return false, nil, nil, err } if commit.Signature == nil { - return false, "", nil, &ErrWontSign{parentSigned} + return false, nil, nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{parentSigned} + return false, nil, nil, &ErrWontSign{parentSigned} } } } @@ -282,16 +307,16 @@ Loop: } // SignMerge determines if we should sign a PR merge commit to the base repository -func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) { +func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, *git.SigningKey, *git.Signature, error) { if err := pr.LoadBaseRepo(ctx); err != nil { log.Error("Unable to get Base Repo for pull request") - return false, "", nil, err + return false, nil, nil, err } repo := pr.BaseRepo signingKey, signer := SigningKey(ctx, repo.RepoPath()) - if signingKey == "" { - return false, "", nil, &ErrWontSign{noKey} + if signingKey == nil { + return false, nil, nil, &ErrWontSign{noKey} } rules := signingModeFromStrings(setting.Repository.Signing.Merges) @@ -302,7 +327,7 @@ Loop: for _, rule := range rules { switch rule { case never: - return false, "", nil, &ErrWontSign{never} + return false, nil, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -311,91 +336,91 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, "", nil, err + return false, nil, nil, err } if len(keys) == 0 { - return false, "", nil, &ErrWontSign{pubkey} + return false, nil, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, "", nil, err + return false, nil, nil, err } if twofaModel == nil { - return false, "", nil, &ErrWontSign{twofa} + return false, nil, nil, &ErrWontSign{twofa} } case approved: protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch) if err != nil { - return false, "", nil, err + return false, nil, nil, err } if protectedBranch == nil { - return false, "", nil, &ErrWontSign{approved} + return false, nil, nil, &ErrWontSign{approved} } if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 { - return false, "", nil, &ErrWontSign{approved} + return false, nil, nil, &ErrWontSign{approved} } case baseSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, "", nil, err + return false, nil, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(baseCommit) if err != nil { - return false, "", nil, err + return false, nil, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{baseSigned} + return false, nil, nil, &ErrWontSign{baseSigned} } case headSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, "", nil, err + return false, nil, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, "", nil, err + return false, nil, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{headSigned} + return false, nil, nil, &ErrWontSign{headSigned} } case commitsSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, "", nil, err + return false, nil, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, "", nil, err + return false, nil, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{commitsSigned} + return false, nil, nil, &ErrWontSign{commitsSigned} } // need to work out merge-base mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit) if err != nil { - return false, "", nil, err + return false, nil, nil, err } commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit) if err != nil { - return false, "", nil, err + return false, nil, nil, err } for _, commit := range commitList { verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{commitsSigned} + return false, nil, nil, &ErrWontSign{commitsSigned} } } } diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index a2e8c2b86a..6005a4744a 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -58,7 +58,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u opts := &user_service.UpdateOptions{} if source.AdminFilter != "" && user.IsAdmin != sr.IsAdmin { // Change existing admin flag only if AdminFilter option is set - opts.IsAdmin = optional.Some(sr.IsAdmin) + opts.IsAdmin = user_service.UpdateOptionFieldFromSync(sr.IsAdmin) } if !sr.IsAdmin && source.RestrictedFilter != "" && user.IsRestricted != sr.IsRestricted { // Change existing restricted flag only if RestrictedFilter option is set diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go index fa2c45ce4a..e6bce04a83 100644 --- a/services/auth/source/ldap/source_search.go +++ b/services/auth/source/ldap/source_search.go @@ -117,10 +117,10 @@ func dial(source *Source) (*ldap.Conn, error) { } if source.SecurityProtocol == SecurityProtocolLDAPS { - return ldap.DialTLS("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port)), tlsConfig) //nolint:staticcheck + return ldap.DialTLS("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port)), tlsConfig) //nolint:staticcheck // DialTLS is deprecated } - conn, err := ldap.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port))) //nolint:staticcheck + conn, err := ldap.Dial("tcp", net.JoinHostPort(source.Host, strconv.Itoa(source.Port))) //nolint:staticcheck // Dial is deprecated if err != nil { return nil, fmt.Errorf("error during Dial: %w", err) } diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index 678b6b2b62..7b401c5c96 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -162,7 +162,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { IsActive: optional.Some(true), } if source.AdminFilter != "" { - opts.IsAdmin = optional.Some(su.IsAdmin) + opts.IsAdmin = user_service.UpdateOptionFieldFromSync(su.IsAdmin) } // Change existing restricted flag only if RestrictedFilter option is set if !su.IsAdmin && source.RestrictedFilter != "" { @@ -178,8 +178,9 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } } - if usr.IsUploadAvatarChanged(su.Avatar) { - if err == nil && source.AttributeAvatar != "" { + if source.AttributeAvatar != "" { + if len(su.Avatar) > 0 && usr.IsUploadAvatarChanged(su.Avatar) { + log.Trace("SyncExternalUsers[%s]: Uploading new avatar for %s", source.AuthSource.Name, usr.Name) _ = user_service.UploadAvatar(ctx, usr, su.Avatar) } } diff --git a/services/auth/source/oauth2/urlmapping.go b/services/auth/source/oauth2/urlmapping.go index d0442d58a8..b9f445caa7 100644 --- a/services/auth/source/oauth2/urlmapping.go +++ b/services/auth/source/oauth2/urlmapping.go @@ -14,11 +14,11 @@ type CustomURLMapping struct { // CustomURLSettings describes the urls values and availability to use when customizing OAuth2 provider URLs type CustomURLSettings struct { - AuthURL Attribute `json:",omitempty"` - TokenURL Attribute `json:",omitempty"` - ProfileURL Attribute `json:",omitempty"` - EmailURL Attribute `json:",omitempty"` - Tenant Attribute `json:",omitempty"` + AuthURL Attribute + TokenURL Attribute + ProfileURL Attribute + EmailURL Attribute + Tenant Attribute } // Attribute describes the availability, and required status for a custom url configuration diff --git a/services/context/api.go b/services/context/api.go index 28f0e43d88..ab50a360f4 100644 --- a/services/context/api.go +++ b/services/context/api.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "net/url" + "slices" "strconv" "strings" @@ -364,11 +365,5 @@ func (ctx *APIContext) IsUserRepoAdmin() bool { // IsUserRepoWriter returns true if current user has "write" privilege in current repo func (ctx *APIContext) IsUserRepoWriter(unitTypes []unit.Type) bool { - for _, unitType := range unitTypes { - if ctx.Repo.CanWrite(unitType) { - return true - } - } - - return false + return slices.ContainsFunc(unitTypes, ctx.Repo.CanWrite) } diff --git a/services/context/base_form.go b/services/context/base_form.go index 5b8cae9e99..81fd7cd328 100644 --- a/services/context/base_form.go +++ b/services/context/base_form.go @@ -12,6 +12,8 @@ import ( ) // FormString returns the first value matching the provided key in the form as a string +// It works the same as http.Request.FormValue: +// try urlencoded request body first, then query string, then multipart form body func (b *Base) FormString(key string, def ...string) string { s := b.Req.FormValue(key) if s == "" { @@ -20,7 +22,7 @@ func (b *Base) FormString(key string, def ...string) string { return s } -// FormStrings returns a string slice for the provided key from the form +// FormStrings returns a values for the key in the form (including query parameters), similar to FormString func (b *Base) FormStrings(key string) []string { if b.Req.Form == nil { if err := b.Req.ParseMultipartForm(32 << 20); err != nil { diff --git a/services/context/permission.go b/services/context/permission.go index 7055f798da..c0a5a98724 100644 --- a/services/context/permission.go +++ b/services/context/permission.go @@ -5,6 +5,7 @@ package context import ( "net/http" + "slices" auth_model "code.gitea.io/gitea/models/auth" repo_model "code.gitea.io/gitea/models/repo" @@ -34,10 +35,8 @@ func CanWriteToBranch() func(ctx *Context) { // RequireUnitWriter returns a middleware for requiring repository write to one of the unit permission func RequireUnitWriter(unitTypes ...unit.Type) func(ctx *Context) { return func(ctx *Context) { - for _, unitType := range unitTypes { - if ctx.Repo.CanWrite(unitType) { - return - } + if slices.ContainsFunc(unitTypes, ctx.Repo.CanWrite) { + return } ctx.NotFound(nil) } diff --git a/services/context/private.go b/services/context/private.go index 3f7637518b..d20e49f588 100644 --- a/services/context/private.go +++ b/services/context/private.go @@ -28,7 +28,6 @@ func init() { }) } -// Deadline is part of the interface for context.Context and we pass this to the request context func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) { if ctx.Override != nil { return ctx.Override.Deadline() @@ -36,7 +35,6 @@ func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) { return ctx.Base.Deadline() } -// Done is part of the interface for context.Context and we pass this to the request context func (ctx *PrivateContext) Done() <-chan struct{} { if ctx.Override != nil { return ctx.Override.Done() @@ -44,7 +42,6 @@ func (ctx *PrivateContext) Done() <-chan struct{} { return ctx.Base.Done() } -// Err is part of the interface for context.Context and we pass this to the request context func (ctx *PrivateContext) Err() error { if ctx.Override != nil { return ctx.Override.Err() @@ -52,14 +49,14 @@ func (ctx *PrivateContext) Err() error { return ctx.Base.Err() } -var privateContextKey any = "default_private_context" +type privateContextKeyType struct{} + +var privateContextKey privateContextKeyType -// GetPrivateContext returns a context for Private routes func GetPrivateContext(req *http.Request) *PrivateContext { return req.Context().Value(privateContextKey).(*PrivateContext) } -// PrivateContexter returns apicontext as middleware func PrivateContexter() func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { diff --git a/services/context/repo.go b/services/context/repo.go index ea772c508d..572211712b 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -71,11 +71,6 @@ func (r *Repository) CanWriteToBranch(ctx context.Context, user *user_model.User return issues_model.CanMaintainerWriteToBranch(ctx, r.Permission, branch, user) } -// CanEnableEditor returns true if repository is editable and user has proper access level. -func (r *Repository) CanEnableEditor(ctx context.Context, user *user_model.User) bool { - return r.RefFullName.IsBranch() && r.CanWriteToBranch(ctx, user, r.BranchName) && r.Repository.CanEnableEditor() && !r.Repository.IsArchived -} - // CanCreateBranch returns true if repository is editable and user has proper access level. func (r *Repository) CanCreateBranch() bool { return r.Permission.CanWrite(unit_model.TypeCode) && r.Repository.CanCreateBranch() @@ -94,58 +89,100 @@ func RepoMustNotBeArchived() func(ctx *Context) { } } -// CanCommitToBranchResults represents the results of CanCommitToBranch -type CanCommitToBranchResults struct { - CanCommitToBranch bool - EditorEnabled bool - UserCanPush bool - RequireSigned bool - WillSign bool - SigningKey string - WontSignReason string +type CommitFormOptions struct { + NeedFork bool + + TargetRepo *repo_model.Repository + TargetFormAction string + WillSubmitToFork bool + CanCommitToBranch bool + UserCanPush bool + RequireSigned bool + WillSign bool + SigningKey *git.SigningKey + WontSignReason string + CanCreatePullRequest bool + CanCreateBasePullRequest bool } -// CanCommitToBranch returns true if repository is editable and user has proper access level -// -// and branch is not protected for push -func (r *Repository) CanCommitToBranch(ctx context.Context, doer *user_model.User) (CanCommitToBranchResults, error) { - protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, r.Repository.ID, r.BranchName) +func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *repo_model.Repository, doerRepoPerm access_model.Permission, refName git.RefName) (*CommitFormOptions, error) { + if !refName.IsBranch() { + // it shouldn't happen because middleware already checks + return nil, util.NewInvalidArgumentErrorf("ref %q is not a branch", refName) + } + + originRepo := targetRepo + branchName := refName.ShortName() + // TODO: CanMaintainerWriteToBranch is a bad name, but it really does what "CanWriteToBranch" does + if !issues_model.CanMaintainerWriteToBranch(ctx, doerRepoPerm, branchName, doer) { + targetRepo = repo_model.GetForkedRepo(ctx, doer.ID, targetRepo.ID) + if targetRepo == nil { + return &CommitFormOptions{NeedFork: true}, nil + } + // now, we get our own forked repo; it must be writable by us. + } + submitToForkedRepo := targetRepo.ID != originRepo.ID + err := targetRepo.GetBaseRepo(ctx) + if err != nil { + return nil, err + } + + protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, targetRepo.ID, branchName) if err != nil { - return CanCommitToBranchResults{}, err + return nil, err } - userCanPush := true - requireSigned := false + canPushWithProtection := true + protectionRequireSigned := false if protectedBranch != nil { - protectedBranch.Repo = r.Repository - userCanPush = protectedBranch.CanUserPush(ctx, doer) - requireSigned = protectedBranch.RequireSignedCommits + protectedBranch.Repo = targetRepo + canPushWithProtection = protectedBranch.CanUserPush(ctx, doer) + protectionRequireSigned = protectedBranch.RequireSignedCommits } - sign, keyID, _, err := asymkey_service.SignCRUDAction(ctx, r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) - - canCommit := r.CanEnableEditor(ctx, doer) && userCanPush - if requireSigned { - canCommit = canCommit && sign - } + willSign, signKeyID, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String()) wontSignReason := "" - if err != nil { - if asymkey_service.IsErrWontSign(err) { - wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) - err = nil - } else { - wontSignReason = "error" - } + if asymkey_service.IsErrWontSign(err) { + wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) + } else if err != nil { + return nil, err } - return CanCommitToBranchResults{ - CanCommitToBranch: canCommit, - EditorEnabled: r.CanEnableEditor(ctx, doer), - UserCanPush: userCanPush, - RequireSigned: requireSigned, - WillSign: sign, - SigningKey: keyID, + canCommitToBranch := !submitToForkedRepo /* same repo */ && targetRepo.CanEnableEditor() && canPushWithProtection + if protectionRequireSigned { + canCommitToBranch = canCommitToBranch && willSign + } + + canCreateBasePullRequest := targetRepo.BaseRepo != nil && targetRepo.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) + canCreatePullRequest := targetRepo.UnitEnabled(ctx, unit_model.TypePullRequests) || canCreateBasePullRequest + + opts := &CommitFormOptions{ + TargetRepo: targetRepo, + WillSubmitToFork: submitToForkedRepo, + CanCommitToBranch: canCommitToBranch, + UserCanPush: canPushWithProtection, + RequireSigned: protectionRequireSigned, + WillSign: willSign, + SigningKey: signKeyID, WontSignReason: wontSignReason, - }, err + + CanCreatePullRequest: canCreatePullRequest, + CanCreateBasePullRequest: canCreateBasePullRequest, + } + editorAction := ctx.PathParam("editor_action") + editorPathParamRemaining := util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + if submitToForkedRepo { + // there is only "default branch" in forked repo, we will use "from_base_branch" to get a new branch from base repo + editorPathParamRemaining = util.PathEscapeSegments(targetRepo.DefaultBranch) + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + "?from_base_branch=" + url.QueryEscape(branchName) + } + if editorAction == "_cherrypick" { + opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + ctx.PathParam("sha") + "/" + editorPathParamRemaining + } else { + opts.TargetFormAction = targetRepo.Link() + "/" + editorAction + "/" + editorPathParamRemaining + } + if ctx.Req.URL.RawQuery != "" { + opts.TargetFormAction += util.Iif(strings.Contains(opts.TargetFormAction, "?"), "&", "?") + ctx.Req.URL.RawQuery + } + return opts, nil } // CanUseTimetracker returns whether a user can use the timetracker. @@ -936,6 +973,15 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) { ctx.ServerError("GetCommitsCount", err) return } + if ctx.Repo.RefFullName.IsTag() { + rel, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Repo.RefFullName.TagName()) + if err == nil && rel.NumCommits <= 0 { + rel.NumCommits = ctx.Repo.CommitsCount + if err := repo_model.UpdateReleaseNumCommits(ctx, rel); err != nil { + log.Error("UpdateReleaseNumCommits", err) + } + } + } ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount ctx.Repo.GitRepo.LastCommitCache = git.NewLastCommitCache(ctx.Repo.CommitsCount, ctx.Repo.Repository.FullName(), ctx.Repo.GitRepo, cache.GetCache()) } diff --git a/services/context/upload/upload.go b/services/context/upload/upload.go index 12aa485aa7..23707950d4 100644 --- a/services/context/upload/upload.go +++ b/services/context/upload/upload.go @@ -11,7 +11,9 @@ import ( "regexp" "strings" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/services/context" ) @@ -39,7 +41,7 @@ func Verify(buf []byte, fileName, allowedTypesStr string) error { allowedTypesStr = strings.ReplaceAll(allowedTypesStr, "|", ",") // compat for old config format allowedTypes := []string{} - for _, entry := range strings.Split(allowedTypesStr, ",") { + for entry := range strings.SplitSeq(allowedTypesStr, ",") { entry = strings.ToLower(strings.TrimSpace(entry)) if entry != "" { allowedTypes = append(allowedTypes, entry) @@ -106,12 +108,17 @@ func AddUploadContext(ctx *context.Context, uploadType string) { ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Attachment.AllowedTypes, "|", ",") ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize - case "repo": - ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/upload-file" - ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/upload-remove" - ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/upload-file" - ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",") - ctx.Data["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles - ctx.Data["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize + default: + setting.PanicInDevOrTesting("Invalid upload type: %s", uploadType) } } + +func AddUploadContextForRepo(ctx reqctx.RequestContext, repo *repo_model.Repository) { + ctxData, repoLink := ctx.GetData(), repo.Link() + ctxData["UploadUrl"] = repoLink + "/upload-file" + ctxData["UploadRemoveUrl"] = repoLink + "/upload-remove" + ctxData["UploadLinkUrl"] = repoLink + "/upload-file" + ctxData["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Upload.AllowedTypes, "|", ",") + ctxData["UploadMaxFiles"] = setting.Repository.Upload.MaxFiles + ctxData["UploadMaxSize"] = setting.Repository.Upload.FileMaxSize +} diff --git a/services/convert/convert.go b/services/convert/convert.go index b93365bbb9..0de3822140 100644 --- a/services/convert/convert.go +++ b/services/convert/convert.go @@ -5,8 +5,11 @@ package convert import ( + "bytes" "context" "fmt" + "net/url" + "path" "strconv" "strings" "time" @@ -14,6 +17,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" @@ -22,6 +26,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" @@ -32,6 +37,7 @@ import ( "code.gitea.io/gitea/services/gitdiff" runnerv1 "code.gitea.io/actions-proto-go/runner/v1" + "github.com/nektos/act/pkg/model" ) // ToEmail convert models.EmailAddress to api.Email @@ -143,7 +149,7 @@ func ToBranchProtection(ctx context.Context, bp *git_model.ProtectedBranch, repo mergeWhitelistUsernames := getWhitelistEntities(readers, bp.MergeWhitelistUserIDs) approvalsWhitelistUsernames := getWhitelistEntities(readers, bp.ApprovalsWhitelistUserIDs) - teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead) + teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests) if err != nil { log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err) } @@ -241,6 +247,242 @@ func ToActionTask(ctx context.Context, t *actions_model.ActionTask) (*api.Action }, nil } +func ToActionWorkflowRun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun) (*api.ActionWorkflowRun, error) { + err := run.LoadAttributes(ctx) + if err != nil { + return nil, err + } + status, conclusion := ToActionsStatus(run.Status) + return &api.ActionWorkflowRun{ + ID: run.ID, + URL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), run.ID), + HTMLURL: run.HTMLURL(), + RunNumber: run.Index, + StartedAt: run.Started.AsLocalTime(), + CompletedAt: run.Stopped.AsLocalTime(), + Event: string(run.Event), + DisplayTitle: run.Title, + HeadBranch: git.RefName(run.Ref).BranchName(), + HeadSha: run.CommitSHA, + Status: status, + Conclusion: conclusion, + Path: fmt.Sprintf("%s@%s", run.WorkflowID, run.Ref), + Repository: ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeNone}), + TriggerActor: ToUser(ctx, run.TriggerUser, nil), + // We do not have a way to get a different User for the actor than the trigger user + Actor: ToUser(ctx, run.TriggerUser, nil), + }, nil +} + +func ToWorkflowRunAction(status actions_model.Status) string { + var action string + switch status { + case actions_model.StatusWaiting, actions_model.StatusBlocked: + action = "requested" + case actions_model.StatusRunning: + action = "in_progress" + } + if status.IsDone() { + action = "completed" + } + return action +} + +func ToActionsStatus(status actions_model.Status) (string, string) { + var action string + var conclusion string + switch status { + // This is a naming conflict of the webhook between Gitea and GitHub Actions + case actions_model.StatusWaiting: + action = "queued" + case actions_model.StatusBlocked: + action = "waiting" + case actions_model.StatusRunning: + action = "in_progress" + } + if status.IsDone() { + action = "completed" + switch status { + case actions_model.StatusSuccess: + conclusion = "success" + case actions_model.StatusCancelled: + conclusion = "cancelled" + case actions_model.StatusFailure: + conclusion = "failure" + case actions_model.StatusSkipped: + conclusion = "skipped" + } + } + return action, conclusion +} + +// ToActionWorkflowJob convert a actions_model.ActionRunJob to an api.ActionWorkflowJob +// task is optional and can be nil +func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task *actions_model.ActionTask, job *actions_model.ActionRunJob) (*api.ActionWorkflowJob, error) { + err := job.LoadAttributes(ctx) + if err != nil { + return nil, err + } + + jobIndex := 0 + jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID) + if err != nil { + return nil, err + } + for i, j := range jobs { + if j.ID == job.ID { + jobIndex = i + break + } + } + + status, conclusion := ToActionsStatus(job.Status) + var runnerID int64 + var runnerName string + var steps []*api.ActionWorkflowStep + + if job.TaskID != 0 { + if task == nil { + task, _, err = db.GetByID[actions_model.ActionTask](ctx, job.TaskID) + if err != nil { + return nil, err + } + } + + runnerID = task.RunnerID + if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok { + runnerName = runner.Name + } + for i, step := range task.Steps { + stepStatus, stepConclusion := ToActionsStatus(job.Status) + steps = append(steps, &api.ActionWorkflowStep{ + Name: step.Name, + Number: int64(i), + Status: stepStatus, + Conclusion: stepConclusion, + StartedAt: step.Started.AsTime().UTC(), + CompletedAt: step.Stopped.AsTime().UTC(), + }) + } + } + + return &api.ActionWorkflowJob{ + ID: job.ID, + // missing api endpoint for this location + URL: fmt.Sprintf("%s/actions/jobs/%d", repo.APIURL(), job.ID), + HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex), + RunID: job.RunID, + // Missing api endpoint for this location, artifacts are available under a nested url + RunURL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID), + Name: job.Name, + Labels: job.RunsOn, + RunAttempt: job.Attempt, + HeadSha: job.Run.CommitSHA, + HeadBranch: git.RefName(job.Run.Ref).BranchName(), + Status: status, + Conclusion: conclusion, + RunnerID: runnerID, + RunnerName: runnerName, + Steps: steps, + CreatedAt: job.Created.AsTime().UTC(), + StartedAt: job.Started.AsTime().UTC(), + CompletedAt: job.Stopped.AsTime().UTC(), + }, nil +} + +func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, commit *git.Commit, folder string, entry *git.TreeEntry) *api.ActionWorkflow { + cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) + cfg := cfgUnit.ActionsConfig() + + defaultBranch, _ := commit.GetBranchName() + + workflowURL := fmt.Sprintf("%s/actions/workflows/%s", repo.APIURL(), util.PathEscapeSegments(entry.Name())) + workflowRepoURL := fmt.Sprintf("%s/src/branch/%s/%s/%s", repo.HTMLURL(ctx), util.PathEscapeSegments(defaultBranch), util.PathEscapeSegments(folder), util.PathEscapeSegments(entry.Name())) + badgeURL := fmt.Sprintf("%s/actions/workflows/%s/badge.svg?branch=%s", repo.HTMLURL(ctx), util.PathEscapeSegments(entry.Name()), url.QueryEscape(repo.DefaultBranch)) + + // See https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#get-a-workflow + // State types: + // - active + // - deleted + // - disabled_fork + // - disabled_inactivity + // - disabled_manually + state := "active" + if cfg.IsWorkflowDisabled(entry.Name()) { + state = "disabled_manually" + } + + // The CreatedAt and UpdatedAt fields currently reflect the timestamp of the latest commit, which can later be refined + // by retrieving the first and last commits for the file history. The first commit would indicate the creation date, + // while the last commit would represent the modification date. The DeletedAt could be determined by identifying + // the last commit where the file existed. However, this implementation has not been done here yet, as it would likely + // cause a significant performance degradation. + createdAt := commit.Author.When + updatedAt := commit.Author.When + + content, err := actions.GetContentFromEntry(entry) + name := entry.Name() + if err == nil { + workflow, err := model.ReadWorkflow(bytes.NewReader(content)) + if err == nil { + // Only use the name when specified in the workflow file + if workflow.Name != "" { + name = workflow.Name + } + } else { + log.Error("getActionWorkflowEntry: Failed to parse workflow: %v", err) + } + } else { + log.Error("getActionWorkflowEntry: Failed to get content from entry: %v", err) + } + + return &api.ActionWorkflow{ + ID: entry.Name(), + Name: name, + Path: path.Join(folder, entry.Name()), + State: state, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + URL: workflowURL, + HTMLURL: workflowRepoURL, + BadgeURL: badgeURL, + } +} + +func ListActionWorkflows(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository) ([]*api.ActionWorkflow, error) { + defaultBranchCommit, err := gitrepo.GetBranchCommit(repo.DefaultBranch) + if err != nil { + return nil, err + } + + folder, entries, err := actions.ListWorkflows(defaultBranchCommit) + if err != nil { + return nil, err + } + + workflows := make([]*api.ActionWorkflow, len(entries)) + for i, entry := range entries { + workflows[i] = getActionWorkflowEntry(ctx, repo, defaultBranchCommit, folder, entry) + } + + return workflows, nil +} + +func GetActionWorkflow(ctx context.Context, gitrepo *git.Repository, repo *repo_model.Repository, workflowID string) (*api.ActionWorkflow, error) { + entries, err := ListActionWorkflows(ctx, gitrepo, repo) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if entry.ID == workflowID { + return entry, nil + } + } + + return nil, util.NewNotExistErrorf("workflow %q not found", workflowID) +} + // ToActionArtifact convert a actions_model.ActionArtifact to an api.ActionArtifact func ToActionArtifact(repo *repo_model.Repository, art *actions_model.ActionArtifact) (*api.ActionArtifact, error) { url := fmt.Sprintf("%s/actions/artifacts/%d", repo.APIURL(), art.ID) @@ -485,7 +727,7 @@ func ToTagProtection(ctx context.Context, pt *git_model.ProtectedTag, repo *repo whitelistUsernames := getWhitelistEntities(readers, pt.AllowlistUserIDs) - teamReaders, err := organization.OrgFromUser(repo.Owner).TeamsWithAccessToRepo(ctx, repo.ID, perm.AccessModeRead) + teamReaders, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.Owner.ID, repo.ID, perm.AccessModeRead, unit.TypeCode, unit.TypePullRequests) if err != nil { log.Error("Repo.Owner.TeamsWithAccessToRepo: %v", err) } diff --git a/services/convert/pull.go b/services/convert/pull.go index 7798bebb08..8f9679f649 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -419,6 +419,9 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs if baseBranch != nil { apiPullRequest.Base.Sha = baseBranch.CommitID } + if pr.HeadRepoID == pr.BaseRepoID { + apiPullRequest.Head.Repository = apiPullRequest.Base.Repository + } // pull request head branch, both repository and branch could not exist if pr.HeadRepo != nil { @@ -431,22 +434,19 @@ func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs if exist { apiPullRequest.Head.Ref = pr.HeadBranch } + if pr.HeadRepoID != pr.BaseRepoID { + p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer) + if err != nil { + log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err) + p.AccessMode = perm.AccessModeNone + } + apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) + } } if apiPullRequest.Head.Ref == "" { apiPullRequest.Head.Ref = pr.GetGitRefName() } - if pr.HeadRepoID == pr.BaseRepoID { - apiPullRequest.Head.Repository = apiPullRequest.Base.Repository - } else { - p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer) - if err != nil { - log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err) - p.AccessMode = perm.AccessModeNone - } - apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) - } - if pr.Flow == issues_model.PullRequestFlowAGit { apiPullRequest.Head.Name = "" } diff --git a/services/convert/pull_test.go b/services/convert/pull_test.go index cd86283c8a..dfbe24d184 100644 --- a/services/convert/pull_test.go +++ b/services/convert/pull_test.go @@ -46,4 +46,11 @@ func TestPullRequest_APIFormat(t *testing.T) { assert.NotNil(t, apiPullRequest) assert.Nil(t, apiPullRequest.Head.Repository) assert.EqualValues(t, -1, apiPullRequest.Head.RepoID) + + apiPullRequests, err := ToAPIPullRequests(git.DefaultContext, pr.BaseRepo, []*issues_model.PullRequest{pr}, nil) + assert.NoError(t, err) + assert.Len(t, apiPullRequests, 1) + assert.NotNil(t, apiPullRequests[0]) + assert.Nil(t, apiPullRequests[0].Head.Repository) + assert.EqualValues(t, -1, apiPullRequests[0].Head.RepoID) } diff --git a/services/convert/status.go b/services/convert/status.go index 6cef63c1cd..b4864a0307 100644 --- a/services/convert/status.go +++ b/services/convert/status.go @@ -5,6 +5,7 @@ package convert import ( "context" + "net/url" git_model "code.gitea.io/gitea/models/git" user_model "code.gitea.io/gitea/models/user" @@ -32,34 +33,29 @@ func ToCommitStatus(ctx context.Context, status *git_model.CommitStatus) *api.Co return apiStatus } +func ToCommitStatuses(ctx context.Context, statuses []*git_model.CommitStatus) []*api.CommitStatus { + apiStatuses := make([]*api.CommitStatus, len(statuses)) + for i, status := range statuses { + apiStatuses[i] = ToCommitStatus(ctx, status) + } + return apiStatuses +} + // ToCombinedStatus converts List of CommitStatus to a CombinedStatus func ToCombinedStatus(ctx context.Context, statuses []*git_model.CommitStatus, repo *api.Repository) *api.CombinedStatus { if len(statuses) == 0 { return nil } - retStatus := &api.CombinedStatus{ - SHA: statuses[0].SHA, + combinedStatus := git_model.CalcCommitStatus(statuses) + + return &api.CombinedStatus{ + State: combinedStatus.State, + Statuses: ToCommitStatuses(ctx, statuses), + SHA: combinedStatus.SHA, TotalCount: len(statuses), Repository: repo, - URL: "", + CommitURL: repo.URL + "/commits/" + url.PathEscape(combinedStatus.SHA), + URL: repo.URL + "/commits/" + url.PathEscape(combinedStatus.SHA) + "/status", } - - retStatus.Statuses = make([]*api.CommitStatus, 0, len(statuses)) - for _, status := range statuses { - retStatus.Statuses = append(retStatus.Statuses, ToCommitStatus(ctx, status)) - if retStatus.State == "" || status.State.NoBetterThan(retStatus.State) { - retStatus.State = status.State - } - } - // According to https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference - // > Additionally, a combined state is returned. The state is one of: - // > failure if any of the contexts report as error or failure - // > pending if there are no statuses or a context is pending - // > success if the latest status for all contexts is success - if retStatus.State.IsError() { - retStatus.State = api.CommitStatusFailure - } - - return retStatus } diff --git a/services/doctor/actions.go b/services/doctor/actions.go index 7c44fb8392..28e26c88eb 100644 --- a/services/doctor/actions.go +++ b/services/doctor/actions.go @@ -19,7 +19,7 @@ func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bo var reposToFix []*repo_model.Repository for page := 1; ; page++ { - repos, _, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + repos, _, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: repo_model.RepositoryListDefaultPageSize, Page: page, diff --git a/services/doctor/dbconsistency.go b/services/doctor/dbconsistency.go index 62326ed07c..d5a133d8b2 100644 --- a/services/doctor/dbconsistency.go +++ b/services/doctor/dbconsistency.go @@ -15,6 +15,7 @@ import ( secret_model "code.gitea.io/gitea/models/secret" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + issue_service "code.gitea.io/gitea/services/issue" ) type consistencyCheck struct { @@ -93,7 +94,7 @@ func prepareDBConsistencyChecks() []consistencyCheck { // find issues without existing repository Name: "Orphaned Issues without existing repository", Counter: issues_model.CountOrphanedIssues, - Fixer: asFixer(issues_model.DeleteOrphanedIssues), + Fixer: asFixer(issue_service.DeleteOrphanedIssues), }, // find releases without existing repository genericOrphanCheck("Orphaned Releases without existing repository", diff --git a/services/doctor/paths.go b/services/doctor/paths.go index 3f62d587ab..4214c36b1a 100644 --- a/services/doctor/paths.go +++ b/services/doctor/paths.go @@ -99,15 +99,14 @@ func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix boo func isWritableDir(path string) error { // There's no platform-independent way of checking if a directory is writable // https://stackoverflow.com/questions/20026320/how-to-tell-if-folder-exists-and-is-writable - tmpFile, err := os.CreateTemp(path, "doctors-order") if err != nil { return err } if err := os.Remove(tmpFile.Name()); err != nil { - fmt.Printf("Warning: can't remove temporary file: '%s'\n", tmpFile.Name()) //nolint:forbidigo + log.Warn("can't remove temporary file: %q", tmpFile.Name()) } - tmpFile.Close() + _ = tmpFile.Close() return nil } diff --git a/services/doctor/repository.go b/services/doctor/repository.go index 6c33426636..359c4a17e0 100644 --- a/services/doctor/repository.go +++ b/services/doctor/repository.go @@ -7,7 +7,6 @@ import ( "context" "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/storage" repo_service "code.gitea.io/gitea/services/repository" @@ -39,7 +38,6 @@ func deleteOrphanedRepos(ctx context.Context) (int64, error) { batchSize := db.MaxBatchInsertSize("repository") e := db.GetEngine(ctx) var deleted int64 - adminUser := &user_model.User{IsAdmin: true} for { select { @@ -60,7 +58,7 @@ func deleteOrphanedRepos(ctx context.Context) (int64, error) { } for _, id := range ids { - if err := repo_service.DeleteRepositoryDirectly(ctx, adminUser, id, true); err != nil { + if err := repo_service.DeleteRepositoryDirectly(ctx, id, true); err != nil { return deleted, err } deleted++ diff --git a/services/feed/feed_test.go b/services/feed/feed_test.go index a3492938c8..705d42a2eb 100644 --- a/services/feed/feed_test.go +++ b/services/feed/feed_test.go @@ -147,7 +147,7 @@ func TestRepoActions(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) _ = db.TruncateBeans(db.DefaultContext, &activities_model.Action{}) - for i := 0; i < 3; i++ { + for i := range 3 { _ = db.Insert(db.DefaultContext, &activities_model.Action{ UserID: 2 + int64(i), ActUserID: 2, diff --git a/services/forms/org.go b/services/forms/org.go index db182f7e96..2ac18ef25c 100644 --- a/services/forms/org.go +++ b/services/forms/org.go @@ -36,7 +36,6 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding // UpdateOrgSettingForm form for updating organization settings type UpdateOrgSettingForm struct { - Name string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"` FullName string `binding:"MaxSize(100)"` Email string `binding:"MaxSize(255)"` Description string `binding:"MaxSize(255)"` @@ -53,6 +52,11 @@ func (f *UpdateOrgSettingForm) Validate(req *http.Request, errs binding.Errors) return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +type RenameOrgForm struct { + OrgName string `binding:"Required"` + NewOrgName string `binding:"Required;Username;MaxSize(40)" locale:"org.org_name_holder"` +} + // ___________ // \__ ___/___ _____ _____ // | |_/ __ \\__ \ / \ diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index a2827e516a..d116bb9f11 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -233,6 +233,7 @@ type WebhookForm struct { Release bool Package bool Status bool + WorkflowRun bool WorkflowJob bool Active bool BranchFilter string `binding:"GlobPattern"` @@ -679,129 +680,6 @@ func (f *NewWikiForm) Validate(req *http.Request, errs binding.Errors) binding.E return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } -// ___________ .___.__ __ -// \_ _____/ __| _/|__|/ |_ -// | __)_ / __ | | \ __\ -// | \/ /_/ | | || | -// /_______ /\____ | |__||__| -// \/ \/ - -// EditRepoFileForm form for changing repository file -type EditRepoFileForm struct { - TreePath string `binding:"Required;MaxSize(500)"` - Content string - CommitSummary string `binding:"MaxSize(100)"` - CommitMessage string - CommitChoice string `binding:"Required;MaxSize(50)"` - NewBranchName string `binding:"GitRefName;MaxSize(100)"` - LastCommit string - Signoff bool - CommitEmail string -} - -// Validate validates the fields -func (f *EditRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// EditPreviewDiffForm form for changing preview diff -type EditPreviewDiffForm struct { - Content string -} - -// Validate validates the fields -func (f *EditPreviewDiffForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// _________ .__ __________.__ __ -// \_ ___ \| |__ __________________ ___.__. \______ \__| ____ | | __ -// / \ \/| | \_/ __ \_ __ \_ __ < | | | ___/ |/ ___\| |/ / -// \ \___| Y \ ___/| | \/| | \/\___ | | | | \ \___| < -// \______ /___| /\___ >__| |__| / ____| |____| |__|\___ >__|_ \ -// \/ \/ \/ \/ \/ \/ - -// CherryPickForm form for changing repository file -type CherryPickForm struct { - CommitSummary string `binding:"MaxSize(100)"` - CommitMessage string - CommitChoice string `binding:"Required;MaxSize(50)"` - NewBranchName string `binding:"GitRefName;MaxSize(100)"` - LastCommit string - Revert bool - Signoff bool - CommitEmail string -} - -// Validate validates the fields -func (f *CherryPickForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// ____ ___ .__ .___ -// | | \______ | | _________ __| _/ -// | | /\____ \| | / _ \__ \ / __ | -// | | / | |_> > |_( <_> ) __ \_/ /_/ | -// |______/ | __/|____/\____(____ /\____ | -// |__| \/ \/ -// - -// UploadRepoFileForm form for uploading repository file -type UploadRepoFileForm struct { - TreePath string `binding:"MaxSize(500)"` - CommitSummary string `binding:"MaxSize(100)"` - CommitMessage string - CommitChoice string `binding:"Required;MaxSize(50)"` - NewBranchName string `binding:"GitRefName;MaxSize(100)"` - Files []string - Signoff bool - CommitEmail string -} - -// Validate validates the fields -func (f *UploadRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// RemoveUploadFileForm form for removing uploaded file -type RemoveUploadFileForm struct { - File string `binding:"Required;MaxSize(50)"` -} - -// Validate validates the fields -func (f *RemoveUploadFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - -// ________ .__ __ -// \______ \ ____ | | _____/ |_ ____ -// | | \_/ __ \| | _/ __ \ __\/ __ \ -// | ` \ ___/| |_\ ___/| | \ ___/ -// /_______ /\___ >____/\___ >__| \___ > -// \/ \/ \/ \/ - -// DeleteRepoFileForm form for deleting repository file -type DeleteRepoFileForm struct { - CommitSummary string `binding:"MaxSize(100)"` - CommitMessage string - CommitChoice string `binding:"Required;MaxSize(50)"` - NewBranchName string `binding:"GitRefName;MaxSize(100)"` - LastCommit string - Signoff bool - CommitEmail string -} - -// Validate validates the fields -func (f *DeleteRepoFileForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { - ctx := context.GetValidateContext(req) - return middleware.Validate(errs, ctx.Data, f, ctx.Locale) -} - // ___________.__ ___________ __ // \__ ___/|__| _____ ____ \__ ___/___________ ____ | | __ ___________ // | | | |/ \_/ __ \ | | \_ __ \__ \ _/ ___\| |/ // __ \_ __ \ diff --git a/services/forms/repo_form_editor.go b/services/forms/repo_form_editor.go new file mode 100644 index 0000000000..3ad2eae75d --- /dev/null +++ b/services/forms/repo_form_editor.go @@ -0,0 +1,57 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forms + +import ( + "net/http" + + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/web/middleware" + "code.gitea.io/gitea/services/context" + + "gitea.com/go-chi/binding" +) + +type CommitCommonForm struct { + TreePath string `binding:"MaxSize(500)"` + CommitSummary string `binding:"MaxSize(100)"` + CommitMessage string + CommitChoice string `binding:"Required;MaxSize(50)"` + NewBranchName string `binding:"GitRefName;MaxSize(100)"` + LastCommit string + Signoff bool + CommitEmail string +} + +func (f *CommitCommonForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetValidateContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} + +type CommitCommonFormInterface interface { + GetCommitCommonForm() *CommitCommonForm +} + +func (f *CommitCommonForm) GetCommitCommonForm() *CommitCommonForm { + return f +} + +type EditRepoFileForm struct { + CommitCommonForm + Content optional.Option[string] +} + +type DeleteRepoFileForm struct { + CommitCommonForm +} + +type UploadRepoFileForm struct { + CommitCommonForm + Files []string +} + +type CherryPickForm struct { + CommitCommonForm + Revert bool +} diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go index b4120f20ed..09e9ec0f65 100644 --- a/services/forms/user_form_test.go +++ b/services/forms/user_form_test.go @@ -7,6 +7,7 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "github.com/gobwas/glob" "github.com/stretchr/testify/assert" @@ -26,12 +27,7 @@ func TestRegisterForm_IsDomainAllowed_Empty(t *testing.T) { } func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) { - oldService := setting.Service - defer func() { - setting.Service = oldService - }() - - setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io")} + defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io")})() tt := []struct { email string @@ -48,12 +44,7 @@ func TestRegisterForm_IsDomainAllowed_InvalidEmail(t *testing.T) { } func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) { - oldService := setting.Service - defer func() { - setting.Service = oldService - }() - - setting.Service.EmailDomainAllowList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")} + defer test.MockVariableValue(&setting.Service.EmailDomainAllowList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.allow")})() tt := []struct { email string @@ -76,13 +67,7 @@ func TestRegisterForm_IsDomainAllowed_AllowedEmail(t *testing.T) { } func TestRegisterForm_IsDomainAllowed_BlockedEmail(t *testing.T) { - oldService := setting.Service - defer func() { - setting.Service = oldService - }() - - setting.Service.EmailDomainAllowList = nil - setting.Service.EmailDomainBlockList = []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")} + defer test.MockVariableValue(&setting.Service.EmailDomainBlockList, []glob.Glob{glob.MustCompile("gitea.io"), glob.MustCompile("*.block")})() tt := []struct { email string diff --git a/services/git/commit.go b/services/git/commit.go index 3faef76782..2e0e8a5096 100644 --- a/services/git/commit.go +++ b/services/git/commit.go @@ -34,9 +34,9 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, } for _, c := range oldCommits { - committer, ok := emailUsers[c.Committer.Email] - if !ok && c.Committer != nil { - committer = &user_model.User{ + committerUser := emailUsers.GetByEmail(c.Committer.Email) // FIXME: why ValidateCommitsWithEmails uses "Author", but ParseCommitsWithSignature uses "Committer"? + if committerUser == nil { + committerUser = &user_model.User{ Name: c.Committer.Name, Email: c.Committer.Email, } @@ -44,7 +44,7 @@ func ParseCommitsWithSignature(ctx context.Context, repo *repo_model.Repository, signCommit := &asymkey_model.SignCommit{ UserCommit: c, - Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.Commit, committer), + Verification: asymkey_service.ParseCommitWithSignatureCommitter(ctx, c.Commit, committerUser), } isOwnerMemberCollaborator := func(user *user_model.User) (bool, error) { @@ -84,7 +84,7 @@ func ParseCommitsWithStatus(ctx context.Context, oldCommits []*asymkey_model.Sig commit := &git_model.SignCommitWithStatuses{ SignCommit: c, } - statuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commit.ID.String(), db.ListOptions{}) + statuses, err := git_model.GetLatestCommitStatus(ctx, repo.ID, commit.ID.String(), db.ListOptionsAll) if err != nil { return nil, err } diff --git a/services/gitdiff/csv.go b/services/gitdiff/csv.go index 8db73c56a3..c10ee14490 100644 --- a/services/gitdiff/csv.go +++ b/services/gitdiff/csv.go @@ -134,7 +134,7 @@ func createCsvDiffSingle(reader *csv.Reader, celltype TableDiffCellType) ([]*Tab return nil, err } cells := make([]*TableDiffCell, len(row)) - for j := 0; j < len(row); j++ { + for j := range row { if celltype == TableDiffCellDel { cells[j] = &TableDiffCell{LeftCell: row[j], Type: celltype} } else { @@ -365,11 +365,11 @@ func getColumnMapping(baseCSVReader, headCSVReader *csvReader) ([]int, []int) { } // Loops through the baseRow and see if there is a match in the head row - for i := 0; i < len(baseRow); i++ { + for i := range baseRow { base2HeadColMap[i] = unmappedColumn baseCell, err := getCell(baseRow, i) if err == nil { - for j := 0; j < len(headRow); j++ { + for j := range headRow { if head2BaseColMap[j] == -1 { headCell, err := getCell(headRow, j) if err == nil && baseCell == headCell { @@ -390,7 +390,7 @@ func getColumnMapping(baseCSVReader, headCSVReader *csvReader) ([]int, []int) { // tryMapColumnsByContent tries to map missing columns by the content of the first lines. func tryMapColumnsByContent(baseCSVReader *csvReader, base2HeadColMap []int, headCSVReader *csvReader, head2BaseColMap []int) { - for i := 0; i < len(base2HeadColMap); i++ { + for i := range base2HeadColMap { headStart := 0 for base2HeadColMap[i] == unmappedColumn && headStart < len(head2BaseColMap) { if head2BaseColMap[headStart] == unmappedColumn { @@ -424,7 +424,7 @@ func getCell(row []string, column int) (string, error) { // countUnmappedColumns returns the count of unmapped columns. func countUnmappedColumns(mapping []int) int { count := 0 - for i := 0; i < len(mapping); i++ { + for i := range mapping { if mapping[i] == unmappedColumn { count++ } diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index a859945378..9964329876 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -214,51 +214,6 @@ func (diffSection *DiffSection) GetLine(idx int) *DiffLine { return diffSection.Lines[idx] } -// GetLine gets a specific line by type (add or del) and file line number -// This algorithm is not quite right. -// Actually now we have "Match" field, it is always right, so use it instead in new GetLine -func (diffSection *DiffSection) getLineLegacy(lineType DiffLineType, idx int) *DiffLine { //nolint:unused - var ( - difference = 0 - addCount = 0 - delCount = 0 - matchDiffLine *DiffLine - ) - -LOOP: - for _, diffLine := range diffSection.Lines { - switch diffLine.Type { - case DiffLineAdd: - addCount++ - case DiffLineDel: - delCount++ - default: - if matchDiffLine != nil { - break LOOP - } - difference = diffLine.RightIdx - diffLine.LeftIdx - addCount = 0 - delCount = 0 - } - - switch lineType { - case DiffLineDel: - if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference { - matchDiffLine = diffLine - } - case DiffLineAdd: - if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference { - matchDiffLine = diffLine - } - } - } - - if addCount == delCount { - return matchDiffLine - } - return nil -} - func defaultDiffMatchPatch() *diffmatchpatch.DiffMatchPatch { dmp := diffmatchpatch.New() dmp.DiffEditCost = 100 @@ -540,10 +495,7 @@ func ParsePatch(ctx context.Context, maxLines, maxLineCharacters, maxFiles int, // OK let's set a reasonable buffer size. // This should be at least the size of maxLineCharacters or 4096 whichever is larger. - readerSize := maxLineCharacters - if readerSize < 4096 { - readerSize = 4096 - } + readerSize := max(maxLineCharacters, 4096) input := bufio.NewReaderSize(reader, readerSize) line, err := input.ReadString('\n') @@ -1351,13 +1303,13 @@ func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model. latestCommit = pull.HeadBranch // opts.AfterCommitID is preferred because it handles PRs from forks correctly and the branch name doesn't } - changedFiles, err := gitRepo.GetFilesChangedBetween(review.CommitSHA, latestCommit) + changedFiles, errIgnored := gitRepo.GetFilesChangedBetween(review.CommitSHA, latestCommit) // There are way too many possible errors. // Examples are various git errors such as the commit the review was based on was gc'ed and hence doesn't exist anymore as well as unrecoverable errors where we should serve a 500 response // Due to the current architecture and physical limitation of needing to compare explicit error messages, we can only choose one approach without the code getting ugly // For SOME of the errors such as the gc'ed commit, it would be best to mark all files as changed // But as that does not work for all potential errors, we simply mark all files as unchanged and drop the error which always works, even if not as good as possible - if err != nil { + if errIgnored != nil { log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err) } @@ -1400,7 +1352,7 @@ outer: } } - return review, err + return review, nil } // CommentAsDiff returns c.Patch as *Diff diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 71394b1915..b84530043a 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -416,7 +416,7 @@ index 0000000..6bb8f39 ` diffBuilder.WriteString(diff) - for i := 0; i < 35; i++ { + for i := range 35 { diffBuilder.WriteString("+line" + strconv.Itoa(i) + "\n") } diff = diffBuilder.String() @@ -453,11 +453,11 @@ index 0000000..6bb8f39 diffBuilder.Reset() diffBuilder.WriteString(diff) - for i := 0; i < 33; i++ { + for i := range 33 { diffBuilder.WriteString("+line" + strconv.Itoa(i) + "\n") } diffBuilder.WriteString("+line33") - for i := 0; i < 512; i++ { + for range 512 { diffBuilder.WriteString("0123456789ABCDEF") } diffBuilder.WriteByte('\n') diff --git a/services/gitdiff/submodule_test.go b/services/gitdiff/submodule_test.go index 3047b23103..152c5b7066 100644 --- a/services/gitdiff/submodule_test.go +++ b/services/gitdiff/submodule_test.go @@ -203,7 +203,6 @@ index 0000000..68972a9 } for _, testcase := range tests { - testcase := testcase t.Run(testcase.name, func(t *testing.T) { diff, err := ParsePatch(db.DefaultContext, setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(testcase.gitdiff), "") assert.NoError(t, err) diff --git a/services/issue/assignee.go b/services/issue/assignee.go index c7e2495568..ba9c91e0ed 100644 --- a/services/issue/assignee.go +++ b/services/issue/assignee.go @@ -54,6 +54,8 @@ func ToggleAssigneeWithNotify(ctx context.Context, issue *issues_model.Issue, do if err != nil { return false, nil, err } + issue.AssigneeID = assigneeID + issue.Assignee = assignee notify_service.IssueChangeAssignee(ctx, doer, issue, assignee, removed, comment) @@ -302,7 +304,7 @@ func CanDoerChangeReviewRequests(ctx context.Context, doer *user_model.User, rep // If the repo's owner is an organization, members of teams with read permission on pull requests can change reviewers if repo.Owner.IsOrganization() { - teams, err := organization.GetTeamsWithAccessToRepo(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead) + teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests) if err != nil { log.Error("GetTeamsWithAccessToRepo: %v", err) return false diff --git a/services/issue/issue.go b/services/issue/issue.go index 455a1ec297..2cb5f2801d 100644 --- a/services/issue/issue.go +++ b/services/issue/issue.go @@ -190,9 +190,13 @@ func DeleteIssue(ctx context.Context, doer *user_model.User, gitRepo *git.Reposi } // delete entries in database - if err := deleteIssue(ctx, issue); err != nil { + attachmentPaths, err := deleteIssue(ctx, issue) + if err != nil { return err } + for _, attachmentPath := range attachmentPaths { + system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPath) + } // delete pull request related git data if issue.IsPull && gitRepo != nil { @@ -256,45 +260,45 @@ func GetRefEndNamesAndURLs(issues []*issues_model.Issue, repoLink string) (map[i } // deleteIssue deletes the issue -func deleteIssue(ctx context.Context, issue *issues_model.Issue) error { +func deleteIssue(ctx context.Context, issue *issues_model.Issue) ([]string, error) { ctx, committer, err := db.TxContext(ctx) if err != nil { - return err + return nil, err } defer committer.Close() - e := db.GetEngine(ctx) - if _, err := e.ID(issue.ID).NoAutoCondition().Delete(issue); err != nil { - return err + if _, err := db.GetEngine(ctx).ID(issue.ID).NoAutoCondition().Delete(issue); err != nil { + return nil, err } // update the total issue numbers if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, false); err != nil { - return err + return nil, err } // if the issue is closed, update the closed issue numbers if issue.IsClosed { if err := repo_model.UpdateRepoIssueNumbers(ctx, issue.RepoID, issue.IsPull, true); err != nil { - return err + return nil, err } } if err := issues_model.UpdateMilestoneCounters(ctx, issue.MilestoneID); err != nil { - return fmt.Errorf("error updating counters for milestone id %d: %w", + return nil, fmt.Errorf("error updating counters for milestone id %d: %w", issue.MilestoneID, err) } if err := activities_model.DeleteIssueActions(ctx, issue.RepoID, issue.ID, issue.Index); err != nil { - return err + return nil, err } // find attachments related to this issue and remove them - if err := issue.LoadAttributes(ctx); err != nil { - return err + if err := issue.LoadAttachments(ctx); err != nil { + return nil, err } + var attachmentPaths []string for i := range issue.Attachments { - system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", issue.Attachments[i].RelativePath()) + attachmentPaths = append(attachmentPaths, issue.Attachments[i].RelativePath()) } // delete all database data still assigned to this issue @@ -318,8 +322,68 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) error { &issues_model.Comment{DependentIssueID: issue.ID}, &issues_model.IssuePin{IssueID: issue.ID}, ); err != nil { + return nil, err + } + + if err := committer.Commit(); err != nil { + return nil, err + } + return attachmentPaths, nil +} + +// DeleteOrphanedIssues delete issues without a repo +func DeleteOrphanedIssues(ctx context.Context) error { + var attachmentPaths []string + err := db.WithTx(ctx, func(ctx context.Context) error { + repoIDs, err := issues_model.GetOrphanedIssueRepoIDs(ctx) + if err != nil { + return err + } + for i := range repoIDs { + paths, err := DeleteIssuesByRepoID(ctx, repoIDs[i]) + if err != nil { + return err + } + attachmentPaths = append(attachmentPaths, paths...) + } + return nil + }) + if err != nil { return err } - return committer.Commit() + // Remove issue attachment files. + for i := range attachmentPaths { + system_model.RemoveStorageWithNotice(ctx, storage.Attachments, "Delete issue attachment", attachmentPaths[i]) + } + return nil +} + +// DeleteIssuesByRepoID deletes issues by repositories id +func DeleteIssuesByRepoID(ctx context.Context, repoID int64) (attachmentPaths []string, err error) { + for { + issues := make([]*issues_model.Issue, 0, db.DefaultMaxInSize) + if err := db.GetEngine(ctx). + Where("repo_id = ?", repoID). + OrderBy("id"). + Limit(db.DefaultMaxInSize). + Find(&issues); err != nil { + return nil, err + } + + if len(issues) == 0 { + break + } + + for _, issue := range issues { + issueAttachPaths, err := deleteIssue(ctx, issue) + if err != nil { + return nil, fmt.Errorf("deleteIssue [issue_id: %d]: %w", issue.ID, err) + } + + attachmentPaths = append(attachmentPaths, issueAttachPaths...) + } + } + + return attachmentPaths, err } diff --git a/services/issue/issue_test.go b/services/issue/issue_test.go index b3df8191e1..bad0d65d1e 100644 --- a/services/issue/issue_test.go +++ b/services/issue/issue_test.go @@ -44,7 +44,7 @@ func TestIssue_DeleteIssue(t *testing.T) { ID: issueIDs[2], } - err = deleteIssue(db.DefaultContext, issue) + _, err = deleteIssue(db.DefaultContext, issue) assert.NoError(t, err) issueIDs, err = issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1) assert.NoError(t, err) @@ -55,7 +55,7 @@ func TestIssue_DeleteIssue(t *testing.T) { assert.NoError(t, err) issue, err = issues_model.GetIssueByID(db.DefaultContext, 4) assert.NoError(t, err) - err = deleteIssue(db.DefaultContext, issue) + _, err = deleteIssue(db.DefaultContext, issue) assert.NoError(t, err) assert.Len(t, attachments, 2) for i := range attachments { @@ -78,7 +78,7 @@ func TestIssue_DeleteIssue(t *testing.T) { assert.NoError(t, err) assert.False(t, left) - err = deleteIssue(db.DefaultContext, issue2) + _, err = deleteIssue(db.DefaultContext, issue2) assert.NoError(t, err) left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1) assert.NoError(t, err) diff --git a/services/issue/status.go b/services/issue/status.go index e18b891175..f9d7dca841 100644 --- a/services/issue/status.go +++ b/services/issue/status.go @@ -24,14 +24,14 @@ func CloseIssue(ctx context.Context, issue *issues_model.Issue, doer *user_model comment, err := issues_model.CloseIssue(dbCtx, issue, doer) if err != nil { if issues_model.IsErrDependenciesLeft(err) { - if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil { + if _, err := issues_model.FinishIssueStopwatch(dbCtx, doer, issue); err != nil { log.Error("Unable to stop stopwatch for issue[%d]#%d: %v", issue.ID, issue.Index, err) } } return err } - if err := issues_model.FinishIssueStopwatchIfPossible(dbCtx, doer, issue); err != nil { + if _, err := issues_model.FinishIssueStopwatch(dbCtx, doer, issue); err != nil { return err } diff --git a/services/lfs/locks.go b/services/lfs/locks.go index 1d464f4a66..264001f0f9 100644 --- a/services/lfs/locks.go +++ b/services/lfs/locks.go @@ -74,10 +74,7 @@ func GetListLockHandler(ctx *context.Context) { } ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType) - cursor := ctx.FormInt("cursor") - if cursor < 0 { - cursor = 0 - } + cursor := max(ctx.FormInt("cursor"), 0) limit := ctx.FormInt("limit") if limit > setting.LFS.LocksPagingNum && setting.LFS.LocksPagingNum > 0 { limit = setting.LFS.LocksPagingNum @@ -239,10 +236,7 @@ func VerifyLockHandler(ctx *context.Context) { ctx.Resp.Header().Set("Content-Type", lfs_module.MediaType) - cursor := ctx.FormInt("cursor") - if cursor < 0 { - cursor = 0 - } + cursor := max(ctx.FormInt("cursor"), 0) limit := ctx.FormInt("limit") if limit > setting.LFS.LocksPagingNum && setting.LFS.LocksPagingNum > 0 { limit = setting.LFS.LocksPagingNum diff --git a/services/lfs/server.go b/services/lfs/server.go index 0a99287ed9..15a51ad534 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "maps" "net/http" "net/url" "path" @@ -202,7 +203,7 @@ func BatchHandler(ctx *context.Context) { exists, err := contentStore.Exists(p) if err != nil { - log.Error("Unable to check if LFS OID[%s] exist. Error: %v", p.Oid, rc.User, rc.Repo, err) + log.Error("Unable to check if LFS object with ID '%s' exists for %s/%s. Error: %v", p.Oid, rc.User, rc.Repo, err) writeStatus(ctx, http.StatusInternalServerError) return } @@ -480,9 +481,7 @@ func buildObjectResponse(rc *requestContext, pointer lfs_module.Pointer, downloa rep.Actions["upload"] = &lfs_module.Link{Href: rc.UploadLink(pointer), Header: header} verifyHeader := make(map[string]string) - for key, value := range header { - verifyHeader[key] = value - } + maps.Copy(verifyHeader, header) // This is only needed to workaround https://github.com/git-lfs/git-lfs/issues/3662 verifyHeader["Accept"] = lfs_module.AcceptHeader diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index 7a47cf3876..b15949f352 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -528,7 +528,7 @@ func TestEmbedBase64Images(t *testing.T) { require.NoError(t, err) mailBody := msgs[0].Body - assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo="/></a> MSG-AFTER`, mailBody) + assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo=".*/></a> MSG-AFTER`, mailBody) }) t.Run("EmbedInstanceImageSkipExternalImage", func(t *testing.T) { diff --git a/services/mailer/sender/message_test.go b/services/mailer/sender/message_test.go index 63d0bc349a..ae153ebf05 100644 --- a/services/mailer/sender/message_test.go +++ b/services/mailer/sender/message_test.go @@ -108,9 +108,9 @@ func extractMailHeaderAndContent(t *testing.T, mail string) (map[string]string, } content := strings.TrimSpace("boundary=" + parts[1]) - hParts := strings.Split(parts[0], "\n") + hParts := strings.SplitSeq(parts[0], "\n") - for _, hPart := range hParts { + for hPart := range hParts { parts := strings.SplitN(hPart, ":", 2) hk := strings.TrimSpace(parts[0]) if hk != "" { diff --git a/services/migrations/codecommit.go b/services/migrations/codecommit.go index 4b2634ef8a..d08b2e6d4a 100644 --- a/services/migrations/codecommit.go +++ b/services/migrations/codecommit.go @@ -155,10 +155,7 @@ func (c *CodeCommitDownloader) GetPullRequests(ctx context.Context, page, perPag } startIndex := (page - 1) * perPage - endIndex := page * perPage - if endIndex > len(allPullRequestIDs) { - endIndex = len(allPullRequestIDs) - } + endIndex := min(page*perPage, len(allPullRequestIDs)) batch := allPullRequestIDs[startIndex:endIndex] prs := make([]*base.PullRequest, 0, len(batch)) @@ -180,11 +177,15 @@ func (c *CodeCommitDownloader) GetPullRequests(ctx context.Context, page, perPag continue } target := orig.PullRequestTargets[0] + description := "" + if orig.Description != nil { + description = *orig.Description + } pr := &base.PullRequest{ Number: number, Title: *orig.Title, PosterName: c.getUsernameFromARN(*orig.AuthorArn), - Content: *orig.Description, + Content: description, State: "open", Created: *orig.CreationDate, Updated: *orig.LastActivityDate, @@ -206,6 +207,10 @@ func (c *CodeCommitDownloader) GetPullRequests(ctx context.Context, page, perPag pr.State = "closed" pr.Closed = orig.LastActivityDate } + if pr.Merged { + pr.MergeCommitSHA = *target.MergeMetadata.MergeCommitId + pr.MergedTime = orig.LastActivityDate + } _ = CheckAndEnsureSafePR(pr, c.baseURL, c) prs = append(prs, pr) diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index acb94439fa..737bff24d0 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -765,7 +765,7 @@ func (g *GiteaLocalUploader) newPullRequest(ctx context.Context, pr *base.PullRe issue := issues_model.Issue{ RepoID: g.repo.ID, Repo: g.repo, - Title: prTitle, + Title: util.TruncateRunes(prTitle, 255), Index: pr.Number, Content: pr.Content, MilestoneID: milestoneID, diff --git a/services/migrations/github.go b/services/migrations/github.go index 662908756a..2ce11615c6 100644 --- a/services/migrations/github.go +++ b/services/migrations/github.go @@ -89,8 +89,8 @@ func NewGithubDownloaderV3(_ context.Context, baseURL, userName, password, token } if token != "" { - tokens := strings.Split(token, ",") - for _, token := range tokens { + tokens := strings.SplitSeq(token, ",") + for token := range tokens { token = strings.TrimSpace(token) ts := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token}, diff --git a/services/migrations/migrate.go b/services/migrations/migrate.go index 961abe16f4..eba9c79df5 100644 --- a/services/migrations/migrate.go +++ b/services/migrations/migrate.go @@ -75,11 +75,9 @@ func IsMigrateURLAllowed(remoteURL string, doer *user_model.User) error { return &git.ErrInvalidCloneAddr{Host: u.Host, IsProtocolInvalid: true, IsPermissionDenied: true, IsURLError: true} } - hostName, _, err := net.SplitHostPort(u.Host) - if err != nil { - // u.Host can be "host" or "host:port" - err = nil //nolint - hostName = u.Host + hostName, _, errIgnored := net.SplitHostPort(u.Host) + if errIgnored != nil { + hostName = u.Host // u.Host can be "host" or "host:port" } // some users only use proxy, there is no DNS resolver. it's safe to ignore the LookupIP error diff --git a/services/notify/notifier.go b/services/notify/notifier.go index 40428454be..875a70e564 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -79,5 +79,7 @@ type Notifier interface { CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) + WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) + WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) } diff --git a/services/notify/notify.go b/services/notify/notify.go index 9f8be4b577..0c6fdf9cef 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -376,6 +376,12 @@ func CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit } } +func WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { + for _, notifier := range notifiers { + notifier.WorkflowRunStatusUpdate(ctx, repo, sender, run) + } +} + func WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { for _, notifier := range notifiers { notifier.WorkflowJobStatusUpdate(ctx, repo, sender, job, task) diff --git a/services/notify/null.go b/services/notify/null.go index 9c794a2342..c3085d7c9e 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -214,5 +214,8 @@ func (*NullNotifier) ChangeDefaultBranch(ctx context.Context, repo *repo_model.R func (*NullNotifier) CreateCommitStatus(ctx context.Context, repo *repo_model.Repository, commit *repository.PushCommit, sender *user_model.User, status *git_model.CommitStatus) { } +func (*NullNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { +} + func (*NullNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, job *actions_model.ActionRunJob, task *actions_model.ActionTask) { } diff --git a/services/oauth2_provider/access_token.go b/services/oauth2_provider/access_token.go index 52a73c9572..e01777031b 100644 --- a/services/oauth2_provider/access_token.go +++ b/services/oauth2_provider/access_token.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package oauth2_provider //nolint +package oauth2_provider import ( "context" @@ -16,7 +16,9 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/util" "github.com/golang-jwt/jwt/v5" ) @@ -83,7 +85,7 @@ func GrantAdditionalScopes(grantScopes string) auth.AccessTokenScope { } var accessScopes []string // the scopes for access control, but not for general information - for _, scope := range strings.Split(grantScopes, " ") { + for scope := range strings.SplitSeq(grantScopes, " ") { if scope != "" && !slices.Contains(generalScopesSupported, scope) { accessScopes = append(accessScopes, scope) } @@ -231,12 +233,11 @@ func NewAccessTokenResponse(ctx context.Context, grant *auth.OAuth2Grant, server }, nil } -// returns a list of "org" and "org:team" strings, -// that the given user is a part of. +// GetOAuthGroupsForUser returns a list of "org" and "org:team" strings, that the given user is a part of. func GetOAuthGroupsForUser(ctx context.Context, user *user_model.User, onlyPublicGroups bool) ([]string, error) { orgs, err := db.Find[org_model.Organization](ctx, org_model.FindOrgOptions{ - UserID: user.ID, - IncludePrivate: !onlyPublicGroups, + UserID: user.ID, + IncludeVisibility: util.Iif(onlyPublicGroups, api.VisibleTypePublic, api.VisibleTypePrivate), }) if err != nil { return nil, fmt.Errorf("GetUserOrgList: %w", err) diff --git a/services/oauth2_provider/additional_scopes_test.go b/services/oauth2_provider/additional_scopes_test.go index 2d4df7aea2..5f375346dc 100644 --- a/services/oauth2_provider/additional_scopes_test.go +++ b/services/oauth2_provider/additional_scopes_test.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package oauth2_provider //nolint +package oauth2_provider import ( "testing" diff --git a/services/oauth2_provider/init.go b/services/oauth2_provider/init.go index e5958099a6..c412bd6433 100644 --- a/services/oauth2_provider/init.go +++ b/services/oauth2_provider/init.go @@ -1,7 +1,7 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package oauth2_provider //nolint +package oauth2_provider import ( "context" diff --git a/services/oauth2_provider/jwtsigningkey.go b/services/oauth2_provider/jwtsigningkey.go index 3bc4f49410..03c7403f75 100644 --- a/services/oauth2_provider/jwtsigningkey.go +++ b/services/oauth2_provider/jwtsigningkey.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package oauth2_provider //nolint +package oauth2_provider import ( "crypto/ecdsa" diff --git a/services/oauth2_provider/token.go b/services/oauth2_provider/token.go index 383bcdb3eb..935c4cc01f 100644 --- a/services/oauth2_provider/token.go +++ b/services/oauth2_provider/token.go @@ -1,7 +1,7 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package oauth2_provider //nolint +package oauth2_provider import ( "errors" diff --git a/services/org/team_test.go b/services/org/team_test.go index aa39771cd2..c1a69d8ee7 100644 --- a/services/org/team_test.go +++ b/services/org/team_test.go @@ -204,7 +204,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { // Create repos. repoIDs := make([]int64, 0) - for i := 0; i < 3; i++ { + for i := range 3 { r, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), repo_service.CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}, true) assert.NoError(t, err, "CreateRepository %d", i) @@ -281,7 +281,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { } // Remove repo and check teams repositories. - assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repoIDs[0]), "DeleteRepository") + assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, repoIDs[0]), "DeleteRepository") teamRepos[0] = repoIDs[1:] teamRepos[1] = repoIDs[1:] teamRepos[3] = repoIDs[1:3] @@ -293,7 +293,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) { // Wipe created items. for i, rid := range repoIDs { if i > 0 { // first repo already deleted. - assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i) + assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, rid), "DeleteRepository %d", i) } } assert.NoError(t, DeleteOrganization(db.DefaultContext, org, false), "DeleteOrganization") diff --git a/services/packages/arch/vercmp.go b/services/packages/arch/vercmp.go index 0d33dda0f1..d44aa530f0 100644 --- a/services/packages/arch/vercmp.go +++ b/services/packages/arch/vercmp.go @@ -34,13 +34,8 @@ func parseEVR(evr string) (epoch, version, release string) { func compareSegments(a, b []string) int { lenA, lenB := len(a), len(b) - var l int - if lenA > lenB { - l = lenB - } else { - l = lenA - } - for i := 0; i < l; i++ { + l := min(lenA, lenB) + for i := range l { if r := compare(a[i], b[i]); r != 0 { return r } diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index e8a2f189c8..605335d0f1 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -310,7 +310,7 @@ func alterRepositoryContent(ctx context.Context, doer *user_model.User, repo *re } func writeObjectToIndex(ctx context.Context, t *files_service.TemporaryUploadRepository, path string, r io.Reader) error { - hash, err := t.HashObject(ctx, r) + hash, err := t.HashObjectAndWrite(ctx, r) if err != nil { return err } diff --git a/services/packages/cleanup/cleanup.go b/services/packages/cleanup/cleanup.go index b7ba2b6ac4..959babe7cd 100644 --- a/services/packages/cleanup/cleanup.go +++ b/services/packages/cleanup/cleanup.go @@ -32,127 +32,136 @@ func CleanupTask(ctx context.Context, olderThan time.Duration) error { return CleanupExpiredData(ctx, olderThan) } -func ExecuteCleanupRules(outerCtx context.Context) error { - ctx, committer, err := db.TxContext(outerCtx) +func executeCleanupOneRulePackage(ctx context.Context, pcr *packages_model.PackageCleanupRule, p *packages_model.Package) (versionDeleted bool, err error) { + olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays) + pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ + PackageID: p.ID, + IsInternal: optional.Some(false), + Sort: packages_model.SortCreatedDesc, + }) if err != nil { - return err + return false, fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err) } - defer committer.Close() - - err = packages_model.IterateEnabledCleanupRules(ctx, func(ctx context.Context, pcr *packages_model.PackageCleanupRule) error { - select { - case <-outerCtx.Done(): - return db.ErrCancelledf("While processing package cleanup rules") - default: + if pcr.KeepCount > 0 { + if pcr.KeepCount < len(pvs) { + pvs = pvs[pcr.KeepCount:] + } else { + pvs = nil } - - if err := pcr.CompiledPattern(); err != nil { - return fmt.Errorf("CleanupRule [%d]: CompilePattern failed: %w", pcr.ID, err) + } + for _, pv := range pvs { + if pcr.Type == packages_model.TypeContainer { + if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil { + return false, fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err) + } else if skip { + log.Debug("Rule[%d]: keep '%s/%s' (container)", pcr.ID, p.Name, pv.Version) + continue + } } - - olderThan := time.Now().AddDate(0, 0, -pcr.RemoveDays) - - packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type) - if err != nil { - return fmt.Errorf("CleanupRule [%d]: GetPackagesByType failed: %w", pcr.ID, err) + toMatch := pv.LowerVersion + if pcr.MatchFullName { + toMatch = p.LowerName + "/" + pv.LowerVersion + } + if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) { + log.Debug("Rule[%d]: keep '%s/%s' (keep pattern)", pcr.ID, p.Name, pv.Version) + continue + } + if pv.CreatedUnix.AsLocalTime().After(olderThan) { + log.Debug("Rule[%d]: keep '%s/%s' (remove days) %v", pcr.ID, p.Name, pv.Version, pv.CreatedUnix.FormatDate()) + continue } + if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) { + log.Debug("Rule[%d]: keep '%s/%s' (remove pattern)", pcr.ID, p.Name, pv.Version) + continue + } + log.Debug("Rule[%d]: remove '%s/%s'", pcr.ID, p.Name, pv.Version) + if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { + log.Error("CleanupRule [%d]: DeletePackageVersionAndReferences failed: %v", pcr.ID, err) + continue + } + versionDeleted = true + } + return versionDeleted, nil +} - anyVersionDeleted := false - for _, p := range packages { - pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ - PackageID: p.ID, - IsInternal: optional.Some(false), - Sort: packages_model.SortCreatedDesc, - Paginator: db.NewAbsoluteListOptions(pcr.KeepCount, 200), - }) - if err != nil { - return fmt.Errorf("CleanupRule [%d]: SearchVersions failed: %w", pcr.ID, err) - } - versionDeleted := false - for _, pv := range pvs { - if pcr.Type == packages_model.TypeContainer { - if skip, err := container_service.ShouldBeSkipped(ctx, pcr, p, pv); err != nil { - return fmt.Errorf("CleanupRule [%d]: container.ShouldBeSkipped failed: %w", pcr.ID, err) - } else if skip { - log.Debug("Rule[%d]: keep '%s/%s' (container)", pcr.ID, p.Name, pv.Version) - continue - } - } +func executeCleanupOneRule(ctx context.Context, pcr *packages_model.PackageCleanupRule) error { + if err := pcr.CompiledPattern(); err != nil { + return fmt.Errorf("CleanupRule [%d]: CompilePattern failed: %w", pcr.ID, err) + } - toMatch := pv.LowerVersion - if pcr.MatchFullName { - toMatch = p.LowerName + "/" + pv.LowerVersion - } + packages, err := packages_model.GetPackagesByType(ctx, pcr.OwnerID, pcr.Type) + if err != nil { + return fmt.Errorf("CleanupRule [%d]: GetPackagesByType failed: %w", pcr.ID, err) + } - if pcr.KeepPatternMatcher != nil && pcr.KeepPatternMatcher.MatchString(toMatch) { - log.Debug("Rule[%d]: keep '%s/%s' (keep pattern)", pcr.ID, p.Name, pv.Version) - continue - } - if pv.CreatedUnix.AsLocalTime().After(olderThan) { - log.Debug("Rule[%d]: keep '%s/%s' (remove days)", pcr.ID, p.Name, pv.Version) - continue - } - if pcr.RemovePatternMatcher != nil && !pcr.RemovePatternMatcher.MatchString(toMatch) { - log.Debug("Rule[%d]: keep '%s/%s' (remove pattern)", pcr.ID, p.Name, pv.Version) - continue + anyVersionDeleted := false + for _, p := range packages { + versionDeleted := false + err = db.WithTx(ctx, func(ctx context.Context) (err error) { + versionDeleted, err = executeCleanupOneRulePackage(ctx, pcr, p) + return err + }) + if err != nil { + log.Error("CleanupRule [%d]: executeCleanupOneRulePackage(%d) failed: %v", pcr.ID, p.ID, err) + continue + } + anyVersionDeleted = anyVersionDeleted || versionDeleted + if versionDeleted { + if pcr.Type == packages_model.TypeCargo { + owner, err := user_model.GetUserByID(ctx, pcr.OwnerID) + if err != nil { + return fmt.Errorf("GetUserByID failed: %w", err) } - - log.Debug("Rule[%d]: remove '%s/%s'", pcr.ID, p.Name, pv.Version) - - if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil { - return fmt.Errorf("CleanupRule [%d]: DeletePackageVersionAndReferences failed: %w", pcr.ID, err) + if err := cargo_service.UpdatePackageIndexIfExists(ctx, owner, owner, p.ID); err != nil { + return fmt.Errorf("CleanupRule [%d]: cargo.UpdatePackageIndexIfExists failed: %w", pcr.ID, err) } + } + } + } - versionDeleted = true - anyVersionDeleted = true + if anyVersionDeleted { + switch pcr.Type { + case packages_model.TypeDebian: + if err := debian_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { + return fmt.Errorf("CleanupRule [%d]: debian.BuildAllRepositoryFiles failed: %w", pcr.ID, err) + } + case packages_model.TypeAlpine: + if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { + return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } + case packages_model.TypeRpm: + if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { + return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err) + } + case packages_model.TypeArch: + release, err := arch_service.AquireRegistryLock(ctx, pcr.OwnerID) + if err != nil { + return err + } + defer release() - if versionDeleted { - if pcr.Type == packages_model.TypeCargo { - owner, err := user_model.GetUserByID(ctx, pcr.OwnerID) - if err != nil { - return fmt.Errorf("GetUserByID failed: %w", err) - } - if err := cargo_service.UpdatePackageIndexIfExists(ctx, owner, owner, p.ID); err != nil { - return fmt.Errorf("CleanupRule [%d]: cargo.UpdatePackageIndexIfExists failed: %w", pcr.ID, err) - } - } + if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { + return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err) } } + } + return nil +} - if anyVersionDeleted { - switch pcr.Type { - case packages_model.TypeDebian: - if err := debian_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { - return fmt.Errorf("CleanupRule [%d]: debian.BuildAllRepositoryFiles failed: %w", pcr.ID, err) - } - case packages_model.TypeAlpine: - if err := alpine_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { - return fmt.Errorf("CleanupRule [%d]: alpine.BuildAllRepositoryFiles failed: %w", pcr.ID, err) - } - case packages_model.TypeRpm: - if err := rpm_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { - return fmt.Errorf("CleanupRule [%d]: rpm.BuildAllRepositoryFiles failed: %w", pcr.ID, err) - } - case packages_model.TypeArch: - release, err := arch_service.AquireRegistryLock(ctx, pcr.OwnerID) - if err != nil { - return err - } - defer release() +func ExecuteCleanupRules(ctx context.Context) error { + return packages_model.IterateEnabledCleanupRules(ctx, func(ctx context.Context, pcr *packages_model.PackageCleanupRule) error { + select { + case <-ctx.Done(): + return db.ErrCancelledf("While processing package cleanup rules") + default: + } - if err := arch_service.BuildAllRepositoryFiles(ctx, pcr.OwnerID); err != nil { - return fmt.Errorf("CleanupRule [%d]: arch.BuildAllRepositoryFiles failed: %w", pcr.ID, err) - } - } + err := executeCleanupOneRule(ctx, pcr) + if err != nil { + log.Error("CleanupRule [%d]: executeCleanupOneRule failed: %v", pcr.ID, err) } return nil }) - if err != nil { - return err - } - - return committer.Commit() } func CleanupExpiredData(outerCtx context.Context, olderThan time.Duration) error { diff --git a/services/packages/container/cleanup.go b/services/packages/container/cleanup.go index 3f5f43bbc0..263562a396 100644 --- a/services/packages/container/cleanup.go +++ b/services/packages/container/cleanup.go @@ -13,7 +13,7 @@ import ( container_module "code.gitea.io/gitea/modules/packages/container" packages_service "code.gitea.io/gitea/services/packages" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" ) // Cleanup removes expired container data @@ -57,7 +57,7 @@ func cleanupExpiredUploadedBlobs(ctx context.Context, olderThan time.Duration) e Type: packages_model.TypeContainer, Version: packages_model.SearchValue{ ExactMatch: true, - Value: container_model.UploadVersion, + Value: container_module.UploadVersion, }, IsInternal: optional.Some(true), HasFiles: optional.Some(false), diff --git a/services/packages/container/common.go b/services/packages/container/common.go index 5a14ed5b7a..02cbff2286 100644 --- a/services/packages/container/common.go +++ b/services/packages/container/common.go @@ -5,11 +5,17 @@ package container import ( "context" + "io" "strings" packages_model "code.gitea.io/gitea/models/packages" + container_service "code.gitea.io/gitea/models/packages/container" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/packages" container_module "code.gitea.io/gitea/modules/packages/container" + + "github.com/opencontainers/image-spec/specs-go/v1" ) // UpdateRepositoryNames updates the repository name property for all packages of the specific owner @@ -22,7 +28,7 @@ func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwner newOwnerName = strings.ToLower(newOwnerName) for _, p := range ps { - if err := packages_model.DeletePropertyByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil { + if err := packages_model.DeletePropertiesByName(ctx, packages_model.PropertyTypePackage, p.ID, container_module.PropertyRepository); err != nil { return err } @@ -33,3 +39,26 @@ func UpdateRepositoryNames(ctx context.Context, owner *user_model.User, newOwner return nil } + +func ParseManifestMetadata(ctx context.Context, rd io.Reader, ownerID int64, imageName string) (*v1.Manifest, *packages_model.PackageFileDescriptor, *container_module.Metadata, error) { + var manifest v1.Manifest + if err := json.NewDecoder(rd).Decode(&manifest); err != nil { + return nil, nil, nil, err + } + configDescriptor, err := container_service.GetContainerBlob(ctx, &container_service.BlobSearchOptions{ + OwnerID: ownerID, + Image: imageName, + Digest: manifest.Config.Digest.String(), + }) + if err != nil { + return nil, nil, nil, err + } + + configReader, err := packages.NewContentStore().OpenBlob(packages.BlobHash256Key(configDescriptor.Blob.HashSHA256)) + if err != nil { + return nil, nil, nil, err + } + defer configReader.Close() + metadata, err := container_module.ParseImageConfig(manifest.Config.MediaType, configReader) + return &manifest, configDescriptor, metadata, err +} diff --git a/services/packages/packages.go b/services/packages/packages.go index bd1d460fd3..517334cbc7 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -563,8 +563,8 @@ func DeletePackageFile(ctx context.Context, pf *packages_model.PackageFile) erro return packages_model.DeleteFileByID(ctx, pf.ID) } -// GetFileStreamByPackageNameAndVersion returns the content of the specific package file -func GetFileStreamByPackageNameAndVersion(ctx context.Context, pvi *PackageInfo, pfi *PackageFileInfo) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { +// OpenFileForDownloadByPackageNameAndVersion returns the content of the specific package file and increases the download counter. +func OpenFileForDownloadByPackageNameAndVersion(ctx context.Context, pvi *PackageInfo, pfi *PackageFileInfo) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { log.Trace("Getting package file stream: %v, %v, %s, %s, %s, %s", pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version, pfi.Filename, pfi.CompositeKey) pv, err := packages_model.GetVersionByNameAndVersion(ctx, pvi.Owner.ID, pvi.PackageType, pvi.Name, pvi.Version) @@ -576,32 +576,38 @@ func GetFileStreamByPackageNameAndVersion(ctx context.Context, pvi *PackageInfo, return nil, nil, nil, err } - return GetFileStreamByPackageVersion(ctx, pv, pfi) + return OpenFileForDownloadByPackageVersion(ctx, pv, pfi) } -// GetFileStreamByPackageVersion returns the content of the specific package file -func GetFileStreamByPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfi *PackageFileInfo) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { +// OpenFileForDownloadByPackageVersion returns the content of the specific package file and increases the download counter. +func OpenFileForDownloadByPackageVersion(ctx context.Context, pv *packages_model.PackageVersion, pfi *PackageFileInfo) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, pfi.Filename, pfi.CompositeKey) if err != nil { return nil, nil, nil, err } - return GetPackageFileStream(ctx, pf) + return OpenFileForDownload(ctx, pf) } -// GetPackageFileStream returns the content of the specific package file -func GetPackageFileStream(ctx context.Context, pf *packages_model.PackageFile) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { +// OpenFileForDownload returns the content of the specific package file and increases the download counter. +func OpenFileForDownload(ctx context.Context, pf *packages_model.PackageFile) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { pb, err := packages_model.GetBlobByID(ctx, pf.BlobID) if err != nil { return nil, nil, nil, err } - return GetPackageBlobStream(ctx, pf, pb, nil) + return OpenBlobForDownload(ctx, pf, pb, nil) } -// GetPackageBlobStream returns the content of the specific package blob +func OpenBlobStream(pb *packages_model.PackageBlob) (io.ReadSeekCloser, error) { + cs := packages_module.NewContentStore() + key := packages_module.BlobHash256Key(pb.HashSHA256) + return cs.OpenBlob(key) +} + +// OpenBlobForDownload returns the content of the specific package blob and increases the download counter. // If the storage supports direct serving and it's enabled, only the direct serving url is returned. -func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, serveDirectReqParams url.Values) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { +func OpenBlobForDownload(ctx context.Context, pf *packages_model.PackageFile, pb *packages_model.PackageBlob, serveDirectReqParams url.Values) (io.ReadSeekCloser, *url.URL, *packages_model.PackageFile, error) { key := packages_module.BlobHash256Key(pb.HashSHA256) cs := packages_module.NewContentStore() @@ -617,7 +623,7 @@ func GetPackageBlobStream(ctx context.Context, pf *packages_model.PackageFile, p } } if u == nil { - s, err = cs.Get(key) + s, err = cs.OpenBlob(key) } if err == nil { diff --git a/services/packages/rpm/repository.go b/services/packages/rpm/repository.go index 5027021c52..fbbf8d7dad 100644 --- a/services/packages/rpm/repository.go +++ b/services/packages/rpm/repository.go @@ -470,7 +470,7 @@ func buildPrimary(ctx context.Context, pv *packages_model.PackageVersion, pfs [] } // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#filelists-xml -func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl +func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl // duplicates with buildOther type Version struct { Epoch string `xml:"epoch,attr"` Version string `xml:"ver,attr"` @@ -517,7 +517,7 @@ func buildFilelists(ctx context.Context, pv *packages_model.PackageVersion, pfs } // https://docs.pulpproject.org/en/2.19/plugins/pulp_rpm/tech-reference/rpm.html#other-xml -func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl +func buildOther(ctx context.Context, pv *packages_model.PackageVersion, pfs []*packages_model.PackageFile, c packageCache, group string) (*repoData, error) { //nolint:dupl // duplicates with buildFilelists type Version struct { Epoch string `xml:"epoch,attr"` Version string `xml:"ver,attr"` diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index 0bfff21746..7952ca6fe3 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -10,93 +10,59 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/structs" "github.com/gobwas/glob" "github.com/pkg/errors" ) // MergeRequiredContextsCommitStatus returns a commit status state for given required contexts -func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, requiredContexts []string) structs.CommitStatusState { - // matchedCount is the number of `CommitStatus.Context` that match any context of `requiredContexts` - matchedCount := 0 - returnedStatus := structs.CommitStatusSuccess - - if len(requiredContexts) > 0 { - requiredContextsGlob := make(map[string]glob.Glob, len(requiredContexts)) - for _, ctx := range requiredContexts { - if gp, err := glob.Compile(ctx); err != nil { - log.Error("glob.Compile %s failed. Error: %v", ctx, err) - } else { - requiredContextsGlob[ctx] = gp - } - } - - for _, gp := range requiredContextsGlob { - var targetStatus structs.CommitStatusState - for _, commitStatus := range commitStatuses { - if gp.Match(commitStatus.Context) { - targetStatus = commitStatus.State - matchedCount++ - break - } - } - - // If required rule not match any action, then it is pending - if targetStatus == "" { - if structs.CommitStatusPending.NoBetterThan(returnedStatus) { - returnedStatus = structs.CommitStatusPending - } - break - } - - if targetStatus.NoBetterThan(returnedStatus) { - returnedStatus = targetStatus - } - } +func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, requiredContexts []string) commitstatus.CommitStatusState { + if len(commitStatuses) == 0 { + return commitstatus.CommitStatusPending } - if matchedCount == 0 && returnedStatus == structs.CommitStatusSuccess { - status := git_model.CalcCommitStatus(commitStatuses) - if status != nil { - return status.State - } - return structs.CommitStatusSuccess + if len(requiredContexts) == 0 { + return git_model.CalcCommitStatus(commitStatuses).State } - return returnedStatus -} - -// IsCommitStatusContextSuccess returns true if all required status check contexts succeed. -func IsCommitStatusContextSuccess(commitStatuses []*git_model.CommitStatus, requiredContexts []string) bool { - // If no specific context is required, require that last commit status is a success - if len(requiredContexts) == 0 { - status := git_model.CalcCommitStatus(commitStatuses) - if status == nil || status.State != structs.CommitStatusSuccess { - return false + requiredContextsGlob := make(map[string]glob.Glob, len(requiredContexts)) + for _, ctx := range requiredContexts { + if gp, err := glob.Compile(ctx); err != nil { + log.Error("glob.Compile %s failed. Error: %v", ctx, err) + } else { + requiredContextsGlob[ctx] = gp } - return true } - for _, ctx := range requiredContexts { - var found bool + requiredCommitStatuses := make([]*git_model.CommitStatus, 0, len(commitStatuses)) + allRequiredContextsMatched := true + for _, gp := range requiredContextsGlob { + requiredContextMatched := false for _, commitStatus := range commitStatuses { - if commitStatus.Context == ctx { - if commitStatus.State != structs.CommitStatusSuccess { - return false - } - - found = true - break + if gp.Match(commitStatus.Context) { + requiredCommitStatuses = append(requiredCommitStatuses, commitStatus) + requiredContextMatched = true } } - if !found { - return false - } + allRequiredContextsMatched = allRequiredContextsMatched && requiredContextMatched + } + if len(requiredCommitStatuses) == 0 { + return commitstatus.CommitStatusPending + } + + returnedStatus := git_model.CalcCommitStatus(requiredCommitStatuses).State + if allRequiredContextsMatched { + return returnedStatus + } + + if returnedStatus == commitstatus.CommitStatusFailure { + return commitstatus.CommitStatusFailure } - return true + // even if part of success, return pending + return commitstatus.CommitStatusPending } // IsPullCommitStatusPass returns if all required status checks PASS @@ -117,7 +83,7 @@ func IsPullCommitStatusPass(ctx context.Context, pr *issues_model.PullRequest) ( } // GetPullRequestCommitStatusState returns pull request merged commit status state -func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (structs.CommitStatusState, error) { +func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullRequest) (commitstatus.CommitStatusState, error) { // Ensure HeadRepo is loaded if err := pr.LoadHeadRepo(ctx); err != nil { return "", errors.Wrap(err, "LoadHeadRepo") @@ -151,7 +117,7 @@ func GetPullRequestCommitStatusState(ctx context.Context, pr *issues_model.PullR return "", errors.Wrap(err, "LoadBaseRepo") } - commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll) + commitStatuses, err := git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll) if err != nil { return "", errors.Wrap(err, "GetLatestCommitStatus") } diff --git a/services/pull/commit_status_test.go b/services/pull/commit_status_test.go index 592acdd55c..a58e788c04 100644 --- a/services/pull/commit_status_test.go +++ b/services/pull/commit_status_test.go @@ -8,58 +8,85 @@ import ( "testing" git_model "code.gitea.io/gitea/models/git" - "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/commitstatus" "github.com/stretchr/testify/assert" ) func TestMergeRequiredContextsCommitStatus(t *testing.T) { - testCases := [][]*git_model.CommitStatus{ + cases := []struct { + commitStatuses []*git_model.CommitStatus + requiredContexts []string + expected commitstatus.CommitStatusState + }{ { - {Context: "Build 1", State: structs.CommitStatusSuccess}, - {Context: "Build 2", State: structs.CommitStatusSuccess}, - {Context: "Build 3", State: structs.CommitStatusSuccess}, + commitStatuses: []*git_model.CommitStatus{}, + requiredContexts: []string{}, + expected: commitstatus.CommitStatusPending, }, { - {Context: "Build 1", State: structs.CommitStatusSuccess}, - {Context: "Build 2", State: structs.CommitStatusSuccess}, - {Context: "Build 2t", State: structs.CommitStatusPending}, + commitStatuses: []*git_model.CommitStatus{ + {Context: "Build xxx", State: commitstatus.CommitStatusSkipped}, + }, + requiredContexts: []string{"Build*"}, + expected: commitstatus.CommitStatusSuccess, }, { - {Context: "Build 1", State: structs.CommitStatusSuccess}, - {Context: "Build 2", State: structs.CommitStatusSuccess}, - {Context: "Build 2t", State: structs.CommitStatusFailure}, + commitStatuses: []*git_model.CommitStatus{ + {Context: "Build 1", State: commitstatus.CommitStatusSkipped}, + {Context: "Build 2", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 3", State: commitstatus.CommitStatusSuccess}, + }, + requiredContexts: []string{"Build*"}, + expected: commitstatus.CommitStatusSuccess, }, { - {Context: "Build 1", State: structs.CommitStatusSuccess}, - {Context: "Build 2", State: structs.CommitStatusSuccess}, - {Context: "Build 2t", State: structs.CommitStatusSuccess}, + commitStatuses: []*git_model.CommitStatus{ + {Context: "Build 1", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2t", State: commitstatus.CommitStatusPending}, + }, + requiredContexts: []string{"Build*", "Build 2t*"}, + expected: commitstatus.CommitStatusPending, }, { - {Context: "Build 1", State: structs.CommitStatusSuccess}, - {Context: "Build 2", State: structs.CommitStatusSuccess}, - {Context: "Build 2t", State: structs.CommitStatusSuccess}, + commitStatuses: []*git_model.CommitStatus{ + {Context: "Build 1", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2t", State: commitstatus.CommitStatusFailure}, + }, + requiredContexts: []string{"Build*", "Build 2t*"}, + expected: commitstatus.CommitStatusFailure, + }, + { + commitStatuses: []*git_model.CommitStatus{ + {Context: "Build 1", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2t", State: commitstatus.CommitStatusFailure}, + }, + requiredContexts: []string{"Build*"}, + expected: commitstatus.CommitStatusFailure, + }, + { + commitStatuses: []*git_model.CommitStatus{ + {Context: "Build 1", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2t", State: commitstatus.CommitStatusSuccess}, + }, + requiredContexts: []string{"Build*", "Build 2t*", "Build 3*"}, + expected: commitstatus.CommitStatusPending, + }, + { + commitStatuses: []*git_model.CommitStatus{ + {Context: "Build 1", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2", State: commitstatus.CommitStatusSuccess}, + {Context: "Build 2t", State: commitstatus.CommitStatusSuccess}, + }, + requiredContexts: []string{"Build*", "Build *", "Build 2t*", "Build 1*"}, + expected: commitstatus.CommitStatusSuccess, }, } - testCasesRequiredContexts := [][]string{ - {"Build*"}, - {"Build*", "Build 2t*"}, - {"Build*", "Build 2t*"}, - {"Build*", "Build 2t*", "Build 3*"}, - {"Build*", "Build *", "Build 2t*", "Build 1*"}, - } - - testCasesExpected := []structs.CommitStatusState{ - structs.CommitStatusSuccess, - structs.CommitStatusPending, - structs.CommitStatusFailure, - structs.CommitStatusPending, - structs.CommitStatusSuccess, - } - - for i, commitStatuses := range testCases { - if MergeRequiredContextsCommitStatus(commitStatuses, testCasesRequiredContexts[i]) != testCasesExpected[i] { - assert.Fail(t, "Test case failed", "Test case %d failed", i+1) - } + for i, c := range cases { + assert.Equal(t, c.expected, MergeRequiredContextsCommitStatus(c.commitStatuses, c.requiredContexts), "case %d", i) } } diff --git a/services/pull/merge.go b/services/pull/merge.go index 256db847ef..cd9aeb2ad1 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -8,11 +8,13 @@ import ( "context" "errors" "fmt" + "maps" "os" "path/filepath" "regexp" "strconv" "strings" + "unicode" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -94,9 +96,7 @@ func getMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issue vars["HeadRepoOwnerName"] = pr.HeadRepo.OwnerName vars["HeadRepoName"] = pr.HeadRepo.Name } - for extraKey, extraValue := range extraVars { - vars[extraKey] = extraValue - } + maps.Copy(vars, extraVars) refs, err := pr.ResolveCrossReferences(ctx) if err == nil { closeIssueIndexes := make([]string, 0, len(refs)) @@ -161,6 +161,41 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr return getMergeMessage(ctx, baseGitRepo, pr, mergeStyle, nil) } +func AddCommitMessageTailer(message, tailerKey, tailerValue string) string { + tailerLine := tailerKey + ": " + tailerValue + message = strings.ReplaceAll(message, "\r\n", "\n") + message = strings.ReplaceAll(message, "\r", "\n") + if strings.Contains(message, "\n"+tailerLine+"\n") || strings.HasSuffix(message, "\n"+tailerLine) { + return message + } + + if !strings.HasSuffix(message, "\n") { + message += "\n" + } + pos1 := strings.LastIndexByte(message[:len(message)-1], '\n') + pos2 := -1 + if pos1 != -1 { + pos2 = strings.IndexByte(message[pos1:], ':') + if pos2 != -1 { + pos2 += pos1 + } + } + var lastLineKey string + if pos1 != -1 && pos2 != -1 { + lastLineKey = message[pos1+1 : pos2] + } + + isLikelyTailerLine := lastLineKey != "" && unicode.IsUpper(rune(lastLineKey[0])) && strings.Contains(message, "-") + for i := 0; isLikelyTailerLine && i < len(lastLineKey); i++ { + r := rune(lastLineKey[i]) + isLikelyTailerLine = unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' + } + if !strings.HasSuffix(message, "\n\n") && !isLikelyTailerLine { + message += "\n" + } + return message + tailerLine +} + // ErrInvalidMergeStyle represents an error if merging with disabled merge strategy type ErrInvalidMergeStyle struct { ID int64 @@ -290,7 +325,7 @@ func handleCloseCrossReferences(ctx context.Context, pr *issues_model.PullReques } // doMergeAndPush performs the merge operation without changing any pull information in database and pushes it up to the base repository -func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, pushTrigger repo_module.PushTrigger) (string, error) { //nolint:unparam +func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, mergeStyle repo_model.MergeStyle, expectedHeadCommitID, message string, pushTrigger repo_module.PushTrigger) (string, error) { //nolint:unparam // non-error result is never used // Clone base repo. mergeCtx, cancel, err := createTemporaryRepoForMerge(ctx, pr, doer, expectedHeadCommitID) if err != nil { @@ -396,10 +431,13 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use func commitAndSignNoAuthor(ctx *mergeContext, message string) error { cmdCommit := git.NewCommand("commit").AddOptionFormat("--message=%s", message) - if ctx.signKeyID == "" { + if ctx.signKey == nil { cmdCommit.AddArguments("--no-gpg-sign") } else { - cmdCommit.AddOptionFormat("-S%s", ctx.signKeyID) + if ctx.signKey.Format != "" { + cmdCommit.AddConfig("gpg.format", ctx.signKey.Format) + } + cmdCommit.AddOptionFormat("-S%s", ctx.signKey.KeyID) } if err := cmdCommit.Run(ctx, ctx.RunOpts()); err != nil { log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index 719cc6b965..31a1e13734 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -27,7 +27,7 @@ type mergeContext struct { doer *user_model.User sig *git.Signature committer *git.Signature - signKeyID string // empty for no-sign, non-empty to sign + signKey *git.SigningKey env []string } @@ -99,9 +99,9 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque mergeCtx.committer = mergeCtx.sig // Determine if we should sign - sign, keyID, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch) + sign, key, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch) if sign { - mergeCtx.signKeyID = keyID + mergeCtx.signKey = key if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { mergeCtx.committer = signer } diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 72660cd3c5..0049c0b117 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -5,7 +5,6 @@ package pull import ( "fmt" - "strings" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -66,18 +65,19 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error { if setting.Repository.PullRequest.AddCoCommitterTrailers && ctx.committer.String() != sig.String() { // add trailer - if !strings.Contains(message, "Co-authored-by: "+sig.String()) { - message += "\nCo-authored-by: " + sig.String() - } - message += fmt.Sprintf("\nCo-committed-by: %s\n", sig.String()) + message = AddCommitMessageTailer(message, "Co-authored-by", sig.String()) + message = AddCommitMessageTailer(message, "Co-committed-by", sig.String()) // FIXME: this one should be removed, it is not really used or widely used } cmdCommit := git.NewCommand("commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). AddOptionFormat("--message=%s", message) - if ctx.signKeyID == "" { + if ctx.signKey == nil { cmdCommit.AddArguments("--no-gpg-sign") } else { - cmdCommit.AddOptionFormat("-S%s", ctx.signKeyID) + if ctx.signKey.Format != "" { + cmdCommit.AddConfig("gpg.format", ctx.signKey.Format) + } + cmdCommit.AddOptionFormat("-S%s", ctx.signKey.KeyID) } if err := cmdCommit.Run(ctx, ctx.RunOpts()); err != nil { log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) diff --git a/services/pull/merge_test.go b/services/pull/merge_test.go index 6df6f55d46..91abeb9d9c 100644 --- a/services/pull/merge_test.go +++ b/services/pull/merge_test.go @@ -65,3 +65,28 @@ func Test_expandDefaultMergeMessage(t *testing.T) { }) } } + +func TestAddCommitMessageTailer(t *testing.T) { + // add tailer for empty message + assert.Equal(t, "\n\nTest-tailer: TestValue", AddCommitMessageTailer("", "Test-tailer", "TestValue")) + + // add tailer for message without newlines + assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTailer("title", "Test-tailer", "TestValue")) + assert.Equal(t, "title\n\nNot tailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\nNot tailer: xxx", "Test-tailer", "TestValue")) + assert.Equal(t, "title\n\nNotTailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\nNotTailer: xxx", "Test-tailer", "TestValue")) + assert.Equal(t, "title\n\nnot-tailer: xxx\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\nnot-tailer: xxx", "Test-tailer", "TestValue")) + + // add tailer for message with one EOL + assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n", "Test-tailer", "TestValue")) + + // add tailer for message with two EOLs + assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\n", "Test-tailer", "TestValue")) + + // add tailer for message with existing tailer (won't duplicate) + assert.Equal(t, "title\n\nTest-tailer: TestValue", AddCommitMessageTailer("title\n\nTest-tailer: TestValue", "Test-tailer", "TestValue")) + assert.Equal(t, "title\n\nTest-tailer: TestValue\n", AddCommitMessageTailer("title\n\nTest-tailer: TestValue\n", "Test-tailer", "TestValue")) + + // add tailer for message with existing tailer and different value (will append) + assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTailer("title\n\nTest-tailer: v1", "Test-tailer", "v2")) + assert.Equal(t, "title\n\nTest-tailer: v1\nTest-tailer: v2", AddCommitMessageTailer("title\n\nTest-tailer: v1\n", "Test-tailer", "v2")) +} diff --git a/services/pull/pull.go b/services/pull/pull.go index 81be797832..701c4f4d32 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -945,12 +945,6 @@ func GetSquashMergeCommitMessages(ctx context.Context, pr *issues_model.PullRequ return stringBuilder.String() } -// GetIssuesLastCommitStatus returns a map of issue ID to the most recent commit's latest status -func GetIssuesLastCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64]*git_model.CommitStatus, error) { - _, lastStatus, err := GetIssuesAllCommitStatus(ctx, issues) - return lastStatus, err -} - // GetIssuesAllCommitStatus returns a map of issue ID to a list of all statuses for the most recent commit as well as a map of issue ID to only the commit's latest status func GetIssuesAllCommitStatus(ctx context.Context, issues issues_model.IssueList) (map[int64][]*git_model.CommitStatus, map[int64]*git_model.CommitStatus, error) { if err := issues.LoadPullRequests(ctx); err != nil { @@ -1004,7 +998,7 @@ func getAllCommitStatus(ctx context.Context, gitRepo *git.Repository, pr *issues return nil, nil, shaErr } - statuses, _, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll) + statuses, err = git_model.GetLatestCommitStatus(ctx, pr.BaseRepo.ID, sha, db.ListOptionsAll) lastStatus = git_model.CalcCommitStatus(statuses) return statuses, lastStatus, err } diff --git a/services/pull/reviewer.go b/services/pull/reviewer.go index bf0d8cb298..52f2f3401c 100644 --- a/services/pull/reviewer.go +++ b/services/pull/reviewer.go @@ -85,5 +85,5 @@ func GetReviewerTeams(ctx context.Context, repo *repo_model.Repository) ([]*orga return nil, nil } - return organization.GetTeamsWithAccessToRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests) + return organization.GetTeamsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypePullRequests) } diff --git a/services/pull/update.go b/services/pull/update.go index 5cc5e2b134..b8f84e3d65 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -41,22 +41,6 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. return fmt.Errorf("HeadBranch of PR %d is up to date", pr.Index) } - if rebase { - defer func() { - go AddTestPullRequestTask(TestPullRequestOptions{ - RepoID: pr.BaseRepo.ID, - Doer: doer, - Branch: pr.BaseBranch, - IsSync: false, - IsForcePush: false, - OldCommitID: "", - NewCommitID: "", - }) - }() - - return updateHeadByRebaseOnToBase(ctx, pr, doer) - } - if err := pr.LoadBaseRepo(ctx); err != nil { log.Error("unable to load BaseRepo for %-v during update-by-merge: %v", pr, err) return fmt.Errorf("unable to load BaseRepo for PR[%d] during update-by-merge: %w", pr.ID, err) @@ -74,6 +58,22 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err) } + defer func() { + go AddTestPullRequestTask(TestPullRequestOptions{ + RepoID: pr.BaseRepo.ID, + Doer: doer, + Branch: pr.BaseBranch, + IsSync: false, + IsForcePush: false, + OldCommitID: "", + NewCommitID: "", + }) + }() + + if rebase { + return updateHeadByRebaseOnToBase(ctx, pr, doer) + } + // TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment // ideally in the future the "merge" functions should be refactored to decouple from the PullRequest // now use a fake reverse PR to switch head&base repos/branches @@ -90,19 +90,6 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. } _, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase) - - defer func() { - go AddTestPullRequestTask(TestPullRequestOptions{ - RepoID: reversePR.HeadRepo.ID, - Doer: doer, - Branch: reversePR.HeadBranch, - IsSync: false, - IsForcePush: false, - OldCommitID: "", - NewCommitID: "", - }) - }() - return err } diff --git a/services/repository/adopt.go b/services/repository/adopt.go index 97ba22ace5..2bd1c55de4 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -196,8 +196,13 @@ func adoptRepository(ctx context.Context, repo *repo_model.Repository, defaultBr return fmt.Errorf("setDefaultBranch: %w", err) } } - if err = updateRepository(ctx, repo, false); err != nil { - return fmt.Errorf("updateRepository: %w", err) + + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_empty", "default_branch"); err != nil { + return fmt.Errorf("UpdateRepositoryCols: %w", err) + } + + if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) } return nil @@ -260,7 +265,7 @@ func checkUnadoptedRepositories(ctx context.Context, userName string, repoNamesT } return err } - repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{ + repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ Actor: ctxUser, Private: true, ListOptions: db.ListOptions{ diff --git a/services/repository/adopt_test.go b/services/repository/adopt_test.go index 6e1dc417b3..86f586c748 100644 --- a/services/repository/adopt_test.go +++ b/services/repository/adopt_test.go @@ -29,7 +29,7 @@ func TestCheckUnadoptedRepositories_Add(t *testing.T) { } total := 30 - for i := 0; i < total; i++ { + for range total { unadopted.add("something") } diff --git a/services/repository/check.go b/services/repository/check.go index b475fbc487..ffcd5ac749 100644 --- a/services/repository/check.go +++ b/services/repository/check.go @@ -162,7 +162,7 @@ func DeleteMissingRepositories(ctx context.Context, doer *user_model.User) error default: } log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) - if err := DeleteRepositoryDirectly(ctx, doer, repo.ID); err != nil { + if err := DeleteRepositoryDirectly(ctx, repo.ID); err != nil { log.Error("Failed to DeleteRepository %-v: Error: %v", repo, err) if err2 := system_model.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err) diff --git a/services/repository/commitstatus/commitstatus.go b/services/repository/commitstatus/commitstatus.go index f369a303e6..fa7a89882a 100644 --- a/services/repository/commitstatus/commitstatus.go +++ b/services/repository/commitstatus/commitstatus.go @@ -14,17 +14,17 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/services/notify" ) func getCacheKey(repoID int64, brancheName string) string { - hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%d:%s", repoID, brancheName))) + hashBytes := sha256.Sum256(fmt.Appendf(nil, "%d:%s", repoID, brancheName)) return fmt.Sprintf("commit_status:%x", hashBytes) } @@ -47,7 +47,7 @@ func getCommitStatusCache(repoID int64, branchName string) *commitStatusCacheVal return nil } -func updateCommitStatusCache(repoID int64, branchName string, state api.CommitStatusState, targetURL string) error { +func updateCommitStatusCache(repoID int64, branchName string, state commitstatus.CommitStatusState, targetURL string) error { c := cache.GetCache() bs, err := json.Marshal(commitStatusCacheValue{ State: state.String(), @@ -127,7 +127,7 @@ func FindReposLastestCommitStatuses(ctx context.Context, repos []*repo_model.Rep for i, repo := range repos { if cv := getCommitStatusCache(repo.ID, repo.DefaultBranch); cv != nil { results[i] = &git_model.CommitStatus{ - State: api.CommitStatusState(cv.State), + State: commitstatus.CommitStatusState(cv.State), TargetURL: cv.TargetURL, } } else { diff --git a/services/repository/create.go b/services/repository/create.go index 83d7d84c08..bed02e5d7e 100644 --- a/services/repository/create.go +++ b/services/repository/create.go @@ -100,8 +100,8 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir // .gitignore if len(opts.Gitignores) > 0 { var buf bytes.Buffer - names := strings.Split(opts.Gitignores, ",") - for _, name := range names { + names := strings.SplitSeq(opts.Gitignores, ",") + for name := range names { data, err = options.Gitignore(name) if err != nil { return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) @@ -191,10 +191,14 @@ func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Re } } - if err = UpdateRepository(ctx, repo, false); err != nil { + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_empty", "default_branch", "default_wiki_branch"); err != nil { return fmt.Errorf("updateRepository: %w", err) } + if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) + } + return nil } @@ -259,7 +263,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, owner *user_model.User, defer func() { if err != nil { // we can not use the ctx because it maybe canceled or timeout - cleanupRepository(doer, repo.ID) + cleanupRepository(repo.ID) } }() @@ -454,8 +458,8 @@ func createRepositoryInDB(ctx context.Context, doer, u *user_model.User, repo *r return nil } -func cleanupRepository(doer *user_model.User, repoID int64) { - if errDelete := DeleteRepositoryDirectly(db.DefaultContext, doer, repoID); errDelete != nil { +func cleanupRepository(repoID int64) { + if errDelete := DeleteRepositoryDirectly(db.DefaultContext, repoID); errDelete != nil { log.Error("cleanupRepository failed: %v", errDelete) // add system notice if err := system_model.CreateRepositoryNotice("DeleteRepositoryDirectly failed when cleanup repository: %v", errDelete); err != nil { diff --git a/services/repository/create_test.go b/services/repository/create_test.go index 8e3fdf88a5..fe464c1441 100644 --- a/services/repository/create_test.go +++ b/services/repository/create_test.go @@ -35,7 +35,7 @@ func TestCreateRepositoryDirectly(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: createdRepo.Name}) - err = DeleteRepositoryDirectly(db.DefaultContext, user2, createdRepo.ID) + err = DeleteRepositoryDirectly(db.DefaultContext, createdRepo.ID) assert.NoError(t, err) // a failed creating because some mock data diff --git a/services/repository/delete.go b/services/repository/delete.go index 046159722a..c48d6e1d56 100644 --- a/services/repository/delete.go +++ b/services/repository/delete.go @@ -29,6 +29,7 @@ import ( "code.gitea.io/gitea/modules/storage" actions_service "code.gitea.io/gitea/services/actions" asymkey_service "code.gitea.io/gitea/services/asymkey" + issue_service "code.gitea.io/gitea/services/issue" "xorm.io/builder" ) @@ -48,7 +49,7 @@ func deleteDBRepository(ctx context.Context, repoID int64) error { // DeleteRepository deletes a repository for a user or organization. // make sure if you call this func to close open sessions (sqlite will otherwise get a deadlock) -func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID int64, ignoreOrgTeams ...bool) error { +func DeleteRepositoryDirectly(ctx context.Context, repoID int64, ignoreOrgTeams ...bool) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err @@ -193,7 +194,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID // Delete Issues and related objects var attachmentPaths []string - if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil { + if attachmentPaths, err = issue_service.DeleteIssuesByRepoID(ctx, repoID); err != nil { return err } @@ -374,7 +375,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID // DeleteOwnerRepositoriesDirectly calls DeleteRepositoryDirectly for all repos of the given owner func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User) error { for { - repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{ + repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{ ListOptions: db.ListOptions{ PageSize: repo_model.RepositoryListDefaultPageSize, Page: 1, @@ -390,7 +391,7 @@ func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User break } for _, repo := range repos { - if err := DeleteRepositoryDirectly(ctx, owner, repo.ID); err != nil { + if err := DeleteRepositoryDirectly(ctx, repo.ID); err != nil { return fmt.Errorf("unable to delete repository %s for %s[%d]. Error: %w", repo.Name, owner.Name, owner.ID, err) } } diff --git a/services/repository/files/content.go b/services/repository/files/content.go index 7a07a0ddca..2c1e88bb59 100644 --- a/services/repository/files/content.go +++ b/services/repository/files/content.go @@ -5,13 +5,14 @@ package files import ( "context" - "fmt" + "io" "net/url" "path" + "strings" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" + "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -34,54 +35,52 @@ func (ct *ContentType) String() string { return string(*ct) } +type GetContentsOrListOptions struct { + TreePath string + IncludeSingleFileContent bool // include the file's content when the tree path is a file + IncludeLfsMetadata bool + IncludeCommitMetadata bool + IncludeCommitMessage bool +} + // GetContentsOrList gets the metadata of a file's contents (*ContentsResponse) if treePath not a tree // directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag -func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string) (any, error) { - if repo.IsEmpty { - return make([]any, 0), nil - } - - // Check that the path given in opts.treePath is valid (not a git path) - cleanTreePath := CleanUploadFileName(treePath) - if cleanTreePath == "" && treePath != "" { - return nil, ErrFilenameInvalid{ - Path: treePath, - } +func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (ret api.ContentsExtResponse, _ error) { + entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath) + if repo.IsEmpty && opts.TreePath == "" { + return api.ContentsExtResponse{DirContents: make([]*api.ContentsResponse, 0)}, nil } - treePath = cleanTreePath - - // Get the commit object for the ref - commit := refCommit.Commit - - entry, err := commit.GetTreeEntryByPath(treePath) if err != nil { - return nil, err + return ret, err } + // get file contents if entry.Type() != "tree" { - return GetContents(ctx, repo, refCommit, treePath, false) + ret.FileContents, err = getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts) + return ret, err } - // We are in a directory, so we return a list of FileContentResponse objects - var fileList []*api.ContentsResponse - - gitTree, err := commit.SubTree(treePath) + // list directory contents + gitTree, err := refCommit.Commit.SubTree(opts.TreePath) if err != nil { - return nil, err + return ret, err } entries, err := gitTree.ListEntries() if err != nil { - return nil, err + return ret, err } + ret.DirContents = make([]*api.ContentsResponse, 0, len(entries)) for _, e := range entries { - subTreePath := path.Join(treePath, e.Name()) - fileContentResponse, err := GetContents(ctx, repo, refCommit, subTreePath, true) + subOpts := opts + subOpts.TreePath = path.Join(opts.TreePath, e.Name()) + subOpts.IncludeSingleFileContent = false // never include file content when listing a directory + fileContentResponse, err := GetFileContents(ctx, repo, gitRepo, refCommit, subOpts) if err != nil { - return nil, err + return ret, err } - fileList = append(fileList, fileContentResponse) + ret.DirContents = append(ret.DirContents, fileContentResponse) } - return fileList, nil + return ret, nil } // GetObjectTypeFromTreeEntry check what content is behind it @@ -100,83 +99,96 @@ func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType { } } -// GetContents gets the metadata on a file's contents. Ref can be a branch, commit or tag -func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePath string, forList bool) (*api.ContentsResponse, error) { +func prepareGetContentsEntry(refCommit *utils.RefCommit, treePath *string) (*git.TreeEntry, error) { // Check that the path given in opts.treePath is valid (not a git path) - cleanTreePath := CleanUploadFileName(treePath) - if cleanTreePath == "" && treePath != "" { - return nil, ErrFilenameInvalid{ - Path: treePath, - } - } - treePath = cleanTreePath - - gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) - if err != nil { - return nil, err - } - defer closer.Close() - - commit := refCommit.Commit - entry, err := commit.GetTreeEntryByPath(treePath) - if err != nil { - return nil, err + cleanTreePath := CleanGitTreePath(*treePath) + if cleanTreePath == "" && *treePath != "" { + return nil, ErrFilenameInvalid{Path: *treePath} } + *treePath = cleanTreePath + // Only allow safe ref types refType := refCommit.RefName.RefType() if refType != git.RefTypeBranch && refType != git.RefTypeTag && refType != git.RefTypeCommit { - return nil, fmt.Errorf("no commit found for the ref [ref: %s]", refCommit.RefName) + return nil, util.NewNotExistErrorf("no commit found for the ref [ref: %s]", refCommit.RefName) } - selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(treePath) + "?ref=" + url.QueryEscape(refCommit.InputRef)) - if err != nil { - return nil, err - } - selfURLString := selfURL.String() + return refCommit.Commit.GetTreeEntryByPath(*treePath) +} - err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID) +// GetFileContents gets the metadata on a file's contents. Ref can be a branch, commit or tag +func GetFileContents(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, opts GetContentsOrListOptions) (*api.ContentsResponse, error) { + entry, err := prepareGetContentsEntry(refCommit, &opts.TreePath) if err != nil { return nil, err } + return getFileContentsByEntryInternal(ctx, repo, gitRepo, refCommit, entry, opts) +} - lastCommit, err := commit.GetCommitByPath(treePath) +func getFileContentsByEntryInternal(_ context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, entry *git.TreeEntry, opts GetContentsOrListOptions) (*api.ContentsResponse, error) { + refType := refCommit.RefName.RefType() + commit := refCommit.Commit + selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(opts.TreePath) + "?ref=" + url.QueryEscape(refCommit.InputRef)) if err != nil { return nil, err } + selfURLString := selfURL.String() // All content types have these fields in populated contentsResponse := &api.ContentsResponse{ - Name: entry.Name(), - Path: treePath, - SHA: entry.ID.String(), - LastCommitSHA: lastCommit.ID.String(), - Size: entry.Size(), - URL: &selfURLString, + Name: entry.Name(), + Path: opts.TreePath, + SHA: entry.ID.String(), + Size: entry.Size(), + URL: &selfURLString, Links: &api.FileLinksResponse{ Self: &selfURLString, }, } - // GitHub doesn't have these fields in the response, but we could follow other similar APIs to name them - // https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits - if lastCommit.Committer != nil { - contentsResponse.LastCommitterDate = lastCommit.Committer.When - } - if lastCommit.Author != nil { - contentsResponse.LastAuthorDate = lastCommit.Author.When + if opts.IncludeCommitMetadata || opts.IncludeCommitMessage { + err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(refCommit.InputRef, refType != git.RefTypeCommit), repo.FullName(), refCommit.CommitID) + if err != nil { + return nil, err + } + + lastCommit, err := refCommit.Commit.GetCommitByPath(opts.TreePath) + if err != nil { + return nil, err + } + + if opts.IncludeCommitMetadata { + contentsResponse.LastCommitSHA = util.ToPointer(lastCommit.ID.String()) + // GitHub doesn't have these fields in the response, but we could follow other similar APIs to name them + // https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#list-commits + if lastCommit.Committer != nil { + contentsResponse.LastCommitterDate = util.ToPointer(lastCommit.Committer.When) + } + if lastCommit.Author != nil { + contentsResponse.LastAuthorDate = util.ToPointer(lastCommit.Author.When) + } + } + if opts.IncludeCommitMessage { + contentsResponse.LastCommitMessage = util.ToPointer(lastCommit.Message()) + } } - // Now populate the rest of the ContentsResponse based on entry type + // Now populate the rest of the ContentsResponse based on the entry type if entry.IsRegular() || entry.IsExecutable() { contentsResponse.Type = string(ContentTypeRegular) // if it is listing the repo root dir, don't waste system resources on reading content - if !forList { - blobResponse, err := GetBlobBySHA(ctx, repo, gitRepo, entry.ID.String()) + if opts.IncludeSingleFileContent { + blobResponse, err := GetBlobBySHA(repo, gitRepo, entry.ID.String()) + if err != nil { + return nil, err + } + contentsResponse.Encoding, contentsResponse.Content = blobResponse.Encoding, blobResponse.Content + contentsResponse.LfsOid, contentsResponse.LfsSize = blobResponse.LfsOid, blobResponse.LfsSize + } else if opts.IncludeLfsMetadata { + contentsResponse.LfsOid, contentsResponse.LfsSize, err = parsePossibleLfsPointerBlob(gitRepo, entry.ID.String()) if err != nil { return nil, err } - contentsResponse.Encoding = blobResponse.Encoding - contentsResponse.Content = blobResponse.Content } } else if entry.IsDir() { contentsResponse.Type = string(ContentTypeDir) @@ -190,7 +202,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut contentsResponse.Target = &targetFromContent } else if entry.IsSubModule() { contentsResponse.Type = string(ContentTypeSubmodule) - submodule, err := commit.GetSubModule(treePath) + submodule, err := commit.GetSubModule(opts.TreePath) if err != nil { return nil, err } @@ -200,7 +212,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut } // Handle links if entry.IsRegular() || entry.IsLink() || entry.IsExecutable() { - downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(treePath)) + downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath)) if err != nil { return nil, err } @@ -208,7 +220,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut contentsResponse.DownloadURL = &downloadURLString } if !entry.IsSubModule() { - htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(treePath)) + htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + refCommit.RefName.RefWebLinkPath() + "/" + util.PathEscapeSegments(opts.TreePath)) if err != nil { return nil, err } @@ -228,8 +240,7 @@ func GetContents(ctx context.Context, repo *repo_model.Repository, refCommit *ut return contentsResponse, nil } -// GetBlobBySHA get the GitBlobResponse of a repository using a sha hash. -func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) { +func GetBlobBySHA(repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) { gitBlob, err := gitRepo.GetBlob(sha) if err != nil { return nil, err @@ -239,12 +250,49 @@ func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git URL: repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()), Size: gitBlob.Size(), } - if gitBlob.Size() <= setting.API.DefaultMaxBlobSize { - content, err := gitBlob.GetBlobContentBase64() - if err != nil { - return nil, err - } - ret.Encoding, ret.Content = util.ToPointer("base64"), &content + + blobSize := gitBlob.Size() + if blobSize > setting.API.DefaultMaxBlobSize { + return ret, nil + } + + var originContent *strings.Builder + if 0 < blobSize && blobSize < lfs.MetaFileMaxSize { + originContent = &strings.Builder{} + } + + content, err := gitBlob.GetBlobContentBase64(originContent) + if err != nil { + return nil, err + } + + ret.Encoding, ret.Content = util.ToPointer("base64"), &content + if originContent != nil { + ret.LfsOid, ret.LfsSize = parsePossibleLfsPointerBuffer(strings.NewReader(originContent.String())) } return ret, nil } + +func parsePossibleLfsPointerBuffer(r io.Reader) (*string, *int64) { + p, _ := lfs.ReadPointer(r) + if p.IsValid() { + return &p.Oid, &p.Size + } + return nil, nil +} + +func parsePossibleLfsPointerBlob(gitRepo *git.Repository, sha string) (*string, *int64, error) { + gitBlob, err := gitRepo.GetBlob(sha) + if err != nil { + return nil, nil, err + } + if gitBlob.Size() > lfs.MetaFileMaxSize { + return nil, nil, nil // not a LFS pointer + } + buf, err := gitBlob.GetBlobContent(lfs.MetaFileMaxSize) + if err != nil { + return nil, nil, err + } + oid, size := parsePossibleLfsPointerBuffer(strings.NewReader(buf)) + return oid, size, nil +} diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index eb10f5c9b1..d72f918074 100644 --- a/services/repository/files/content_test.go +++ b/services/repository/files/content_test.go @@ -5,57 +5,21 @@ package files import ( "testing" - "time" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/gitrepo" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/contexttest" _ "code.gitea.io/gitea/models/actions" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { unittest.MainTest(m) } -func getExpectedReadmeContentsResponse() *api.ContentsResponse { - treePath := "README.md" - sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f" - encoding := "base64" - content := "IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x" - selfURL := "https://try.gitea.io/api/v1/repos/user2/repo1/contents/" + treePath + "?ref=master" - htmlURL := "https://try.gitea.io/user2/repo1/src/branch/master/" + treePath - gitURL := "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/" + sha - downloadURL := "https://try.gitea.io/user2/repo1/raw/branch/master/" + treePath - return &api.ContentsResponse{ - Name: treePath, - Path: treePath, - SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", - LastCommitSHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", - LastCommitterDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)), - LastAuthorDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)), - Type: "file", - Size: 30, - Encoding: &encoding, - Content: &content, - URL: &selfURL, - HTMLURL: &htmlURL, - GitURL: &gitURL, - DownloadURL: &downloadURL, - Links: &api.FileLinksResponse{ - Self: &selfURL, - GitURL: &gitURL, - HTMLURL: &htmlURL, - }, - } -} - func TestGetContents(t *testing.T) { unittest.PrepareTestEnv(t) ctx, _ := contexttest.MockContext(t, "user2/repo1") @@ -64,146 +28,22 @@ func TestGetContents(t *testing.T) { contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - treePath := "README.md" - refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch) - require.NoError(t, err) - expectedContentsResponse := getExpectedReadmeContentsResponse() - - t.Run("Get README.md contents with GetContents(ctx, )", func(t *testing.T) { - fileContentResponse, err := GetContents(ctx, ctx.Repo.Repository, refCommit, treePath, false) - assert.Equal(t, expectedContentsResponse, fileContentResponse) + // GetContentsOrList's behavior is fully tested in integration tests, so we don't need to test it here. + + t.Run("GetBlobBySHA", func(t *testing.T) { + sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" + ctx.SetPathParam("id", "1") + ctx.SetPathParam("sha", sha) + gbr, err := GetBlobBySHA(ctx.Repo.Repository, ctx.Repo.GitRepo, ctx.PathParam("sha")) + expectedGBR := &api.GitBlobResponse{ + Content: util.ToPointer("dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"), + Encoding: util.ToPointer("base64"), + URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d", + SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", + Size: 180, + } assert.NoError(t, err) + assert.Equal(t, expectedGBR, gbr) }) } - -func TestGetContentsOrListForDir(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - treePath := "" // root dir - refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch) - require.NoError(t, err) - - readmeContentsResponse := getExpectedReadmeContentsResponse() - // because will be in a list, doesn't have encoding and content - readmeContentsResponse.Encoding = nil - readmeContentsResponse.Content = nil - - expectedContentsListResponse := []*api.ContentsResponse{ - readmeContentsResponse, - } - - t.Run("Get root dir contents with GetContentsOrList(ctx, )", func(t *testing.T) { - fileContentResponse, err := GetContentsOrList(ctx, ctx.Repo.Repository, refCommit, treePath) - assert.EqualValues(t, expectedContentsListResponse, fileContentResponse) - assert.NoError(t, err) - }) -} - -func TestGetContentsOrListForFile(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - treePath := "README.md" - refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch) - require.NoError(t, err) - - expectedContentsResponse := getExpectedReadmeContentsResponse() - - t.Run("Get README.md contents with GetContentsOrList(ctx, )", func(t *testing.T) { - fileContentResponse, err := GetContentsOrList(ctx, ctx.Repo.Repository, refCommit, treePath) - assert.EqualValues(t, expectedContentsResponse, fileContentResponse) - assert.NoError(t, err) - }) -} - -func TestGetContentsErrors(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - repo := ctx.Repo.Repository - refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch) - require.NoError(t, err) - - t.Run("bad treePath", func(t *testing.T) { - badTreePath := "bad/tree.md" - fileContentResponse, err := GetContents(ctx, repo, refCommit, badTreePath, false) - assert.Error(t, err) - assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]") - assert.Nil(t, fileContentResponse) - }) -} - -func TestGetContentsOrListErrors(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - ctx.SetPathParam("id", "1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - repo := ctx.Repo.Repository - refCommit, err := utils.ResolveRefCommit(ctx, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch) - require.NoError(t, err) - - t.Run("bad treePath", func(t *testing.T) { - badTreePath := "bad/tree.md" - fileContentResponse, err := GetContentsOrList(ctx, repo, refCommit, badTreePath) - assert.Error(t, err) - assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]") - assert.Nil(t, fileContentResponse) - }) -} - -func TestGetBlobBySHA(t *testing.T) { - unittest.PrepareTestEnv(t) - ctx, _ := contexttest.MockContext(t, "user2/repo1") - contexttest.LoadRepo(t, ctx, 1) - contexttest.LoadRepoCommit(t, ctx) - contexttest.LoadUser(t, ctx, 2) - contexttest.LoadGitRepo(t, ctx) - defer ctx.Repo.GitRepo.Close() - - sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" - ctx.SetPathParam("id", "1") - ctx.SetPathParam("sha", sha) - - gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository) - if err != nil { - t.Fail() - } - - gbr, err := GetBlobBySHA(ctx, ctx.Repo.Repository, gitRepo, ctx.PathParam("sha")) - expectedGBR := &api.GitBlobResponse{ - Content: util.ToPointer("dHJlZSAyYTJmMWQ0NjcwNzI4YTJlMTAwNDllMzQ1YmQ3YTI3NjQ2OGJlYWI2CmF1dGhvciB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+IDE0ODk5NTY0NzkgLTA0MDAKY29tbWl0dGVyIEV0aGFuIEtvZW5pZyA8ZXRoYW50a29lbmlnQGdtYWlsLmNvbT4gMTQ4OTk1NjQ3OSAtMDQwMAoKSW5pdGlhbCBjb21taXQK"), - Encoding: util.ToPointer("base64"), - URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d", - SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", - Size: 180, - } - assert.NoError(t, err) - assert.Equal(t, expectedGBR, gbr) -} diff --git a/services/repository/files/diff.go b/services/repository/files/diff.go index 0b3550452a..50d01f9d7c 100644 --- a/services/repository/files/diff.go +++ b/services/repository/files/diff.go @@ -29,7 +29,7 @@ func GetDiffPreview(ctx context.Context, repo *repo_model.Repository, branch, tr } // Add the object to the database - objectHash, err := t.HashObject(ctx, strings.NewReader(content)) + objectHash, err := t.HashObjectAndWrite(ctx, strings.NewReader(content)) if err != nil { return nil, err } diff --git a/services/repository/files/file.go b/services/repository/files/file.go index c4991b458d..13d171d139 100644 --- a/services/repository/files/file.go +++ b/services/repository/files/file.go @@ -19,12 +19,17 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" ) -func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) { +func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treePaths []string) (files []*api.ContentsResponse) { var size int64 for _, treePath := range treePaths { - fileContents, _ := GetContents(ctx, repo, refCommit, treePath, false) // ok if fails, then will be nil + // ok if fails, then will be nil + fileContents, _ := GetFileContents(ctx, repo, gitRepo, refCommit, GetContentsOrListOptions{ + TreePath: treePath, + IncludeSingleFileContent: true, + IncludeCommitMetadata: true, + }) if fileContents != nil && fileContents.Content != nil && *fileContents.Content != "" { - // if content isn't empty (e.g. due to the single blob being too large), add file size to response size + // if content isn't empty (e.g., due to the single blob being too large), add file size to response size size += int64(len(*fileContents.Content)) } if size > setting.API.DefaultMaxResponseSize { @@ -38,8 +43,8 @@ func GetContentsListFromTreePaths(ctx context.Context, repo *repo_model.Reposito return files } -func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) { - files := GetContentsListFromTreePaths(ctx, repo, refCommit, treeNames) +func GetFilesResponseFromCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, refCommit *utils.RefCommit, treeNames []string) (*api.FilesResponse, error) { + files := GetContentsListFromTreePaths(ctx, repo, gitRepo, refCommit, treeNames) fileCommitResponse, _ := GetFileCommitResponse(repo, refCommit.Commit) // ok if fails, then will be nil verification := GetPayloadCommitVerification(ctx, refCommit.Commit) filesResponse := &api.FilesResponse{ @@ -134,15 +139,17 @@ func (err ErrFilenameInvalid) Unwrap() error { return util.ErrInvalidArgument } -// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory -func CleanUploadFileName(name string) string { - // Rebase the filename +// CleanGitTreePath cleans a tree path for git, it returns an empty string the path is invalid (e.g.: contains ".git" part) +func CleanGitTreePath(name string) string { name = util.PathJoinRel(name) // Git disallows any filenames to have a .git directory in them. - for _, part := range strings.Split(name, "/") { + for part := range strings.SplitSeq(name, "/") { if strings.ToLower(part) == ".git" { return "" } } + if name == "." { + name = "" + } return name } diff --git a/services/repository/files/file_test.go b/services/repository/files/file_test.go index 169cafba0d..cdb6a266ff 100644 --- a/services/repository/files/file_test.go +++ b/services/repository/files/file_test.go @@ -10,17 +10,18 @@ import ( ) func TestCleanUploadFileName(t *testing.T) { - t.Run("Clean regular file", func(t *testing.T) { - name := "this/is/test" - cleanName := CleanUploadFileName(name) - expectedCleanName := name - assert.Equal(t, expectedCleanName, cleanName) - }) - - t.Run("Clean a .git path", func(t *testing.T) { - name := "this/is/test/.git" - cleanName := CleanUploadFileName(name) - expectedCleanName := "" - assert.Equal(t, expectedCleanName, cleanName) - }) + cases := []struct { + input, expected string + }{ + {"", ""}, + {".", ""}, + {"a/./b", "a/b"}, + {"a.git", "a.git"}, + {".git/b", ""}, + {"a/.git", ""}, + {"/a/../../b", "b"}, + } + for _, c := range cases { + assert.Equal(t, c.expected, CleanGitTreePath(c.input), "input: %q", c.input) + } } diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index 5fbf748206..11a8744b7f 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -44,7 +44,6 @@ type ApplyDiffPatchOptions struct { NewBranch string Message string Content string - SHA string Author *IdentityOptions Committer *IdentityOptions Dates *CommitDateOptions diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index 493ff9998d..c2f61c8223 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -128,7 +128,7 @@ func (t *TemporaryUploadRepository) LsFiles(ctx context.Context, filenames ...st } fileList := make([]string, 0, len(filenames)) - for _, line := range bytes.Split(stdOut.Bytes(), []byte{'\000'}) { + for line := range bytes.SplitSeq(stdOut.Bytes(), []byte{'\000'}) { fileList = append(fileList, string(line)) } @@ -164,8 +164,8 @@ func (t *TemporaryUploadRepository) RemoveFilesFromIndex(ctx context.Context, fi return nil } -// HashObject writes the provided content to the object db and returns its hash -func (t *TemporaryUploadRepository) HashObject(ctx context.Context, content io.Reader) (string, error) { +// HashObjectAndWrite writes the provided content to the object db and returns its hash +func (t *TemporaryUploadRepository) HashObjectAndWrite(ctx context.Context, content io.Reader) (string, error) { stdOut := new(bytes.Buffer) stdErr := new(bytes.Buffer) @@ -293,15 +293,18 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit } var sign bool - var keyID string + var key *git.SigningKey var signer *git.Signature if opts.ParentCommitID != "" { - sign, keyID, signer, _ = asymkey_service.SignCRUDAction(ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID) + sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID) } else { - sign, keyID, signer, _ = asymkey_service.SignInitialCommit(ctx, t.repo.RepoPath(), opts.DoerUser) + sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, t.repo.RepoPath(), opts.DoerUser) } if sign { - cmdCommitTree.AddOptionFormat("-S%s", keyID) + if key.Format != "" { + cmdCommitTree.AddConfig("gpg.format", key.Format) + } + cmdCommitTree.AddOptionFormat("-S%s", key.KeyID) if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email { // Add trailers diff --git a/services/repository/files/tree.go b/services/repository/files/tree.go index 8427fcbacc..f2cbacbf1c 100644 --- a/services/repository/files/tree.go +++ b/services/repository/files/tree.go @@ -94,11 +94,7 @@ func GetTreeBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git if len(entries) > perPage { tree.Truncated = true } - if rangeStart+perPage < len(entries) { - rangeEnd = rangeStart + perPage - } else { - rangeEnd = len(entries) - } + rangeEnd = min(rangeStart+perPage, len(entries)) tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart) for e := rangeStart; e < rangeEnd; e++ { i := e - rangeStart @@ -165,7 +161,7 @@ func newTreeViewNodeFromEntry(ctx context.Context, renderedIconPool *fileicon.Re FullPath: path.Join(parentDir, entry.Name()), } - entryInfo := fileicon.EntryInfoFromGitTreeEntry(entry) + entryInfo := fileicon.EntryInfoFromGitTreeEntry(commit, node.FullPath, entry) node.EntryIcon = fileicon.RenderEntryIconHTML(renderedIconPool, entryInfo) if entryInfo.EntryMode.IsDir() { entryInfo.IsOpen = true diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 712914a27e..e871f777e5 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "path" + "slices" "strings" "time" @@ -87,14 +88,32 @@ func (err ErrRepoFileDoesNotExist) Unwrap() error { return util.ErrNotExist } +type LazyReadSeeker interface { + io.ReadSeeker + io.Closer + OpenLazyReader() error +} + // ChangeRepoFiles adds, updates or removes multiple files in the given repository -func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (*structs.FilesResponse, error) { +func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *ChangeRepoFilesOptions) (_ *structs.FilesResponse, errRet error) { + var addedLfsPointers []lfs.Pointer + defer func() { + if errRet != nil { + for _, lfsPointer := range addedLfsPointers { + _, err := git_model.RemoveLFSMetaObjectByOid(ctx, repo.ID, lfsPointer.Oid) + if err != nil { + log.Error("ChangeRepoFiles: RemoveLFSMetaObjectByOid failed: %v", err) + } + } + } + }() + err := repo.MustNotBeArchived() if err != nil { return nil, err } - // If no branch name is set, assume default branch + // If no branch name is set, assume the default branch if opts.OldBranch == "" { opts.OldBranch = repo.DefaultBranch } @@ -126,14 +145,14 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } // Check that the path given in opts.treePath is valid (not a git path) - treePath := CleanUploadFileName(file.TreePath) + treePath := CleanGitTreePath(file.TreePath) if treePath == "" { return nil, ErrFilenameInvalid{ Path: file.TreePath, } } // If there is a fromTreePath (we are copying it), also clean it up - fromTreePath := CleanUploadFileName(file.FromTreePath) + fromTreePath := CleanGitTreePath(file.FromTreePath) if fromTreePath == "" && file.FromTreePath != "" { return nil, ErrFilenameInvalid{ Path: file.FromTreePath, @@ -203,13 +222,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } // Find the file we want to delete in the index - inFilelist := false - for _, indexFile := range filesInIndex { - if indexFile == file.TreePath { - inFilelist = true - break - } - } + inFilelist := slices.Contains(filesInIndex, file.TreePath) if !inFilelist { return nil, ErrRepoFileDoesNotExist{ Path: file.TreePath, @@ -225,7 +238,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use return nil, err // Couldn't get a commit for the branch } - // Assigned LastCommitID in opts if it hasn't been set + // Assigned LastCommitID in "opts" if it hasn't been set if opts.LastCommitID == "" { opts.LastCommitID = commit.ID.String() } else { @@ -237,22 +250,25 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use } for _, file := range opts.Files { - if err := handleCheckErrors(file, commit, opts); err != nil { + if err = handleCheckErrors(file, commit, opts); err != nil { return nil, err } } } - contentStore := lfs.NewContentStore() + lfsContentStore := lfs.NewContentStore() for _, file := range opts.Files { switch file.Operation { - case "create", "update": - if err := CreateOrUpdateFile(ctx, t, file, contentStore, repo.ID, hasOldBranch); err != nil { + case "create", "update", "rename", "upload": + addedLfsPointer, err := modifyFile(ctx, t, file, lfsContentStore, repo.ID) + if err != nil { return nil, err } + if addedLfsPointer != nil { + addedLfsPointers = append(addedLfsPointers, *addedLfsPointer) + } case "delete": - // Remove the file from the index - if err := t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil { + if err = t.RemoveFilesFromIndex(ctx, file.TreePath); err != nil { return nil, err } default: @@ -299,7 +315,7 @@ func ChangeRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *use // FIXME: this call seems not right, why it needs to read the file content again // FIXME: why it uses the NewBranch as "ref", it should use the commit ID because the response is only for this commit - filesResponse, err := GetFilesResponseFromCommit(ctx, repo, utils.NewRefCommit(git.RefNameFromBranch(opts.NewBranch), commit), treePaths) + filesResponse, err := GetFilesResponseFromCommit(ctx, repo, gitRepo, utils.NewRefCommit(git.RefNameFromBranch(opts.NewBranch), commit), treePaths) if err != nil { return nil, err } @@ -372,22 +388,33 @@ func (err ErrSHAOrCommitIDNotProvided) Error() string { // handles the check for various issues for ChangeRepoFiles func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRepoFilesOptions) error { - if file.Operation == "update" || file.Operation == "delete" { - fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) - if err != nil { - return err + // check old entry (fromTreePath/fromEntry) + if file.Operation == "update" || file.Operation == "upload" || file.Operation == "delete" || file.Operation == "rename" { + var fromEntryIDString string + { + fromEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) + if file.Operation == "upload" && git.IsErrNotExist(err) { + fromEntry = nil + } else if err != nil { + return err + } + if fromEntry != nil { + fromEntryIDString = fromEntry.ID.String() + file.Options.executable = fromEntry.IsExecutable() // FIXME: legacy hacky approach, it shouldn't prepare the "Options" in the "check" function + } } + if file.SHA != "" { - // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error - if file.SHA != fromEntry.ID.String() { + // If the SHA given doesn't match the SHA of the fromTreePath, throw error + if file.SHA != fromEntryIDString { return pull_service.ErrSHADoesNotMatch{ Path: file.Options.treePath, GivenSHA: file.SHA, - CurrentSHA: fromEntry.ID.String(), + CurrentSHA: fromEntryIDString, } } } else if opts.LastCommitID != "" { - // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw + // If a lastCommitID given doesn't match the branch head's commitID throw // an error, but only if we aren't creating a new branch. if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { if changed, err := commit.FileChangedSinceCommit(file.Options.treePath, opts.LastCommitID); err != nil { @@ -405,13 +432,13 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep // haven't been made. We throw an error if one wasn't provided. return ErrSHAOrCommitIDNotProvided{} } - file.Options.executable = fromEntry.IsExecutable() } - if file.Operation == "create" || file.Operation == "update" { - // For the path where this file will be created/updated, we need to make - // sure no parts of the path are existing files or links except for the last - // item in the path which is the file name, and that shouldn't exist IF it is - // a new file OR is being moved to a new path. + + // check new entry (treePath/treeEntry) + if file.Operation == "create" || file.Operation == "update" || file.Operation == "upload" || file.Operation == "rename" { + // For operation's target path, we need to make sure no parts of the path are existing files or links + // except for the last item in the path (which is the file name). + // And that shouldn't exist IF it is a new file OR is being moved to a new path. treePathParts := strings.Split(file.Options.treePath, "/") subTreePath := "" for index, part := range treePathParts { @@ -448,7 +475,7 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep Type: git.EntryModeTree, } } else if file.Options.fromTreePath != file.Options.treePath || file.Operation == "create" { - // The entry shouldn't exist if we are creating new file or moving to a new path + // The entry shouldn't exist if we are creating the new file or moving to a new path return ErrRepoFileAlreadyExists{ Path: file.Options.treePath, } @@ -459,21 +486,23 @@ func handleCheckErrors(file *ChangeRepoFile, commit *git.Commit, opts *ChangeRep return nil } -// CreateOrUpdateFile handles creating or updating a file for ChangeRepoFiles -func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64, hasOldBranch bool) error { +func modifyFile(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile, contentStore *lfs.ContentStore, repoID int64) (addedLfsPointer *lfs.Pointer, _ error) { + if rd, ok := file.ContentReader.(LazyReadSeeker); ok { + if err := rd.OpenLazyReader(); err != nil { + return nil, fmt.Errorf("OpenLazyReader: %w", err) + } + defer rd.Close() + } + // Get the two paths (might be the same if not moving) from the index if they exist filesInIndex, err := t.LsFiles(ctx, file.TreePath, file.FromTreePath) if err != nil { - return fmt.Errorf("UpdateRepoFile: %w", err) + return nil, fmt.Errorf("LsFiles: %w", err) } // If is a new file (not updating) then the given path shouldn't exist if file.Operation == "create" { - for _, indexFile := range filesInIndex { - if indexFile == file.TreePath { - return ErrRepoFileAlreadyExists{ - Path: file.TreePath, - } - } + if slices.Contains(filesInIndex, file.TreePath) { + return nil, ErrRepoFileAlreadyExists{Path: file.TreePath} } } @@ -481,78 +510,178 @@ func CreateOrUpdateFile(ctx context.Context, t *TemporaryUploadRepository, file if file.Options.fromTreePath != file.Options.treePath && len(filesInIndex) > 0 { for _, indexFile := range filesInIndex { if indexFile == file.Options.fromTreePath { - if err := t.RemoveFilesFromIndex(ctx, file.FromTreePath); err != nil { - return err + if err = t.RemoveFilesFromIndex(ctx, file.FromTreePath); err != nil { + return nil, err } } } } - treeObjectContentReader := file.ContentReader - var lfsMetaObject *git_model.LFSMetaObject - if setting.LFS.StartServer && hasOldBranch { - // Check there is no way this can return multiple infos - attributesMap, err := attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ - Attributes: []string{attribute.Filter}, - Filenames: []string{file.Options.treePath}, - }) + var writeObjectRet *writeRepoObjectRet + switch file.Operation { + case "create", "update", "upload": + writeObjectRet, err = writeRepoObjectForModify(ctx, t, file) + case "rename": + writeObjectRet, err = writeRepoObjectForRename(ctx, t, file) + default: + return nil, util.NewInvalidArgumentErrorf("unknown file modification operation: '%s'", file.Operation) + } + if err != nil { + return nil, err + } + + // Add the object to the index, the "file.Options.executable" is set in handleCheckErrors by the caller (legacy hacky approach) + if err = t.AddObjectToIndex(ctx, util.Iif(file.Options.executable, "100755", "100644"), writeObjectRet.ObjectHash, file.Options.treePath); err != nil { + return nil, err + } + + if writeObjectRet.LfsContent == nil { + return nil, nil // No LFS pointer, so nothing to do + } + defer writeObjectRet.LfsContent.Close() + + // Now we must store the content into an LFS object + lfsMetaObject, err := git_model.NewLFSMetaObject(ctx, repoID, writeObjectRet.LfsPointer) + if err != nil { + return nil, err + } + exist, err := contentStore.Exists(lfsMetaObject.Pointer) + if err != nil { + return nil, err + } + if !exist { + err = contentStore.Put(lfsMetaObject.Pointer, writeObjectRet.LfsContent) if err != nil { - return err + if _, errRemove := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); errRemove != nil { + return nil, fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, errRemove, err) + } + return nil, err } + } + return &lfsMetaObject.Pointer, nil +} + +func checkIsLfsFileInGitAttributes(ctx context.Context, t *TemporaryUploadRepository, paths []string) (ret []bool, err error) { + attributesMap, err := attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ + Attributes: []string{attribute.Filter}, + Filenames: paths, + }) + if err != nil { + return nil, err + } + for _, p := range paths { + isLFSFile := attributesMap[p] != nil && attributesMap[p].Get(attribute.Filter).ToString().Value() == "lfs" + ret = append(ret, isLFSFile) + } + return ret, nil +} - if attributesMap[file.Options.treePath] != nil && attributesMap[file.Options.treePath].Get(attribute.Filter).ToString().Value() == "lfs" { - // OK so we are supposed to LFS this data! - pointer, err := lfs.GeneratePointer(treeObjectContentReader) +type writeRepoObjectRet struct { + ObjectHash string + LfsContent io.ReadCloser // if not nil, then the caller should store its content in LfsPointer, then close it + LfsPointer lfs.Pointer +} + +// writeRepoObjectForModify hashes the git object for create or update operations +func writeRepoObjectForModify(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) { + ret = &writeRepoObjectRet{} + treeObjectContentReader := file.ContentReader + if setting.LFS.StartServer { + checkIsLfsFiles, err := checkIsLfsFileInGitAttributes(ctx, t, []string{file.Options.treePath}) + if err != nil { + return nil, err + } + if checkIsLfsFiles[0] { + // OK, so we are supposed to LFS this data! + ret.LfsPointer, err = lfs.GeneratePointer(file.ContentReader) if err != nil { - return err + return nil, err + } + if _, err = file.ContentReader.Seek(0, io.SeekStart); err != nil { + return nil, err } - lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: repoID} - treeObjectContentReader = strings.NewReader(pointer.StringContent()) + ret.LfsContent = io.NopCloser(file.ContentReader) + treeObjectContentReader = strings.NewReader(ret.LfsPointer.StringContent()) } } - // Add the object to the database - objectHash, err := t.HashObject(ctx, treeObjectContentReader) + ret.ObjectHash, err = t.HashObjectAndWrite(ctx, treeObjectContentReader) if err != nil { - return err + return nil, err } + return ret, nil +} - // Add the object to the index - if file.Options.executable { - if err := t.AddObjectToIndex(ctx, "100755", objectHash, file.Options.treePath); err != nil { - return err - } - } else { - if err := t.AddObjectToIndex(ctx, "100644", objectHash, file.Options.treePath); err != nil { - return err +// writeRepoObjectForRename the same as writeRepoObjectForModify buf for "rename" +func writeRepoObjectForRename(ctx context.Context, t *TemporaryUploadRepository, file *ChangeRepoFile) (ret *writeRepoObjectRet, err error) { + lastCommitID, err := t.GetLastCommit(ctx) + if err != nil { + return nil, err + } + commit, err := t.GetCommit(lastCommitID) + if err != nil { + return nil, err + } + oldEntry, err := commit.GetTreeEntryByPath(file.Options.fromTreePath) + if err != nil { + return nil, err + } + + ret = &writeRepoObjectRet{ObjectHash: oldEntry.ID.String()} + if !setting.LFS.StartServer { + return ret, nil + } + + checkIsLfsFiles, err := checkIsLfsFileInGitAttributes(ctx, t, []string{file.Options.fromTreePath, file.Options.treePath}) + if err != nil { + return nil, err + } + oldIsLfs, newIsLfs := checkIsLfsFiles[0], checkIsLfsFiles[1] + + // If the old and new paths are both in lfs or both not in lfs, the object hash of the old file can be used directly + // as the object doesn't change + if oldIsLfs == newIsLfs { + return ret, nil + } + + oldEntryBlobPointerBy := func(f func(r io.Reader) (lfs.Pointer, error)) (lfsPointer lfs.Pointer, err error) { + r, err := oldEntry.Blob().DataAsync() + if err != nil { + return lfsPointer, err } + defer r.Close() + return f(r) } - if lfsMetaObject != nil { - // We have an LFS object - create it - lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, lfsMetaObject.RepositoryID, lfsMetaObject.Pointer) + var treeObjectContentReader io.ReadCloser + if oldIsLfs { + // If the old is in lfs but the new isn't, read the content from lfs and add it as a normal git object + pointer, err := oldEntryBlobPointerBy(lfs.ReadPointer) if err != nil { - return err + return nil, err } - exist, err := contentStore.Exists(lfsMetaObject.Pointer) + treeObjectContentReader, err = lfs.ReadMetaObject(pointer) if err != nil { - return err + return nil, err } - if !exist { - _, err := file.ContentReader.Seek(0, io.SeekStart) - if err != nil { - return err - } - if err := contentStore.Put(lfsMetaObject.Pointer, file.ContentReader); err != nil { - if _, err2 := git_model.RemoveLFSMetaObjectByOid(ctx, repoID, lfsMetaObject.Oid); err2 != nil { - return fmt.Errorf("unable to remove failed inserted LFS object %s: %v (Prev Error: %w)", lfsMetaObject.Oid, err2, err) - } - return err - } + defer treeObjectContentReader.Close() + } else { + // If the new is in lfs but the old isn't, read the content from the git object and generate a lfs pointer of it + ret.LfsPointer, err = oldEntryBlobPointerBy(lfs.GeneratePointer) + if err != nil { + return nil, err } + ret.LfsContent, err = oldEntry.Blob().DataAsync() + if err != nil { + return nil, err + } + treeObjectContentReader = io.NopCloser(strings.NewReader(ret.LfsPointer.StringContent())) } - - return nil + ret.ObjectHash, err = t.HashObjectAndWrite(ctx, treeObjectContentReader) + if err != nil { + return nil, err + } + return ret, nil } // VerifyBranchProtection verify the branch protection for modifying the given treePath on the given branch diff --git a/services/repository/files/upload.go b/services/repository/files/upload.go index 68a071cd28..b783cbd01d 100644 --- a/services/repository/files/upload.go +++ b/services/repository/files/upload.go @@ -8,15 +8,11 @@ import ( "fmt" "os" "path" - "strings" + "sync" - git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/git/attribute" - "code.gitea.io/gitea/modules/lfs" - "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/log" ) // UploadRepoFileOptions contains the uploaded repository file options @@ -32,208 +28,84 @@ type UploadRepoFileOptions struct { Committer *IdentityOptions } -type uploadInfo struct { - upload *repo_model.Upload - lfsMetaObject *git_model.LFSMetaObject +type lazyLocalFileReader struct { + *os.File + localFilename string + counter int + mu sync.Mutex } -func cleanUpAfterFailure(ctx context.Context, infos *[]uploadInfo, t *TemporaryUploadRepository, original error) error { - for _, info := range *infos { - if info.lfsMetaObject == nil { - continue - } - if !info.lfsMetaObject.Existing { - if _, err := git_model.RemoveLFSMetaObjectByOid(ctx, t.repo.ID, info.lfsMetaObject.Oid); err != nil { - original = fmt.Errorf("%w, %v", original, err) // We wrap the original error - as this is the underlying error that required the fallback - } - } - } - return original -} - -// UploadRepoFiles uploads files to the given repository -func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UploadRepoFileOptions) error { - if len(opts.Files) == 0 { - return nil - } +var _ LazyReadSeeker = (*lazyLocalFileReader)(nil) - uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files) - if err != nil { - return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err) - } +func (l *lazyLocalFileReader) Close() error { + l.mu.Lock() + defer l.mu.Unlock() - names := make([]string, len(uploads)) - infos := make([]uploadInfo, len(uploads)) - for i, upload := range uploads { - // Check file is not lfs locked, will return nil if lock setting not enabled - filepath := path.Join(opts.TreePath, upload.Name) - lfsLock, err := git_model.GetTreePathLock(ctx, repo.ID, filepath) - if err != nil { - return err - } - if lfsLock != nil && lfsLock.OwnerID != doer.ID { - u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID) - if err != nil { - return err + if l.counter > 0 { + l.counter-- + if l.counter == 0 { + if err := l.File.Close(); err != nil { + return fmt.Errorf("close file %s: %w", l.localFilename, err) } - return git_model.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: u.Name} - } - - names[i] = upload.Name - infos[i] = uploadInfo{upload: upload} - } - - t, err := NewTemporaryUploadRepository(repo) - if err != nil { - return err - } - defer t.Close() - - hasOldBranch := true - if err = t.Clone(ctx, opts.OldBranch, true); err != nil { - if !git.IsErrBranchNotExist(err) || !repo.IsEmpty { - return err - } - if err = t.Init(ctx, repo.ObjectFormatName); err != nil { - return err - } - hasOldBranch = false - opts.LastCommitID = "" - } - if hasOldBranch { - if err = t.SetDefaultIndex(ctx); err != nil { - return err - } - } - - var attributesMap map[string]*attribute.Attributes - // when uploading to an empty repo, the old branch doesn't exist, but some "global gitattributes" or "info/attributes" may exist - if setting.LFS.StartServer { - attributesMap, err = attribute.CheckAttributes(ctx, t.gitRepo, "" /* use temp repo's working dir */, attribute.CheckAttributeOpts{ - Attributes: []string{attribute.Filter}, - Filenames: names, - }) - if err != nil { - return err + l.File = nil } + return nil } + return fmt.Errorf("file %s already closed", l.localFilename) +} - // Copy uploaded files into repository. - // TODO: there is a small problem: when uploading LFS files with ".gitattributes", the "check-attr" runs before this loop, - // so LFS files are not able to be added as LFS objects. Ideally we need to do in 3 steps in the future: - // 1. Add ".gitattributes" to git index - // 2. Run "check-attr" (the previous attribute.CheckAttributes call) - // 3. Add files to git index (this loop) - // This problem is trivial so maybe no need to spend too much time on it at the moment. - for i := range infos { - if err := copyUploadedLFSFileIntoRepository(ctx, &infos[i], attributesMap, t, opts.TreePath); err != nil { - return err - } - } +func (l *lazyLocalFileReader) OpenLazyReader() error { + l.mu.Lock() + defer l.mu.Unlock() - // Now write the tree - treeHash, err := t.WriteTree(ctx) - if err != nil { - return err + if l.File != nil { + l.counter++ + return nil } - // Now commit the tree - commitOpts := &CommitTreeUserOptions{ - ParentCommitID: opts.LastCommitID, - TreeHash: treeHash, - CommitMessage: opts.Message, - SignOff: opts.Signoff, - DoerUser: doer, - AuthorIdentity: opts.Author, - CommitterIdentity: opts.Committer, - } - commitHash, err := t.CommitTree(ctx, commitOpts) + file, err := os.Open(l.localFilename) if err != nil { return err } + l.File = file + l.counter = 1 + return nil +} - // Now deal with LFS objects - for i := range infos { - if infos[i].lfsMetaObject == nil { - continue - } - infos[i].lfsMetaObject, err = git_model.NewLFSMetaObject(ctx, infos[i].lfsMetaObject.RepositoryID, infos[i].lfsMetaObject.Pointer) - if err != nil { - // OK Now we need to cleanup - return cleanUpAfterFailure(ctx, &infos, t, err) - } - // Don't move the files yet - we need to ensure that - // everything can be inserted first - } - - // OK now we can insert the data into the store - there's no way to clean up the store - // once it's in there, it's in there. - contentStore := lfs.NewContentStore() - for _, info := range infos { - if err := uploadToLFSContentStore(info, contentStore); err != nil { - return cleanUpAfterFailure(ctx, &infos, t, err) - } - } - - // Then push this tree to NewBranch - if err := t.Push(ctx, doer, commitHash, opts.NewBranch); err != nil { - return err +// UploadRepoFiles uploads files to the given repository +func UploadRepoFiles(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, opts *UploadRepoFileOptions) error { + if len(opts.Files) == 0 { + return nil } - return repo_model.DeleteUploads(ctx, uploads...) -} - -func copyUploadedLFSFileIntoRepository(ctx context.Context, info *uploadInfo, attributesMap map[string]*attribute.Attributes, t *TemporaryUploadRepository, treePath string) error { - file, err := os.Open(info.upload.LocalPath()) + uploads, err := repo_model.GetUploadsByUUIDs(ctx, opts.Files) if err != nil { - return err + return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %w", opts.Files, err) } - defer file.Close() - var objectHash string - if setting.LFS.StartServer && attributesMap[info.upload.Name] != nil && attributesMap[info.upload.Name].Get(attribute.Filter).ToString().Value() == "lfs" { - // Handle LFS - // FIXME: Inefficient! this should probably happen in models.Upload - pointer, err := lfs.GeneratePointer(file) - if err != nil { - return err - } - - info.lfsMetaObject = &git_model.LFSMetaObject{Pointer: pointer, RepositoryID: t.repo.ID} - - if objectHash, err = t.HashObject(ctx, strings.NewReader(pointer.StringContent())); err != nil { - return err - } - } else if objectHash, err = t.HashObject(ctx, file); err != nil { - return err + changeOpts := &ChangeRepoFilesOptions{ + LastCommitID: opts.LastCommitID, + OldBranch: opts.OldBranch, + NewBranch: opts.NewBranch, + Message: opts.Message, + Signoff: opts.Signoff, + Author: opts.Author, + Committer: opts.Committer, + } + for _, upload := range uploads { + changeOpts.Files = append(changeOpts.Files, &ChangeRepoFile{ + Operation: "upload", + TreePath: path.Join(opts.TreePath, upload.Name), + ContentReader: &lazyLocalFileReader{localFilename: upload.LocalPath()}, + }) } - // Add the object to the index - return t.AddObjectToIndex(ctx, "100644", objectHash, path.Join(treePath, info.upload.Name)) -} - -func uploadToLFSContentStore(info uploadInfo, contentStore *lfs.ContentStore) error { - if info.lfsMetaObject == nil { - return nil - } - exist, err := contentStore.Exists(info.lfsMetaObject.Pointer) + _, err = ChangeRepoFiles(ctx, repo, doer, changeOpts) if err != nil { return err } - if !exist { - file, err := os.Open(info.upload.LocalPath()) - if err != nil { - return err - } - - defer file.Close() - // FIXME: Put regenerates the hash and copies the file over. - // I guess this strictly ensures the soundness of the store but this is inefficient. - if err := contentStore.Put(info.lfsMetaObject.Pointer, file); err != nil { - // OK Now we need to cleanup - // Can't clean up the store, once uploaded there they're there. - return err - } + if err := repo_model.DeleteUploads(ctx, uploads...); err != nil { + log.Error("DeleteUploads: %v", err) } return nil } diff --git a/services/repository/fork.go b/services/repository/fork.go index bd1554f163..8bd3498b17 100644 --- a/services/repository/fork.go +++ b/services/repository/fork.go @@ -124,7 +124,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork defer func() { if err != nil { // we can not use the ctx because it maybe canceled or timeout - cleanupRepository(doer, repo.ID) + cleanupRepository(repo.ID) } }() @@ -209,7 +209,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork // ConvertForkToNormalRepository convert the provided repo from a forked repo to normal repo func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Repository) error { - err := db.WithTx(ctx, func(ctx context.Context) error { + return db.WithTx(ctx, func(ctx context.Context) error { repo, err := repo_model.GetRepositoryByID(ctx, repo.ID) if err != nil { return err @@ -226,16 +226,8 @@ func ConvertForkToNormalRepository(ctx context.Context, repo *repo_model.Reposit repo.IsFork = false repo.ForkID = 0 - - if err := updateRepository(ctx, repo, false); err != nil { - log.Error("Unable to update repository %-v whilst converting from fork. Error: %v", repo, err) - return err - } - - return nil + return repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_fork", "fork_id") }) - - return err } type findForksOptions struct { diff --git a/services/repository/fork_test.go b/services/repository/fork_test.go index 9edc0aa39f..5375f79028 100644 --- a/services/repository/fork_test.go +++ b/services/repository/fork_test.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -38,7 +39,7 @@ func TestForkRepository(t *testing.T) { assert.False(t, repo_model.IsErrReachLimitOfRepo(err)) // change AllowForkWithoutMaximumLimit to false for the test - setting.Repository.AllowForkWithoutMaximumLimit = false + defer test.MockVariableValue(&setting.Repository.AllowForkWithoutMaximumLimit, false)() // user has reached maximum limit of repositories user.MaxRepoCreation = 0 fork2, err := ForkRepository(git.DefaultContext, user, user, ForkRepoOptions{ @@ -68,7 +69,7 @@ func TestForkRepositoryCleanup(t *testing.T) { assert.NoError(t, err) assert.True(t, exist) - err = DeleteRepositoryDirectly(db.DefaultContext, user2, fork.ID) + err = DeleteRepositoryDirectly(db.DefaultContext, fork.ID) assert.NoError(t, err) // a failed creating because some mock data diff --git a/services/repository/generate.go b/services/repository/generate.go index b02f7c9482..867b5d7855 100644 --- a/services/repository/generate.go +++ b/services/repository/generate.go @@ -42,10 +42,8 @@ type expansion struct { var defaultTransformers = []transformer{ {Name: "SNAKE", Transform: xstrings.ToSnakeCase}, {Name: "KEBAB", Transform: xstrings.ToKebabCase}, - {Name: "CAMEL", Transform: func(str string) string { - return xstrings.FirstRuneToLower(xstrings.ToCamelCase(str)) - }}, - {Name: "PASCAL", Transform: xstrings.ToCamelCase}, + {Name: "CAMEL", Transform: xstrings.ToCamelCase}, + {Name: "PASCAL", Transform: xstrings.ToPascalCase}, {Name: "LOWER", Transform: strings.ToLower}, {Name: "UPPER", Transform: strings.ToUpper}, {Name: "TITLE", Transform: util.ToTitleCase}, @@ -255,43 +253,35 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch) } -func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) { - tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-" + repo.Name) +// GenerateGitContent generates git content from a template repository +func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) (err error) { + tmpDir, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-" + generateRepo.Name) if err != nil { - return fmt.Errorf("failed to create temp dir for repository %s: %w", repo.FullName(), err) + return fmt.Errorf("failed to create temp dir for repository %s: %w", generateRepo.FullName(), err) } defer cleanup() - if err = generateRepoCommit(ctx, repo, templateRepo, generateRepo, tmpDir); err != nil { + if err = generateRepoCommit(ctx, generateRepo, templateRepo, generateRepo, tmpDir); err != nil { return fmt.Errorf("generateRepoCommit: %w", err) } // re-fetch repo - if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { + if generateRepo, err = repo_model.GetRepositoryByID(ctx, generateRepo.ID); err != nil { return fmt.Errorf("getRepositoryByID: %w", err) } // if there was no default branch supplied when generating the repo, use the default one from the template - if strings.TrimSpace(repo.DefaultBranch) == "" { - repo.DefaultBranch = templateRepo.DefaultBranch + if strings.TrimSpace(generateRepo.DefaultBranch) == "" { + generateRepo.DefaultBranch = templateRepo.DefaultBranch } - if err = gitrepo.SetDefaultBranch(ctx, repo, repo.DefaultBranch); err != nil { + if err = gitrepo.SetDefaultBranch(ctx, generateRepo, generateRepo.DefaultBranch); err != nil { return fmt.Errorf("setDefaultBranch: %w", err) } - if err = UpdateRepository(ctx, repo, false); err != nil { + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, generateRepo, "default_branch"); err != nil { return fmt.Errorf("updateRepository: %w", err) } - return nil -} - -// GenerateGitContent generates git content from a template repository -func GenerateGitContent(ctx context.Context, templateRepo, generateRepo *repo_model.Repository) error { - if err := generateGitContent(ctx, generateRepo, templateRepo, generateRepo); err != nil { - return err - } - if err := repo_module.UpdateRepoSize(ctx, generateRepo); err != nil { return fmt.Errorf("failed to update size for repository: %w", err) } diff --git a/services/repository/generate_test.go b/services/repository/generate_test.go index b0f97d0ffb..1163c392c9 100644 --- a/services/repository/generate_test.go +++ b/services/repository/generate_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var giteaTemplate = []byte(` @@ -65,3 +66,26 @@ func TestFileNameSanitize(t *testing.T) { assert.Equal(t, "_", fileNameSanitize("\u0000")) assert.Equal(t, "ç›®æ ‡", fileNameSanitize("ç›®æ ‡")) } + +func TestTransformers(t *testing.T) { + cases := []struct { + name string + expected string + }{ + {"SNAKE", "abc_def_xyz"}, + {"KEBAB", "abc-def-xyz"}, + {"CAMEL", "abcDefXyz"}, + {"PASCAL", "AbcDefXyz"}, + {"LOWER", "abc_def-xyz"}, + {"UPPER", "ABC_DEF-XYZ"}, + {"TITLE", "Abc_def-Xyz"}, + } + + input := "Abc_Def-XYZ" + assert.Len(t, defaultTransformers, len(cases)) + for i, c := range cases { + tf := defaultTransformers[i] + require.Equal(t, c.name, tf.Name) + assert.Equal(t, c.expected, tf.Transform(input), "case %s", c.name) + } +} diff --git a/services/repository/gitgraph/graph_models.go b/services/repository/gitgraph/graph_models.go index c45662836b..02b0268cd9 100644 --- a/services/repository/gitgraph/graph_models.go +++ b/services/repository/gitgraph/graph_models.go @@ -121,7 +121,7 @@ func (graph *Graph) LoadAndProcessCommits(ctx context.Context, repository *repo_ return repo_model.IsOwnerMemberCollaborator(ctx, repository, user.ID) }, &keyMap) - statuses, _, err := git_model.GetLatestCommitStatus(ctx, repository.ID, c.Commit.ID.String(), db.ListOptions{}) + statuses, err := git_model.GetLatestCommitStatus(ctx, repository.ID, c.Commit.ID.String(), db.ListOptionsAll) if err != nil { log.Error("GetLatestCommitStatus: %v", err) } else { @@ -232,8 +232,8 @@ func newRefsFromRefNames(refNames []byte) []git.Reference { continue } refName := string(refNameBytes) - if strings.HasPrefix(refName, "tag: ") { - refName = strings.TrimPrefix(refName, "tag: ") + if after, ok := strings.CutPrefix(refName, "tag: "); ok { + refName = after } else { refName = strings.TrimPrefix(refName, "HEAD -> ") } diff --git a/services/repository/gitgraph/graph_test.go b/services/repository/gitgraph/graph_test.go index 4c48b94aa2..93fa1aec6a 100644 --- a/services/repository/gitgraph/graph_test.go +++ b/services/repository/gitgraph/graph_test.go @@ -6,6 +6,7 @@ package gitgraph import ( "bytes" "fmt" + "slices" "strings" "testing" @@ -117,13 +118,7 @@ func TestReleaseUnusedColors(t *testing.T) { if parser.firstAvailable == -1 { // All in use for _, color := range parser.availableColors { - found := false - for _, oldColor := range parser.oldColors { - if oldColor == color { - found = true - break - } - } + found := slices.Contains(parser.oldColors, color) if !found { t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should be available but is not", testcase.availableColors, @@ -141,13 +136,7 @@ func TestReleaseUnusedColors(t *testing.T) { // Some in use for i := parser.firstInUse; i != parser.firstAvailable; i = (i + 1) % len(parser.availableColors) { color := parser.availableColors[i] - found := false - for _, oldColor := range parser.oldColors { - if oldColor == color { - found = true - break - } - } + found := slices.Contains(parser.oldColors, color) if !found { t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should be available but is not", testcase.availableColors, @@ -163,13 +152,7 @@ func TestReleaseUnusedColors(t *testing.T) { } for i := parser.firstAvailable; i != parser.firstInUse; i = (i + 1) % len(parser.availableColors) { color := parser.availableColors[i] - found := false - for _, oldColor := range parser.oldColors { - if oldColor == color { - found = true - break - } - } + found := slices.Contains(parser.oldColors, color) if found { t.Errorf("In testcase:\n%d\t%d\t%d %d =>\n%d\t%d\t%d %d: %d should not be available but is", testcase.availableColors, diff --git a/services/repository/init.go b/services/repository/init.go index bd777b8a2f..1eeeb4aa4f 100644 --- a/services/repository/init.go +++ b/services/repository/init.go @@ -42,9 +42,12 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi cmd := git.NewCommand("commit", "--message=Initial commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email) - sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u) + sign, key, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u) if sign { - cmd.AddOptionFormat("-S%s", keyID) + if key.Format != "" { + cmd.AddConfig("gpg.format", key.Format) + } + cmd.AddOptionFormat("-S%s", key.KeyID) if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { // need to set the committer to the KeyID owner diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go index 34e01df723..8d6f11372c 100644 --- a/services/repository/merge_upstream.go +++ b/services/repository/merge_upstream.go @@ -18,7 +18,7 @@ import ( ) // MergeUpstream merges the base repository's default branch into the fork repository's current branch. -func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { +func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string, ffOnly bool) (mergeStyle string, err error) { if err = repo.MustNotBeArchived(); err != nil { return "", err } @@ -45,6 +45,11 @@ func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_ return "", err } + // If ff_only is requested and fast-forward failed, return error + if ffOnly { + return "", util.NewInvalidArgumentErrorf("fast-forward merge not possible: branch has diverged") + } + // TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment // ideally in the future the "merge" functions should be refactored to decouple from the PullRequest fakeIssue := &issue_model.Issue{ diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 003be1a9ab..0a3dc45339 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -149,9 +149,9 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, return repo, fmt.Errorf("SyncRepoBranchesWithRepo: %v", err) } + // if releases migration are not requested, we will sync all tags here + // otherwise, the releases sync will be done out of this function if !opts.Releases { - // note: this will greatly improve release (tag) sync - // for pull-mirrors with many tags repo.IsMirror = opts.Mirror if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { log.Error("Failed to synchronize tags to releases for repository: %v", err) @@ -220,10 +220,14 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, } repo.IsMirror = true - if err = UpdateRepository(ctx, repo, false); err != nil { + if err = repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "num_watches", "is_empty", "default_branch", "default_wiki_branch", "is_mirror"); err != nil { return nil, err } + if err = repo_module.UpdateRepoSize(ctx, repo); err != nil { + log.Error("Failed to update size for repository: %v", err) + } + // this is necessary for sync local tags from remote configName := fmt.Sprintf("remote.%s.fetch", mirrorModel.GetRemoteName()) if stdout, _, err := git.NewCommand("config"). diff --git a/services/repository/push.go b/services/repository/push.go index 31794034ba..af3c873d15 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -232,7 +232,7 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error { } if len(addTags)+len(delTags) > 0 { - if err := PushUpdateAddDeleteTags(ctx, repo, gitRepo, addTags, delTags); err != nil { + if err := PushUpdateAddDeleteTags(ctx, repo, gitRepo, pusher, addTags, delTags); err != nil { return fmt.Errorf("PushUpdateAddDeleteTags: %w", err) } } @@ -342,17 +342,17 @@ func pushDeleteBranch(ctx context.Context, repo *repo_model.Repository, pusher * } // PushUpdateAddDeleteTags updates a number of added and delete tags -func PushUpdateAddDeleteTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, addTags, delTags []string) error { +func PushUpdateAddDeleteTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, pusher *user_model.User, addTags, delTags []string) error { return db.WithTx(ctx, func(ctx context.Context) error { - if err := repo_model.PushUpdateDeleteTagsContext(ctx, repo, delTags); err != nil { + if err := repo_model.PushUpdateDeleteTags(ctx, repo, delTags); err != nil { return err } - return pushUpdateAddTags(ctx, repo, gitRepo, addTags) + return pushUpdateAddTags(ctx, repo, gitRepo, pusher, addTags) }) } // pushUpdateAddTags updates a number of add tags -func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, tags []string) error { +func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, pusher *user_model.User, tags []string) error { if len(tags) == 0 { return nil } @@ -378,8 +378,6 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap)) - emailToUser := make(map[string]*user_model.User) - for i, lowerTag := range lowerTags { tag, err := gitRepo.GetTag(tags[i]) if err != nil { @@ -397,29 +395,12 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo if sig == nil { sig = commit.Committer } - var author *user_model.User - createdAt := time.Unix(1, 0) + createdAt := time.Unix(1, 0) if sig != nil { - var ok bool - author, ok = emailToUser[sig.Email] - if !ok { - author, err = user_model.GetUserByEmail(ctx, sig.Email) - if err != nil && !user_model.IsErrUserNotExist(err) { - return fmt.Errorf("GetUserByEmail: %w", err) - } - if author != nil { - emailToUser[sig.Email] = author - } - } createdAt = sig.When } - commitsCount, err := commit.CommitsCount() - if err != nil { - return fmt.Errorf("CommitsCount: %w", err) - } - rel, has := relMap[lowerTag] parts := strings.SplitN(tag.Message, "\n", 2) @@ -435,31 +416,26 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo LowerTagName: lowerTag, Target: "", Sha1: commit.ID.String(), - NumCommits: commitsCount, + NumCommits: -1, // the commits count will be updated when the UI needs it Note: note, IsDraft: false, IsPrerelease: false, IsTag: true, + PublisherID: pusher.ID, CreatedUnix: timeutil.TimeStamp(createdAt.Unix()), } - if author != nil { - rel.PublisherID = author.ID - } newReleases = append(newReleases, rel) } else { rel.Sha1 = commit.ID.String() rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix()) - rel.NumCommits = commitsCount if rel.IsTag { rel.Title = parts[0] rel.Note = note - if author != nil { - rel.PublisherID = author.ID - } } else { rel.IsDraft = false } + rel.PublisherID = pusher.ID if err = repo_model.UpdateRelease(ctx, rel); err != nil { return fmt.Errorf("Update: %w", err) } diff --git a/services/repository/repository.go b/services/repository/repository.go index 739ef1ec38..e574dc6c01 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -69,7 +69,7 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod notify_service.DeleteRepository(ctx, doer, repo) } - return DeleteRepositoryDirectly(ctx, doer, repo.ID) + return DeleteRepositoryDirectly(ctx, repo.ID) } // PushCreateRepo creates a repository when a new repository is pushed to an appropriate namespace @@ -127,9 +127,42 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error) { return db.WithTx(ctx, func(ctx context.Context) error { repo.IsPrivate = false - if err = updateRepository(ctx, repo, true); err != nil { - return fmt.Errorf("MakeRepoPublic: %w", err) + if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil { + return err + } + + if err = repo.LoadOwner(ctx); err != nil { + return fmt.Errorf("LoadOwner: %w", err) } + if repo.Owner.IsOrganization() { + // Organization repository need to recalculate access table when visibility is changed. + if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { + return fmt.Errorf("recalculateTeamAccesses: %w", err) + } + } + + // Create/Remove git-daemon-export-ok for git-daemon... + if err := checkDaemonExportOK(ctx, repo); err != nil { + return err + } + + forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID) + if err != nil { + return fmt.Errorf("getRepositoriesByForkID: %w", err) + } + + if repo.Owner.Visibility != structs.VisibleTypePrivate { + for i := range forkRepos { + if err = MakeRepoPublic(ctx, forkRepos[i]); err != nil { + return fmt.Errorf("MakeRepoPublic[%d]: %w", forkRepos[i].ID, err) + } + } + } + + // If visibility is changed, we need to update the issue indexer. + // Since the data in the issue indexer have field to indicate if the repo is public or not. + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + return nil }) } @@ -137,9 +170,51 @@ func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err error) { return db.WithTx(ctx, func(ctx context.Context) error { repo.IsPrivate = true - if err = updateRepository(ctx, repo, true); err != nil { - return fmt.Errorf("MakeRepoPrivate: %w", err) + if err := repo_model.UpdateRepositoryColsNoAutoTime(ctx, repo, "is_private"); err != nil { + return err + } + + if err = repo.LoadOwner(ctx); err != nil { + return fmt.Errorf("LoadOwner: %w", err) } + if repo.Owner.IsOrganization() { + // Organization repository need to recalculate access table when visibility is changed. + if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { + return fmt.Errorf("recalculateTeamAccesses: %w", err) + } + } + + // If repo has become private, we need to set its actions to private. + _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).Cols("is_private").Update(&activities_model.Action{ + IsPrivate: true, + }) + if err != nil { + return err + } + + if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil { + return err + } + + // Create/Remove git-daemon-export-ok for git-daemon... + if err := checkDaemonExportOK(ctx, repo); err != nil { + return err + } + + forkRepos, err := repo_model.GetRepositoriesByForkID(ctx, repo.ID) + if err != nil { + return fmt.Errorf("getRepositoriesByForkID: %w", err) + } + for i := range forkRepos { + if err = MakeRepoPrivate(ctx, forkRepos[i]); err != nil { + return fmt.Errorf("MakeRepoPrivate[%d]: %w", forkRepos[i].ID, err) + } + } + + // If visibility is changed, we need to update the issue indexer. + // Since the data in the issue indexer have field to indicate if the repo is public or not. + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + return nil }) } diff --git a/services/repository/template.go b/services/repository/template.go index 621bd95cb1..6906a60083 100644 --- a/services/repository/template.go +++ b/services/repository/template.go @@ -102,7 +102,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ defer func() { if err != nil { // we can not use the ctx because it maybe canceled or timeout - cleanupRepository(doer, generateRepo.ID) + cleanupRepository(generateRepo.ID) } }() diff --git a/services/user/update.go b/services/user/update.go index 4a39f4f783..d7354542bf 100644 --- a/services/user/update.go +++ b/services/user/update.go @@ -15,6 +15,26 @@ import ( "code.gitea.io/gitea/modules/structs" ) +type UpdateOptionField[T any] struct { + FieldValue T + FromSync bool +} + +func UpdateOptionFieldFromValue[T any](value T) optional.Option[UpdateOptionField[T]] { + return optional.Some(UpdateOptionField[T]{FieldValue: value}) +} + +func UpdateOptionFieldFromSync[T any](value T) optional.Option[UpdateOptionField[T]] { + return optional.Some(UpdateOptionField[T]{FieldValue: value, FromSync: true}) +} + +func UpdateOptionFieldFromPtr[T any](value *T) optional.Option[UpdateOptionField[T]] { + if value == nil { + return optional.None[UpdateOptionField[T]]() + } + return UpdateOptionFieldFromValue(*value) +} + type UpdateOptions struct { KeepEmailPrivate optional.Option[bool] FullName optional.Option[string] @@ -32,7 +52,7 @@ type UpdateOptions struct { DiffViewStyle optional.Option[string] AllowCreateOrganization optional.Option[bool] IsActive optional.Option[bool] - IsAdmin optional.Option[bool] + IsAdmin optional.Option[UpdateOptionField[bool]] EmailNotificationsPreference optional.Option[string] SetLastLogin bool RepoAdminChangeTeamAccess optional.Option[bool] @@ -111,13 +131,18 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er cols = append(cols, "is_restricted") } if opts.IsAdmin.Has() { - if !opts.IsAdmin.Value() && user_model.IsLastAdminUser(ctx, u) { - return user_model.ErrDeleteLastAdminUser{UID: u.ID} + if opts.IsAdmin.Value().FieldValue /* true */ { + u.IsAdmin = opts.IsAdmin.Value().FieldValue // set IsAdmin=true + cols = append(cols, "is_admin") + } else if !user_model.IsLastAdminUser(ctx, u) /* not the last admin */ { + u.IsAdmin = opts.IsAdmin.Value().FieldValue // it's safe to change it from false to true (not the last admin) + cols = append(cols, "is_admin") + } else /* IsAdmin=false but this is the last admin user */ { //nolint:gocritic // make it easier to read + if !opts.IsAdmin.Value().FromSync { + return user_model.ErrDeleteLastAdminUser{UID: u.ID} + } + // else: syncing from external-source, this user is the last admin, so skip the "IsAdmin=false" change } - - u.IsAdmin = opts.IsAdmin.Value() - - cols = append(cols, "is_admin") } if opts.Visibility.Has() { diff --git a/services/user/update_test.go b/services/user/update_test.go index fc24a6c212..27513e8040 100644 --- a/services/user/update_test.go +++ b/services/user/update_test.go @@ -22,7 +22,11 @@ func TestUpdateUser(t *testing.T) { admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) assert.Error(t, UpdateUser(db.DefaultContext, admin, &UpdateOptions{ - IsAdmin: optional.Some(false), + IsAdmin: UpdateOptionFieldFromValue(false), + })) + + assert.NoError(t, UpdateUser(db.DefaultContext, admin, &UpdateOptions{ + IsAdmin: UpdateOptionFieldFromSync(false), })) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 28}) @@ -38,7 +42,7 @@ func TestUpdateUser(t *testing.T) { MaxRepoCreation: optional.Some(10), IsRestricted: optional.Some(true), IsActive: optional.Some(false), - IsAdmin: optional.Some(true), + IsAdmin: UpdateOptionFieldFromValue(true), Visibility: optional.Some(structs.VisibleTypePrivate), KeepActivityPrivate: optional.Some(true), Language: optional.Some("lang"), @@ -60,7 +64,7 @@ func TestUpdateUser(t *testing.T) { assert.Equal(t, opts.MaxRepoCreation.Value(), user.MaxRepoCreation) assert.Equal(t, opts.IsRestricted.Value(), user.IsRestricted) assert.Equal(t, opts.IsActive.Value(), user.IsActive) - assert.Equal(t, opts.IsAdmin.Value(), user.IsAdmin) + assert.Equal(t, opts.IsAdmin.Value().FieldValue, user.IsAdmin) assert.Equal(t, opts.Visibility.Value(), user.Visibility) assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate) assert.Equal(t, opts.Language.Value(), user.Language) @@ -80,7 +84,7 @@ func TestUpdateUser(t *testing.T) { assert.Equal(t, opts.MaxRepoCreation.Value(), user.MaxRepoCreation) assert.Equal(t, opts.IsRestricted.Value(), user.IsRestricted) assert.Equal(t, opts.IsActive.Value(), user.IsActive) - assert.Equal(t, opts.IsAdmin.Value(), user.IsAdmin) + assert.Equal(t, opts.IsAdmin.Value().FieldValue, user.IsAdmin) assert.Equal(t, opts.Visibility.Value(), user.Visibility) assert.Equal(t, opts.KeepActivityPrivate.Value(), user.KeepActivityPrivate) assert.Equal(t, opts.Language.Value(), user.Language) diff --git a/services/user/user.go b/services/user/user.go index 1aeebff142..c7252430de 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/agit" asymkey_service "code.gitea.io/gitea/services/asymkey" @@ -177,8 +178,8 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { PageSize: repo_model.RepositoryListDefaultPageSize, Page: 1, }, - UserID: u.ID, - IncludePrivate: true, + UserID: u.ID, + IncludeVisibility: structs.VisibleTypePrivate, }) if err != nil { return fmt.Errorf("unable to find org list for %s[%d]. Error: %w", u.Name, u.ID, err) diff --git a/services/versioned_migration/migration.go b/services/versioned_migration/migration.go index daec89d7c1..b66d853531 100644 --- a/services/versioned_migration/migration.go +++ b/services/versioned_migration/migration.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package versioned_migration //nolint +package versioned_migration import ( "context" diff --git a/services/webhook/dingtalk.go b/services/webhook/dingtalk.go index ce907bf0cb..5bbc610fe5 100644 --- a/services/webhook/dingtalk.go +++ b/services/webhook/dingtalk.go @@ -176,6 +176,12 @@ func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil } +func (dingtalkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DingtalkPayload, error) { + text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true) + + return createDingtalkPayload(text, text, "Workflow Run", p.WorkflowRun.HTMLURL), nil +} + func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) { text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true) diff --git a/services/webhook/discord.go b/services/webhook/discord.go index 7b68430508..0426964181 100644 --- a/services/webhook/discord.go +++ b/services/webhook/discord.go @@ -278,6 +278,12 @@ func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, er return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil } +func (d discordConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DiscordPayload, error) { + text, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false) + + return d.createPayload(p.Sender, text, "", p.WorkflowRun.HTMLURL, color), nil +} + func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) { text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false) diff --git a/services/webhook/feishu.go b/services/webhook/feishu.go index 274aaf90b3..b6ee80c44c 100644 --- a/services/webhook/feishu.go +++ b/services/webhook/feishu.go @@ -5,9 +5,13 @@ package webhook import ( "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" "fmt" "net/http" "strings" + "time" webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" @@ -16,10 +20,12 @@ import ( ) type ( - // FeishuPayload represents + // FeishuPayload represents the payload for Feishu webhook FeishuPayload struct { - MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media - Content struct { + Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp for signature verification + Sign string `json:"sign,omitempty"` // Signature for verification + MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media + Content struct { Text string `json:"text"` } `json:"content"` } @@ -172,15 +178,41 @@ func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, err return newFeishuTextPayload(text), nil } +func (feishuConvertor) WorkflowRun(p *api.WorkflowRunPayload) (FeishuPayload, error) { + text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true) + + return newFeishuTextPayload(text), nil +} + func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) { text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true) return newFeishuTextPayload(text), nil } +// feishuGenSign generates a signature for Feishu webhook +// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot +func feishuGenSign(secret string, timestamp int64) string { + // key="{timestamp}\n{secret}", then hmac-sha256, then base64 encode + stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret) + h := hmac.New(sha256.New, []byte(stringToSign)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { - var pc payloadConvertor[FeishuPayload] = feishuConvertor{} - return newJSONRequest(pc, w, t, true) + payload, err := newPayload(feishuConvertor{}, []byte(t.PayloadContent), t.EventType) + if err != nil { + return nil, nil, err + } + + // Add timestamp and signature if secret is provided + if w.Secret != "" { + timestamp := time.Now().Unix() + payload.Timestamp = timestamp + payload.Sign = feishuGenSign(w.Secret, timestamp) + } + + return prepareJSONRequest(payload, w, t, false /* no default headers */) } func init() { diff --git a/services/webhook/feishu_test.go b/services/webhook/feishu_test.go index c4249bdb30..7e200ea132 100644 --- a/services/webhook/feishu_test.go +++ b/services/webhook/feishu_test.go @@ -168,6 +168,7 @@ func TestFeishuJSONPayload(t *testing.T) { URL: "https://feishu.example.com/", Meta: `{}`, HTTPMethod: "POST", + Secret: "secret", } task := &webhook_model.HookTask{ HookID: hook.ID, @@ -183,10 +184,13 @@ func TestFeishuJSONPayload(t *testing.T) { assert.Equal(t, "POST", req.Method) assert.Equal(t, "https://feishu.example.com/", req.URL.String()) - assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256")) assert.Equal(t, "application/json", req.Header.Get("Content-Type")) var body FeishuPayload err = json.NewDecoder(req.Body).Decode(&body) assert.NoError(t, err) assert.Equal(t, "[test/repo:test] \r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1\r\n[2020558](http://localhost:3000/test/repo/commit/2020558fe2e34debb818a514715839cabd25e778) commit message - user1", body.Content.Text) + assert.Equal(t, feishuGenSign(hook.Secret, body.Timestamp), body.Sign) + + // a separate sign test, the result is generated by official python code, so the algo must be correct + assert.Equal(t, "rWZ84lcag1x9aBFhn1gtV4ZN+4gme3pilfQNMk86vKg=", feishuGenSign("a", 1)) } diff --git a/services/webhook/general.go b/services/webhook/general.go index 251659e75e..be457e46f5 100644 --- a/services/webhook/general.go +++ b/services/webhook/general.go @@ -327,6 +327,37 @@ func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatte return text, color } +func getWorkflowRunPayloadInfo(p *api.WorkflowRunPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) { + description := p.WorkflowRun.Conclusion + if description == "" { + description = p.WorkflowRun.Status + } + refLink := linkFormatter(p.WorkflowRun.HTMLURL, fmt.Sprintf("%s(#%d)", p.WorkflowRun.DisplayTitle, p.WorkflowRun.ID)+"["+base.ShortSha(p.WorkflowRun.HeadSha)+"]:"+description) + + text = fmt.Sprintf("Workflow Run %s: %s", p.Action, refLink) + switch description { + case "waiting": + color = orangeColor + case "queued": + color = orangeColorLight + case "success": + color = greenColor + case "failure": + color = redColor + case "cancelled": + color = yellowColor + case "skipped": + color = purpleColor + default: + color = greyColor + } + if withSender { + text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName) + } + + return text, color +} + func getWorkflowJobPayloadInfo(p *api.WorkflowJobPayload, linkFormatter linkFormatter, withSender bool) (text string, color int) { description := p.WorkflowJob.Conclusion if description == "" { diff --git a/services/webhook/matrix.go b/services/webhook/matrix.go index 5bc7ba097e..3e9163f78c 100644 --- a/services/webhook/matrix.go +++ b/services/webhook/matrix.go @@ -252,6 +252,12 @@ func (m matrixConvertor) Status(p *api.CommitStatusPayload) (MatrixPayload, erro return m.newPayload(text) } +func (m matrixConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MatrixPayload, error) { + text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true) + + return m.newPayload(text) +} + func (m matrixConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MatrixPayload, error) { text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true) diff --git a/services/webhook/msteams.go b/services/webhook/msteams.go index 07d28c3867..450a544b42 100644 --- a/services/webhook/msteams.go +++ b/services/webhook/msteams.go @@ -318,6 +318,20 @@ func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, er ), nil } +func (msteamsConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MSTeamsPayload, error) { + title, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false) + + return createMSTeamsPayload( + p.Repo, + p.Sender, + title, + "", + p.WorkflowRun.HTMLURL, + color, + &MSTeamsFact{"WorkflowRun:", p.WorkflowRun.DisplayTitle}, + ), nil +} + func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) { title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false) diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go index 9e3f21de29..672abd5c95 100644 --- a/services/webhook/notifier.go +++ b/services/webhook/notifier.go @@ -5,10 +5,8 @@ package webhook import ( "context" - "fmt" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" @@ -18,6 +16,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/repository" @@ -295,6 +294,43 @@ func (m *webhookNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu } } +func (m *webhookNotifier) DeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) { + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + if issue.IsPull { + if err := issue.LoadPullRequest(ctx); err != nil { + log.Error("LoadPullRequest: %v", err) + return + } + if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventPullRequest, &api.PullRequestPayload{ + Action: api.HookIssueDeleted, + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, doer), + Repository: convert.ToRepo(ctx, issue.Repo, permission), + Sender: convert.ToUser(ctx, doer, nil), + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } + } else { + if err := issue.LoadRepo(ctx); err != nil { + log.Error("issue.LoadRepo: %v", err) + return + } + if err := issue.LoadPoster(ctx); err != nil { + log.Error("issue.LoadPoster: %v", err) + return + } + if err := PrepareWebhooks(ctx, EventSource{Repository: issue.Repo}, webhook_module.HookEventIssues, &api.IssuePayload{ + Action: api.HookIssueDeleted, + Index: issue.Index, + Issue: convert.ToAPIIssue(ctx, issue.Poster, issue), + Repository: convert.ToRepo(ctx, issue.Repo, permission), + Sender: convert.ToUser(ctx, doer, nil), + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } + } +} + func (m *webhookNotifier) NewPullRequest(ctx context.Context, pull *issues_model.PullRequest, mentions []*user_model.User) { if err := pull.LoadIssue(ctx); err != nil { log.Error("pull.LoadIssue: %v", err) @@ -956,72 +992,61 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_ org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) } - err := job.LoadAttributes(ctx) + status, _ := convert.ToActionsStatus(job.Status) + + convertedJob, err := convert.ToActionWorkflowJob(ctx, repo, task, job) if err != nil { - log.Error("Error loading job attributes: %v", err) + log.Error("ToActionWorkflowJob: %v", err) return } - jobIndex := 0 - jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID) + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{ + Action: status, + WorkflowJob: convertedJob, + Organization: org, + Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), + Sender: convert.ToUser(ctx, sender, nil), + }); err != nil { + log.Error("PrepareWebhooks: %v", err) + } +} + +func (*webhookNotifier) WorkflowRunStatusUpdate(ctx context.Context, repo *repo_model.Repository, sender *user_model.User, run *actions_model.ActionRun) { + source := EventSource{ + Repository: repo, + Owner: repo.Owner, + } + + var org *api.Organization + if repo.Owner.IsOrganization() { + org = convert.ToOrganization(ctx, organization.OrgFromUser(repo.Owner)) + } + + status := convert.ToWorkflowRunAction(run.Status) + + gitRepo, err := gitrepo.OpenRepository(ctx, repo) if err != nil { - log.Error("Error loading getting run jobs: %v", err) + log.Error("OpenRepository: %v", err) return } - for i, j := range jobs { - if j.ID == job.ID { - jobIndex = i - break - } - } + defer gitRepo.Close() - status, conclusion := toActionStatus(job.Status) - var runnerID int64 - var runnerName string - var steps []*api.ActionWorkflowStep + convertedWorkflow, err := convert.GetActionWorkflow(ctx, gitRepo, repo, run.WorkflowID) + if err != nil { + log.Error("GetActionWorkflow: %v", err) + return + } - if task != nil { - runnerID = task.RunnerID - if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok { - runnerName = runner.Name - } - for i, step := range task.Steps { - stepStatus, stepConclusion := toActionStatus(job.Status) - steps = append(steps, &api.ActionWorkflowStep{ - Name: step.Name, - Number: int64(i), - Status: stepStatus, - Conclusion: stepConclusion, - StartedAt: step.Started.AsTime().UTC(), - CompletedAt: step.Stopped.AsTime().UTC(), - }) - } + convertedRun, err := convert.ToActionWorkflowRun(ctx, repo, run) + if err != nil { + log.Error("ToActionWorkflowRun: %v", err) + return } - if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowJob, &api.WorkflowJobPayload{ - Action: status, - WorkflowJob: &api.ActionWorkflowJob{ - ID: job.ID, - // missing api endpoint for this location - URL: fmt.Sprintf("%s/actions/runs/%d/jobs/%d", repo.APIURL(), job.RunID, job.ID), - HTMLURL: fmt.Sprintf("%s/jobs/%d", job.Run.HTMLURL(), jobIndex), - RunID: job.RunID, - // Missing api endpoint for this location, artifacts are available under a nested url - RunURL: fmt.Sprintf("%s/actions/runs/%d", repo.APIURL(), job.RunID), - Name: job.Name, - Labels: job.RunsOn, - RunAttempt: job.Attempt, - HeadSha: job.Run.CommitSHA, - HeadBranch: git.RefName(job.Run.Ref).BranchName(), - Status: status, - Conclusion: conclusion, - RunnerID: runnerID, - RunnerName: runnerName, - Steps: steps, - CreatedAt: job.Created.AsTime().UTC(), - StartedAt: job.Started.AsTime().UTC(), - CompletedAt: job.Stopped.AsTime().UTC(), - }, + if err := PrepareWebhooks(ctx, source, webhook_module.HookEventWorkflowRun, &api.WorkflowRunPayload{ + Action: status, + Workflow: convertedWorkflow, + WorkflowRun: convertedRun, Organization: org, Repo: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}), Sender: convert.ToUser(ctx, sender, nil), @@ -1029,29 +1054,3 @@ func (*webhookNotifier) WorkflowJobStatusUpdate(ctx context.Context, repo *repo_ log.Error("PrepareWebhooks: %v", err) } } - -func toActionStatus(status actions_model.Status) (string, string) { - var action string - var conclusion string - switch status { - // This is a naming conflict of the webhook between Gitea and GitHub Actions - case actions_model.StatusWaiting: - action = "queued" - case actions_model.StatusBlocked: - action = "waiting" - case actions_model.StatusRunning: - action = "in_progress" - } - if status.IsDone() { - action = "completed" - switch status { - case actions_model.StatusSuccess: - conclusion = "success" - case actions_model.StatusCancelled: - conclusion = "cancelled" - case actions_model.StatusFailure: - conclusion = "failure" - } - } - return action, conclusion -} diff --git a/services/webhook/packagist.go b/services/webhook/packagist.go index 8829d95da6..e6a00b0293 100644 --- a/services/webhook/packagist.go +++ b/services/webhook/packagist.go @@ -114,6 +114,10 @@ func (pc packagistConvertor) Status(_ *api.CommitStatusPayload) (PackagistPayloa return PackagistPayload{}, nil } +func (pc packagistConvertor) WorkflowRun(_ *api.WorkflowRunPayload) (PackagistPayload, error) { + return PackagistPayload{}, nil +} + func (pc packagistConvertor) WorkflowJob(_ *api.WorkflowJobPayload) (PackagistPayload, error) { return PackagistPayload{}, nil } diff --git a/services/webhook/payloader.go b/services/webhook/payloader.go index adb7243fb1..b607bf3250 100644 --- a/services/webhook/payloader.go +++ b/services/webhook/payloader.go @@ -29,6 +29,7 @@ type payloadConvertor[T any] interface { Wiki(*api.WikiPayload) (T, error) Package(*api.PackagePayload) (T, error) Status(*api.CommitStatusPayload) (T, error) + WorkflowRun(*api.WorkflowRunPayload) (T, error) WorkflowJob(*api.WorkflowJobPayload) (T, error) } @@ -81,6 +82,8 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module return convertUnmarshalledJSON(rc.Package, data) case webhook_module.HookEventStatus: return convertUnmarshalledJSON(rc.Status, data) + case webhook_module.HookEventWorkflowRun: + return convertUnmarshalledJSON(rc.WorkflowRun, data) case webhook_module.HookEventWorkflowJob: return convertUnmarshalledJSON(rc.WorkflowJob, data) } @@ -92,7 +95,10 @@ func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t * if err != nil { return nil, nil, err } + return prepareJSONRequest(payload, w, t, withDefaultHeaders) +} +func prepareJSONRequest[T any](payload T, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { body, err := json.MarshalIndent(payload, "", " ") if err != nil { return nil, nil, err diff --git a/services/webhook/slack.go b/services/webhook/slack.go index 589ef3fe9b..3d645a55d0 100644 --- a/services/webhook/slack.go +++ b/services/webhook/slack.go @@ -173,6 +173,12 @@ func (s slackConvertor) Status(p *api.CommitStatusPayload) (SlackPayload, error) return s.createPayload(text, nil), nil } +func (s slackConvertor) WorkflowRun(p *api.WorkflowRunPayload) (SlackPayload, error) { + text, _ := getWorkflowRunPayloadInfo(p, SlackLinkFormatter, true) + + return s.createPayload(text, nil), nil +} + func (s slackConvertor) WorkflowJob(p *api.WorkflowJobPayload) (SlackPayload, error) { text, _ := getWorkflowJobPayloadInfo(p, SlackLinkFormatter, true) diff --git a/services/webhook/telegram.go b/services/webhook/telegram.go index ca74eabe1c..fdd428b45c 100644 --- a/services/webhook/telegram.go +++ b/services/webhook/telegram.go @@ -180,6 +180,12 @@ func (t telegramConvertor) Status(p *api.CommitStatusPayload) (TelegramPayload, return createTelegramPayloadHTML(text), nil } +func (telegramConvertor) WorkflowRun(p *api.WorkflowRunPayload) (TelegramPayload, error) { + text, _ := getWorkflowRunPayloadInfo(p, htmlLinkFormatter, true) + + return createTelegramPayloadHTML(text), nil +} + func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload, error) { text, _ := getWorkflowJobPayloadInfo(p, htmlLinkFormatter, true) @@ -189,7 +195,7 @@ func (telegramConvertor) WorkflowJob(p *api.WorkflowJobPayload) (TelegramPayload func createTelegramPayloadHTML(msgHTML string) TelegramPayload { // https://core.telegram.org/bots/api#formatting-options return TelegramPayload{ - Message: strings.TrimSpace(markup.Sanitize(msgHTML)), + Message: strings.TrimSpace(string(markup.Sanitize(msgHTML))), ParseMode: "HTML", DisableWebPreview: true, } diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 6bac02712b..5a805347e3 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -13,6 +13,7 @@ import ( webhook_model "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" webhook_module "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/convert" @@ -84,7 +85,8 @@ func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { func TestWebhookUserMail(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) - setting.Service.NoReplyAddress = "no-reply.com" + defer test.MockVariableValue(&setting.Service.NoReplyAddress, "no-reply.com")() + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) assert.Equal(t, user.GetPlaceholderEmail(), convert.ToUser(db.DefaultContext, user, nil).Email) assert.Equal(t, user.Email, convert.ToUser(db.DefaultContext, user, user).Email) diff --git a/services/webhook/wechatwork.go b/services/webhook/wechatwork.go index 2b19822caf..1875317406 100644 --- a/services/webhook/wechatwork.go +++ b/services/webhook/wechatwork.go @@ -181,6 +181,12 @@ func (wc wechatworkConvertor) Status(p *api.CommitStatusPayload) (WechatworkPayl return newWechatworkMarkdownPayload(text), nil } +func (wc wechatworkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (WechatworkPayload, error) { + text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true) + + return newWechatworkMarkdownPayload(text), nil +} + func (wc wechatworkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (WechatworkPayload, error) { text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true) diff --git a/services/webtheme/webtheme.go b/services/webtheme/webtheme.go index 58aea3bc74..4e89d6dbac 100644 --- a/services/webtheme/webtheme.go +++ b/services/webtheme/webtheme.go @@ -70,11 +70,11 @@ func parseThemeMetaInfoToMap(cssContent string) map[string]string { m := map[string]string{} for _, item := range matchedItems { v := item[3] - if strings.HasPrefix(v, `"`) { - v = strings.TrimSuffix(strings.TrimPrefix(v, `"`), `"`) + if after, ok := strings.CutPrefix(v, `"`); ok { + v = strings.TrimSuffix(after, `"`) v = strings.ReplaceAll(v, `\"`, `"`) - } else if strings.HasPrefix(v, `'`) { - v = strings.TrimSuffix(strings.TrimPrefix(v, `'`), `'`) + } else if after, ok := strings.CutPrefix(v, `'`); ok { + v = strings.TrimSuffix(after, `'`) v = strings.ReplaceAll(v, `\'`, `'`) } m[item[2]] = v diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 9405f7cfc8..0a955406e2 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -194,7 +194,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) if sign { - commitTreeOpts.KeyID = signingKey + commitTreeOpts.Key = signingKey if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { committer = signer } @@ -316,7 +316,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) if sign { - commitTreeOpts.KeyID = signingKey + commitTreeOpts.Key = signingKey if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { committer = signer } diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index f441c2939b..6ea3ca9c1b 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -116,9 +116,9 @@ func TestGitPathToWebPath(t *testing.T) { func TestUserWebGitPathConsistency(t *testing.T) { maxLen := 20 b := make([]byte, maxLen) - for i := 0; i < 1000; i++ { + for range 1000 { l := rand.Intn(maxLen) - for j := 0; j < l; j++ { + for j := range l { r := rand.Intn(0x80-0x20) + 0x20 b[j] = byte(r) } diff --git a/tailwind.config.js b/tailwind.config.js index fe285432f3..fa233a9814 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -29,11 +29,10 @@ export default { important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles content: [ isProduction && '!./templates/devtest/**/*', - isProduction && '!./web_src/js/standalone/devtest.js', + isProduction && '!./web_src/js/standalone/devtest.ts', '!./templates/swagger/v1_json.tmpl', '!./templates/user/auth/oidc_wellknown.tmpl', '!**/*_test.go', - '!./modules/{public,options,templates}/bindata.go', './{build,models,modules,routers,services}/**/*.go', './templates/**/*.tmpl', './web_src/js/**/*.{ts,js,vue}', diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 0c6889b599..985caf6bdf 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -90,7 +90,7 @@ {{ctx.Locale.Tr "packages.settings.delete"}} </div> <div class="content"> - {{ctx.Locale.Tr "packages.settings.delete.notice" (`<span class="name"></span>`|SafeHTML) (`<span class="dataVersion"></span>`|SafeHTML)}} + {{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "name") (HTMLFormat `<span class="%s"></span>` "dataVersion")}} </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 762013af47..ce08d49656 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -7,7 +7,7 @@ </div> </h4> <div class="ui attached segment"> - {{template "shared/repo_search" .}} + {{template "shared/repo/search" .}} </div> <div class="ui attached table segment"> <table class="ui very basic striped table unstackable"> @@ -103,7 +103,7 @@ </div> <div class="content"> <p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p> - {{ctx.Locale.Tr "repo.settings.delete_notices_2" (`<span class="name"></span>`|SafeHTML)}}<br> + {{ctx.Locale.Tr "repo.settings.delete_notices_2" (HTMLFormat `<span class="%s"></span>` "name")}}<br> {{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}<br> </div> {{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/user/view.tmpl b/templates/admin/user/view.tmpl index 31616ffbf9..67f9148e64 100644 --- a/templates/admin/user/view.tmpl +++ b/templates/admin/user/view.tmpl @@ -26,7 +26,7 @@ {{ctx.Locale.Tr "admin.repositories"}} ({{ctx.Locale.Tr "admin.total" .ReposTotal}}) </h4> <div class="ui attached segment"> - {{template "explore/repo_list" .}} + {{template "shared/repo/list" .}} </div> <h4 class="ui top attached header"> {{ctx.Locale.Tr "settings.organization"}} ({{ctx.Locale.Tr "admin.total" .OrgsTotal}}) diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl index 35e14d38d3..dacc518873 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -197,7 +197,7 @@ <span class="stopwatch-issue">{{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}}</span> </a> <div class="tw-flex tw-gap-1"> - <form class="stopwatch-commit form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/toggle"> + <form class="stopwatch-commit form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/stop"> {{.CsrfTokenHtml}} <button type="submit" diff --git a/templates/explore/repos.tmpl b/templates/explore/repos.tmpl index 53742bf0d9..68da398306 100644 --- a/templates/explore/repos.tmpl +++ b/templates/explore/repos.tmpl @@ -2,8 +2,8 @@ <div role="main" aria-label="{{.Title}}" class="page-content explore repositories"> {{template "explore/navbar" .}} <div class="ui container"> - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} </div> </div> diff --git a/templates/home.tmpl b/templates/home.tmpl index 116dc487dc..cc9da82605 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -4,10 +4,10 @@ <div class="center"> <img class="logo" width="220" height="220" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}"> <div class="hero"> - <h1 class="ui icon header title"> + <h1 class="ui icon header title tw-text-balance"> {{AppName}} </h1> - <h2>{{ctx.Locale.Tr "startpage.app_desc"}}</h2> + <h2 class="tw-text-balance">{{ctx.Locale.Tr "startpage.app_desc"}}</h2> </div> </div> </div> @@ -16,7 +16,7 @@ <h1 class="hero ui icon header"> {{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}} </h1> - <p class="large"> + <p class="large tw-text-balance"> {{ctx.Locale.Tr "startpage.install_desc" "https://docs.gitea.com/installation/install-from-binary" "https://github.com/go-gitea/gitea/tree/master/docker" "https://docs.gitea.com/installation/install-from-package"}} </p> </div> @@ -24,7 +24,7 @@ <h1 class="hero ui icon header"> {{svg "octicon-device-desktop"}} {{ctx.Locale.Tr "startpage.platform"}} </h1> - <p class="large"> + <p class="large tw-text-balance"> {{ctx.Locale.Tr "startpage.platform_desc" "https://go.dev/"}} </p> </div> @@ -34,7 +34,7 @@ <h1 class="hero ui icon header"> {{svg "octicon-rocket"}} {{ctx.Locale.Tr "startpage.lightweight"}} </h1> - <p class="large"> + <p class="large tw-text-balance"> {{ctx.Locale.Tr "startpage.lightweight_desc"}} </p> </div> @@ -42,7 +42,7 @@ <h1 class="hero ui icon header"> {{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}} </h1> - <p class="large"> + <p class="large tw-text-balance"> {{ctx.Locale.Tr "startpage.license_desc" "https://code.gitea.io/gitea" "code.gitea.io/gitea" "https://github.com/go-gitea/gitea"}} </p> </div> diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl index 80519361fd..90798b5d7c 100644 --- a/templates/org/header.tmpl +++ b/templates/org/header.tmpl @@ -18,7 +18,7 @@ {{end}} </span> </div> - {{if .RenderedDescription}}<div class="render-content markup tw-break-anywhere">{{.RenderedDescription}}</div>{{end}} + {{if .RenderedDescription}}<div class="render-content markup">{{.RenderedDescription}}</div>{{end}} <div class="text light meta tw-mt-1"> {{if .Org.Location}}<div class="flex-text-block">{{svg "octicon-location"}} <span>{{.Org.Location}}</span></div>{{end}} {{if .Org.Website}}<div class="flex-text-block">{{svg "octicon-link"}} <a class="muted" target="_blank" rel="noopener noreferrer me" href="{{.Org.Website}}">{{.Org.Website}}</a></div>{{end}} diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl index cffdfabfaa..3cde3554c9 100644 --- a/templates/org/home.tmpl +++ b/templates/org/home.tmpl @@ -8,8 +8,8 @@ {{if .ProfileReadmeContent}} <div id="readme_profile" class="render-content markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div> {{end}} - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} </div> diff --git a/templates/org/member/members.tmpl b/templates/org/member/members.tmpl index 4388dc9520..2d0f4bc423 100644 --- a/templates/org/member/members.tmpl +++ b/templates/org/member/members.tmpl @@ -73,7 +73,7 @@ {{ctx.Locale.Tr "org.members.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.members.leave.detail" (HTMLFormat `<span class="%s"></span>` "dataOrganizationName")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> @@ -82,7 +82,7 @@ {{ctx.Locale.Tr "org.members.remove"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.members.remove.detail" (HTMLFormat `<span class="%s"></span>` "name") (HTMLFormat `<span class="%s"></span>` "dataOrganizationName")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/org/settings/delete.tmpl b/templates/org/settings/delete.tmpl deleted file mode 100644 index e1ef471e34..0000000000 --- a/templates/org/settings/delete.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings delete")}} - - <div class="org-setting-content"> - <h4 class="ui top attached error header"> - {{ctx.Locale.Tr "org.settings.delete_account"}} - </h4> - <div class="ui attached error segment"> - <div class="ui red message"> - <p class="text left">{{svg "octicon-alert"}} {{ctx.Locale.Tr "org.settings.delete_prompt"}}</p> - </div> - <form class="ui form ignore-dirty" id="delete-form" action="{{.Link}}" method="post"> - {{.CsrfTokenHtml}} - <div class="inline required field {{if .Err_OrgName}}error{{end}}"> - <label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}}</label> - <input id="org_name" name="org_name" value="" autocomplete="off" autofocus required> - </div> - <button class="ui red button delete-button" data-type="form" data-form="#delete-form"> - {{ctx.Locale.Tr "org.settings.confirm_delete_account"}} - </button> - </form> - </div> - </div> - -<div class="ui g-modal-confirm delete modal"> - <div class="header"> - {{svg "octicon-trash"}} - {{ctx.Locale.Tr "org.settings.delete_org_title"}} - </div> - <div class="content"> - <p>{{ctx.Locale.Tr "org.settings.delete_org_desc"}}</p> - </div> - {{template "base/modal_actions_confirm" .}} -</div> - -{{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl index ce792f667c..58475de7e7 100644 --- a/templates/org/settings/navbar.tmpl +++ b/templates/org/settings/navbar.tmpl @@ -41,8 +41,5 @@ </div> </details> {{end}} - <a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete"> - {{ctx.Locale.Tr "org.settings.delete"}} - </a> </div> </div> diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index f4583bbe36..d94bb4c62b 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -1,101 +1,97 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings options")}} - <div class="org-setting-content"> - <h4 class="ui top attached header"> - {{ctx.Locale.Tr "org.settings.options"}} - </h4> - <div class="ui attached segment"> - <form class="ui form" action="{{.Link}}" method="post"> - {{.CsrfTokenHtml}} - <div class="required field {{if .Err_Name}}error{{end}}"> - <label for="org_name">{{ctx.Locale.Tr "org.org_name_holder"}} - <span class="text red tw-hidden" id="org-name-change-prompt"> - <br>{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}<br>{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}} - </span> - </label> - <input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" required maxlength="40"> - </div> - <div class="field {{if .Err_FullName}}error{{end}}"> - <label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label> - <input id="full_name" name="full_name" value="{{.Org.FullName}}" maxlength="100"> - </div> - <div class="field {{if .Err_Email}}error{{end}}"> - <label for="email">{{ctx.Locale.Tr "org.settings.email"}}</label> - <input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255"> - </div> - <div class="field {{if .Err_Description}}error{{end}}"> - {{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}} - <label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label> - <textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea> - </div> - <div class="field {{if .Err_Website}}error{{end}}"> - <label for="website">{{ctx.Locale.Tr "org.settings.website"}}</label> - <input id="website" name="website" type="url" value="{{.Org.Website}}" maxlength="255"> - </div> - <div class="field"> - <label for="location">{{ctx.Locale.Tr "org.settings.location"}}</label> - <input id="location" name="location" value="{{.Org.Location}}" maxlength="50"> - </div> - <div class="divider"></div> - <div class="field" id="visibility_box"> - <label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label> - <div class="field"> - <div class="ui radio checkbox"> - <input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label> - </div> - </div> - <div class="field"> - <div class="ui radio checkbox"> - <input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label> - </div> - </div> - <div class="field"> - <div class="ui radio checkbox"> - <input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label> - </div> - </div> - </div> +<div class="ui segments org-setting-content"> + <h4 class="ui top attached header"> + {{ctx.Locale.Tr "org.settings.options"}} + </h4> + <div class="ui attached segment"> + <form class="ui form" action="{{.Link}}" method="post"> + {{.CsrfTokenHtml}} + <div class="field {{if .Err_FullName}}error{{end}}"> + <label for="full_name">{{ctx.Locale.Tr "org.org_full_name_holder"}}</label> + <input id="full_name" name="full_name" value="{{.Org.FullName}}" maxlength="100"> + </div> + <div class="field {{if .Err_Email}}error{{end}}"> + <label for="email">{{ctx.Locale.Tr "org.settings.email"}}</label> + <input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255"> + </div> + <div class="field {{if .Err_Description}}error{{end}}"> + {{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}} + <label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label> + <textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea> + </div> + <div class="field {{if .Err_Website}}error{{end}}"> + <label for="website">{{ctx.Locale.Tr "org.settings.website"}}</label> + <input id="website" name="website" type="url" value="{{.Org.Website}}" maxlength="255"> + </div> + <div class="field"> + <label for="location">{{ctx.Locale.Tr "org.settings.location"}}</label> + <input id="location" name="location" value="{{.Org.Location}}" maxlength="50"> + </div> + + <div class="divider"></div> + <div class="field" id="visibility_box"> + <label for="visibility">{{ctx.Locale.Tr "org.settings.visibility"}}</label> + <div class="field"> + <div class="ui radio checkbox"> + <input class="enable-system-radio" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.visibility.public"}}</label> + </div> + </div> + <div class="field"> + <div class="ui radio checkbox"> + <input class="enable-system-radio" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.visibility.limited"}}</label> + </div> + </div> + <div class="field"> + <div class="ui radio checkbox"> + <input class="enable-system-radio" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.visibility.private"}}</label> + </div> + </div> + </div> - <div class="field" id="permission_box"> - <label>{{ctx.Locale.Tr "org.settings.permission"}}</label> - <div class="field"> - <div class="ui checkbox"> - <input type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}> - <label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label> - </div> - </div> - </div> + <div class="field" id="permission_box"> + <label>{{ctx.Locale.Tr "org.settings.permission"}}</label> + <div class="field"> + <div class="ui checkbox"> + <input type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}> + <label>{{ctx.Locale.Tr "org.settings.repoadminchangeteam"}}</label> + </div> + </div> + </div> - {{if .SignedUser.IsAdmin}} - <div class="divider"></div> + {{if .SignedUser.IsAdmin}} + <div class="divider"></div> - <div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}"> - <label for="max_repo_creation">{{ctx.Locale.Tr "admin.users.max_repo_creation"}}</label> - <input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}"> - <p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p> - </div> - {{end}} + <div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}"> + <label for="max_repo_creation">{{ctx.Locale.Tr "admin.users.max_repo_creation"}}</label> + <input id="max_repo_creation" name="max_repo_creation" type="number" min="-1" value="{{.Org.MaxRepoCreation}}"> + <p class="help">{{ctx.Locale.Tr "admin.users.max_repo_creation_desc"}}</p> + </div> + {{end}} - <div class="field"> - <button class="ui primary button">{{ctx.Locale.Tr "org.settings.update_settings"}}</button> - </div> - </form> + <div class="field"> + <button class="ui primary button">{{ctx.Locale.Tr "org.settings.update_settings"}}</button> + </div> + </form> - <div class="divider"></div> + <div class="divider"></div> - <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data"> - {{.CsrfTokenHtml}} - <div class="inline field"> - {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}} - </div> - <div class="field"> - <button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button> - <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button> - </div> - </form> - </div> + <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data"> + {{.CsrfTokenHtml}} + <div class="inline field"> + {{template "shared/avatar_upload_crop" dict "LabelText" (ctx.Locale.Tr "settings.choose_new_avatar")}} + </div> + <div class="field"> + <button class="ui primary button">{{ctx.Locale.Tr "settings.update_avatar"}}</button> + <button class="ui red button link-action" data-url="{{.Link}}/avatar/delete">{{ctx.Locale.Tr "settings.delete_current_avatar"}}</button> </div> + </form> + </div> +</div> + +{{template "org/settings/options_dangerzone" .}} + {{template "org/settings/layout_footer" .}} diff --git a/templates/org/settings/options_dangerzone.tmpl b/templates/org/settings/options_dangerzone.tmpl new file mode 100644 index 0000000000..01cf3fd405 --- /dev/null +++ b/templates/org/settings/options_dangerzone.tmpl @@ -0,0 +1,93 @@ +<h4 class="ui top attached error header"> + {{ctx.Locale.Tr "repo.settings.danger_zone"}} +</h4> +<div class="ui attached error danger segment"> + <div class="flex-list"> + <div class="flex-item tw-items-center"> + <div class="flex-item-main"> + <div class="flex-item-title">{{ctx.Locale.Tr "org.settings.rename"}}</div> + <div class="flex-item-body">{{ctx.Locale.Tr "org.settings.rename_desc"}}</div> + </div> + <div class="flex-item-trailing"> + <button class="ui basic red show-modal button" data-modal="#rename-org-modal">{{ctx.Locale.Tr "org.settings.rename"}}</button> + </div> + </div> + + <div class="flex-item"> + <div class="flex-item-main"> + <div class="flex-item-title">{{ctx.Locale.Tr "org.settings.delete_account"}}</div> + <div class="flex-item-body">{{ctx.Locale.Tr "org.settings.delete_prompt"}}</div> + </div> + <div class="flex-item-trailing"> + <button class="ui basic red show-modal button" data-modal="#delete-org-modal">{{ctx.Locale.Tr "org.settings.delete_account"}}</button> + </div> + </div> + </div> +</div> + +<div class="ui small modal" id="rename-org-modal"> + <div class="header"> + {{ctx.Locale.Tr "org.settings.rename"}} + </div> + <div class="content"> + <ul class="ui warning message"> + <li>{{ctx.Locale.Tr "org.settings.rename_notices_1"}}</li> + <li>{{ctx.Locale.Tr "org.settings.rename_notices_2"}}</li> + </ul> + <form class="ui form form-fetch-action" action="{{.Link}}/rename" method="post"> + {{.CsrfTokenHtml}} + <div class="field"> + <label> + {{ctx.Locale.Tr "org.settings.name_confirm"}} + <span class="text red">{{.Org.Name}}</span> + </label> + </div> + <div class="required field"> + <label for="org_name_to_rename">{{ctx.Locale.Tr "org.org_name_holder"}}</label> + <input id="org_name_to_rename" name="org_name" required> + </div> + + <div class="required field"> + <label>{{ctx.Locale.Tr "org.settings.rename_new_org_name"}}</label> + <input name="new_org_name" required> + </div> + + <div class="actions"> + <button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> + <button class="ui red button">{{ctx.Locale.Tr "org.settings.rename"}}</button> + </div> + </form> + </div> +</div> + +<div class="ui small modal" id="delete-org-modal"> + <div class="header"> + {{ctx.Locale.Tr "org.settings.delete_account"}} + </div> + <div class="content"> + <ul class="ui warning message"> + <li>{{ctx.Locale.Tr "org.settings.delete_notices_1"}}</li> + <li>{{ctx.Locale.Tr "org.settings.delete_notices_2" .Org.Name}}</li> + <li>{{ctx.Locale.Tr "org.settings.delete_notices_3" .Org.Name}}</li> + <li>{{ctx.Locale.Tr "org.settings.delete_notices_4" .Org.Name}}</li> + </ul> + <form class="ui form form-fetch-action" action="{{.Link}}/delete" method="post"> + {{.CsrfTokenHtml}} + <div class="field"> + <label> + {{ctx.Locale.Tr "org.settings.name_confirm"}} + <span class="text red">{{.Org.Name}}</span> + </label> + </div> + <div class="required field"> + <label>{{ctx.Locale.Tr "org.org_name_holder"}}</label> + <input name="org_name" required> + </div> + + <div class="actions"> + <button class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button> + <button class="ui red button">{{ctx.Locale.Tr "org.settings.delete_account"}}</button> + </div> + </form> + </div> +</div> diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index 5433f01530..4bc063f90c 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -81,7 +81,7 @@ {{ctx.Locale.Tr "org.members.remove"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.remove.detail" (`<span class="name"></span>`|SafeHTML) (`<span class="dataTeamName"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.members.remove.detail" (HTMLFormat `<span class="%s"></span>` "name") (HTMLFormat `<span class="%s"></span>` "dataTeamName")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 8390bf0acd..6dd5cb3eeb 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -90,7 +90,7 @@ {{ctx.Locale.Tr "org.teams.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "name")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index 432df10749..cdd2789128 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -49,7 +49,7 @@ {{ctx.Locale.Tr "org.teams.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.teams.leave.detail" (`<span class="name"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "name")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index 7d89f8c6e2..1a1335aaa6 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -16,7 +16,17 @@ </div> <div class="field"> <label>{{svg "octicon-code"}} {{ctx.Locale.Tr "packages.container.digest"}}</label> - <div class="markup"><pre class="code-block"><code>{{range .PackageDescriptor.Files}}{{if eq .File.LowerName "manifest.json"}}{{.Properties.GetByName "container.digest"}}{{end}}{{end}}</code></pre></div> + <div class="markup"> + <div class="code-block-container code-overflow-scroll"> + <pre class="code-block"><code> + {{- range .PackageDescriptor.Files -}} + {{- if eq .File.LowerName "manifest.json" -}} + {{- .Properties.GetByName "container.digest" -}}{{"\n"}} + {{- end -}} + {{- end -}} + </code></pre> + </div> + </div> </div> <div class="field"> <label>{{ctx.Locale.Tr "packages.registry.documentation" "Container" "https://docs.gitea.com/usage/packages/container/"}}</label> @@ -39,7 +49,11 @@ {{/* "unknown/unknown" is attestation-manifest, so we should skip it */}} {{if ne .Platform "unknown/unknown"}} <tr> - <td><a class="tw-font-mono" href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .Digest}}">{{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}}</a></td> + <td> + <a class="tw-font-mono" href="{{$.PackageDescriptor.PackageWebLink}}/{{$.PackageDescriptor.Version.LowerVersion}}/{{PathEscape .Digest}}"> + {{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}} + </a> + </td> <td>{{.Platform}}</td> <td>{{FileSize .Size}}</td> </tr> @@ -55,12 +69,24 @@ {{.PackageDescriptor.Metadata.Description}} </div> {{end}} - {{if .PackageDescriptor.Metadata.ImageLayers}} - <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.layers"}}</h4> + + {{/* a container manifest may contain sub manifests, so here we try to display some information of the sub manifest, + not perfect, just better than before */}} + {{$imageMetadata := .ContainerImageMetadata}} + {{if $imageMetadata.ImageLayers}} + <h4 class="ui top attached header flex-text-block"> + {{ctx.Locale.Tr "packages.container.layers"}} + {{/* only show the platform if the image metadata is not the package's, which means that it is a sub manifest */}} + {{if ne .ContainerImageMetadata .PackageDescriptor.Metadata}} + <span class="tw-text-sm flex-text-inline" title="{{ctx.Locale.Tr "packages.container.details.platform"}}"> + ({{svg "octicon-cpu" 12}} {{.ContainerImageMetadata.Platform}}) + </span> + {{end}} + </h4> <div class="ui attached segment tw-break-anywhere"> - <table class="ui very basic compact table"> + <table class="ui very basic compact table tw-font-mono"> <tbody> - {{range .PackageDescriptor.Metadata.ImageLayers}} + {{range $imageMetadata.ImageLayers}} <tr> <td>{{.}}</td> </tr> @@ -69,10 +95,10 @@ </table> </div> {{end}} - {{if .PackageDescriptor.Metadata.Labels}} + {{if $imageMetadata.Labels}} <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.labels"}}</h4> <div class="ui attached segment"> - <table class="ui very basic compact table"> + <table class="ui very basic compact table tw-font-mono"> <thead> <tr> <th>{{ctx.Locale.Tr "packages.container.labels.key"}}</th> @@ -80,7 +106,7 @@ </tr> </thead> <tbody> - {{range $key, $value := .PackageDescriptor.Metadata.Labels}} + {{range $key, $value := $imageMetadata.Labels}} <tr> <td class="tw-align-top">{{$key}}</td> <td class="tw-break-anywhere">{{$value}}</td> diff --git a/templates/package/content/pypi.tmpl b/templates/package/content/pypi.tmpl index 2a22a6ed71..2625c160fe 100644 --- a/templates/package/content/pypi.tmpl +++ b/templates/package/content/pypi.tmpl @@ -4,7 +4,7 @@ <div class="ui form"> <div class="field"> <label>{{svg "octicon-terminal"}} {{ctx.Locale.Tr "packages.pypi.install"}}</label> - <div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> {{.PackageDescriptor.Package.Name}}</code></pre></div> + <div class="markup"><pre class="code-block"><code>pip install --index-url <origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/pypi/simple/"></origin-url> --extra-index-url https://pypi.org/ {{.PackageDescriptor.Package.Name}}</code></pre></div> </div> <div class="field"> <label>{{ctx.Locale.Tr "packages.registry.documentation" "PyPI" "https://docs.gitea.com/usage/packages/pypi/"}}</label> diff --git a/templates/package/shared/view.tmpl b/templates/package/shared/view.tmpl index 713e1bbfc5..52673accf9 100644 --- a/templates/package/shared/view.tmpl +++ b/templates/package/shared/view.tmpl @@ -1,4 +1,5 @@ <div class="issue-title-header"> + {{$packageVersionLink := print $.PackageDescriptor.PackageWebLink "/" (PathEscape .PackageDescriptor.Version.LowerVersion)}} <h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1> <div> {{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}} @@ -9,8 +10,8 @@ {{end}} </div> </div> -<div class="issue-content"> - <div class="issue-content-left"> +<div class="packages-content"> + <div class="packages-content-left"> {{template "package/content/alpine" .}} {{template "package/content/arch" .}} {{template "package/content/cargo" .}} @@ -34,7 +35,7 @@ {{template "package/content/swift" .}} {{template "package/content/vagrant" .}} </div> - <div class="issue-content-right ui segment"> + <div class="ui segment packages-content-right"> <strong>{{ctx.Locale.Tr "packages.details"}}</strong> <div class="ui relaxed list flex-items-block"> <div class="item">{{svg .PackageDescriptor.Package.Type.SVGName}} {{.PackageDescriptor.Package.Type.Name}}</div> @@ -74,8 +75,8 @@ <div class="ui relaxed list"> {{range .PackageDescriptor.Files}} <div class="item"> - <a href="{{$.Link}}/files/{{.File.ID}}">{{.File.Name}}</a> - <span class="text small file-size">{{FileSize .Blob.Size}}</span> + <a href="{{$packageVersionLink}}/files/{{.File.ID}}">{{.File.Name}}</a> + <span class="text small tw-whitespace-nowrap">{{FileSize .Blob.Size}}</span> </div> {{end}} </div> @@ -98,7 +99,7 @@ <div class="item">{{svg "octicon-issue-opened"}} <a href="{{.PackageDescriptor.Repository.Link}}/issues">{{ctx.Locale.Tr "repo.issues"}}</a></div> {{end}} {{if .CanWritePackages}} - <div class="item">{{svg "octicon-tools"}} <a href="{{.Link}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div> + <div class="item">{{svg "octicon-tools"}} <a href="{{$packageVersionLink}}/settings">{{ctx.Locale.Tr "repo.settings"}}</a></div> {{end}} </div> {{end}} diff --git a/templates/post-install.tmpl b/templates/post-install.tmpl index 0c9aa35c90..9baac4f84c 100644 --- a/templates/post-install.tmpl +++ b/templates/post-install.tmpl @@ -4,7 +4,7 @@ <!-- the "cup" has a handler, so move it a little leftward to make it visually in the center --> <div class="tw-ml-[-30px]"><img width="160" src="{{AssetUrlPrefix}}/img/loading.png" alt aria-hidden="true"></div> <div class="tw-my-[2em] tw-text-[18px]"> - <a id="goto-user-login" href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "install.installing_desc"}}</a> + <a id="goto-after-install" href="{{AppSubUrl}}{{Iif .IsAccountCreated "/user/login" "/user/sign_up"}}">{{ctx.Locale.Tr "install.installing_desc"}}</a> </div> </div> </div> diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl index 7e89db0005..6aa776da02 100644 --- a/templates/projects/view.tmpl +++ b/templates/projects/view.tmpl @@ -130,12 +130,12 @@ <input class="project-column-id" type="hidden" name="id"> <div class="required field"> <label class="project-column-title-label" for="project-column-title-input">title</label> - <input id="project-column-title-input" name="title" value="{{.Title}}" required> + <input id="project-column-title-input" name="title" required> </div> <div class="field"> <label class="project-column-color-label" for="project-column-color-input">color</label> <div class="js-color-picker-input column"> - <input maxlength="7" placeholder="#c320f6" id="project-column-color-input" name="color" value="{{.Color}}"> + <input maxlength="7" placeholder="#c320f6" id="project-column-color-input" name="color"> {{template "repo/issue/label_precolors"}} </div> </div> diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index 4ebedcd73b..23df61a43c 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -36,15 +36,25 @@ <div class="run-list-meta">{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince $run.Updated}}</div> <div class="run-list-meta">{{svg "octicon-stopwatch" 16}}{{$run.Duration}}</div> </div> - {{if and ($.AllowDeleteWorkflowRuns) ($run.Status.IsDone)}} - <button class="btn interact-bg link-action tw-p-2" - data-url="{{$run.Link}}/delete" - data-modal-confirm="{{ctx.Locale.Tr "actions.runs.delete.description"}}" - data-tooltip-content="{{ctx.Locale.Tr "actions.runs.delete"}}" - > - {{svg "octicon-trash"}} - </button> - {{end}} + <div class="ui dropdown jump tw-p-2"> + {{svg "octicon-kebab-horizontal"}} + <div class="menu flex-items-menu"> + <a class="item" href="{{$run.Link}}/workflow">{{svg "octicon-play"}}{{ctx.Locale.Tr "actions.runs.view_workflow_file"}}</a> + {{if and $.CanWriteRepoUnitActions (not $run.Status.IsDone)}} + <a class="item link-action" data-url="{{$run.Link}}/cancel"> + {{svg "octicon-x"}}{{ctx.Locale.Tr "actions.runs.cancel"}} + </a> + {{end}} + {{if and $.CanWriteRepoUnitActions $run.Status.IsDone}} + <a class="item link-action" + data-url="{{$run.Link}}/delete" + data-modal-confirm="{{ctx.Locale.Tr "actions.runs.delete.description"}}" + > + {{svg "octicon-trash"}}{{ctx.Locale.Tr "actions.runs.delete"}} + </a> + {{end}} + </div> + </div> </div> </div> {{end}} diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index d0741cdc0b..4e338ffcfc 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -4,7 +4,7 @@ data-actions-url="{{.ActionsURL}}" data-locale-approve="{{ctx.Locale.Tr "repo.diff.review.approve"}}" - data-locale-cancel="{{ctx.Locale.Tr "cancel"}}" + data-locale-cancel="{{ctx.Locale.Tr "actions.runs.cancel"}}" data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}" data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}" diff --git a/templates/repo/actions/workflow_dispatch_inputs.tmpl b/templates/repo/actions/workflow_dispatch_inputs.tmpl index 8b8292af1d..37538a318f 100644 --- a/templates/repo/actions/workflow_dispatch_inputs.tmpl +++ b/templates/repo/actions/workflow_dispatch_inputs.tmpl @@ -33,7 +33,8 @@ </div> {{end}} <div class="ui field"> - <button class="ui tiny primary button" type="submit">{{ctx.Locale.Tr "actions.workflow.run"}}</button> + {{/* use autofocus here to prevent the "branch selection" dropdown from getting focus, otherwise it will auto popup */}} + <button class="ui tiny primary button" autofocus type="submit">{{ctx.Locale.Tr "actions.workflow.run"}}</button> </div> {{end}} {{range .workflows}} diff --git a/templates/repo/blame.tmpl b/templates/repo/blame.tmpl index 9596fe837a..c4d9f0741f 100644 --- a/templates/repo/blame.tmpl +++ b/templates/repo/blame.tmpl @@ -82,6 +82,8 @@ </table> {{end}}{{/* end if .IsFileTooLarge */}} <div class="code-line-menu tippy-target"> + {{/*FIXME: the "HasSourceRenderedToggle" is never set on blame page, it should mean "whether the file is renderable". + If the file is renderable, then it must has the "display=source" parameter to make sure the file view page shows the source code, then line number works. */}} {{if $.Permission.CanRead ctx.Consts.RepoUnitTypeIssues}} <a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a> {{end}} diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index fffe3a08cc..9e86641c6f 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -107,8 +107,14 @@ {{end}} </td> <td class="two wide ui"> - {{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}} - <div class="commit-divergence"> + {{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}} + {{$tooltipDivergence := ""}} + {{if or .CommitsBehind .CommitsAhead}} + {{$tooltipDivergence = ctx.Locale.Tr "repo.branch.commits_divergence_from" .CommitsBehind .CommitsAhead $.DefaultBranchBranch.DBBranch.Name}} + {{else}} + {{$tooltipDivergence = ctx.Locale.Tr "repo.branch.commits_no_divergence" $.DefaultBranchBranch.DBBranch.Name}} + {{end}} + <div class="commit-divergence" data-tooltip-content="{{$tooltipDivergence}}"> <div class="bar-group"> <div class="count count-behind">{{.CommitsBehind}}</div> {{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}} @@ -119,7 +125,7 @@ <div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div> </div> </div> - {{end}} + {{end}} </td> <td class="two wide tw-text-right"> {{if not .LatestPullRequest}} diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 7abd377108..46f641824b 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -5,7 +5,7 @@ <div class="ui container fluid padded"> <div class="ui top attached header clearing segment tw-relative commit-header"> <div class="tw-flex tw-mb-4 tw-gap-1"> - <h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message $.Repository}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses}}</h3> + <h3 class="tw-mb-0 tw-flex-1"><span class="commit-summary" title="{{.Commit.Summary}}">{{ctx.RenderUtils.RenderCommitMessage .Commit.Message $.Repository}}</span>{{template "repo/commit_statuses" dict "Status" .CommitStatus "Statuses" .CommitStatuses "AdditionalClasses" "tw-inline"}}</h3> {{if not $.PageIsWiki}} <div class="commit-header-buttons"> <a class="ui primary tiny button" href="{{.SourcePath}}"> @@ -75,7 +75,7 @@ {{.CsrfTokenHtml}} <div class="field"> <label> - {{ctx.Locale.Tr "repo.branch.new_branch_from" (`<span class="text" id="modal-create-branch-from-span"></span>`|SafeHTML)}} + {{ctx.Locale.Tr "repo.branch.new_branch_from" (HTMLFormat `<span class="%s" id="%s"></span>` "text" "modal-create-branch-from-span")}} </label> </div> <div class="required field"> @@ -100,7 +100,7 @@ <input type="hidden" name="create_tag" value="true"> <div class="field"> <label> - {{ctx.Locale.Tr "repo.tag.create_tag_from" (`<span class="text" id="modal-create-tag-from-span"></span>`|SafeHTML)}} + {{ctx.Locale.Tr "repo.tag.create_tag_from" (HTMLFormat `<span class="%s" id="%s"></span>` "text" "modal-create-tag-from-span")}} </label> </div> <div class="required field"> diff --git a/templates/repo/commit_status.tmpl b/templates/repo/commit_status.tmpl index eb700ab2bb..7184f5f8eb 100644 --- a/templates/repo/commit_status.tmpl +++ b/templates/repo/commit_status.tmpl @@ -14,3 +14,6 @@ {{if eq .State "warning"}} {{svg "gitea-exclamation" 18 "commit-status icon text yellow"}} {{end}} +{{if eq .State "skipped"}} + {{svg "octicon-skip" 18 "commit-status icon text grey"}} +{{end}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index fa96d2f0e2..22abf9a219 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -148,7 +148,7 @@ <a class="item" rel="nofollow" href="{{$.BeforeSourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> {{else}} <a class="item" rel="nofollow" href="{{$.SourcePath}}/{{PathEscapeSegments .Name}}">{{ctx.Locale.Tr "repo.diff.view_file"}}</a> - {{if and $.Repository.CanEnableEditor $.CanEditFile (not $file.IsLFSFile) (not $file.IsBin)}} + {{if and $.Repository.CanEnableEditor $.CanEditFile}} <a class="item" rel="nofollow" href="{{$.HeadRepoLink}}/_edit/{{PathEscapeSegments $.HeadBranchName}}/{{PathEscapeSegments $file.Name}}?return_uri={{print $.BackToLink "#diff-" $file.NameHash | QueryEscape}}">{{ctx.Locale.Tr "repo.editor.edit_this_file"}}</a> {{end}} {{end}} diff --git a/templates/repo/editor/cherry_pick.tmpl b/templates/repo/editor/cherry_pick.tmpl index f9c9eef5aa..7981fd0761 100644 --- a/templates/repo/editor/cherry_pick.tmpl +++ b/templates/repo/editor/cherry_pick.tmpl @@ -3,15 +3,14 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui edit form" method="post" action="{{.RepoLink}}/_cherrypick/{{.SHA}}/{{.BranchName | PathEscapeSegments}}"> + <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"> {{.CsrfTokenHtml}} - <input type="hidden" name="last_commit" value="{{.last_commit}}"> - <input type="hidden" name="page_has_posted" value="true"> + {{template "repo/editor/common_top" .}} <input type="hidden" name="revert" value="{{if eq .CherryPickType "revert"}}true{{else}}false{{end}}"> <div class="repo-editor-header"> - <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> - {{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .SHA)}} - {{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .SHA)}} + <div class="breadcrumb"> + {{$shaurl := printf "%s/commit/%s" $.RepoLink (PathEscape .FromCommitID)}} + {{$shalink := HTMLFormat `<a class="ui primary sha label" href="%s">%s</a>` $shaurl (ShortSha .FromCommitID)}} {{if eq .CherryPickType "revert"}} {{ctx.Locale.Tr "repo.editor.revert" $shalink}} {{else}} diff --git a/templates/repo/editor/commit_form.tmpl b/templates/repo/editor/commit_form.tmpl index 8f46c47b96..7067614444 100644 --- a/templates/repo/editor/commit_form.tmpl +++ b/templates/repo/editor/commit_form.tmpl @@ -1,11 +1,11 @@ <div class="commit-form-wrapper"> {{ctx.AvatarUtils.Avatar .SignedUser 40 "commit-avatar"}} <div class="commit-form"> - <h3>{{- if .CanCommitToBranch.WillSign}} - <span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CanCommitToBranch.SigningKey}}">{{svg "octicon-lock" 24}}</span> + <h3>{{- if .CommitFormOptions.WillSign}} + <span title="{{ctx.Locale.Tr "repo.signing.will_sign" .CommitFormOptions.SigningKey}}">{{svg "octicon-lock" 24}}</span> {{ctx.Locale.Tr "repo.editor.commit_signed_changes"}} {{- else}} - <span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CanCommitToBranch.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span> + <span title="{{ctx.Locale.Tr (printf "repo.signing.wont_sign.%s" .CommitFormOptions.WontSignReason)}}">{{svg "octicon-unlock" 24}}</span> {{ctx.Locale.Tr "repo.editor.commit_changes"}} {{- end}}</h3> <div class="field"> @@ -22,17 +22,17 @@ </div> <div class="quick-pull-choice js-quick-pull-choice"> <div class="field"> - <div class="ui radio checkbox {{if not .CanCommitToBranch.CanCommitToBranch}}disabled{{end}}"> + <div class="ui radio checkbox {{if not .CommitFormOptions.CanCommitToBranch}}disabled{{end}}"> <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="direct" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "direct"}}checked{{end}}> <label> {{svg "octicon-git-commit"}} {{ctx.Locale.Tr "repo.editor.commit_directly_to_this_branch" .BranchName}} - {{if not .CanCommitToBranch.CanCommitToBranch}} + {{if not .CommitFormOptions.CanCommitToBranch}} <div class="ui visible small warning message"> {{ctx.Locale.Tr "repo.editor.no_commit_to_branch"}} <ul> - {{if not .CanCommitToBranch.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}} - {{if and .CanCommitToBranch.RequireSigned (not .CanCommitToBranch.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}} + {{if not .CommitFormOptions.UserCanPush}}<li>{{ctx.Locale.Tr "repo.editor.user_no_push_to_branch"}}</li>{{end}} + {{if and .CommitFormOptions.RequireSigned (not .CommitFormOptions.WillSign)}}<li>{{ctx.Locale.Tr "repo.editor.require_signed_commit"}}</li>{{end}} </ul> </div> {{end}} @@ -42,14 +42,14 @@ {{if and (not .Repository.IsEmpty) (not .IsEditingFileOnly)}} <div class="field"> <div class="ui radio checkbox"> - {{if .CanCreatePullRequest}} + {{if .CommitFormOptions.CanCreatePullRequest}} <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.propose_file_change"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> {{else}} <input type="radio" class="js-quick-pull-choice-option" name="commit_choice" value="commit-to-new-branch" data-button-text="{{ctx.Locale.Tr "repo.editor.commit_changes"}}" {{if eq .commit_choice "commit-to-new-branch"}}checked{{end}}> {{end}} <label> {{svg "octicon-git-pull-request"}} - {{if .CanCreatePullRequest}} + {{if .CommitFormOptions.CanCreatePullRequest}} {{ctx.Locale.Tr "repo.editor.create_new_branch"}} {{else}} {{ctx.Locale.Tr "repo.editor.create_new_branch_np"}} @@ -58,7 +58,7 @@ </div> </div> <div class="quick-pull-branch-name {{if not (eq .commit_choice "commit-to-new-branch")}}tw-hidden{{end}}"> - <div class="new-branch-name-input field {{if .Err_NewBranchName}}error{{end}}"> + <div class="new-branch-name-input field"> {{svg "octicon-git-branch"}} <input type="text" name="new_branch_name" maxlength="100" value="{{.new_branch_name}}" class="input-contrast tw-mr-1 js-quick-pull-new-branch-name" placeholder="{{ctx.Locale.Tr "repo.editor.new_branch_name_desc"}}" {{if eq .commit_choice "commit-to-new-branch"}}required{{end}} title="{{ctx.Locale.Tr "repo.editor.new_branch_name"}}"> <span class="text-muted js-quick-pull-normalization-info"></span> @@ -67,7 +67,7 @@ {{end}} </div> {{if and .CommitCandidateEmails (gt (len .CommitCandidateEmails) 1)}} - <div class="field {{if .Err_CommitEmail}}error{{end}}"> + <div class="field"> <label>{{ctx.Locale.Tr "repo.editor.commit_email"}}</label> <select class="ui selection dropdown" name="commit_email"> {{- range $email := .CommitCandidateEmails -}} @@ -77,6 +77,7 @@ </div> {{end}} </div> + <input type="hidden" name="last_commit" value="{{.last_commit}}"> <button id="commit-button" type="submit" class="ui primary button"> {{if eq .commit_choice "commit-to-new-branch"}}{{ctx.Locale.Tr "repo.editor.propose_file_change"}}{{else}}{{ctx.Locale.Tr "repo.editor.commit_changes"}}{{end}} </button> diff --git a/templates/repo/editor/common_breadcrumb.tmpl b/templates/repo/editor/common_breadcrumb.tmpl new file mode 100644 index 0000000000..8cfbe09d3e --- /dev/null +++ b/templates/repo/editor/common_breadcrumb.tmpl @@ -0,0 +1,16 @@ +<div class="breadcrumb"> + <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> + {{$n := len .TreeNames}} + {{$l := Eval $n "-" 1}} + {{range $i, $v := .TreeNames}} + <div class="breadcrumb-divider">/</div> + {{if eq $i $l}} + <input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr (Iif $.PageIsUpload "repo.editor.add_subdir" "repo.editor.name_your_file")}}" data-editorconfig="{{$.EditorconfigJson}}" {{Iif $.PageIsUpload "" "required"}} autofocus> + <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span> + {{else}} + <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> + {{end}} + {{end}} + <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{or .ReturnURI (print $.BranchLink "/" (PathEscapeSegments .TreePath))}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span> + <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}"> +</div> diff --git a/templates/repo/editor/common_top.tmpl b/templates/repo/editor/common_top.tmpl new file mode 100644 index 0000000000..23280ed565 --- /dev/null +++ b/templates/repo/editor/common_top.tmpl @@ -0,0 +1,6 @@ +{{if .CommitFormOptions.WillSubmitToFork}} +<div class="ui blue message"> + {{$repoLinkHTML := HTMLFormat `<a href="%s">%s</a>` .CommitFormOptions.TargetRepo.Link .CommitFormOptions.TargetRepo.FullName}} + {{ctx.Locale.Tr "repo.editor.fork_edit_description" $repoLinkHTML}} +</div> +{{end}} diff --git a/templates/repo/editor/delete.tmpl b/templates/repo/editor/delete.tmpl index 2c0c2fc792..bf6143f1cb 100644 --- a/templates/repo/editor/delete.tmpl +++ b/templates/repo/editor/delete.tmpl @@ -3,9 +3,9 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui form" method="post"> + <form class="ui form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"> {{.CsrfTokenHtml}} - <input type="hidden" name="last_commit" value="{{.last_commit}}"> + {{template "repo/editor/common_top" .}} {{template "repo/editor/commit_form" .}} </form> </div> diff --git a/templates/repo/editor/edit.tmpl b/templates/repo/editor/edit.tmpl index ae8a60c20c..0911d02e1f 100644 --- a/templates/repo/editor/edit.tmpl +++ b/templates/repo/editor/edit.tmpl @@ -3,56 +3,49 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui edit form" method="post" + <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}" data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}" data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}" > {{.CsrfTokenHtml}} - <input type="hidden" name="last_commit" value="{{.last_commit}}"> - <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> + {{template "repo/editor/common_top" .}} <div class="repo-editor-header"> - <div class="ui breadcrumb field{{if .Err_TreePath}} error{{end}}"> - <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> - {{$n := len .TreeNames}} - {{$l := Eval $n "-" 1}} - {{range $i, $v := .TreeNames}} - <div class="breadcrumb-divider">/</div> - {{if eq $i $l}} - <input id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.name_your_file"}}" data-editorconfig="{{$.EditorconfigJson}}" required autofocus> - <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span> - {{else}} - <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> - {{end}} - {{end}} - <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{if .ReturnURI}}{{.ReturnURI}}{{else}}{{$.BranchLink}}{{if not .IsNewFile}}/{{PathEscapeSegments .TreePath}}{{end}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span> - <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required> - </div> + {{template "repo/editor/common_breadcrumb" .}} </div> - <div class="field"> - <div class="ui top attached header"> - <div class="ui compact small menu small-menu-items repo-editor-menu"> - <a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a> - <a class="item" data-tab="preview" data-preview-url="{{.Repository.Link}}/markup" data-preview-context-ref="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a> - {{if not .IsNewFile}} - <a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a> - {{end}} - </div> - </div> - <div class="ui bottom attached segment tw-p-0"> - <div class="ui active tab tw-rounded-b" data-tab="write"> - <textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" - data-previewable-extensions="{{.PreviewableExtensions}}" - data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea> - <div class="editor-loading is-loading"></div> + {{if not .NotEditableReason}} + <div class="field"> + <div class="ui top attached header"> + <div class="ui compact small menu small-menu-items repo-editor-menu"> + <a class="active item" data-tab="write">{{svg "octicon-code"}} {{if .IsNewFile}}{{ctx.Locale.Tr "repo.editor.new_file"}}{{else}}{{ctx.Locale.Tr "repo.editor.edit_file"}}{{end}}</a> + <a class="item" data-tab="preview" data-preview-url="{{.Repository.Link}}/markup" data-preview-context-ref="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}">{{svg "octicon-eye"}} {{ctx.Locale.Tr "preview"}}</a> + {{if not .IsNewFile}} + <a class="item" data-tab="diff" hx-params="context,content" hx-vals='{"context":"{{.BranchLink}}"}' hx-include="#edit_area" hx-swap="innerHTML" hx-target=".tab[data-tab='diff']" hx-indicator=".tab[data-tab='diff']" hx-post="{{.RepoLink}}/_preview/{{.BranchName | PathEscapeSegments}}/{{.TreePath | PathEscapeSegments}}">{{svg "octicon-diff"}} {{ctx.Locale.Tr "repo.editor.preview_changes"}}</a> + {{end}} + </div> </div> - <div class="ui tab tw-px-4 tw-py-3" data-tab="preview"> - {{ctx.Locale.Tr "loading"}} + <div class="ui bottom attached segment tw-p-0"> + <div class="ui active tab tw-rounded-b" data-tab="write"> + <textarea id="edit_area" name="content" class="tw-hidden" data-id="repo-{{.Repository.Name}}-{{.TreePath}}" + data-previewable-extensions="{{.PreviewableExtensions}}" + data-line-wrap-extensions="{{.LineWrapExtensions}}">{{.FileContent}}</textarea> + <div class="editor-loading is-loading"></div> + </div> + <div class="ui tab tw-px-4 tw-py-3" data-tab="preview"> + {{ctx.Locale.Tr "loading"}} + </div> + <div class="ui tab" data-tab="diff"> + <div class="tw-p-16"></div> + </div> </div> - <div class="ui tab" data-tab="diff"> - <div class="tw-p-16"></div> + </div> + {{else}} + <div class="field"> + <div class="ui segment tw-text-center"> + <h4 class="tw-font-semibold tw-mb-2">{{.NotEditableReason}}</h4> + <p>{{ctx.Locale.Tr "repo.editor.file_not_editable_hint"}}</p> </div> </div> - </div> + {{end}} {{template "repo/editor/commit_form" .}} </form> </div> diff --git a/templates/repo/editor/fork.tmpl b/templates/repo/editor/fork.tmpl new file mode 100644 index 0000000000..e28b2ba7a2 --- /dev/null +++ b/templates/repo/editor/fork.tmpl @@ -0,0 +1,18 @@ +{{template "base/head" .}} +<div role="main" aria-label="{{.Title}}" class="page-content repository"> + {{template "repo/header" .}} + <div class="ui container"> + {{template "base/alert" .}} + <form class="ui form form-fetch-action" method="post" action="{{.RepoLink}}/_fork/{{.BranchName | PathEscapeSegments}}"> + {{.CsrfTokenHtml}} + <div class="tw-text-center"> + <div class="tw-my-[40px]"> + <h3>{{ctx.Locale.Tr "repo.editor.fork_create"}}</h3> + <p>{{ctx.Locale.Tr "repo.editor.fork_create_description"}}</p> + </div> + <button class="ui primary button">{{ctx.Locale.Tr "repo.fork_repo"}}</button> + </div> + </form> + </div> +</div> +{{template "base/footer" .}} diff --git a/templates/repo/editor/patch.tmpl b/templates/repo/editor/patch.tmpl index 33a7c2a89d..fa00edd92e 100644 --- a/templates/repo/editor/patch.tmpl +++ b/templates/repo/editor/patch.tmpl @@ -3,15 +3,14 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui edit form" method="post" action="{{.RepoLink}}/_diffpatch/{{.BranchName | PathEscapeSegments}}" + <form class="ui edit form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}" data-text-empty-confirm-header="{{ctx.Locale.Tr "repo.editor.commit_empty_file_header"}}" data-text-empty-confirm-content="{{ctx.Locale.Tr "repo.editor.commit_empty_file_text"}}" > {{.CsrfTokenHtml}} - <input type="hidden" name="last_commit" value="{{.last_commit}}"> - <input type="hidden" name="page_has_posted" value="{{.PageHasPosted}}"> + {{template "repo/editor/common_top" .}} <div class="repo-editor-header"> - <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> + <div class="breadcrumb"> {{ctx.Locale.Tr "repo.editor.patching"}} <a class="section" href="{{$.RepoLink}}">{{.Repository.FullName}}</a> <div class="breadcrumb-divider">:</div> diff --git a/templates/repo/editor/upload.tmpl b/templates/repo/editor/upload.tmpl index 5725020406..3e36c77b3b 100644 --- a/templates/repo/editor/upload.tmpl +++ b/templates/repo/editor/upload.tmpl @@ -3,25 +3,11 @@ {{template "repo/header" .}} <div class="ui container"> {{template "base/alert" .}} - <form class="ui comment form" method="post"> + <form class="ui comment form form-fetch-action" method="post" action="{{.CommitFormOptions.TargetFormAction}}"> {{.CsrfTokenHtml}} + {{template "repo/editor/common_top" .}} <div class="repo-editor-header"> - <div class="ui breadcrumb field {{if .Err_TreePath}}error{{end}}"> - <a class="section" href="{{$.BranchLink}}">{{.Repository.Name}}</a> - {{$n := len .TreeNames}} - {{$l := Eval $n "-" 1}} - {{range $i, $v := .TreeNames}} - <div class="breadcrumb-divider">/</div> - {{if eq $i $l}} - <input type="text" id="file-name" maxlength="255" value="{{$v}}" placeholder="{{ctx.Locale.Tr "repo.editor.add_subdir"}}" autofocus> - <span data-tooltip-content="{{ctx.Locale.Tr "repo.editor.filename_help"}}">{{svg "octicon-info"}}</span> - {{else}} - <span class="section"><a href="{{$.BranchLink}}/{{index $.TreePaths $i | PathEscapeSegments}}">{{$v}}</a></span> - {{end}} - {{end}} - <span>{{ctx.Locale.Tr "repo.editor.or"}} <a href="{{$.BranchLink}}{{if not .IsNewFile}}/{{.TreePath | PathEscapeSegments}}{{end}}">{{ctx.Locale.Tr "repo.editor.cancel_lower"}}</a></span> - <input type="hidden" id="tree_path" name="tree_path" value="{{.TreePath}}" required> - </div> + {{template "repo/editor/common_breadcrumb" .}} </div> <div class="field"> {{template "repo/upload" .}} diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index ae3f95045b..32b5c8401b 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -47,7 +47,7 @@ <h3>{{ctx.Locale.Tr "repo.create_new_repo_command"}}</h3> <div class="markup"> <pre><code>touch README.md -git init +git init{{if ne .Repository.ObjectFormatName "sha1"}} --object-format={{.Repository.ObjectFormatName}}{{end}}{{/* for sha256 repo, it needs to set "object-format" explicitly*/}} {{if ne .Repository.DefaultBranch "master"}}git checkout -b {{.Repository.DefaultBranch}}{{end}} git add README.md git commit -m "first commit" diff --git a/templates/repo/forks.tmpl b/templates/repo/forks.tmpl index 725b67c651..6e46457893 100644 --- a/templates/repo/forks.tmpl +++ b/templates/repo/forks.tmpl @@ -1,20 +1,12 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content repository forks"> {{template "repo/header" .}} - <div class="ui container"> + <div class="ui container fork-list"> <h2 class="ui dividing header"> {{ctx.Locale.Tr "repo.forks"}} </h2> - <div class="flex-list"> - {{range .Forks}} - <div class="flex-item tw-border-0 repo-fork-item"> - <span>{{ctx.AvatarUtils.Avatar .Owner}}</span> - <span><a href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a> / <a href="{{.Link}}">{{.Name}}</a></span> - </div> - {{end}} - </div> + {{template "shared/repo/list" .}} + {{template "base/paginate" .}} </div> - - {{template "base/paginate" .}} </div> {{template "base/footer" .}} diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index 2fbb6ba428..b61076ff46 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -230,7 +230,7 @@ <div class="overflow-menu-items"> {{if(and .Repository.IsBeingCreated (.Permission.CanRead ctx.Consts.RepoUnitTypeCode))}} <a class="{{if not .PageIsRepoSettings}}active {{end}}item" href="{{.RepoLink}}"> - {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.migrating_status"}} + {{svg "octicon-clock"}} {{ctx.Locale.Tr "repo.migration_status"}} </a> {{end}} diff --git a/templates/repo/home_sidebar_bottom.tmpl b/templates/repo/home_sidebar_bottom.tmpl index f66faf6d10..01e630ccbf 100644 --- a/templates/repo/home_sidebar_bottom.tmpl +++ b/templates/repo/home_sidebar_bottom.tmpl @@ -10,8 +10,8 @@ </a> </div> <div class="flex-item"> - <div class="flex-item-icon"> - {{svg "octicon-tag" 16}} + <div class="flex-item-leading"> + <div class="tw-mt-1">{{svg "octicon-tag" 16}}</div> </div> <div class="flex-item-main"> <div class="flex-item-header"> diff --git a/templates/repo/issue/card.tmpl b/templates/repo/issue/card.tmpl index 41fe6cea8f..6dae49c455 100644 --- a/templates/repo/issue/card.tmpl +++ b/templates/repo/issue/card.tmpl @@ -4,7 +4,7 @@ {{if $attachments}} <div class="card-attachment-images"> {{range $attachments}} - <img src="{{.DownloadURL}}" alt="{{.Name}}" /> + <img loading="lazy" src="{{.DownloadURL}}" alt="{{.Name}}" /> {{end}} </div> {{end}} @@ -63,16 +63,21 @@ {{if or .Labels .Assignees}} <div class="issue-card-bottom"> - <div class="labels-list"> - {{range .Labels}} - <a target="_blank" href="{{$.Issue.Repo.Link}}/issues?labels={{.ID}}">{{ctx.RenderUtils.RenderLabel .}}</a> + {{/* the labels container must always be present, to help the assignees list right-aligned */}} + <div class="issue-card-bottom-part labels-list"> + {{range $label := .Labels}} + {{$link := print $.Issue.Repo.Link "/issues"}} + {{$link = QueryBuild $link "labels" $label.ID}} + {{ctx.RenderUtils.RenderLabelWithLink $label $link}} {{end}} </div> - <div class="issue-card-assignees"> + {{if .Assignees}} + <div class="issue-card-bottom-part tw-justify-end"> {{range .Assignees}} - <a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 28}}</a> + <a target="_blank" href="{{.HomeLink}}" data-tooltip-content="{{ctx.Locale.Tr "repo.projects.column.assigned_to"}} {{.Name}}">{{ctx.AvatarUtils.Avatar . 24}}</a> {{end}} </div> + {{end}} </div> {{end}} {{end}} diff --git a/templates/repo/issue/filter_item_label.tmpl b/templates/repo/issue/filter_item_label.tmpl index 0883d93804..04c3605a6a 100644 --- a/templates/repo/issue/filter_item_label.tmpl +++ b/templates/repo/issue/filter_item_label.tmpl @@ -22,7 +22,7 @@ <span class="label-filter-exclude-info">{{ctx.Locale.Tr "repo.issues.filter_label_exclude"}}</span> <div class="divider"></div> <a class="item label-filter-query-default" href="{{QueryBuild $queryLink "labels" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_label_no_select"}}</a> - <a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" 0}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a> + <a class="item label-filter-query-not-set" href="{{QueryBuild $queryLink "labels" "0"}}">{{ctx.Locale.Tr "repo.issues.filter_label_select_no_label"}}</a> {{/* The logic here is not the same as the label selector in the issue sidebar. The one in the issue sidebar renders "repo labels | divider | org labels". Maybe the logic should be updated to be consistent.*/}} diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index 048b5f37b7..bfdf94513e 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -15,7 +15,7 @@ <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_milestone"}}"> </div> <div class="divider"></div> - <a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" 0}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a> + <a class="{{if not $.MilestoneID}}active selected {{end}}item" href="{{QueryBuild $queryLink "milestone" NIL}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_all"}}</a> <a class="{{if $.MilestoneID}}{{if eq $.MilestoneID -1}}active selected {{end}}{{end}}item" href="{{QueryBuild $queryLink "milestone" -1}}">{{ctx.Locale.Tr "repo.issues.filter_milestone_none"}}</a> {{if .OpenMilestones}} <div class="divider"></div> @@ -106,7 +106,7 @@ </span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> - <a class="{{if eq .ViewType "all"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "all"}}">{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}</a> + <a class="{{if eq .ViewType "all"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "all"}}">{{if .PageIsPullList}}{{ctx.Locale.Tr "repo.issues.filter_type.all_pull_requests"}}{{else}}{{ctx.Locale.Tr "repo.issues.filter_type.all_issues"}}{{end}}</a> <a class="{{if eq .ViewType "assigned"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "assigned"}}">{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}</a> <a class="{{if eq .ViewType "created_by"}}active {{end}}item" href="{{QueryBuild $queryLink "type" "created_by"}}">{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}</a> {{if .PageIsPullList}} diff --git a/templates/repo/issue/labels/label_edit_modal.tmpl b/templates/repo/issue/labels/label_edit_modal.tmpl index 364fc508f1..6837d66dce 100644 --- a/templates/repo/issue/labels/label_edit_modal.tmpl +++ b/templates/repo/issue/labels/label_edit_modal.tmpl @@ -5,7 +5,7 @@ > <div class="header"></div> <div class="content"> - <form class="ui form ignore-dirty" method="post"> + <form class="ui form ignore-dirty form-fetch-action" method="post"> {{.CsrfTokenHtml}} <input name="id" type="hidden"> <div class="required field"> @@ -50,7 +50,8 @@ <div class="field"> <label for="color">{{ctx.Locale.Tr "repo.issues.label_color"}}</label> <div class="column js-color-picker-input"> - <input name="color" value="#70c24a" placeholder="#c320f6" required maxlength="7"> + <!-- the "#" is optional because backend NormalizeColor is able to handle it, API also accepts both formats, and it is easier for users to directly copy-paste a hex value --> + <input name="color" value="#70c24a" placeholder="#c320f6" required pattern="^#?([\dA-Fa-f]{3}|[\dA-Fa-f]{6})$" maxlength="7"> {{template "repo/issue/label_precolors"}} </div> </div> diff --git a/templates/repo/issue/labels/label_list.tmpl b/templates/repo/issue/labels/label_list.tmpl index cc231971e0..c8a6b46cc4 100644 --- a/templates/repo/issue/labels/label_list.tmpl +++ b/templates/repo/issue/labels/label_list.tmpl @@ -26,7 +26,7 @@ <div class="divider"></div> {{end}} - <ul class="issue-label-list"> + <ul class="issue-label-list muted-links"> {{$canEditLabel := and (not $.PageIsOrgSettingsLabels) (not $.Repository.IsArchived) (or $.CanWriteIssues $.CanWritePulls)}} {{$canEditLabel = or $canEditLabel $.PageIsOrgSettingsLabels}} {{range .Labels}} @@ -42,9 +42,8 @@ <a class="open-issues" href="{{$.RepoLink}}/issues?labels={{.ID}}">{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues.label_open_issues" .NumOpenIssues}}</a> {{end}} </div> - <div class="label-operation tw-flex"> + <div class="label-operation"> {{template "repo/issue/labels/label_archived" .}} - <div class="tw-flex tw-ml-auto"> {{if $canEditLabel}} <a class="edit-label-button" href="#" data-label-id="{{.ID}}" data-label-name="{{.Name}}" data-label-color="{{.Color}}" @@ -57,7 +56,6 @@ data-modal-confirm-content="{{ctx.Locale.Tr "repo.issues.label_deletion_desc"}}" >{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.label_delete"}}</a> {{end}} - </div> </div> </li> {{end}} diff --git a/templates/repo/issue/search.tmpl b/templates/repo/issue/search.tmpl index 07732ab5e7..de6c194ee8 100644 --- a/templates/repo/issue/search.tmpl +++ b/templates/repo/issue/search.tmpl @@ -8,6 +8,7 @@ <input type="hidden" name="project" value="{{$.ProjectID}}"> <input type="hidden" name="assignee" value="{{$.AssigneeID}}"> <input type="hidden" name="poster" value="{{$.PosterUsername}}"> + <input type="hidden" name="sort" value="{{$.SortType}}"> {{end}} {{template "shared/search/input" dict "Value" .Keyword}} {{if .PageIsIssueList}} diff --git a/templates/repo/issue/sidebar/assignee_list.tmpl b/templates/repo/issue/sidebar/assignee_list.tmpl index 95105dd184..1d2279a45d 100644 --- a/templates/repo/issue/sidebar/assignee_list.tmpl +++ b/templates/repo/issue/sidebar/assignee_list.tmpl @@ -27,7 +27,7 @@ </div> </div> </div> - <div class="ui list muted-links flex-items-block tw-flex tw-flex-col tw-gap-2"> + <div class="ui relaxed list muted-links flex-items-block"> <span class="item empty-list {{if $issueAssignees}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_assignees"}}</span> {{range $issueAssignees}} <a class="item" href="{{$pageMeta.RepoLink}}/{{if $pageMeta.IsPullRequest}}pulls{{else}}issues{{end}}?assignee={{.ID}}"> diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl index ed514f6725..15c8760d1a 100644 --- a/templates/repo/issue/sidebar/label_list.tmpl +++ b/templates/repo/issue/sidebar/label_list.tmpl @@ -43,7 +43,7 @@ </div> </div> - <div class="ui list labels-list tw-my-2 tw-flex tw-gap-2"> + <div class="ui list labels-list"> <span class="item empty-list {{if $data.SelectedLabelIDs}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span> {{range $data.AllLabels}} {{if .IsChecked}} diff --git a/templates/repo/issue/sidebar/reviewer_list.tmpl b/templates/repo/issue/sidebar/reviewer_list.tmpl index 9cb11ec7ee..2b5ca80fe7 100644 --- a/templates/repo/issue/sidebar/reviewer_list.tmpl +++ b/templates/repo/issue/sidebar/reviewer_list.tmpl @@ -43,7 +43,7 @@ </div> </div> - <div class="ui relaxed list flex-items-block tw-my-4"> + <div class="ui relaxed list flex-items-block"> <span class="item empty-list {{if or $data.OriginalReviews $data.CurrentPullReviewers}}tw-hidden{{end}}"> {{ctx.Locale.Tr "repo.issues.new.no_reviewers"}} </span> diff --git a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl index 0176aede7a..ab8600b068 100644 --- a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl +++ b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl @@ -16,14 +16,14 @@ </a> <div class="divider"></div> {{if $.IsStopwatchRunning}} - <a class="item issue-stop-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/toggle"> + <a class="item issue-stop-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/stop"> {{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_stop"}} </a> <a class="item issue-cancel-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/cancel"> {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_discard"}} </a> {{else}} - <a class="item issue-start-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/toggle"> + <a class="item issue-start-time link-action" data-url="{{.Issue.Link}}/times/stopwatch/start"> {{svg "octicon-stopwatch"}} {{ctx.Locale.Tr "repo.issues.timetracker_timer_start"}} </a> <a class="item issue-add-time show-modal" data-modal="#issue-time-manually-add-modal"> diff --git a/templates/repo/issue/sidebar/wip_switch.tmpl b/templates/repo/issue/sidebar/wip_switch.tmpl index b007399deb..8c40908f62 100644 --- a/templates/repo/issue/sidebar/wip_switch.tmpl +++ b/templates/repo/issue/sidebar/wip_switch.tmpl @@ -1,5 +1,5 @@ {{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}} - <a class="toggle-wip tw-block tw-mt-2" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title"> + <a data-global-init="initPullRequestWipToggle" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title"> {{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}} </a> {{end}} diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl index 2155f78656..e865050559 100644 --- a/templates/repo/issue/view_content/attachments.tmpl +++ b/templates/repo/issue/view_content/attachments.tmpl @@ -31,7 +31,7 @@ {{if FilenameIsImage .Name}} {{if not (StringUtils.Contains (StringUtils.ToString $.RenderedContent) .UUID)}} <a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}"> - <img alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}"> + <img loading="lazy" alt="{{.Name}}" src="{{.DownloadURL}}" title="{{ctx.Locale.Tr "repo.issues.attachment.open_tab" .Name}}"> </a> {{end}} {{end}} diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 8f49bcf07e..50b8f58fc3 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -163,12 +163,13 @@ </span> <div class="detail flex-text-block"> {{svg "octicon-git-commit"}} + {{/* the content is a link like <a href="{RepoLink}/commit/{CommitID}">message title</a> (from CreateRefComment) */}} <span class="text grey muted-links">{{.Content | SanitizeHTML}}</span> </div> </div> {{else if eq .Type 7}} {{if or .AddedLabels .RemovedLabels}} - <div class="timeline-item event" id="{{.HashTag}}"> + <div class="timeline-item event with-labels-list-inline" id="{{.HashTag}}"> <span class="badge">{{svg "octicon-tag"}}</span> {{template "shared/user/avatarlink" dict "user" .Poster}} <span class="text grey muted-links"> @@ -615,7 +616,7 @@ <div class="timeline-item-group"> <div class="timeline-item event" id="{{.HashTag}}"> <a class="timeline-avatar"{{if gt .Poster.ID 0}} href="{{.Poster.HomeLink}}"{{end}}> - <img alt src="{{.Poster.AvatarLink ctx}}" width="40" height="40"> + <img loading="lazy" alt src="{{.Poster.AvatarLink ctx}}" width="40" height="40"> </a> <span class="badge grey">{{svg "octicon-x" 16}}</span> <span class="text grey muted-links"> diff --git a/templates/repo/issue/view_content/pull_merge_box.tmpl b/templates/repo/issue/view_content/pull_merge_box.tmpl index 641520247d..46bcd3b8b3 100644 --- a/templates/repo/issue/view_content/pull_merge_box.tmpl +++ b/templates/repo/issue/view_content/pull_merge_box.tmpl @@ -95,7 +95,7 @@ {{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}} </div> {{if or .HasIssuesOrPullsWritePermission .IsIssuePoster}} - <button class="ui compact button toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title"> + <button class="ui compact button" data-global-init="initPullRequestWipToggle" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title"> {{ctx.Locale.Tr "repo.pulls.remove_prefix" .WorkInProgressPrefix}} </button> {{end}} diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl index c5c697edff..79ac5cc8a1 100644 --- a/templates/repo/migrate/migrate.tmpl +++ b/templates/repo/migrate/migrate.tmpl @@ -19,7 +19,7 @@ <div class="header tw-text-center"> {{.Title}} </div> - <div class="description tw-text-center"> + <div class="description tw-text-center tw-text-balance"> {{ctx.Locale.Tr (printf "repo.migrate.%s.description" .Name)}} </div> </div> diff --git a/templates/repo/pulls/fork.tmpl b/templates/repo/pulls/fork.tmpl index adf9e250c1..0d775ed6a0 100644 --- a/templates/repo/pulls/fork.tmpl +++ b/templates/repo/pulls/fork.tmpl @@ -6,7 +6,7 @@ </h3> <div class="ui attached segment"> {{template "base/alert" .}} - <form class="ui form left-right-form" action="{{.Link}}" method="post"> + <form class="ui form form-fetch-action left-right-form" action="{{.Link}}" method="post"> {{.CsrfTokenHtml}} <div class="inline required field {{if .Err_Owner}}error{{end}}"> <label>{{ctx.Locale.Tr "repo.owner"}}</label> diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index 01ec0ff188..882ffe40b7 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -50,7 +50,11 @@ {{svg (MigrationIcon $release.Repo.GetOriginalURLHostname) 20 "tw-mr-1"}}{{$release.OriginalAuthor}} {{else if $release.Publisher}} {{ctx.AvatarUtils.Avatar $release.Publisher 20 "tw-mr-1"}} - <a href="{{$release.Publisher.HomeLink}}">{{$release.Publisher.GetDisplayName}}</a> + {{if gt $release.PublisherID 0}} + <a href="{{$release.Publisher.HomeLink}}">{{$release.Publisher.GetDisplayName}}</a> + {{else}} + {{$release.Publisher.GetDisplayName}} + {{end}} {{else}} Ghost {{end}} @@ -61,7 +65,7 @@ {{if $release.CreatedUnix}} <span class="time">{{DateUtils.TimeSince $release.CreatedUnix}}</span> {{end}} - {{if and (not $release.IsDraft) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}} + {{if and (gt $release.NumCommits 0) (not $release.IsDraft) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}} | <span class="ahead"><a href="{{$.RepoLink}}/compare/{{$release.TagName | PathEscapeSegments}}...{{$release.TargetBehind | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.release.ahead.commits" $release.NumCommitsBehind}}</a> {{ctx.Locale.Tr "repo.release.ahead.target" $release.TargetBehind}}</span> {{end}} </p> @@ -88,7 +92,7 @@ {{end}} {{range $att := $release.Attachments}} <li class="item"> - <a target="_blank" class="tw-flex-grow-[2] gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}"> + <a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}"> <strong class="flex-text-inline">{{svg "octicon-package" 16 "download-icon"}}<span class="gt-ellipsis">{{$att.Name}}</span></strong> </a> <div class="attachment-right-info flex-text-inline"> diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl index 7064b4c7ba..e941a2c4b6 100644 --- a/templates/repo/settings/collaboration.tmpl +++ b/templates/repo/settings/collaboration.tmpl @@ -63,13 +63,33 @@ {{.Name}} </a> <div class="flex-item-body flex-text-block"> + {{/*FIXME: TEAM-UNIT-PERMISSION this display is not right, search the fixme keyword to see more details */}} {{svg "octicon-shield-lock"}} - {{if eq .AccessMode 1}}{{ctx.Locale.Tr "repo.settings.collaboration.read"}}{{else if eq .AccessMode 2}}{{ctx.Locale.Tr "repo.settings.collaboration.write"}}{{else if eq .AccessMode 3}}{{ctx.Locale.Tr "repo.settings.collaboration.admin"}}{{else if eq .AccessMode 4}}{{ctx.Locale.Tr "repo.settings.collaboration.owner"}}{{else}}{{ctx.Locale.Tr "repo.settings.collaboration.undefined"}}{{end}} + {{if eq .AccessMode 0}} + {{ctx.Locale.Tr "repo.settings.collaboration.per_unit"}} + {{else if eq .AccessMode 1}} + {{ctx.Locale.Tr "repo.settings.collaboration.read"}} + {{else if eq .AccessMode 2}} + {{ctx.Locale.Tr "repo.settings.collaboration.write"}} + {{else if eq .AccessMode 3}} + {{ctx.Locale.Tr "repo.settings.collaboration.admin"}} + {{else if eq .AccessMode 4}} + {{ctx.Locale.Tr "repo.settings.collaboration.owner"}} + {{else}} + {{ctx.Locale.Tr "repo.settings.collaboration.undefined"}} + {{end}} </div> - {{if or (eq .AccessMode 1) (eq .AccessMode 2)}} + {{if or (eq .AccessMode 0) (eq .AccessMode 1) (eq .AccessMode 2)}} {{$first := true}} <div class="flex-item-body" data-tooltip-content="{{ctx.Locale.Tr "repo.settings.change_team_permission_tip"}}"> - Sections: {{range $u, $unit := $.Units}}{{if and ($.Repo.UnitEnabled ctx $unit.Type) ($team.UnitEnabled ctx $unit.Type)}}{{if $first}}{{$first = false}}{{else}}, {{end}}{{ctx.Locale.Tr $unit.NameKey}}{{end}}{{end}} {{if $first}}None{{end}} + Units: + {{range $u, $unit := $.Units}} + {{- if and ($.Repo.UnitEnabled ctx $unit.Type) ($team.UnitEnabled ctx $unit.Type) -}} + {{- Iif $first "" ", "}}{{ctx.Locale.Tr $unit.NameKey -}} + {{- $first = false -}} + {{- end -}} + {{end}} + {{if $first}}None{{end}} </div> {{end}} </div> diff --git a/templates/repo/settings/lfs_file.tmpl b/templates/repo/settings/lfs_file.tmpl index 7698f77b2a..cd1b168401 100644 --- a/templates/repo/settings/lfs_file.tmpl +++ b/templates/repo/settings/lfs_file.tmpl @@ -21,7 +21,7 @@ {{else if not .IsTextFile}} <div class="view-raw"> {{if .IsImageFile}} - <img alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}"> + <img loading="lazy" alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}"> {{else if .IsVideoFile}} <video controls src="{{$.RawFileLink}}"> <strong>{{ctx.Locale.Tr "repo.video_not_supported_in_browser"}}</strong> @@ -30,8 +30,6 @@ <audio controls src="{{$.RawFileLink}}"> <strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong> </audio> - {{else if .IsPDFFile}} - <div class="pdf-content is-loading" data-global-init="initPdfViewer" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "diff.view_file"}}"></div> {{else}} <a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> {{end}} diff --git a/templates/repo/settings/webhook/settings.tmpl b/templates/repo/settings/webhook/settings.tmpl index 16ad263e42..a330448c9e 100644 --- a/templates/repo/settings/webhook/settings.tmpl +++ b/templates/repo/settings/webhook/settings.tmpl @@ -263,6 +263,16 @@ <div class="fourteen wide column"> <label>{{ctx.Locale.Tr "repo.settings.event_header_workflow"}}</label> </div> + <!-- Workflow Run Event --> + <div class="seven wide column"> + <div class="field"> + <div class="ui checkbox"> + <input name="workflow_run" type="checkbox" {{if .Webhook.HookEvents.Get "workflow_run"}}checked{{end}}> + <label>{{ctx.Locale.Tr "repo.settings.event_workflow_run"}}</label> + <span class="help">{{ctx.Locale.Tr "repo.settings.event_workflow_run_desc"}}</span> + </div> + </div> + </div> <!-- Workflow Job Event --> <div class="seven wide column"> <div class="field"> @@ -288,7 +298,7 @@ <label for="authorization_header">{{ctx.Locale.Tr "repo.settings.authorization_header"}}</label> <input id="authorization_header" name="authorization_header" type="text" value="{{.Webhook.HeaderAuthorization}}"{{if eq .HookType "matrix"}} placeholder="Bearer $access_token" required{{end}}> {{if ne .HookType "matrix"}}{{/* Matrix doesn't make the authorization optional but it is implied by the help string, should be changed.*/}} - <span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" ("<code>Bearer token123456</code>, <code>Basic YWxhZGRpbjpvcGVuc2VzYW1l</code>" | SafeHTML)}}</span> + <span class="help">{{ctx.Locale.Tr "repo.settings.authorization_header_desc" (HTMLFormat "<code>%s</code>, <code>%s</code>" "Bearer token123456" "Basic YWxhZGRpbjpvcGVuc2VzYW1l")}}</span> {{end}} </div> diff --git a/templates/repo/tag/list.tmpl b/templates/repo/tag/list.tmpl index 9789943b49..8b33b96f86 100644 --- a/templates/repo/tag/list.tmpl +++ b/templates/repo/tag/list.tmpl @@ -5,9 +5,7 @@ {{template "base/alert" .}} {{template "repo/release_tag_header" .}} <h4 class="ui top attached header"> - <div class="five wide column tw-flex tw-items-center"> - {{.TagCount}} {{ctx.Locale.Tr "repo.release.tags"}} - </div> + {{.TagCount}} {{ctx.Locale.Tr "repo.release.tags"}} </h4> {{$canReadReleases := $.Permission.CanRead ctx.Consts.RepoUnitTypeReleases}} <div class="ui attached segment"> @@ -15,53 +13,49 @@ {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.tag_kind") "Tooltip" (ctx.Locale.Tr "search.tag_tooltip")}} </form> </div> - <div class="ui attached table segment"> + <div class="ui attached segment tw-p-0"> {{if .Releases}} - <table class="ui very basic striped fixed table single line" id="tags-table"> - <tbody class="tag-list"> - {{range $idx, $release := .Releases}} - <tr> - <td class="tag-list-row"> - <h3 class="tag-list-row-title tw-mb-2"> - {{if $canReadReleases}} - <a class="tag-list-row-link tw-flex tw-items-center" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> - {{else}} - <a class="tag-list-row-link tw-flex tw-items-center" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> - {{end}} - </h3> - <div class="download tw-flex tw-items-center"> - {{if $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}} - {{if .CreatedUnix}} - <span class="tw-mr-2">{{svg "octicon-clock" 16 "tw-mr-1"}}{{DateUtils.TimeSince .CreatedUnix}}</span> - {{end}} + <div class="ui divided list" id="tags-table"> + {{range $idx, $release := .Releases}} + <div class="item tag-list-row tw-p-4"> + <h3 class="tag-list-row-title tw-mb-2"> + {{if $canReadReleases}} + <a class="tag-list-row-link" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> + {{else}} + <a class="tag-list-row-link" href="{{$.RepoLink}}/src/tag/{{.TagName | PathEscapeSegments}}" rel="nofollow">{{.TagName}}</a> + {{end}} + </h3> + <div class="flex-text-block muted-links tw-gap-4 tw-flex-wrap"> + {{if $.Permission.CanRead ctx.Consts.RepoUnitTypeCode}} + {{if .CreatedUnix}} + <span class="flex-text-inline">{{svg "octicon-clock"}}{{DateUtils.TimeSince .CreatedUnix}}</span> + {{end}} - <a class="tw-mr-2 tw-font-mono muted" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .Sha1}}</a> + <a class="flex-text-inline tw-font-mono" href="{{$.RepoLink}}/src/commit/{{.Sha1}}" rel="nofollow">{{svg "octicon-git-commit"}}{{ShortSha .Sha1}}</a> - {{if not $.DisableDownloadSourceArchives}} - <a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}ZIP</a> - <a class="archive-link tw-mr-2 muted" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip" 16 "tw-mr-1"}}TAR.GZ</a> - {{end}} + {{if not $.DisableDownloadSourceArchives}} + <a class="archive-link flex-text-inline" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.zip" rel="nofollow">{{svg "octicon-file-zip"}}ZIP</a> + <a class="archive-link flex-text-inline" href="{{$.RepoLink}}/archive/{{.TagName | PathEscapeSegments}}.tar.gz" rel="nofollow">{{svg "octicon-file-zip"}}TAR.GZ</a> + {{end}} - {{if (and $canReadReleases $.CanCreateRelease $release.IsTag)}} - <a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a> - {{end}} + {{if (and $canReadReleases $.CanCreateRelease $release.IsTag)}} + <a class="flex-text-inline" href="{{$.RepoLink}}/releases/new?tag={{.TagName}}">{{svg "octicon-tag"}}{{ctx.Locale.Tr "repo.release.new_release"}}</a> + {{end}} - {{if (and ($.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) $release.IsTag)}} - <a class="ui delete-button tw-mr-2 muted" data-url="{{$.RepoLink}}/tags/delete" data-id="{{.ID}}"> - {{svg "octicon-trash" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.delete_tag"}} - </a> - {{end}} + {{if (and ($.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) $release.IsTag)}} + <a class="flex-text-inline link-action" data-url="{{$.RepoLink}}/tags/delete?id={{.ID}}" data-modal-confirm="#confirm-delete-tag-modal"> + {{svg "octicon-trash"}}{{ctx.Locale.Tr "repo.release.delete_tag"}} + </a> + {{end}} - {{if and $canReadReleases (not $release.IsTag)}} - <a class="tw-mr-2 muted" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.release.detail"}}</a> - {{end}} - {{end}} - </div> - </td> - </tr> - {{end}} - </tbody> - </table> + {{if and $canReadReleases (not $release.IsTag)}} + <a class="flex-text-inline" href="{{$.RepoLink}}/releases/tag/{{.TagName | PathEscapeSegments}}">{{svg "octicon-tag"}}{{ctx.Locale.Tr "repo.release.detail"}}</a> + {{end}} + {{end}} + </div> + </div> + {{end}} + </div> {{else}} {{if .NumTags}} <p class="tw-p-4">{{ctx.Locale.Tr "no_results_found"}}</p> @@ -73,9 +67,8 @@ </div> {{if $.Permission.CanWrite ctx.Consts.RepoUnitTypeCode}} -<div class="ui g-modal-confirm delete modal"> +<div id="confirm-delete-tag-modal" class="ui small modal"> <div class="header"> - {{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.release.delete_tag"}} </div> <div class="content"> diff --git a/templates/repo/unicode_escape_prompt.tmpl b/templates/repo/unicode_escape_prompt.tmpl index 8bceafa8bb..f8226ec728 100644 --- a/templates/repo/unicode_escape_prompt.tmpl +++ b/templates/repo/unicode_escape_prompt.tmpl @@ -1,22 +1,22 @@ {{if .EscapeStatus}} {{if .EscapeStatus.HasInvisible}} - <div class="ui warning message unicode-escape-prompt tw-text-left"> + <div class="ui warning message unicode-escape-prompt"> <button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button> <div class="header"> {{ctx.Locale.Tr "repo.invisible_runes_header"}} </div> - <p>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</p> + <div>{{ctx.Locale.Tr "repo.invisible_runes_description"}}</div> {{if .EscapeStatus.HasAmbiguous}} - <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p> + <div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div> {{end}} </div> {{else if .EscapeStatus.HasAmbiguous}} - <div class="ui warning message unicode-escape-prompt tw-text-left"> + <div class="ui warning message unicode-escape-prompt"> <button class="btn close icon hide-panel" data-panel-closest=".message">{{svg "octicon-x" 16 "close inside"}}</button> <div class="header"> {{ctx.Locale.Tr "repo.ambiguous_runes_header"}} </div> - <p>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</p> + <div>{{ctx.Locale.Tr "repo.ambiguous_runes_description"}}</div> </div> {{end}} {{end}} diff --git a/templates/repo/view_content.tmpl b/templates/repo/view_content.tmpl index 292a2f878c..3ba04a9974 100644 --- a/templates/repo/view_content.tmpl +++ b/templates/repo/view_content.tmpl @@ -41,8 +41,8 @@ <a href="{{.Repository.Link}}/find/{{.RefTypeNameSubURL}}" class="ui compact basic button">{{ctx.Locale.Tr "repo.find_file.go_to_file"}}</a> {{end}} - {{if and .CanWriteCode .RefFullName.IsBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} - <button class="ui dropdown basic compact jump button"{{if not .Repository.CanEnableEditor}} disabled{{end}}> + {{if and .RefFullName.IsBranch (not .IsViewFile)}} + <button class="ui dropdown basic compact jump button repo-add-file" {{if not .Repository.CanEnableEditor}}disabled{{end}}> {{ctx.Locale.Tr "repo.editor.add_file"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}} <div class="menu"> @@ -69,7 +69,7 @@ {{if not $isTreePathRoot}} {{$treeNameIdxLast := Eval (len .TreeNames) "-" 1}} - <span class="breadcrumb repo-path tw-ml-1"> + <span class="breadcrumb"> <a class="section" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}" title="{{.Repository.Name}}">{{StringUtils.EllipsisString .Repository.Name 30}}</a> {{- range $i, $v := .TreeNames -}} <span class="breadcrumb-divider">/</span> diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl index dc789a2648..1486d7181d 100644 --- a/templates/repo/view_file.tmpl +++ b/templates/repo/view_file.tmpl @@ -1,4 +1,6 @@ -<div {{if .ReadmeInList}}id="readme" {{end}}class="{{TabSizeClass .Editorconfig .FileTreePath}} non-diff-file-content"> +<div {{if .ReadmeInList}}id="readme"{{end}} class="{{TabSizeClass .Editorconfig .FileTreePath}} non-diff-file-content" + data-global-init="initRepoFileView" data-raw-file-link="{{.RawFileLink}}"> + {{- if .FileError}} <div class="ui error message"> <div class="text left tw-whitespace-pre">{{.FileError}}</div> @@ -27,18 +29,19 @@ <div class="file-header-left tw-flex tw-items-center tw-py-2 tw-pr-4"> {{if .ReadmeInList}} {{svg "octicon-book" 16 "tw-mr-2"}} - <strong><a class="muted" href="#readme">{{.FileTreePath}}</a></strong> + <strong><a class="muted" href="#readme">{{.ReadmeInList}}</a></strong> {{else}} {{template "repo/file_info" .}} {{end}} </div> - <div class="file-header-right file-actions tw-flex tw-items-center tw-flex-wrap"> - {{if .HasSourceRenderedToggle}} - <div class="ui compact icon buttons"> - <a href="?display=source" class="ui mini basic button {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a> - <a href="{{$.Link}}" class="ui mini basic button {{if .IsDisplayingRendered}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_rendered"}}">{{svg "octicon-file" 15}}</a> - </div> - {{end}} + <div class="file-header-right file-actions flex-text-block tw-flex-wrap"> + {{/* this componment is also controlled by frontend plugin renders */}} + <div class="ui compact icon buttons file-view-toggle-buttons {{Iif .HasSourceRenderedToggle "" "tw-hidden"}}"> + {{if .IsRepresentableAsText}} + <a href="?display=source" class="ui mini basic button file-view-toggle-source {{if .IsDisplayingSource}}active{{end}}" data-tooltip-content="{{ctx.Locale.Tr "repo.file_view_source"}}">{{svg "octicon-code" 15}}</a> + {{end}} + <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> + </div> {{if not .ReadmeInList}} <div class="ui buttons tw-mr-1"> <a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a> @@ -55,7 +58,10 @@ {{end}} </div> <a download class="btn-octicon" data-tooltip-content="{{ctx.Locale.Tr "repo.download_file"}}" href="{{$.RawFileLink}}">{{svg "octicon-download"}}</a> - <a class="btn-octicon {{if not .CanCopyContent}} disabled{{end}}" data-global-click="onCopyContentButtonClick" {{if or .IsImageFile (and .HasSourceRenderedToggle (not .IsDisplayingSource))}} data-link="{{$.RawFileLink}}"{{end}} data-tooltip-content="{{if .CanCopyContent}}{{ctx.Locale.Tr "copy_content"}}{{else}}{{ctx.Locale.Tr "copy_type_unsupported"}}{{end}}">{{svg "octicon-copy"}}</a> + <a class="btn-octicon {{if not .CanCopyContent}}disabled{{end}}" data-global-click="onCopyContentButtonClick" + {{if not .IsDisplayingSource}}data-raw-file-link="{{$.RawFileLink}}"{{end}} + data-tooltip-content="{{if .CanCopyContent}}{{ctx.Locale.Tr "copy_content"}}{{else}}{{ctx.Locale.Tr "copy_type_unsupported"}}{{end}}" + >{{svg "octicon-copy"}}</a> {{if .EnableFeed}} <a class="btn-octicon" href="{{$.RepoLink}}/rss/{{$.RefTypeNameSubURL}}/{{PathEscapeSegments .TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}"> {{svg "octicon-rss"}} @@ -82,41 +88,24 @@ {{end}} </div> </h4> + <div class="ui bottom attached table unstackable segment"> - {{if not (or .IsMarkup .IsRenderedHTML)}} - {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} + {{if not .IsMarkup}} + {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} {{end}} - <div class="file-view{{if .IsMarkup}} markup {{.MarkupType}}{{else if .IsPlainText}} plain-text{{else if .IsTextSource}} code-view{{end}}"> + <div class="file-view {{if .IsMarkup}}markup {{.MarkupType}}{{else if .IsPlainText}}plain-text{{else if .IsDisplayingSource}}code-view{{end}}"> {{if .IsFileTooLarge}} {{template "shared/filetoolarge" dict "RawFileLink" .RawFileLink}} {{else if not .FileSize}} {{template "shared/fileisempty"}} {{else if .IsMarkup}} - {{if .FileContent}}{{.FileContent}}{{end}} + {{.FileContent}} {{else if .IsPlainText}} <pre>{{if .FileContent}}{{.FileContent}}{{end}}</pre> - {{else if not .IsTextSource}} - <div class="view-raw"> - {{if .IsImageFile}} - <img alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}"> - {{else if .IsVideoFile}} - <video controls src="{{$.RawFileLink}}"> - <strong>{{ctx.Locale.Tr "repo.video_not_supported_in_browser"}}</strong> - </video> - {{else if .IsAudioFile}} - <audio controls src="{{$.RawFileLink}}"> - <strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong> - </audio> - {{else if .IsPDFFile}} - <div class="pdf-content is-loading" data-global-init="initPdfViewer" data-src="{{$.RawFileLink}}" data-fallback-button-text="{{ctx.Locale.Tr "repo.diff.view_file"}}"></div> - {{else}} - <a href="{{$.RawFileLink}}" rel="nofollow" class="tw-p-4">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> - {{end}} - </div> - {{else if .FileSize}} + {{else if .FileContent}} <table> <tbody> - {{range $idx, $code := .FileContent}} + {{range $idx, $code := .FileContent}} {{$line := Eval $idx "+" 1}} <tr> <td id="L{{$line}}" class="lines-num"><span id="L{{$line}}" data-line-number="{{$line}}"></span></td> @@ -125,17 +114,38 @@ {{end}} <td rel="L{{$line}}" class="lines-code chroma"><code class="code-inner">{{$code}}</code></td> </tr> - {{end}} + {{end}} </tbody> </table> - <div class="code-line-menu tippy-target"> - {{if $.Permission.CanRead ctx.Consts.RepoUnitTypeIssues}} - <a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a> + {{else}} + <div class="view-raw"> + {{if .IsImageFile}} + <img alt="{{$.RawFileLink}}" src="{{$.RawFileLink}}"> + {{else if .IsVideoFile}} + <video controls src="{{$.RawFileLink}}"> + <strong>{{ctx.Locale.Tr "repo.video_not_supported_in_browser"}}</strong> + </video> + {{else if .IsAudioFile}} + <audio controls src="{{$.RawFileLink}}"> + <strong>{{ctx.Locale.Tr "repo.audio_not_supported_in_browser"}}</strong> + </audio> + {{else}} + <div class="file-view-render-container"> + <div class="file-view-raw-prompt tw-p-4"> + <a href="{{$.RawFileLink}}" rel="nofollow">{{ctx.Locale.Tr "repo.file_view_raw"}}</a> + </div> + </div> {{end}} - <a class="item view_git_blame" role="menuitem" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a> - <a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a> </div> {{end}} </div> + + <div class="code-line-menu tippy-target"> + {{if $.Permission.CanRead ctx.Consts.RepoUnitTypeIssues}} + <a class="item ref-in-new-issue" role="menuitem" data-url-issue-new="{{.RepoLink}}/issues/new" data-url-param-body-link="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}" rel="nofollow noindex">{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}</a> + {{end}} + <a class="item view_git_blame" role="menuitem" href="{{.Repository.Link}}/blame/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.view_git_blame"}}</a> + <a class="item copy-line-permalink" role="menuitem" data-url="{{.Repository.Link}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}{{if $.HasSourceRenderedToggle}}?display=source{{end}}">{{ctx.Locale.Tr "repo.file_copy_permalink"}}</a> + </div> </div> </div> diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index c8ee059e89..b655f735a3 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -41,6 +41,9 @@ </a> {{else}} <a class="entry-name" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}" title="{{$entry.Name}}">{{$entry.Name}}</a> + {{if $entry.IsLink}} + <a class="entry-symbol-link flex-text-inline" data-tooltip-content title="{{ctx.Locale.Tr "repo.find_file.follow_symlink"}}" href="{{$.TreeLink}}/{{PathEscapeSegments $entry.Name}}?follow_symlink=1">{{svg "octicon-link" 12}}</a> + {{end}} {{end}} {{end}} </div> diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl index 5ebccc69e9..12f0983904 100644 --- a/templates/repo/wiki/new.tmpl +++ b/templates/repo/wiki/new.tmpl @@ -18,7 +18,7 @@ {{ctx.Locale.Tr "repo.wiki.page_name_desc"}} </div> - {{$content := .content}} + {{$content := .WikiEditContent}} {{if not .PageIsWikiEdit}} {{$content = ctx.Locale.Tr "repo.wiki.welcome"}} {{end}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 89befcd7c5..f6f82fb52d 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -62,36 +62,34 @@ {{end}} <div class="wiki-content-parts"> - {{if .sidebarTocContent}} + {{if .WikiSidebarTocHTML}} <div class="render-content markup wiki-content-sidebar wiki-content-toc"> - {{.sidebarTocContent | SafeHTML}} + {{.WikiSidebarTocHTML}} </div> {{end}} - <div class="render-content markup wiki-content-main {{if or .sidebarTocContent .sidebarPresent}}with-sidebar{{end}}"> - {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus "root" $}} - {{.content | SafeHTML}} + <div class="render-content markup wiki-content-main {{if or .WikiSidebarTocHTML .WikiSidebarHTML}}with-sidebar{{end}}"> + {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .EscapeStatus}} + {{.WikiContentHTML}} </div> - {{if .sidebarPresent}} + {{if .WikiSidebarHTML}} <div class="render-content markup wiki-content-sidebar"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Sidebar?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} - {{template "repo/unicode_escape_prompt" dict "EscapeStatus" .sidebarEscapeStatus "root" $}} - {{.sidebarContent | SafeHTML}} + {{.WikiSidebarHTML}} </div> {{end}} <div class="tw-clear-both"></div> - {{if .footerPresent}} + {{if .WikiFooterHTML}} <div class="render-content markup wiki-content-footer"> {{if and .CanWriteWiki (not .Repository.IsMirror)}} <a class="tw-float-right muted" href="{{.RepoLink}}/wiki/_Footer?action=_edit" aria-label="{{ctx.Locale.Tr "repo.wiki.edit_page_button"}}">{{svg "octicon-pencil"}}</a> {{end}} - {{template "repo/unicode_escape_prompt" dict "footerEscapeStatus" .sidebarEscapeStatus "root" $}} - {{.footerContent | SafeHTML}} + {{.WikiFooterHTML}} </div> {{end}} </div> diff --git a/templates/shared/issuelist.tmpl b/templates/shared/issuelist.tmpl index 30670c3b0f..b25bcc14b9 100644 --- a/templates/shared/issuelist.tmpl +++ b/templates/shared/issuelist.tmpl @@ -3,11 +3,14 @@ {{range .Issues}} <div class="flex-item"> - <div class="flex-item-icon"> - {{if $.CanWriteIssuesOrPulls}} - <input type="checkbox" autocomplete="off" class="issue-checkbox tw-mr-4" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} "{{.Title}}""> - {{end}} - {{template "shared/issueicon" .}} + <div class="flex-item-leading"> + {{/* using some tw helpers is the only way to align the checkbox */}} + <div class="flex-text-inline tw-mt-[2px]"> + {{if $.CanWriteIssuesOrPulls}} + <input type="checkbox" autocomplete="off" class="issue-checkbox tw-mr-[14px]" data-issue-id={{.ID}} aria-label="{{ctx.Locale.Tr "repo.issues.action_check"}} "{{.Title}}""> + {{end}} + {{template "shared/issueicon" .}} + </div> </div> <div class="flex-item-main"> @@ -19,7 +22,7 @@ {{template "repo/commit_statuses" dict "Status" (index $.CommitLastStatus .PullRequest.ID) "Statuses" (index $.CommitStatuses .PullRequest.ID)}} {{end}} {{end}} - <span class="labels-list tw-ml-1"> + <span class="labels-list"> {{range .Labels}} <a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.RenderUtils.RenderLabel .}}</a> {{end}} @@ -32,7 +35,7 @@ </div> {{end}} </div> - <div class="flex-item-body"> + <div class="flex-item-body tw-mt-1"> <a class="index" href="{{if .Link}}{{.Link}}{{else}}{{$.Link}}/{{.Index}}{{end}}"> {{if eq $.listType "dashboard"}} {{.Repo.FullName}}#{{.Index}} diff --git a/templates/explore/repo_list.tmpl b/templates/shared/repo/list.tmpl index ad190b89b2..2c8af14f9c 100644 --- a/templates/explore/repo_list.tmpl +++ b/templates/shared/repo/list.tmpl @@ -2,12 +2,16 @@ {{range .Repos}} <div class="flex-item"> <div class="flex-item-leading"> - {{template "repo/icon" .}} + {{if $.ShowRepoOwnerAvatar}} + {{ctx.AvatarUtils.Avatar .Owner 24}} + {{else}} + {{template "repo/icon" .}} + {{end}} </div> <div class="flex-item-main"> <div class="flex-item-header"> <div class="flex-item-title"> - {{if and (or $.PageIsExplore $.PageIsProfileStarList) .Owner}} + {{if and $.ShowRepoOwnerOnList .Owner}} <a class="text primary name" href="{{.Owner.HomeLink}}">{{.Owner.Name}}</a>/ {{end}} <a class="text primary name" href="{{.Link}}">{{.Name}}</a> diff --git a/templates/shared/repo_search.tmpl b/templates/shared/repo/search.tmpl index a909061184..a909061184 100644 --- a/templates/shared/repo_search.tmpl +++ b/templates/shared/repo/search.tmpl diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index 91f04e0b53..1190dc54ec 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -103,7 +103,7 @@ <ul class="user-badges"> {{range .Badges}} <li> - <img width="64" height="64" src="{{.ImageURL}}" alt="{{.Description}}" data-tooltip-content="{{.Description}}"> + <img loading="lazy" width="64" height="64" src="{{.ImageURL}}" alt="{{.Description}}" data-tooltip-content="{{.Description}}"> </li> {{end}} </ul> diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index c97e525660..879f59df2e 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -75,6 +75,49 @@ } } }, + "/admin/actions/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Lists all jobs", + "operationId": "listAdminWorkflowJobs", + "parameters": [ + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowJobsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/admin/actions/runners": { "get": { "produces": [ @@ -177,6 +220,73 @@ } } }, + "/admin/actions/runs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Lists all runs", + "operationId": "listAdminWorkflowRuns", + "parameters": [ + { + "type": "string", + "description": "workflow event name", + "name": "event", + "in": "query" + }, + { + "type": "string", + "description": "workflow branch", + "name": "branch", + "in": "query" + }, + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "triggered by user", + "name": "actor", + "in": "query" + }, + { + "type": "string", + "description": "triggering sha of the workflow run", + "name": "head_sha", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowRunsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/admin/cron": { "get": { "produces": [ @@ -656,7 +766,7 @@ }, { "type": "string", - "description": "user's login name to search for", + "description": "identifier of the user, provided by the external authenticator", "name": "login_name", "in": "query" }, @@ -732,7 +842,7 @@ "parameters": [ { "type": "string", - "description": "username of user to delete", + "description": "username of the user to delete", "name": "username", "in": "path", "required": true @@ -774,7 +884,7 @@ "parameters": [ { "type": "string", - "description": "username of user to edit", + "description": "username of the user whose data is to be edited", "name": "username", "in": "path", "required": true @@ -816,7 +926,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose badges are to be listed", "name": "username", "in": "path", "required": true @@ -846,7 +956,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user to whom a badge is to be added", "name": "username", "in": "path", "required": true @@ -880,7 +990,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose badge is to be deleted", "name": "username", "in": "path", "required": true @@ -922,7 +1032,7 @@ "parameters": [ { "type": "string", - "description": "username of the user", + "description": "username of the user who is to receive a public key", "name": "username", "in": "path", "required": true @@ -961,7 +1071,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose public key is to be deleted", "name": "username", "in": "path", "required": true @@ -1004,7 +1114,7 @@ "parameters": [ { "type": "string", - "description": "username of the user that will own the created organization", + "description": "username of the user who will own the created organization", "name": "username", "in": "path", "required": true @@ -1044,7 +1154,7 @@ "parameters": [ { "type": "string", - "description": "existing username of user", + "description": "current username of the user", "name": "username", "in": "path", "required": true @@ -1087,7 +1197,7 @@ "parameters": [ { "type": "string", - "description": "username of the user. This user will own the created repository", + "description": "username of the user who will own the created repository", "name": "username", "in": "path", "required": true @@ -1799,6 +1909,56 @@ } } }, + "/orgs/{org}/actions/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get org-level workflow jobs", + "operationId": "getOrgWorkflowJobs", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowJobsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/orgs/{org}/actions/runners": { "get": { "produces": [ @@ -1957,6 +2117,80 @@ } } }, + "/orgs/{org}/actions/runs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "organization" + ], + "summary": "Get org-level workflow runs", + "operationId": "getOrgWorkflowRuns", + "parameters": [ + { + "type": "string", + "description": "name of the organization", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "workflow event name", + "name": "event", + "in": "query" + }, + { + "type": "string", + "description": "workflow branch", + "name": "branch", + "in": "query" + }, + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "triggered by user", + "name": "actor", + "in": "query" + }, + { + "type": "string", + "description": "triggering sha of the workflow run", + "name": "head_sha", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowRunsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/orgs/{org}/actions/secrets": { "get": { "produces": [ @@ -2259,16 +2493,16 @@ ], "responses": { "201": { - "description": "response when creating an org-level variable" - }, - "204": { - "description": "response when creating an org-level variable" + "description": "successfully created the org-level variable" }, "400": { "$ref": "#/responses/error" }, - "404": { - "$ref": "#/responses/notFound" + "409": { + "description": "variable name already exists." + }, + "500": { + "$ref": "#/responses/error" } } }, @@ -2482,7 +2716,7 @@ }, { "type": "string", - "description": "user to check", + "description": "username of the user to check", "name": "username", "in": "path", "required": true @@ -2513,7 +2747,7 @@ }, { "type": "string", - "description": "user to block", + "description": "username of the user to block", "name": "username", "in": "path", "required": true @@ -2553,7 +2787,7 @@ }, { "type": "string", - "description": "user to unblock", + "description": "username of the user to unblock", "name": "username", "in": "path", "required": true @@ -3024,7 +3258,7 @@ }, { "type": "string", - "description": "username of the user", + "description": "username of the user to check for an organization membership", "name": "username", "in": "path", "required": true @@ -3061,7 +3295,7 @@ }, { "type": "string", - "description": "username of the user", + "description": "username of the user to remove from the organization", "name": "username", "in": "path", "required": true @@ -3135,7 +3369,7 @@ }, { "type": "string", - "description": "username of the user", + "description": "username of the user to check for a public organization membership", "name": "username", "in": "path", "required": true @@ -3169,7 +3403,7 @@ }, { "type": "string", - "description": "username of the user", + "description": "username of the user whose membership is to be publicized", "name": "username", "in": "path", "required": true @@ -3206,7 +3440,7 @@ }, { "type": "string", - "description": "username of the user", + "description": "username of the user whose membership is to be concealed", "name": "username", "in": "path", "required": true @@ -4519,6 +4753,109 @@ } } }, + "/repos/{owner}/{repo}/actions/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Lists all jobs for a repository", + "operationId": "listWorkflowJobs", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowJobsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/repos/{owner}/{repo}/actions/jobs/{job_id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets a specific workflow job for a workflow run", + "operationId": "getWorkflowJob", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "id of the job", + "name": "job_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowJob" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/jobs/{job_id}/logs": { "get": { "produces": [ @@ -4758,7 +5095,132 @@ } } }, + "/repos/{owner}/{repo}/actions/runs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Lists all runs for a repository run", + "operationId": "getWorkflowRuns", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "workflow event name", + "name": "event", + "in": "query" + }, + { + "type": "string", + "description": "workflow branch", + "name": "branch", + "in": "query" + }, + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "triggered by user", + "name": "actor", + "in": "query" + }, + { + "type": "string", + "description": "triggering sha of the workflow run", + "name": "head_sha", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ArtifactsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/runs/{run}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Gets a specific workflow run", + "operationId": "GetWorkflowRun", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "id of the run", + "name": "run", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowRun" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + }, "delete": { "produces": [ "application/json" @@ -4856,6 +5318,70 @@ } } }, + "/repos/{owner}/{repo}/actions/runs/{run}/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Lists all jobs for a workflow run", + "operationId": "listWorkflowRunJobs", + "parameters": [ + { + "type": "string", + "description": "name of the owner", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "runid of the workflow run", + "name": "run", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowJobsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/actions/secrets": { "get": { "produces": [ @@ -5263,14 +5789,14 @@ "201": { "description": "response when creating a repo-level variable" }, - "204": { - "description": "response when creating a repo-level variable" - }, "400": { "$ref": "#/responses/error" }, - "404": { - "$ref": "#/responses/notFound" + "409": { + "description": "variable name already exists." + }, + "500": { + "$ref": "#/responses/error" } } }, @@ -6404,7 +6930,7 @@ }, { "type": "string", - "description": "username of the collaborator", + "description": "username of the user to check for being a collaborator", "name": "collaborator", "in": "path", "required": true @@ -6448,7 +6974,7 @@ }, { "type": "string", - "description": "username of the collaborator to add", + "description": "username of the user to add or update as a collaborator", "name": "collaborator", "in": "path", "required": true @@ -6548,7 +7074,7 @@ }, { "type": "string", - "description": "username of the collaborator", + "description": "username of the collaborator whose permissions are to be obtained", "name": "collaborator", "in": "path", "required": true @@ -6898,13 +7424,14 @@ }, "/repos/{owner}/{repo}/contents": { "get": { + "description": "This API follows GitHub's design, and it is not easy to use. Recommend users to use our \"contents-ext\" API instead.", "produces": [ "application/json" ], "tags": [ "repository" ], - "summary": "Gets the metadata of all the entries of the root dir", + "summary": "Gets the metadata of all the entries of the root dir.", "operationId": "repoGetContentsList", "parameters": [ { @@ -6992,15 +7519,72 @@ } } }, + "/repos/{owner}/{repo}/contents-ext/{filepath}": { + "get": { + "description": "It guarantees that only one of the response fields is set if the request succeeds. Users can pass \"includes=file_content\" or \"includes=lfs_metadata\" to retrieve more fields. \"includes=file_content\" only works for single file, if you need to retrieve file contents in batch, use \"file-contents\" API after listing the directory.", + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "The extended \"contents\" API, to get file metadata and/or content, or list a directory.", + "operationId": "repoGetContentsExt", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "path of the dir, file, symlink or submodule in the repo. Swagger requires path parameter to be \"required\", you can leave it empty or pass a single dot (\".\") to get the root directory.", + "name": "filepath", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "the name of the commit/branch/tag, default to the repository’s default branch.", + "name": "ref", + "in": "query" + }, + { + "type": "string", + "description": "By default this API's response only contains file's metadata. Use comma-separated \"includes\" options to retrieve more fields. Option \"file_content\" will try to retrieve the file content, \"lfs_metadata\" will try to retrieve LFS metadata, \"commit_metadata\" will try to retrieve commit metadata, and \"commit_message\" will try to retrieve commit message.", + "name": "includes", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/ContentsExtResponse" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/repos/{owner}/{repo}/contents/{filepath}": { "get": { + "description": "This API follows GitHub's design, and it is not easy to use. Recommend users to use the \"contents-ext\" API instead.", "produces": [ "application/json" ], "tags": [ "repository" ], - "summary": "Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir", + "summary": "Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir.", "operationId": "repoGetContents", "parameters": [ { @@ -7218,6 +7802,9 @@ "404": { "$ref": "#/responses/error" }, + "422": { + "$ref": "#/responses/error" + }, "423": { "$ref": "#/responses/repoArchivedError" } @@ -7325,7 +7912,7 @@ }, "/repos/{owner}/{repo}/file-contents": { "get": { - "description": "See the POST method. This GET method supports to use JSON encoded request body in query parameter.", + "description": "See the POST method. This GET method supports using JSON encoded request body in query parameter.", "produces": [ "application/json" ], @@ -11109,7 +11696,7 @@ "$ref": "#/responses/notFound" }, "409": { - "description": "Cannot cancel a non existent stopwatch" + "description": "Cannot cancel a non-existent stopwatch" } } } @@ -11215,7 +11802,7 @@ "$ref": "#/responses/notFound" }, "409": { - "description": "Cannot stop a non existent stopwatch" + "description": "Cannot stop a non-existent stopwatch" } } } @@ -11364,7 +11951,7 @@ }, { "type": "string", - "description": "user to subscribe", + "description": "username of the user to subscribe the issue to", "name": "user", "in": "path", "required": true @@ -11422,7 +12009,7 @@ }, { "type": "string", - "description": "user witch unsubscribe", + "description": "username of the user to unsubscribe from an issue", "name": "user", "in": "path", "required": true @@ -12292,7 +12879,7 @@ }, { "type": "string", - "description": "The name of the commit/branch/tag. Default the repository’s default branch", + "description": "The name of the commit/branch/tag. Default to the repository’s default branch", "name": "ref", "in": "query" } @@ -12871,6 +13458,7 @@ "enum": [ "oldest", "recentupdate", + "recentclose", "leastupdate", "mostcomment", "leastcomment", @@ -14435,7 +15023,7 @@ }, { "type": "string", - "description": "The name of the commit/branch/tag. Default the repository’s default branch", + "description": "The name of the commit/branch/tag. Default to the repository’s default branch", "name": "ref", "in": "query" } @@ -15163,6 +15751,42 @@ } } }, + "/repos/{owner}/{repo}/signing-key.pub": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "repository" + ], + "summary": "Get signing-key.pub for given repository", + "operationId": "repoSigningKeySSH", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ssh public key", + "schema": { + "type": "string" + } + } + } + } + }, "/repos/{owner}/{repo}/stargazers": { "get": { "produces": [ @@ -16197,7 +16821,7 @@ }, { "type": "string", - "description": "username of user", + "description": "username of the user whose tracked times are to be listed", "name": "user", "in": "path", "required": true @@ -16996,6 +17620,26 @@ } } }, + "/signing-key.pub": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "miscellaneous" + ], + "summary": "Get default signing-key.pub", + "operationId": "getSigningKeySSH", + "responses": { + "200": { + "description": "ssh public key", + "schema": { + "type": "string" + } + } + } + } + }, "/teams/{id}": { "get": { "produces": [ @@ -17200,7 +17844,7 @@ }, { "type": "string", - "description": "username of the member to list", + "description": "username of the user whose data is to be listed", "name": "username", "in": "path", "required": true @@ -17235,7 +17879,7 @@ }, { "type": "string", - "description": "username of the user to add", + "description": "username of the user to add to a team", "name": "username", "in": "path", "required": true @@ -17273,7 +17917,7 @@ }, { "type": "string", - "description": "username of the user to remove", + "description": "username of the user to remove from a team", "name": "username", "in": "path", "required": true @@ -17527,6 +18171,49 @@ } } }, + "/user/actions/jobs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get workflow jobs", + "operationId": "getUserWorkflowJobs", + "parameters": [ + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowJobsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/user/actions/runners": { "get": { "produces": [ @@ -17644,6 +18331,73 @@ } } }, + "/user/actions/runs": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Get workflow runs", + "operationId": "getUserWorkflowRuns", + "parameters": [ + { + "type": "string", + "description": "workflow event name", + "name": "event", + "in": "query" + }, + { + "type": "string", + "description": "workflow branch", + "name": "branch", + "in": "query" + }, + { + "type": "string", + "description": "workflow status (pending, queued, in_progress, failure, success, skipped)", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "triggered by user", + "name": "actor", + "in": "query" + }, + { + "type": "string", + "description": "triggering sha of the workflow run", + "name": "head_sha", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "$ref": "#/responses/WorkflowRunsList" + }, + "400": { + "$ref": "#/responses/error" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/user/actions/secrets/{secretname}": { "put": { "consumes": [ @@ -17863,16 +18617,13 @@ ], "responses": { "201": { - "description": "response when creating a variable" - }, - "204": { - "description": "response when creating a variable" + "description": "successfully created the user-level variable" }, "400": { "$ref": "#/responses/error" }, - "404": { - "$ref": "#/responses/notFound" + "409": { + "description": "variable name already exists." } } }, @@ -18145,7 +18896,7 @@ "parameters": [ { "type": "string", - "description": "user to check", + "description": "username of the user to check", "name": "username", "in": "path", "required": true @@ -18169,7 +18920,7 @@ "parameters": [ { "type": "string", - "description": "user to block", + "description": "username of the user to block", "name": "username", "in": "path", "required": true @@ -18202,7 +18953,7 @@ "parameters": [ { "type": "string", - "description": "user to unblock", + "description": "username of the user to unblock", "name": "username", "in": "path", "required": true @@ -18364,7 +19115,7 @@ "parameters": [ { "type": "string", - "description": "username of followed user", + "description": "username of the user to check for authenticated followers", "name": "username", "in": "path", "required": true @@ -18388,7 +19139,7 @@ "parameters": [ { "type": "string", - "description": "username of user to follow", + "description": "username of the user to follow", "name": "username", "in": "path", "required": true @@ -18415,7 +19166,7 @@ "parameters": [ { "type": "string", - "description": "username of user to unfollow", + "description": "username of the user to unfollow", "name": "username", "in": "path", "required": true @@ -19369,7 +20120,7 @@ "parameters": [ { "type": "string", - "description": "username of user to get", + "description": "username of the user whose data is to be listed", "name": "username", "in": "path", "required": true @@ -19398,7 +20149,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose activity feeds are to be listed", "name": "username", "in": "path", "required": true @@ -19452,7 +20203,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose followers are to be listed", "name": "username", "in": "path", "required": true @@ -19493,7 +20244,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose followed users are to be listed", "name": "username", "in": "path", "required": true @@ -19531,14 +20282,14 @@ "parameters": [ { "type": "string", - "description": "username of following user", + "description": "username of the following user", "name": "username", "in": "path", "required": true }, { "type": "string", - "description": "username of followed user", + "description": "username of the followed user", "name": "target", "in": "path", "required": true @@ -19567,7 +20318,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose GPG key list is to be obtained", "name": "username", "in": "path", "required": true @@ -19608,7 +20359,7 @@ "parameters": [ { "type": "string", - "description": "username of user to get", + "description": "username of the user whose heatmap is to be obtained", "name": "username", "in": "path", "required": true @@ -19637,7 +20388,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose public keys are to be listed", "name": "username", "in": "path", "required": true @@ -19684,7 +20435,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose organizations are to be listed", "name": "username", "in": "path", "required": true @@ -19725,7 +20476,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose permissions are to be obtained", "name": "username", "in": "path", "required": true @@ -19764,7 +20515,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose owned repos are to be listed", "name": "username", "in": "path", "required": true @@ -19805,7 +20556,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose starred repos are to be listed", "name": "username", "in": "path", "required": true @@ -19849,7 +20600,7 @@ "parameters": [ { "type": "string", - "description": "username of the user", + "description": "username of the user whose watched repos are to be listed", "name": "username", "in": "path", "required": true @@ -19890,7 +20641,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of to user whose access tokens are to be listed", "name": "username", "in": "path", "required": true @@ -19932,7 +20683,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose token is to be created", "name": "username", "in": "path", "required": true @@ -19971,7 +20722,7 @@ "parameters": [ { "type": "string", - "description": "username of user", + "description": "username of the user whose token is to be deleted", "name": "username", "in": "path", "required": true @@ -20386,23 +21137,251 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ActionWorkflowJob": { + "description": "ActionWorkflowJob represents a WorkflowJob", + "type": "object", + "properties": { + "completed_at": { + "type": "string", + "format": "date-time", + "x-go-name": "CompletedAt" + }, + "conclusion": { + "type": "string", + "x-go-name": "Conclusion" + }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "CreatedAt" + }, + "head_branch": { + "type": "string", + "x-go-name": "HeadBranch" + }, + "head_sha": { + "type": "string", + "x-go-name": "HeadSha" + }, + "html_url": { + "type": "string", + "x-go-name": "HTMLURL" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + }, + "x-go-name": "Labels" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "run_attempt": { + "type": "integer", + "format": "int64", + "x-go-name": "RunAttempt" + }, + "run_id": { + "type": "integer", + "format": "int64", + "x-go-name": "RunID" + }, + "run_url": { + "type": "string", + "x-go-name": "RunURL" + }, + "runner_id": { + "type": "integer", + "format": "int64", + "x-go-name": "RunnerID" + }, + "runner_name": { + "type": "string", + "x-go-name": "RunnerName" + }, + "started_at": { + "type": "string", + "format": "date-time", + "x-go-name": "StartedAt" + }, + "status": { + "type": "string", + "x-go-name": "Status" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/ActionWorkflowStep" + }, + "x-go-name": "Steps" + }, + "url": { + "type": "string", + "x-go-name": "URL" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "ActionWorkflowJobsResponse": { + "description": "ActionWorkflowJobsResponse returns ActionWorkflowJobs", + "type": "object", + "properties": { + "jobs": { + "type": "array", + "items": { + "$ref": "#/definitions/ActionWorkflowJob" + }, + "x-go-name": "Entries" + }, + "total_count": { + "type": "integer", + "format": "int64", + "x-go-name": "TotalCount" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "ActionWorkflowRun": { "description": "ActionWorkflowRun represents a WorkflowRun", "type": "object", "properties": { + "actor": { + "$ref": "#/definitions/User" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "x-go-name": "CompletedAt" + }, + "conclusion": { + "type": "string", + "x-go-name": "Conclusion" + }, + "display_title": { + "type": "string", + "x-go-name": "DisplayTitle" + }, + "event": { + "type": "string", + "x-go-name": "Event" + }, + "head_branch": { + "type": "string", + "x-go-name": "HeadBranch" + }, + "head_repository": { + "$ref": "#/definitions/Repository" + }, "head_sha": { "type": "string", "x-go-name": "HeadSha" }, + "html_url": { + "type": "string", + "x-go-name": "HTMLURL" + }, "id": { "type": "integer", "format": "int64", "x-go-name": "ID" }, + "path": { + "type": "string", + "x-go-name": "Path" + }, + "repository": { + "$ref": "#/definitions/Repository" + }, "repository_id": { "type": "integer", "format": "int64", "x-go-name": "RepositoryID" + }, + "run_attempt": { + "type": "integer", + "format": "int64", + "x-go-name": "RunAttempt" + }, + "run_number": { + "type": "integer", + "format": "int64", + "x-go-name": "RunNumber" + }, + "started_at": { + "type": "string", + "format": "date-time", + "x-go-name": "StartedAt" + }, + "status": { + "type": "string", + "x-go-name": "Status" + }, + "trigger_actor": { + "$ref": "#/definitions/User" + }, + "url": { + "type": "string", + "x-go-name": "URL" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "ActionWorkflowRunsResponse": { + "description": "ActionWorkflowRunsResponse returns ActionWorkflowRuns", + "type": "object", + "properties": { + "total_count": { + "type": "integer", + "format": "int64", + "x-go-name": "TotalCount" + }, + "workflow_runs": { + "type": "array", + "items": { + "$ref": "#/definitions/ActionWorkflowRun" + }, + "x-go-name": "Entries" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, + "ActionWorkflowStep": { + "description": "ActionWorkflowStep represents a step of a WorkflowJob", + "type": "object", + "properties": { + "completed_at": { + "type": "string", + "format": "date-time", + "x-go-name": "CompletedAt" + }, + "conclusion": { + "type": "string", + "x-go-name": "Conclusion" + }, + "name": { + "type": "string", + "x-go-name": "Name" + }, + "number": { + "type": "integer", + "format": "int64", + "x-go-name": "Number" + }, + "started_at": { + "type": "string", + "format": "date-time", + "x-go-name": "StartedAt" + }, + "status": { + "type": "string", + "x-go-name": "Status" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -20544,7 +21523,7 @@ "x-go-name": "Time" }, "user_name": { - "description": "User who spent the time (optional)", + "description": "username of the user who spent the time working on the issue (optional)", "type": "string", "x-go-name": "User" } @@ -20891,7 +21870,7 @@ ], "properties": { "content": { - "description": "new or updated file content, must be base64 encoded", + "description": "new or updated file content, it must be base64 encoded", "type": "string", "x-go-name": "ContentBase64" }, @@ -20901,11 +21880,13 @@ "x-go-name": "FromPath" }, "operation": { - "description": "indicates what to do with the file", + "description": "indicates what to do with the file: \"create\" for creating a new file, \"update\" for updating an existing file,\n\"upload\" for creating or updating a file, \"rename\" for renaming a file, and \"delete\" for deleting an existing file.", "type": "string", "enum": [ "create", "update", + "upload", + "rename", "delete" ], "x-go-name": "Operation" @@ -20916,7 +21897,7 @@ "x-go-name": "Path" }, "sha": { - "description": "sha is the SHA for the file that already exists, required for update or delete", + "description": "the blob ID (SHA) for the file that already exists, required for changing existing files", "type": "string", "x-go-name": "SHA" } @@ -21032,7 +22013,17 @@ "x-go-name": "SHA" }, "state": { - "$ref": "#/definitions/CommitStatusState" + "type": "string", + "enum": [ + "pending", + "success", + "error", + "failure", + "warning", + "skipped" + ], + "x-go-enum-desc": "pending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped", + "x-go-name": "State" }, "statuses": { "type": "array", @@ -21260,7 +22251,17 @@ "x-go-name": "ID" }, "status": { - "$ref": "#/definitions/CommitStatusState" + "type": "string", + "enum": [ + "pending", + "success", + "error", + "failure", + "warning", + "skipped" + ], + "x-go-enum-desc": "pending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped", + "x-go-name": "State" }, "target_url": { "type": "string", @@ -21278,11 +22279,6 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, - "CommitStatusState": { - "description": "CommitStatusState holds the state of a CommitStatus\nIt can be \"pending\", \"success\", \"error\" and \"failure\"", - "type": "string", - "x-go-package": "code.gitea.io/gitea/modules/structs" - }, "CommitUser": { "type": "object", "title": "CommitUser contains information of a user in the context of a commit.", @@ -21322,6 +22318,22 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "ContentsExtResponse": { + "type": "object", + "properties": { + "dir_contents": { + "type": "array", + "items": { + "$ref": "#/definitions/ContentsResponse" + }, + "x-go-name": "DirContents" + }, + "file_contents": { + "$ref": "#/definitions/ContentsResponse" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "ContentsResponse": { "description": "ContentsResponse contains information about a repo's entry's (dir, file, symlink, submodule) metadata and content", "type": "object", @@ -21356,6 +22368,10 @@ "format": "date-time", "x-go-name": "LastAuthorDate" }, + "last_commit_message": { + "type": "string", + "x-go-name": "LastCommitMessage" + }, "last_commit_sha": { "type": "string", "x-go-name": "LastCommitSHA" @@ -21365,6 +22381,15 @@ "format": "date-time", "x-go-name": "LastCommitterDate" }, + "lfs_oid": { + "type": "string", + "x-go-name": "LfsOid" + }, + "lfs_size": { + "type": "integer", + "format": "int64", + "x-go-name": "LfsSize" + }, "name": { "type": "string", "x-go-name": "Name" @@ -22037,6 +23062,7 @@ "x-go-name": "RepoAdminChangeTeamAccess" }, "username": { + "description": "username of the organization", "type": "string", "x-go-name": "UserName" }, @@ -22226,6 +23252,10 @@ "type": "boolean", "x-go-name": "IsPrerelease" }, + "tag_message": { + "type": "string", + "x-go-name": "TagMessage" + }, "tag_name": { "type": "string", "x-go-name": "TagName" @@ -22331,7 +23361,17 @@ "x-go-name": "Description" }, "state": { - "$ref": "#/definitions/CommitStatusState" + "type": "string", + "enum": [ + "pending", + "success", + "error", + "failure", + "warning", + "skipped" + ], + "x-go-enum-desc": "pending CommitStatusPending CommitStatusPending is for when the CommitStatus is Pending\nsuccess CommitStatusSuccess CommitStatusSuccess is for when the CommitStatus is Success\nerror CommitStatusError CommitStatusError is for when the CommitStatus is Error\nfailure CommitStatusFailure CommitStatusFailure is for when the CommitStatus is Failure\nwarning CommitStatusWarning CommitStatusWarning is for when the CommitStatus is Warning\nskipped CommitStatusSkipped CommitStatusSkipped is for when CommitStatus is Skipped", + "x-go-name": "State" }, "target_url": { "type": "string", @@ -22473,7 +23513,9 @@ "x-go-name": "FullName" }, "login_name": { + "description": "identifier of the user, provided by the external authenticator (if configured)", "type": "string", + "default": "empty", "x-go-name": "LoginName" }, "must_change_password": { @@ -22498,6 +23540,7 @@ "x-go-name": "SourceID" }, "username": { + "description": "username of the user", "type": "string", "x-go-name": "Username" }, @@ -22627,7 +23670,7 @@ "x-go-name": "NewBranchName" }, "sha": { - "description": "sha is the SHA for the file that already exists", + "description": "the blob ID (SHA) for the file that already exists, it is required for changing existing files", "type": "string", "x-go-name": "SHA" }, @@ -23483,7 +24526,9 @@ "x-go-name": "Location" }, "login_name": { + "description": "identifier of the user, provided by the external authenticator (if configured)", "type": "string", + "default": "empty", "x-go-name": "LoginName" }, "max_repo_creation": { @@ -23542,6 +24587,7 @@ "x-go-name": "UserID" }, "username": { + "description": "username of the user", "type": "string", "x-go-name": "UserName" }, @@ -24000,6 +25046,15 @@ "type": "string", "x-go-name": "Encoding" }, + "lfs_oid": { + "type": "string", + "x-go-name": "LfsOid" + }, + "lfs_size": { + "type": "integer", + "format": "int64", + "x-go-name": "LfsSize" + }, "sha": { "type": "string", "x-go-name": "SHA" @@ -24756,6 +25811,10 @@ "branch": { "type": "string", "x-go-name": "Branch" + }, + "ff_only": { + "type": "boolean", + "x-go-name": "FfOnly" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" @@ -24865,7 +25924,8 @@ "gogs", "onedev", "gitbucket", - "codebase" + "codebase", + "codecommit" ], "x-go-name": "Service" }, @@ -25252,7 +26312,7 @@ "x-go-name": "RepoAdminChangeTeamAccess" }, "username": { - "description": "deprecated", + "description": "username of the organization\ndeprecated", "type": "string", "x-go-name": "UserName" }, @@ -25496,6 +26556,7 @@ "x-go-name": "Name" }, "username": { + "description": "username of the user", "type": "string", "x-go-name": "UserName" } @@ -26959,6 +28020,7 @@ "x-go-name": "UserID" }, "user_name": { + "description": "username of the user", "type": "string", "x-go-name": "UserName" } @@ -27062,7 +28124,7 @@ "x-go-name": "NewBranchName" }, "sha": { - "description": "sha is the SHA for the file that already exists", + "description": "the blob ID (SHA) for the file that already exists, it is required for changing existing files", "type": "string", "x-go-name": "SHA" }, @@ -27200,12 +28262,12 @@ "x-go-name": "Location" }, "login": { - "description": "the user's username", + "description": "login of the user, same as `username`", "type": "string", "x-go-name": "UserName" }, "login_name": { - "description": "the user's authentication sign-in name.", + "description": "identifier of the user, provided by the external authenticator (if configured)", "type": "string", "default": "empty", "x-go-name": "LoginName" @@ -27741,6 +28803,12 @@ "$ref": "#/definitions/Compare" } }, + "ContentsExtResponse": { + "description": "", + "schema": { + "$ref": "#/definitions/ContentsExtResponse" + } + }, "ContentsListResponse": { "description": "ContentsListResponse", "schema": { @@ -28527,6 +29595,30 @@ } } }, + "WorkflowJob": { + "description": "WorkflowJob", + "schema": { + "$ref": "#/definitions/ActionWorkflowJob" + } + }, + "WorkflowJobsList": { + "description": "WorkflowJobsList", + "schema": { + "$ref": "#/definitions/ActionWorkflowJobsResponse" + } + }, + "WorkflowRun": { + "description": "WorkflowRun", + "schema": { + "$ref": "#/definitions/ActionWorkflowRun" + } + }, + "WorkflowRunsList": { + "description": "WorkflowRunsList", + "schema": { + "$ref": "#/definitions/ActionWorkflowRunsResponse" + } + }, "conflict": { "description": "APIConflict is a conflict empty response" }, diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index d66568199d..a3f6e1471f 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -7,6 +7,9 @@ {{end}} </h4> <div class="ui attached segment"> + {{if .IsFirstTimeRegistration}} + <p>{{ctx.Locale.Tr "auth.sign_up_tip"}}</p> + {{end}} <form class="ui form" action="{{.SignUpLink}}" method="post"> {{.CsrfTokenHtml}} {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister)}} diff --git a/templates/user/dashboard/feeds.tmpl b/templates/user/dashboard/feeds.tmpl index 4ee12fa783..37dede7af6 100644 --- a/templates/user/dashboard/feeds.tmpl +++ b/templates/user/dashboard/feeds.tmpl @@ -91,7 +91,7 @@ {{range $push.Commits}} {{$commitLink := printf "%s/commit/%s" $repoLink .Sha1}} <div class="flex-text-block"> - <img alt class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16"> + <img loading="lazy" alt class="ui avatar" src="{{$push.AvatarLink ctx .AuthorEmail}}" title="{{.AuthorName}}" width="16" height="16"> <a class="ui sha label" href="{{$commitLink}}">{{ShortSha .Sha1}}</a> <span class="text truncate"> {{ctx.RenderUtils.RenderCommitMessage .Message $repo}} diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl index 6ef53ab654..e4dd27e63b 100644 --- a/templates/user/notification/notification_subscriptions.tmpl +++ b/templates/user/notification/notification_subscriptions.tmpl @@ -69,8 +69,8 @@ {{template "shared/issuelist" dict "." . "listType" "dashboard"}} {{end}} {{else}} - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} {{end}} </div> diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index e5c3412ddd..74a53b937d 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -17,8 +17,8 @@ {{template "user/dashboard/feeds" .}} {{else if eq .TabName "stars"}} <div class="stars"> - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} </div> {{else if eq .TabName "following"}} @@ -30,8 +30,8 @@ {{else if eq .TabName "organizations"}} {{template "repo/user_cards" .}} {{else}} - {{template "shared/repo_search" .}} - {{template "explore/repo_list" .}} + {{template "shared/repo/search" .}} + {{template "shared/repo/list" .}} {{template "base/paginate" .}} {{end}} </div> diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl index 501f238c7a..8c24da7fc9 100644 --- a/templates/user/settings/applications.tmpl +++ b/templates/user/settings/applications.tmpl @@ -68,7 +68,7 @@ </label> </div> <div> - <div class="tw-my-2">{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (`href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`|SafeHTML)}}</div> + <div class="tw-my-2">{{ctx.Locale.Tr "settings.access_token_desc" (HTMLFormat `href="%s/api/swagger" target="_blank"` AppSubUrl) (HTMLFormat `href="%s" target="_blank"` "https://docs.gitea.com/development/oauth2-provider#scopes")}}</div> <table class="ui table unstackable tw-my-2"> {{range $category := .TokenCategories}} <tr> diff --git a/templates/user/settings/organization.tmpl b/templates/user/settings/organization.tmpl index 16c27b52cd..a48ca9ec9b 100644 --- a/templates/user/settings/organization.tmpl +++ b/templates/user/settings/organization.tmpl @@ -47,7 +47,7 @@ {{ctx.Locale.Tr "org.members.leave"}} </div> <div class="content"> - <p>{{ctx.Locale.Tr "org.members.leave.detail" (`<span class="dataOrganizationName"></span>`|SafeHTML)}}</p> + <p>{{ctx.Locale.Tr "org.members.leave.detail" (HTMLFormat `<span class="%s"></span>` "dataOrganizationName")}}</p> </div> {{template "base/modal_actions_confirm" .}} </div> diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index af631ca8fd..4a408dbd7b 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -4,7 +4,7 @@ // This is primarily coped from /tests/integration/integration_test.go // TODO: Move common functions to shared file -//nolint:forbidigo +//nolint:forbidigo // use of print functions is allowed in tests package e2e import ( diff --git a/tests/integration/actions_runner_test.go b/tests/integration/actions_runner_test.go index 75402929a1..6cc5a10e0f 100644 --- a/tests/integration/actions_runner_test.go +++ b/tests/integration/actions_runner_test.go @@ -115,10 +115,9 @@ func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1 } type mockTaskOutcome struct { - result runnerv1.Result - outputs map[string]string - logRows []*runnerv1.LogRow - execTime time.Duration + result runnerv1.Result + outputs map[string]string + logRows []*runnerv1.LogRow } func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTaskOutcome) { @@ -145,7 +144,6 @@ func (r *mockRunner) execTask(t *testing.T, task *runnerv1.Task, outcome *mockTa sentOutputKeys = append(sentOutputKeys, outputKey) assert.ElementsMatch(t, sentOutputKeys, resp.Msg.SentOutputs) } - time.Sleep(outcome.execTime) resp, err := r.client.runnerServiceClient.UpdateTask(t.Context(), connect.NewRequest(&runnerv1.UpdateTaskRequest{ State: &runnerv1.TaskState{ Id: task.Id, diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index e755481d09..088491d570 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" actions_module "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/json" @@ -633,12 +634,12 @@ jobs: assert.NotEmpty(t, addFileResp) sha = addFileResp.Commit.SHA assert.Eventually(t, func() bool { - latestCommitStatuses, _, err := git_model.GetLatestCommitStatus(db.DefaultContext, repo.ID, sha, db.ListOptionsAll) + latestCommitStatuses, err := git_model.GetLatestCommitStatus(db.DefaultContext, repo.ID, sha, db.ListOptionsAll) assert.NoError(t, err) if len(latestCommitStatuses) == 0 { return false } - if latestCommitStatuses[0].State == api.CommitStatusPending { + if latestCommitStatuses[0].State == commitstatus.CommitStatusPending { insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context) return true } @@ -676,17 +677,17 @@ jobs: } func checkCommitStatusAndInsertFakeStatus(t *testing.T, repo *repo_model.Repository, sha string) { - latestCommitStatuses, _, err := git_model.GetLatestCommitStatus(db.DefaultContext, repo.ID, sha, db.ListOptionsAll) + latestCommitStatuses, err := git_model.GetLatestCommitStatus(db.DefaultContext, repo.ID, sha, db.ListOptionsAll) assert.NoError(t, err) assert.Len(t, latestCommitStatuses, 1) - assert.Equal(t, api.CommitStatusPending, latestCommitStatuses[0].State) + assert.Equal(t, commitstatus.CommitStatusPending, latestCommitStatuses[0].State) insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context) } func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL, context string) { err := commitstatus_service.CreateCommitStatus(db.DefaultContext, repo, user_model.NewActionsUser(), sha, &git_model.CommitStatus{ - State: api.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, TargetURL: targetURL, Context: context, }) @@ -719,7 +720,7 @@ func TestWorkflowDispatchPublicApi(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch jobs: @@ -799,7 +800,7 @@ func TestWorkflowDispatchPublicApiWithInputs(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: @@ -890,7 +891,7 @@ func TestWorkflowDispatchPublicApiJSON(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: @@ -976,7 +977,7 @@ func TestWorkflowDispatchPublicApiWithInputsJSON(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: @@ -1070,7 +1071,7 @@ func TestWorkflowDispatchPublicApiWithInputsNonDefaultBranchJSON(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch jobs: @@ -1106,7 +1107,7 @@ jobs: { Operation: "update", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: @@ -1208,7 +1209,7 @@ func TestWorkflowApi(t *testing.T) { { Operation: "create", TreePath: ".gitea/workflows/dispatch.yml", - ContentReader: strings.NewReader(`name: test + ContentReader: strings.NewReader(` on: workflow_dispatch: { inputs: { myinput: { default: def }, myinput2: { default: def2 }, myinput3: { type: boolean, default: false } } } jobs: diff --git a/tests/integration/api_helper_for_declarative_test.go b/tests/integration/api_helper_for_declarative_test.go index 083535a9a5..b30cdfd0fc 100644 --- a/tests/integration/api_helper_for_declarative_test.go +++ b/tests/integration/api_helper_for_declarative_test.go @@ -263,7 +263,7 @@ func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64) var req *RequestWrapper var resp *httptest.ResponseRecorder - for i := 0; i < 6; i++ { + for range 6 { req = NewRequestWithJSON(t, http.MethodPost, urlStr, &forms.MergePullRequestForm{ MergeMessageField: "doAPIMergePullRequest Merge", Do: string(repo_model.MergeStyleMerge), diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go index e035f7200b..370c90a100 100644 --- a/tests/integration/api_issue_test.go +++ b/tests/integration/api_issue_test.go @@ -166,7 +166,7 @@ func TestAPICreateIssueParallel(t *testing.T) { urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repoBefore.Name) var wg sync.WaitGroup - for i := 0; i < 10; i++ { + for i := range 10 { wg.Add(1) go func(parentT *testing.T, i int) { parentT.Run(fmt.Sprintf("ParallelCreateIssue_%d", i), func(t *testing.T) { @@ -267,10 +267,7 @@ func TestAPISearchIssues(t *testing.T) { defer tests.PrepareTestEnv(t)() // as this API was used in the frontend, it uses UI page size - expectedIssueCount := 20 // from the fixtures - if expectedIssueCount > setting.UI.IssuePagingNum { - expectedIssueCount = setting.UI.IssuePagingNum - } + expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures link, _ := url.Parse("/api/v1/repos/issues/search") token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue) @@ -371,10 +368,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) { defer tests.PrepareTestEnv(t)() // as this API was used in the frontend, it uses UI page size - expectedIssueCount := 20 // from the fixtures - if expectedIssueCount > setting.UI.IssuePagingNum { - expectedIssueCount = setting.UI.IssuePagingNum - } + expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures link, _ := url.Parse("/api/v1/repos/issues/search") token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue) diff --git a/tests/integration/api_packages_chef_test.go b/tests/integration/api_packages_chef_test.go index 86b3be9d0c..8f2c2592e7 100644 --- a/tests/integration/api_packages_chef_test.go +++ b/tests/integration/api_packages_chef_test.go @@ -181,7 +181,7 @@ nwIDAQAB var data []byte if version == "1.3" { - data = []byte(fmt.Sprintf( + data = fmt.Appendf(nil, "Method:%s\nPath:%s\nX-Ops-Content-Hash:%s\nX-Ops-Sign:version=%s\nX-Ops-Timestamp:%s\nX-Ops-UserId:%s\nX-Ops-Server-API-Version:%s", req.Method, path.Clean(req.URL.Path), @@ -190,17 +190,17 @@ nwIDAQAB req.Header.Get("X-Ops-Timestamp"), username, req.Header.Get("X-Ops-Server-Api-Version"), - )) + ) } else { sum := sha1.Sum([]byte(path.Clean(req.URL.Path))) - data = []byte(fmt.Sprintf( + data = fmt.Appendf(nil, "Method:%s\nHashed Path:%s\nX-Ops-Content-Hash:%s\nX-Ops-Timestamp:%s\nX-Ops-UserId:%s", req.Method, base64.StdEncoding.EncodeToString(sum[:]), req.Header.Get("X-Ops-Content-Hash"), req.Header.Get("X-Ops-Timestamp"), username, - )) + ) } for k := range req.Header { diff --git a/tests/integration/api_packages_container_test.go b/tests/integration/api_packages_container_test.go index b2db77685d..204f099bbe 100644 --- a/tests/integration/api_packages_container_test.go +++ b/tests/integration/api_packages_container_test.go @@ -7,6 +7,7 @@ import ( "bytes" "crypto/sha256" "encoding/base64" + "encoding/hex" "fmt" "net/http" "strconv" @@ -17,7 +18,6 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" - container_model "code.gitea.io/gitea/models/packages/container" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" container_module "code.gitea.io/gitea/modules/packages/container" @@ -57,7 +57,7 @@ func TestPackageContainer(t *testing.T) { return values } - images := []string{"test", "te/st"} + images := []string{"test", "sub/name"} tags := []string{"latest", "main"} multiTag := "multi" @@ -70,7 +70,8 @@ func TestPackageContainer(t *testing.T) { configContent := `{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/true"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"container":"b89fe92a887d55c0961f02bdfbfd8ac3ddf66167db374770d2d9e9fab3311510","container_config":{"Hostname":"b89fe92a887d","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/true\"]"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"created":"2022-01-01T00:00:00.000000000Z","docker_version":"20.10.12","history":[{"created":"2022-01-01T00:00:00.000000000Z","created_by":"/bin/sh -c #(nop) COPY file:0e7589b0c800daaf6fa460d2677101e4676dd9491980210cb345480e513f3602 in /true "},{"created":"2022-01-01T00:00:00.000000001Z","created_by":"/bin/sh -c #(nop) CMD [\"/true\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:0ff3b91bdf21ecdf2f2f3d4372c2098a14dbe06cd678e8f0a85fd4902d00e2e2"]}}` manifestDigest := "sha256:4f10484d1c1bb13e3956b4de1cd42db8e0f14a75be1617b60f2de3cd59c803c6" - manifestContent := `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` + manifestContent := `{"schemaVersion":2,"mediaType":"` + container_module.ContentTypeDockerDistributionManifestV2 + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` + manifestContentType := container_module.ContentTypeDockerDistributionManifestV2 untaggedManifestDigest := "sha256:4305f5f5572b9a426b88909b036e52ee3cf3d7b9c1b01fac840e90747f56623d" untaggedManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}` @@ -250,7 +251,7 @@ func TestPackageContainer(t *testing.T) { assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location")) assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest")) - pv, err := packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, container_model.UploadVersion) + pv, err := packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, container_module.UploadVersion) assert.NoError(t, err) pfs, err := packages_model.GetFilesByVersionID(db.DefaultContext, pv.ID) @@ -296,11 +297,22 @@ func TestPackageContainer(t *testing.T) { SetHeader("Content-Range", "1-10") MakeRequest(t, req, http.StatusRequestedRangeNotSatisfiable) - contentRange := fmt.Sprintf("0-%d", len(blobContent)-1) - req.SetHeader("Content-Range", contentRange) + // first patch without Content-Range + req = NewRequestWithBody(t, "PATCH", setting.AppURL+uploadURL[1:], bytes.NewReader(blobContent[:1])). + AddTokenAuth(userToken) + resp = MakeRequest(t, req, http.StatusAccepted) + assert.NotEmpty(t, resp.Header().Get("Location")) + assert.Equal(t, "0-0", resp.Header().Get("Range")) + + // then send remaining content with Content-Range + req = NewRequestWithBody(t, "PATCH", setting.AppURL+uploadURL[1:], bytes.NewReader(blobContent[1:])). + SetHeader("Content-Range", fmt.Sprintf("1-%d", len(blobContent)-1)). + AddTokenAuth(userToken) resp = MakeRequest(t, req, http.StatusAccepted) + contentRange := fmt.Sprintf("0-%d", len(blobContent)-1) assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid")) + assert.NotEmpty(t, resp.Header().Get("Location")) assert.Equal(t, contentRange, resp.Header().Get("Range")) uploadURL = resp.Header().Get("Location") @@ -310,7 +322,8 @@ func TestPackageContainer(t *testing.T) { resp = MakeRequest(t, req, http.StatusNoContent) assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid")) - assert.Equal(t, fmt.Sprintf("0-%d", len(blobContent)), resp.Header().Get("Range")) + assert.Equal(t, uploadURL, resp.Header().Get("Location")) + assert.Equal(t, contentRange, resp.Header().Get("Range")) pbu, err = packages_model.GetBlobUploadByID(db.DefaultContext, uuid) assert.NoError(t, err) @@ -341,7 +354,8 @@ func TestPackageContainer(t *testing.T) { resp = MakeRequest(t, req, http.StatusNoContent) assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid")) - assert.Equal(t, "0-0", resp.Header().Get("Range")) + // FIXME: undefined behavior when the uploaded content is empty: https://github.com/opencontainers/distribution-spec/issues/578 + assert.Nil(t, resp.Header().Values("Range")) req = NewRequest(t, "DELETE", setting.AppURL+uploadURL[1:]). AddTokenAuth(userToken) @@ -429,7 +443,7 @@ func TestPackageContainer(t *testing.T) { assert.Len(t, pd.Files, 3) for _, pfd := range pd.Files { switch pfd.File.Name { - case container_model.ManifestFilename: + case container_module.ManifestFilename: assert.True(t, pfd.File.IsLead) assert.Equal(t, "application/vnd.docker.distribution.manifest.v2+json", pfd.Properties.GetByName(container_module.PropertyMediaType)) assert.Equal(t, manifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest)) @@ -492,7 +506,7 @@ func TestPackageContainer(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) assert.Equal(t, strconv.Itoa(len(manifestContent)), resp.Header().Get("Content-Length")) - assert.Equal(t, oci.MediaTypeImageManifest, resp.Header().Get("Content-Type")) + assert.Equal(t, manifestContentType, resp.Header().Get("Content-Type")) assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest")) assert.Equal(t, manifestContent, resp.Body.String()) }) @@ -531,7 +545,7 @@ func TestPackageContainer(t *testing.T) { assert.Len(t, pd.Files, 3) for _, pfd := range pd.Files { - if pfd.File.Name == container_model.ManifestFilename { + if pfd.File.Name == container_module.ManifestFilename { assert.True(t, pfd.File.IsLead) assert.Equal(t, oci.MediaTypeImageManifest, pfd.Properties.GetByName(container_module.PropertyMediaType)) assert.Equal(t, untaggedManifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest)) @@ -623,6 +637,22 @@ func TestPackageContainer(t *testing.T) { assert.Equal(t, blobContent, resp.Body.Bytes()) }) + t.Run("GetBlob/Empty", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + emptyDigestBuf := sha256.Sum256(nil) + emptyDigest := "sha256:" + hex.EncodeToString(emptyDigestBuf[:]) + req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, emptyDigest), strings.NewReader("")).AddTokenAuth(userToken) + MakeRequest(t, req, http.StatusCreated) + + req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, emptyDigest)).AddTokenAuth(userToken) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "0", resp.Header().Get("Content-Length")) + + req = NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, emptyDigest)).AddTokenAuth(userToken) + resp = MakeRequest(t, req, http.StatusOK) + assert.Equal(t, "0", resp.Header().Get("Content-Length")) + }) + t.Run("GetTagList", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -732,7 +762,7 @@ func TestPackageContainer(t *testing.T) { url := fmt.Sprintf("%sv2/%s/parallel", setting.AppURL, user.Name) var wg sync.WaitGroup - for i := 0; i < 10; i++ { + for i := range 10 { wg.Add(1) content := []byte{byte(i)} diff --git a/tests/integration/api_packages_debian_test.go b/tests/integration/api_packages_debian_test.go index 98027d774c..3ae60d2aa2 100644 --- a/tests/integration/api_packages_debian_test.go +++ b/tests/integration/api_packages_debian_test.go @@ -284,7 +284,7 @@ func TestPackageDebian(t *testing.T) { // because "Iterate" keeps a dangling SQL session but the callback function still uses the same session to execute statements. // The "Iterate" problem has been checked by TestContextSafety now, so here we only need to check the cleanup logic with a small number packagesCount := 2 - for i := 0; i < packagesCount; i++ { + for i := range packagesCount { uploadURL := fmt.Sprintf("%s/pool/%s/%s/upload", rootURL, "test", "main") req := NewRequestWithBody(t, "PUT", uploadURL, createArchive(packageName, "1.0."+strconv.Itoa(i), "all")).AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusCreated) diff --git a/tests/integration/api_packages_goproxy_test.go b/tests/integration/api_packages_goproxy_test.go index dab9fefc5e..fa0ee5b901 100644 --- a/tests/integration/api_packages_goproxy_test.go +++ b/tests/integration/api_packages_goproxy_test.go @@ -87,7 +87,7 @@ func TestPackageGo(t *testing.T) { AddBasicAuth(user.Name) MakeRequest(t, req, http.StatusConflict) - time.Sleep(time.Second) + time.Sleep(time.Second) // Ensure the timestamp is different, then the "list" below can have stable order content = createArchive(map[string][]byte{ packageName + "@" + packageVersion2 + "/go.mod": []byte(goModContent), diff --git a/tests/integration/api_packages_maven_test.go b/tests/integration/api_packages_maven_test.go index 408c8805c2..30ef1884cd 100644 --- a/tests/integration/api_packages_maven_test.go +++ b/tests/integration/api_packages_maven_test.go @@ -321,7 +321,7 @@ func TestPackageMavenConcurrent(t *testing.T) { defer tests.PrintCurrentTest(t)() var wg sync.WaitGroup - for i := 0; i < 10; i++ { + for i := range 10 { wg.Add(1) go func(i int) { putFile(t, fmt.Sprintf("/%s/%s.jar", packageVersion, strconv.Itoa(i)), "test", http.StatusCreated) diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go index c0e69a82cd..65b1b9845a 100644 --- a/tests/integration/api_packages_nuget_test.go +++ b/tests/integration/api_packages_nuget_test.go @@ -46,21 +46,30 @@ func TestPackageNuGet(t *testing.T) { defer tests.PrepareTestEnv(t)() type FeedEntryProperties struct { - Version string `xml:"Version"` - NormalizedVersion string `xml:"NormalizedVersion"` Authors string `xml:"Authors"` + Copyright string `xml:"Copyright,omitempty"` + Created nuget.TypedValue[time.Time] `xml:"Created"` Dependencies string `xml:"Dependencies"` Description string `xml:"Description"` - VersionDownloadCount nuget.TypedValue[int64] `xml:"VersionDownloadCount"` + DevelopmentDependency nuget.TypedValue[bool] `xml:"DevelopmentDependency"` DownloadCount nuget.TypedValue[int64] `xml:"DownloadCount"` - PackageSize nuget.TypedValue[int64] `xml:"PackageSize"` - Created nuget.TypedValue[time.Time] `xml:"Created"` + ID string `xml:"Id"` + IconURL string `xml:"IconUrl,omitempty"` + Language string `xml:"Language,omitempty"` LastUpdated nuget.TypedValue[time.Time] `xml:"LastUpdated"` - Published nuget.TypedValue[time.Time] `xml:"Published"` + LicenseURL string `xml:"LicenseUrl,omitempty"` + MinClientVersion string `xml:"MinClientVersion,omitempty"` + NormalizedVersion string `xml:"NormalizedVersion"` + Owners string `xml:"Owners,omitempty"` + PackageSize nuget.TypedValue[int64] `xml:"PackageSize"` ProjectURL string `xml:"ProjectUrl,omitempty"` + Published nuget.TypedValue[time.Time] `xml:"Published"` ReleaseNotes string `xml:"ReleaseNotes,omitempty"` RequireLicenseAcceptance nuget.TypedValue[bool] `xml:"RequireLicenseAcceptance"` + Tags string `xml:"Tags,omitempty"` Title string `xml:"Title"` + Version string `xml:"Version"` + VersionDownloadCount nuget.TypedValue[int64] `xml:"VersionDownloadCount"` } type FeedEntry struct { @@ -86,28 +95,54 @@ func TestPackageNuGet(t *testing.T) { readToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadPackage) badToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadNotification) - packageName := "test.package" + packageName := "test.package" // id + packageID := packageName packageVersion := "1.0.3" packageAuthors := "KN4CK3R" packageDescription := "Gitea Test Package" + symbolFilename := "test.pdb" symbolID := "d910bb6948bd4c6cb40155bcf52c3c94" + packageCopyright := "Package Copyright" + packageIconURL := "https://gitea.io/favicon.png" + packageLanguage := "Package Language" + packageLicenseURL := "https://gitea.io/license" + packageMinClientVersion := "1.0.0.0" + packageOwners := "Package Owners" + packageProjectURL := "https://gitea.io" + packageReleaseNotes := "Package Release Notes" + packageTags := "tag_1 tag_2 tag_3" + packageTitle := "Package Title" + packageDevelopmentDependency := true + packageRequireLicenseAcceptance := true + createNuspec := func(id, version string) string { return `<?xml version="1.0" encoding="utf-8"?> -<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> - <metadata> - <id>` + id + `</id> - <version>` + version + `</version> - <authors>` + packageAuthors + `</authors> - <description>` + packageDescription + `</description> - <dependencies> - <group targetFramework=".NETStandard2.0"> - <dependency id="Microsoft.CSharp" version="4.5.0" /> - </group> - </dependencies> - </metadata> -</package>` + <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd"> + <metadata minClientVersion="` + packageMinClientVersion + `"> + <authors>` + packageAuthors + `</authors> + <copyright>` + packageCopyright + `</copyright> + <description>` + packageDescription + `</description> + <developmentDependency>true</developmentDependency> + <iconUrl>` + packageIconURL + `</iconUrl> + <id>` + id + `</id> + <language>` + packageLanguage + `</language> + <licenseUrl>` + packageLicenseURL + `</licenseUrl> + <owners>` + packageOwners + `</owners> + <projectUrl>` + packageProjectURL + `</projectUrl> + <releaseNotes>` + packageReleaseNotes + `</releaseNotes> + <requireLicenseAcceptance>true</requireLicenseAcceptance> + <tags>` + packageTags + `</tags> + <title>` + packageTitle + `</title> + <version>` + version + `</version> + <dependencies> + <group targetFramework=".NETStandard2.0"> + <dependency id="Microsoft.CSharp" version="4.5.0" /> + </group> + </dependencies> + </metadata> + </package>` } createPackage := func(id, version string) *bytes.Buffer { @@ -393,7 +428,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID) assert.NoError(t, err) - assert.Equal(t, int64(412), pb.Size) + assert.Equal(t, int64(610), pb.Size) case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion): assert.False(t, pf.IsLead) @@ -405,7 +440,7 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID) assert.NoError(t, err) - assert.Equal(t, int64(427), pb.Size) + assert.Equal(t, int64(996), pb.Size) case symbolFilename: assert.False(t, pf.IsLead) @@ -736,10 +771,24 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) var result FeedEntry decodeXML(t, resp, &result) - assert.Equal(t, packageName, result.Properties.Title) - assert.Equal(t, packageVersion, result.Properties.Version) assert.Equal(t, packageAuthors, result.Properties.Authors) assert.Equal(t, packageDescription, result.Properties.Description) + assert.Equal(t, packageID, result.Properties.ID) + assert.Equal(t, packageVersion, result.Properties.Version) + + assert.Equal(t, packageCopyright, result.Properties.Copyright) + assert.Equal(t, packageDevelopmentDependency, result.Properties.DevelopmentDependency.Value) + assert.Equal(t, packageIconURL, result.Properties.IconURL) + assert.Equal(t, packageLanguage, result.Properties.Language) + assert.Equal(t, packageLicenseURL, result.Properties.LicenseURL) + assert.Equal(t, packageMinClientVersion, result.Properties.MinClientVersion) + assert.Equal(t, packageOwners, result.Properties.Owners) + assert.Equal(t, packageProjectURL, result.Properties.ProjectURL) + assert.Equal(t, packageReleaseNotes, result.Properties.ReleaseNotes) + assert.Equal(t, packageRequireLicenseAcceptance, result.Properties.RequireLicenseAcceptance.Value) + assert.Equal(t, packageTags, result.Properties.Tags) + assert.Equal(t, packageTitle, result.Properties.Title) + assert.Equal(t, "Microsoft.CSharp:4.5.0:.NETStandard2.0", result.Properties.Dependencies) }) diff --git a/tests/integration/api_packages_rpm_test.go b/tests/integration/api_packages_rpm_test.go index 469bd1fc6c..bd1959f64e 100644 --- a/tests/integration/api_packages_rpm_test.go +++ b/tests/integration/api_packages_rpm_test.go @@ -157,9 +157,14 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, t.Run("Download", func(t *testing.T) { defer tests.PrintCurrentTest(t)() + // download the package without the file name req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture)) resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, content, resp.Body.Bytes()) + // download the package with a file name (it can be anything) + req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s/any-file-name", groupURL, packageName, packageVersion, packageArchitecture)) + resp = MakeRequest(t, req, http.StatusOK) assert.Equal(t, content, resp.Body.Bytes()) }) @@ -447,7 +452,8 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`, pub, err := openpgp.ReadArmoredKeyRing(gpgResp.Body) require.NoError(t, err) - req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture)) + rpmFileName := fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture) + req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture, rpmFileName)) resp := MakeRequest(t, req, http.StatusOK) _, sigs, err := rpmutils.Verify(resp.Body, pub) diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go index 786addbd76..f10b098885 100644 --- a/tests/integration/api_packages_test.go +++ b/tests/integration/api_packages_test.go @@ -15,9 +15,9 @@ import ( auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" - container_model "code.gitea.io/gitea/models/packages/container" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + container_module "code.gitea.io/gitea/modules/packages/container" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -538,7 +538,7 @@ func TestPackageCleanup(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, pbs) - _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, "cleanup-test", container_model.UploadVersion) + _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, "cleanup-test", container_module.UploadVersion) assert.NoError(t, err) err = packages_cleanup_service.CleanupTask(db.DefaultContext, duration) @@ -548,7 +548,7 @@ func TestPackageCleanup(t *testing.T) { assert.NoError(t, err) assert.Empty(t, pbs) - _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, "cleanup-test", container_model.UploadVersion) + _, err = packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, "cleanup-test", container_module.UploadVersion) assert.ErrorIs(t, err, packages_model.ErrPackageNotExist) }) @@ -636,12 +636,16 @@ func TestPackageCleanup(t *testing.T) { }, { Name: "Mixed", - Versions: []version{ - {Version: "keep", ShouldExist: true, Created: time.Now().Add(time.Duration(10000)).Unix()}, - {Version: "dummy", ShouldExist: true, Created: 1}, - {Version: "test-3", ShouldExist: true}, - {Version: "test-4", ShouldExist: false, Created: 1}, - }, + Versions: func(limit, removeDays int) []version { + aa := []version{ + {Version: "keep", ShouldExist: true, Created: time.Now().Add(time.Duration(10000)).Unix()}, + {Version: "dummy", ShouldExist: true, Created: 1}, + } + for i := range limit { + aa = append(aa, version{Version: fmt.Sprintf("test-%v", i+3), ShouldExist: util.Iif(i < removeDays, true, false), Created: time.Now().AddDate(0, 0, -i).Unix()}) + } + return aa + }(220, 7), Rule: &packages_model.PackageCleanupRule{ Enabled: true, KeepCount: 1, @@ -686,7 +690,7 @@ func TestPackageCleanup(t *testing.T) { err = packages_service.DeletePackageVersionAndReferences(db.DefaultContext, pv) assert.NoError(t, err) } else { - assert.ErrorIs(t, err, packages_model.ErrPackageNotExist) + assert.ErrorIs(t, err, packages_model.ErrPackageNotExist, v.Version) } } diff --git a/tests/integration/api_repo_archive_test.go b/tests/integration/api_repo_archive_test.go index e698148d84..97c2c0d54b 100644 --- a/tests/integration/api_repo_archive_test.go +++ b/tests/integration/api_repo_archive_test.go @@ -12,7 +12,9 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/tests" @@ -58,9 +60,12 @@ func TestAPIDownloadArchive(t *testing.T) { link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name)) MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest) + + t.Run("GitHubStyle", testAPIDownloadArchiveGitHubStyle) + t.Run("PrivateRepo", testAPIDownloadArchivePrivateRepo) } -func TestAPIDownloadArchive2(t *testing.T) { +func testAPIDownloadArchiveGitHubStyle(t *testing.T) { defer tests.PrepareTestEnv(t)() repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) @@ -95,7 +100,13 @@ func TestAPIDownloadArchive2(t *testing.T) { bs, err = io.ReadAll(resp.Body) assert.NoError(t, err) assert.Len(t, bs, 382) +} - link, _ = url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master", user2.Name, repo.Name)) - MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusBadRequest) +func testAPIDownloadArchivePrivateRepo(t *testing.T) { + _ = repo_model.UpdateRepositoryColsNoAutoTime(t.Context(), &repo_model.Repository{ID: 1, IsPrivate: true}, "is_private") + MakeRequest(t, NewRequest(t, "HEAD", "/api/v1/repos/user2/repo1/archive/master.zip"), http.StatusNotFound) + MakeRequest(t, NewRequest(t, "HEAD", "/api/v1/repos/user2/repo1/zipball/master"), http.StatusNotFound) + _ = repo_model.UpdateRepoUnitPublicAccess(t.Context(), &repo_model.RepoUnit{RepoID: 1, Type: unit.TypeCode, AnonymousAccessMode: perm.AccessModeRead}) + MakeRequest(t, NewRequest(t, "HEAD", "/api/v1/repos/user2/repo1/archive/master.zip"), http.StatusOK) + MakeRequest(t, NewRequest(t, "HEAD", "/api/v1/repos/user2/repo1/zipball/master"), http.StatusOK) } diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go index 0a7f37facb..af3bc54680 100644 --- a/tests/integration/api_repo_file_create_test.go +++ b/tests/integration/api_repo_file_create_test.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "github.com/stretchr/testify/assert" @@ -52,8 +53,8 @@ func getCreateFileOptions() api.CreateFileOptions { func normalizeFileContentResponseCommitTime(c *api.ContentsResponse) { // decoded JSON response may contain different timezone from the one parsed by git commit // so we need to normalize the time to UTC to make "assert.Equal" pass - c.LastCommitterDate = c.LastCommitterDate.UTC() - c.LastAuthorDate = c.LastAuthorDate.UTC() + c.LastCommitterDate = util.ToPointer(c.LastCommitterDate.UTC()) + c.LastAuthorDate = util.ToPointer(c.LastAuthorDate.UTC()) } type apiFileResponseInfo struct { @@ -74,9 +75,9 @@ func getExpectedFileResponseForCreate(info apiFileResponseInfo) *api.FileRespons Name: path.Base(info.treePath), Path: info.treePath, SHA: sha, - LastCommitSHA: info.lastCommitSHA, - LastCommitterDate: info.lastCommitterWhen, - LastAuthorDate: info.lastAuthorWhen, + LastCommitSHA: util.ToPointer(info.lastCommitSHA), + LastCommitterDate: util.ToPointer(info.lastCommitterWhen), + LastAuthorDate: util.ToPointer(info.lastAuthorWhen), Size: 16, Type: "file", Encoding: &encoding, @@ -130,7 +131,7 @@ func BenchmarkAPICreateFileSmall(b *testing.B) { repo1 := unittest.AssertExistsAndLoadBean(b, &repo_model.Repository{ID: 1}) // public repo b.ResetTimer() - for n := 0; n < b.N; n++ { + for n := 0; b.Loop(); n++ { treePath := fmt.Sprintf("update/file%d.txt", n) _, _ = createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath) } @@ -145,7 +146,7 @@ func BenchmarkAPICreateFileMedium(b *testing.B) { repo1 := unittest.AssertExistsAndLoadBean(b, &repo_model.Repository{ID: 1}) // public repo b.ResetTimer() - for n := 0; n < b.N; n++ { + for n := 0; b.Loop(); n++ { treePath := fmt.Sprintf("update/file%d.txt", n) copy(data, treePath) _, _ = createFileInBranch(user2, repo1, treePath, repo1.DefaultBranch, treePath) diff --git a/tests/integration/api_repo_file_delete_test.go b/tests/integration/api_repo_file_delete_test.go index 47730cd933..9dd47f93e6 100644 --- a/tests/integration/api_repo_file_delete_test.go +++ b/tests/integration/api_repo_file_delete_test.go @@ -20,20 +20,22 @@ import ( func getDeleteFileOptions() *api.DeleteFileOptions { return &api.DeleteFileOptions{ - FileOptions: api.FileOptions{ - BranchName: "master", - NewBranchName: "master", - Message: "Removing the file new/file.txt", - Author: api.Identity{ - Name: "John Doe", - Email: "johndoe@example.com", - }, - Committer: api.Identity{ - Name: "Jane Doe", - Email: "janedoe@example.com", + FileOptionsWithSHA: api.FileOptionsWithSHA{ + FileOptions: api.FileOptions{ + BranchName: "master", + NewBranchName: "master", + Message: "Removing the file new/file.txt", + Author: api.Identity{ + Name: "John Doe", + Email: "johndoe@example.com", + }, + Committer: api.Identity{ + Name: "Jane Doe", + Email: "janedoe@example.com", + }, }, + SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", }, - SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", } } @@ -110,7 +112,7 @@ func TestAPIDeleteFile(t *testing.T) { deleteFileOptions.SHA = "badsha" req = NewRequestWithJSON(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath), &deleteFileOptions). AddTokenAuth(token2) - MakeRequest(t, req, http.StatusBadRequest) + MakeRequest(t, req, http.StatusUnprocessableEntity) // Test creating a file in repo16 by user4 who does not have write access fileID++ diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go index 1605cfbd0b..9a56711da6 100644 --- a/tests/integration/api_repo_file_update_test.go +++ b/tests/integration/api_repo_file_update_test.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" "github.com/stretchr/testify/assert" @@ -27,7 +28,7 @@ func getUpdateFileOptions() *api.UpdateFileOptions { content := "This is updated text" contentEncoded := base64.StdEncoding.EncodeToString([]byte(content)) return &api.UpdateFileOptions{ - DeleteFileOptions: api.DeleteFileOptions{ + FileOptionsWithSHA: api.FileOptionsWithSHA{ FileOptions: api.FileOptions{ BranchName: "master", NewBranchName: "master", @@ -60,9 +61,9 @@ func getExpectedFileResponseForUpdate(info apiFileResponseInfo) *api.FileRespons Name: path.Base(info.treePath), Path: info.treePath, SHA: sha, - LastCommitSHA: info.lastCommitSHA, - LastCommitterDate: info.lastCommitterWhen, - LastAuthorDate: info.lastAuthorWhen, + LastCommitSHA: util.ToPointer(info.lastCommitSHA), + LastCommitterDate: util.ToPointer(info.lastCommitterWhen), + LastAuthorDate: util.ToPointer(info.lastAuthorWhen), Type: "file", Size: 20, Encoding: &encoding, diff --git a/tests/integration/api_repo_get_contents_list_test.go b/tests/integration/api_repo_get_contents_list_test.go index 9b192c6304..563d6fcc10 100644 --- a/tests/integration/api_repo_get_contents_list_test.go +++ b/tests/integration/api_repo_get_contents_list_test.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" repo_service "code.gitea.io/gitea/services/repository" "github.com/stretchr/testify/assert" @@ -35,9 +36,9 @@ func getExpectedContentsListResponseForContents(ref, refType, lastCommitSHA stri Name: path.Base(treePath), Path: treePath, SHA: sha, - LastCommitSHA: lastCommitSHA, - LastCommitterDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)), - LastAuthorDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)), + LastCommitSHA: util.ToPointer(lastCommitSHA), + LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))), + LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))), Type: "file", Size: 30, URL: &selfURL, @@ -65,7 +66,6 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo - treePath := "" // root dir // Get user2's token session := loginUser(t, user2.Name) @@ -94,7 +94,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // ref is default ref ref := repo1.DefaultBranch refType := "branch" - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents?ref=%s", user2.Name, repo1.Name, ref) resp := MakeRequest(t, req, http.StatusOK) var contentsListResponse []*api.ContentsResponse DecodeJSON(t, resp, &contentsListResponse) @@ -106,7 +106,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // No ref refType = "branch" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", user2.Name, repo1.Name) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsListResponse) assert.NotNil(t, contentsListResponse) @@ -117,7 +117,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // ref is the branch we created above in setup ref = newBranch refType = "branch" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents?ref=%s", user2.Name, repo1.Name, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsListResponse) assert.NotNil(t, contentsListResponse) @@ -131,7 +131,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // ref is the new tag we created above in setup ref = newTag refType = "tag" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/?ref=%s", user2.Name, repo1.Name, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsListResponse) assert.NotNil(t, contentsListResponse) @@ -145,7 +145,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // ref is a commit ref = commitID refType = "commit" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/?ref=%s", user2.Name, repo1.Name, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsListResponse) assert.NotNil(t, contentsListResponse) @@ -154,21 +154,21 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // Test file contents a file with a bad ref ref = "badref" - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/?ref=%s", user2.Name, repo1.Name, ref) MakeRequest(t, req, http.StatusNotFound) // Test accessing private ref with user token that does not have access - should fail - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", user2.Name, repo16.Name). AddTokenAuth(token4) MakeRequest(t, req, http.StatusNotFound) // Test access private ref of owner of token - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md", user2.Name, repo16.Name). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", user2.Name, repo16.Name). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) // Test access of org org3 private repo file by owner user2 - req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", org3.Name, repo3.Name, treePath). + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/", org3.Name, repo3.Name). AddTokenAuth(token2) MakeRequest(t, req, http.StatusOK) } diff --git a/tests/integration/api_repo_get_contents_test.go b/tests/integration/api_repo_get_contents_test.go index a7de97c052..33df74f6ee 100644 --- a/tests/integration/api_repo_get_contents_test.go +++ b/tests/integration/api_repo_get_contents_test.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "net/url" + "slices" "testing" "time" @@ -20,9 +21,9 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" repo_service "code.gitea.io/gitea/services/repository" - "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) *api.ContentsResponse { @@ -34,9 +35,9 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) Name: treePath, Path: treePath, SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", - LastCommitSHA: lastCommitSHA, - LastCommitterDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)), - LastAuthorDate: time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400)), + LastCommitSHA: util.ToPointer(lastCommitSHA), + LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))), + LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))), Type: "file", Size: 30, Encoding: util.ToPointer("base64"), @@ -54,7 +55,11 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string) } func TestAPIGetContents(t *testing.T) { - onGiteaRun(t, testAPIGetContents) + onGiteaRun(t, func(t *testing.T, u *url.URL) { + testAPIGetContentsRefFormats(t) + testAPIGetContents(t, u) + testAPIGetContentsExt(t) + }) } func testAPIGetContents(t *testing.T, u *url.URL) { @@ -76,30 +81,34 @@ func testAPIGetContents(t *testing.T, u *url.URL) { // Get the commit ID of the default branch gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo1) - assert.NoError(t, err) + require.NoError(t, err) defer gitRepo.Close() // Make a new branch in repo1 newBranch := "test_branch" err = repo_service.CreateNewBranch(git.DefaultContext, user2, repo1, gitRepo, repo1.DefaultBranch, newBranch) - assert.NoError(t, err) + require.NoError(t, err) commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch) - assert.NoError(t, err) + require.NoError(t, err) // Make a new tag in repo1 newTag := "test_tag" err = gitRepo.CreateTag(newTag, commitID) - assert.NoError(t, err) + require.NoError(t, err) /*** END SETUP ***/ + // not found + req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/no-such/file.md", user2.Name, repo1.Name) + resp := MakeRequest(t, req, http.StatusNotFound) + assert.Contains(t, resp.Body.String(), "object does not exist [id: , rel_path: no-such]") + // ref is default ref ref := repo1.DefaultBranch refType := "branch" - req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) - resp := MakeRequest(t, req, http.StatusOK) + req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) + resp = MakeRequest(t, req, http.StatusOK) var contentsResponse api.ContentsResponse DecodeJSON(t, resp, &contentsResponse) - assert.NotNil(t, contentsResponse) lastCommit, _ := gitRepo.GetCommitByPath("README.md") expectedContentsResponse := getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String()) assert.Equal(t, *expectedContentsResponse, contentsResponse) @@ -109,17 +118,15 @@ func testAPIGetContents(t *testing.T, u *url.URL) { req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsResponse) - assert.NotNil(t, contentsResponse) expectedContentsResponse = getExpectedContentsResponseForContents(repo1.DefaultBranch, refType, lastCommit.ID.String()) assert.Equal(t, *expectedContentsResponse, contentsResponse) - // ref is the branch we created above in setup + // ref is the branch we created above in setup ref = newBranch refType = "branch" req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsResponse) - assert.NotNil(t, contentsResponse) branchCommit, _ := gitRepo.GetBranchCommit(ref) lastCommit, _ = branchCommit.GetCommitByPath("README.md") expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String()) @@ -131,7 +138,6 @@ func testAPIGetContents(t *testing.T, u *url.URL) { req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsResponse) - assert.NotNil(t, contentsResponse) tagCommit, _ := gitRepo.GetTagCommit(ref) lastCommit, _ = tagCommit.GetCommitByPath("README.md") expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, lastCommit.ID.String()) @@ -143,7 +149,6 @@ func testAPIGetContents(t *testing.T, u *url.URL) { req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, ref) resp = MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &contentsResponse) - assert.NotNil(t, contentsResponse) expectedContentsResponse = getExpectedContentsResponseForContents(ref, refType, commitID) assert.Equal(t, *expectedContentsResponse, contentsResponse) @@ -168,9 +173,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) { MakeRequest(t, req, http.StatusOK) } -func TestAPIGetContentsRefFormats(t *testing.T) { - defer tests.PrepareTestEnv(t)() - +func testAPIGetContentsRefFormats(t *testing.T) { file := "README.md" sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" content := "# repo1\n\nDescription for repo1" @@ -203,3 +206,98 @@ func TestAPIGetContentsRefFormats(t *testing.T) { // FIXME: this is an incorrect behavior, non-existing branch falls back to default branch _ = MakeRequest(t, NewRequest(t, http.MethodGet, "/api/v1/repos/user2/repo1/raw/README.md?ref=no-such"), http.StatusOK) } + +func testAPIGetContentsExt(t *testing.T) { + session := loginUser(t, "user2") + token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + t.Run("DirContents", func(t *testing.T) { + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext?ref=sub-home-md-img-check") + resp := MakeRequest(t, req, http.StatusOK) + var contentsResponse api.ContentsExtResponse + DecodeJSON(t, resp, &contentsResponse) + assert.Nil(t, contentsResponse.FileContents) + assert.NotNil(t, contentsResponse.DirContents) + + req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/.?ref=sub-home-md-img-check") + resp = MakeRequest(t, req, http.StatusOK) + contentsResponse = api.ContentsExtResponse{} + DecodeJSON(t, resp, &contentsResponse) + assert.Nil(t, contentsResponse.FileContents) + assert.NotNil(t, contentsResponse.DirContents) + + req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs?ref=sub-home-md-img-check") + resp = MakeRequest(t, req, http.StatusOK) + contentsResponse = api.ContentsExtResponse{} + DecodeJSON(t, resp, &contentsResponse) + assert.Nil(t, contentsResponse.FileContents) + assert.Equal(t, "README.md", contentsResponse.DirContents[0].Name) + assert.Nil(t, contentsResponse.DirContents[0].Encoding) + assert.Nil(t, contentsResponse.DirContents[0].Content) + assert.Nil(t, contentsResponse.DirContents[0].LastCommitSHA) + assert.Nil(t, contentsResponse.DirContents[0].LastCommitMessage) + + // "includes=file_content" shouldn't affect directory listing + req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs?ref=sub-home-md-img-check&includes=file_content") + resp = MakeRequest(t, req, http.StatusOK) + contentsResponse = api.ContentsExtResponse{} + DecodeJSON(t, resp, &contentsResponse) + assert.Nil(t, contentsResponse.FileContents) + assert.Equal(t, "README.md", contentsResponse.DirContents[0].Name) + assert.Nil(t, contentsResponse.DirContents[0].Encoding) + assert.Nil(t, contentsResponse.DirContents[0].Content) + + req = NewRequestf(t, "GET", "/api/v1/repos/user2/lfs/contents-ext?includes=file_content,lfs_metadata").AddTokenAuth(token2) + resp = session.MakeRequest(t, req, http.StatusOK) + contentsResponse = api.ContentsExtResponse{} + DecodeJSON(t, resp, &contentsResponse) + assert.Nil(t, contentsResponse.FileContents) + respFileIdx := slices.IndexFunc(contentsResponse.DirContents, func(response *api.ContentsResponse) bool { return response.Name == "jpeg.jpg" }) + require.NotEqual(t, -1, respFileIdx) + respFile := contentsResponse.DirContents[respFileIdx] + assert.Equal(t, "jpeg.jpg", respFile.Name) + assert.Nil(t, respFile.Encoding) + assert.Nil(t, respFile.Content) + assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize) + assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid) + }) + t.Run("FileContents", func(t *testing.T) { + // by default, no file content or commit info is returned + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs/README.md?ref=sub-home-md-img-check") + resp := MakeRequest(t, req, http.StatusOK) + var contentsResponse api.ContentsExtResponse + DecodeJSON(t, resp, &contentsResponse) + assert.Nil(t, contentsResponse.DirContents) + assert.Equal(t, "README.md", contentsResponse.FileContents.Name) + assert.Nil(t, contentsResponse.FileContents.Encoding) + assert.Nil(t, contentsResponse.FileContents.Content) + assert.Nil(t, contentsResponse.FileContents.LastCommitSHA) + assert.Nil(t, contentsResponse.FileContents.LastCommitMessage) + + // file content is only returned when `includes=file_content` + req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/contents-ext/docs/README.md?ref=sub-home-md-img-check&includes=file_content,commit_metadata,commit_message") + resp = MakeRequest(t, req, http.StatusOK) + contentsResponse = api.ContentsExtResponse{} + DecodeJSON(t, resp, &contentsResponse) + assert.Nil(t, contentsResponse.DirContents) + assert.Equal(t, "README.md", contentsResponse.FileContents.Name) + assert.NotNil(t, contentsResponse.FileContents.Encoding) + assert.NotNil(t, contentsResponse.FileContents.Content) + assert.Equal(t, "4649299398e4d39a5c09eb4f534df6f1e1eb87cc", *contentsResponse.FileContents.LastCommitSHA) + assert.Equal(t, "Test how READMEs render images when found in a subfolder\n", *contentsResponse.FileContents.LastCommitMessage) + + req = NewRequestf(t, "GET", "/api/v1/repos/user2/lfs/contents-ext/jpeg.jpg?includes=file_content").AddTokenAuth(token2) + resp = session.MakeRequest(t, req, http.StatusOK) + contentsResponse = api.ContentsExtResponse{} + DecodeJSON(t, resp, &contentsResponse) + assert.Nil(t, contentsResponse.DirContents) + assert.NotNil(t, contentsResponse.FileContents) + respFile := contentsResponse.FileContents + assert.Equal(t, "jpeg.jpg", respFile.Name) + assert.NotNil(t, respFile.Encoding) + assert.NotNil(t, respFile.Content) + assert.Nil(t, contentsResponse.FileContents.LastCommitSHA) + assert.Nil(t, contentsResponse.FileContents.LastCommitMessage) + assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize) + assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid) + }) +} diff --git a/tests/integration/api_repo_languages_test.go b/tests/integration/api_repo_languages_test.go index 1045aef57d..6347a43b4e 100644 --- a/tests/integration/api_repo_languages_test.go +++ b/tests/integration/api_repo_languages_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) @@ -32,7 +34,8 @@ func TestRepoLanguages(t *testing.T) { "content": "package main", "commit_choice": "direct", }) - session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.RedirectURL(resp)) // let gitea calculate language stats time.Sleep(time.Second) diff --git a/tests/integration/api_repo_lfs_test.go b/tests/integration/api_repo_lfs_test.go index 6b42b83bc5..ec6a3a3b57 100644 --- a/tests/integration/api_repo_lfs_test.go +++ b/tests/integration/api_repo_lfs_test.go @@ -20,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -226,9 +227,7 @@ func TestAPILFSBatch(t *testing.T) { t.Run("FileTooBig", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - - oldMaxFileSize := setting.LFS.MaxFileSize - setting.LFS.MaxFileSize = 2 + defer test.MockVariableValue(&setting.LFS.MaxFileSize, 2)() req := newRequest(t, &lfs.BatchRequest{ Operation: "upload", @@ -243,8 +242,6 @@ func TestAPILFSBatch(t *testing.T) { assert.NotNil(t, br.Objects[0].Error) assert.Equal(t, http.StatusUnprocessableEntity, br.Objects[0].Error.Code) assert.Equal(t, "Size must be less than or equal to 2", br.Objects[0].Error.Message) - - setting.LFS.MaxFileSize = oldMaxFileSize }) t.Run("AddMeta", func(t *testing.T) { diff --git a/tests/integration/api_repo_license_test.go b/tests/integration/api_repo_license_test.go index 52d3085694..fb4450a2bd 100644 --- a/tests/integration/api_repo_license_test.go +++ b/tests/integration/api_repo_license_test.go @@ -12,12 +12,13 @@ import ( auth_model "code.gitea.io/gitea/models/auth" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" "github.com/stretchr/testify/assert" ) var testLicenseContent = ` -Copyright (c) 2024 Gitea +Copyright (c) 2024 Gitea Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -48,7 +49,8 @@ func TestAPIRepoLicense(t *testing.T) { "content": testLicenseContent, "commit_choice": "direct", }) - session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.RedirectURL(resp)) // let gitea update repo license time.Sleep(time.Second) diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 672c2a2c8b..a2c3a467c6 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -586,7 +586,7 @@ func TestAPIRepoTransfer(t *testing.T) { // cleanup repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}) - _ = repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repo.ID) + _ = repo_service.DeleteRepositoryDirectly(db.DefaultContext, repo.ID) } func transfer(t *testing.T) *repo_model.Repository { diff --git a/tests/integration/api_repo_variables_test.go b/tests/integration/api_repo_variables_test.go index 7847962b07..b5c88af279 100644 --- a/tests/integration/api_repo_variables_test.go +++ b/tests/integration/api_repo_variables_test.go @@ -35,11 +35,11 @@ func TestAPIRepoVariables(t *testing.T) { }, { Name: "_", - ExpectedStatus: http.StatusNoContent, + ExpectedStatus: http.StatusCreated, }, { Name: "TEST_VAR", - ExpectedStatus: http.StatusNoContent, + ExpectedStatus: http.StatusCreated, }, { Name: "test_var", @@ -81,7 +81,7 @@ func TestAPIRepoVariables(t *testing.T) { req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ Value: "initial_val", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusNoContent) + MakeRequest(t, req, http.StatusCreated) cases := []struct { Name string @@ -138,7 +138,7 @@ func TestAPIRepoVariables(t *testing.T) { req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ Value: "initial_val", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusNoContent) + MakeRequest(t, req, http.StatusCreated) req = NewRequest(t, "DELETE", url).AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) diff --git a/tests/integration/api_user_variables_test.go b/tests/integration/api_user_variables_test.go index 367b83e7d4..d430c9e21d 100644 --- a/tests/integration/api_user_variables_test.go +++ b/tests/integration/api_user_variables_test.go @@ -29,11 +29,11 @@ func TestAPIUserVariables(t *testing.T) { }, { Name: "_", - ExpectedStatus: http.StatusNoContent, + ExpectedStatus: http.StatusCreated, }, { Name: "TEST_VAR", - ExpectedStatus: http.StatusNoContent, + ExpectedStatus: http.StatusCreated, }, { Name: "test_var", @@ -75,7 +75,7 @@ func TestAPIUserVariables(t *testing.T) { req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ Value: "initial_val", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusNoContent) + MakeRequest(t, req, http.StatusCreated) cases := []struct { Name string @@ -132,7 +132,7 @@ func TestAPIUserVariables(t *testing.T) { req := NewRequestWithJSON(t, "POST", url, api.CreateVariableOption{ Value: "initial_val", }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusNoContent) + MakeRequest(t, req, http.StatusCreated) req = NewRequest(t, "DELETE", url).AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) diff --git a/tests/integration/auth_ldap_test.go b/tests/integration/auth_ldap_test.go index c00e88b88b..24f0c03bed 100644 --- a/tests/integration/auth_ldap_test.go +++ b/tests/integration/auth_ldap_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" @@ -437,8 +438,8 @@ func TestLDAPGroupTeamSyncAddMember(t *testing.T) { Name: gitLDAPUser.UserName, }) usersOrgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: user.ID, - IncludePrivate: true, + UserID: user.ID, + IncludeVisibility: structs.VisibleTypePrivate, }) assert.NoError(t, err) allOrgTeams, err := organization.GetUserOrgTeams(db.DefaultContext, org.ID, user.ID) diff --git a/tests/integration/cmd_keys_test.go b/tests/integration/cmd_keys_test.go index 61f11c58b0..3878302ef0 100644 --- a/tests/integration/cmd_keys_test.go +++ b/tests/integration/cmd_keys_test.go @@ -13,7 +13,7 @@ import ( "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" - "github.com/urfave/cli/v2" + "github.com/urfave/cli/v3" ) func Test_CmdKeys(t *testing.T) { @@ -36,18 +36,21 @@ func Test_CmdKeys(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - out := new(bytes.Buffer) - app := cli.NewApp() - app.Writer = out - app.Commands = []*cli.Command{cmd.CmdKeys} + var stdout, stderr bytes.Buffer + app := &cli.Command{ + Writer: &stdout, + ErrWriter: &stderr, + Commands: []*cli.Command{cmd.CmdKeys}, + } cmd.CmdKeys.HideHelp = true - err := app.Run(append([]string{"prog"}, tt.args...)) + err := app.Run(t.Context(), append([]string{"prog"}, tt.args...)) if tt.wantErr { assert.Error(t, err) + assert.Equal(t, tt.expectedOutput, stderr.String()) } else { assert.NoError(t, err) + assert.Equal(t, tt.expectedOutput, stdout.String()) } - assert.Equal(t, tt.expectedOutput, out.String()) }) } }) diff --git a/tests/integration/editor_test.go b/tests/integration/editor_test.go index a5936d86de..ac47ed0094 100644 --- a/tests/integration/editor_test.go +++ b/tests/integration/editor_test.go @@ -7,6 +7,7 @@ import ( "bytes" "fmt" "io" + "maps" "mime/multipart" "net/http" "net/http/httptest" @@ -19,289 +20,278 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestCreateFile(t *testing.T) { +func TestEditor(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content") + sessionUser2 := loginUser(t, "user2") + t.Run("EditFileNotAllowed", testEditFileNotAllowed) + t.Run("DiffPreview", testEditorDiffPreview) + t.Run("CreateFile", testEditorCreateFile) + t.Run("EditFile", func(t *testing.T) { + testEditFile(t, sessionUser2, "user2", "repo1", "master", "README.md", "Hello, World (direct)\n") + testEditFileToNewBranch(t, sessionUser2, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (commit-to-new-branch)\n") + }) + t.Run("PatchFile", testEditorPatchFile) + t.Run("DeleteFile", func(t *testing.T) { + viewLink := "/user2/repo1/src/branch/branch2/README.md" + sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusOK) + testEditorActionPostRequest(t, sessionUser2, "/user2/repo1/_delete/branch2/README.md", map[string]string{"commit_choice": "direct"}) + sessionUser2.MakeRequest(t, NewRequest(t, "GET", viewLink), http.StatusNotFound) + }) + t.Run("ForkToEditFile", func(t *testing.T) { + testForkToEditFile(t, loginUser(t, "user4"), "user4", "user2", "repo1", "master", "README.md") + }) + t.Run("WebGitCommitEmail", testEditorWebGitCommitEmail) + t.Run("ProtectedBranch", testEditorProtectedBranch) }) } -func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) *httptest.ResponseRecorder { - // Request editor page - newURL := fmt.Sprintf("/%s/%s/_new/%s/", user, repo, branch) - req := NewRequest(t, "GET", newURL) - resp := session.MakeRequest(t, req, http.StatusOK) - - doc := NewHTMLParser(t, resp.Body) - lastCommit := doc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) +func testEditorCreateFile(t *testing.T) { + session := loginUser(t, "user2") + testCreateFile(t, session, "user2", "repo1", "master", "test.txt", "Content") + testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{ + "tree_path": "test.txt", + "commit_choice": "direct", + "new_branch_name": "master", + }, `A file named "test.txt" already exists in this repository.`) + testEditorActionPostRequestError(t, session, "/user2/repo1/_new/master/", map[string]string{ + "tree_path": "test.txt", + "commit_choice": "commit-to-new-branch", + "new_branch_name": "master", + }, `Branch "master" already exists in this repository.`) +} - // Save new file to master branch - req = NewRequestWithValues(t, "POST", newURL, map[string]string{ - "_csrf": doc.GetCSRF(), - "last_commit": lastCommit, +func testCreateFile(t *testing.T, session *TestSession, user, repo, branch, filePath, content string) { + testEditorActionEdit(t, session, user, repo, "_new", branch, "", map[string]string{ "tree_path": filePath, "content": content, "commit_choice": "direct", }) - return session.MakeRequest(t, req, http.StatusSeeOther) } -func TestCreateFileOnProtectedBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - - csrf := GetUserCSRFToken(t, session) - // Change master branch to protected - req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ - "_csrf": csrf, - "rule_name": "master", - "enable_push": "true", - }) - session.MakeRequest(t, req, http.StatusSeeOther) - // Check if master branch has been locked successfully - flashMsg := session.GetCookieFlashMessage() - assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg) - - // Request editor page - req = NewRequest(t, "GET", "/user2/repo1/_new/master/") - resp := session.MakeRequest(t, req, http.StatusOK) - - doc := NewHTMLParser(t, resp.Body) - lastCommit := doc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) - - // Save new file to master branch - req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{ - "_csrf": doc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": "test.txt", - "content": "Content", - "commit_choice": "direct", - }) - - resp = session.MakeRequest(t, req, http.StatusOK) - // Check body for error message - assert.Contains(t, resp.Body.String(), "Cannot commit to protected branch "master".") - - // remove the protected branch - csrf = GetUserCSRFToken(t, session) - - // Change master branch to protected - req = NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/1/delete", map[string]string{ - "_csrf": csrf, - }) - - resp = session.MakeRequest(t, req, http.StatusOK) - - res := make(map[string]string) - assert.NoError(t, json.NewDecoder(resp.Body).Decode(&res)) - assert.Equal(t, "/user2/repo1/settings/branches", res["redirect"]) - - // Check if master branch has been locked successfully - flashMsg = session.GetCookieFlashMessage() - assert.Equal(t, `Removing branch protection rule "1" failed.`, flashMsg.ErrorMsg) +func testEditorProtectedBranch(t *testing.T) { + session := loginUser(t, "user2") + // Change the "master" branch to "protected" + req := NewRequestWithValues(t, "POST", "/user2/repo1/settings/branches/edit", map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "rule_name": "master", + "enable_push": "true", }) + session.MakeRequest(t, req, http.StatusSeeOther) + flashMsg := session.GetCookieFlashMessage() + assert.Equal(t, `Branch protection for rule "master" has been updated.`, flashMsg.SuccessMsg) + + // Try to commit a file to the "master" branch and it should fail + resp := testEditorActionPostRequest(t, session, "/user2/repo1/_new/master/", map[string]string{"tree_path": "test-protected-branch.txt", "commit_choice": "direct"}) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, `Cannot commit to protected branch "master".`, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) } -func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) *httptest.ResponseRecorder { - // Get to the 'edit this file' page - req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath)) +func testEditorActionPostRequest(t *testing.T, session *TestSession, requestPath string, params map[string]string) *httptest.ResponseRecorder { + req := NewRequest(t, "GET", requestPath) resp := session.MakeRequest(t, req, http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - lastCommit := htmlDoc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) - - // Submit the edits - req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath), - map[string]string{ - "_csrf": htmlDoc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": filePath, - "content": newContent, - "commit_choice": "direct", - }, - ) - session.MakeRequest(t, req, http.StatusSeeOther) + form := map[string]string{ + "_csrf": htmlDoc.GetCSRF(), + "last_commit": htmlDoc.GetInputValueByName("last_commit"), + } + maps.Copy(form, params) + req = NewRequestWithValues(t, "POST", requestPath, form) + return session.MakeRequest(t, req, NoExpectedStatus) +} - // Verify the change - req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", branch, filePath)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, newContent, resp.Body.String()) +func testEditorActionPostRequestError(t *testing.T, session *TestSession, requestPath string, params map[string]string, errorMessage string) { + resp := testEditorActionPostRequest(t, session, requestPath, params) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, errorMessage, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) +} +func testEditorActionEdit(t *testing.T, session *TestSession, user, repo, editorAction, branch, filePath string, params map[string]string) *httptest.ResponseRecorder { + params["tree_path"] = util.IfZero(params["tree_path"], filePath) + newBranchName := util.Iif(params["commit_choice"] == "direct", branch, params["new_branch_name"]) + resp := testEditorActionPostRequest(t, session, fmt.Sprintf("/%s/%s/%s/%s/%s", user, repo, editorAction, branch, filePath), params) + assert.Equal(t, http.StatusOK, resp.Code) + assert.NotEmpty(t, test.RedirectURL(resp)) + req := NewRequest(t, "GET", path.Join(user, repo, "raw/branch", newBranchName, params["tree_path"])) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, params["content"], resp.Body.String()) return resp } -func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) *httptest.ResponseRecorder { - // Get to the 'edit this file' page - req := NewRequest(t, "GET", path.Join(user, repo, "_edit", branch, filePath)) - resp := session.MakeRequest(t, req, http.StatusOK) - - htmlDoc := NewHTMLParser(t, resp.Body) - lastCommit := htmlDoc.GetInputValueByName("last_commit") - assert.NotEmpty(t, lastCommit) - - // Submit the edits - req = NewRequestWithValues(t, "POST", path.Join(user, repo, "_edit", branch, filePath), - map[string]string{ - "_csrf": htmlDoc.GetCSRF(), - "last_commit": lastCommit, - "tree_path": filePath, - "content": newContent, - "commit_choice": "commit-to-new-branch", - "new_branch_name": targetBranch, - }, - ) - session.MakeRequest(t, req, http.StatusSeeOther) - - // Verify the change - req = NewRequest(t, "GET", path.Join(user, repo, "raw/branch", targetBranch, filePath)) - resp = session.MakeRequest(t, req, http.StatusOK) - assert.Equal(t, newContent, resp.Body.String()) +func testEditFile(t *testing.T, session *TestSession, user, repo, branch, filePath, newContent string) { + testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{ + "content": newContent, + "commit_choice": "direct", + }) +} - return resp +func testEditFileToNewBranch(t *testing.T, session *TestSession, user, repo, branch, targetBranch, filePath, newContent string) { + testEditorActionEdit(t, session, user, repo, "_edit", branch, filePath, map[string]string{ + "content": newContent, + "commit_choice": "commit-to-new-branch", + "new_branch_name": targetBranch, + }) } -func TestEditFile(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - testEditFile(t, session, "user2", "repo1", "master", "README.md", "Hello, World (Edited)\n") +func testEditorDiffPreview(t *testing.T) { + session := loginUser(t, "user2") + req := NewRequestWithValues(t, "POST", "/user2/repo1/_preview/master/README.md", map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "content": "Hello, World (Edited)\n", }) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), `<span class="added-code">Hello, World (Edited)</span>`) } -func TestEditFileToNewBranch(t *testing.T) { - onGiteaRun(t, func(t *testing.T, u *url.URL) { - session := loginUser(t, "user2") - testEditFileToNewBranch(t, session, "user2", "repo1", "master", "feature/test", "README.md", "Hello, World (Edited)\n") +func testEditorPatchFile(t *testing.T) { + session := loginUser(t, "user2") + pathContentCommon := `diff --git a/patch-file-1.txt b/patch-file-1.txt +new file mode 100644 +index 0000000000..aaaaaaaaaa +--- /dev/null ++++ b/patch-file-1.txt +@@ -0,0 +1 @@ ++` + testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/master/", map[string]string{ + "content": pathContentCommon + "patched content\n", + "commit_choice": "commit-to-new-branch", + "new_branch_name": "patched-branch", + }) + resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/raw/branch/patched-branch/patch-file-1.txt"), http.StatusOK) + assert.Equal(t, "patched content\n", resp.Body.String()) + + // patch again, it should fail + resp = testEditorActionPostRequest(t, session, "/user2/repo1/_diffpatch/patched-branch/", map[string]string{ + "content": pathContentCommon + "another patched content\n", + "commit_choice": "commit-to-new-branch", + "new_branch_name": "patched-branch-1", }) + assert.Equal(t, "Unable to apply patch", test.ParseJSONError(resp.Body.Bytes()).ErrorMessage) } -func TestWebGitCommitEmail(t *testing.T) { - onGiteaRun(t, func(t *testing.T, _ *url.URL) { - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - require.True(t, user.KeepEmailPrivate) - - repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath()) - defer gitRepo.Close() - getLastCommit := func(t *testing.T) *git.Commit { - c, err := gitRepo.GetBranchCommit("master") - require.NoError(t, err) - return c +func testEditorWebGitCommitEmail(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + require.True(t, user.KeepEmailPrivate) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + gitRepo, _ := git.OpenRepository(git.DefaultContext, repo1.RepoPath()) + defer gitRepo.Close() + getLastCommit := func(t *testing.T) *git.Commit { + c, err := gitRepo.GetBranchCommit("master") + require.NoError(t, err) + return c + } + + session := loginUser(t, user.Name) + + makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder { + lastCommit := getLastCommit(t) + params["_csrf"] = GetUserCSRFToken(t, session) + params["last_commit"] = lastCommit.ID.String() + params["commit_choice"] = "direct" + req := NewRequestWithValues(t, "POST", link, params) + resp := session.MakeRequest(t, req, NoExpectedStatus) + newCommit := getLastCommit(t) + if expectedUserName == "" { + require.Equal(t, lastCommit.ID.String(), newCommit.ID.String()) + respErr := test.ParseJSONError(resp.Body.Bytes()) + assert.Equal(t, translation.NewLocale("en-US").TrString("repo.editor.invalid_commit_email"), respErr.ErrorMessage) + } else { + require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String()) + assert.Equal(t, expectedUserName, newCommit.Author.Name) + assert.Equal(t, expectedEmail, newCommit.Author.Email) + assert.Equal(t, expectedUserName, newCommit.Committer.Name) + assert.Equal(t, expectedEmail, newCommit.Committer.Email) } + return resp + } + + uploadFile := func(t *testing.T, name, content string) string { + body := &bytes.Buffer{} + uploadForm := multipart.NewWriter(body) + file, _ := uploadForm.CreateFormFile("file", name) + _, _ = io.Copy(file, strings.NewReader(content)) + _ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session)) + _ = uploadForm.Close() + + req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body) + req.Header.Add("Content-Type", uploadForm.FormDataContentType()) + resp := session.MakeRequest(t, req, http.StatusOK) - session := loginUser(t, user.Name) - - makeReq := func(t *testing.T, link string, params map[string]string, expectedUserName, expectedEmail string) *httptest.ResponseRecorder { - lastCommit := getLastCommit(t) - params["_csrf"] = GetUserCSRFToken(t, session) - params["last_commit"] = lastCommit.ID.String() - params["commit_choice"] = "direct" - req := NewRequestWithValues(t, "POST", link, params) - resp := session.MakeRequest(t, req, NoExpectedStatus) - newCommit := getLastCommit(t) - if expectedUserName == "" { - require.Equal(t, lastCommit.ID.String(), newCommit.ID.String()) - htmlDoc := NewHTMLParser(t, resp.Body) - errMsg := htmlDoc.doc.Find(".ui.negative.message").Text() - assert.Contains(t, errMsg, translation.NewLocale("en-US").Tr("repo.editor.invalid_commit_email")) - } else { - require.NotEqual(t, lastCommit.ID.String(), newCommit.ID.String()) - assert.Equal(t, expectedUserName, newCommit.Author.Name) - assert.Equal(t, expectedEmail, newCommit.Author.Email) - assert.Equal(t, expectedUserName, newCommit.Committer.Name) - assert.Equal(t, expectedEmail, newCommit.Committer.Email) - } - return resp - } + respMap := map[string]string{} + DecodeJSON(t, resp, &respMap) + return respMap["uuid"] + } + + t.Run("EmailInactive", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}) + require.False(t, email.IsActivated) + makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ + "tree_path": "README.md", + "content": "test content", + "commit_email": email.Email, + }, "", "") + }) - uploadFile := func(t *testing.T, name, content string) string { - body := &bytes.Buffer{} - uploadForm := multipart.NewWriter(body) - file, _ := uploadForm.CreateFormFile("file", name) - _, _ = io.Copy(file, strings.NewReader(content)) - _ = uploadForm.WriteField("_csrf", GetUserCSRFToken(t, session)) - _ = uploadForm.Close() - - req := NewRequestWithBody(t, "POST", "/user2/repo1/upload-file", body) - req.Header.Add("Content-Type", uploadForm.FormDataContentType()) - resp := session.MakeRequest(t, req, http.StatusOK) - - respMap := map[string]string{} - DecodeJSON(t, resp, &respMap) - return respMap["uuid"] - } + t.Run("EmailInvalid", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true}) + require.NotEqual(t, email.UID, user.ID) + makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ + "tree_path": "README.md", + "content": "test content", + "commit_email": email.Email, + }, "", "") + }) - t.Run("EmailInactive", func(t *testing.T) { + testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) { + t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 35, UID: user.ID}) - require.False(t, email.IsActivated) - makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ - "tree_path": "README.md", - "content": "test content", - "commit_email": email.Email, - }, "", "") + paramsForKeepPrivate["commit_email"] = "" + resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org") }) - - t.Run("EmailInvalid", func(t *testing.T) { + t.Run("ChooseEmail", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{ID: 1, IsActivated: true}) - require.NotEqual(t, email.UID, user.ID) - makeReq(t, "/user2/repo1/_edit/master/README.md", map[string]string{ - "tree_path": "README.md", - "content": "test content", - "commit_email": email.Email, - }, "", "") - }) - - testWebGit := func(t *testing.T, linkForKeepPrivate string, paramsForKeepPrivate map[string]string, linkForChosenEmail string, paramsForChosenEmail map[string]string) (resp1, resp2 *httptest.ResponseRecorder) { - t.Run("DefaultEmailKeepPrivate", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - paramsForKeepPrivate["commit_email"] = "" - resp1 = makeReq(t, linkForKeepPrivate, paramsForKeepPrivate, "User Two", "user2@noreply.example.org") - }) - t.Run("ChooseEmail", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - paramsForChosenEmail["commit_email"] = "user2@example.com" - resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com") - }) - return resp1, resp2 - } - - t.Run("Edit", func(t *testing.T) { - testWebGit(t, - "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"}, - "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"}, - ) + paramsForChosenEmail["commit_email"] = "user2@example.com" + resp2 = makeReq(t, linkForChosenEmail, paramsForChosenEmail, "User Two", "user2@example.com") }) + return resp1, resp2 + } + + t.Run("Edit", func(t *testing.T) { + testWebGit(t, + "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for keep private"}, + "/user2/repo1/_edit/master/README.md", map[string]string{"tree_path": "README.md", "content": "for chosen email"}, + ) + }) - t.Run("UploadDelete", func(t *testing.T) { - file1UUID := uploadFile(t, "file1", "File 1") - file2UUID := uploadFile(t, "file2", "File 2") - testWebGit(t, - "/user2/repo1/_upload/master", map[string]string{"files": file1UUID}, - "/user2/repo1/_upload/master", map[string]string{"files": file2UUID}, - ) - testWebGit(t, - "/user2/repo1/_delete/master/file1", map[string]string{}, - "/user2/repo1/_delete/master/file2", map[string]string{}, - ) - }) + t.Run("UploadDelete", func(t *testing.T) { + file1UUID := uploadFile(t, "file1", "File 1") + file2UUID := uploadFile(t, "file2", "File 2") + testWebGit(t, + "/user2/repo1/_upload/master", map[string]string{"files": file1UUID}, + "/user2/repo1/_upload/master", map[string]string{"files": file2UUID}, + ) + testWebGit(t, + "/user2/repo1/_delete/master/file1", map[string]string{}, + "/user2/repo1/_delete/master/file2", map[string]string{}, + ) + }) - t.Run("ApplyPatchCherryPick", func(t *testing.T) { - testWebGit(t, - "/user2/repo1/_diffpatch/master", map[string]string{ - "tree_path": "__dummy__", - "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt + t.Run("ApplyPatchCherryPick", func(t *testing.T) { + testWebGit(t, + "/user2/repo1/_diffpatch/master", map[string]string{ + "tree_path": "__dummy__", + "content": `diff --git a/patch-file-1.txt b/patch-file-1.txt new file mode 100644 index 0000000000..aaaaaaaaaa --- /dev/null @@ -309,10 +299,10 @@ index 0000000000..aaaaaaaaaa @@ -0,0 +1 @@ +File 1 `, - }, - "/user2/repo1/_diffpatch/master", map[string]string{ - "tree_path": "__dummy__", - "content": `diff --git a/patch-file-2.txt b/patch-file-2.txt + }, + "/user2/repo1/_diffpatch/master", map[string]string{ + "tree_path": "__dummy__", + "content": `diff --git a/patch-file-2.txt b/patch-file-2.txt new file mode 100644 index 0000000000..bbbbbbbbbb --- /dev/null @@ -320,20 +310,146 @@ index 0000000000..bbbbbbbbbb @@ -0,0 +1 @@ +File 2 `, - }, - ) - - commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt") - require.NoError(t, err) - commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt") - require.NoError(t, err) - resp1, _ := testWebGit(t, - "/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"}, - "/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"}, - ) - - // By the way, test the "cherrypick" page: a successful revert redirects to the main branch - assert.Equal(t, "/user2/repo1/src/branch/master", resp1.Header().Get("Location")) - }) + }, + ) + + commit1, err := gitRepo.GetCommitByPath("patch-file-1.txt") + require.NoError(t, err) + commit2, err := gitRepo.GetCommitByPath("patch-file-2.txt") + require.NoError(t, err) + resp1, _ := testWebGit(t, + "/user2/repo1/_cherrypick/"+commit1.ID.String()+"/master", map[string]string{"revert": "true"}, + "/user2/repo1/_cherrypick/"+commit2.ID.String()+"/master", map[string]string{"revert": "true"}, + ) + + // By the way, test the "cherrypick" page: a successful revert redirects to the main branch + assert.Equal(t, "/user2/repo1/src/branch/master", test.RedirectURL(resp1)) }) } + +func testForkToEditFile(t *testing.T, session *TestSession, user, owner, repo, branch, filePath string) { + forkToEdit := func(t *testing.T, session *TestSession, owner, repo, operation, branch, filePath string) { + // visit the base repo, see the "Add File" button + req := NewRequest(t, "GET", path.Join(owner, repo)) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + AssertHTMLElement(t, htmlDoc, ".repo-add-file", 1) + + // attempt to edit a file, see the guideline page + req = NewRequest(t, "GET", path.Join(owner, repo, operation, branch, filePath)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "Fork Repository to Propose Changes") + + // fork the repository + req = NewRequestWithValues(t, "POST", path.Join(owner, repo, "_fork", branch), map[string]string{"_csrf": GetUserCSRFToken(t, session)}) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.JSONEq(t, `{"redirect":""}`, resp.Body.String()) + } + + t.Run("ForkButArchived", func(t *testing.T) { + // Fork repository because we can't edit it + forkToEdit(t, session, owner, repo, "_edit", branch, filePath) + + // Archive the repository + req := NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"), + map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "repo_name": repo, + "action": "archive", + }, + ) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Check editing archived repository is disabled + req = NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)).SetHeader("Accept", "text/html") + resp := session.MakeRequest(t, req, http.StatusNotFound) + assert.Contains(t, resp.Body.String(), "You have forked this repository but your fork is not editable.") + + // Unfork the repository + req = NewRequestWithValues(t, "POST", path.Join(user, repo, "settings"), + map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "repo_name": repo, + "action": "convert_fork", + }, + ) + session.MakeRequest(t, req, http.StatusSeeOther) + }) + + // Fork repository again, and check the existence of the forked repo with unique name + forkToEdit(t, session, owner, repo, "_edit", branch, filePath) + session.MakeRequest(t, NewRequestf(t, "GET", "/%s/%s-1", user, repo), http.StatusOK) + + t.Run("CheckBaseRepoForm", func(t *testing.T) { + // the base repo's edit form should have the correct action and upload links (pointing to the forked repo) + req := NewRequest(t, "GET", path.Join(owner, repo, "_upload", branch, filePath)+"?foo=bar") + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + + uploadForm := htmlDoc.doc.Find(".form-fetch-action") + formAction := uploadForm.AttrOr("action", "") + assert.Equal(t, fmt.Sprintf("/%s/%s-1/_upload/%s/%s?from_base_branch=%s&foo=bar", user, repo, branch, filePath, branch), formAction) + uploadLink := uploadForm.Find(".dropzone").AttrOr("data-link-url", "") + assert.Equal(t, fmt.Sprintf("/%s/%s-1/upload-file", user, repo), uploadLink) + newBranchName := uploadForm.Find("input[name=new_branch_name]").AttrOr("value", "") + assert.Equal(t, user+"-patch-1", newBranchName) + commitChoice := uploadForm.Find("input[name=commit_choice][checked]").AttrOr("value", "") + assert.Equal(t, "commit-to-new-branch", commitChoice) + lastCommit := uploadForm.Find("input[name=last_commit]").AttrOr("value", "") + assert.NotEmpty(t, lastCommit) + }) + + t.Run("ViewBaseEditFormAndCommitToFork", func(t *testing.T) { + req := NewRequest(t, "GET", path.Join(owner, repo, "_edit", branch, filePath)) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + editRequestForm := map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "last_commit": htmlDoc.GetInputValueByName("last_commit"), + "tree_path": filePath, + "content": "new content in fork", + "commit_choice": "commit-to-new-branch", + } + // change a file in the forked repo with existing branch name (should fail) + editRequestForm["new_branch_name"] = "master" + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm) + resp = session.MakeRequest(t, req, http.StatusBadRequest) + respJSON := test.ParseJSONError(resp.Body.Bytes()) + assert.Equal(t, `Branch "master" already exists in your fork, please choose a new branch name.`, respJSON.ErrorMessage) + + // change a file in the forked repo (should succeed) + newBranchName := htmlDoc.GetInputValueByName("new_branch_name") + editRequestForm["new_branch_name"] = newBranchName + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s-1/_edit/%s/%s?from_base_branch=%s", user, repo, branch, filePath, branch), editRequestForm) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, fmt.Sprintf("/%s/%s/compare/%s...%s/%s-1:%s", owner, repo, branch, user, repo, newBranchName), test.RedirectURL(resp)) + + // check the file in the fork's branch is changed + req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s-1/src/branch/%s/%s", user, repo, newBranchName, filePath)) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Contains(t, resp.Body.String(), "new content in fork") + }) +} + +func testEditFileNotAllowed(t *testing.T) { + sessionUser1 := loginUser(t, "user1") // admin, all access + sessionUser4 := loginUser(t, "user4") + // "_cherrypick" has a different route pattern, so skip its test + operations := []string{"_new", "_edit", "_delete", "_upload", "_diffpatch"} + for _, operation := range operations { + t.Run(operation, func(t *testing.T) { + // Branch does not exist + targetLink := path.Join("user2", "repo1", operation, "missing", "README.md") + sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound) + + // Private repository + targetLink = path.Join("user2", "repo2", operation, "master", "Home.md") + sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusOK) + sessionUser4.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound) + + // Empty repository + targetLink = path.Join("org41", "repo61", operation, "master", "README.md") + sessionUser1.MakeRequest(t, NewRequest(t, "GET", targetLink), http.StatusNotFound) + }) + } +} diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go index 8cebfaf32a..6a8c70f12f 100644 --- a/tests/integration/empty_repo_test.go +++ b/tests/integration/empty_repo_test.go @@ -10,7 +10,6 @@ import ( "io" "mime/multipart" "net/http" - "net/http/httptest" "strings" "testing" @@ -30,7 +29,7 @@ import ( "github.com/stretchr/testify/require" ) -func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) *httptest.ResponseRecorder { +func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, treePath, content string) { url := fmt.Sprintf("/%s/%s/_new/%s", user, repo, branch) req := NewRequestWithValues(t, "POST", url, map[string]string{ "_csrf": GetUserCSRFToken(t, session), @@ -38,7 +37,8 @@ func testAPINewFile(t *testing.T, session *TestSession, user, repo, branch, tree "tree_path": treePath, "content": content, }) - return session.MakeRequest(t, req, http.StatusSeeOther) + resp := session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.RedirectURL(resp)) } func TestEmptyRepo(t *testing.T) { @@ -87,7 +87,7 @@ func TestEmptyRepoAddFile(t *testing.T) { "content": "newly-added-test-file", }) - resp = session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusOK) redirect := test.RedirectURL(resp) assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/test-file.md", redirect) @@ -154,9 +154,9 @@ func TestEmptyRepoUploadFile(t *testing.T) { "files": respMap["uuid"], "tree_path": "", }) - resp = session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusOK) redirect := test.RedirectURL(resp) - assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch+"/", redirect) + assert.Equal(t, "/user30/empty/src/branch/"+setting.Repository.DefaultBranch, redirect) req = NewRequest(t, "GET", redirect) resp = session.MakeRequest(t, req, http.StatusOK) diff --git a/tests/integration/ephemeral_actions_runner_deletion_test.go b/tests/integration/ephemeral_actions_runner_deletion_test.go index 765fcac8d7..40f8c643a8 100644 --- a/tests/integration/ephemeral_actions_runner_deletion_test.go +++ b/tests/integration/ephemeral_actions_runner_deletion_test.go @@ -50,9 +50,7 @@ func testEphemeralActionsRunnerDeletionByRepository(t *testing.T) { task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 52}) assert.Equal(t, actions_model.StatusRunning, task.Status) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - err = repo_service.DeleteRepositoryDirectly(t.Context(), user, task.RepoID, true) + err = repo_service.DeleteRepositoryDirectly(t.Context(), task.RepoID, true) assert.NoError(t, err) _, err = actions_model.GetRunnerByID(t.Context(), 34350) diff --git a/tests/integration/git_general_test.go b/tests/integration/git_general_test.go index ed60bdb58a..3b0f9589d2 100644 --- a/tests/integration/git_general_test.go +++ b/tests/integration/git_general_test.go @@ -26,6 +26,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/setting" @@ -713,7 +714,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { commitID := path.Base(commitURL) - addCommitStatus := func(status api.CommitStatusState) func(*testing.T) { + addCommitStatus := func(status commitstatus.CommitStatusState) func(*testing.T) { return doAPICreateCommitStatus(ctx, commitID, api.CreateStatusOption{ State: status, TargetURL: "http://test.ci/", @@ -723,7 +724,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { } // Call API to add Pending status for commit - t.Run("CreateStatus", addCommitStatus(api.CommitStatusPending)) + t.Run("CreateStatus", addCommitStatus(commitstatus.CommitStatusPending)) // Cancel not existing auto merge ctx.ExpectedCode = http.StatusNotFound @@ -752,7 +753,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { assert.False(t, pr.HasMerged) // Call API to add Failure status for commit - t.Run("CreateStatus", addCommitStatus(api.CommitStatusFailure)) + t.Run("CreateStatus", addCommitStatus(commitstatus.CommitStatusFailure)) // Check pr status pr, err = doAPIGetPullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t) @@ -760,7 +761,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) { assert.False(t, pr.HasMerged) // Call API to add Success status for commit - t.Run("CreateStatus", addCommitStatus(api.CommitStatusSuccess)) + t.Run("CreateStatus", addCommitStatus(commitstatus.CommitStatusSuccess)) // wait to let gitea merge stuff time.Sleep(time.Second) diff --git a/tests/integration/git_push_test.go b/tests/integration/git_push_test.go index bac7b4f48b..d716847b54 100644 --- a/tests/integration/git_push_test.go +++ b/tests/integration/git_push_test.go @@ -27,7 +27,7 @@ func TestGitPush(t *testing.T) { func testGitPush(t *testing.T, u *url.URL) { t.Run("Push branches at once", func(t *testing.T) { runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) { - for i := 0; i < 100; i++ { + for i := range 100 { branchName := fmt.Sprintf("branch-%d", i) pushed = append(pushed, branchName) doGitCreateBranch(gitPath, branchName)(t) @@ -40,7 +40,7 @@ func testGitPush(t *testing.T, u *url.URL) { t.Run("Push branches exists", func(t *testing.T) { runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) { - for i := 0; i < 10; i++ { + for i := range 10 { branchName := fmt.Sprintf("branch-%d", i) if i < 5 { pushed = append(pushed, branchName) @@ -54,7 +54,7 @@ func testGitPush(t *testing.T, u *url.URL) { pushed = pushed[:0] // do some changes for the first 5 branches created above - for i := 0; i < 5; i++ { + for i := range 5 { branchName := fmt.Sprintf("branch-%d", i) pushed = append(pushed, branchName) @@ -75,7 +75,7 @@ func testGitPush(t *testing.T, u *url.URL) { t.Run("Push branches one by one", func(t *testing.T) { runTestGitPush(t, u, func(t *testing.T, gitPath string) (pushed, deleted []string) { - for i := 0; i < 100; i++ { + for i := range 100 { branchName := fmt.Sprintf("branch-%d", i) doGitCreateBranch(gitPath, branchName)(t) doGitPushTestRepository(gitPath, "origin", branchName)(t) @@ -101,14 +101,14 @@ func testGitPush(t *testing.T, u *url.URL) { doGitPushTestRepository(gitPath, "origin", "master")(t) // make sure master is the default branch instead of a branch we are going to delete pushed = append(pushed, "master") - for i := 0; i < 100; i++ { + for i := range 100 { branchName := fmt.Sprintf("branch-%d", i) pushed = append(pushed, branchName) doGitCreateBranch(gitPath, branchName)(t) } doGitPushTestRepository(gitPath, "origin", "--all")(t) - for i := 0; i < 10; i++ { + for i := range 10 { branchName := fmt.Sprintf("branch-%d", i) doGitPushTestRepository(gitPath, "origin", "--delete", branchName)(t) deleted = append(deleted, branchName) @@ -191,7 +191,7 @@ func runTestGitPush(t *testing.T, u *url.URL, gitOperation func(t *testing.T, gi assert.Equal(t, commitID, branch.CommitID) } - require.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repo.ID)) + require.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, repo.ID)) } func TestPushPullRefs(t *testing.T) { diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_ssh_git_test.go index 32de200f63..56f9f87783 100644 --- a/tests/integration/gpg_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -4,7 +4,10 @@ package integration import ( + "crypto/ed25519" + "crypto/rand" "encoding/base64" + "encoding/pem" "fmt" "net/url" "os" @@ -23,6 +26,7 @@ import ( "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" ) func TestGPGGit(t *testing.T) { @@ -42,6 +46,37 @@ func TestGPGGit(t *testing.T) { defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() + testGitSigning(t) +} + +func TestSSHGit(t *testing.T) { + tmpDir := t.TempDir() // use a temp dir to store the SSH keys + err := os.Chmod(tmpDir, 0o700) + assert.NoError(t, err) + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err, "ed25519.GenerateKey") + sshPubKey, err := ssh.NewPublicKey(pub) + require.NoError(t, err, "ssh.NewPublicKey") + + err = os.WriteFile(tmpDir+"/id_ed25519.pub", ssh.MarshalAuthorizedKey(sshPubKey), 0o600) + require.NoError(t, err, "os.WriteFile id_ed25519.pub") + block, err := ssh.MarshalPrivateKey(priv, "") + require.NoError(t, err, "ssh.MarshalPrivateKey") + err = os.WriteFile(tmpDir+"/id_ed25519", pem.EncodeToMemory(block), 0o600) + require.NoError(t, err, "os.WriteFile id_ed25519") + + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, tmpDir+"/id_ed25519.pub")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningFormat, "ssh")() + defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() + defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() + + testGitSigning(t) +} + +func testGitSigning(t *testing.T) { username := "user2" user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) baseAPITestContext := NewAPITestContext(t, username, "repo1") diff --git a/tests/integration/html_helper.go b/tests/integration/html_helper.go index 874fc32228..4d589b32e7 100644 --- a/tests/integration/html_helper.go +++ b/tests/integration/html_helper.go @@ -42,7 +42,7 @@ func (doc *HTMLDoc) GetCSRF() string { return doc.GetInputValueByName("_csrf") } -// AssertHTMLElement check if element by selector exists or does not exist depending on checkExists +// AssertHTMLElement check if the element by selector exists or does not exist depending on checkExists func AssertHTMLElement[T int | bool](t testing.TB, doc *HTMLDoc, selector string, checkExists T) { sel := doc.doc.Find(selector) switch v := any(checkExists).(type) { diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index d5b7bb7a3e..21126b63c5 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -1,7 +1,7 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//nolint:forbidigo +//nolint:forbidigo // use of print functions is allowed in tests package integration import ( diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index b403b3cf17..b5dca58357 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -76,14 +76,11 @@ func TestViewIssuesSortByType(t *testing.T) { htmlDoc := NewHTMLParser(t, resp.Body) issuesSelection := getIssuesSelection(t, htmlDoc) - expectedNumIssues := unittest.GetCount(t, + expectedNumIssues := min(unittest.GetCount(t, &issues_model.Issue{RepoID: repo.ID, PosterID: user.ID}, unittest.Cond("is_closed=?", false), unittest.Cond("is_pull=?", false), - ) - if expectedNumIssues > setting.UI.IssuePagingNum { - expectedNumIssues = setting.UI.IssuePagingNum - } + ), setting.UI.IssuePagingNum) assert.Equal(t, expectedNumIssues, issuesSelection.Length()) issuesSelection.Each(func(_ int, selection *goquery.Selection) { @@ -151,6 +148,22 @@ func testNewIssue(t *testing.T, session *TestSession, user, repo, title, content return issueURL } +func testIssueDelete(t *testing.T, session *TestSession, issueURL string) { + req := NewRequestWithValues(t, "POST", path.Join(issueURL, "delete"), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + }) + session.MakeRequest(t, req, http.StatusSeeOther) +} + +func testIssueAssign(t *testing.T, session *TestSession, repoLink string, issueID, assigneeID int64) { + req := NewRequestWithValues(t, "POST", fmt.Sprintf(repoLink+"/issues/assignee?issue_ids=%d", issueID), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "id": strconv.FormatInt(assigneeID, 10), + "action": "", // empty action means assign + }) + session.MakeRequest(t, req, http.StatusOK) +} + func testIssueAddComment(t *testing.T, session *TestSession, issueURL, content, status string) int64 { req := NewRequest(t, "GET", issueURL) resp := session.MakeRequest(t, req, http.StatusOK) @@ -482,10 +495,7 @@ func TestSearchIssues(t *testing.T) { session := loginUser(t, "user2") - expectedIssueCount := 20 // from the fixtures - if expectedIssueCount > setting.UI.IssuePagingNum { - expectedIssueCount = setting.UI.IssuePagingNum - } + expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures link, _ := url.Parse("/issues/search") req := NewRequest(t, "GET", link.String()) @@ -576,10 +586,7 @@ func TestSearchIssues(t *testing.T) { func TestSearchIssuesWithLabels(t *testing.T) { defer tests.PrepareTestEnv(t)() - expectedIssueCount := 20 // from the fixtures - if expectedIssueCount > setting.UI.IssuePagingNum { - expectedIssueCount = setting.UI.IssuePagingNum - } + expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures session := loginUser(t, "user1") link, _ := url.Parse("/issues/search") diff --git a/tests/integration/lfs_view_test.go b/tests/integration/lfs_view_test.go index 64ffebaa78..c26ece22be 100644 --- a/tests/integration/lfs_view_test.go +++ b/tests/integration/lfs_view_test.go @@ -68,14 +68,15 @@ func TestLFSRender(t *testing.T) { req := NewRequest(t, "GET", "/user2/lfs/src/branch/master/crypt.bin") resp := session.MakeRequest(t, req, http.StatusOK) - doc := NewHTMLParser(t, resp.Body).doc + doc := NewHTMLParser(t, resp.Body) fileInfo := doc.Find("div.file-info-entry").First().Text() assert.Contains(t, fileInfo, "LFS") - rawLink, exists := doc.Find("div.file-view > div.view-raw > a").Attr("href") - assert.True(t, exists, "Download link should render instead of content because this is a binary file") - assert.Equal(t, "/user2/lfs/media/branch/master/crypt.bin", rawLink, "The download link should use the proper /media link because it's in LFS") + // find new file view container + fileViewContainer := doc.Find("[data-global-init=initRepoFileView]") + assert.Equal(t, "/user2/lfs/media/branch/master/crypt.bin", fileViewContainer.AttrOr("data-raw-file-link", "")) + AssertHTMLElement(t, doc, ".view-raw > .file-view-render-container > .file-view-raw-prompt", 1) }) // check that a directory with a README file shows its text diff --git a/tests/integration/org_count_test.go b/tests/integration/org_count_test.go index fb71e690c2..c48008e627 100644 --- a/tests/integration/org_count_test.go +++ b/tests/integration/org_count_test.go @@ -120,8 +120,8 @@ func doCheckOrgCounts(username string, orgCounts map[string]int, strict bool, ca }) orgs, err := db.Find[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ - UserID: user.ID, - IncludePrivate: true, + UserID: user.ID, + IncludeVisibility: api.VisibleTypePrivate, }) assert.NoError(t, err) diff --git a/tests/integration/org_test.go b/tests/integration/org_test.go index 9a93455858..3ed7baa5ba 100644 --- a/tests/integration/org_test.go +++ b/tests/integration/org_test.go @@ -10,12 +10,17 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestOrgRepos(t *testing.T) { @@ -40,7 +45,7 @@ func TestOrgRepos(t *testing.T) { sel := htmlDoc.doc.Find("a.name") assert.Len(t, repos, len(sel.Nodes)) - for i := 0; i < len(repos); i++ { + for i := range repos { assert.Equal(t, repos[i], strings.TrimSpace(sel.Eq(i).Text())) } } @@ -217,4 +222,32 @@ func TestTeamSearch(t *testing.T) { session = loginUser(t, user5.Name) req = NewRequestf(t, "GET", "/org/%s/teams/-/search?q=%s", org.Name, "team") session.MakeRequest(t, req, http.StatusNotFound) + + t.Run("SearchWithPermission", func(t *testing.T) { + ctx := t.Context() + const testOrgID int64 = 500 + const testRepoID int64 = 2000 + testTeam := &organization.Team{OrgID: testOrgID, LowerName: "test_team", AccessMode: perm.AccessModeNone} + require.NoError(t, db.Insert(ctx, testTeam)) + require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: testOrgID, TeamID: testTeam.ID, RepoID: testRepoID})) + require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: testOrgID, TeamID: testTeam.ID, Type: unit.TypeCode, AccessMode: perm.AccessModeRead})) + require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: testOrgID, TeamID: testTeam.ID, Type: unit.TypeIssues, AccessMode: perm.AccessModeWrite})) + + teams, err := organization.GetTeamsWithAccessToAnyRepoUnit(ctx, testOrgID, testRepoID, perm.AccessModeRead, unit.TypeCode, unit.TypeIssues) + require.NoError(t, err) + assert.Len(t, teams, 1) // can read "code" or "issues" + + teams, err = organization.GetTeamsWithAccessToAnyRepoUnit(ctx, testOrgID, testRepoID, perm.AccessModeWrite, unit.TypeCode) + require.NoError(t, err) + assert.Empty(t, teams) // cannot write "code" + + teams, err = organization.GetTeamsWithAccessToAnyRepoUnit(ctx, testOrgID, testRepoID, perm.AccessModeWrite, unit.TypeIssues) + require.NoError(t, err) + assert.Len(t, teams, 1) // can write "issues" + + _, _ = db.GetEngine(ctx).ID(testTeam.ID).Update(&organization.Team{AccessMode: perm.AccessModeWrite}) + teams, err = organization.GetTeamsWithAccessToAnyRepoUnit(ctx, testOrgID, testRepoID, perm.AccessModeWrite, unit.TypeCode) + require.NoError(t, err) + assert.Len(t, teams, 1) // team permission is "write", so can write "code" + }) } diff --git a/tests/integration/project_test.go b/tests/integration/project_test.go index 13213c254d..43a489d4c4 100644 --- a/tests/integration/project_test.go +++ b/tests/integration/project_test.go @@ -47,7 +47,7 @@ func TestMoveRepoProjectColumns(t *testing.T) { err := project_model.NewProject(db.DefaultContext, &project1) assert.NoError(t, err) - for i := 0; i < 3; i++ { + for i := range 3 { err = project_model.NewColumn(db.DefaultContext, &project_model.Column{ Title: fmt.Sprintf("column %d", i+1), ProjectID: project1.ID, diff --git a/tests/integration/pull_compare_test.go b/tests/integration/pull_compare_test.go index 86bdd1b9e3..f95a2f1690 100644 --- a/tests/integration/pull_compare_test.go +++ b/tests/integration/pull_compare_test.go @@ -13,7 +13,6 @@ import ( issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/test" repo_service "code.gitea.io/gitea/services/repository" "code.gitea.io/gitea/tests" @@ -76,10 +75,9 @@ func TestPullCompare(t *testing.T) { assert.Positive(t, editButtonCount, "Expected to find a button to edit a file in the PR diff view but there were none") repoForked := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) - user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // delete the head repository and revisit the PR diff view - err := repo_service.DeleteRepositoryDirectly(db.DefaultContext, user2, repoForked.ID) + err := repo_service.DeleteRepositoryDirectly(db.DefaultContext, repoForked.ID) assert.NoError(t, err) req = NewRequest(t, "GET", prFilesURL) @@ -161,7 +159,8 @@ func TestPullCompare_EnableAllowEditsFromMaintainer(t *testing.T) { "commit_summary": "user2 updated the file", "commit_choice": "direct", }) - user2Session.MakeRequest(t, req, http.StatusSeeOther) + resp = user2Session.MakeRequest(t, req, http.StatusOK) + assert.NotEmpty(t, test.RedirectURL(resp)) } } }) diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go index cf50d5e639..73b4c22070 100644 --- a/tests/integration/pull_merge_test.go +++ b/tests/integration/pull_merge_test.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/queue" @@ -768,7 +769,7 @@ func TestPullAutoMergeAfterCommitStatusSucceed(t *testing.T) { }() err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{ - State: api.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, TargetURL: "https://gitea.com", Context: "gitea/actions", }) @@ -848,7 +849,7 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApproval(t *testing.T) { }() err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{ - State: api.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, TargetURL: "https://gitea.com", Context: "gitea/actions", }) @@ -977,14 +978,12 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing. }() err = commitstatus_service.CreateCommitStatus(db.DefaultContext, baseRepo, user1, sha, &git_model.CommitStatus{ - State: api.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, TargetURL: "https://gitea.com", Context: "gitea/actions", }) assert.NoError(t, err) - time.Sleep(2 * time.Second) - // reload pr again pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) assert.False(t, pr.HasMerged) @@ -997,8 +996,6 @@ func TestPullAutoMergeAfterCommitStatusSucceedAndApprovalForAgitFlow(t *testing. htmlDoc := NewHTMLParser(t, resp.Body) testSubmitReview(t, approveSession, htmlDoc.GetCSRF(), "user2", "repo1", strconv.Itoa(int(pr.Index)), sha, "approve", http.StatusOK) - time.Sleep(2 * time.Second) - // realod pr again pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}) assert.True(t, pr.HasMerged) diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go index 4d43847f1b..49326a594a 100644 --- a/tests/integration/pull_status_test.go +++ b/tests/integration/pull_status_test.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" @@ -55,20 +56,20 @@ func TestPullCreate_CommitStatus(t *testing.T) { commitID := path.Base(commitURL) - statusList := []api.CommitStatusState{ - api.CommitStatusPending, - api.CommitStatusError, - api.CommitStatusFailure, - api.CommitStatusSuccess, - api.CommitStatusWarning, + statusList := []commitstatus.CommitStatusState{ + commitstatus.CommitStatusPending, + commitstatus.CommitStatusError, + commitstatus.CommitStatusFailure, + commitstatus.CommitStatusSuccess, + commitstatus.CommitStatusWarning, } - statesIcons := map[api.CommitStatusState]string{ - api.CommitStatusPending: "octicon-dot-fill", - api.CommitStatusSuccess: "octicon-check", - api.CommitStatusError: "gitea-exclamation", - api.CommitStatusFailure: "octicon-x", - api.CommitStatusWarning: "gitea-exclamation", + statesIcons := map[commitstatus.CommitStatusState]string{ + commitstatus.CommitStatusPending: "octicon-dot-fill", + commitstatus.CommitStatusSuccess: "octicon-check", + commitstatus.CommitStatusError: "gitea-exclamation", + commitstatus.CommitStatusFailure: "octicon-x", + commitstatus.CommitStatusWarning: "gitea-exclamation", } testCtx := NewAPITestContext(t, "user1", "repo1", auth_model.AccessTokenScopeWriteRepository) @@ -99,7 +100,7 @@ func TestPullCreate_CommitStatus(t *testing.T) { repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: "user1", Name: "repo1"}) css := unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: repo1.ID, SHA: commitID}) - assert.Equal(t, api.CommitStatusWarning, css.State) + assert.Equal(t, commitstatus.CommitStatusSuccess, css.State) }) } @@ -128,7 +129,7 @@ func TestPullCreate_EmptyChangesWithDifferentCommits(t *testing.T) { session := loginUser(t, "user1") testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "") testEditFileToNewBranch(t, session, "user1", "repo1", "master", "status1", "README.md", "status1") - testEditFileToNewBranch(t, session, "user1", "repo1", "status1", "status1", "README.md", "# repo1\n\nDescription for repo1") + testEditFile(t, session, "user1", "repo1", "status1", "README.md", "# repo1\n\nDescription for repo1") url := path.Join("user1", "repo1", "compare", "master...status1") req := NewRequestWithValues(t, "POST", url, diff --git a/tests/integration/release_test.go b/tests/integration/release_test.go index 125f0810a7..88a58787af 100644 --- a/tests/integration/release_test.go +++ b/tests/integration/release_test.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "testing" - "time" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" @@ -68,9 +67,6 @@ func TestViewReleases(t *testing.T) { session := loginUser(t, "user2") req := NewRequest(t, "GET", "/user2/repo1/releases") session.MakeRequest(t, req, http.StatusOK) - - // if CI is to slow this test fail, so lets wait a bit - time.Sleep(time.Millisecond * 100) } func TestViewReleasesNoLogin(t *testing.T) { @@ -118,7 +114,7 @@ func TestCreateReleasePaging(t *testing.T) { session := loginUser(t, "user2") // Create enough releases to have paging - for i := 0; i < 12; i++ { + for i := range 12 { version := fmt.Sprintf("v0.0.%d", i) createNewRelease(t, session, "/user2/repo1", version, version, false, false) } diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go index 504d2adacc..0097a7f62e 100644 --- a/tests/integration/repo_commits_test.go +++ b/tests/integration/repo_commits_test.go @@ -12,6 +12,7 @@ import ( "testing" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -76,7 +77,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) { // Call API to add status for commit ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository) t.Run("CreateStatus", doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{ - State: api.CommitStatusState(state), + State: commitstatus.CommitStatusState(state), TargetURL: "http://test.ci/", Description: "", Context: "testci", @@ -120,7 +121,7 @@ func testRepoCommitsWithStatus(t *testing.T, resp, respOne *httptest.ResponseRec assert.NotNil(t, status) if assert.Len(t, statuses, 1) { - assert.Equal(t, api.CommitStatusState(state), statuses[0].State) + assert.Equal(t, commitstatus.CommitStatusState(state), statuses[0].State) assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/statuses/65f1bf27bc3bf70f64657658635e66094edbcb4d", statuses[0].URL) assert.Equal(t, "http://test.ci/", statuses[0].TargetURL) assert.Empty(t, statuses[0].Description) @@ -168,13 +169,13 @@ func TestRepoCommitsStatusParallel(t *testing.T) { assert.NotEmpty(t, commitURL) var wg sync.WaitGroup - for i := 0; i < 10; i++ { + for i := range 10 { wg.Add(1) go func(parentT *testing.T, i int) { parentT.Run(fmt.Sprintf("ParallelCreateStatus_%d", i), func(t *testing.T) { ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository) runBody := doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{ - State: api.CommitStatusPending, + State: commitstatus.CommitStatusPending, TargetURL: "http://test.ci/", Description: "", Context: "testci", @@ -205,14 +206,14 @@ func TestRepoCommitsStatusMultiple(t *testing.T) { // Call API to add status for commit ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository) t.Run("CreateStatus", doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{ - State: api.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, TargetURL: "http://test.ci/", Description: "", Context: "testci", })) t.Run("CreateStatus", doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{ - State: api.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, TargetURL: "http://test.ci/", Description: "", Context: "other_context", diff --git a/tests/integration/repo_fork_test.go b/tests/integration/repo_fork_test.go index a7010af14a..95325eefeb 100644 --- a/tests/integration/repo_fork_test.go +++ b/tests/integration/repo_fork_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" org_service "code.gitea.io/gitea/services/org" "code.gitea.io/gitea/tests" @@ -51,7 +52,8 @@ func testRepoFork(t *testing.T, session *TestSession, ownerName, repoName, forkO "repo_name": forkRepoName, "fork_single_branch": forkBranch, }) - session.MakeRequest(t, req, http.StatusSeeOther) + resp = session.MakeRequest(t, req, http.StatusOK) + assert.Equal(t, fmt.Sprintf("/%s/%s", forkOwnerName, forkRepoName), test.RedirectURL(resp)) // Step4: check the existence of the forked repo req = NewRequestf(t, "GET", "/%s/%s", forkOwnerName, forkRepoName) @@ -82,7 +84,7 @@ func TestRepoForkToOrg(t *testing.T) { func TestForkListLimitedAndPrivateRepos(t *testing.T) { defer tests.PrepareTestEnv(t)() - forkItemSelector := ".repo-fork-item" + forkItemSelector := ".fork-list .flex-item" user1Sess := loginUser(t, "user1") user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"}) diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go index e928b04e9b..d33d31c646 100644 --- a/tests/integration/repo_merge_upstream_test.go +++ b/tests/integration/repo_merge_upstream_test.go @@ -147,5 +147,37 @@ func TestRepoMergeUpstream(t *testing.T) { return queryMergeUpstreamButtonLink(htmlDoc) == "" }, 5*time.Second, 100*time.Millisecond) }) + + t.Run("FastForwardOnly", func(t *testing.T) { + // Create a clean branch for fast-forward testing + req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/test-repo-fork/branches/_new/branch/master", forkUser.Name), map[string]string{ + "_csrf": GetUserCSRFToken(t, session), + "new_branch_name": "ff-test-branch", + }) + session.MakeRequest(t, req, http.StatusSeeOther) + + // Add content to base repository that can be fast-forwarded + require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "ff-test.txt", "master", "ff-content-1")) + + // ff_only=true with fast-forward possible (should succeed) + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{ + Branch: "ff-test-branch", + FfOnly: true, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusOK) + + var mergeResp api.MergeUpstreamResponse + DecodeJSON(t, resp, &mergeResp) + assert.Equal(t, "fast-forward", mergeResp.MergeStyle) + + // ff_only=true when fast-forward is not possible (should fail) + require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "another-file.txt", "master", "more-content")) + + req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/test-repo-fork/merge-upstream", forkUser.Name), &api.MergeUpstreamRequest{ + Branch: "fork-branch", + FfOnly: true, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusBadRequest) + }) }) } diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go index c04d09af08..adfe07519f 100644 --- a/tests/integration/repo_test.go +++ b/tests/integration/repo_test.go @@ -27,6 +27,7 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRepoView(t *testing.T) { @@ -41,6 +42,7 @@ func TestRepoView(t *testing.T) { t.Run("BlameFileInRepo", testBlameFileInRepo) t.Run("ViewRepoDirectory", testViewRepoDirectory) t.Run("ViewRepoDirectoryReadme", testViewRepoDirectoryReadme) + t.Run("ViewRepoSymlink", testViewRepoSymlink) t.Run("MarkDownReadmeImage", testMarkDownReadmeImage) t.Run("MarkDownReadmeImageSubfolder", testMarkDownReadmeImageSubfolder) t.Run("GeneratedSourceLink", testGeneratedSourceLink) @@ -412,6 +414,21 @@ func testViewRepoDirectoryReadme(t *testing.T) { missing("symlink-loop", "/user2/readme-test/src/branch/symlink-loop/") } +func testViewRepoSymlink(t *testing.T) { + session := loginUser(t, "user2") + req := NewRequest(t, "GET", "/user2/readme-test/src/branch/symlink") + resp := session.MakeRequest(t, req, http.StatusOK) + + htmlDoc := NewHTMLParser(t, resp.Body) + AssertHTMLElement(t, htmlDoc, ".entry-symbol-link", true) + followSymbolLinkHref := htmlDoc.Find(".entry-symbol-link").AttrOr("href", "") + require.Equal(t, "/user2/readme-test/src/branch/symlink/README.md?follow_symlink=1", followSymbolLinkHref) + + req = NewRequest(t, "GET", followSymbolLinkHref) + resp = session.MakeRequest(t, req, http.StatusSeeOther) + assert.Equal(t, "/user2/readme-test/src/branch/symlink/some/other/path/awefulcake.txt?follow_symlink=1", resp.Header().Get("Location")) +} + func testMarkDownReadmeImage(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -523,7 +540,7 @@ func TestGenerateRepository(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: generatedRepo.Name}) - err = repo_service.DeleteRepositoryDirectly(db.DefaultContext, user2, generatedRepo.ID) + err = repo_service.DeleteRepositoryDirectly(db.DefaultContext, generatedRepo.ID) assert.NoError(t, err) // a failed creating because some mock data diff --git a/tests/integration/repo_webhook_test.go b/tests/integration/repo_webhook_test.go index 13e3d198ea..1da7bc9d3c 100644 --- a/tests/integration/repo_webhook_test.go +++ b/tests/integration/repo_webhook_test.go @@ -9,15 +9,16 @@ import ( "net/http" "net/http/httptest" "net/url" + "path" "strings" "testing" - "time" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/commitstatus" "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/json" api "code.gitea.io/gitea/modules/structs" @@ -131,19 +132,19 @@ func (m *mockWebhookProvider) Close() { } func Test_WebhookCreate(t *testing.T) { - var payloads []api.CreatePayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.CreatePayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = string(webhook_module.HookEventCreate) - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.CreatePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.CreatePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = string(webhook_module.HookEventCreate) + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -163,19 +164,19 @@ func Test_WebhookCreate(t *testing.T) { } func Test_WebhookDelete(t *testing.T) { - var payloads []api.DeletePayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.DeletePayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "delete" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.DeletePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.DeletePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "delete" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -196,19 +197,19 @@ func Test_WebhookDelete(t *testing.T) { } func Test_WebhookFork(t *testing.T) { - var payloads []api.ForkPayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.ForkPayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "fork" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.ForkPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.ForkPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "fork" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user1") @@ -228,19 +229,19 @@ func Test_WebhookFork(t *testing.T) { } func Test_WebhookIssueComment(t *testing.T) { - var payloads []api.IssueCommentPayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.IssueCommentPayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "issue_comment" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.IssueCommentPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.IssueCommentPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "issue_comment" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -312,19 +313,19 @@ func Test_WebhookIssueComment(t *testing.T) { } func Test_WebhookRelease(t *testing.T) { - var payloads []api.ReleasePayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.ReleasePayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "release" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.ReleasePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.ReleasePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "release" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -345,19 +346,19 @@ func Test_WebhookRelease(t *testing.T) { } func Test_WebhookPush(t *testing.T) { - var payloads []api.PushPayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.PushPayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "push" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.PushPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.PushPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "push" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -416,19 +417,19 @@ func Test_WebhookPushDevBranch(t *testing.T) { } func Test_WebhookIssue(t *testing.T) { - var payloads []api.IssuePayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.IssuePayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "issues" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.IssuePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.IssuePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "issues" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -445,6 +446,78 @@ func Test_WebhookIssue(t *testing.T) { assert.Equal(t, "user2/repo1", payloads[0].Issue.Repo.FullName) assert.Equal(t, "Title1", payloads[0].Issue.Title) assert.Equal(t, "Description1", payloads[0].Issue.Body) + assert.Positive(t, payloads[0].Issue.Created.Unix()) + assert.Positive(t, payloads[0].Issue.Updated.Unix()) + }) +} + +func Test_WebhookIssueDelete(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.IssuePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.IssuePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "issue" + }, http.StatusOK) + defer provider.Close() + + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "issues") + issueURL := testNewIssue(t, session, "user2", "repo1", "Title1", "Description1") + + // 2. trigger the webhook + testIssueDelete(t, session, issueURL) + + // 3. validate the webhook is triggered + assert.Equal(t, "issue", triggeredEvent) + require.Len(t, payloads, 2) + assert.EqualValues(t, "deleted", payloads[1].Action) + assert.Equal(t, "repo1", payloads[1].Issue.Repo.Name) + assert.Equal(t, "user2/repo1", payloads[1].Issue.Repo.FullName) + assert.Equal(t, "Title1", payloads[1].Issue.Title) + assert.Equal(t, "Description1", payloads[1].Issue.Body) + }) +} + +func Test_WebhookIssueAssign(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.PullRequestPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.PullRequestPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "pull_request_assign" + }, http.StatusOK) + defer provider.Close() + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request_assign") + + // 2. trigger the webhook, issue 2 is a pull request + testIssueAssign(t, session, repo1.Link(), 2, user2.ID) + + // 3. validate the webhook is triggered + assert.Equal(t, "pull_request_assign", triggeredEvent) + assert.Len(t, payloads, 1) + assert.EqualValues(t, "assigned", payloads[0].Action) + assert.Equal(t, "repo1", payloads[0].PullRequest.Base.Repository.Name) + assert.Equal(t, "user2/repo1", payloads[0].PullRequest.Base.Repository.FullName) + assert.Equal(t, "issue2", payloads[0].PullRequest.Title) + assert.Equal(t, "content for the second issue", payloads[0].PullRequest.Body) + assert.Equal(t, user2.ID, payloads[0].PullRequest.Assignee.ID) }) } @@ -521,19 +594,19 @@ func Test_WebhookIssueMilestone(t *testing.T) { } func Test_WebhookPullRequest(t *testing.T) { - var payloads []api.PullRequestPayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.PullRequestPayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "pull_request" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.PullRequestPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.PullRequestPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "pull_request" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -557,20 +630,58 @@ func Test_WebhookPullRequest(t *testing.T) { }) } -func Test_WebhookPullRequestComment(t *testing.T) { - var payloads []api.IssueCommentPayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.IssueCommentPayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "pull_request_comment" - }, http.StatusOK) - defer provider.Close() +func Test_WebhookPullRequestDelete(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.PullRequestPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.PullRequestPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "pull_request" + }, http.StatusOK) + defer provider.Close() + + // 1. create a new webhook with special webhook for repo1 + session := loginUser(t, "user2") + testAPICreateWebhookForRepo(t, session, "user2", "repo1", provider.URL(), "pull_request") + testAPICreateBranch(t, session, "user2", "repo1", "master", "master2", http.StatusCreated) + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + issueURL := testCreatePullToDefaultBranch(t, session, repo1, repo1, "master2", "first pull request") + + // 2. trigger the webhook + testIssueDelete(t, session, path.Join(repo1.Link(), "pulls", issueURL)) + + // 3. validate the webhook is triggered + assert.Equal(t, "pull_request", triggeredEvent) + require.Len(t, payloads, 2) + assert.EqualValues(t, "deleted", payloads[1].Action) + assert.Equal(t, "repo1", payloads[1].PullRequest.Base.Repository.Name) + assert.Equal(t, "user2/repo1", payloads[1].PullRequest.Base.Repository.FullName) + assert.Equal(t, 0, *payloads[1].PullRequest.Additions) + assert.Equal(t, 0, *payloads[1].PullRequest.ChangedFiles) + assert.Equal(t, 0, *payloads[1].PullRequest.Deletions) + }) +} + +func Test_WebhookPullRequestComment(t *testing.T) { onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.IssueCommentPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.IssueCommentPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "pull_request_comment" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -596,19 +707,19 @@ func Test_WebhookPullRequestComment(t *testing.T) { } func Test_WebhookWiki(t *testing.T) { - var payloads []api.WikiPayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.WikiPayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "wiki" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.WikiPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.WikiPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "wiki" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -628,19 +739,19 @@ func Test_WebhookWiki(t *testing.T) { } func Test_WebhookRepository(t *testing.T) { - var payloads []api.RepositoryPayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.RepositoryPayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "repository" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.RepositoryPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.RepositoryPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "repository" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user1") @@ -660,19 +771,19 @@ func Test_WebhookRepository(t *testing.T) { } func Test_WebhookPackage(t *testing.T) { - var payloads []api.PackagePayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - content, _ := io.ReadAll(r.Body) - var payload api.PackagePayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "package" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.PackagePayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + content, _ := io.ReadAll(r.Body) + var payload api.PackagePayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "package" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user1") @@ -697,24 +808,24 @@ func Test_WebhookPackage(t *testing.T) { } func Test_WebhookStatus(t *testing.T) { - var payloads []api.CommitStatusPayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - assert.Contains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should contain status") - assert.Contains(t, r.Header["X-Github-Hook-Installation-Target-Type"], "repository", "X-GitHub-Hook-Installation-Target-Type should contain repository") - assert.Contains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should contain status") - assert.Contains(t, r.Header["X-Gitea-Hook-Installation-Target-Type"], "repository", "X-Gitea-Hook-Installation-Target-Type should contain repository") - assert.Contains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should contain status") - content, _ := io.ReadAll(r.Body) - var payload api.CommitStatusPayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "status" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.CommitStatusPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + assert.Contains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should contain status") + assert.Contains(t, r.Header["X-Github-Hook-Installation-Target-Type"], "repository", "X-GitHub-Hook-Installation-Target-Type should contain repository") + assert.Contains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should contain status") + assert.Contains(t, r.Header["X-Gitea-Hook-Installation-Target-Type"], "repository", "X-Gitea-Hook-Installation-Target-Type should contain repository") + assert.Contains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should contain status") + content, _ := io.ReadAll(r.Body) + var payload api.CommitStatusPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "status" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -732,7 +843,7 @@ func Test_WebhookStatus(t *testing.T) { // update a status for a commit via API doAPICreateCommitStatus(testCtx, commitID, api.CreateStatusOption{ - State: api.CommitStatusSuccess, + State: commitstatus.CommitStatusSuccess, TargetURL: "http://test.ci/", Description: "", Context: "testci", @@ -750,16 +861,16 @@ func Test_WebhookStatus(t *testing.T) { } func Test_WebhookStatus_NoWrongTrigger(t *testing.T) { - var trigger string - provider := newMockWebhookProvider(func(r *http.Request) { - assert.NotContains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should not contain status") - assert.NotContains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should not contain status") - assert.NotContains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should not contain status") - trigger = "push" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var trigger string + provider := newMockWebhookProvider(func(r *http.Request) { + assert.NotContains(t, r.Header["X-Github-Event-Type"], "status", "X-GitHub-Event-Type should not contain status") + assert.NotContains(t, r.Header["X-Gitea-Event-Type"], "status", "X-Gitea-Event-Type should not contain status") + assert.NotContains(t, r.Header["X-Gogs-Event-Type"], "status", "X-Gogs-Event-Type should not contain status") + trigger = "push" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 session := loginUser(t, "user2") @@ -775,22 +886,22 @@ func Test_WebhookStatus_NoWrongTrigger(t *testing.T) { } func Test_WebhookWorkflowJob(t *testing.T) { - var payloads []api.WorkflowJobPayload - var triggeredEvent string - provider := newMockWebhookProvider(func(r *http.Request) { - assert.Contains(t, r.Header["X-Github-Event-Type"], "workflow_job", "X-GitHub-Event-Type should contain workflow_job") - assert.Contains(t, r.Header["X-Gitea-Event-Type"], "workflow_job", "X-Gitea-Event-Type should contain workflow_job") - assert.Contains(t, r.Header["X-Gogs-Event-Type"], "workflow_job", "X-Gogs-Event-Type should contain workflow_job") - content, _ := io.ReadAll(r.Body) - var payload api.WorkflowJobPayload - err := json.Unmarshal(content, &payload) - assert.NoError(t, err) - payloads = append(payloads, payload) - triggeredEvent = "workflow_job" - }, http.StatusOK) - defer provider.Close() - onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + var payloads []api.WorkflowJobPayload + var triggeredEvent string + provider := newMockWebhookProvider(func(r *http.Request) { + assert.Contains(t, r.Header["X-Github-Event-Type"], "workflow_job", "X-GitHub-Event-Type should contain workflow_job") + assert.Contains(t, r.Header["X-Gitea-Event-Type"], "workflow_job", "X-Gitea-Event-Type should contain workflow_job") + assert.Contains(t, r.Header["X-Gogs-Event-Type"], "workflow_job", "X-Gogs-Event-Type should contain workflow_job") + content, _ := io.ReadAll(r.Body) + var payload api.WorkflowJobPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + payloads = append(payloads, payload) + triggeredEvent = "workflow_job" + }, http.StatusOK) + defer provider.Close() + // 1. create a new webhook with special webhook for repo1 user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) session := loginUser(t, "user2") @@ -850,8 +961,7 @@ jobs: // 4. Execute a single Job task := runner.fetchTask(t) outcome := &mockTaskOutcome{ - result: runnerv1.Result_RESULT_SUCCESS, - execTime: time.Millisecond, + result: runnerv1.Result_RESULT_SUCCESS, } runner.execTask(t, task, outcome) @@ -872,8 +982,7 @@ jobs: assert.Equal(t, commitID, payloads[3].WorkflowJob.HeadSha) assert.Equal(t, "repo1", payloads[3].Repo.Name) assert.Equal(t, "user2/repo1", payloads[3].Repo.FullName) - assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/runs/%d/jobs/%d", payloads[3].WorkflowJob.RunID, payloads[3].WorkflowJob.ID)) - assert.Contains(t, payloads[3].WorkflowJob.URL, payloads[3].WorkflowJob.RunURL) + assert.Contains(t, payloads[3].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[3].WorkflowJob.ID)) assert.Contains(t, payloads[3].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 0)) assert.Len(t, payloads[3].WorkflowJob.Steps, 1) @@ -887,8 +996,7 @@ jobs: // 6. Execute a single Job task = runner.fetchTask(t) outcome = &mockTaskOutcome{ - result: runnerv1.Result_RESULT_FAILURE, - execTime: time.Millisecond, + result: runnerv1.Result_RESULT_FAILURE, } runner.execTask(t, task, outcome) @@ -910,9 +1018,207 @@ jobs: assert.Equal(t, commitID, payloads[6].WorkflowJob.HeadSha) assert.Equal(t, "repo1", payloads[6].Repo.Name) assert.Equal(t, "user2/repo1", payloads[6].Repo.FullName) - assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/runs/%d/jobs/%d", payloads[6].WorkflowJob.RunID, payloads[6].WorkflowJob.ID)) - assert.Contains(t, payloads[6].WorkflowJob.URL, payloads[6].WorkflowJob.RunURL) + assert.Contains(t, payloads[6].WorkflowJob.URL, fmt.Sprintf("/actions/jobs/%d", payloads[6].WorkflowJob.ID)) assert.Contains(t, payloads[6].WorkflowJob.HTMLURL, fmt.Sprintf("/jobs/%d", 1)) assert.Len(t, payloads[6].WorkflowJob.Steps, 2) }) } + +type workflowRunWebhook struct { + URL string + payloads []api.WorkflowRunPayload + triggeredEvent string +} + +func Test_WebhookWorkflowRun(t *testing.T) { + webhookData := &workflowRunWebhook{} + provider := newMockWebhookProvider(func(r *http.Request) { + assert.Contains(t, r.Header["X-Github-Event-Type"], "workflow_run", "X-GitHub-Event-Type should contain workflow_run") + assert.Contains(t, r.Header["X-Gitea-Event-Type"], "workflow_run", "X-Gitea-Event-Type should contain workflow_run") + assert.Contains(t, r.Header["X-Gogs-Event-Type"], "workflow_run", "X-Gogs-Event-Type should contain workflow_run") + content, _ := io.ReadAll(r.Body) + var payload api.WorkflowRunPayload + err := json.Unmarshal(content, &payload) + assert.NoError(t, err) + webhookData.payloads = append(webhookData.payloads, payload) + webhookData.triggeredEvent = "workflow_run" + }, http.StatusOK) + defer provider.Close() + webhookData.URL = provider.URL() + + tests := []struct { + name string + callback func(t *testing.T, webhookData *workflowRunWebhook) + }{ + { + name: "WorkflowRun", + callback: testWebhookWorkflowRun, + }, + { + name: "WorkflowRunDepthLimit", + callback: testWebhookWorkflowRunDepthLimit, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + webhookData.payloads = nil + webhookData.triggeredEvent = "" + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + test.callback(t, webhookData) + }) + }) + } +} + +func testWebhookWorkflowRun(t *testing.T, webhookData *workflowRunWebhook) { + // 1. create a new webhook with special webhook for repo1 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", webhookData.URL, "workflow_run") + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + + gitRepo1, err := gitrepo.OpenRepository(t.Context(), repo1) + assert.NoError(t, err) + + runner := newMockRunner() + runner.registerAsRepoRunner(t, "user2", "repo1", "mock-runner", []string{"ubuntu-latest"}, false) + + // 2.1 add workflow_run workflow file to the repo + + opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+"dispatch.yml", ` +on: + workflow_run: + workflows: ["Push"] + types: + - completed +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - run: echo 'test the webhook' +`) + createWorkflowFile(t, token, "user2", "repo1", ".gitea/workflows/dispatch.yml", opts) + + // 2.2 trigger the webhooks + + // add workflow file to the repo + // init the workflow + wfTreePath := ".gitea/workflows/push.yml" + wfFileContent := `name: Push +on: push +jobs: + wf1-job: + runs-on: ubuntu-latest + steps: + - run: echo 'test the webhook' + wf2-job: + runs-on: ubuntu-latest + needs: wf1-job + steps: + - run: echo 'cmd 1' + - run: echo 'cmd 2' +` + opts = getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent) + createWorkflowFile(t, token, "user2", "repo1", wfTreePath, opts) + + commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch) + assert.NoError(t, err) + + // 3. validate the webhook is triggered + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + assert.Len(t, webhookData.payloads, 1) + assert.Equal(t, "requested", webhookData.payloads[0].Action) + assert.Equal(t, "queued", webhookData.payloads[0].WorkflowRun.Status) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[0].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[0].WorkflowRun.HeadSha) + assert.Equal(t, "repo1", webhookData.payloads[0].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[0].Repo.FullName) + + // 4. Execute two Jobs + task := runner.fetchTask(t) + outcome := &mockTaskOutcome{ + result: runnerv1.Result_RESULT_SUCCESS, + } + runner.execTask(t, task, outcome) + + task = runner.fetchTask(t) + outcome = &mockTaskOutcome{ + result: runnerv1.Result_RESULT_FAILURE, + } + runner.execTask(t, task, outcome) + + // 7. validate the webhook is triggered + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + assert.Len(t, webhookData.payloads, 3) + assert.Equal(t, "completed", webhookData.payloads[1].Action) + assert.Equal(t, "push", webhookData.payloads[1].WorkflowRun.Event) + + // 3. validate the webhook is triggered + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + assert.Len(t, webhookData.payloads, 3) + assert.Equal(t, "requested", webhookData.payloads[2].Action) + assert.Equal(t, "queued", webhookData.payloads[2].WorkflowRun.Status) + assert.Equal(t, "workflow_run", webhookData.payloads[2].WorkflowRun.Event) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[2].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[2].WorkflowRun.HeadSha) + assert.Equal(t, "repo1", webhookData.payloads[2].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[2].Repo.FullName) +} + +func testWebhookWorkflowRunDepthLimit(t *testing.T, webhookData *workflowRunWebhook) { + // 1. create a new webhook with special webhook for repo1 + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + session := loginUser(t, "user2") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + + testAPICreateWebhookForRepo(t, session, "user2", "repo1", webhookData.URL, "workflow_run") + + repo1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1}) + + gitRepo1, err := gitrepo.OpenRepository(t.Context(), repo1) + assert.NoError(t, err) + + // 2. trigger the webhooks + + // add workflow file to the repo + // init the workflow + wfTreePath := ".gitea/workflows/push.yml" + wfFileContent := `name: Endless Loop +on: + push: + workflow_run: + types: + - requested +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - run: echo 'test the webhook' +` + opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent) + createWorkflowFile(t, token, "user2", "repo1", wfTreePath, opts) + + commitID, err := gitRepo1.GetBranchCommitID(repo1.DefaultBranch) + assert.NoError(t, err) + + // 3. validate the webhook is triggered + assert.Equal(t, "workflow_run", webhookData.triggeredEvent) + // 1x push + 5x workflow_run requested chain + assert.Len(t, webhookData.payloads, 6) + for i := range 6 { + assert.Equal(t, "requested", webhookData.payloads[i].Action) + assert.Equal(t, "queued", webhookData.payloads[i].WorkflowRun.Status) + assert.Equal(t, repo1.DefaultBranch, webhookData.payloads[i].WorkflowRun.HeadBranch) + assert.Equal(t, commitID, webhookData.payloads[i].WorkflowRun.HeadSha) + if i == 0 { + assert.Equal(t, "push", webhookData.payloads[i].WorkflowRun.Event) + } else { + assert.Equal(t, "workflow_run", webhookData.payloads[i].WorkflowRun.Event) + } + assert.Equal(t, "repo1", webhookData.payloads[i].Repo.Name) + assert.Equal(t, "user2/repo1", webhookData.payloads[i].Repo.FullName) + } +} diff --git a/tests/integration/repofiles_change_test.go b/tests/integration/repofiles_change_test.go index ce55a2f943..dc389f5680 100644 --- a/tests/integration/repofiles_change_test.go +++ b/tests/integration/repofiles_change_test.go @@ -17,6 +17,7 @@ import ( "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/contexttest" files_service "code.gitea.io/gitea/services/repository/files" @@ -58,6 +59,40 @@ func getUpdateRepoFilesOptions(repo *repo_model.Repository) *files_service.Chang } } +func getUpdateRepoFilesRenameOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions { + return &files_service.ChangeRepoFilesOptions{ + Files: []*files_service.ChangeRepoFile{ + // move normally + { + Operation: "rename", + FromTreePath: "README.md", + TreePath: "README.txt", + }, + // move from in lfs + { + Operation: "rename", + FromTreePath: "crypt.bin", + TreePath: "crypt1.bin", + }, + // move from lfs to normal + { + Operation: "rename", + FromTreePath: "jpeg.jpg", + TreePath: "jpeg.jpeg", + }, + // move from normal to lfs + { + Operation: "rename", + FromTreePath: "CONTRIBUTING.md", + TreePath: "CONTRIBUTING.md.bin", + }, + }, + OldBranch: repo.DefaultBranch, + NewBranch: repo.DefaultBranch, + Message: "Rename files", + } +} + func getDeleteRepoFilesOptions(repo *repo_model.Repository) *files_service.ChangeRepoFilesOptions { return &files_service.ChangeRepoFilesOptions{ Files: []*files_service.ChangeRepoFile{ @@ -120,9 +155,9 @@ func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git. Name: path.Base(treePath), Path: treePath, SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", - LastCommitSHA: lastCommit.ID.String(), - LastCommitterDate: lastCommit.Committer.When, - LastAuthorDate: lastCommit.Author.When, + LastCommitSHA: util.ToPointer(lastCommit.ID.String()), + LastCommitterDate: util.ToPointer(lastCommit.Committer.When), + LastAuthorDate: util.ToPointer(lastCommit.Author.When), Type: "file", Size: 18, Encoding: &encoding, @@ -163,7 +198,7 @@ func getExpectedFileResponseForRepoFilesCreate(commitID string, lastCommit *git. SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", }, }, - Message: "Updates README.md\n", + Message: "Creates new/file.txt\n", Tree: &api.CommitMeta{ URL: setting.AppURL + "api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc", SHA: "f93e3a1a1525fb5b91020git dda86e44810c87a2d7bc", @@ -190,9 +225,9 @@ func getExpectedFileResponseForRepoFilesUpdate(commitID, filename, lastCommitSHA Name: filename, Path: filename, SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647", - LastCommitSHA: lastCommitSHA, - LastCommitterDate: lastCommitterWhen, - LastAuthorDate: lastAuthorWhen, + LastCommitSHA: util.ToPointer(lastCommitSHA), + LastCommitterDate: util.ToPointer(lastCommitterWhen), + LastAuthorDate: util.ToPointer(lastAuthorWhen), Type: "file", Size: 43, Encoding: &encoding, @@ -248,6 +283,114 @@ func getExpectedFileResponseForRepoFilesUpdate(commitID, filename, lastCommitSHA } } +func getExpectedFileResponseForRepoFilesUpdateRename(commitID, lastCommitSHA string) *api.FilesResponse { + details := []struct { + filename, sha, content string + size int64 + lfsOid *string + lfsSize *int64 + }{ + { + filename: "README.txt", + sha: "8276d2a29779af982c0afa976bdb793b52d442a8", + size: 22, + content: "IyBBbiBMRlMtZW5hYmxlZCByZXBvCg==", + }, + { + filename: "crypt1.bin", + sha: "d4a41a0d4db4949e129bd22f871171ea988103ef", + size: 129, + content: "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6MmVjY2RiNDM4MjVkMmE0OWQ5OWQ1NDJkYWEyMDA3NWNmZjFkOTdkOWQyMzQ5YTg5NzdlZmU5YzAzNjYxNzM3YwpzaXplIDIwNDgK", + lfsOid: util.ToPointer("2eccdb43825d2a49d99d542daa20075cff1d97d9d2349a8977efe9c03661737c"), + lfsSize: util.ToPointer(int64(2048)), + }, + { + filename: "jpeg.jpeg", + sha: "71911bf48766c7181518c1070911019fbb00b1fc", + size: 107, + content: "/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=", + }, + { + filename: "CONTRIBUTING.md.bin", + sha: "2b6c6c4eaefa24b22f2092c3d54b263ff26feb58", + size: 127, + content: "dmVyc2lvbiBodHRwczovL2dpdC1sZnMuZ2l0aHViLmNvbS9zcGVjL3YxCm9pZCBzaGEyNTY6N2I2YjJjODhkYmE5Zjc2MGExYTU4NDY5YjY3ZmVlMmI2OThlZjdlOTM5OWM0Y2E0ZjM0YTE0Y2NiZTM5ZjYyMwpzaXplIDI3Cg==", + lfsOid: util.ToPointer("7b6b2c88dba9f760a1a58469b67fee2b698ef7e9399c4ca4f34a14ccbe39f623"), + lfsSize: util.ToPointer(int64(27)), + }, + } + + var responses []*api.ContentsResponse + for _, detail := range details { + selfURL := setting.AppURL + "api/v1/repos/user2/lfs/contents/" + detail.filename + "?ref=master" + htmlURL := setting.AppURL + "user2/lfs/src/branch/master/" + detail.filename + gitURL := setting.AppURL + "api/v1/repos/user2/lfs/git/blobs/" + detail.sha + downloadURL := setting.AppURL + "user2/lfs/raw/branch/master/" + detail.filename + // don't set time related fields because there might be different time in one operation + responses = append(responses, &api.ContentsResponse{ + Name: detail.filename, + Path: detail.filename, + SHA: detail.sha, + LastCommitSHA: util.ToPointer(lastCommitSHA), + Type: "file", + Size: detail.size, + Encoding: util.ToPointer("base64"), + Content: &detail.content, + URL: &selfURL, + HTMLURL: &htmlURL, + GitURL: &gitURL, + DownloadURL: &downloadURL, + Links: &api.FileLinksResponse{ + Self: &selfURL, + GitURL: &gitURL, + HTMLURL: &htmlURL, + }, + LfsOid: detail.lfsOid, + LfsSize: detail.lfsSize, + }) + } + + return &api.FilesResponse{ + Files: responses, + Commit: &api.FileCommitResponse{ + CommitMeta: api.CommitMeta{ + URL: setting.AppURL + "api/v1/repos/user2/lfs/git/commits/" + commitID, + SHA: commitID, + }, + HTMLURL: setting.AppURL + "user2/lfs/commit/" + commitID, + Author: &api.CommitUser{ + Identity: api.Identity{ + Name: "User Two", + Email: "user2@noreply.example.org", + }, + }, + Committer: &api.CommitUser{ + Identity: api.Identity{ + Name: "User Two", + Email: "user2@noreply.example.org", + }, + }, + Parents: []*api.CommitMeta{ + { + URL: setting.AppURL + "api/v1/repos/user2/lfs/git/commits/73cf03db6ece34e12bf91e8853dc58f678f2f82d", + SHA: "73cf03db6ece34e12bf91e8853dc58f678f2f82d", + }, + }, + Message: "Rename files\n", + Tree: &api.CommitMeta{ + URL: setting.AppURL + "api/v1/repos/user2/lfs/git/trees/5307376dc3a5557dc1c403c29a8984668ca9ecb5", + SHA: "5307376dc3a5557dc1c403c29a8984668ca9ecb5", + }, + }, + Verification: &api.PayloadCommitVerification{ + Verified: false, + Reason: "gpg.error.not_signed_commit", + Signature: "", + Payload: "", + }, + } +} + func TestChangeRepoFilesForCreate(t *testing.T) { // setup onGiteaRun(t, func(t *testing.T, u *url.URL) { @@ -369,6 +512,38 @@ func TestChangeRepoFilesForUpdateWithFileMove(t *testing.T) { }) } +func TestChangeRepoFilesForUpdateWithFileRename(t *testing.T) { + onGiteaRun(t, func(t *testing.T, u *url.URL) { + ctx, _ := contexttest.MockContext(t, "user2/lfs") + ctx.SetPathParam("id", "54") + contexttest.LoadRepo(t, ctx, 54) + contexttest.LoadRepoCommit(t, ctx) + contexttest.LoadUser(t, ctx, 2) + contexttest.LoadGitRepo(t, ctx) + defer ctx.Repo.GitRepo.Close() + + repo := ctx.Repo.Repository + opts := getUpdateRepoFilesRenameOptions(repo) + + // test + filesResponse, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, ctx.Doer, opts) + + // asserts + assert.NoError(t, err) + gitRepo, _ := gitrepo.OpenRepository(git.DefaultContext, repo) + defer gitRepo.Close() + + commit, _ := gitRepo.GetBranchCommit(repo.DefaultBranch) + lastCommit, _ := commit.GetCommitByPath(opts.Files[0].TreePath) + expectedFileResponse := getExpectedFileResponseForRepoFilesUpdateRename(commit.ID.String(), lastCommit.ID.String()) + for _, file := range filesResponse.Files { + file.LastCommitterDate, file.LastAuthorDate = nil, nil // there might be different time in one operation, so we ignore them + } + assert.Len(t, filesResponse.Files, 4) + assert.Equal(t, expectedFileResponse.Files, filesResponse.Files) + }) +} + // Test opts with branch names removed, should get same results as above test func TestChangeRepoFilesWithoutBranchNames(t *testing.T) { // setup diff --git a/tests/integration/setting_test.go b/tests/integration/setting_test.go index 9dad9ca716..64fd28c5e9 100644 --- a/tests/integration/setting_test.go +++ b/tests/integration/setting_test.go @@ -8,6 +8,7 @@ import ( "testing" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -92,8 +93,7 @@ func TestSettingShowUserEmailProfile(t *testing.T) { func TestSettingLandingPage(t *testing.T) { defer tests.PrepareTestEnv(t)() - - landingPage := setting.LandingPageURL + defer test.MockVariableValue(&setting.LandingPageURL)() setting.LandingPageURL = setting.LandingPageHome req := NewRequest(t, "GET", "/") @@ -113,6 +113,4 @@ func TestSettingLandingPage(t *testing.T) { req = NewRequest(t, "GET", "/") resp = MakeRequest(t, req, http.StatusSeeOther) assert.Equal(t, "/user/login", resp.Header().Get("Location")) - - setting.LandingPageURL = landingPage } diff --git a/tests/integration/signup_test.go b/tests/integration/signup_test.go index e86851352e..c08f57d33e 100644 --- a/tests/integration/signup_test.go +++ b/tests/integration/signup_test.go @@ -22,8 +22,7 @@ import ( func TestSignup(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.Service.EnableCaptcha = false + defer test.MockVariableValue(&setting.Service.EnableCaptcha, false)() req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ "user_name": "exampleUser", @@ -40,9 +39,8 @@ func TestSignup(t *testing.T) { func TestSignupAsRestricted(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.Service.EnableCaptcha = false - setting.Service.DefaultUserIsRestricted = true + defer test.MockVariableValue(&setting.Service.EnableCaptcha, false)() + defer test.MockVariableValue(&setting.Service.DefaultUserIsRestricted, true)() req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{ "user_name": "restrictedUser", @@ -62,8 +60,7 @@ func TestSignupAsRestricted(t *testing.T) { func TestSignupEmailValidation(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.Service.EnableCaptcha = false + defer test.MockVariableValue(&setting.Service.EnableCaptcha, false)() tests := []struct { email string diff --git a/tests/integration/ssh_key_test.go b/tests/integration/ssh_key_test.go index fbdda9b3af..b34a986be3 100644 --- a/tests/integration/ssh_key_test.go +++ b/tests/integration/ssh_key_test.go @@ -27,7 +27,7 @@ func doCheckRepositoryEmptyStatus(ctx APITestContext, isEmpty bool) func(*testin func doAddChangesToCheckout(dstPath, filename string) func(*testing.T) { return func(t *testing.T) { - assert.NoError(t, os.WriteFile(filepath.Join(dstPath, filename), []byte(fmt.Sprintf("# Testing Repository\n\nOriginally created in: %s at time: %v", dstPath, time.Now())), 0o644)) + assert.NoError(t, os.WriteFile(filepath.Join(dstPath, filename), fmt.Appendf(nil, "# Testing Repository\n\nOriginally created in: %s at time: %v", dstPath, time.Now()), 0o644)) assert.NoError(t, git.AddChanges(dstPath, true)) signature := git.Signature{ Email: "test@example.com", diff --git a/tests/integration/timetracking_test.go b/tests/integration/timetracking_test.go index 8985dfdbce..4e8109be96 100644 --- a/tests/integration/timetracking_test.go +++ b/tests/integration/timetracking_test.go @@ -46,11 +46,11 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo AssertHTMLElement(t, htmlDoc, ".issue-add-time", canTrackTime) issueLink := path.Join(user, repo, "issues", issue) - req = NewRequestWithValues(t, "POST", path.Join(issueLink, "times", "stopwatch", "toggle"), map[string]string{ + reqStart := NewRequestWithValues(t, "POST", path.Join(issueLink, "times", "stopwatch", "start"), map[string]string{ "_csrf": htmlDoc.GetCSRF(), }) if canTrackTime { - session.MakeRequest(t, req, http.StatusOK) + session.MakeRequest(t, reqStart, http.StatusOK) req = NewRequest(t, "GET", issueLink) resp = session.MakeRequest(t, req, http.StatusOK) @@ -65,10 +65,10 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo // Sleep for 1 second to not get wrong order for stopping timer time.Sleep(time.Second) - req = NewRequestWithValues(t, "POST", path.Join(issueLink, "times", "stopwatch", "toggle"), map[string]string{ + reqStop := NewRequestWithValues(t, "POST", path.Join(issueLink, "times", "stopwatch", "stop"), map[string]string{ "_csrf": htmlDoc.GetCSRF(), }) - session.MakeRequest(t, req, http.StatusOK) + session.MakeRequest(t, reqStop, http.StatusOK) req = NewRequest(t, "GET", issueLink) resp = session.MakeRequest(t, req, http.StatusOK) @@ -77,6 +77,6 @@ func testViewTimetrackingControls(t *testing.T, session *TestSession, user, repo events = htmlDoc.doc.Find(".event > span.text") assert.Contains(t, events.Last().Text(), "worked for ") } else { - session.MakeRequest(t, req, http.StatusNotFound) + session.MakeRequest(t, reqStart, http.StatusNotFound) } } diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go index 2c9a916ec2..34692d9cab 100644 --- a/tests/integration/user_test.go +++ b/tests/integration/user_test.go @@ -257,8 +257,8 @@ func TestListStopWatches(t *testing.T) { } func TestUserLocationMapLink(t *testing.T) { - setting.Service.UserLocationMapURL = "https://example/foo/" defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Service.UserLocationMapURL, "https://example/foo/")() session := loginUser(t, "user2") req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{ diff --git a/tests/integration/webfinger_test.go b/tests/integration/webfinger_test.go index 2afaed4a45..757d442cd2 100644 --- a/tests/integration/webfinger_test.go +++ b/tests/integration/webfinger_test.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" @@ -20,11 +21,7 @@ import ( func TestWebfinger(t *testing.T) { defer tests.PrepareTestEnv(t)() - - setting.Federation.Enabled = true - defer func() { - setting.Federation.Enabled = false - }() + defer test.MockVariableValue(&setting.Federation.Enabled, true)() user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) diff --git a/tests/integration/workflow_run_api_check_test.go b/tests/integration/workflow_run_api_check_test.go new file mode 100644 index 0000000000..6a80bb5118 --- /dev/null +++ b/tests/integration/workflow_run_api_check_test.go @@ -0,0 +1,167 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + + "github.com/stretchr/testify/assert" +) + +func TestAPIWorkflowRun(t *testing.T) { + t.Run("AdminRuns", func(t *testing.T) { + testAPIWorkflowRunBasic(t, "/api/v1/admin/actions", "User1", 802, auth_model.AccessTokenScopeReadAdmin, auth_model.AccessTokenScopeReadRepository) + }) + t.Run("UserRuns", func(t *testing.T) { + testAPIWorkflowRunBasic(t, "/api/v1/user/actions", "User2", 803, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository) + }) + t.Run("OrgRuns", func(t *testing.T) { + testAPIWorkflowRunBasic(t, "/api/v1/orgs/org3/actions", "User1", 802, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadRepository) + }) + t.Run("RepoRuns", func(t *testing.T) { + testAPIWorkflowRunBasic(t, "/api/v1/repos/org3/repo5/actions", "User2", 802, auth_model.AccessTokenScopeReadRepository) + }) +} + +func testAPIWorkflowRunBasic(t *testing.T, apiRootURL, userUsername string, runID int64, scope ...auth_model.AccessTokenScope) { + defer tests.PrepareTestEnv(t)() + token := getUserToken(t, userUsername, scope...) + + apiRunsURL := fmt.Sprintf("%s/%s", apiRootURL, "runs") + req := NewRequest(t, "GET", apiRunsURL).AddTokenAuth(token) + runnerListResp := MakeRequest(t, req, http.StatusOK) + runnerList := api.ActionWorkflowRunsResponse{} + DecodeJSON(t, runnerListResp, &runnerList) + + foundRun := false + + for _, run := range runnerList.Entries { + // Verify filtering works + verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", run.Status, "", "", "", "") + verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, run.Conclusion, "", "", "", "", "") + verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", "", "", run.HeadBranch, "", "") + verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", "", run.Event, "", "", "") + verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", "", "", "", run.TriggerActor.UserName, "") + verifyWorkflowRunCanbeFoundWithStatusFilter(t, apiRunsURL, token, run.ID, "", "", "", "", run.TriggerActor.UserName, run.HeadSha) + + // Verify run url works + req := NewRequest(t, "GET", run.URL).AddTokenAuth(token) + runResp := MakeRequest(t, req, http.StatusOK) + apiRun := api.ActionWorkflowRun{} + DecodeJSON(t, runResp, &apiRun) + assert.Equal(t, run.ID, apiRun.ID) + assert.Equal(t, run.Status, apiRun.Status) + assert.Equal(t, run.Conclusion, apiRun.Conclusion) + assert.Equal(t, run.Event, apiRun.Event) + + // Verify jobs list works + req = NewRequest(t, "GET", fmt.Sprintf("%s/%s", run.URL, "jobs")).AddTokenAuth(token) + jobsResp := MakeRequest(t, req, http.StatusOK) + jobList := api.ActionWorkflowJobsResponse{} + DecodeJSON(t, jobsResp, &jobList) + + if run.ID == runID { + foundRun = true + assert.Len(t, jobList.Entries, 1) + for _, job := range jobList.Entries { + // Check the jobs list of the run + verifyWorkflowJobCanbeFoundWithStatusFilter(t, fmt.Sprintf("%s/%s", run.URL, "jobs"), token, job.ID, "", job.Status) + verifyWorkflowJobCanbeFoundWithStatusFilter(t, fmt.Sprintf("%s/%s", run.URL, "jobs"), token, job.ID, job.Conclusion, "") + // Check the run independent job list + verifyWorkflowJobCanbeFoundWithStatusFilter(t, fmt.Sprintf("%s/%s", apiRootURL, "jobs"), token, job.ID, "", job.Status) + verifyWorkflowJobCanbeFoundWithStatusFilter(t, fmt.Sprintf("%s/%s", apiRootURL, "jobs"), token, job.ID, job.Conclusion, "") + + // Verify job url works + req := NewRequest(t, "GET", job.URL).AddTokenAuth(token) + jobsResp := MakeRequest(t, req, http.StatusOK) + apiJob := api.ActionWorkflowJob{} + DecodeJSON(t, jobsResp, &apiJob) + assert.Equal(t, job.ID, apiJob.ID) + assert.Equal(t, job.RunID, apiJob.RunID) + assert.Equal(t, job.Status, apiJob.Status) + assert.Equal(t, job.Conclusion, apiJob.Conclusion) + } + } + } + assert.True(t, foundRun, "Expected to find run with ID %d", runID) +} + +func verifyWorkflowRunCanbeFoundWithStatusFilter(t *testing.T, runAPIURL, token string, id int64, conclusion, status, event, branch, actor, headSHA string) { + filter := url.Values{} + if conclusion != "" { + filter.Add("status", conclusion) + } + if status != "" { + filter.Add("status", status) + } + if event != "" { + filter.Set("event", event) + } + if branch != "" { + filter.Set("branch", branch) + } + if actor != "" { + filter.Set("actor", actor) + } + if headSHA != "" { + filter.Set("head_sha", headSHA) + } + req := NewRequest(t, "GET", runAPIURL+"?"+filter.Encode()).AddTokenAuth(token) + runResp := MakeRequest(t, req, http.StatusOK) + runList := api.ActionWorkflowRunsResponse{} + DecodeJSON(t, runResp, &runList) + + found := false + for _, run := range runList.Entries { + if conclusion != "" { + assert.Equal(t, conclusion, run.Conclusion) + } + if status != "" { + assert.Equal(t, status, run.Status) + } + if event != "" { + assert.Equal(t, event, run.Event) + } + if branch != "" { + assert.Equal(t, branch, run.HeadBranch) + } + if actor != "" { + assert.Equal(t, actor, run.Actor.UserName) + } + found = found || run.ID == id + } + assert.True(t, found, "Expected to find run with ID %d", id) +} + +func verifyWorkflowJobCanbeFoundWithStatusFilter(t *testing.T, runAPIURL, token string, id int64, conclusion, status string) { + filter := conclusion + if filter == "" { + filter = status + } + if filter == "" { + return + } + req := NewRequest(t, "GET", runAPIURL+"?status="+filter).AddTokenAuth(token) + jobListResp := MakeRequest(t, req, http.StatusOK) + jobList := api.ActionWorkflowJobsResponse{} + DecodeJSON(t, jobListResp, &jobList) + + found := false + for _, job := range jobList.Entries { + if conclusion != "" { + assert.Equal(t, conclusion, job.Conclusion) + } else { + assert.Equal(t, status, job.Status) + } + found = found || job.ID == id + } + assert.True(t, found, "Expected to find job with ID %d", id) +} diff --git a/tests/test_utils.go b/tests/test_utils.go index 4d8a8635d6..ad692058a6 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -1,7 +1,6 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//nolint:forbidigo package tests import ( @@ -55,7 +54,7 @@ func InitTest(requireGitea bool) { // Notice: when doing "ssh push", Gitea executes sub processes, debugger won't work for the sub processes. giteaConf = "tests/sqlite.ini" _ = os.Setenv("GITEA_CONF", giteaConf) - fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf) + _, _ = fmt.Fprintf(os.Stderr, "Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) if !setting.EnableSQLite3 { testlogger.Fatalf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify` + "\n") } diff --git a/tools/lint-go-gopls.sh b/tools/lint-go-gopls.sh index a222ea14d7..2cd26ca6fe 100755 --- a/tools/lint-go-gopls.sh +++ b/tools/lint-go-gopls.sh @@ -11,7 +11,7 @@ IGNORE_PATTERNS=( # current absolute path, indicating a error was found. This is necessary # because the tool does not set non-zero exit code when errors are found. # ref: https://github.com/golang/go/issues/67078 -ERROR_LINES=$("$GO" run "$GOPLS_PACKAGE" check "$@" 2>/dev/null | grep -E "^$PWD" | grep -vFf <(printf '%s\n' "${IGNORE_PATTERNS[@]}")); +ERROR_LINES=$("$GO" run "$GOPLS_PACKAGE" check -severity=warning "$@" 2>/dev/null | grep -E "^$PWD" | grep -vFf <(printf '%s\n' "${IGNORE_PATTERNS[@]}")); NUM_ERRORS=$(echo -n "$ERROR_LINES" | wc -l) if [ "$NUM_ERRORS" -eq "0" ]; then diff --git a/web_src/css/actions.css b/web_src/css/actions.css index 665893a287..c43ebe21a0 100644 --- a/web_src/css/actions.css +++ b/web_src/css/actions.css @@ -59,6 +59,7 @@ .run-list-ref { display: inline-block !important; + max-width: 105px; } @media (max-width: 767.98px) { diff --git a/web_src/css/base.css b/web_src/css/base.css index a28fb7b92a..529ddd5386 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -30,6 +30,15 @@ --page-spacing: 16px; /* space between page elements */ --page-margin-x: 32px; /* minimum space on left and right side of page */ --page-space-bottom: 64px; /* space between last page element and footer */ + + /* z-index */ + --z-index-modal: 1001; /* modal dialog, hard-coded from Fomantic modal.css */ + --z-index-toast: 1002; /* should be larger than modal */ + + --font-size-label: 12px; /* font size of individual labels */ + + --gap-inline: 0.25rem; /* gap for inline texts and elements, for example: the spaces for sentence with labels, button text, etc */ + --gap-block: 0.25rem; /* gap for element blocks, for example: spaces between buttons, menu image & title, header icon & title etc */ } @media (min-width: 768px) and (max-width: 1200px) { @@ -318,6 +327,16 @@ a.label, background: var(--color-hover); } +.empty-placeholder { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 40px; + padding-bottom: 40px; + text-align: center; + text-wrap: balance; +} + .inline-code-block { padding: 2px 4px; border-radius: .24em; @@ -815,10 +834,6 @@ overflow-menu .ui.label { display: block; } -.code-view .lines-num span::after { - cursor: pointer; -} - .lines-type-marker { vertical-align: top; white-space: nowrap; @@ -855,39 +870,13 @@ overflow-menu .ui.label { .lines-escape { width: 0; white-space: nowrap; + padding: 0; } .lines-code { padding-left: 5px; } -.file-view tr.active { - color: inherit !important; - background: inherit !important; -} - -.file-view tr.active .lines-num, -.file-view tr.active .lines-code { - background: var(--color-highlight-bg) !important; -} - -.file-view tr.active:last-of-type .lines-code { - border-bottom-right-radius: var(--border-radius); -} - -.file-view tr.active .lines-num { - position: relative; -} - -.file-view tr.active .lines-num::before { - content: ""; - position: absolute; - left: 0; - width: 2px; - height: 100%; - background: var(--color-highlight-fg); -} - .code-inner { font: 12px var(--fonts-monospace); white-space: pre-wrap; @@ -938,12 +927,12 @@ overflow-menu .ui.label { margin-right: 4px; } -.top-line-blame { +tr.top-line-blame { border-top: 1px solid var(--color-secondary); } -.code-view tr.top-line-blame:first-of-type { - border-top: none; +tr.top-line-blame:first-of-type { + border-top: none; /* merge code lines belonging to the same commit into one block */ } .lines-code .bottom-line, @@ -951,15 +940,6 @@ overflow-menu .ui.label { border-bottom: 1px solid var(--color-secondary); } -.code-view { - background: var(--color-code-bg); - border-radius: var(--border-radius); -} - -.code-view table { - width: 100%; -} - .migrate .svg.gitea-git { color: var(--color-git); } @@ -1118,7 +1098,7 @@ table th[data-sortt-desc] .svg { .flex-text-inline { display: inline-flex; align-items: center; - gap: .25rem; + gap: var(--gap-inline); vertical-align: middle; min-width: 0; /* make ellipsis work */ } @@ -1146,7 +1126,7 @@ table th[data-sortt-desc] .svg { .flex-text-block { display: flex; align-items: center; - gap: .5rem; + gap: var(--gap-block); min-width: 0; } @@ -1161,7 +1141,7 @@ the "!important" is necessary to override Fomantic UI menu item styles, meanwhil .ui.dropdown .menu.flex-items-menu > .item:not(.hidden, .filtered, .tw-hidden) { display: flex !important; align-items: center; - gap: .5rem; + gap: var(--gap-block); min-width: 0; } .ui.dropdown .menu.flex-items-menu > .item img, diff --git a/web_src/css/features/projects.css b/web_src/css/features/projects.css index 1d09e9c7ea..7fd5150970 100644 --- a/web_src/css/features/projects.css +++ b/web_src/css/features/projects.css @@ -5,6 +5,7 @@ flex-wrap: nowrap; overflow: auto; margin: 0 0.5em; + min-height: max(calc(100vh - 400px), 300px); max-height: calc(100vh - 120px); } diff --git a/web_src/css/form.css b/web_src/css/form.css index cf8fe96bea..c51eba1bc9 100644 --- a/web_src/css/form.css +++ b/web_src/css/form.css @@ -220,6 +220,7 @@ textarea:focus, color: var(--color-secondary-dark-5); padding-bottom: 0.6em; display: inline-block; + text-wrap: balance; } .m-captcha-style { diff --git a/web_src/css/home.css b/web_src/css/home.css index 77d2ecf92b..195d1f5d96 100644 --- a/web_src/css/home.css +++ b/web_src/css/home.css @@ -21,7 +21,7 @@ } .home .hero .svg { - color: var(--color-green); + color: var(--color-logo); height: 40px; width: 50px; vertical-align: bottom; @@ -40,7 +40,7 @@ } .home a { - color: var(--color-green); + color: var(--color-logo); } .page-footer { diff --git a/web_src/css/index.css b/web_src/css/index.css index c20aa028e4..291cd04b2b 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -62,7 +62,7 @@ @import "./repo/issue-label.css"; @import "./repo/issue-list.css"; @import "./repo/list-header.css"; -@import "./repo/linebutton.css"; +@import "./repo/file-view.css"; @import "./repo/wiki.css"; @import "./repo/header.css"; @import "./repo/home.css"; @@ -70,6 +70,7 @@ @import "./repo/reactions.css"; @import "./repo/clone.css"; @import "./repo/commit-sign.css"; +@import "./repo/packages.css"; @import "./editor/fileeditor.css"; @import "./editor/combomarkdowneditor.css"; diff --git a/web_src/css/markup/content.css b/web_src/css/markup/content.css index ace028b4d0..c6a89edf25 100644 --- a/web_src/css/markup/content.css +++ b/web_src/css/markup/content.css @@ -2,7 +2,7 @@ overflow: hidden; font-size: 16px; line-height: 1.5 !important; - overflow-wrap: anywhere; + overflow-wrap: break-word; } .markup > *:first-child { @@ -134,6 +134,13 @@ margin-bottom: 16px; } +/* override p:last-child from base.css. +Fomantic assumes that <p>/<hX> elements only have margins between elements, but not for the first's top or last's bottom. +In markup content, we always use bottom margin for all elements */ +.markup p:last-child { + margin-bottom: 16px; +} + .markup hr { height: 4px; padding: 0; @@ -309,10 +316,18 @@ box-sizing: initial; } +.file-view.markup { + padding: 1em 2em; +} + +.file-view.markup:has(.file-not-rendered-prompt) { + padding: 0; /* let the file-not-rendered-prompt layout itself */ +} + /* this background ensures images can break <hr>. We can only do this on cases where the background is known and not transparent. */ -.markup.file-view img, -.markup.file-view video, +.file-view.markup img, +.file-view.markup video, .comment-body .markup img, /* regular comment */ .comment-body .markup video, .comment-content .markup img, /* code comment */ diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css index 8edf31ddbd..deaaf83680 100644 --- a/web_src/css/modules/animations.css +++ b/web_src/css/modules/animations.css @@ -52,8 +52,7 @@ form.single-button-form.is-loading .button { } .markup pre.is-loading, -.editor-loading.is-loading, -.pdf-content.is-loading { +.editor-loading.is-loading { height: var(--height-loading); } diff --git a/web_src/css/modules/breadcrumb.css b/web_src/css/modules/breadcrumb.css index ca488c2150..77e31ef627 100644 --- a/web_src/css/modules/breadcrumb.css +++ b/web_src/css/modules/breadcrumb.css @@ -1,14 +1,10 @@ .breadcrumb { display: flex; - flex-wrap: wrap; align-items: center; gap: 3px; + overflow-wrap: anywhere; } .breadcrumb .breadcrumb-divider { color: var(--color-text-light-2); } - -.breadcrumb > * { - display: inline; -} diff --git a/web_src/css/modules/dimmer.css b/web_src/css/modules/dimmer.css index 8924821370..7d1ca6171a 100644 --- a/web_src/css/modules/dimmer.css +++ b/web_src/css/modules/dimmer.css @@ -20,7 +20,7 @@ opacity: 1; } -.ui.dimmer > * { +.ui.dimmer > .ui.modal { position: static; margin-top: auto !important; margin-bottom: auto !important; diff --git a/web_src/css/modules/label.css b/web_src/css/modules/label.css index 1e42668aa1..bb6f1b512f 100644 --- a/web_src/css/modules/label.css +++ b/web_src/css/modules/label.css @@ -4,25 +4,19 @@ .ui.label { display: inline-flex; align-items: center; - gap: .25rem; + gap: var(--gap-inline); min-width: 0; - vertical-align: middle; - line-height: 1; + max-width: 100%; background: var(--color-label-bg); color: var(--color-label-text); - padding: 0.3em 0.5em; - font-size: 0.85714286rem; + padding: 2px 6px; + font-size: var(--font-size-label); font-weight: var(--font-weight-medium); border: 0 solid transparent; - border-radius: 0.28571429rem; + border-radius: var(--border-radius); white-space: nowrap; -} - -.ui.label:first-child { - margin-left: 0; -} -.ui.label:last-child { - margin-right: 0; + overflow: hidden; + text-overflow: ellipsis; } a.ui.label { @@ -292,3 +286,58 @@ a.ui.ui.ui.basic.grey.label:hover { .ui.large.label { font-size: 1rem; } + +/* To let labels break up and wrap across multiple lines (issue title, comment event), use "display: contents here" to apply parent layout. +If the labels-list itself needs some layouts, use extra classes or "tw" helpers. */ +.labels-list { + display: contents; + font-size: var(--font-size-label); /* it must match the label font size, otherwise the height mismatches */ +} + +.labels-list a { + max-width: 100%; /* for ellipsis */ +} + +.labels-list .ui.label { + min-height: 20px; + padding-top: 0; + padding-bottom: 0; +} + +.with-labels-list-inline .labels-list .ui.label + .ui.label { + margin-left: var(--gap-inline); +} + +.with-labels-list-inline .labels-list .ui.label { + line-height: var(--line-height-default); +} + +/* Scoped labels with different colors on left and right */ +.ui.label.scope-parent { + background: none !important; + padding: 0 !important; + gap: 0 !important; +} + +.ui.label.scope-parent > .ui.label { + margin: 0 !important; /* scoped label's margin is handled by the parent */ +} + +.ui.label.scope-left { + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} + +.ui.label.scope-middle { + border-radius: 0; +} + +.ui.label.scope-right { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.ui.label.archived-label { + filter: grayscale(0.5); + opacity: 0.5; +} diff --git a/web_src/css/modules/list.css b/web_src/css/modules/list.css index 73760390de..46422cb97d 100644 --- a/web_src/css/modules/list.css +++ b/web_src/css/modules/list.css @@ -5,7 +5,6 @@ list-style-type: none; margin: 1em 0; padding: 0; - font-size: 1em; } .ui.list:first-child { diff --git a/web_src/css/modules/navbar.css b/web_src/css/modules/navbar.css index b09b271ad4..ab431e3675 100644 --- a/web_src/css/modules/navbar.css +++ b/web_src/css/modules/navbar.css @@ -129,8 +129,8 @@ background: var(--color-primary); border: 2px solid var(--color-nav-bg); position: absolute; - left: 6px; - top: -9px; + left: calc(100% - 9px); + bottom: calc(100% - 9px); min-width: 17px; height: 17px; border-radius: 11px; /* (height + 2 * borderThickness) / 2 */ diff --git a/web_src/css/modules/toast.css b/web_src/css/modules/toast.css index 1145f3b1b5..330d3b176e 100644 --- a/web_src/css/modules/toast.css +++ b/web_src/css/modules/toast.css @@ -3,7 +3,7 @@ position: fixed; opacity: 0; transition: all .2s ease; - z-index: 500; + z-index: var(--z-index-toast); border-radius: var(--border-radius); box-shadow: 0 8px 24px var(--color-shadow); display: flex; diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 306db34300..a72709c382 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -73,10 +73,21 @@ overflow: hidden; } +.issue-content-right .ui.list { + margin: 0.5em 0; + max-width: 100%; +} + .issue-sidebar-combo > .ui.dropdown .item:not(.checked) .item-check-mark { visibility: hidden; } +.issue-content-right .ui.list.labels-list { + display: flex; + gap: var(--gap-inline); + flex-wrap: wrap; +} + @media (max-width: 767.98px) { .issue-content-left, .issue-content-right { @@ -139,11 +150,6 @@ td .commit-summary { } } -.repo-path { - display: flex; - overflow-wrap: anywhere; -} - .repository.file.list .non-diff-file-content .header .icon { font-size: 1em; } @@ -177,42 +183,6 @@ td .commit-summary { cursor: default; } -.view-raw { - display: flex; - justify-content: center; - align-items: center; -} - -.view-raw > * { - max-width: 100%; -} - -.view-raw audio, -.view-raw video, -.view-raw img { - margin: 1rem 0; - border-radius: 0; - object-fit: contain; -} - -.view-raw img[src$=".svg" i] { - max-height: 600px !important; - max-width: 600px !important; -} - -.pdf-content { - width: 100%; - height: 600px; - border: none !important; - display: flex; - align-items: center; - justify-content: center; -} - -.pdf-content .pdf-fallback-button { - margin: 50px auto; -} - .repository.file.list .non-diff-file-content .plain-text { padding: 1em 2em; } @@ -235,10 +205,6 @@ td .commit-summary { padding: 0 !important; } -.non-diff-file-content .pdfobject { - border-radius: 0 0 var(--border-radius) var(--border-radius); -} - .repo-editor-header { width: 100%; } @@ -523,7 +489,7 @@ td .commit-summary { .repository.view.issue .comment-list .timeline-item, .repository.view.issue .comment-list .timeline-item-group { - padding: 16px 0; + padding: 8px 0; } .repository.view.issue .comment-list .timeline-item-group .timeline-item { @@ -577,6 +543,11 @@ td .commit-summary { justify-content: center; } +.repository.view.issue .comment-list .timeline-item.commits-list .badge { + margin-right: 0; + height: 28px; +} + .repository.view.issue .comment-list .timeline-item .badge .svg { width: 22px; height: 22px; @@ -601,10 +572,6 @@ td .commit-summary { padding-top: 0; } -.repository.view.issue .comment-list .timeline-item.commits-list .ui.avatar { - margin-right: 0.25em; -} - .repository.view.issue .comment-list .timeline-item.event > .commit-status-link { float: right; margin-right: 8px; @@ -1226,33 +1193,6 @@ td .commit-summary { font-weight: var(--font-weight-normal); } -.empty-placeholder { - display: flex; - flex-direction: column; - align-items: center; - padding-top: 40px; - padding-bottom: 40px; -} - -.repository.packages .file-size { - white-space: nowrap; -} - -.file-view.markup { - padding: 1em 2em; -} - -.file-view.markup:has(.file-not-rendered-prompt) { - padding: 0; /* let the file-not-rendered-prompt layout itself */ -} - -.file-not-rendered-prompt { - padding: 1rem; - text-align: center; - font-size: 1rem !important; /* use consistent styles for various containers (code, markup, etc) */ - line-height: var(--line-height-default) !important; /* same as above */ -} - .repository .activity-header { display: flex; justify-content: space-between; @@ -1575,49 +1515,6 @@ td .commit-summary { border-bottom-right-radius: 4px; } -.labels-list { - display: inline-flex; - flex-wrap: wrap; - gap: 2.5px; - align-items: center; -} - -.labels-list .label, .scope-parent > .label { - padding: 0 6px; - min-height: 20px; - line-height: 1.3; /* there is a `font-size: 1.25em` for inside emoji, so here the line-height needs to be larger slightly */ -} - -/* Scoped labels with different colors on left and right */ -.ui.label.scope-parent { - background: none !important; - padding: 0 !important; - gap: 0 !important; -} - -.archived-label { - filter: grayscale(0.5); - opacity: 0.5; -} - -.ui.label.scope-left { - border-bottom-right-radius: 0; - border-top-right-radius: 0; - margin-right: 0; -} - -.ui.label.scope-middle { - border-radius: 0; - margin-left: 0; - margin-right: 0; -} - -.ui.label.scope-right { - border-bottom-left-radius: 0; - border-top-left-radius: 0; - margin-left: 0; -} - .repo-button-row { margin: 8px 0; display: flex; @@ -1865,6 +1762,7 @@ tbody.commit-list { border-radius: 0; display: flex; flex-direction: column; + gap: 0.5em; } /* fomantic's last-child selector does not work with hidden last child */ @@ -2054,10 +1952,6 @@ tbody.commit-list { box-shadow: 0 0.5rem 1rem var(--color-shadow) !important; } -.migrate-entry .description { - text-wrap: balance; -} - .commits-table .commits-table-right form { display: flex; align-items: center; diff --git a/web_src/css/repo/file-view.css b/web_src/css/repo/file-view.css new file mode 100644 index 0000000000..907f136afe --- /dev/null +++ b/web_src/css/repo/file-view.css @@ -0,0 +1,92 @@ +.file-view tr.active .lines-num, +.file-view tr.active .lines-escape, +.file-view tr.active .lines-code { + background: var(--color-highlight-bg); +} + +/* set correct border radius on the last active lines, to avoid border overflow */ +.file-view tr.active:last-of-type .lines-code { + border-bottom-right-radius: var(--border-radius); +} + +.file-view tr.active .lines-num { + position: relative; +} + +/* add a darker "handler" at the beginning of the active line */ +.file-view tr.active .lines-num::before { + content: ""; + position: absolute; + left: 0; + width: 2px; + height: 100%; + background: var(--color-highlight-fg); +} + +.file-view .file-not-rendered-prompt { + padding: 1rem; + text-align: center; + font-size: 1rem !important; /* use consistent styles for various containers (code, markup, etc) */ + line-height: var(--line-height-default) !important; /* same as above */ +} + +/* ".code-view" is always used with ".file-view", to show the code of a file */ +.file-view.code-view { + background: var(--color-code-bg); + border-radius: var(--border-radius); +} + +.file-view.code-view table { + width: 100%; +} + +.file-view.code-view .lines-num span::after { + cursor: pointer; +} + +.file-view.code-view .lines-num:hover { + color: var(--color-text-dark); +} + +.file-view.code-view .ui.button.code-line-button { + border: 1px solid var(--color-secondary); + padding: 1px 4px; + margin: 0; + min-height: 0; + position: absolute; + left: 6px; +} + +.file-view.code-view .ui.button.code-line-button:hover { + background: var(--color-secondary); +} + +.view-raw { + display: flex; + justify-content: center; +} + +.view-raw > * { + max-width: 100%; +} + +.view-raw audio, +.view-raw video, +.view-raw img { + margin: 1rem; + border-radius: 0; + object-fit: contain; +} + +.view-raw img[src$=".svg" i] { + max-height: 600px !important; + max-width: 600px !important; +} + +.file-view-render-container { + width: 100%; +} + +.file-view-render-container :last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); /* to match the "ui segment" bottom radius */ +} diff --git a/web_src/css/repo/home.css b/web_src/css/repo/home.css index 61b0a1f962..ee371f1b1c 100644 --- a/web_src/css/repo/home.css +++ b/web_src/css/repo/home.css @@ -67,6 +67,7 @@ .repo-view-content { flex: 1; + min-width: 0; } .language-stats { diff --git a/web_src/css/repo/issue-card.css b/web_src/css/repo/issue-card.css index fb832bd05a..327919b1fe 100644 --- a/web_src/css/repo/issue-card.css +++ b/web_src/css/repo/issue-card.css @@ -7,6 +7,7 @@ padding: 8px 10px; border: 1px solid var(--color-secondary); background: var(--color-card); + color: var(--color-text); /* it can't inherit from parent because the card already has its own background */ } .issue-card-icon, @@ -28,13 +29,16 @@ display: flex; width: 100%; justify-content: space-between; - gap: 0.25em; + gap: 1em; } -.issue-card-assignees { +.issue-card-bottom-part { display: flex; + flex: 1; align-items: center; gap: 0.25em; - justify-content: end; flex-wrap: wrap; + overflow: hidden; + max-width: fit-content; + max-height: fit-content; } diff --git a/web_src/css/repo/issue-label.css b/web_src/css/repo/issue-label.css index 0a25d31da9..f75c73b50f 100644 --- a/web_src/css/repo/issue-label.css +++ b/web_src/css/repo/issue-label.css @@ -4,41 +4,46 @@ margin: 0; } -.issue-label-list .item { +.issue-label-list > .item { border-bottom: 1px solid var(--color-secondary); display: flex; padding: 1em 0; margin: 0; } -.issue-label-list .item:first-child { +.issue-label-list > .item:first-child { padding-top: 0; } -.issue-label-list .item:last-child { +.issue-label-list > .item:last-child { border-bottom: none; padding-bottom: 0; } -.issue-label-list .item .label-title { +.issue-label-list > .item .label-title { width: 33%; + padding-right: 1em; } -.issue-label-list .item .label-issues { +.issue-label-list > .item .label-issues { width: 33%; + padding-right: 1em; } -.issue-label-list .item .label-operation { +.issue-label-list > .item .label-operation { width: 33%; + display: flex; + flex-wrap: wrap; + gap: 0.5em; + justify-content: end; + align-items: center; } -.issue-label-list .item a { +.issue-label-list > .item .label-operation a { font-size: 12px; - padding-right: 10px; - color: var(--color-text-light); } -.issue-label-list .item.org-label { +.issue-label-list > .item.org-label { opacity: 0.7; } diff --git a/web_src/css/repo/linebutton.css b/web_src/css/repo/linebutton.css deleted file mode 100644 index f7e3d48b48..0000000000 --- a/web_src/css/repo/linebutton.css +++ /dev/null @@ -1,16 +0,0 @@ -.code-view .lines-num:hover { - color: var(--color-text-dark) !important; -} - -.ui.button.code-line-button { - border: 1px solid var(--color-secondary); - padding: 1px 4px; - margin: 0; - min-height: 0; - position: absolute; - left: 6px; -} - -.ui.button.code-line-button:hover { - background: var(--color-secondary); -} diff --git a/web_src/css/repo/packages.css b/web_src/css/repo/packages.css new file mode 100644 index 0000000000..75675f5243 --- /dev/null +++ b/web_src/css/repo/packages.css @@ -0,0 +1,25 @@ +.packages-content { + display: flex; + align-items: flex-start; + gap: 16px; +} + +.packages-content-left { + margin: 0 !important; + width: calc(100% - 250px - 16px); +} + +.packages-content-right { + margin: 0 !important; + width: 250px; +} + +@media (max-width: 767.98px) { + .packages-content { + flex-direction: column; + } + .packages-content-left, + .packages-content-right { + width: 100%; + } +} diff --git a/web_src/css/repo/release-tag.css b/web_src/css/repo/release-tag.css index a37f46b14c..4b42c992ef 100644 --- a/web_src/css/repo/release-tag.css +++ b/web_src/css/repo/release-tag.css @@ -70,8 +70,12 @@ flex-wrap: wrap; } +#release-list .release-entry .attachment-list > .item a { + min-width: 300px; +} + #release-list .release-entry .attachment-list .attachment-right-info { - flex-grow: 1; + flex-shrink: 0; min-width: 300px; } @@ -87,10 +91,6 @@ border-bottom: none; } -#tags-table .tag-list-row { - padding: 8px 12px; -} - #tags-table .tag-list-row-title { font-size: 18px; font-weight: var(--font-weight-normal); diff --git a/web_src/css/repo/wiki.css b/web_src/css/repo/wiki.css index ca59dadb9c..144cb1206c 100644 --- a/web_src/css/repo/wiki.css +++ b/web_src/css/repo/wiki.css @@ -39,10 +39,6 @@ min-width: 150px; } -.repository.wiki .wiki-content-sidebar .ui.message.unicode-escape-prompt p { - display: none; -} - .repository.wiki .wiki-content-footer { margin-top: 1em; } diff --git a/web_src/css/shared/flex-list.css b/web_src/css/shared/flex-list.css index 0f54779252..24abe8fd9d 100644 --- a/web_src/css/shared/flex-list.css +++ b/web_src/css/shared/flex-list.css @@ -33,14 +33,6 @@ color: var(--color-primary) !important; } -.flex-item .flex-item-icon { - align-self: baseline; /* mainly used by the issue list, to align the leading icon with the title */ -} - -.flex-item .flex-item-icon + .flex-item-main { - align-self: baseline; -} - .flex-item .flex-item-trailing { display: flex; gap: 0.5rem; @@ -54,7 +46,9 @@ display: inline-flex; flex-wrap: wrap; align-items: center; - gap: .25rem; + /* labels are under effect of this gap here because they are display:contents. Ideally we should make wrapping + of labels work without display: contents and set this to a static value again. */ + gap: var(--gap-inline); max-width: 100%; color: var(--color-text); font-size: 16px; diff --git a/web_src/css/themes/theme-gitea-dark.css b/web_src/css/themes/theme-gitea-dark.css index 5ddee0a746..48fbd14dfb 100644 --- a/web_src/css/themes/theme-gitea-dark.css +++ b/web_src/css/themes/theme-gitea-dark.css @@ -185,6 +185,7 @@ gitea-theme-meta-info { --color-orange-badge-bg: #f2711c1a; --color-orange-badge-hover-bg: #f2711c4d; --color-git: #f05133; + --color-logo: #609926; /* target-based colors */ --color-body: #1b1f23; --color-box-header: #1a1d1f; diff --git a/web_src/css/themes/theme-gitea-light.css b/web_src/css/themes/theme-gitea-light.css index 1a4183c0d2..eaff717417 100644 --- a/web_src/css/themes/theme-gitea-light.css +++ b/web_src/css/themes/theme-gitea-light.css @@ -185,6 +185,7 @@ gitea-theme-meta-info { --color-orange-badge-bg: #f2711c1a; --color-orange-badge-hover-bg: #f2711c4d; --color-git: #f05133; + --color-logo: #609926; /* target-based colors */ --color-body: #ffffff; --color-box-header: #f1f3f5; diff --git a/web_src/fomantic/build/components/dropdown.js b/web_src/fomantic/build/components/dropdown.js index 9d0e07b33b..3ad0984865 100644 --- a/web_src/fomantic/build/components/dropdown.js +++ b/web_src/fomantic/build/components/dropdown.js @@ -525,6 +525,7 @@ $.fn.dropdown = function(parameters) { return true; } if(settings.onShow.call(element) !== false) { + $module.fomanticExt.onDropdownAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items module.animate.show(function() { if( module.can.click() ) { module.bind.intent(); @@ -752,7 +753,7 @@ $.fn.dropdown = function(parameters) { if(module.is.searchSelection() && module.can.show() && module.is.focusedOnSearch() ) { module.show(); } - settings.onAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items + $module.fomanticExt.onDropdownAfterFiltered.call(element); // GITEA-PATCH: callback to correctly handle the filtered items } ; if(settings.useLabels && module.has.maxSelections()) { @@ -3993,8 +3994,6 @@ $.fn.dropdown.settings = { onShow : function(){}, onHide : function(){}, - onAfterFiltered: function(){}, // GITEA-PATCH: callback to correctly handle the filtered items - /* Component */ name : 'Dropdown', namespace : 'dropdown', diff --git a/web_src/fomantic/build/components/modal.js b/web_src/fomantic/build/components/modal.js index 420ecc250b..3f578ccfcc 100644 --- a/web_src/fomantic/build/components/modal.js +++ b/web_src/fomantic/build/components/modal.js @@ -467,7 +467,7 @@ $.fn.modal = function(parameters) { ignoreRepeatedEvents = false; return false; } - + $module.fomanticExt.onModalBeforeHidden.call(element); // GITEA-PATCH: handle more UI updates before hidden if( module.is.animating() || module.is.active() ) { if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) { module.remove.active(); @@ -641,7 +641,7 @@ $.fn.modal = function(parameters) { $module .off('mousedown' + elementEventNamespace) ; - } + } $dimmer .off('mousedown' + elementEventNamespace) ; @@ -877,7 +877,7 @@ $.fn.modal = function(parameters) { ? $(document).scrollTop() + settings.padding : $(document).scrollTop() + (module.cache.contextHeight - module.cache.height - settings.padding), marginLeft: -(module.cache.width / 2) - }) + }) ; } else { $module @@ -886,7 +886,7 @@ $.fn.modal = function(parameters) { ? -(module.cache.height / 2) : settings.padding / 2, marginLeft: -(module.cache.width / 2) - }) + }) ; } module.verbose('Setting modal offset for legacy mode'); diff --git a/web_src/js/bootstrap.ts b/web_src/js/bootstrap.ts index 9e41673b86..96a2759a23 100644 --- a/web_src/js/bootstrap.ts +++ b/web_src/js/bootstrap.ts @@ -2,6 +2,7 @@ // to make sure the error handler always works, we should never import `window.config`, because // some user's custom template breaks it. import type {Intent} from './types.ts'; +import {html} from './utils/html.ts'; // This sets up the URL prefix used in webpack's chunk loading. // This file must be imported before any lazy-loading is being attempted. @@ -23,7 +24,7 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { let msgDiv = msgContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); if (!msgDiv) { const el = document.createElement('div'); - el.innerHTML = `<div class="ui container js-global-error tw-my-[--page-spacing]"><div class="ui ${msgType} message tw-text-center tw-whitespace-pre-line"></div></div>`; + el.innerHTML = html`<div class="ui container js-global-error tw-my-[--page-spacing]"><div class="ui ${msgType} message tw-text-center tw-whitespace-pre-line"></div></div>`; msgDiv = el.childNodes[0] as HTMLDivElement; } // merge duplicated messages into "the message (count)" format diff --git a/web_src/js/components/ActivityHeatmap.vue b/web_src/js/components/ActivityHeatmap.vue index eaa9b0ffb1..296cb61cff 100644 --- a/web_src/js/components/ActivityHeatmap.vue +++ b/web_src/js/components/ActivityHeatmap.vue @@ -1,7 +1,7 @@ <script lang="ts" setup> // TODO: Switch to upstream after https://github.com/razorness/vue3-calendar-heatmap/pull/34 is merged import {CalendarHeatmap} from '@silverwind/vue3-calendar-heatmap'; -import {onMounted, ref} from 'vue'; +import {onMounted, shallowRef} from 'vue'; import type {Value as HeatmapValue, Locale as HeatmapLocale} from '@silverwind/vue3-calendar-heatmap'; defineProps<{ @@ -24,7 +24,7 @@ const colorRange = [ 'var(--color-primary-dark-4)', ]; -const endDate = ref(new Date()); +const endDate = shallowRef(new Date()); onMounted(() => { // work around issue with first legend color being rendered twice and legend cut off diff --git a/web_src/js/components/ContextPopup.vue b/web_src/js/components/ContextPopup.vue index 0aae202d42..5ec4499e48 100644 --- a/web_src/js/components/ContextPopup.vue +++ b/web_src/js/components/ContextPopup.vue @@ -2,16 +2,16 @@ import {SvgIcon} from '../svg.ts'; import {GET} from '../modules/fetch.ts'; import {getIssueColor, getIssueIcon} from '../features/issue.ts'; -import {computed, onMounted, ref} from 'vue'; +import {computed, onMounted, shallowRef, useTemplateRef} from 'vue'; import type {IssuePathInfo} from '../types.ts'; const {appSubUrl, i18n} = window.config; -const loading = ref(false); -const issue = ref(null); -const renderedLabels = ref(''); +const loading = shallowRef(false); +const issue = shallowRef(null); +const renderedLabels = shallowRef(''); const i18nErrorOccurred = i18n.error_occurred; -const i18nErrorMessage = ref(null); +const i18nErrorMessage = shallowRef(null); const createdAt = computed(() => new Date(issue.value.created_at).toLocaleDateString(undefined, {year: 'numeric', month: 'short', day: 'numeric'})); const body = computed(() => { @@ -22,7 +22,7 @@ const body = computed(() => { return body; }); -const root = ref<HTMLElement | null>(null); +const root = useTemplateRef('root'); onMounted(() => { root.value.addEventListener('ce-load-context-popup', (e: CustomEventInit<IssuePathInfo>) => { diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index fc6a7bd281..e938814ec6 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -6,7 +6,7 @@ import {fomanticQuery} from '../modules/fomantic/base.ts'; const {appSubUrl, assetUrlPrefix, pageData} = window.config; -type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning'; +type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning' | 'skipped'; type CommitStatusMap = { [status in CommitStatus]: { @@ -22,6 +22,7 @@ const commitStatus: CommitStatusMap = { error: {name: 'gitea-exclamation', color: 'red'}, failure: {name: 'octicon-x', color: 'red'}, warning: {name: 'gitea-exclamation', color: 'yellow'}, + skipped: {name: 'octicon-skip', color: 'grey'}, }; export default defineComponent({ @@ -38,7 +39,7 @@ export default defineComponent({ return { tab, repos: [], - reposTotalCount: 0, + reposTotalCount: null, reposFilter, archivedFilter, privateFilter, @@ -112,9 +113,6 @@ export default defineComponent({ const el = document.querySelector('#dashboard-repo-list'); this.changeReposFilter(this.reposFilter); fomanticQuery(el.querySelector('.ui.dropdown')).dropdown(); - nextTick(() => { - this.$refs.search?.focus(); - }); this.textArchivedFilterTitles = { 'archived': this.textShowOnlyArchived, @@ -218,7 +216,9 @@ export default defineComponent({ this.searchRepos(); }, - changePage(page: number) { + async changePage(page: number) { + if (this.isLoading) return; + this.page = page; if (this.page > this.finalPage) { this.page = this.finalPage; @@ -228,7 +228,7 @@ export default defineComponent({ } this.repos = []; this.counts[`${this.reposFilter}:${this.archivedFilter}:${this.privateFilter}`] = 0; - this.searchRepos(); + await this.searchRepos(); }, async searchRepos() { @@ -240,12 +240,20 @@ export default defineComponent({ let response, json; try { + const firstLoad = this.reposTotalCount === null; if (!this.reposTotalCount) { const totalCountSearchURL = `${this.subUrl}/repo/search?count_only=1&uid=${this.uid}&team_id=${this.teamId}&q=&page=1&mode=`; response = await GET(totalCountSearchURL); this.reposTotalCount = parseInt(response.headers.get('X-Total-Count') ?? '0'); } - + if (firstLoad && this.reposTotalCount) { + nextTick(() => { + // MDN: If there's no focused element, this is the Document.body or Document.documentElement. + if ((document.activeElement === document.body || document.activeElement === document.documentElement)) { + this.$refs.search.focus({preventScroll: true}); + } + }); + } response = await GET(searchedURL); json = await response.json(); } catch { @@ -298,7 +306,7 @@ export default defineComponent({ return commitStatus[status].color; }, - reposFilterKeyControl(e: KeyboardEvent) { + async reposFilterKeyControl(e: KeyboardEvent) { switch (e.key) { case 'Enter': document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click(); @@ -307,7 +315,7 @@ export default defineComponent({ if (this.activeIndex > 0) { this.activeIndex--; } else if (this.page > 1) { - this.changePage(this.page - 1); + await this.changePage(this.page - 1); this.activeIndex = this.searchLimit - 1; } break; @@ -316,17 +324,17 @@ export default defineComponent({ this.activeIndex++; } else if (this.page < this.finalPage) { this.activeIndex = 0; - this.changePage(this.page + 1); + await this.changePage(this.page + 1); } break; case 'ArrowRight': if (this.page < this.finalPage) { - this.changePage(this.page + 1); + await this.changePage(this.page + 1); } break; case 'ArrowLeft': if (this.page > 1) { - this.changePage(this.page - 1); + await this.changePage(this.page - 1); } break; } @@ -347,7 +355,7 @@ export default defineComponent({ <h4 class="ui top attached header tw-flex tw-items-center"> <div class="tw-flex-1 tw-flex tw-items-center"> {{ textMyRepos }} - <span class="ui grey label tw-ml-2">{{ reposTotalCount }}</span> + <span v-if="reposTotalCount" class="ui grey label tw-ml-2">{{ reposTotalCount }}</span> </div> <a class="tw-flex tw-items-center muted" :href="subUrl + '/repo/create' + (isOrganization ? '?org=' + organizationId : '')" :data-tooltip-content="textNewRepo"> <svg-icon name="octicon-plus"/> @@ -418,7 +426,7 @@ export default defineComponent({ </div> <div v-if="repos.length" class="ui attached table segment tw-rounded-b"> <ul class="repo-owner-name-list"> - <li class="tw-flex tw-items-center tw-py-2" v-for="repo, index in repos" :class="{'active': index === activeIndex}" :key="repo.id"> + <li class="tw-flex tw-items-center tw-py-2" v-for="(repo, index) in repos" :class="{'active': index === activeIndex}" :key="repo.id"> <a class="repo-list-link muted" :href="repo.link"> <svg-icon :name="repoIcon(repo)" :size="16" class="repo-list-icon"/> <div class="text truncate">{{ repo.full_name }}</div> @@ -426,7 +434,7 @@ export default defineComponent({ <svg-icon name="octicon-archive" :size="16"/> </div> </a> - <a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link" :data-tooltip-content="repo.locale_latest_commit_status_state"> + <a class="tw-flex tw-items-center" v-if="repo.latest_commit_status_state" :href="repo.latest_commit_status_state_link || null" :data-tooltip-content="repo.locale_latest_commit_status_state"> <!-- the commit status icon logic is taken from templates/repo/commit_status.tmpl --> <svg-icon :name="statusIcon(repo.latest_commit_status_state)" :class="'tw-ml-2 commit-status icon text ' + statusColor(repo.latest_commit_status_state)" :size="16"/> </a> diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue index 5426a672cb..981d10c1c1 100644 --- a/web_src/js/components/DiffFileTree.vue +++ b/web_src/js/components/DiffFileTree.vue @@ -60,8 +60,8 @@ function updateState(visible: boolean) { </script> <template> + <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often --> <div v-if="store.fileTreeIsVisible" class="diff-file-tree-items"> - <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often --> <DiffFileTreeItem v-for="item in store.diffFileTree.TreeRoot.Children" :key="item.FullName" :item="item"/> </div> </template> diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue index 24bf590082..f15f093ff8 100644 --- a/web_src/js/components/DiffFileTreeItem.vue +++ b/web_src/js/components/DiffFileTreeItem.vue @@ -1,6 +1,6 @@ <script lang="ts" setup> import {SvgIcon, type SvgName} from '../svg.ts'; -import {ref} from 'vue'; +import {shallowRef} from 'vue'; import {type DiffStatus, type DiffTreeEntry, diffTreeStore} from '../modules/diff-file.ts'; const props = defineProps<{ @@ -8,7 +8,7 @@ const props = defineProps<{ }>(); const store = diffTreeStore(); -const collapsed = ref(props.item.IsViewed); +const collapsed = shallowRef(props.item.IsViewed); function getIconForDiffStatus(pType: DiffStatus) { const diffTypes: Record<DiffStatus, { name: SvgName, classes: Array<string> }> = { diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index 4f291f5ca1..b2c28414c0 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -1,19 +1,19 @@ <script lang="ts" setup> -import {computed, onMounted, onUnmounted, ref, watch} from 'vue'; +import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue'; import {SvgIcon} from '../svg.ts'; import {toggleElem} from '../utils/dom.ts'; const {csrfToken, pageData} = window.config; -const mergeForm = ref(pageData.pullRequestMergeForm); +const mergeForm = pageData.pullRequestMergeForm; -const mergeTitleFieldValue = ref(''); -const mergeMessageFieldValue = ref(''); -const deleteBranchAfterMerge = ref(false); -const autoMergeWhenSucceed = ref(false); +const mergeTitleFieldValue = shallowRef(''); +const mergeMessageFieldValue = shallowRef(''); +const deleteBranchAfterMerge = shallowRef(false); +const autoMergeWhenSucceed = shallowRef(false); -const mergeStyle = ref(''); -const mergeStyleDetail = ref({ +const mergeStyle = shallowRef(''); +const mergeStyleDetail = shallowRef({ hideMergeMessageTexts: false, textDoMerge: '', mergeTitleFieldText: '', @@ -21,33 +21,33 @@ const mergeStyleDetail = ref({ hideAutoMerge: false, }); -const mergeStyleAllowedCount = ref(0); +const mergeStyleAllowedCount = shallowRef(0); -const showMergeStyleMenu = ref(false); -const showActionForm = ref(false); +const showMergeStyleMenu = shallowRef(false); +const showActionForm = shallowRef(false); const mergeButtonStyleClass = computed(() => { - if (mergeForm.value.allOverridableChecksOk) return 'primary'; + if (mergeForm.allOverridableChecksOk) return 'primary'; return autoMergeWhenSucceed.value ? 'primary' : 'red'; }); const forceMerge = computed(() => { - return mergeForm.value.canMergeNow && !mergeForm.value.allOverridableChecksOk; + return mergeForm.canMergeNow && !mergeForm.allOverridableChecksOk; }); watch(mergeStyle, (val) => { - mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e: any) => e.name === val); + mergeStyleDetail.value = mergeForm.mergeStyles.find((e: any) => e.name === val); for (const elem of document.querySelectorAll('[data-pull-merge-style]')) { toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); } }); onMounted(() => { - mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0); + mergeStyleAllowedCount.value = mergeForm.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0); - let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name; - if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name; - switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow); + let mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.defaultMergeStyle)?.name; + if (!mergeStyle) mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed)?.name; + switchMergeStyle(mergeStyle, !mergeForm.canMergeNow); document.addEventListener('mouseup', hideMergeStyleMenu); }); @@ -63,7 +63,7 @@ function hideMergeStyleMenu() { function toggleActionForm(show: boolean) { showActionForm.value = show; if (!show) return; - deleteBranchAfterMerge.value = mergeForm.value.defaultDeleteBranchAfterMerge; + deleteBranchAfterMerge.value = mergeForm.defaultDeleteBranchAfterMerge; mergeTitleFieldValue.value = mergeStyleDetail.value.mergeTitleFieldText; mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText; } @@ -74,7 +74,7 @@ function switchMergeStyle(name: string, autoMerge = false) { } function clearMergeMessage() { - mergeMessageFieldValue.value = mergeForm.value.defaultMergeMessage; + mergeMessageFieldValue.value = mergeForm.defaultMergeMessage; } </script> diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index af300622b4..2eb2211269 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -177,7 +177,7 @@ export default defineComponent({ }, }, - async mounted() { // eslint-disable-line @typescript-eslint/no-misused-promises + async mounted() { // load job data and then auto-reload periodically // need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener await this.loadJob(); @@ -439,7 +439,8 @@ export default defineComponent({ }); </script> <template> - <div class="ui container action-view-container"> + <!-- make the view container full width to make users easier to read logs --> + <div class="ui fluid container"> <div class="action-view-header"> <div class="action-info-summary"> <div class="action-info-summary-title"> diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index 77b85bd7e2..bbdfda41d0 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -1,9 +1,9 @@ <script lang="ts" setup> // @ts-expect-error - module exports no types import {VueBarGraph} from 'vue-bar-graph'; -import {computed, onMounted, ref} from 'vue'; +import {computed, onMounted, shallowRef, useTemplateRef} from 'vue'; -const colors = ref({ +const colors = shallowRef({ barColor: 'green', textColor: 'black', textAltColor: 'white', @@ -41,8 +41,8 @@ const graphWidth = computed(() => { return activityTopAuthors.length * 40; }); -const styleElement = ref<HTMLElement | null>(null); -const altStyleElement = ref<HTMLElement | null>(null); +const styleElement = useTemplateRef('styleElement'); +const altStyleElement = useTemplateRef('altStyleElement'); onMounted(() => { const refStyle = window.getComputedStyle(styleElement.value); diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue index f04fc065b6..f331a26fe9 100644 --- a/web_src/js/components/RepoCodeFrequency.vue +++ b/web_src/js/components/RepoCodeFrequency.vue @@ -23,7 +23,7 @@ import { import {chartJsColors} from '../utils/color.ts'; import {sleep} from '../utils.ts'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; -import {onMounted, ref} from 'vue'; +import {onMounted, shallowRef} from 'vue'; const {pageData} = window.config; @@ -47,10 +47,10 @@ defineProps<{ }; }>(); -const isLoading = ref(false); -const errorText = ref(''); -const repoLink = ref(pageData.repoLink || []); -const data = ref<DayData[]>([]); +const isLoading = shallowRef(false); +const errorText = shallowRef(''); +const repoLink = pageData.repoLink; +const data = shallowRef<DayData[]>([]); onMounted(() => { fetchGraphData(); @@ -61,7 +61,7 @@ async function fetchGraphData() { try { let response: Response; do { - response = await GET(`${repoLink.value}/activity/code-frequency/data`); + response = await GET(`${repoLink}/activity/code-frequency/data`); if (response.status === 202) { await sleep(1000); // wait for 1 second before retrying } diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue index 1006ea30bb..754acb997d 100644 --- a/web_src/js/components/RepoContributors.vue +++ b/web_src/js/components/RepoContributors.vue @@ -397,7 +397,7 @@ export default defineComponent({ <div class="ui top attached header tw-flex tw-flex-1"> <b class="ui right">#{{ index + 1 }}</b> <a :href="contributor.home_link"> - <img class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link" alt=""> + <img loading="lazy" class="ui avatar tw-align-middle" height="40" width="40" :src="contributor.avatar_link" alt=""> </a> <div class="tw-ml-2"> <a v-if="contributor.home_link !== ''" :href="contributor.home_link"><h4>{{ contributor.name }}</h4></a> diff --git a/web_src/js/components/RepoRecentCommits.vue b/web_src/js/components/RepoRecentCommits.vue index 1b3d8fd459..27aa27dfc3 100644 --- a/web_src/js/components/RepoRecentCommits.vue +++ b/web_src/js/components/RepoRecentCommits.vue @@ -21,7 +21,7 @@ import { import {chartJsColors} from '../utils/color.ts'; import {sleep} from '../utils.ts'; import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; -import {onMounted, ref} from 'vue'; +import {onMounted, ref, shallowRef} from 'vue'; const {pageData} = window.config; @@ -43,9 +43,9 @@ defineProps<{ }; }>(); -const isLoading = ref(false); -const errorText = ref(''); -const repoLink = ref(pageData.repoLink || []); +const isLoading = shallowRef(false); +const errorText = shallowRef(''); +const repoLink = pageData.repoLink; const data = ref<DayData[]>([]); onMounted(() => { @@ -57,7 +57,7 @@ async function fetchGraphData() { try { let response: Response; do { - response = await GET(`${repoLink.value}/activity/recent-commits/data`); + response = await GET(`${repoLink}/activity/recent-commits/data`); if (response.status === 202) { await sleep(1000); // wait for 1 second before retrying } diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue index c692142792..1f90f92586 100644 --- a/web_src/js/components/ViewFileTree.vue +++ b/web_src/js/components/ViewFileTree.vue @@ -1,11 +1,9 @@ <script lang="ts" setup> import ViewFileTreeItem from './ViewFileTreeItem.vue'; -import {onMounted, ref} from 'vue'; -import {pathEscapeSegments} from '../utils/url.ts'; -import {GET} from '../modules/fetch.ts'; -import {createElementFromHTML} from '../utils/dom.ts'; +import {onMounted, useTemplateRef} from 'vue'; +import {createViewFileTreeStore} from './ViewFileTreeStore.ts'; -const elRoot = ref<HTMLElement | null>(null); +const elRoot = useTemplateRef('elRoot'); const props = defineProps({ repoLink: {type: String, required: true}, @@ -13,52 +11,20 @@ const props = defineProps({ currentRefNameSubURL: {type: String, required: true}, }); -const files = ref([]); -const selectedItem = ref(''); - -async function loadChildren(treePath: string, subPath: string = '') { - const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`); - const json = await response.json(); - const poolSvgs = []; - for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) { - if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent); - } - if (poolSvgs.length) { - const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>'); - svgContainer.innerHTML = poolSvgs.join(''); - document.body.append(svgContainer); - } - return json.fileTreeNodes ?? null; -} - -async function loadViewContent(url: string) { - url = url.includes('?') ? url.replace('?', '?only_content=true') : `${url}?only_content=true`; - const response = await GET(url); - document.querySelector('.repo-view-content').innerHTML = await response.text(); -} - -async function navigateTreeView(treePath: string) { - const url = `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`; - window.history.pushState({treePath, url}, null, url); - selectedItem.value = treePath; - await loadViewContent(url); -} - +const store = createViewFileTreeStore(props); onMounted(async () => { - selectedItem.value = props.treePath; - files.value = await loadChildren('', props.treePath); + store.rootFiles = await store.loadChildren('', props.treePath); elRoot.value.closest('.is-loading')?.classList?.remove('is-loading'); window.addEventListener('popstate', (e) => { - selectedItem.value = e.state?.treePath || ''; - if (e.state?.url) loadViewContent(e.state.url); + store.selectedItem = e.state?.treePath || ''; + if (e.state?.url) store.loadViewContent(e.state.url); }); }); </script> <template> <div class="view-file-tree-items" ref="elRoot"> - <!-- only render the tree if we're visible. in many cases this is something that doesn't change very often --> - <ViewFileTreeItem v-for="item in files" :key="item.name" :item="item" :selected-item="selectedItem" :navigate-view-content="navigateTreeView" :load-children="loadChildren"/> + <ViewFileTreeItem v-for="item in store.rootFiles" :key="item.name" :item="item" :store="store"/> </div> </template> diff --git a/web_src/js/components/ViewFileTreeItem.vue b/web_src/js/components/ViewFileTreeItem.vue index c39fa1f4ae..5173c7eb46 100644 --- a/web_src/js/components/ViewFileTreeItem.vue +++ b/web_src/js/components/ViewFileTreeItem.vue @@ -1,10 +1,12 @@ <script lang="ts" setup> import {SvgIcon} from '../svg.ts'; -import {ref} from 'vue'; +import {isPlainClick} from '../utils/dom.ts'; +import {shallowRef} from 'vue'; +import {type createViewFileTreeStore} from './ViewFileTreeStore.ts'; type Item = { entryName: string; - entryMode: string; + entryMode: 'blob' | 'exec' | 'tree' | 'commit' | 'symlink' | 'unknown'; entryIcon: string; entryIconOpen: string; fullPath: string; @@ -14,103 +16,67 @@ type Item = { const props = defineProps<{ item: Item, - navigateViewContent:(treePath: string) => void, - loadChildren:(treePath: string, subPath?: string) => Promise<Item[]>, - selectedItem?: string, + store: ReturnType<typeof createViewFileTreeStore> }>(); -const isLoading = ref(false); -const children = ref(props.item.children); -const collapsed = ref(!props.item.children); +const store = props.store; +const isLoading = shallowRef(false); +const children = shallowRef(props.item.children); +const collapsed = shallowRef(!props.item.children); const doLoadChildren = async () => { collapsed.value = !collapsed.value; - if (!collapsed.value && props.loadChildren) { + if (!collapsed.value) { isLoading.value = true; try { - children.value = await props.loadChildren(props.item.fullPath); + children.value = await store.loadChildren(props.item.fullPath); } finally { isLoading.value = false; } } }; -const doLoadDirContent = () => { - doLoadChildren(); - props.navigateViewContent(props.item.fullPath); +const onItemClick = (e: MouseEvent) => { + // only handle the click event with page partial reloading if the user didn't press any special key + // let browsers handle special keys like "Ctrl+Click" + if (!isPlainClick(e)) return; + e.preventDefault(); + if (props.item.entryMode === 'tree') doLoadChildren(); + store.navigateTreeView(props.item.fullPath); }; -const doLoadFileContent = () => { - props.navigateViewContent(props.item.fullPath); -}; - -const doGotoSubModule = () => { - location.href = props.item.submoduleUrl; -}; </script> -<!--title instead of tooltip above as the tooltip needs too much work with the current methods, i.e. not being loaded or staying open for "too long"--> <template> - <div - v-if="item.entryMode === 'commit'" class="tree-item type-submodule" - :title="item.entryName" - @click.stop="doGotoSubModule" - > - <!-- submodule --> - <div class="item-content"> - <!-- eslint-disable-next-line vue/no-v-html --> - <span class="tw-contents" v-html="item.entryIcon"/> - <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span> - </div> - </div> - <div - v-else-if="item.entryMode === 'symlink'" class="tree-item type-symlink" - :class="{'selected': selectedItem === item.fullPath}" + <a + class="tree-item silenced" + :class="{ + 'selected': store.selectedItem === item.fullPath, + 'type-submodule': item.entryMode === 'commit', + 'type-directory': item.entryMode === 'tree', + 'type-symlink': item.entryMode === 'symlink', + 'type-file': item.entryMode === 'blob' || item.entryMode === 'exec', + }" :title="item.entryName" - @click.stop="doLoadFileContent" + :href="store.buildTreePathWebUrl(item.fullPath)" + @click.stop="onItemClick" > - <!-- symlink --> - <div class="item-content"> - <!-- eslint-disable-next-line vue/no-v-html --> - <span class="tw-contents" v-html="item.entryIcon"/> - <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span> - </div> - </div> - <div - v-else-if="item.entryMode !== 'tree'" class="tree-item type-file" - :class="{'selected': selectedItem === item.fullPath}" - :title="item.entryName" - @click.stop="doLoadFileContent" - > - <!-- file --> - <div class="item-content"> - <!-- eslint-disable-next-line vue/no-v-html --> - <span class="tw-contents" v-html="item.entryIcon"/> - <span class="gt-ellipsis tw-flex-1">{{ item.entryName }}</span> - </div> - </div> - <div - v-else class="tree-item type-directory" - :class="{'selected': selectedItem === item.fullPath}" - :title="item.entryName" - @click.stop="doLoadDirContent" - > - <!-- directory --> - <div class="item-toggle"> + <div v-if="item.entryMode === 'tree'" class="item-toggle"> <SvgIcon v-if="isLoading" name="octicon-sync" class="circular-spin"/> - <SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop="doLoadChildren"/> + <SvgIcon v-else :name="collapsed ? 'octicon-chevron-right' : 'octicon-chevron-down'" @click.stop.prevent="doLoadChildren"/> </div> <div class="item-content"> <!-- eslint-disable-next-line vue/no-v-html --> <span class="tw-contents" v-html="(!collapsed && item.entryIconOpen) ? item.entryIconOpen : item.entryIcon"/> <span class="gt-ellipsis">{{ item.entryName }}</span> </div> - </div> + </a> <div v-if="children?.length" v-show="!collapsed" class="sub-items"> - <ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :selected-item="selectedItem" :navigate-view-content="navigateViewContent" :load-children="loadChildren"/> + <ViewFileTreeItem v-for="childItem in children" :key="childItem.entryName" :item="childItem" :store="store"/> </div> </template> + <style scoped> .sub-items { display: flex; diff --git a/web_src/js/components/ViewFileTreeStore.ts b/web_src/js/components/ViewFileTreeStore.ts new file mode 100644 index 0000000000..e2155bd58a --- /dev/null +++ b/web_src/js/components/ViewFileTreeStore.ts @@ -0,0 +1,45 @@ +import {reactive} from 'vue'; +import {GET} from '../modules/fetch.ts'; +import {pathEscapeSegments} from '../utils/url.ts'; +import {createElementFromHTML} from '../utils/dom.ts'; +import {html} from '../utils/html.ts'; + +export function createViewFileTreeStore(props: { repoLink: string, treePath: string, currentRefNameSubURL: string}) { + const store = reactive({ + rootFiles: [], + selectedItem: props.treePath, + + async loadChildren(treePath: string, subPath: string = '') { + const response = await GET(`${props.repoLink}/tree-view/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}?sub_path=${encodeURIComponent(subPath)}`); + const json = await response.json(); + const poolSvgs = []; + for (const [svgId, svgContent] of Object.entries(json.renderedIconPool ?? {})) { + if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent); + } + if (poolSvgs.length) { + const svgContainer = createElementFromHTML(html`<div class="global-svg-icon-pool tw-hidden"></div>`); + svgContainer.innerHTML = poolSvgs.join(''); + document.body.append(svgContainer); + } + return json.fileTreeNodes ?? null; + }, + + async loadViewContent(url: string) { + url = url.includes('?') ? url.replace('?', '?only_content=true') : `${url}?only_content=true`; + const response = await GET(url); + document.querySelector('.repo-view-content').innerHTML = await response.text(); + }, + + async navigateTreeView(treePath: string) { + const url = store.buildTreePathWebUrl(treePath); + window.history.pushState({treePath, url}, null, url); + store.selectedItem = treePath; + await store.loadViewContent(url); + }, + + buildTreePathWebUrl(treePath: string) { + return `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`; + }, + }); + return store; +} diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts index ae399e48b3..22a7890857 100644 --- a/web_src/js/features/common-button.ts +++ b/web_src/js/features/common-button.ts @@ -43,13 +43,16 @@ export function initGlobalDeleteButton(): void { fomanticQuery(modal).modal({ closable: false, - onApprove: async () => { + onApprove: () => { // if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."` if (btn.getAttribute('data-type') === 'form') { const formSelector = btn.getAttribute('data-form'); const form = document.querySelector<HTMLFormElement>(formSelector); if (!form) throw new Error(`no form named ${formSelector} found`); + modal.classList.add('is-loading'); // the form is not in the modal, so also add loading indicator to the modal + form.classList.add('is-loading'); form.submit(); + return false; // prevent modal from closing automatically } // prepare an AJAX form by data attributes @@ -62,12 +65,15 @@ export function initGlobalDeleteButton(): void { postData.append('id', value); } } - - const response = await POST(btn.getAttribute('data-url'), {data: postData}); - if (response.ok) { - const data = await response.json(); - window.location.href = data.redirect; - } + (async () => { + const response = await POST(btn.getAttribute('data-url'), {data: postData}); + if (response.ok) { + const data = await response.json(); + window.location.href = data.redirect; + } + })(); + modal.classList.add('is-loading'); // the request is in progress, so also add loading indicator to the modal + return false; // prevent modal from closing automatically }, }).modal('show'); }); @@ -158,13 +164,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) { } } - fomanticQuery(elModal).modal('setting', { - onApprove: () => { - // "form-fetch-action" can handle network errors gracefully, - // so keep the modal dialog to make users can re-submit the form if anything wrong happens. - if (elModal.querySelector('.form-fetch-action')) return false; - }, - }).modal('show'); + fomanticQuery(elModal).modal('show'); } export function initGlobalButtons(): void { diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index 2da481e521..3ca361b6e2 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -1,11 +1,11 @@ import {request} from '../modules/fetch.ts'; -import {showErrorToast} from '../modules/toast.ts'; -import {addDelegatedEventListener, submitEventSubmitter} from '../utils/dom.ts'; -import {confirmModal} from './comp/ConfirmModal.ts'; +import {hideToastsAll, showErrorToast} from '../modules/toast.ts'; +import {addDelegatedEventListener, createElementFromHTML, submitEventSubmitter} from '../utils/dom.ts'; +import {confirmModal, createConfirmModal} from './comp/ConfirmModal.ts'; import type {RequestOpts} from '../types.ts'; import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; -const {appSubUrl, i18n} = window.config; +const {appSubUrl} = window.config; // fetchActionDoRedirect does real redirection to bypass the browser's limitations of "location" // more details are in the backend's fetch-redirect handler @@ -23,10 +23,20 @@ function fetchActionDoRedirect(redirect: string) { } async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: RequestOpts) { + const showErrorForResponse = (code: number, message: string) => { + showErrorToast(`Error ${code || 'request'}: ${message}`); + }; + + let respStatus = 0; + let respText = ''; try { + hideToastsAll(); const resp = await request(url, opt); - if (resp.status === 200) { - let {redirect} = await resp.json(); + respStatus = resp.status; + respText = await resp.text(); + const respJson = JSON.parse(respText); + if (respStatus === 200) { + let {redirect} = respJson; redirect = redirect || actionElem.getAttribute('data-redirect'); ignoreAreYouSure(actionElem); // ignore the areYouSure check before reloading if (redirect) { @@ -35,29 +45,32 @@ async function fetchActionDoRequest(actionElem: HTMLElement, url: string, opt: R window.location.reload(); } return; - } else if (resp.status >= 400 && resp.status < 500) { - const data = await resp.json(); + } + + if (respStatus >= 400 && respStatus < 500 && respJson?.errorMessage) { // the code was quite messy, sometimes the backend uses "err", sometimes it uses "error", and even "user_error" // but at the moment, as a new approach, we only use "errorMessage" here, backend can use JSONError() to respond. - if (data.errorMessage) { - showErrorToast(data.errorMessage, {useHtmlBody: data.renderFormat === 'html'}); - } else { - showErrorToast(`server error: ${resp.status}`); - } + showErrorToast(respJson.errorMessage, {useHtmlBody: respJson.renderFormat === 'html'}); } else { - showErrorToast(`server error: ${resp.status}`); + showErrorForResponse(respStatus, respText); } } catch (e) { - if (e.name !== 'AbortError') { - console.error('error when doRequest', e); - showErrorToast(`${i18n.network_error} ${e}`); + if (e.name === 'SyntaxError') { + showErrorForResponse(respStatus, (respText || '').substring(0, 100)); + } else if (e.name !== 'AbortError') { + console.error('fetchActionDoRequest error', e); + showErrorForResponse(respStatus, `${e}`); } } actionElem.classList.remove('is-loading', 'loading-icon-2px'); } -async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) { +async function onFormFetchActionSubmit(formEl: HTMLFormElement, e: SubmitEvent) { e.preventDefault(); + await submitFormFetchAction(formEl, submitEventSubmitter(e)); +} + +export async function submitFormFetchAction(formEl: HTMLFormElement, formSubmitter?: HTMLElement) { if (formEl.classList.contains('is-loading')) return; formEl.classList.add('is-loading'); @@ -66,9 +79,8 @@ async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) { } const formMethod = formEl.getAttribute('method') || 'get'; - const formActionUrl = formEl.getAttribute('action'); + const formActionUrl = formEl.getAttribute('action') || window.location.href; const formData = new FormData(formEl); - const formSubmitter = submitEventSubmitter(e); const [submitterName, submitterValue] = [formSubmitter?.getAttribute('name'), formSubmitter?.getAttribute('value')]; if (submitterName) { formData.append(submitterName, submitterValue || ''); @@ -96,36 +108,52 @@ async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) { await fetchActionDoRequest(formEl, reqUrl, reqOpt); } -async function linkAction(el: HTMLElement, e: Event) { +async function onLinkActionClick(el: HTMLElement, e: Event) { // A "link-action" can post AJAX request to its "data-url" // Then the browser is redirected to: the "redirect" in response, or "data-redirect" attribute, or current URL by reloading. - // If the "link-action" has "data-modal-confirm" attribute, a confirm modal dialog will be shown before taking action. + // If the "link-action" has "data-modal-confirm" attribute, a "confirm modal dialog" will be shown before taking action. + // Attribute "data-modal-confirm" can be a modal element by "#the-modal-id", or a string content for the modal dialog. e.preventDefault(); const url = el.getAttribute('data-url'); const doRequest = async () => { - if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but A doesn't have disabled attribute + if ('disabled' in el) el.disabled = true; // el could be A or BUTTON, but "A" doesn't have the "disabled" attribute await fetchActionDoRequest(el, url, {method: el.getAttribute('data-link-action-method') || 'POST'}); if ('disabled' in el) el.disabled = false; }; - const modalConfirmContent = el.getAttribute('data-modal-confirm') || - el.getAttribute('data-modal-confirm-content') || ''; - if (!modalConfirmContent) { + let elModal: HTMLElement | null = null; + const dataModalConfirm = el.getAttribute('data-modal-confirm') || ''; + if (dataModalConfirm.startsWith('#')) { + // eslint-disable-next-line unicorn/prefer-query-selector + elModal = document.getElementById(dataModalConfirm.substring(1)); + if (elModal) { + elModal = createElementFromHTML(elModal.outerHTML); + elModal.removeAttribute('id'); + } + } + if (!elModal) { + const modalConfirmContent = dataModalConfirm || el.getAttribute('data-modal-confirm-content') || ''; + if (modalConfirmContent) { + const isRisky = el.classList.contains('red') || el.classList.contains('negative'); + elModal = createConfirmModal({ + header: el.getAttribute('data-modal-confirm-header') || '', + content: modalConfirmContent, + confirmButtonColor: isRisky ? 'red' : 'primary', + }); + } + } + + if (!elModal) { await doRequest(); return; } - const isRisky = el.classList.contains('red') || el.classList.contains('negative'); - if (await confirmModal({ - header: el.getAttribute('data-modal-confirm-header') || '', - content: modalConfirmContent, - confirmButtonColor: isRisky ? 'red' : 'primary', - })) { + if (await confirmModal(elModal)) { await doRequest(); } } export function initGlobalFetchAction() { - addDelegatedEventListener(document, 'submit', '.form-fetch-action', formFetchAction); - addDelegatedEventListener(document, 'click', '.link-action', linkAction); + addDelegatedEventListener(document, 'submit', '.form-fetch-action', onFormFetchActionSubmit); + addDelegatedEventListener(document, 'click', '.link-action', onLinkActionClick); } diff --git a/web_src/js/features/comp/ConfirmModal.ts b/web_src/js/features/comp/ConfirmModal.ts index 1ce490ec2e..97a73eace6 100644 --- a/web_src/js/features/comp/ConfirmModal.ts +++ b/web_src/js/features/comp/ConfirmModal.ts @@ -1,24 +1,33 @@ import {svg} from '../../svg.ts'; -import {htmlEscape} from 'escape-goat'; +import {html, htmlRaw} from '../../utils/html.ts'; import {createElementFromHTML} from '../../utils/dom.ts'; import {fomanticQuery} from '../../modules/fomantic/base.ts'; const {i18n} = window.config; -export function confirmModal({header = '', content = '', confirmButtonColor = 'primary'} = {}): Promise<boolean> { - return new Promise((resolve) => { - const headerHtml = header ? `<div class="header">${htmlEscape(header)}</div>` : ''; - const modal = createElementFromHTML(` - <div class="ui g-modal-confirm modal"> - ${headerHtml} - <div class="content">${htmlEscape(content)}</div> - <div class="actions"> - <button class="ui cancel button">${svg('octicon-x')} ${htmlEscape(i18n.modal_cancel)}</button> - <button class="ui ${confirmButtonColor} ok button">${svg('octicon-check')} ${htmlEscape(i18n.modal_confirm)}</button> - </div> +type ConfirmModalOptions = { + header?: string; + content?: string; + confirmButtonColor?: 'primary' | 'red' | 'green' | 'blue'; +} + +export function createConfirmModal({header = '', content = '', confirmButtonColor = 'primary'}:ConfirmModalOptions = {}): HTMLElement { + const headerHtml = header ? html`<div class="header">${header}</div>` : ''; + return createElementFromHTML(html` + <div class="ui g-modal-confirm modal"> + ${htmlRaw(headerHtml)} + <div class="content">${content}</div> + <div class="actions"> + <button class="ui cancel button">${htmlRaw(svg('octicon-x'))} ${i18n.modal_cancel}</button> + <button class="ui ${confirmButtonColor} ok button">${htmlRaw(svg('octicon-check'))} ${i18n.modal_confirm}</button> </div> - `); - document.body.append(modal); + </div> + `.trim()); +} + +export function confirmModal(modal: HTMLElement | ConfirmModalOptions): Promise<boolean> { + if (!(modal instanceof HTMLElement)) modal = createConfirmModal(modal); + return new Promise((resolve) => { const $modal = fomanticQuery(modal); $modal.modal({ onApprove() { diff --git a/web_src/js/features/comp/EditorUpload.test.ts b/web_src/js/features/comp/EditorUpload.test.ts index 55f3f74389..e6e5f4de13 100644 --- a/web_src/js/features/comp/EditorUpload.test.ts +++ b/web_src/js/features/comp/EditorUpload.test.ts @@ -1,4 +1,4 @@ -import {removeAttachmentLinksFromMarkdown} from './EditorUpload.ts'; +import {pasteAsMarkdownLink, removeAttachmentLinksFromMarkdown} from './EditorUpload.ts'; test('removeAttachmentLinksFromMarkdown', () => { expect(removeAttachmentLinksFromMarkdown('a foo b', 'foo')).toBe('a foo b'); @@ -12,3 +12,13 @@ test('removeAttachmentLinksFromMarkdown', () => { expect(removeAttachmentLinksFromMarkdown('a <img src="/attachments/foo"> b', 'foo')).toBe('a b'); expect(removeAttachmentLinksFromMarkdown('a <img src="/attachments/foo" width="100"/> b', 'foo')).toBe('a b'); }); + +test('preparePasteAsMarkdownLink', () => { + expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 0}, 'bar')).toBeNull(); + expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 0}, 'https://gitea.com')).toBeNull(); + expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 3}, 'bar')).toBeNull(); + expect(pasteAsMarkdownLink({value: 'foo', selectionStart: 0, selectionEnd: 3}, 'https://gitea.com')).toBe('[foo](https://gitea.com)'); + expect(pasteAsMarkdownLink({value: '..(url)', selectionStart: 3, selectionEnd: 6}, 'https://gitea.com')).toBe('[url](https://gitea.com)'); + expect(pasteAsMarkdownLink({value: '[](url)', selectionStart: 3, selectionEnd: 6}, 'https://gitea.com')).toBeNull(); + expect(pasteAsMarkdownLink({value: 'https://example.com', selectionStart: 0, selectionEnd: 19}, 'https://gitea.com')).toBeNull(); +}); diff --git a/web_src/js/features/comp/EditorUpload.ts b/web_src/js/features/comp/EditorUpload.ts index f6d5731422..bf78f58daf 100644 --- a/web_src/js/features/comp/EditorUpload.ts +++ b/web_src/js/features/comp/EditorUpload.ts @@ -114,21 +114,30 @@ async function handleUploadFiles(editor: CodeMirrorEditor | TextareaEditor, drop export function removeAttachmentLinksFromMarkdown(text: string, fileUuid: string) { text = text.replace(new RegExp(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), ''); - text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), ''); + text = text.replace(new RegExp(`[<]img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), ''); return text; } -function handleClipboardText(textarea: HTMLTextAreaElement, e: ClipboardEvent, text: string, isShiftDown: boolean) { +export function pasteAsMarkdownLink(textarea: {value: string, selectionStart: number, selectionEnd: number}, pastedText: string): string | null { + const {value, selectionStart, selectionEnd} = textarea; + const selectedText = value.substring(selectionStart, selectionEnd); + const trimmedText = pastedText.trim(); + const beforeSelection = value.substring(0, selectionStart); + const afterSelection = value.substring(selectionEnd); + const isInMarkdownLink = beforeSelection.endsWith('](') && afterSelection.startsWith(')'); + const asMarkdownLink = selectedText && isUrl(trimmedText) && !isUrl(selectedText) && !isInMarkdownLink; + return asMarkdownLink ? `[${selectedText}](${trimmedText})` : null; +} + +function handleClipboardText(textarea: HTMLTextAreaElement, e: ClipboardEvent, pastedText: string, isShiftDown: boolean) { // pasting with "shift" means "paste as original content" in most applications if (isShiftDown) return; // let the browser handle it // when pasting links over selected text, turn it into [text](link) - const {value, selectionStart, selectionEnd} = textarea; - const selectedText = value.substring(selectionStart, selectionEnd); - const trimmedText = text.trim(); - if (selectedText && isUrl(trimmedText) && !isUrl(selectedText)) { + const pastedAsMarkdown = pasteAsMarkdownLink(textarea, pastedText); + if (pastedAsMarkdown) { e.preventDefault(); - replaceTextareaSelection(textarea, `[${selectedText}](${trimmedText})`); + replaceTextareaSelection(textarea, pastedAsMarkdown); } // else, let the browser handle it } diff --git a/web_src/js/features/comp/LabelEdit.ts b/web_src/js/features/comp/LabelEdit.ts index 55351cd900..423440129c 100644 --- a/web_src/js/features/comp/LabelEdit.ts +++ b/web_src/js/features/comp/LabelEdit.ts @@ -1,5 +1,6 @@ import {toggleElem} from '../../utils/dom.ts'; import {fomanticQuery} from '../../modules/fomantic/base.ts'; +import {submitFormFetchAction} from '../common-fetch-action.ts'; function nameHasScope(name: string): boolean { return /.*[^/]\/[^/].*/.test(name); @@ -70,7 +71,8 @@ export function initCompLabelEdit(pageSelector: string) { form.reportValidity(); return false; } - form.submit(); + submitFormFetchAction(form); + return false; }, }).modal('show'); }; diff --git a/web_src/js/features/comp/SearchUserBox.ts b/web_src/js/features/comp/SearchUserBox.ts index 9fedb3ed24..4b13a2141f 100644 --- a/web_src/js/features/comp/SearchUserBox.ts +++ b/web_src/js/features/comp/SearchUserBox.ts @@ -1,4 +1,4 @@ -import {htmlEscape} from 'escape-goat'; +import {htmlEscape} from '../../utils/html.ts'; import {fomanticQuery} from '../../modules/fomantic/base.ts'; const {appSubUrl} = window.config; diff --git a/web_src/js/features/copycontent.ts b/web_src/js/features/copycontent.ts index d58f6c8246..0fec2a6235 100644 --- a/web_src/js/features/copycontent.ts +++ b/web_src/js/features/copycontent.ts @@ -9,17 +9,17 @@ const {i18n} = window.config; export function initCopyContent() { registerGlobalEventFunc('click', 'onCopyContentButtonClick', async (btn: HTMLElement) => { if (btn.classList.contains('disabled') || btn.classList.contains('is-loading')) return; - let content; - let isRasterImage = false; - const link = btn.getAttribute('data-link'); + const rawFileLink = btn.getAttribute('data-raw-file-link'); - // when data-link is present, we perform a fetch. this is either because - // the text to copy is not in the DOM, or it is an image which should be + let content, isRasterImage = false; + + // when "data-raw-link" is present, we perform a fetch. this is either because + // the text to copy is not in the DOM, or it is an image that should be // fetched to copy in full resolution - if (link) { + if (rawFileLink) { btn.classList.add('is-loading', 'loading-icon-2px'); try { - const res = await GET(link, {credentials: 'include', redirect: 'follow'}); + const res = await GET(rawFileLink, {credentials: 'include', redirect: 'follow'}); const contentType = res.headers.get('content-type'); if (contentType.startsWith('image/') && !contentType.startsWith('image/svg')) { diff --git a/web_src/js/features/dropzone.ts b/web_src/js/features/dropzone.ts index b2ba7651c4..20f7ceb6c3 100644 --- a/web_src/js/features/dropzone.ts +++ b/web_src/js/features/dropzone.ts @@ -1,5 +1,5 @@ import {svg} from '../svg.ts'; -import {htmlEscape} from 'escape-goat'; +import {html} from '../utils/html.ts'; import {clippie} from 'clippie'; import {showTemporaryTooltip} from '../modules/tippy.ts'; import {GET, POST} from '../modules/fetch.ts'; @@ -33,14 +33,14 @@ export function generateMarkdownLinkForAttachment(file: Partial<CustomDropzoneFi // Scale down images from HiDPI monitors. This uses the <img> tag because it's the only // method to change image size in Markdown that is supported by all implementations. // Make the image link relative to the repo path, then the final URL is "/sub-path/owner/repo/attachments/{uuid}" - fileMarkdown = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(file.name)}" src="attachments/${htmlEscape(file.uuid)}">`; + fileMarkdown = html`<img width="${Math.round(width / dppx)}" alt="${file.name}" src="attachments/${file.uuid}">`; } else { // Markdown always renders the image with a relative path, so the final URL is "/sub-path/owner/repo/attachments/{uuid}" // TODO: it should also use relative path for consistency, because absolute is ambiguous for "/sub-path/attachments" or "/attachments" fileMarkdown = ``; } } else if (isVideoFile(file)) { - fileMarkdown = `<video src="attachments/${htmlEscape(file.uuid)}" title="${htmlEscape(file.name)}" controls></video>`; + fileMarkdown = html`<video src="attachments/${file.uuid}" title="${file.name}" controls></video>`; } return fileMarkdown; } diff --git a/web_src/js/features/emoji.ts b/web_src/js/features/emoji.ts index 135620e51e..69afe491e2 100644 --- a/web_src/js/features/emoji.ts +++ b/web_src/js/features/emoji.ts @@ -1,4 +1,5 @@ import emojis from '../../../assets/emoji.json' with {type: 'json'}; +import {html} from '../utils/html.ts'; const {assetUrlPrefix, customEmojis} = window.config; @@ -24,12 +25,11 @@ for (const key of emojiKeys) { export function emojiHTML(name: string) { let inner; if (Object.hasOwn(customEmojis, name)) { - inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`; + inner = html`<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`; } else { inner = emojiString(name); } - - return `<span class="emoji" title=":${name}:">${inner}</span>`; + return html`<span class="emoji" title=":${name}:">${inner}</span>`; } // retrieve string for given emoji name diff --git a/web_src/js/features/file-view.ts b/web_src/js/features/file-view.ts new file mode 100644 index 0000000000..d803f53c0d --- /dev/null +++ b/web_src/js/features/file-view.ts @@ -0,0 +1,76 @@ +import type {FileRenderPlugin} from '../render/plugin.ts'; +import {newRenderPlugin3DViewer} from '../render/plugins/3d-viewer.ts'; +import {newRenderPluginPdfViewer} from '../render/plugins/pdf-viewer.ts'; +import {registerGlobalInitFunc} from '../modules/observer.ts'; +import {createElementFromHTML, showElem, toggleClass} from '../utils/dom.ts'; +import {html} from '../utils/html.ts'; +import {basename} from '../utils.ts'; + +const plugins: FileRenderPlugin[] = []; + +function initPluginsOnce(): void { + if (plugins.length) return; + plugins.push(newRenderPlugin3DViewer(), newRenderPluginPdfViewer()); +} + +function findFileRenderPlugin(filename: string, mimeType: string): FileRenderPlugin | null { + return plugins.find((plugin) => plugin.canHandle(filename, mimeType)) || null; +} + +function showRenderRawFileButton(elFileView: HTMLElement, renderContainer: HTMLElement | null): void { + const toggleButtons = elFileView.querySelector('.file-view-toggle-buttons'); + showElem(toggleButtons); + const displayingRendered = Boolean(renderContainer); + toggleClass(toggleButtons.querySelectorAll('.file-view-toggle-source'), 'active', !displayingRendered); // it may not exist + toggleClass(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) { + const elViewRawPrompt = container.querySelector('.file-view-raw-prompt'); + if (!rawFileLink || !elViewRawPrompt) throw new Error('unexpected file view container'); + + let rendered = false, errorMsg = ''; + try { + const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType); + if (plugin) { + container.classList.add('is-loading'); + container.setAttribute('data-render-name', plugin.name); // not used yet + await plugin.render(container, rawFileLink); + rendered = true; + } + } catch (e) { + errorMsg = `${e}`; + } finally { + container.classList.remove('is-loading'); + } + + if (rendered) { + elViewRawPrompt.remove(); + return; + } + + // remove all children from the container, and only show the raw file link + container.replaceChildren(elViewRawPrompt); + + if (errorMsg) { + const elErrorMessage = createElementFromHTML(html`<div class="ui error message">${errorMsg}</div>`); + elViewRawPrompt.insertAdjacentElement('afterbegin', elErrorMessage); + } +} + +export function initRepoFileView(): void { + registerGlobalInitFunc('initRepoFileView', async (elFileView: HTMLElement) => { + initPluginsOnce(); + const rawFileLink = elFileView.getAttribute('data-raw-file-link'); + const mimeType = elFileView.getAttribute('data-mime-type') || ''; // not used yet + // TODO: we should also provide the prefetched file head bytes to let the plugin decide whether to render or not + const plugin = findFileRenderPlugin(basename(rawFileLink), mimeType); + if (!plugin) return; + + const renderContainer = elFileView.querySelector<HTMLElement>('.file-view-render-container'); + showRenderRawFileButton(elFileView, renderContainer); + // maybe in the future multiple plugins can render the same file, so we should not assume only one plugin will render it + if (renderContainer) await renderRawFileToContainer(renderContainer, rawFileLink, mimeType); + }); +} diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts index 34df4757f9..ca4bcce881 100644 --- a/web_src/js/features/install.ts +++ b/web_src/js/features/install.ts @@ -104,7 +104,7 @@ function initPreInstall() { } function initPostInstall() { - const el = document.querySelector('#goto-user-login'); + const el = document.querySelector('#goto-after-install'); if (!el) return; const targetUrl = el.getAttribute('href'); diff --git a/web_src/js/features/repo-code.ts b/web_src/js/features/repo-code.ts index c699de59e6..bf7fd762b0 100644 --- a/web_src/js/features/repo-code.ts +++ b/web_src/js/features/repo-code.ts @@ -110,10 +110,15 @@ function showLineButton() { } export function initRepoCodeView() { - if (!document.querySelector('.code-view .lines-num')) return; + // When viewing a file or blame, there is always a ".file-view" element, + // but the ".code-view" class is only present when viewing the "code" of a file; it is not present when viewing a PDF file. + // Since the ".file-view" will be dynamically reloaded when navigating via the left file tree (eg: view a PDF file, then view a source code file, etc.) + // the "code-view" related event listeners should always be added when the current page contains ".file-view" element. + if (!document.querySelector('.repo-view-container .file-view')) return; + // "file code view" and "blame" pages need this "line number button" feature let selRangeStart: string; - addDelegatedEventListener(document, 'click', '.lines-num span', (el: HTMLElement, e: KeyboardEvent) => { + addDelegatedEventListener(document, 'click', '.code-view .lines-num span', (el: HTMLElement, e: KeyboardEvent) => { if (!selRangeStart || !e.shiftKey) { selRangeStart = el.getAttribute('id'); selectRange(selRangeStart); @@ -125,12 +130,14 @@ export function initRepoCodeView() { showLineButton(); }); + // apply the selected range from the URL hash const onHashChange = () => { if (!window.location.hash) return; + if (!document.querySelector('.code-view .lines-num')) return; const range = window.location.hash.substring(1); const first = selectRange(range); if (first) { - // set scrollRestoration to 'manual' when there is a hash in url, so that the scroll position will not be remembered after refreshing + // set scrollRestoration to 'manual' when there is a hash in the URL, so that the scroll position will not be remembered after refreshing if (window.history.scrollRestoration !== 'manual') window.history.scrollRestoration = 'manual'; first.scrollIntoView({block: 'start'}); showLineButton(); diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index 0f77508f70..f3ca13460c 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -1,4 +1,4 @@ -import {htmlEscape} from 'escape-goat'; +import {html, htmlRaw} from '../utils/html.ts'; import {createCodeEditor} from './codeeditor.ts'; import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts'; import {attachRefIssueContextPopup} from './contextpopup.ts'; @@ -7,6 +7,7 @@ import {initDropzone} from './dropzone.ts'; import {confirmModal} from './comp/ConfirmModal.ts'; import {applyAreYouSure, ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; +import {submitFormFetchAction} from './common-fetch-action.ts'; function initEditPreviewTab(elForm: HTMLFormElement) { const elTabMenu = elForm.querySelector('.repo-editor-menu'); @@ -86,10 +87,10 @@ export function initRepoEditor() { if (i < parts.length - 1) { if (trimValue.length) { const linkElement = createElementFromHTML( - `<span class="section"><a href="#">${htmlEscape(value)}</a></span>`, + html`<span class="section"><a href="#">${value}</a></span>`, ); const dividerElement = createElementFromHTML( - `<div class="breadcrumb-divider">/</div>`, + html`<div class="breadcrumb-divider">/</div>`, ); links.push(linkElement); dividers.push(dividerElement); @@ -112,7 +113,7 @@ export function initRepoEditor() { if (!warningDiv) { warningDiv = document.createElement('div'); warningDiv.classList.add('ui', 'warning', 'message', 'flash-message', 'flash-warning', 'space-related'); - warningDiv.innerHTML = '<p>File path contains leading or trailing whitespace.</p>'; + warningDiv.innerHTML = html`<p>File path contains leading or trailing whitespace.</p>`; // Add display 'block' because display is set to 'none' in formantic\build\semantic.css warningDiv.style.display = 'block'; const inputContainer = document.querySelector('.repo-editor-header'); @@ -141,38 +142,36 @@ export function initRepoEditor() { } }); + const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form'); + // on the upload page, there is no editor(textarea) const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area'); if (!editArea) return; - const elForm = document.querySelector<HTMLFormElement>('.repository.editor .edit.form'); + // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage + // to enable or disable the commit button + const commitButton = document.querySelector<HTMLButtonElement>('#commit-button'); + const dirtyFileClass = 'dirty-file'; + + const syncCommitButtonState = () => { + const dirty = elForm.classList.contains(dirtyFileClass); + commitButton.disabled = !dirty; + }; + // Registering a custom listener for the file path and the file content + // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added + applyAreYouSure(elForm, { + silent: true, + dirtyClass: dirtyFileClass, + fieldSelector: ':input:not(.commit-form-wrapper :input)', + change: syncCommitButtonState, + }); + syncCommitButtonState(); // disable the "commit" button when no content changes + initEditPreviewTab(elForm); (async () => { const editor = await createCodeEditor(editArea, filenameInput); - // Using events from https://github.com/codedance/jquery.AreYouSure#advanced-usage - // to enable or disable the commit button - const commitButton = document.querySelector<HTMLButtonElement>('#commit-button'); - const dirtyFileClass = 'dirty-file'; - - // Disabling the button at the start - if (document.querySelector<HTMLInputElement>('input[name="page_has_posted"]').value !== 'true') { - commitButton.disabled = true; - } - - // Registering a custom listener for the file path and the file content - // FIXME: it is not quite right here (old bug), it causes double-init, the global areYouSure "dirty" class will also be added - applyAreYouSure(elForm, { - silent: true, - dirtyClass: dirtyFileClass, - fieldSelector: ':input:not(.commit-form-wrapper :input)', - change($form: any) { - const dirty = $form[0]?.classList.contains(dirtyFileClass); - commitButton.disabled = !dirty; - }, - }); - // Update the editor from query params, if available, // only after the dirtyFileClass initialization const params = new URLSearchParams(window.location.search); @@ -181,7 +180,7 @@ export function initRepoEditor() { editor.setValue(value); } - commitButton?.addEventListener('click', async (e) => { + commitButton.addEventListener('click', async (e) => { // A modal which asks if an empty file should be committed if (!editArea.value) { e.preventDefault(); @@ -190,14 +189,15 @@ export function initRepoEditor() { content: elForm.getAttribute('data-text-empty-confirm-content'), })) { ignoreAreYouSure(elForm); - elForm.submit(); + submitFormFetchAction(elForm); } } }); })(); } -export function renderPreviewPanelContent(previewPanel: Element, content: string) { - previewPanel.innerHTML = `<div class="render-content markup">${content}</div>`; +export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) { + // the content is from the server, so it is safe to use innerHTML + previewPanel.innerHTML = html`<div class="render-content markup">${htmlRaw(htmlContent)}</div>`; attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue')); } diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 3ea5fb70c0..762fbf51bb 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -1,6 +1,6 @@ import {updateIssuesMeta} from './repo-common.ts'; import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts'; -import {htmlEscape} from 'escape-goat'; +import {html} from '../utils/html.ts'; import {confirmModal} from './comp/ConfirmModal.ts'; import {showErrorToast} from '../modules/toast.ts'; import {createSortable} from '../modules/sortable.ts'; @@ -138,10 +138,10 @@ function initDropdownUserRemoteSearch(el: Element) { // the content is provided by backend IssuePosters handler processedResults.length = 0; for (const item of resp.results) { - let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`; - if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`; + let nameHtml = html`<img class="ui avatar tw-align-middle" src="${item.avatar_link}" aria-hidden="true" alt width="20" height="20"><span class="gt-ellipsis">${item.username}</span>`; + if (item.full_name) nameHtml += html`<span class="search-fullname tw-ml-2">${item.full_name}</span>`; if (selectedUsername.toLowerCase() === item.username.toLowerCase()) selectedUsername = item.username; - processedResults.push({value: item.username, name: html}); + processedResults.push({value: item.username, name: nameHtml}); } resp.results = processedResults; return resp; diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index bc7d4dee19..49e8fc40a2 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -1,4 +1,4 @@ -import {htmlEscape} from 'escape-goat'; +import {html, htmlEscape} from '../utils/html.ts'; import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts'; import { addDelegatedEventListener, @@ -17,6 +17,7 @@ import {showErrorToast} from '../modules/toast.ts'; import {initRepoIssueSidebar} from './repo-issue-sidebar.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts'; +import {registerGlobalInitFunc} from '../modules/observer.ts'; const {appSubUrl} = window.config; @@ -45,8 +46,7 @@ export function initRepoIssueSidebarDependency() { if (String(issue.id) === currIssueId) continue; filteredResponse.results.push({ value: issue.id, - name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div> -<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`, + name: html`<div class="gt-ellipsis">#${issue.number} ${issue.title}</div><div class="text small tw-break-anywhere">${issue.repository.full_name}</div>`, }); } return filteredResponse; @@ -416,25 +416,20 @@ export function initRepoIssueWipNewTitle() { export function initRepoIssueWipToggle() { // Toggle WIP for existing PR - queryElems(document, '.toggle-wip', (el) => el.addEventListener('click', async (e) => { + registerGlobalInitFunc('initPullRequestWipToggle', (toggleWip) => toggleWip.addEventListener('click', async (e) => { e.preventDefault(); - const toggleWip = el; const title = toggleWip.getAttribute('data-title'); const wipPrefix = toggleWip.getAttribute('data-wip-prefix'); const updateUrl = toggleWip.getAttribute('data-update-url'); - try { - const params = new URLSearchParams(); - params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`); - - const response = await POST(updateUrl, {data: params}); - if (!response.ok) { - throw new Error('Failed to toggle WIP status'); - } - window.location.reload(); - } catch (error) { - console.error(error); + const params = new URLSearchParams(); + params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`); + const response = await POST(updateUrl, {data: params}); + if (!response.ok) { + showErrorToast(`Failed to toggle 'work in progress' status`); + return; } + window.location.reload(); })); } diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts index 0e4d78872d..e2aa13f490 100644 --- a/web_src/js/features/repo-new.ts +++ b/web_src/js/features/repo-new.ts @@ -1,5 +1,5 @@ import {hideElem, querySingleVisibleElem, showElem, toggleElem} from '../utils/dom.ts'; -import {htmlEscape} from 'escape-goat'; +import {htmlEscape} from '../utils/html.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {sanitizeRepoName} from './repo-common.ts'; diff --git a/web_src/js/features/repo-projects.ts b/web_src/js/features/repo-projects.ts index dc4aa8274b..ad0feb6101 100644 --- a/web_src/js/features/repo-projects.ts +++ b/web_src/js/features/repo-projects.ts @@ -114,7 +114,6 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void { window.location.reload(); // newly added column, need to reload the page return; } - fomanticQuery(elModal).modal('hide'); // update the newly saved column title and color in the project board (to avoid reload) const elEditButton = writableProjectBoard.querySelector<HTMLButtonElement>(`.show-project-column-modal-edit[${attrDataColumnId}="${columnId}"]`); @@ -134,6 +133,8 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void { elBoardColumn.style.removeProperty('color'); queryElemChildren<HTMLElement>(elBoardColumn, '.divider', (divider) => divider.style.removeProperty('color')); } + + fomanticQuery(elModal).modal('hide'); } finally { elForm.classList.remove('is-loading'); } diff --git a/web_src/js/features/repo-wiki.ts b/web_src/js/features/repo-wiki.ts index f94d3ef3d1..6ae0947077 100644 --- a/web_src/js/features/repo-wiki.ts +++ b/web_src/js/features/repo-wiki.ts @@ -2,6 +2,7 @@ import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMar import {fomanticMobileScreen} from '../modules/fomantic.ts'; import {POST} from '../modules/fetch.ts'; import type {ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; +import {html, htmlRaw} from '../utils/html.ts'; async function initRepoWikiFormEditor() { const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea'); @@ -30,7 +31,7 @@ async function initRepoWikiFormEditor() { const response = await POST(editor.previewUrl, {data: formData}); const data = await response.text(); lastContent = newContent; - previewTarget.innerHTML = `<div class="render-content markup ui segment">${data}</div>`; + previewTarget.innerHTML = html`<div class="render-content markup ui segment">${htmlRaw(data)}</div>`; } catch (error) { console.error('Error rendering preview:', error); } finally { diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index a5cd5ae7c4..07f9c435b8 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -134,7 +134,7 @@ function updateStopwatchData(data: any) { const {repo_owner_name, repo_name, issue_index, seconds} = watch; const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`; document.querySelector('.stopwatch-link')?.setAttribute('href', issueUrl); - document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/toggle`); + document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/stop`); document.querySelector('.stopwatch-cancel')?.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`); const stopwatchIssue = document.querySelector('.stopwatch-issue'); if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`; diff --git a/web_src/js/features/tribute.ts b/web_src/js/features/tribute.ts index cf98377ae7..43c21ebe6d 100644 --- a/web_src/js/features/tribute.ts +++ b/web_src/js/features/tribute.ts @@ -1,5 +1,5 @@ import {emojiKeys, emojiHTML, emojiString} from './emoji.ts'; -import {htmlEscape} from 'escape-goat'; +import {html, htmlRaw} from '../utils/html.ts'; type TributeItem = Record<string, any>; @@ -26,17 +26,18 @@ export async function attachTribute(element: HTMLElement) { return emojiString(item.original); }, menuItemTemplate: (item: TributeItem) => { - return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`; + return html`<div class="tribute-item">${htmlRaw(emojiHTML(item.original))}<span>${item.original}</span></div>`; }, }, { // mentions values: window.config.mentionValues ?? [], requireLeadingSpace: true, menuItemTemplate: (item: TributeItem) => { - return ` + const fullNameHtml = item.original.fullname && item.original.fullname !== '' ? html`<span class="fullname">${item.original.fullname}</span>` : ''; + return html` <div class="tribute-item"> - <img alt src="${htmlEscape(item.original.avatar)}" width="21" height="21"/> - <span class="name">${htmlEscape(item.original.name)}</span> - ${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''} + <img alt src="${item.original.avatar}" width="21" height="21"/> + <span class="name">${item.original.name}</span> + ${htmlRaw(fullNameHtml)} </div> `; }, diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts index 9e97ec0492..e4b540122d 100644 --- a/web_src/js/globals.d.ts +++ b/web_src/js/globals.d.ts @@ -25,11 +25,6 @@ declare module 'htmx.org/dist/htmx.esm.js' { export default value; } -declare module 'uint8-to-base64' { - export function encode(arrayBuffer: Uint8Array): string; - export function decode(base64str: string): Uint8Array; -} - declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' { const value = await import('swagger-ui-dist'); export default value.SwaggerUIBundle; diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 7e84773bc1..347aad2709 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -19,7 +19,7 @@ import {initRepoIssueContentHistory} from './features/repo-issue-content.ts'; import {initStopwatch} from './features/stopwatch.ts'; import {initFindFileInRepo} from './features/repo-findfile.ts'; import {initMarkupContent} from './markup/content.ts'; -import {initPdfViewer} from './render/pdf.ts'; +import {initRepoFileView} from './features/file-view.ts'; import {initUserAuthOauth2, initUserCheckAppUrl} from './features/user-auth.ts'; import {initRepoPullRequestAllowMaintainerEdit, initRepoPullRequestReview, initRepoIssueSidebarDependency, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; @@ -159,10 +159,11 @@ onDomReady(() => { initUserAuthWebAuthnRegister, initUserSettings, initRepoDiffView, - initPdfViewer, initColorPickers, initOAuth2SettingsDisableCheckbox, + + initRepoFileView, ]); // it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions. diff --git a/web_src/js/markup/anchors.ts b/web_src/js/markup/anchors.ts index 483d72bd5b..a0d49911fe 100644 --- a/web_src/js/markup/anchors.ts +++ b/web_src/js/markup/anchors.ts @@ -5,21 +5,24 @@ const removePrefix = (str: string): string => str.replace(/^user-content-/, ''); const hasPrefix = (str: string): boolean => str.startsWith('user-content-'); // scroll to anchor while respecting the `user-content` prefix that exists on the target -function scrollToAnchor(encodedId: string): void { - if (!encodedId) return; - const id = decodeURIComponent(encodedId); - const prefixedId = addPrefix(id); - let el = document.querySelector(`#${prefixedId}`); +function scrollToAnchor(encodedId?: string): void { + // FIXME: need to rewrite this function with new a better markup anchor generation logic, too many tricks here + let elemId: string; + try { + elemId = decodeURIComponent(encodedId ?? ''); + } catch {} // ignore the errors, since the "encodedId" is from user's input + if (!elemId) return; + + const prefixedId = addPrefix(elemId); + // eslint-disable-next-line unicorn/prefer-query-selector + let el = document.getElementById(prefixedId); // check for matching user-generated `a[name]` - if (!el) { - el = document.querySelector(`a[name="${CSS.escape(prefixedId)}"]`); - } + el = el ?? document.querySelector(`a[name="${CSS.escape(prefixedId)}"]`); // compat for links with old 'user-content-' prefixed hashes - if (!el && hasPrefix(id)) { - return document.querySelector(`#${id}`)?.scrollIntoView(); - } + // eslint-disable-next-line unicorn/prefer-query-selector + el = (!el && hasPrefix(elemId)) ? document.getElementById(elemId) : el; el?.scrollIntoView(); } diff --git a/web_src/js/markup/html2markdown.ts b/web_src/js/markup/html2markdown.ts index 8c2d2f8c86..5866d0d259 100644 --- a/web_src/js/markup/html2markdown.ts +++ b/web_src/js/markup/html2markdown.ts @@ -1,4 +1,4 @@ -import {htmlEscape} from 'escape-goat'; +import {html, htmlRaw} from '../utils/html.ts'; type Processor = (el: HTMLElement) => string | HTMLElement | void; @@ -38,10 +38,10 @@ function prepareProcessors(ctx:ProcessorContext): Processors { IMG(el: HTMLElement) { const alt = el.getAttribute('alt') || 'image'; const src = el.getAttribute('src'); - const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : ''; - const heightAttr = el.hasAttribute('height') ? ` height="${htmlEscape(el.getAttribute('height') || '')}"` : ''; + const widthAttr = el.hasAttribute('width') ? htmlRaw` width="${el.getAttribute('width') || ''}"` : ''; + const heightAttr = el.hasAttribute('height') ? htmlRaw` height="${el.getAttribute('height') || ''}"` : ''; if (widthAttr || heightAttr) { - return `<img alt="${htmlEscape(alt)}"${widthAttr}${heightAttr} src="${htmlEscape(src)}">`; + return html`<img alt="${alt}"${widthAttr}${heightAttr} src="${src}">`; } return ``; }, diff --git a/web_src/js/markup/mermaid.ts b/web_src/js/markup/mermaid.ts index ac24b3bcba..33d9a1ed9b 100644 --- a/web_src/js/markup/mermaid.ts +++ b/web_src/js/markup/mermaid.ts @@ -2,6 +2,7 @@ import {isDarkTheme} from '../utils.ts'; import {makeCodeCopyButton} from './codecopy.ts'; import {displayError} from './common.ts'; import {queryElems} from '../utils/dom.ts'; +import {html, htmlRaw} from '../utils/html.ts'; const {mermaidMaxSourceCharacters} = window.config; @@ -46,7 +47,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void const iframe = document.createElement('iframe'); iframe.classList.add('markup-content-iframe', 'tw-invisible'); - iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`; + iframe.srcdoc = html`<html><head><style>${htmlRaw(iframeCss)}</style></head><body>${htmlRaw(svg)}</body></html>`; const mermaidBlock = document.createElement('div'); mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden'); diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 0360b8ef95..ccc22073d7 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -9,9 +9,9 @@ const fomanticDropdownFn = $.fn.dropdown; // use our own `$().dropdown` function to patch Fomantic's dropdown module export function initAriaDropdownPatch() { if ($.fn.dropdown === ariaDropdownFn) throw new Error('initAriaDropdownPatch could only be called once'); - $.fn.dropdown.settings.onAfterFiltered = onAfterFiltered; $.fn.dropdown = ariaDropdownFn; $.fn.fomanticExt.onResponseKeepSelectedItem = onResponseKeepSelectedItem; + $.fn.fomanticExt.onDropdownAfterFiltered = onDropdownAfterFiltered; (ariaDropdownFn as FomanticInitFunction).settings = fomanticDropdownFn.settings; } @@ -71,11 +71,11 @@ function updateSelectionLabel(label: HTMLElement) { } } -function onAfterFiltered(this: any) { - const $dropdown = $(this); +function onDropdownAfterFiltered(this: any) { + const $dropdown = $(this).closest('.ui.dropdown'); // "this" can be the "ui dropdown" or "<select>" const hideEmptyDividers = $dropdown.dropdown('setting', 'hideDividers') === 'empty'; const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); - if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); + if (hideEmptyDividers && itemsMenu) hideScopedEmptyDividers(itemsMenu); } // delegate the dropdown's template functions and callback functions to add aria attributes. diff --git a/web_src/js/modules/fomantic/modal.ts b/web_src/js/modules/fomantic/modal.ts index 6a2c558890..a96c7785e1 100644 --- a/web_src/js/modules/fomantic/modal.ts +++ b/web_src/js/modules/fomantic/modal.ts @@ -1,5 +1,7 @@ import $ from 'jquery'; import type {FomanticInitFunction} from '../../types.ts'; +import {queryElems} from '../../utils/dom.ts'; +import {hideToastsFrom} from '../toast.ts'; const fomanticModalFn = $.fn.modal; @@ -8,6 +10,8 @@ export function initAriaModalPatch() { if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once'); $.fn.modal = ariaModalFn; (ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings; + $.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden; + $.fn.modal.settings.onApprove = onModalApproveDefault; } // the patched `$.fn.modal` modal function @@ -27,3 +31,33 @@ function ariaModalFn(this: any, ...args: Parameters<FomanticInitFunction>) { } return ret; } + +function onModalBeforeHidden(this: any) { + const $modal = $(this); + const elModal = $modal[0]; + hideToastsFrom(elModal.closest('.ui.dimmer') ?? document.body); + + // reset the form after the modal is hidden, after other modal events and handlers (e.g. "onApprove", form submit) + setTimeout(() => { + queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset()); + }, 0); +} + +function onModalApproveDefault(this: any) { + const $modal = $(this); + const selectors = $modal.modal('setting', 'selector'); + const elModal = $modal[0]; + const elApprove = elModal.querySelector(selectors.approve); + const elForm = elApprove?.closest('form'); + if (!elForm) return true; // no form, just allow closing the modal + + // "form-fetch-action" can handle network errors gracefully, + // so keep the modal dialog to make users can re-submit the form if anything wrong happens. + if (elForm.matches('.form-fetch-action')) return false; + + // There is an abuse for the "modal" + "form" combination, the "Approve" button is a traditional form submit button in the form. + // Then "approve" and "submit" occur at the same time, the modal will be closed immediately before the form is submitted. + // So here we prevent the modal from closing automatically by returning false, add the "is-loading" class to the form element. + elForm.classList.add('is-loading'); + return false; +} diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts index af715f48b9..2a1d998d76 100644 --- a/web_src/js/modules/tippy.ts +++ b/web_src/js/modules/tippy.ts @@ -2,6 +2,7 @@ import tippy, {followCursor} from 'tippy.js'; import {isDocumentFragmentOrElementNode} from '../utils/dom.ts'; import {formatDatetime} from '../utils/time.ts'; import type {Content, Instance, Placement, Props} from 'tippy.js'; +import {html} from '../utils/html.ts'; type TippyOpts = { role?: string, @@ -9,7 +10,7 @@ type TippyOpts = { } & Partial<Props>; const visibleInstances = new Set<Instance>(); -const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`; +const arrowSvg = html`<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`; export function createTippy(target: Element, opts: TippyOpts = {}): Instance { // the callback functions should be destructured from opts, @@ -40,6 +41,7 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance { } } visibleInstances.add(instance); + target.setAttribute('aria-controls', instance.popper.id); return onShow?.(instance); }, arrow: arrow ?? (theme === 'bare' ? false : arrowSvg), @@ -180,13 +182,25 @@ export function initGlobalTooltips(): void { } export function showTemporaryTooltip(target: Element, content: Content): void { - // if the target is inside a dropdown, the menu will be hidden soon - // so display the tooltip on the dropdown instead - target = target.closest('.ui.dropdown') || target; - const tippy = target._tippy ?? attachTooltip(target, content); - tippy.setContent(content); - if (!tippy.state.isShown) tippy.show(); - tippy.setProps({ + // if the target is inside a dropdown or tippy popup, the menu will be hidden soon + // so display the tooltip on the "aria-controls" element or dropdown instead + let refClientRect: DOMRect; + const popupTippyId = target.closest(`[data-tippy-root]`)?.id; + if (popupTippyId) { + // for example, the "Copy Permalink" button in the "File View" page for the selected lines + target = document.body; + refClientRect = document.querySelector(`[aria-controls="${CSS.escape(popupTippyId)}"]`)?.getBoundingClientRect(); + refClientRect = refClientRect ?? new DOMRect(0, 0, 0, 0); // fallback to empty rect if not found, tippy doesn't accept null + } else { + // for example, the "Copy Link" button in the issue header dropdown menu + target = target.closest('.ui.dropdown') ?? target; + refClientRect = target.getBoundingClientRect(); + } + const tooltipTippy = target._tippy ?? attachTooltip(target, content); + tooltipTippy.setContent(content); + tooltipTippy.setProps({getReferenceClientRect: () => refClientRect}); + if (!tooltipTippy.state.isShown) tooltipTippy.show(); + tooltipTippy.setProps({ onHidden: (tippy) => { // reset the default tooltip content, if no default, then this temporary tooltip could be destroyed if (!attachTooltip(target)) { diff --git a/web_src/js/modules/toast.ts b/web_src/js/modules/toast.ts index 36e2321743..ed807a4977 100644 --- a/web_src/js/modules/toast.ts +++ b/web_src/js/modules/toast.ts @@ -1,6 +1,6 @@ -import {htmlEscape} from 'escape-goat'; +import {htmlEscape} from '../utils/html.ts'; import {svg} from '../svg.ts'; -import {animateOnce, showElem} from '../utils/dom.ts'; +import {animateOnce, queryElems, showElem} from '../utils/dom.ts'; import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown import type {Intent} from '../types.ts'; import type {SvgName} from '../svg.ts'; @@ -37,17 +37,20 @@ const levels: ToastLevels = { type ToastOpts = { useHtmlBody?: boolean, - preventDuplicates?: boolean, + preventDuplicates?: boolean | string, } & Options; +type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast }; + // See https://github.com/apvarun/toastify-js#api for options function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast { const body = useHtmlBody ? String(message) : htmlEscape(message); - const key = `${level}-${body}`; + const parent = document.querySelector('.ui.dimmer.active') ?? document.body; + const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${body}` : preventDuplicates) : ''; - // prevent showing duplicate toasts with same level and message, and give a visual feedback for end users + // prevent showing duplicate toasts with the same level and message, and give visual feedback for end users if (preventDuplicates) { - const toastEl = document.querySelector(`.toastify[data-toast-unique-key="${CSS.escape(key)}"]`); + const toastEl = parent.querySelector(`:scope > .toastify.on[data-toast-unique-key="${CSS.escape(duplicateKey)}"]`); if (toastEl) { const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number'); showElem(toastDupNumEl); @@ -59,6 +62,7 @@ function showToast(message: string, level: Intent, {gravity, position, duration, const {icon, background, duration: levelDuration} = levels[level ?? 'info']; const toast = Toastify({ + selector: parent, text: ` <div class='toast-icon'>${svg(icon)}</div> <div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</div> @@ -74,7 +78,8 @@ function showToast(message: string, level: Intent, {gravity, position, duration, toast.showToast(); toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast()); - toast.toastElement.setAttribute('data-toast-unique-key', key); + toast.toastElement.setAttribute('data-toast-unique-key', duplicateKey); + (toast.toastElement as ToastifyElement)._giteaToastifyInstance = toast; return toast; } @@ -89,3 +94,15 @@ export function showWarningToast(message: string, opts?: ToastOpts): Toast { export function showErrorToast(message: string, opts?: ToastOpts): Toast { return showToast(message, 'error', opts); } + +function hideToastByElement(el: Element): void { + (el as ToastifyElement)?._giteaToastifyInstance?.hideToast(); +} + +export function hideToastsFrom(parent: Element): void { + queryElems(parent, ':scope > .toastify.on', hideToastByElement); +} + +export function hideToastsAll(): void { + queryElems(document, '.toastify.on', hideToastByElement); +} diff --git a/web_src/js/render/pdf.ts b/web_src/js/render/pdf.ts deleted file mode 100644 index 283b4ed85c..0000000000 --- a/web_src/js/render/pdf.ts +++ /dev/null @@ -1,17 +0,0 @@ -import {htmlEscape} from 'escape-goat'; -import {registerGlobalInitFunc} from '../modules/observer.ts'; - -export async function initPdfViewer() { - registerGlobalInitFunc('initPdfViewer', async (el: HTMLInputElement) => { - const pdfobject = await import(/* webpackChunkName: "pdfobject" */'pdfobject'); - - const src = el.getAttribute('data-src'); - const fallbackText = el.getAttribute('data-fallback-button-text'); - pdfobject.embed(src, el, { - fallbackLink: htmlEscape` - <a role="button" class="ui basic button pdf-fallback-button" href="[url]">${fallbackText}</a> - `, - }); - el.classList.remove('is-loading'); - }); -} diff --git a/web_src/js/render/plugin.ts b/web_src/js/render/plugin.ts new file mode 100644 index 0000000000..a8dd0a7c05 --- /dev/null +++ b/web_src/js/render/plugin.ts @@ -0,0 +1,10 @@ +export type FileRenderPlugin = { + // unique plugin name + name: string; + + // test if plugin can handle a specified file + canHandle: (filename: string, mimeType: string) => boolean; + + // render file content + render: (container: HTMLElement, fileUrl: string, options?: any) => Promise<void>; +} diff --git a/web_src/js/render/plugins/3d-viewer.ts b/web_src/js/render/plugins/3d-viewer.ts new file mode 100644 index 0000000000..2a0929359d --- /dev/null +++ b/web_src/js/render/plugins/3d-viewer.ts @@ -0,0 +1,60 @@ +import type {FileRenderPlugin} from '../plugin.ts'; +import {extname} from '../../utils.ts'; + +// support common 3D model file formats, use online-3d-viewer library for rendering + +// eslint-disable-next-line multiline-comment-style +/* 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(/* webpackChunkName: "online-3d-viewer" */'online-3d-viewer'); + const viewer = new OV.EmbeddedViewer(container, { + backgroundColor: new OV.RGBAColor(59, 68, 76, 0), + defaultColor: new OV.RGBColor(65, 131, 196), + edgeSettings: new OV.EdgeSettings(false, new OV.RGBColor(0, 0, 0), 1), + }); + viewer.LoadModelFromUrlList([fileUrl]); + }, + }; +} diff --git a/web_src/js/render/plugins/pdf-viewer.ts b/web_src/js/render/plugins/pdf-viewer.ts new file mode 100644 index 0000000000..40623be055 --- /dev/null +++ b/web_src/js/render/plugins/pdf-viewer.ts @@ -0,0 +1,20 @@ +import type {FileRenderPlugin} from '../plugin.ts'; + +export function newRenderPluginPdfViewer(): FileRenderPlugin { + return { + name: 'pdf-viewer', + + canHandle(filename: string, _mimeType: string): boolean { + return filename.toLowerCase().endsWith('.pdf'); + }, + + async render(container: HTMLElement, fileUrl: string): Promise<void> { + const PDFObject = await import(/* webpackChunkName: "pdfobject" */'pdfobject'); + // TODO: the PDFObject library does not support dynamic height adjustment, + container.style.height = `${window.innerHeight - 100}px`; + if (!PDFObject.default.embed(fileUrl, container)) { + throw new Error('Unable to render the PDF file'); + } + }, + }; +} diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts index 7b377e1ab4..50c9536f37 100644 --- a/web_src/js/svg.ts +++ b/web_src/js/svg.ts @@ -1,5 +1,6 @@ import {defineComponent, h, type PropType} from 'vue'; import {parseDom, serializeXml} from './utils.ts'; +import {html, htmlRaw} from './utils/html.ts'; import giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg'; import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg'; import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox.svg'; @@ -220,7 +221,7 @@ export const SvgIcon = defineComponent({ const classes = Array.from(svgOuter.classList); if (this.symbolId) { classes.push('tw-hidden', 'svg-symbol-container'); - svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`; + svgInnerHtml = html`<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${htmlRaw(svgInnerHtml)}</symbol>`; } // create VNode return h('svg', { diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 8f758bf9ac..8b540cebb1 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -314,6 +314,7 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st export function createElementFromHTML<T extends HTMLElement>(htmlString: string): T { htmlString = htmlString.trim(); // some tags like "tr" are special, it must use a correct parent container to create + // eslint-disable-next-line github/unescaped-html-literal -- FIXME: maybe we need to use other approaches to create elements from HTML, e.g. using DOMParser if (htmlString.startsWith('<tr')) { const container = document.createElement('table'); container.innerHTML = htmlString; @@ -369,3 +370,8 @@ export function addDelegatedEventListener<T extends HTMLElement, E extends Event listener(elem as T, e as E); }, options); } + +/** Returns whether a click event is a left-click without any modifiers held */ +export function isPlainClick(e: MouseEvent) { + return e.button === 0 && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey; +} diff --git a/web_src/js/utils/html.test.ts b/web_src/js/utils/html.test.ts new file mode 100644 index 0000000000..3028b7bb0a --- /dev/null +++ b/web_src/js/utils/html.test.ts @@ -0,0 +1,8 @@ +import {html, htmlEscape, htmlRaw} from './html.ts'; + +test('html', async () => { + expect(html`<a>${'<>&\'"'}</a>`).toBe(`<a><>&'"</a>`); + expect(html`<a>${htmlRaw('<img>')}</a>`).toBe(`<a><img></a>`); + expect(html`<a>${htmlRaw`<img ${'&'}>`}</a>`).toBe(`<a><img &></a>`); + expect(htmlEscape(`<a></a>`)).toBe(`<a></a>`); +}); diff --git a/web_src/js/utils/html.ts b/web_src/js/utils/html.ts new file mode 100644 index 0000000000..22e5703c34 --- /dev/null +++ b/web_src/js/utils/html.ts @@ -0,0 +1,32 @@ +export function htmlEscape(s: string, ...args: Array<any>): string { + if (args.length !== 0) throw new Error('use html or htmlRaw instead of htmlEscape'); // check legacy usages + return s.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/</g, '<') + .replace(/>/g, '>'); +} + +class rawObject { + private readonly value: string; + constructor(v: string) { this.value = v } + toString(): string { return this.value } +} + +export function html(tmpl: TemplateStringsArray, ...parts: Array<any>): string { + let output = tmpl[0]; + for (let i = 0; i < parts.length; i++) { + const value = parts[i]; + const valueEscaped = (value instanceof rawObject) ? value.toString() : htmlEscape(String(parts[i])); + output = output + valueEscaped + tmpl[i + 1]; + } + return output; +} + +export function htmlRaw(s: string|TemplateStringsArray, ...tmplParts: Array<any>): rawObject { + if (typeof s === 'string') { + if (tmplParts.length !== 0) throw new Error("either htmlRaw('str') or htmlRaw`tmpl`"); + return new rawObject(s); + } + return new rawObject(html(s, ...tmplParts)); +} |