diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ab30e1789d..4128f1466f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ "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": {}, 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/.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 703be8f681..0791a17c71 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ _test .vscode __debug_bin* +# Visual Studio +/.vs/ + *.cgo1.go *.cgo2.c _cgo_defun.c @@ -39,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/.ignore b/.ignore index 5b96dabd38..29912ad5c3 100644 --- a/.ignore +++ b/.ignore @@ -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 \ diff --git a/MAINTAINERS b/MAINTAINERS index 7d21f449fe..7643ab000f 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -64,3 +64,4 @@ Rowan Bohde (@bohde) hiifong (@hiifong) metiftikci (@metiftikci) Christopher Homberger (@ChristopherHX) +Tobias Balle-Petersen (@tobiasbp) diff --git a/Makefile b/Makefile index d10250bbc7..c868ef4463 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,8 @@ 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.0 +GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.19.0 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest @@ -120,8 +121,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 +149,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 +220,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 +231,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 +250,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 +275,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 +302,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 +380,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 +823,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 1693b0a506..d961444239 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -625,8 +625,8 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/google/go-github/v61/github", - "path": "github.com/google/go-github/v61/github/LICENSE", + "name": "github.com/google/go-github/v71/github", + "path": "github.com/google/go-github/v71/github/LICENSE", "licenseText": "Copyright (c) 2013 The go-github AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, { @@ -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", @@ -1109,11 +1114,6 @@ "path": "github.com/xanzy/ssh-agent/LICENSE", "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", diff --git a/build.go b/build.go index 234579b514..e81ba54690 100644 --- a/build.go +++ b/build.go @@ -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") + if len(os.Args) != 3 { + fmt.Println("usage: ./generate-bindata {local-directory} {bindata-filename}") + os.Exit(1) } - 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]) + 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) } - - update, newHash := needsUpdate(dir, filename) - - if !update { - fmt.Printf("bindata for %s already up-to-date\n", packageName) - return - } - - 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) - } - _ = 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 274ec181d1..069ad6600c 100644 --- a/cmd/admin_auth_ldap.go +++ b/cmd/admin_auth_ldap.go @@ -9,9 +9,10 @@ import ( "strings" "code.gitea.io/gitea/models/auth" + "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 ( @@ -23,8 +24,8 @@ type ( } ) -var ( - commonLdapCLIFlags = []cli.Flag{ +func commonLdapCLIFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "name", Usage: "Authentication name.", @@ -102,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.", @@ -156,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 { @@ -210,8 +223,8 @@ func newAuthService() *authService { } } -// parseAuthSource assigns values on authSource according to command line flags. -func parseAuthSource(c *cli.Context, authSource *auth.Source) { +// parseAuthSourceLdap assigns values on authSource according to command line flags. +func parseAuthSourceLdap(c *cli.Command, authSource *auth.Source) { if c.IsSet("name") { authSource.Name = c.String("name") } @@ -227,10 +240,11 @@ func parseAuthSource(c *cli.Context, authSource *auth.Source) { if c.IsSet("disable-synchronize-users") { authSource.IsSyncEnabled = !c.Bool("disable-synchronize-users") } + authSource.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") } // 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") } @@ -243,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 } @@ -298,9 +312,6 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error { if c.IsSet("allow-deactivate-all") { config.AllowDeactivateAll = c.Bool("allow-deactivate-all") } - if c.IsSet("skip-local-2fa") { - config.SkipLocalTwoFA = c.Bool("skip-local-2fa") - } if c.IsSet("enable-groups") { config.GroupsEnabled = c.Bool("enable-groups") } @@ -338,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 } @@ -376,7 +382,7 @@ func (a *authService) addLdapBindDn(c *cli.Context) error { }, } - parseAuthSource(c, authSource) + parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } @@ -385,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 } @@ -398,7 +401,7 @@ func (a *authService) updateLdapBindDn(c *cli.Context) error { return err } - parseAuthSource(c, authSource) + parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } @@ -407,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 } @@ -427,7 +427,7 @@ func (a *authService) addLdapSimpleAuth(c *cli.Context) error { }, } - parseAuthSource(c, authSource) + parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); err != nil { return err } @@ -436,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 } @@ -449,7 +446,7 @@ func (a *authService) updateLdapSimpleAuth(c *cli.Context) error { return err } - parseAuthSource(c, authSource) + parseAuthSourceLdap(c, authSource) if err := parseLdapConfig(c, authSource.Cfg.(*ldap.Source)); 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 8e6239ac33..d1aa753500 100644 --- a/cmd/admin_auth_oauth.go +++ b/cmd/admin_auth_oauth.go @@ -4,18 +4,20 @@ package cmd import ( + "context" "errors" "fmt" "net/url" auth_model "code.gitea.io/gitea/models/auth" + "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: "", @@ -120,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{ @@ -156,7 +169,6 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source { OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"), CustomURLMapping: customURLMapping, IconURL: c.String("icon-url"), - SkipLocalTwoFA: c.Bool("skip-local-2fa"), Scopes: c.StringSlice("scopes"), RequiredClaimName: c.String("required-claim-name"), RequiredClaimValue: c.String("required-claim-value"), @@ -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,27 +193,25 @@ func runAddOauth(c *cli.Context) error { } } - return auth_model.CreateSource(ctx, &auth_model.Source{ - Type: auth_model.OAuth2, - Name: c.String("name"), - IsActive: true, - Cfg: config, + return a.createAuthSource(ctx, &auth_model.Source{ + Type: auth_model.OAuth2, + Name: c.String("name"), + IsActive: true, + Cfg: config, + TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""), }) } -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 } @@ -294,6 +301,6 @@ func runUpdateOauth(c *cli.Context) error { oAuth2Config.CustomURLMapping = customURLMapping source.Cfg = oAuth2Config - - return auth_model.UpdateSource(ctx, source) + source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") + 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 similarity index 67% rename from cmd/admin_auth_stmp.go rename to cmd/admin_auth_smtp.go index d724746905..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"} @@ -117,17 +125,11 @@ func parseSMTPConfig(c *cli.Context, conf *smtp.Source) error { if c.IsSet("disable-helo") { conf.DisableHelo = c.Bool("disable-helo") } - if c.IsSet("skip-local-2fa") { - conf.SkipLocalTwoFA = c.Bool("skip-local-2fa") - } 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 } @@ -155,27 +157,25 @@ func runAddSMTP(c *cli.Context) error { smtpConfig.Auth = "PLAIN" } - return auth_model.CreateSource(ctx, &auth_model.Source{ - Type: auth_model.SMTP, - Name: c.String("name"), - IsActive: active, - Cfg: &smtpConfig, + return a.createAuthSource(ctx, &auth_model.Source{ + Type: auth_model.SMTP, + Name: c.String("name"), + IsActive: active, + Cfg: &smtpConfig, + TwoFactorPolicy: util.Iif(c.Bool("skip-local-2fa"), "skip", ""), }) } -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 } @@ -195,6 +195,6 @@ func runUpdateSMTP(c *cli.Context) error { } source.Cfg = smtpConfig - - return auth_model.UpdateSource(ctx, source) + source.TwoFactorPolicy = util.Iif(c.Bool("skip-local-2fa"), "skip", "") + 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 3a24cf6c5f..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,11 +79,13 @@ wiki, issues, labels, releases, release_assets, milestones, pull_requests, comme }, } -func runDumpRepository(ctx *cli.Context) error { - stdCtx, cancel := installSignals() - defer cancel() +func runDumpRepository(ctx context.Context, cmd *cli.Command) error { + setupConsoleLogger(log.INFO, log.CanColorStderr, os.Stderr) - if err := initDB(stdCtx); err != nil { + setting.DisableLoggerInit() + setting.LoadSettings() // cannot access skip_tls_verify settings otherwise + + if err := initDB(ctx); err != nil { return err } @@ -100,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/") { @@ -119,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 @@ -135,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 @@ -164,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 { @@ -179,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..6a2fa07a93 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 { 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 b18508459f..8c6001e727 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -11,7 +11,6 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "strconv" "strings" "time" @@ -20,7 +19,7 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" - "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/lfstransfer" @@ -34,15 +33,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/kballard/go-shellquote" - "github.com/urfave/cli/v2" -) - -const ( - verbUploadPack = "git-upload-pack" - verbUploadArchive = "git-upload-archive" - verbReceivePack = "git-receive-pack" - verbLfsAuthenticate = "git-lfs-authenticate" - verbLfsTransfer = "git-lfs-transfer" + "github.com/urfave/cli/v3" ) // CmdServ represents the available serv sub-command. @@ -78,22 +69,6 @@ func setup(ctx context.Context, debug bool) { } } -var ( - // keep getAccessMode() in sync - allowedCommands = container.SetOf( - verbUploadPack, - verbUploadArchive, - verbReceivePack, - verbLfsAuthenticate, - verbLfsTransfer, - ) - allowedCommandsLfs = container.SetOf( - verbLfsAuthenticate, - verbLfsTransfer, - ) - alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) -) - // fail prints message to stdout, it's mainly used for git serv and git hook commands. // The output will be passed to git client and shown to user. func fail(ctx context.Context, userMessage, logMsgFmt string, args ...any) error { @@ -139,19 +114,20 @@ func handleCliResponseExtra(extra private.ResponseExtra) error { func getAccessMode(verb, lfsVerb string) perm.AccessMode { switch verb { - case verbUploadPack, verbUploadArchive: + case git.CmdVerbUploadPack, git.CmdVerbUploadArchive: return perm.AccessModeRead - case verbReceivePack: + case git.CmdVerbReceivePack: return perm.AccessModeWrite - case verbLfsAuthenticate, verbLfsTransfer: + case git.CmdVerbLfsAuthenticate, git.CmdVerbLfsTransfer: switch lfsVerb { - case "upload": + case git.CmdSubVerbLfsUpload: return perm.AccessModeWrite - case "download": + case git.CmdSubVerbLfsDownload: return perm.AccessModeRead } } // should be unreachable + setting.PanicInDevOrTesting("unknown verb: %s %s", verb, lfsVerb) return perm.AccessModeNone } @@ -176,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")) @@ -230,41 +203,37 @@ func runServ(c *cli.Context) error { log.Debug("SSH_ORIGINAL_COMMAND: %s", os.Getenv("SSH_ORIGINAL_COMMAND")) } - words, err := shellquote.Split(cmd) + sshCmdArgs, err := shellquote.Split(cmd) if err != nil { return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err) } - if len(words) < 2 { + if len(sshCmdArgs) < 2 { 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 } } return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd) } - verb := words[0] - repoPath := strings.TrimPrefix(words[1], "/") - - var lfsVerb string - - rr := strings.SplitN(repoPath, "/", 2) - if len(rr) != 2 { + repoPath := strings.TrimPrefix(sshCmdArgs[1], "/") + repoPathFields := strings.SplitN(repoPath, "/", 2) + if len(repoPathFields) != 2 { return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath) } - username := rr[0] - reponame := strings.TrimSuffix(rr[1], ".git") + username := repoPathFields[0] + reponame := strings.TrimSuffix(repoPathFields[1], ".git") // “the-repo-name" or "the-repo-name.wiki" // LowerCase and trim the repoPath as that's how they are stored. // This should be done after splitting the repoPath into username and reponame // so that username and reponame are not affected. repoPath = strings.ToLower(strings.TrimSpace(repoPath)) - if alphaDashDotPattern.MatchString(reponame) { + if !repo.IsValidSSHAccessRepoName(reponame) { return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) } @@ -286,22 +255,23 @@ func runServ(c *cli.Context) error { }() } - if allowedCommands.Contains(verb) { - if allowedCommandsLfs.Contains(verb) { - if !setting.LFS.StartServer { - return fail(ctx, "LFS Server is not enabled", "") - } - if verb == verbLfsTransfer && !setting.LFS.AllowPureSSH { - return fail(ctx, "LFS SSH transfer is not enabled", "") - } - if len(words) > 2 { - lfsVerb = words[2] - } - } - } else { + verb, lfsVerb := sshCmdArgs[0], "" + if !git.IsAllowedVerbForServe(verb) { return fail(ctx, "Unknown git command", "Unknown git command %s", verb) } + if git.IsAllowedVerbForServeLfs(verb) { + if !setting.LFS.StartServer { + return fail(ctx, "LFS Server is not enabled", "") + } + if verb == git.CmdVerbLfsTransfer && !setting.LFS.AllowPureSSH { + return fail(ctx, "LFS SSH transfer is not enabled", "") + } + if len(sshCmdArgs) > 2 { + lfsVerb = sshCmdArgs[2] + } + } + requestedMode := getAccessMode(verb, lfsVerb) results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) @@ -310,7 +280,7 @@ func runServ(c *cli.Context) error { } // LFS SSH protocol - if verb == verbLfsTransfer { + if verb == git.CmdVerbLfsTransfer { token, err := getLFSAuthToken(ctx, lfsVerb, results) if err != nil { return err @@ -319,7 +289,7 @@ func runServ(c *cli.Context) error { } // LFS token authentication - if verb == verbLfsAuthenticate { + if verb == git.CmdVerbLfsAuthenticate { url := fmt.Sprintf("%s%s/%s.git/info/lfs", setting.AppURL, url.PathEscape(results.OwnerName), url.PathEscape(results.RepoName)) token, err := getLFSAuthToken(ctx, lfsVerb, results) 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 9b30480300..6fbd610e62 100644 --- a/contrib/backport/backport.go +++ b/contrib/backport/backport.go @@ -12,21 +12,19 @@ import ( "net/http" "os" "os/exec" - "os/signal" "path" "strconv" "strings" - "syscall" - "github.com/google/go-github/v61/github" - "github.com/urfave/cli/v2" + "github.com/google/go-github/v71/github" + "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 42d181a00a..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 @@ -524,6 +520,10 @@ INTERNAL_TOKEN = ;; ;; On user registration, record the IP address and user agent of the user to help identify potential abuse. ;; RECORD_USER_SIGNUP_METADATA = false +;; +;; Set the two-factor auth behavior. +;; Set to "enforced", to force users to enroll into Two-Factor Authentication, users without 2FA have no access to repositories via API or web. +;TWO_FACTOR_AUTH = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1186,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 @@ -1223,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- ". or "ssh- , ssh- ". +;TRUSTED_SSH_KEYS = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/flake.lock b/flake.lock index 2f7b86359b..da3f19bbd2 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1739214665, - "narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=", + "lastModified": 1747179050, + "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a", + "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index bd234a1b61..20fee4a6e6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module code.gitea.io/gitea -go 1.24 +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: @@ -60,13 +60,12 @@ 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 github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 github.com/golang-jwt/jwt/v5 v5.2.2 - github.com/google/go-github/v61 v61.0.0 + github.com/google/go-github/v71 v71.0.0 github.com/google/licenseclassifier/v2 v2.0.0 github.com/google/pprof v0.0.0-20250422154841-e1f9c1950416 github.com/google/uuid v1.6.0 @@ -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,9 +276,7 @@ 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.4 +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 replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0 @@ -325,6 +284,8 @@ replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-tra // TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2 +replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 + exclude github.com/gofrs/uuid v3.2.0+incompatible exclude github.com/gofrs/uuid v4.0.0+incompatible diff --git a/go.sum b/go.sum index 9d71981e16..d8c19e3813 100644 --- a/go.sum +++ b/go.sum @@ -14,12 +14,12 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= -git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= -gitea.com/gitea/act v0.261.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8= -gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= +gitea.com/gitea/act v0.261.6 h1:CjZwKOyejonNFDmsXOw3wGm5Vet573hHM6VMLsxtvPY= +gitea.com/gitea/act v0.261.6/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40= gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= +gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4= +gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g= @@ -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= @@ -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= @@ -420,8 +382,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= -github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= +github.com/google/go-github/v71 v71.0.0 h1:Zi16OymGKZZMm8ZliffVVJ/Q9YZreDKONCr+WUd0Z30= +github.com/google/go-github/v71 v71.0.0/go.mod h1:URZXObp2BLlMjwu0O8g4y6VBneUj2bCHgnI8FfgZ51M= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= @@ -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= diff --git a/main.go b/main.go index 756c3e0f9b..2c25bac4e3 100644 --- a/main.go +++ b/main.go @@ -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/artifact.go b/models/actions/artifact.go index 524224f070..757bd13acd 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -30,6 +30,25 @@ const ( ArtifactStatusDeleted // 6, ArtifactStatusDeleted is the status of an artifact that is deleted ) +func (status ArtifactStatus) ToString() string { + switch status { + case ArtifactStatusUploadPending: + return "upload is not yet completed" + case ArtifactStatusUploadConfirmed: + return "upload is completed" + case ArtifactStatusUploadError: + return "upload failed" + case ArtifactStatusExpired: + return "expired" + case ArtifactStatusPendingDeletion: + return "pending deletion" + case ArtifactStatusDeleted: + return "deleted" + default: + return "unknown" + } +} + func init() { db.RegisterModel(new(ActionArtifact)) } diff --git a/models/actions/run.go b/models/actions/run.go index 5f077940c5..498a73dc20 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -16,6 +16,7 @@ import ( 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/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -171,6 +172,7 @@ func (run *ActionRun) IsSchedule() bool { func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { _, err := db.GetEngine(ctx).ID(repo.ID). + NoAutoTime(). SetExpr("num_action_runs", builder.Select("count(*)").From("action_run"). Where(builder.Eq{"repo_id": repo.ID}), @@ -342,13 +344,13 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork return committer.Commit() } -func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { +func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) { var run ActionRun - has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run) + has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", runID, repoID).Get(&run) if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf("run with id %d: %w", id, util.ErrNotExist) + return nil, fmt.Errorf("run with id %d: %w", runID, util.ErrNotExist) } return &run, nil @@ -419,17 +421,10 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { if run.Status != 0 || slices.Contains(cols, "status") { if run.RepoID == 0 { - run, err = GetRunByID(ctx, run.ID) - if err != nil { - return err - } + setting.PanicInDevOrTesting("RepoID should not be 0") } - if run.Repo == nil { - repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) - if err != nil { - return err - } - run.Repo = repo + if err = run.LoadRepo(ctx); err != nil { + return err } if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { return err diff --git a/models/actions/run_job.go b/models/actions/run_job.go index d0dfd10db6..c0df19b020 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -51,7 +51,7 @@ func (job *ActionRunJob) Duration() time.Duration { func (job *ActionRunJob) LoadRun(ctx context.Context) error { if job.Run == nil { - run, err := GetRunByID(ctx, job.RunID) + run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) if err != nil { return err } @@ -142,7 +142,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col { // Other goroutines may aggregate the status of the run and update it too. // So we need load the run and its jobs before updating the run. - run, err := GetRunByID(ctx, job.RunID) + run, err := GetRunByRepoAndID(ctx, job.RepoID, job.RunID) if err != nil { return 0, err } diff --git a/models/actions/runner.go b/models/actions/runner.go index b55723efa0..81d4249ae0 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -5,6 +5,7 @@ package actions import ( "context" + "errors" "fmt" "strings" "time" @@ -298,6 +299,23 @@ func DeleteRunner(ctx context.Context, id int64) error { return err } +// DeleteEphemeralRunner deletes a ephemeral runner by given ID. +func DeleteEphemeralRunner(ctx context.Context, id int64) error { + runner, err := GetRunnerByID(ctx, id) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + return nil + } + return err + } + if !runner.Ephemeral { + return nil + } + + _, err = db.DeleteByID[ActionRunner](ctx, id) + return err +} + // CreateRunner creates new runner. func CreateRunner(ctx context.Context, t *ActionRunner) error { if t.OwnerID != 0 && t.RepoID != 0 { 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 43f11b2730..63259582f6 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -336,6 +336,11 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { sess.Cols(cols...) } _, err := sess.Update(task) + + // Automatically delete the ephemeral runner if the task is done + if err == nil && task.Status.IsDone() && util.SliceContainsString(cols, "status") { + return DeleteEphemeralRunner(ctx, task.RunnerID) + } return err } diff --git a/models/actions/task_list.go b/models/actions/task_list.go index df4b43c5ef..0c80397899 100644 --- a/models/actions/task_list.go +++ b/models/actions/task_list.go @@ -48,6 +48,7 @@ func (tasks TaskList) LoadAttributes(ctx context.Context) error { type FindTaskOptions struct { db.ListOptions RepoID int64 + JobID int64 OwnerID int64 CommitSHA string Status Status @@ -61,6 +62,9 @@ func (opts FindTaskOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } + if opts.JobID > 0 { + cond = cond.And(builder.Eq{"job_id": opts.JobID}) + } if opts.OwnerID > 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } diff --git a/models/actions/utils.go b/models/actions/utils.go index 12657942fc..f6ba661ae3 100644 --- a/models/actions/utils.go +++ b/models/actions/utils.go @@ -82,3 +82,22 @@ func calculateDuration(started, stopped timeutil.TimeStamp, status Status) time. } return timeSince(s).Truncate(time.Second) } + +// best effort function to convert an action schedule to action run, to be used in GenerateGiteaContext +func (s *ActionSchedule) ToActionRun() *ActionRun { + return &ActionRun{ + Title: s.Title, + RepoID: s.RepoID, + Repo: s.Repo, + OwnerID: s.OwnerID, + WorkflowID: s.WorkflowID, + TriggerUserID: s.TriggerUserID, + TriggerUser: s.TriggerUser, + Ref: s.Ref, + CommitSHA: s.CommitSHA, + Event: s.Event, + EventPayload: s.EventPayload, + Created: s.Created, + Updated: s.Updated, + } +} diff --git a/models/activities/action.go b/models/activities/action.go index c89ba3e14e..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) { @@ -530,7 +526,7 @@ func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder. if opts.RequestedTeam != nil { env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam) - teamRepoIDs, err := env.RepoIDs(ctx, 1, opts.RequestedUser.NumRepos) + teamRepoIDs, err := env.RepoIDs(ctx) if err != nil { return nil, fmt.Errorf("GetTeamRepositories: %w", 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 ff81ad78a1..940651d359 100644 --- a/models/activities/statistic.go +++ b/models/activities/statistic.go @@ -17,13 +17,16 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "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 type Statistic struct { Counter struct { - User, Org, PublicKey, + UsersActive, UsersNotActive, + Org, PublicKey, Repo, Watch, Star, Access, Issue, IssueClosed, IssueOpen, Comment, Oauth, Follow, @@ -53,8 +56,20 @@ type IssueByRepositoryCount struct { // GetStatistic returns the database statistics func GetStatistic(ctx context.Context) (stats Statistic) { e := db.GetEngine(ctx) - stats.Counter.User = user_model.CountUsers(ctx, nil) - stats.Counter.Org, _ = db.Count[organization.Organization](ctx, organization.FindOrgOptions{IncludePrivate: true}) + + // Number of active users + usersActiveOpts := user_model.CountUserFilter{ + IsActive: optional.Some(true), + } + stats.Counter.UsersActive = user_model.CountUsers(ctx, &usersActiveOpts) + + // Number of inactive users + usersNotActiveOpts := user_model.CountUserFilter{ + IsActive: optional.Some(false), + } + stats.Counter.UsersNotActive = user_model.CountUsers(ctx, &usersNotActiveOpts) + + 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/ssh_key_parse.go b/models/asymkey/ssh_key_parse.go index 46dcf4d894..00d75b8e82 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 rawPub := struct { Name string P, Q, G, Y *big.Int 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/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/auth/source.go b/models/auth/source.go index a3a250cd91..7d7bc0f03c 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -58,6 +58,15 @@ var Names = map[Type]string{ // Config represents login config as far as the db is concerned type Config interface { convert.Conversion + SetAuthSource(*Source) +} + +type ConfigBase struct { + AuthSource *Source +} + +func (p *ConfigBase) SetAuthSource(s *Source) { + p.AuthSource = s } // SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set @@ -104,19 +113,15 @@ func RegisterTypeConfig(typ Type, exemplar Config) { } } -// SourceSettable configurations can have their authSource set on them -type SourceSettable interface { - SetAuthSource(*Source) -} - // Source represents an external way for authorizing users. type Source struct { - ID int64 `xorm:"pk autoincr"` - Type Type - Name string `xorm:"UNIQUE"` - IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"` - IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"` - Cfg convert.Conversion `xorm:"TEXT"` + ID int64 `xorm:"pk autoincr"` + Type Type + Name string `xorm:"UNIQUE"` + IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"` + IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"` + TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"` + Cfg Config `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -140,9 +145,7 @@ func (source *Source) BeforeSet(colName string, val xorm.Cell) { return } source.Cfg = constructor() - if settable, ok := source.Cfg.(SourceSettable); ok { - settable.SetAuthSource(source) - } + source.Cfg.SetAuthSource(source) } } @@ -200,6 +203,10 @@ func (source *Source) SkipVerify() bool { return ok && skipVerifiable.IsSkipVerify() } +func (source *Source) TwoFactorShouldSkip() bool { + return source.TwoFactorPolicy == "skip" +} + // CreateSource inserts a AuthSource in the DB if not already // existing with the given name. func CreateSource(ctx context.Context, source *Source) error { @@ -223,9 +230,7 @@ func CreateSource(ctx context.Context, source *Source) error { return nil } - if settable, ok := source.Cfg.(SourceSettable); ok { - settable.SetAuthSource(source) - } + source.Cfg.SetAuthSource(source) registerableSource, ok := source.Cfg.(RegisterableSource) if !ok { @@ -320,9 +325,7 @@ func UpdateSource(ctx context.Context, source *Source) error { return nil } - if settable, ok := source.Cfg.(SourceSettable); ok { - settable.SetAuthSource(source) - } + source.Cfg.SetAuthSource(source) registerableSource, ok := source.Cfg.(RegisterableSource) if !ok { diff --git a/models/auth/source_test.go b/models/auth/source_test.go index 84aede0a6b..64c7460b64 100644 --- a/models/auth/source_test.go +++ b/models/auth/source_test.go @@ -19,6 +19,8 @@ import ( ) type TestSource struct { + auth_model.ConfigBase + Provider string ClientID string ClientSecret string diff --git a/models/auth/twofactor.go b/models/auth/twofactor.go index d0c341a192..200ce7c7c0 100644 --- a/models/auth/twofactor.go +++ b/models/auth/twofactor.go @@ -164,3 +164,13 @@ func DeleteTwoFactorByID(ctx context.Context, id, userID int64) error { } return nil } + +func HasTwoFactorOrWebAuthn(ctx context.Context, id int64) (bool, error) { + has, err := HasTwoFactorByUID(ctx, id) + if err != nil { + return false, err + } else if has { + return true, nil + } + return HasWebAuthnRegistrationsByUID(ctx, id) +} diff --git a/models/db/context.go b/models/db/context.go index 4b98796ef0..05d7d72daa 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")) } 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/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_artifact.yml b/models/fixtures/action_artifact.yml index 485474108f..ee8ef0d5ce 100644 --- a/models/fixtures/action_artifact.yml +++ b/models/fixtures/action_artifact.yml @@ -11,6 +11,24 @@ content_encoding: "" artifact_path: "abc.txt" artifact_name: "artifact-download" + status: 2 + created_unix: 1712338649 + updated_unix: 1712338649 + expired_unix: 1720114649 + +- + id: 2 + run_id: 791 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "30/20/1712348022422036662.chunk" + artifact_path: "abc.txt" + artifact_name: "artifact-download-incomplete" status: 1 created_unix: 1712338649 updated_unix: 1712338649 @@ -87,3 +105,39 @@ created_unix: 1730330775 updated_unix: 1730330775 expired_unix: 1738106775 + +- + id: 24 + run_id: 795 + runner_id: 1 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/zip" + artifact_path: "artifact-795-1.zip" + artifact_name: "artifact-795-1" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 + +- + id: 25 + run_id: 795 + runner_id: 1 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/zip" + artifact_path: "artifact-795-2.zip" + artifact_name: "artifact-795-2" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 1db849352f..ae7dc481ec 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -48,7 +48,7 @@ commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" is_fork_pull_request: 0 - status: 1 + status: 6 # running started: 1683636528 stopped: 1683636626 created: 1683636108 @@ -74,3 +74,23 @@ updated: 1683636626 need_approval: 0 approved_by: 0 + +- + id: 795 + title: "to be deleted (test)" + repo_id: 2 + owner_id: 2 + workflow_id: "test.yaml" + index: 191 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 2 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 8837e6ec2d..72f8627224 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -69,3 +69,33 @@ status: 5 started: 1683636528 stopped: 1683636626 + +- + id: 198 + run_id: 795 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job_1 + attempt: 1 + job_id: job_1 + task_id: 53 + status: 1 + started: 1683636528 + stopped: 1683636626 + +- + id: 199 + run_id: 795 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job_2 + attempt: 1 + job_id: job_2 + task_id: 54 + status: 2 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_runner.yml b/models/fixtures/action_runner.yml index dce2d41cfb..ecb7214006 100644 --- a/models/fixtures/action_runner.yml +++ b/models/fixtures/action_runner.yml @@ -38,3 +38,14 @@ repo_id: 0 description: "This runner is going to be deleted" agent_labels: '["runner_to_be_deleted","linux"]' +- + id: 34350 + name: runner_to_be_deleted-org-ephemeral + uuid: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20 + token_hash: 3FF231BD-FBB7-4E4B-9602-E6F28363EF20 + ephemeral: true + version: "1.0.0" + owner_id: 3 + repo_id: 0 + description: "This runner is going to be deleted" + agent_labels: '["runner_to_be_deleted","linux"]' diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index 506a47d8a0..c79fb07050 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -117,3 +117,63 @@ log_length: 707 log_size: 90179 log_expired: 0 +- + id: 52 + job_id: 196 + attempt: 1 + runner_id: 34350 + status: 6 # running + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: f8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 707 + log_size: 90179 + log_expired: 0 +- + id: 53 + job_id: 198 + attempt: 1 + runner_id: 1 + status: 1 + started: 1683636528 + stopped: 1683636626 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784223 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 0 + log_size: 0 + log_expired: 0 +- + id: 54 + job_id: 199 + attempt: 1 + runner_id: 1 + status: 2 + started: 1683636528 + stopped: 1683636626 + repo_id: 2 + owner_id: 2 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784224 + token_salt: ffffffffff + token_last_eight: ffffffff + log_filename: artifact-test2/2f/47.log + log_in_storage: 1 + log_length: 0 + log_size: 0 + log_expired: 0 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/protected_branch.go b/models/git/protected_branch.go index a3caed73c4..19b02ccab9 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 { 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 28f9fd531d..6020edc95a 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" @@ -123,7 +124,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 () 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..84d5948640 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -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 - } +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 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]) - } - 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/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/migrations.go b/models/migrations/migrations.go index 31b035eb31..176372486e 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -381,6 +381,7 @@ func prepareMigrationTasks() []*migration { newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard), newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode), newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable), + newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor), } return preparedMigrations } diff --git a/models/migrations/v1_11/v111.go b/models/migrations/v1_11/v111.go index ff108479a9..1c8527b2aa 100644 --- a/models/migrations/v1_11/v111.go +++ b/models/migrations/v1_11/v111.go @@ -5,6 +5,7 @@ package v1_11 //nolint 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/v115.go b/models/migrations/v1_11/v115.go index 8c631cfd0b..c44c6d88e4 100644 --- a/models/migrations/v1_11/v115.go +++ b/models/migrations/v1_11/v115.go @@ -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_20/v259.go b/models/migrations/v1_20/v259.go index 5b8ced4ad7..0fdeb45957 100644 --- a/models/migrations/v1_20/v259.go +++ b/models/migrations/v1_20/v259.go @@ -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_22/v294_test.go b/models/migrations/v1_22/v294_test.go index a1d702cb77..c3de332650 100644 --- a/models/migrations/v1_22/v294_test.go +++ b/models/migrations/v1_22/v294_test.go @@ -4,7 +4,6 @@ package v1_22 //nolint 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_23/v299.go b/models/migrations/v1_23/v299.go index f6db960c3b..e5fde3749b 100644 --- a/models/migrations/v1_23/v299.go +++ b/models/migrations/v1_23/v299.go @@ -14,5 +14,9 @@ func AddContentVersionToIssueAndComment(x *xorm.Engine) error { ContentVersion int `xorm:"NOT NULL DEFAULT 0"` } - return x.Sync(new(Comment), new(Issue)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Comment), new(Issue)) + return err } diff --git a/models/migrations/v1_23/v300.go b/models/migrations/v1_23/v300.go index f1f1cccdbf..51de43da5e 100644 --- a/models/migrations/v1_23/v300.go +++ b/models/migrations/v1_23/v300.go @@ -13,5 +13,9 @@ func AddForcePushBranchProtection(x *xorm.Engine) error { ForcePushAllowlistTeamIDs []int64 `xorm:"JSON TEXT"` ForcePushAllowlistDeployKeys bool `xorm:"NOT NULL DEFAULT false"` } - return x.Sync(new(ProtectedBranch)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(ProtectedBranch)) + return err } diff --git a/models/migrations/v1_23/v301.go b/models/migrations/v1_23/v301.go index b7797f6c6b..99c8e3d8ea 100644 --- a/models/migrations/v1_23/v301.go +++ b/models/migrations/v1_23/v301.go @@ -10,5 +10,9 @@ func AddSkipSecondaryAuthColumnToOAuth2ApplicationTable(x *xorm.Engine) error { type oauth2Application struct { SkipSecondaryAuthorization bool `xorm:"NOT NULL DEFAULT FALSE"` } - return x.Sync(new(oauth2Application)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(oauth2Application)) + return err } diff --git a/models/migrations/v1_23/v302.go b/models/migrations/v1_23/v302.go index d7ea03eb3d..5d2e9b1438 100644 --- a/models/migrations/v1_23/v302.go +++ b/models/migrations/v1_23/v302.go @@ -14,5 +14,8 @@ func AddIndexToActionTaskStoppedLogExpired(x *xorm.Engine) error { Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"` LogExpired bool `xorm:"index(stopped_log_expired)"` } - return x.Sync(new(ActionTask)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionTask)) + return err } diff --git a/models/migrations/v1_23/v302_test.go b/models/migrations/v1_23/v302_test.go new file mode 100644 index 0000000000..29e85ae9d9 --- /dev/null +++ b/models/migrations/v1_23/v302_test.go @@ -0,0 +1,51 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func Test_AddIndexToActionTaskStoppedLogExpired(t *testing.T) { + type ActionTask struct { + ID int64 + JobID int64 + Attempt int64 + RunnerID int64 `xorm:"index"` + Status int `xorm:"index"` + Started timeutil.TimeStamp `xorm:"index"` + Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"` + + RepoID int64 `xorm:"index"` + OwnerID int64 `xorm:"index"` + CommitSHA string `xorm:"index"` + IsForkPullRequest bool + + Token string `xorm:"-"` + TokenHash string `xorm:"UNIQUE"` // sha256 of token + TokenSalt string + TokenLastEight string `xorm:"index token_last_eight"` + + LogFilename string // file name of log + LogInStorage bool // read log from database or from storage + LogLength int64 // lines count + LogSize int64 // blob size + LogIndexes []int64 `xorm:"LONGBLOB"` // line number to offset + LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted + + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated index"` + } + + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(ActionTask)) + defer deferable() + + assert.NoError(t, AddIndexToActionTaskStoppedLogExpired(x)) +} diff --git a/models/migrations/v1_23/v303.go b/models/migrations/v1_23/v303.go index adfe917d3f..1e36388930 100644 --- a/models/migrations/v1_23/v303.go +++ b/models/migrations/v1_23/v303.go @@ -19,5 +19,9 @@ func AddCommentMetaDataColumn(x *xorm.Engine) error { CommentMetaData *CommentMetaData `xorm:"JSON TEXT"` // put all non-index metadata in a single field } - return x.Sync(new(Comment)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Comment)) + return err } diff --git a/models/migrations/v1_23/v304.go b/models/migrations/v1_23/v304.go index 65cffedbd9..e108f47779 100644 --- a/models/migrations/v1_23/v304.go +++ b/models/migrations/v1_23/v304.go @@ -9,5 +9,8 @@ func AddIndexForReleaseSha1(x *xorm.Engine) error { type Release struct { Sha1 string `xorm:"INDEX VARCHAR(64)"` } - return x.Sync(new(Release)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(Release)) + return err } diff --git a/models/migrations/v1_23/v304_test.go b/models/migrations/v1_23/v304_test.go new file mode 100644 index 0000000000..955219d3f9 --- /dev/null +++ b/models/migrations/v1_23/v304_test.go @@ -0,0 +1,40 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_23 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func Test_AddIndexForReleaseSha1(t *testing.T) { + type Release struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX UNIQUE(n)"` + PublisherID int64 `xorm:"INDEX"` + TagName string `xorm:"INDEX UNIQUE(n)"` + OriginalAuthor string + OriginalAuthorID int64 `xorm:"index"` + LowerTagName string + Target string + Title string + Sha1 string `xorm:"VARCHAR(64)"` + NumCommits int64 + Note string `xorm:"TEXT"` + IsDraft bool `xorm:"NOT NULL DEFAULT false"` + IsPrerelease bool `xorm:"NOT NULL DEFAULT false"` + IsTag bool `xorm:"NOT NULL DEFAULT false"` // will be true only if the record is a tag and has no related releases + CreatedUnix timeutil.TimeStamp `xorm:"INDEX"` + } + + // Prepare and load the testing database + x, deferable := base.PrepareTestEnv(t, 0, new(Release)) + defer deferable() + + assert.NoError(t, AddIndexForReleaseSha1(x)) +} diff --git a/models/migrations/v1_23/v306.go b/models/migrations/v1_23/v306.go index 276b438e95..a1e698fe31 100644 --- a/models/migrations/v1_23/v306.go +++ b/models/migrations/v1_23/v306.go @@ -9,5 +9,9 @@ func AddBlockAdminMergeOverrideBranchProtection(x *xorm.Engine) error { type ProtectedBranch struct { BlockAdminMergeOverride bool `xorm:"NOT NULL DEFAULT false"` } - return x.Sync(new(ProtectedBranch)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(ProtectedBranch)) + return err } diff --git a/models/migrations/v1_23/v310.go b/models/migrations/v1_23/v310.go index 394417f5a0..c856a708f9 100644 --- a/models/migrations/v1_23/v310.go +++ b/models/migrations/v1_23/v310.go @@ -12,5 +12,9 @@ func AddPriorityToProtectedBranch(x *xorm.Engine) error { Priority int64 `xorm:"NOT NULL DEFAULT 0"` } - return x.Sync(new(ProtectedBranch)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(ProtectedBranch)) + return err } diff --git a/models/migrations/v1_23/v311.go b/models/migrations/v1_23/v311.go index 0fc1ac8c0e..21293d83be 100644 --- a/models/migrations/v1_23/v311.go +++ b/models/migrations/v1_23/v311.go @@ -11,6 +11,9 @@ func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error { type Issue struct { TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"` } - - return x.Sync(new(Issue)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Issue)) + return err } diff --git a/models/migrations/v1_24/v312.go b/models/migrations/v1_24/v312.go index 9766dc1ccf..367a6c4947 100644 --- a/models/migrations/v1_24/v312.go +++ b/models/migrations/v1_24/v312.go @@ -17,5 +17,9 @@ func (pullAutoMerge) TableName() string { } func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error { - return x.Sync(new(pullAutoMerge)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(pullAutoMerge)) + return err } diff --git a/models/migrations/v1_24/v315.go b/models/migrations/v1_24/v315.go index aefb872d0f..22a72c31e9 100644 --- a/models/migrations/v1_24/v315.go +++ b/models/migrations/v1_24/v315.go @@ -11,6 +11,9 @@ func AddEphemeralToActionRunner(x *xorm.Engine) error { type ActionRunner struct { Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"` } - - return x.Sync(new(ActionRunner)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(ActionRunner)) + return err } diff --git a/models/migrations/v1_24/v316.go b/models/migrations/v1_24/v316.go index 0378133e53..e7f04333cc 100644 --- a/models/migrations/v1_24/v316.go +++ b/models/migrations/v1_24/v316.go @@ -16,5 +16,9 @@ func AddDescriptionForSecretsAndVariables(x *xorm.Engine) error { Description string `xorm:"TEXT"` } - return x.Sync(new(Secret), new(ActionVariable)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Secret), new(ActionVariable)) + return err } diff --git a/models/migrations/v1_24/v318.go b/models/migrations/v1_24/v318.go index 83fb0061d3..3e08c3d504 100644 --- a/models/migrations/v1_24/v318.go +++ b/models/migrations/v1_24/v318.go @@ -13,5 +13,9 @@ func AddRepoUnitAnonymousAccessMode(x *xorm.Engine) error { type RepoUnit struct { //revive:disable-line:exported AnonymousAccessMode perm.AccessMode `xorm:"NOT NULL DEFAULT 0"` } - return x.Sync(&RepoUnit{}) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(RepoUnit)) + return err } diff --git a/models/migrations/v1_24/v319.go b/models/migrations/v1_24/v319.go index 6983c38605..6571ddf75b 100644 --- a/models/migrations/v1_24/v319.go +++ b/models/migrations/v1_24/v319.go @@ -11,6 +11,9 @@ func AddExclusiveOrderColumnToLabelTable(x *xorm.Engine) error { type Label struct { ExclusiveOrder int `xorm:"DEFAULT 0"` } - - return x.Sync(new(Label)) + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, new(Label)) + return err } diff --git a/models/migrations/v1_24/v320.go b/models/migrations/v1_24/v320.go new file mode 100644 index 0000000000..1d34444826 --- /dev/null +++ b/models/migrations/v1_24/v320.go @@ -0,0 +1,57 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 //nolint + +import ( + "code.gitea.io/gitea/modules/json" + + "xorm.io/xorm" +) + +func MigrateSkipTwoFactor(x *xorm.Engine) error { + type LoginSource struct { + TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"` + } + _, err := x.SyncWithOptions( + xorm.SyncOptions{ + IgnoreConstrains: true, + IgnoreIndices: true, + }, + new(LoginSource), + ) + if err != nil { + return err + } + + type LoginSourceSimple struct { + ID int64 + Cfg string + } + + var loginSources []LoginSourceSimple + err = x.Table("login_source").Find(&loginSources) + if err != nil { + return err + } + + for _, source := range loginSources { + if source.Cfg == "" { + continue + } + + var cfg map[string]any + err = json.Unmarshal([]byte(source.Cfg), &cfg) + if err != nil { + return err + } + + if cfg["SkipLocalTwoFA"] == true { + _, err = x.Exec("UPDATE login_source SET two_factor_policy = 'skip' WHERE id = ?", source.ID) + if err != nil { + return err + } + } + } + return nil +} 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/org_test.go b/models/organization/org_test.go index 666a6c44d4..234325a8cd 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -334,7 +334,7 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) { testSuccess := func(userID int64, expectedRepoIDs []int64) { env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) assert.NoError(t, err) - repoIDs, err := env.RepoIDs(db.DefaultContext, 1, 100) + repoIDs, err := env.RepoIDs(db.DefaultContext) assert.NoError(t, err) assert.Equal(t, expectedRepoIDs, repoIDs) } @@ -342,25 +342,6 @@ func TestAccessibleReposEnv_RepoIDs(t *testing.T) { testSuccess(4, []int64{3, 32}) } -func TestAccessibleReposEnv_Repos(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) - testSuccess := func(userID int64, expectedRepoIDs []int64) { - env, err := repo_model.AccessibleReposEnv(db.DefaultContext, org, userID) - assert.NoError(t, err) - repos, err := env.Repos(db.DefaultContext, 1, 100) - assert.NoError(t, err) - expectedRepos := make(repo_model.RepositoryList, len(expectedRepoIDs)) - for i, repoID := range expectedRepoIDs { - expectedRepos[i] = unittest.AssertExistsAndLoadBean(t, - &repo_model.Repository{ID: repoID}) - } - assert.Equal(t, expectedRepos, repos) - } - testSuccess(2, []int64{3, 5, 32}) - testSuccess(4, []int64{3, 32}) -} - func TestAccessibleReposEnv_MirrorRepos(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}) 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 9d0c9f0077..45efb192c8 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -522,3 +522,7 @@ func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *u return perm.CanRead(unitType) } + +func PermissionNoAccess() Permission { + return Permission{AccessMode: perm_model.AccessModeNone} +} 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/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/commit_checker.go b/models/renderhelper/commit_checker.go index 4815643e67..407e45fb54 100644 --- a/models/renderhelper/commit_checker.go +++ b/models/renderhelper/commit_checker.go @@ -47,7 +47,7 @@ func (c *commitChecker) IsCommitIDExisting(commitID string) bool { c.gitRepo, c.gitRepoCloser = r, closer } - exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashs with gogit edition. + exist = c.gitRepo.IsReferenceExist(commitID) // Don't use IsObjectExist since it doesn't support short hashes with gogit edition. c.commitCache[commitID] = exist return exist } 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) { ./image

`, 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, "

any

\n", rendered) + }) } diff --git a/models/repo/org_repo.go b/models/repo/org_repo.go index fa519d25b1..96f21ba2ac 100644 --- a/models/repo/org_repo.go +++ b/models/repo/org_repo.go @@ -48,8 +48,7 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (Repo // accessible to a particular user type AccessibleReposEnvironment interface { CountRepos(ctx context.Context) (int64, error) - RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) - Repos(ctx context.Context, page, pageSize int) (RepositoryList, error) + RepoIDs(ctx context.Context) ([]int64, error) MirrorRepos(ctx context.Context) (RepositoryList, error) AddKeyword(keyword string) SetSort(db.SearchOrderBy) @@ -132,40 +131,18 @@ func (env *accessibleReposEnv) CountRepos(ctx context.Context) (int64, error) { return repoCount, nil } -func (env *accessibleReposEnv) RepoIDs(ctx context.Context, page, pageSize int) ([]int64, error) { - if page <= 0 { - page = 1 - } - - repoIDs := make([]int64, 0, pageSize) +func (env *accessibleReposEnv) RepoIDs(ctx context.Context) ([]int64, error) { + var repoIDs []int64 return repoIDs, db.GetEngine(ctx). Table("repository"). Join("INNER", "team_repo", "`team_repo`.repo_id=`repository`.id"). Where(env.cond()). - GroupBy("`repository`.id,`repository`."+strings.Fields(string(env.orderBy))[0]). + GroupBy("`repository`.id,`repository`." + strings.Fields(string(env.orderBy))[0]). OrderBy(string(env.orderBy)). - Limit(pageSize, (page-1)*pageSize). Cols("`repository`.id"). Find(&repoIDs) } -func (env *accessibleReposEnv) Repos(ctx context.Context, page, pageSize int) (RepositoryList, error) { - repoIDs, err := env.RepoIDs(ctx, page, pageSize) - if err != nil { - return nil, fmt.Errorf("GetUserRepositoryIDs: %w", err) - } - - repos := make([]*Repository, 0, len(repoIDs)) - if len(repoIDs) == 0 { - return repos, nil - } - - return repos, db.GetEngine(ctx). - In("`repository`.id", repoIDs). - OrderBy(string(env.orderBy)). - Find(&repos) -} - func (env *accessibleReposEnv) MirrorRepoIDs(ctx context.Context) ([]int64, error) { repoIDs := make([]int64, 0, 10) return repoIDs, db.GetEngine(ctx). 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 2977dfb9f1..5aae02c6d8 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -64,18 +64,18 @@ func (err ErrRepoIsArchived) Error() string { } type globalVarsStruct struct { - validRepoNamePattern *regexp.Regexp - invalidRepoNamePattern *regexp.Regexp - reservedRepoNames []string - reservedRepoPatterns []string + validRepoNamePattern *regexp.Regexp + invalidRepoNamePattern *regexp.Regexp + reservedRepoNames []string + reservedRepoNamePatterns []string } var globalVars = sync.OnceValue(func() *globalVarsStruct { return &globalVarsStruct{ - validRepoNamePattern: regexp.MustCompile(`[-.\w]+`), - invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`), - reservedRepoNames: []string{".", "..", "-"}, - reservedRepoPatterns: []string{"*.git", "*.wiki", "*.rss", "*.atom"}, + validRepoNamePattern: regexp.MustCompile(`^[-.\w]+$`), + invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`), + reservedRepoNames: []string{".", "..", "-"}, + reservedRepoNamePatterns: []string{"*.wiki", "*.git", "*.rss", "*.atom"}, } }) @@ -86,7 +86,16 @@ func IsUsableRepoName(name string) error { // Note: usually this error is normally caught up earlier in the UI return db.ErrNameCharsNotAllowed{Name: name} } - return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoPatterns, name) + return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns, name) +} + +// IsValidSSHAccessRepoName is like IsUsableRepoName, but it allows "*.wiki" because wiki repo needs to be accessed in SSH code +func IsValidSSHAccessRepoName(name string) bool { + vars := globalVars() + if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) { + return false + } + return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoNamePatterns[1:], name) == nil } // TrustModelType defines the types of trust model for this repository 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_test.go b/models/repo/repo_test.go index b2604ab575..66abe864fc 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -216,8 +216,23 @@ func TestIsUsableRepoName(t *testing.T) { assert.Error(t, IsUsableRepoName("-")) assert.Error(t, IsUsableRepoName("🌞")) + assert.Error(t, IsUsableRepoName("the/repo")) assert.Error(t, IsUsableRepoName("the..repo")) assert.Error(t, IsUsableRepoName("foo.wiki")) assert.Error(t, IsUsableRepoName("foo.git")) assert.Error(t, IsUsableRepoName("foo.RSS")) } + +func TestIsValidSSHAccessRepoName(t *testing.T) { + assert.True(t, IsValidSSHAccessRepoName("a")) + assert.True(t, IsValidSSHAccessRepoName("-1_.")) + assert.True(t, IsValidSSHAccessRepoName(".profile")) + assert.True(t, IsValidSSHAccessRepoName("foo.wiki")) + + assert.False(t, IsValidSSHAccessRepoName("-")) + assert.False(t, IsValidSSHAccessRepoName("🌞")) + assert.False(t, IsValidSSHAccessRepoName("the/repo")) + assert.False(t, IsValidSSHAccessRepoName("the..repo")) + assert.False(t, IsValidSSHAccessRepoName("foo.git")) + assert.False(t, IsValidSSHAccessRepoName("foo.RSS")) +} 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 b669145d68..b4a3592cbc 100644 --- a/models/repo/transfer.go +++ b/models/repo/transfer.go @@ -249,7 +249,7 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m } repo.Status = RepositoryPendingTransfer - if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil { + if err := UpdateRepositoryColsNoAutoTime(ctx, repo, "status"); err != nil { return err } diff --git a/models/repo/update.go b/models/repo/update.go index 15c8c48d5b..f82ff7c76c 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -25,7 +25,7 @@ func UpdateRepositoryOwnerNames(ctx context.Context, ownerID int64, ownerName st } defer committer.Close() - if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").Update(&Repository{ + if _, err := db.GetEngine(ctx).Where("owner_id = ?", ownerID).Cols("owner_name").NoAutoTime().Update(&Repository{ OwnerName: ownerName, }); err != nil { return err @@ -40,14 +40,20 @@ func UpdateRepositoryUpdatedTime(ctx context.Context, repoID int64, updateTime t return err } -// UpdateRepositoryCols updates repository's columns -func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string) error { +// UpdateRepositoryColsWithAutoTime updates repository's columns +func UpdateRepositoryColsWithAutoTime(ctx context.Context, repo *Repository, cols ...string) error { + if len(cols) == 0 { + return nil + } _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).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 { + if len(cols) == 0 { + return nil + } _, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).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/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 100f924cc6..86a3549345 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -828,6 +828,7 @@ func IsLastAdminUser(ctx context.Context, user *User) bool { type CountUserFilter struct { LastLoginSince *int64 IsAdmin optional.Option[bool] + IsActive optional.Option[bool] } // CountUsers returns number of users. @@ -848,6 +849,10 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 { if opts.IsAdmin.Has() { cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()}) } + + if opts.IsActive.Has() { + cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()}) + } } count, err := sess.Where(cond).Count(new(User)) @@ -1146,8 +1151,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, @@ -1161,7 +1166,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 } @@ -1171,7 +1184,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)) } @@ -1198,7 +1211,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.GetEmail()] = user + results[email.LowerEmail] = user } } } @@ -1208,9 +1221,9 @@ func GetUsersByEmails(ctx context.Context, emails []string) (map[string]*User, e return nil, err } for _, user := range users { - 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 90e8bf13a8..a2597ba3f5 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIsUsableUsername(t *testing.T) { @@ -48,14 +49,43 @@ func TestOAuth2Application_LoadUser(t *testing.T) { assert.NotNil(t, user) } -func TestGetUserEmailsByNames(t *testing.T) { +func TestUserEmails(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - - // ignore none active user email - assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"})) - assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"})) - - assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"})) + t.Run("GetUserEmailsByNames", func(t *testing.T) { + // ignore none active user email + assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user9"})) + assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "user5"})) + assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(db.DefaultContext, []string{"user8", "org7"})) + }) + t.Run("GetUsersByEmails", func(t *testing.T) { + 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) + }) + } + }) } func TestCanCreateOrganization(t *testing.T) { @@ -78,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) @@ -90,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}) } @@ -174,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) @@ -503,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, @@ -576,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/modules/actions/workflows.go b/modules/actions/workflows.go index a538b6e290..e7677edc59 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 } @@ -311,6 +314,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 +331,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 +565,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 +605,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) } 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/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/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 similarity index 55% rename from modules/structs/commit_status.go rename to modules/commitstatus/commit_status.go index dc880ef5eb..12004474ed 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 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 + +// 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/basic.go b/modules/fileicon/basic.go index 040a8e87de..9c513ccbd9 100644 --- a/modules/fileicon/basic.go +++ b/modules/fileicon/basic.go @@ -6,22 +6,26 @@ package fileicon import ( "html/template" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/util" ) -func BasicThemeIcon(entry *git.TreeEntry) template.HTML { +func BasicEntryIconName(entry *EntryInfo) string { svgName := "octicon-file" switch { - case entry.IsLink(): + case entry.EntryMode.IsLink(): svgName = "octicon-file-symlink-file" - if te, err := entry.FollowLink(); err == nil && te.IsDir() { + if entry.SymlinkToMode.IsDir() { svgName = "octicon-file-directory-symlink" } - case entry.IsDir(): - svgName = "octicon-file-directory-fill" - case entry.IsSubModule(): + case entry.EntryMode.IsDir(): + svgName = util.Iif(entry.IsOpen, "octicon-file-directory-open-fill", "octicon-file-directory-fill") + case entry.EntryMode.IsSubModule(): svgName = "octicon-file-submodule" } - return svg.RenderHTML(svgName) + return svgName +} + +func BasicEntryIconHTML(entry *EntryInfo) template.HTML { + return svg.RenderHTML(BasicEntryIconName(entry)) } diff --git a/modules/fileicon/entry.go b/modules/fileicon/entry.go new file mode 100644 index 0000000000..e4ded363e5 --- /dev/null +++ b/modules/fileicon/entry.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon + +import "code.gitea.io/gitea/modules/git" + +type EntryInfo struct { + FullName string + EntryMode git.EntryMode + SymlinkToMode git.EntryMode + IsOpen bool +} + +func EntryInfoFromGitTreeEntry(gitEntry *git.TreeEntry) *EntryInfo { + ret := &EntryInfo{FullName: gitEntry.Name(), EntryMode: gitEntry.Mode()} + if gitEntry.IsLink() { + if te, err := gitEntry.FollowLink(); err == nil && te.IsDir() { + ret.SymlinkToMode = te.Mode() + } + } + return ret +} + +func EntryInfoFolder() *EntryInfo { + return &EntryInfo{EntryMode: git.EntryModeTree} +} + +func EntryInfoFolderOpen() *EntryInfo { + return &EntryInfo{EntryMode: git.EntryModeTree, IsOpen: true} +} diff --git a/modules/fileicon/material.go b/modules/fileicon/material.go index 557f7ca9e4..449f527ee8 100644 --- a/modules/fileicon/material.go +++ b/modules/fileicon/material.go @@ -9,11 +9,12 @@ import ( "strings" "sync" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/options" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/util" ) type materialIconRulesData struct { @@ -69,41 +70,51 @@ func (m *MaterialIconProvider) renderFileIconSVG(p *RenderedIconPool, name, svg, } svgID := "svg-mfi-" + name svgCommonAttrs := `class="svg git-entry-icon ` + extraClass + `" width="16" height="16" aria-hidden="true"` + svgHTML := template.HTML(``) } -func (m *MaterialIconProvider) FileIcon(p *RenderedIconPool, entry *git.TreeEntry) template.HTML { +func (m *MaterialIconProvider) EntryIconHTML(p *RenderedIconPool, entry *EntryInfo) template.HTML { if m.rules == nil { - return BasicThemeIcon(entry) + return BasicEntryIconHTML(entry) } - if entry.IsLink() { - if te, err := entry.FollowLink(); err == nil && te.IsDir() { + if entry.EntryMode.IsLink() { + if entry.SymlinkToMode.IsDir() { // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work return svg.RenderHTML("material-folder-symlink", 16, "octicon-file-directory-symlink") } return svg.RenderHTML("octicon-file-symlink-file") // TODO: find some better icons for them } - name := m.findIconNameByGit(entry) - // the material icon pack's "folder" icon doesn't look good, so use our built-in one - // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work - if iconSVG, ok := m.svgs[name]; ok && name != "folder" && iconSVG != "" { - // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work - extraClass := "octicon-file" - switch { - case entry.IsDir(): - extraClass = "octicon-file-directory-fill" - case entry.IsSubModule(): - extraClass = "octicon-file-submodule" + name := m.FindIconName(entry) + iconSVG := m.svgs[name] + if iconSVG == "" { + name = "file" + if entry.EntryMode.IsDir() { + name = util.Iif(entry.IsOpen, "folder-open", "folder") + } + iconSVG = m.svgs[name] + if iconSVG == "" { + setting.PanicInDevOrTesting("missing file icon for %s", name) } - return m.renderFileIconSVG(p, name, iconSVG, extraClass) } - // TODO: use an interface or wrapper for git.Entry to make the code testable. - return BasicThemeIcon(entry) + + // keep the old "octicon-xxx" class name to make some "theme plugin selector" could still work + extraClass := "octicon-file" + switch { + case entry.EntryMode.IsDir(): + extraClass = BasicEntryIconName(entry) + case entry.EntryMode.IsSubModule(): + extraClass = "octicon-file-submodule" + } + return m.renderFileIconSVG(p, name, iconSVG, extraClass) } func (m *MaterialIconProvider) findIconNameWithLangID(s string) string { @@ -118,13 +129,17 @@ func (m *MaterialIconProvider) findIconNameWithLangID(s string) string { return "" } -func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string { - fileNameLower := strings.ToLower(path.Base(name)) - if isDir { +func (m *MaterialIconProvider) FindIconName(entry *EntryInfo) string { + if entry.EntryMode.IsSubModule() { + return "folder-git" + } + + fileNameLower := strings.ToLower(path.Base(entry.FullName)) + if entry.EntryMode.IsDir() { if s, ok := m.rules.FolderNames[fileNameLower]; ok { return s } - return "folder" + return util.Iif(entry.IsOpen, "folder-open", "folder") } if s, ok := m.rules.FileNames[fileNameLower]; ok { @@ -146,10 +161,3 @@ func (m *MaterialIconProvider) FindIconName(name string, isDir bool) string { return "file" } - -func (m *MaterialIconProvider) findIconNameByGit(entry *git.TreeEntry) string { - if entry.IsSubModule() { - return "folder-git" - } - return m.FindIconName(entry.Name(), entry.IsDir()) -} diff --git a/modules/fileicon/material_test.go b/modules/fileicon/material_test.go index f36385aaf3..68353d2189 100644 --- a/modules/fileicon/material_test.go +++ b/modules/fileicon/material_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/fileicon" + "code.gitea.io/gitea/modules/git" "github.com/stretchr/testify/assert" ) @@ -19,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("foo.php", false)) - assert.Equal(t, "php", p.FindIconName("foo.PHP", false)) - assert.Equal(t, "javascript", p.FindIconName("foo.js", false)) - assert.Equal(t, "visualstudio", p.FindIconName("foo.vba", false)) + 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})) } diff --git a/modules/fileicon/render.go b/modules/fileicon/render.go index 1d014693fd..8ed86b9ac0 100644 --- a/modules/fileicon/render.go +++ b/modules/fileicon/render.go @@ -7,7 +7,6 @@ import ( "html/template" "strings" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" ) @@ -34,19 +33,9 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML { return template.HTML(sb.String()) } -// TODO: use an interface or struct to replace "*git.TreeEntry", to decouple the fileicon module from git module - -func RenderEntryIcon(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML { +func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML { if setting.UI.FileIconTheme == "material" { - return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry) + return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry) } - return BasicThemeIcon(entry) -} - -func RenderEntryIconOpen(renderedIconPool *RenderedIconPool, entry *git.TreeEntry) template.HTML { - // TODO: add "open icon" support - if setting.UI.FileIconTheme == "material" { - return DefaultMaterialIconProvider().FileIcon(renderedIconPool, entry) - } - return BasicThemeIcon(entry) + return BasicEntryIconHTML(entry) } diff --git a/modules/git/attribute/checker.go b/modules/git/attribute/checker.go index c17006a154..167b31416e 100644 --- a/modules/git/attribute/checker.go +++ b/modules/git/attribute/checker.go @@ -39,7 +39,12 @@ func checkAttrCommand(gitRepo *git.Repository, treeish string, filenames, attrib ) cancel = deleteTemporaryFile } - } // else: no treeish, assume it is a not a bare repo, read from working directory + } else { + // Read from existing index, in cases where the repo is bare and has an index, + // or the work tree contains unstaged changes that shouldn't affect the attribute check. + // It is caller's responsibility to add changed ".gitattributes" into the index if they want to respect the new changes. + cmd.AddArguments("--cached") + } cmd.AddDynamicArguments(attributes...) if len(filenames) > 0 { diff --git a/modules/git/attribute/checker_test.go b/modules/git/attribute/checker_test.go index 97db43460b..67fbda8918 100644 --- a/modules/git/attribute/checker_test.go +++ b/modules/git/attribute/checker_test.go @@ -57,8 +57,18 @@ func Test_Checker(t *testing.T) { assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) }) + t.Run("Run git check-attr in bare repository using index", func(t *testing.T) { + attrs, err := CheckAttributes(t.Context(), gitRepo, "", CheckAttributeOpts{ + Filenames: []string{"i-am-a-python.p"}, + Attributes: LinguistAttributes, + }) + assert.NoError(t, err) + assert.Len(t, attrs, 1) + assert.Equal(t, expectedAttrs(), attrs["i-am-a-python.p"]) + }) + if !git.DefaultFeatures().SupportCheckAttrOnBare { - t.Skip("git version 2.40 is required to support run check-attr on bare repo") + t.Skip("git version 2.40 is required to support run check-attr on bare repo without using index") return } 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/cmdverb.go b/modules/git/cmdverb.go new file mode 100644 index 0000000000..3d6f4ae0c6 --- /dev/null +++ b/modules/git/cmdverb.go @@ -0,0 +1,36 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +const ( + CmdVerbUploadPack = "git-upload-pack" + CmdVerbUploadArchive = "git-upload-archive" + CmdVerbReceivePack = "git-receive-pack" + CmdVerbLfsAuthenticate = "git-lfs-authenticate" + CmdVerbLfsTransfer = "git-lfs-transfer" + + CmdSubVerbLfsUpload = "upload" + CmdSubVerbLfsDownload = "download" +) + +func IsAllowedVerbForServe(verb string) bool { + switch verb { + case CmdVerbUploadPack, + CmdVerbUploadArchive, + CmdVerbReceivePack, + CmdVerbLfsAuthenticate, + CmdVerbLfsTransfer: + return true + } + return false +} + +func IsAllowedVerbForServeLfs(verb string) bool { + switch verb { + case CmdVerbLfsAuthenticate, + CmdVerbLfsTransfer: + return true + } + return false +} 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 3e790e89d9..1c1648eb8b 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -34,7 +34,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. @@ -166,6 +166,8 @@ type CommitsCountOptions struct { Not string Revision []string RelPath []string + Since string + Until string } // CommitsCount returns number of total commits of until given revision. @@ -199,8 +201,8 @@ func (c *Commit) CommitsCount() (int64, error) { } // CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize -func (c *Commit) CommitsByRange(page, pageSize int, not string) ([]*Commit, error) { - return c.repo.commitsByRange(c.ID, page, pageSize, not) +func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) { + return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until) } // CommitsBefore returns all the commits before current revision @@ -275,8 +277,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 { + if len(commitID) == 0 { + continue + } + c, ok := commitsMap[commitID] if ok { commitCommits[path] = c continue } - if len(commitID) == 0 { - continue - } - - _, err := batchStdinWriter.Write([]byte(commitID + "\n")) + c, err := commit.repo.GetCommit(commitID) // Ensure the commit exists in the repository if err != nil { return nil, err } - _, typ, size, err := ReadBatchLine(batchReader) - 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) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("unable to read commit %q: %w", objectID.String(), err) + } + if len(line) == 0 { + break + } + + 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) + } } - _, _ = payloadSB.Write(line) - break readLoop + headerKey = string(k) // it also resets the headerValue to empty string if not inHeader + headerValue = v + } else { + headerValue = append(headerValue, v...) } - return nil, err - } - if pgpsig { - if len(line) > 0 && line[0] == ' ' { - _, _ = signatureSB.Write(line[1:]) - continue - } - pgpsig = false - } - - if !message { - // This is probably not correct but is copied from go-gits interpretation... - trimmed := bytes.TrimSpace(line) - if len(trimmed) == 0 { - message = true + if headerKey != commitHeaderGpgsig && headerKey != commitHeaderGpgsigSha256 { _, _ = payloadSB.Write(line) - continue - } - - split := bytes.SplitN(trimmed, []byte{' '}, 2) - var data []byte - if len(split) > 1 { - data = split[1] - } - - switch string(split[0]) { - case "tree": - commit.Tree = *NewTree(gitRepo, MustIDFromString(string(data))) - _, _ = 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 - } + commit.CommitMessage = messageSB.String() + 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 1698676906 +0100 committer Adam Majer 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 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 1563741793 +0200 committer silverwind 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 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 1711702962 +0100 committer KN4CK3R 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 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/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 72f35711f0..4066a1ca7b 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -89,7 +89,8 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { return commits[0], nil } -func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not string) ([]*Commit, error) { +// commitsByRangeWithTime returns the specific page commits before current revision, with not, since, until support +func (repo *Repository) commitsByRangeWithTime(id ObjectID, page, pageSize int, not, since, until string) ([]*Commit, error) { cmd := NewCommand("log"). AddOptionFormat("--skip=%d", (page-1)*pageSize). AddOptionFormat("--max-count=%d", pageSize). @@ -99,6 +100,12 @@ func (repo *Repository) commitsByRange(id ObjectID, page, pageSize int, not stri if not != "" { cmd.AddOptionValues("--not", not) } + if since != "" { + cmd.AddOptionFormat("--since=%s", since) + } + if until != "" { + cmd.AddOptionFormat("--until=%s", until) + } stdout, _, err := cmd.RunStdBytes(repo.Ctx, &RunOpts{Dir: repo.Path}) if err != nil { @@ -212,6 +219,8 @@ type CommitsByFileAndRangeOptions struct { File string Not string Page int + Since string + Until string } // CommitsByFileAndRange return the commits according revision file and the page @@ -231,6 +240,12 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) if opts.Not != "" { gitCmd.AddOptionValues("--not", opts.Not) } + if opts.Since != "" { + gitCmd.AddOptionFormat("--since=%s", opts.Since) + } + if opts.Until != "" { + gitCmd.AddOptionFormat("--until=%s", opts.Until) + } gitCmd.AddDashesAndList(opts.File) err := gitCmd.Run(repo.Ctx, &RunOpts{ @@ -532,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_stats.go b/modules/git/repo_stats.go index 76fe92bb34..8c6f31c38c 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -40,7 +40,9 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) since := fromTime.Format(time.RFC3339) - stdout, _, runErr := NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso").AddOptionFormat("--since='%s'", since).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) + stdout, _, runErr := NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso"). + AddOptionFormat("--since=%s", since). + RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) if runErr != nil { return nil, runErr } @@ -60,7 +62,8 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) _ = stdoutWriter.Close() }() - gitCmd := NewCommand("log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso").AddOptionFormat("--since='%s'", since) + gitCmd := NewCommand("log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso"). + AddOptionFormat("--since=%s", since) if len(branch) == 0 { gitCmd.AddArguments("--branches=*") } else { 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_entry.go b/modules/git/tree_entry.go index a2e1579290..57856d90ee 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -78,7 +78,7 @@ func (te *TreeEntry) FollowLinks(optLimit ...int) (*TreeEntry, error) { } limit := util.OptionalArg(optLimit, 10) entry := te - for i := 0; i < limit; i++ { + for range limit { if !entry.IsLink() { break } diff --git a/modules/git/tree_entry_mode.go b/modules/git/tree_entry_mode.go index 1193bec4f1..d815a8bc2e 100644 --- a/modules/git/tree_entry_mode.go +++ b/modules/git/tree_entry_mode.go @@ -30,6 +30,31 @@ func (e EntryMode) String() string { return strconv.FormatInt(int64(e), 8) } +// IsSubModule if the entry is a sub module +func (e EntryMode) IsSubModule() bool { + return e == EntryModeCommit +} + +// IsDir if the entry is a sub dir +func (e EntryMode) IsDir() bool { + return e == EntryModeTree +} + +// IsLink if the entry is a symlink +func (e EntryMode) IsLink() bool { + return e == EntryModeSymlink +} + +// IsRegular if the entry is a regular file +func (e EntryMode) IsRegular() bool { + return e == EntryModeBlob +} + +// IsExecutable if the entry is an executable file (not necessarily binary) +func (e EntryMode) IsExecutable() bool { + return e == EntryModeExec +} + func ParseEntryMode(mode string) (EntryMode, error) { switch mode { case "000000": diff --git a/modules/git/tree_entry_nogogit.go b/modules/git/tree_entry_nogogit.go index 81fb638d56..0c0e1835f1 100644 --- a/modules/git/tree_entry_nogogit.go +++ b/modules/git/tree_entry_nogogit.go @@ -59,27 +59,27 @@ func (te *TreeEntry) Size() int64 { // IsSubModule if the entry is a sub module func (te *TreeEntry) IsSubModule() bool { - return te.entryMode == EntryModeCommit + return te.entryMode.IsSubModule() } // IsDir if the entry is a sub dir func (te *TreeEntry) IsDir() bool { - return te.entryMode == EntryModeTree + return te.entryMode.IsDir() } // IsLink if the entry is a symlink func (te *TreeEntry) IsLink() bool { - return te.entryMode == EntryModeSymlink + return te.entryMode.IsLink() } // IsRegular if the entry is a regular file func (te *TreeEntry) IsRegular() bool { - return te.entryMode == EntryModeBlob + return te.entryMode.IsRegular() } // IsExecutable if the entry is an executable file (not necessarily binary) func (te *TreeEntry) IsExecutable() bool { - return te.entryMode == EntryModeExec + return te.entryMode.IsExecutable() } // Blob returns the blob object the entry 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/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/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/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/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/markup/common/footnote.go b/modules/markup/common/footnote.go index 9a4f18ed7f..26ab60bc1e 100644 --- a/modules/markup/common/footnote.go +++ b/modules/markup/common/footnote.go @@ -409,9 +409,9 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt _, _ = w.Write(n.Name) _, _ = w.WriteString(`">`) + _, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) // FIXME: here and below, need to keep the classes _, _ = w.WriteString(is) - _, _ = w.WriteString(``) + _, _ = w.WriteString(` `) // 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/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: "" strings.NewReader(""), - // 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(""), @@ -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 ``, 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) { `, ) }) + + t.Run("IssueFootnote", func(t *testing.T) { + test( + "foo[^1][^2]\n\n[^1]: bar\n[^2]: baz", + `

foo1 2

+
+
+
    +
  1. +

    bar ↩︎

    +
  2. +
  3. +

    baz ↩︎

    +
  4. +
+
`, + ) + }) } 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..f67437465c 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" { 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>`) test("", `<style>a</STYLE>`) + + // other special tags, our special behavior + test("This is another definition of the second term.

Footnotes

-

Here is a simple footnote,1 and here is a longer one.2

+

Here is a simple footnote,1 and here is a longer one.2


    @@ -252,7 +252,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..427ed842ec 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))) } 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_heading.go b/modules/markup/markdown/transform_heading.go index 5f8a12794d..cac3cd6617 100644 --- a/modules/markup/markdown/transform_heading.go +++ b/modules/markup/markdown/transform_heading.go @@ -16,7 +16,7 @@ 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 diff --git a/modules/metrics/collector.go b/modules/metrics/collector.go index 230260ff94..4d2ec287a9 100755 --- a/modules/metrics/collector.go +++ b/modules/metrics/collector.go @@ -184,7 +184,7 @@ func NewCollector() Collector { Users: prometheus.NewDesc( namespace+"users", "Number of Users", - nil, nil, + []string{"state"}, nil, ), Watches: prometheus.NewDesc( namespace+"watches", @@ -373,7 +373,14 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) { ch <- prometheus.MustNewConstMetric( c.Users, prometheus.GaugeValue, - float64(stats.Counter.User), + float64(stats.Counter.UsersActive), + "active", // state label + ) + ch <- prometheus.MustNewConstMetric( + c.Users, + prometheus.GaugeValue, + float64(stats.Counter.UsersNotActive), + "inactive", // state label ) ch <- prometheus.MustNewConstMetric( c.Watches, 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/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 similarity index 100% rename from modules/options/dynamic.go rename to 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 similarity index 65% rename from models/packages/container/const.go rename to 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..2fce7d976a 100644 --- a/modules/packages/container/metadata.go +++ b/modules/packages/container/metadata.go @@ -4,6 +4,7 @@ package container import ( + "errors" "fmt" "io" "strings" @@ -83,7 +84,8 @@ func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) { func parseOCIImageConfig(r io.Reader) (*Metadata, error) { var image oci.Image - if err := json.NewDecoder(r).Decode(&image); err != nil { + // EOF means empty input, still use the default data + if err := json.NewDecoder(r).Decode(&image); err != nil && !errors.Is(err, io.EOF) { return nil, err } diff --git a/modules/packages/container/metadata_test.go b/modules/packages/container/metadata_test.go index 665499b2e6..74b0a379c6 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) { @@ -59,3 +60,9 @@ func TestParseImageConfig(t *testing.T) { assert.Equal(t, projectURL, metadata.ProjectURL) assert.Equal(t, repositoryURL, metadata.RepositoryURL) } + +func TestParseOCIImageConfig(t *testing.T) { + metadata, err := parseOCIImageConfig(strings.NewReader("")) + require.NoError(t, err) + assert.Equal(t, &Metadata{Type: TypeOCI, Platform: DefaultPlatform, ImageLayers: []string{}}, 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 = ` - - ` + id + ` - ` + semver + ` - ` + authors + ` - true - ` + projectURL + ` - ` + description + ` - ` + releaseNotes + ` - - README.md - - - - - - + + ` + authors + ` + ` + copyright + ` + ` + description + ` + true + ` + iconURL + ` + ` + id + ` + ` + language + ` + ` + licenseURL + ` + ` + owners + ` + ` + projectURL + ` + README.md + ` + releaseNotes + ` + + true + ` + tags + ` + ` + title + ` + ` + versionStr + ` + + + + + + ` const symbolsNuspecContent = ` ` + id + ` - ` + semver + ` + ` + versionStr + ` ` + description + ` @@ -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/private/serv.go b/modules/private/serv.go index 10e9f7995c..b1dafbd81b 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -46,18 +46,16 @@ type ServCommandResults struct { } // ServCommand preps for a serv call -func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, ResponseExtra) { +func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verb, lfsVerb string) (*ServCommandResults, ResponseExtra) { reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", keyID, url.PathEscape(ownerName), url.PathEscape(repoName), mode, ) - for _, verb := range verbs { - if verb != "" { - reqURL += "&verb=" + url.QueryEscape(verb) - } - } + reqURL += "&verb=" + url.QueryEscape(verb) + // reqURL += "&lfs_verb=" + url.QueryEscape(lfsVerb) // TODO: actually there is no use of this parameter. In the future, the URL construction should be more flexible + _ = lfsVerb req := newInternalRequestAPI(ctx, reqURL, "GET") return requestJSONResp(req, &ServCommandResults{}) } 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 similarity index 100% rename from modules/public/serve_dynamic.go rename to 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/commits_test.go b/modules/repository/commits_test.go index 6e407015c2..030cd7714d 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -200,5 +200,3 @@ func TestListToPushCommits(t *testing.T) { assert.Equal(t, now, pushCommits.Commits[1].Timestamp) } } - -// TODO TestPushUpdate 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/session/key.go b/modules/session/key.go new file mode 100644 index 0000000000..c3da997c67 --- /dev/null +++ b/modules/session/key.go @@ -0,0 +1,11 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package session + +const ( + KeyUID = "uid" + KeyUname = "uname" + + KeyUserHasTwoFactorAuth = "userHasTwoFactorAuth" +) 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 2f798b75c7..3ae4c005c7 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -39,6 +39,7 @@ var ( CSRFCookieName = "_csrf" CSRFCookieHTTPOnly = true RecordUserSignupMetadata = false + TwoFactorAuthEnforced = false ) // loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set @@ -142,6 +143,15 @@ func loadSecurityFrom(rootCfg ConfigProvider) { PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) + twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String() + switch twoFactorAuth { + case "": + case "enforced": + TwoFactorAuthEnforced = true + default: + log.Fatal("Invalid two-factor auth option: %s", twoFactorAuth) + } + InternalToken = loadSecret(sec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN") if InstallLock && InternalToken == "" { // if Gitea has been installed but the InternalToken hasn't been generated (upgrade from an old release), we should generate 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..f43af1a8c0 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 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/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/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/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 fb784bd8b3..abc8076387 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -101,6 +101,8 @@ type Repository struct { AllowSquash bool `json:"allow_squash_merge"` AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"` AllowRebaseUpdate bool `json:"allow_rebase_update"` + AllowManualMerge bool `json:"allow_manual_merge"` + AutodetectManualMerge bool `json:"autodetect_manual_merge"` DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"` DefaultMergeStyle string `json:"default_merge_style"` DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"` @@ -111,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"` @@ -357,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..c501470a37 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 @@ -104,9 +104,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 +129,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_tag.go b/modules/structs/repo_tag.go index 5722513f4f..bb8bfd10cb 100644 --- a/modules/structs/repo_tag.go +++ b/modules/structs/repo_tag.go @@ -11,8 +11,8 @@ type Tag struct { Message string `json:"message"` ID string `json:"id"` Commit *CommitMeta `json:"commit"` - ZipballURL string `json:"zipball_url"` - TarballURL string `json:"tarball_url"` + ZipballURL string `json:"zipball_url,omitempty"` + TarballURL string `json:"tarball_url,omitempty"` } // AnnotatedTag represents an annotated tag 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..7338e45739 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -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_app.go b/modules/structs/user_app.go index a7d2e28b41..15811ceb66 100644 --- a/modules/structs/user_app.go +++ b/modules/structs/user_app.go @@ -11,11 +11,13 @@ import ( // AccessToken represents an API access token. // swagger:response AccessToken type AccessToken struct { - ID int64 `json:"id"` - Name string `json:"name"` - Token string `json:"sha1"` - TokenLastEight string `json:"token_last_eight"` - Scopes []string `json:"scopes"` + ID int64 `json:"id"` + Name string `json:"name"` + Token string `json:"sha1"` + TokenLastEight string `json:"token_last_eight"` + Scopes []string `json:"scopes"` + Created time.Time `json:"created_at"` + Updated time.Time `json:"last_used_at"` } // AccessTokenList represents a list of API access token. @@ -23,9 +25,11 @@ type AccessToken struct { type AccessTokenList []*AccessToken // CreateAccessTokenOption options when create access token +// swagger:model CreateAccessTokenOption type CreateAccessTokenOption struct { // required: true - Name string `json:"name" binding:"Required"` + Name string `json:"name" binding:"Required"` + // example: ["all", "read:activitypub","read:issue", "write:misc", "read:notification", "read:organization", "read:package", "read:repository", "read:user"] Scopes []string `json:"scopes"` } 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 08eed59a89..16225a852a 100644 --- a/modules/structs/user_key.go +++ b/modules/structs/user_key.go @@ -15,7 +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"` + 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 c9d93e089c..d55d4f87c5 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -162,22 +162,6 @@ func NewFuncMap() template.FuncMap { "FilenameIsImage": filenameIsImage, "TabSizeClass": tabSizeClass, - - // for backward compatibility only, do not use them anymore - "TimeSince": timeSinceLegacy, - "TimeSinceUnix": timeSinceLegacy, - "DateTime": dateTimeLegacy, - - "RenderEmoji": renderEmojiLegacy, - "RenderLabel": renderLabelLegacy, - "RenderLabels": renderLabelsLegacy, - "RenderIssueTitle": renderIssueTitleLegacy, - - "RenderMarkdownToHtml": renderMarkdownToHtmlLegacy, - - "RenderCommitMessage": renderCommitMessageLegacy, - "RenderCommitMessageLinkSubject": renderCommitMessageLinkSubjectLegacy, - "RenderCommitBody": renderCommitBodyLegacy, } } @@ -367,7 +351,3 @@ func QueryBuild(a ...any) template.URL { } return template.URL(s) } - -func panicIfDevOrTesting() { - setting.PanicInDevOrTesting("legacy template functions are for backward compatibility only, do not use them in new code") -} diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index 529284f7e8..f51936354e 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -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..0d84f8598b 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 { @@ -159,9 +158,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 +217,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 similarity index 100% rename from modules/templates/dynamic.go rename to modules/templates/templates_dynamic.go diff --git a/modules/templates/util_date_legacy.go b/modules/templates/util_date_legacy.go deleted file mode 100644 index ceefb00447..0000000000 --- a/modules/templates/util_date_legacy.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package templates - -import ( - "html/template" - - "code.gitea.io/gitea/modules/translation" -) - -func dateTimeLegacy(format string, datetime any, _ ...string) template.HTML { - panicIfDevOrTesting() - if s, ok := datetime.(string); ok { - datetime = parseLegacy(s) - } - return dateTimeFormat(format, datetime) -} - -func timeSinceLegacy(time any, _ translation.Locale) template.HTML { - panicIfDevOrTesting() - return TimeSince(time) -} diff --git a/modules/templates/util_date_test.go b/modules/templates/util_date_test.go index f3a2409a9f..2c1f2d242e 100644 --- a/modules/templates/util_date_test.go +++ b/modules/templates/util_date_test.go @@ -17,12 +17,12 @@ import ( func TestDateTime(t *testing.T) { testTz, _ := time.LoadLocation("America/New_York") defer test.MockVariableValue(&setting.DefaultUILocation, testTz)() + defer test.MockVariableValue(&setting.IsProd, true)() defer test.MockVariableValue(&setting.IsInTesting, false)() du := NewDateUtils() refTimeStr := "2018-01-01T00:00:00Z" - refDateStr := "2018-01-01" refTime, _ := time.Parse(time.RFC3339, refTimeStr) refTimeStamp := timeutil.TimeStamp(refTime.Unix()) @@ -31,18 +31,9 @@ func TestDateTime(t *testing.T) { assert.EqualValues(t, "-", du.AbsoluteShort(time.Time{})) assert.EqualValues(t, "-", du.AbsoluteShort(timeutil.TimeStamp(0))) - actual := dateTimeLegacy("short", "invalid") - assert.EqualValues(t, `-`, actual) - - actual = dateTimeLegacy("short", refTimeStr) + actual := du.AbsoluteShort(refTime) assert.EqualValues(t, `2018-01-01`, actual) - actual = du.AbsoluteShort(refTime) - assert.EqualValues(t, `2018-01-01`, actual) - - actual = dateTimeLegacy("short", refDateStr) - assert.EqualValues(t, `2018-01-01`, actual) - actual = du.AbsoluteShort(refTimeStamp) assert.EqualValues(t, `2017-12-31`, actual) @@ -53,6 +44,7 @@ func TestDateTime(t *testing.T) { func TestTimeSince(t *testing.T) { testTz, _ := time.LoadLocation("America/New_York") defer test.MockVariableValue(&setting.DefaultUILocation, testTz)() + defer test.MockVariableValue(&setting.IsProd, true)() defer test.MockVariableValue(&setting.IsInTesting, false)() du := NewDateUtils() @@ -67,6 +59,6 @@ func TestTimeSince(t *testing.T) { actual = timeSinceTo(&refTime, time.Time{}) assert.EqualValues(t, `2018-01-01 00:00:00 +00:00`, actual) - actual = timeSinceLegacy(timeutil.TimeStampNano(refTime.UnixNano()), nil) + actual = du.TimeSince(timeutil.TimeStampNano(refTime.UnixNano())) assert.EqualValues(t, `2017-12-31 19:00:00 -05:00`, actual) } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index 521233db40..14655a53c3 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -14,6 +14,8 @@ import ( "unicode" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/renderhelper" + "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" @@ -34,25 +36,25 @@ func NewRenderUtils(ctx reqctx.RequestContext) *RenderUtils { } // RenderCommitMessage renders commit message with XSS-safe and special links. -func (ut *RenderUtils) RenderCommitMessage(msg string, metas map[string]string) template.HTML { +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. - fullMessage, err := markup.PostProcessCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), cleanMsg) + // we can safely assume that it will not return any error, since there shouldn't be any special HTML. + // "repo" can be nil when rendering commit messages for deleted repositories in a user's dashboard feed. + fullMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), cleanMsg) if err != nil { log.Error("PostProcessCommitMessage: %v", err) return "" } msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n") if len(msgLines) == 0 { - return template.HTML("") + return "" } return renderCodeBlock(template.HTML(msgLines[0])) } // RenderCommitMessageLinkSubject renders commit message as a XSS-safe link to // the provided default url, handling for special links without email to links. -func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, metas map[string]string) template.HTML { +func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, repo *repo.Repository) template.HTML { msgLine := strings.TrimLeftFunc(msg, unicode.IsSpace) lineEnd := strings.IndexByte(msgLine, '\n') if lineEnd > 0 { @@ -63,9 +65,8 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me return "" } - // we can safely assume that it will not return any error, since there - // shouldn't be any special HTML. - renderedMessage, err := markup.PostProcessCommitMessageSubject(markup.NewRenderContext(ut.ctx).WithMetas(metas), urlDefault, template.HTMLEscapeString(msgLine)) + // we can safely assume that it will not return any error, since there shouldn't be any special HTML. + renderedMessage, err := markup.PostProcessCommitMessageSubject(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), urlDefault, template.HTMLEscapeString(msgLine)) if err != nil { log.Error("PostProcessCommitMessageSubject: %v", err) return "" @@ -74,7 +75,7 @@ func (ut *RenderUtils) RenderCommitMessageLinkSubject(msg, urlDefault string, me } // RenderCommitBody extracts the body of a commit message without its title. -func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) template.HTML { +func (ut *RenderUtils) RenderCommitBody(msg string, repo *repo.Repository) template.HTML { msgLine := strings.TrimSpace(msg) lineEnd := strings.IndexByte(msgLine, '\n') if lineEnd > 0 { @@ -87,7 +88,7 @@ func (ut *RenderUtils) RenderCommitBody(msg string, metas map[string]string) tem return "" } - renderedMessage, err := markup.PostProcessCommitMessage(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(msgLine)) + renderedMessage, err := markup.PostProcessCommitMessage(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(msgLine)) if err != nil { log.Error("PostProcessCommitMessage: %v", err) return "" @@ -105,8 +106,8 @@ func renderCodeBlock(htmlEscapedTextToRender template.HTML) template.HTML { } // RenderIssueTitle renders issue/pull title with defined post processors -func (ut *RenderUtils) RenderIssueTitle(text string, metas map[string]string) template.HTML { - renderedText, err := markup.PostProcessIssueTitle(markup.NewRenderContext(ut.ctx).WithMetas(metas), template.HTMLEscapeString(text)) +func (ut *RenderUtils) RenderIssueTitle(text string, repo *repo.Repository) template.HTML { + renderedText, err := markup.PostProcessIssueTitle(renderhelper.NewRenderContextRepoComment(ut.ctx, repo), template.HTMLEscapeString(text)) if err != nil { log.Error("PostProcessIssueTitle: %v", err) return "" diff --git a/modules/templates/util_render_legacy.go b/modules/templates/util_render_legacy.go deleted file mode 100644 index 8f7b84c83d..0000000000 --- a/modules/templates/util_render_legacy.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package templates - -import ( - "context" - "html/template" - - issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/modules/reqctx" - "code.gitea.io/gitea/modules/translation" -) - -func renderEmojiLegacy(ctx context.Context, text string) template.HTML { - panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderEmoji(text) -} - -func renderLabelLegacy(ctx context.Context, locale translation.Locale, label *issues_model.Label) template.HTML { - panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderLabel(label) -} - -func renderLabelsLegacy(ctx context.Context, locale translation.Locale, labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML { - panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderLabels(labels, repoLink, issue) -} - -func renderMarkdownToHtmlLegacy(ctx context.Context, input string) template.HTML { //nolint:revive - panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).MarkdownToHtml(input) -} - -func renderCommitMessageLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML { - panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessage(msg, metas) -} - -func renderCommitMessageLinkSubjectLegacy(ctx context.Context, msg, urlDefault string, metas map[string]string) template.HTML { - panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitMessageLinkSubject(msg, urlDefault, metas) -} - -func renderIssueTitleLegacy(ctx context.Context, text string, metas map[string]string) template.HTML { - panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderIssueTitle(text, metas) -} - -func renderCommitBodyLegacy(ctx context.Context, msg string, metas map[string]string) template.HTML { - panicIfDevOrTesting() - return NewRenderUtils(reqctx.FromContext(ctx)).RenderCommitBody(msg, metas) -} diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 460b9dc190..9b51d0cd57 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -11,11 +11,11 @@ import ( "testing" "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/reqctx" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" @@ -47,19 +47,8 @@ mail@domain.com return strings.ReplaceAll(s, "", " ") } -var testMetas = map[string]string{ - "user": "user13", - "repo": "repo11", - "repoPath": "../../tests/gitea-repositories-meta/user13/repo11.git/", - "markdownNewLineHardBreak": "true", - "markupAllowShortIssuePattern": "true", -} - func TestMain(m *testing.M) { - unittest.InitSettingsForTesting() - if err := git.InitSimple(context.Background()); err != nil { - log.Fatal("git init failed, err: %v", err) - } + setting.Markdown.RenderOptionsComment.ShortIssuePattern = true markup.Init(&markup.RenderHelperFuncs{ IsUsernameMentionable: func(ctx context.Context, username string) bool { return username == "mention-user" @@ -74,46 +63,52 @@ func newTestRenderUtils(t *testing.T) *RenderUtils { return NewRenderUtils(ctx) } -func TestRenderCommitBody(t *testing.T) { - defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() - type args struct { - msg string +func TestRenderRepoComment(t *testing.T) { + mockRepo := &repo.Repository{ + ID: 1, OwnerName: "user13", Name: "repo11", + Owner: &user_model.User{ID: 13, Name: "user13"}, + Units: []*repo.RepoUnit{}, } - tests := []struct { - name string - args args - want template.HTML - }{ - { - name: "multiple lines", - args: args{ - msg: "first line\nsecond line", + t.Run("RenderCommitBody", func(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + type args struct { + msg string + } + tests := []struct { + name string + args args + want template.HTML + }{ + { + name: "multiple lines", + args: args{ + msg: "first line\nsecond line", + }, + want: "second line", }, - want: "second line", - }, - { - name: "multiple lines with leading newlines", - args: args{ - msg: "\n\n\n\nfirst line\nsecond line", + { + name: "multiple lines with leading newlines", + args: args{ + msg: "\n\n\n\nfirst line\nsecond line", + }, + want: "second line", }, - want: "second line", - }, - { - name: "multiple lines with trailing newlines", - args: args{ - msg: "first line\nsecond line\n\n\n", + { + name: "multiple lines with trailing newlines", + args: args{ + msg: "first line\nsecond line\n\n\n", + }, + want: "second line", }, - want: "second line", - }, - } - ut := newTestRenderUtils(t) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, nil), "RenderCommitBody(%v, %v)", tt.args.msg, nil) - }) - } + } + ut := newTestRenderUtils(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, ut.RenderCommitBody(tt.args.msg, mockRepo), "RenderCommitBody(%v, %v)", tt.args.msg, nil) + }) + } - expected := `/just/a/path.bin + expected := `/just/a/path.bin https://example.com/file.bin [local link](file.bin) [remote link](https://example.com) @@ -132,22 +127,22 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit @mention-user test #123 space` - assert.Equal(t, expected, string(newTestRenderUtils(t).RenderCommitBody(testInput(), testMetas))) -} + assert.Equal(t, expected, string(newTestRenderUtils(t).RenderCommitBody(testInput(), mockRepo))) + }) -func TestRenderCommitMessage(t *testing.T) { - expected := `space @mention-user ` - assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), testMetas)) -} + t.Run("RenderCommitMessage", func(t *testing.T) { + expected := `space @mention-user ` + assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessage(testInput(), mockRepo)) + }) -func TestRenderCommitMessageLinkSubject(t *testing.T) { - expected := `space @mention-user` - assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", testMetas)) -} + t.Run("RenderCommitMessageLinkSubject", func(t *testing.T) { + expected := `space @mention-user` + assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", mockRepo)) + }) -func TestRenderIssueTitle(t *testing.T) { - defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() - expected := ` space @mention-user + t.Run("RenderIssueTitle", func(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + expected := ` space @mention-user /just/a/path.bin https://example.com/file.bin [local link](file.bin) @@ -168,8 +163,9 @@ mail@domain.com #123 space ` - expected = strings.ReplaceAll(expected, "", " ") - assert.Equal(t, expected, string(newTestRenderUtils(t).RenderIssueTitle(testInput(), testMetas))) + expected = strings.ReplaceAll(expected, "", " ") + assert.Equal(t, expected, string(newTestRenderUtils(t).RenderIssueTitle(testInput(), mockRepo))) + }) } func TestRenderMarkdownToHtml(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, `
    `) } 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/util/map.go b/modules/util/map.go new file mode 100644 index 0000000000..f307faad1f --- /dev/null +++ b/modules/util/map.go @@ -0,0 +1,13 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +func GetMapValueOrDefault[T any](m map[string]any, key string, defaultValue T) T { + if value, ok := m[key]; ok { + if v, ok := value.(T); ok { + return v + } + } + return defaultValue +} diff --git a/modules/util/map_test.go b/modules/util/map_test.go new file mode 100644 index 0000000000..1a141cec88 --- /dev/null +++ b/modules/util/map_test.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetMapValueOrDefault(t *testing.T) { + testMap := map[string]any{ + "key1": "value1", + "key2": 42, + "key3": nil, + } + + assert.Equal(t, "value1", GetMapValueOrDefault(testMap, "key1", "default")) + assert.Equal(t, 42, GetMapValueOrDefault(testMap, "key2", 0)) + + assert.Equal(t, "default", GetMapValueOrDefault(testMap, "key4", "default")) + assert.Equal(t, 100, GetMapValueOrDefault(testMap, "key5", 100)) + + assert.Equal(t, "default", GetMapValueOrDefault(testMap, "key3", "default")) +} 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..1531ccd01c 100644 --- a/modules/web/router_path.go +++ b/modules/web/router_path.go @@ -99,7 +99,7 @@ func isValidMethod(name string) bool { func newRouterPathMatcher(methods, pattern string, h ...any) *routerPathMatcher { middlewares, handlerFunc := wrapMiddlewareAndHandler(nil, 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) 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/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 d097399252..a20244d6ad 100644 --- a/options/fileicon/material-icon-rules.json +++ b/options/fileicon/material-icon-rules.json @@ -716,6 +716,14 @@ ".designs": "folder-theme", "_designs": "folder-theme", "__designs__": "folder-theme", + "palette": "folder-theme", + ".palette": "folder-theme", + "_palette": "folder-theme", + "__palette__": "folder-theme", + "palettes": "folder-theme", + ".palettes": "folder-theme", + "_palettes": "folder-theme", + "__palettes__": "folder-theme", "webpack": "folder-webpack", ".webpack": "folder-webpack", "_webpack": "folder-webpack", @@ -884,6 +892,14 @@ ".music": "folder-audio", "_music": "folder-audio", "__music__": "folder-audio", + "song": "folder-audio", + ".song": "folder-audio", + "_song": "folder-audio", + "__song__": "folder-audio", + "songs": "folder-audio", + ".songs": "folder-audio", + "_songs": "folder-audio", + "__songs__": "folder-audio", "sound": "folder-audio", ".sound": "folder-audio", "_sound": "folder-audio", @@ -1136,10 +1152,18 @@ ".benchmarks": "folder-benchmark", "_benchmarks": "folder-benchmark", "__benchmarks__": "folder-benchmark", + "bench": "folder-benchmark", + ".bench": "folder-benchmark", + "_bench": "folder-benchmark", + "__bench__": "folder-benchmark", "performance": "folder-benchmark", ".performance": "folder-benchmark", "_performance": "folder-benchmark", "__performance__": "folder-benchmark", + "perf": "folder-benchmark", + ".perf": "folder-benchmark", + "_perf": "folder-benchmark", + "__perf__": "folder-benchmark", "profiling": "folder-benchmark", ".profiling": "folder-benchmark", "_profiling": "folder-benchmark", @@ -1236,10 +1260,18 @@ ".sandbox": "folder-sandbox", "_sandbox": "folder-sandbox", "__sandbox__": "folder-sandbox", + "sandboxes": "folder-sandbox", + ".sandboxes": "folder-sandbox", + "_sandboxes": "folder-sandbox", + "__sandboxes__": "folder-sandbox", "playground": "folder-sandbox", ".playground": "folder-sandbox", "_playground": "folder-sandbox", "__playground__": "folder-sandbox", + "playgrounds": "folder-sandbox", + ".playgrounds": "folder-sandbox", + "_playgrounds": "folder-sandbox", + "__playgrounds__": "folder-sandbox", "scons": "folder-scons", ".scons": "folder-scons", "_scons": "folder-scons", @@ -1584,46 +1616,46 @@ ".archival": "folder-archive", "_archival": "folder-archive", "__archival__": "folder-archive", - "bkp": "folder-archive", - ".bkp": "folder-archive", - "_bkp": "folder-archive", - "__bkp__": "folder-archive", - "bkps": "folder-archive", - ".bkps": "folder-archive", - "_bkps": "folder-archive", - "__bkps__": "folder-archive", - "bak": "folder-archive", - ".bak": "folder-archive", - "_bak": "folder-archive", - "__bak__": "folder-archive", - "baks": "folder-archive", - ".baks": "folder-archive", - "_baks": "folder-archive", - "__baks__": "folder-archive", - "backup": "folder-archive", - ".backup": "folder-archive", - "_backup": "folder-archive", - "__backup__": "folder-archive", - "backups": "folder-archive", - ".backups": "folder-archive", - "_backups": "folder-archive", - "__backups__": "folder-archive", - "back-up": "folder-archive", - ".back-up": "folder-archive", - "_back-up": "folder-archive", - "__back-up__": "folder-archive", - "back-ups": "folder-archive", - ".back-ups": "folder-archive", - "_back-ups": "folder-archive", - "__back-ups__": "folder-archive", - "history": "folder-archive", - ".history": "folder-archive", - "_history": "folder-archive", - "__history__": "folder-archive", - "histories": "folder-archive", - ".histories": "folder-archive", - "_histories": "folder-archive", - "__histories__": "folder-archive", + "bkp": "folder-backup", + ".bkp": "folder-backup", + "_bkp": "folder-backup", + "__bkp__": "folder-backup", + "bkps": "folder-backup", + ".bkps": "folder-backup", + "_bkps": "folder-backup", + "__bkps__": "folder-backup", + "bak": "folder-backup", + ".bak": "folder-backup", + "_bak": "folder-backup", + "__bak__": "folder-backup", + "baks": "folder-backup", + ".baks": "folder-backup", + "_baks": "folder-backup", + "__baks__": "folder-backup", + "backup": "folder-backup", + ".backup": "folder-backup", + "_backup": "folder-backup", + "__backup__": "folder-backup", + "backups": "folder-backup", + ".backups": "folder-backup", + "_backups": "folder-backup", + "__backups__": "folder-backup", + "back-up": "folder-backup", + ".back-up": "folder-backup", + "_back-up": "folder-backup", + "__back-up__": "folder-backup", + "back-ups": "folder-backup", + ".back-ups": "folder-backup", + "_back-ups": "folder-backup", + "__back-ups__": "folder-backup", + "history": "folder-backup", + ".history": "folder-backup", + "_history": "folder-backup", + "__history__": "folder-backup", + "histories": "folder-backup", + ".histories": "folder-backup", + "_histories": "folder-backup", + "__histories__": "folder-backup", "batch": "folder-batch", ".batch": "folder-batch", "_batch": "folder-batch", @@ -1680,6 +1712,14 @@ ".constants": "folder-constant", "_constants": "folder-constant", "__constants__": "folder-constant", + "const": "folder-constant", + ".const": "folder-constant", + "_const": "folder-constant", + "__const__": "folder-constant", + "consts": "folder-constant", + ".consts": "folder-constant", + "_consts": "folder-constant", + "__consts__": "folder-constant", "container": "folder-container", ".container": "folder-container", "_container": "folder-container", @@ -1868,14 +1908,14 @@ ".hooks": "folder-hook", "_hooks": "folder-hook", "__hooks__": "folder-hook", - "trigger": "folder-hook", - ".trigger": "folder-hook", - "_trigger": "folder-hook", - "__trigger__": "folder-hook", - "triggers": "folder-hook", - ".triggers": "folder-hook", - "_triggers": "folder-hook", - "__triggers__": "folder-hook", + "trigger": "folder-trigger", + ".trigger": "folder-trigger", + "_trigger": "folder-trigger", + "__trigger__": "folder-trigger", + "triggers": "folder-trigger", + ".triggers": "folder-trigger", + "_triggers": "folder-trigger", + "__triggers__": "folder-trigger", "job": "folder-job", ".job": "folder-job", "_job": "folder-job", @@ -3051,6 +3091,46 @@ ".kql": "folder-kusto", "_kql": "folder-kusto", "__kql__": "folder-kusto", + "policy": "folder-policy", + ".policy": "folder-policy", + "_policy": "folder-policy", + "__policy__": "folder-policy", + "policies": "folder-policy", + ".policies": "folder-policy", + "_policies": "folder-policy", + "__policies__": "folder-policy", + "attachment": "folder-attachment", + ".attachment": "folder-attachment", + "_attachment": "folder-attachment", + "__attachment__": "folder-attachment", + "attachments": "folder-attachment", + ".attachments": "folder-attachment", + "_attachments": "folder-attachment", + "__attachments__": "folder-attachment", + "bibliography": "folder-bibliography", + ".bibliography": "folder-bibliography", + "_bibliography": "folder-bibliography", + "__bibliography__": "folder-bibliography", + "bibliographies": "folder-bibliography", + ".bibliographies": "folder-bibliography", + "_bibliographies": "folder-bibliography", + "__bibliographies__": "folder-bibliography", + "book": "folder-bibliography", + ".book": "folder-bibliography", + "_book": "folder-bibliography", + "__book__": "folder-bibliography", + "books": "folder-bibliography", + ".books": "folder-bibliography", + "_books": "folder-bibliography", + "__books__": "folder-bibliography", + "link": "folder-link", + ".link": "folder-link", + "_link": "folder-link", + "__link__": "folder-link", + "links": "folder-link", + ".links": "folder-link", + "_links": "folder-link", + "__links__": "folder-link", "meta-inf": "folder-config", ".meta-inf": "folder-config", "_meta-inf": "folder-config", @@ -3797,6 +3877,14 @@ ".designs": "folder-theme-open", "_designs": "folder-theme-open", "__designs__": "folder-theme-open", + "palette": "folder-theme-open", + ".palette": "folder-theme-open", + "_palette": "folder-theme-open", + "__palette__": "folder-theme-open", + "palettes": "folder-theme-open", + ".palettes": "folder-theme-open", + "_palettes": "folder-theme-open", + "__palettes__": "folder-theme-open", "webpack": "folder-webpack-open", ".webpack": "folder-webpack-open", "_webpack": "folder-webpack-open", @@ -3965,6 +4053,14 @@ ".music": "folder-audio-open", "_music": "folder-audio-open", "__music__": "folder-audio-open", + "song": "folder-audio-open", + ".song": "folder-audio-open", + "_song": "folder-audio-open", + "__song__": "folder-audio-open", + "songs": "folder-audio-open", + ".songs": "folder-audio-open", + "_songs": "folder-audio-open", + "__songs__": "folder-audio-open", "sound": "folder-audio-open", ".sound": "folder-audio-open", "_sound": "folder-audio-open", @@ -4217,10 +4313,18 @@ ".benchmarks": "folder-benchmark-open", "_benchmarks": "folder-benchmark-open", "__benchmarks__": "folder-benchmark-open", + "bench": "folder-benchmark-open", + ".bench": "folder-benchmark-open", + "_bench": "folder-benchmark-open", + "__bench__": "folder-benchmark-open", "performance": "folder-benchmark-open", ".performance": "folder-benchmark-open", "_performance": "folder-benchmark-open", "__performance__": "folder-benchmark-open", + "perf": "folder-benchmark-open", + ".perf": "folder-benchmark-open", + "_perf": "folder-benchmark-open", + "__perf__": "folder-benchmark-open", "profiling": "folder-benchmark-open", ".profiling": "folder-benchmark-open", "_profiling": "folder-benchmark-open", @@ -4317,10 +4421,18 @@ ".sandbox": "folder-sandbox-open", "_sandbox": "folder-sandbox-open", "__sandbox__": "folder-sandbox-open", + "sandboxes": "folder-sandbox-open", + ".sandboxes": "folder-sandbox-open", + "_sandboxes": "folder-sandbox-open", + "__sandboxes__": "folder-sandbox-open", "playground": "folder-sandbox-open", ".playground": "folder-sandbox-open", "_playground": "folder-sandbox-open", "__playground__": "folder-sandbox-open", + "playgrounds": "folder-sandbox-open", + ".playgrounds": "folder-sandbox-open", + "_playgrounds": "folder-sandbox-open", + "__playgrounds__": "folder-sandbox-open", "scons": "folder-scons-open", ".scons": "folder-scons-open", "_scons": "folder-scons-open", @@ -4665,46 +4777,46 @@ ".archival": "folder-archive-open", "_archival": "folder-archive-open", "__archival__": "folder-archive-open", - "bkp": "folder-archive-open", - ".bkp": "folder-archive-open", - "_bkp": "folder-archive-open", - "__bkp__": "folder-archive-open", - "bkps": "folder-archive-open", - ".bkps": "folder-archive-open", - "_bkps": "folder-archive-open", - "__bkps__": "folder-archive-open", - "bak": "folder-archive-open", - ".bak": "folder-archive-open", - "_bak": "folder-archive-open", - "__bak__": "folder-archive-open", - "baks": "folder-archive-open", - ".baks": "folder-archive-open", - "_baks": "folder-archive-open", - "__baks__": "folder-archive-open", - "backup": "folder-archive-open", - ".backup": "folder-archive-open", - "_backup": "folder-archive-open", - "__backup__": "folder-archive-open", - "backups": "folder-archive-open", - ".backups": "folder-archive-open", - "_backups": "folder-archive-open", - "__backups__": "folder-archive-open", - "back-up": "folder-archive-open", - ".back-up": "folder-archive-open", - "_back-up": "folder-archive-open", - "__back-up__": "folder-archive-open", - "back-ups": "folder-archive-open", - ".back-ups": "folder-archive-open", - "_back-ups": "folder-archive-open", - "__back-ups__": "folder-archive-open", - "history": "folder-archive-open", - ".history": "folder-archive-open", - "_history": "folder-archive-open", - "__history__": "folder-archive-open", - "histories": "folder-archive-open", - ".histories": "folder-archive-open", - "_histories": "folder-archive-open", - "__histories__": "folder-archive-open", + "bkp": "folder-backup-open", + ".bkp": "folder-backup-open", + "_bkp": "folder-backup-open", + "__bkp__": "folder-backup-open", + "bkps": "folder-backup-open", + ".bkps": "folder-backup-open", + "_bkps": "folder-backup-open", + "__bkps__": "folder-backup-open", + "bak": "folder-backup-open", + ".bak": "folder-backup-open", + "_bak": "folder-backup-open", + "__bak__": "folder-backup-open", + "baks": "folder-backup-open", + ".baks": "folder-backup-open", + "_baks": "folder-backup-open", + "__baks__": "folder-backup-open", + "backup": "folder-backup-open", + ".backup": "folder-backup-open", + "_backup": "folder-backup-open", + "__backup__": "folder-backup-open", + "backups": "folder-backup-open", + ".backups": "folder-backup-open", + "_backups": "folder-backup-open", + "__backups__": "folder-backup-open", + "back-up": "folder-backup-open", + ".back-up": "folder-backup-open", + "_back-up": "folder-backup-open", + "__back-up__": "folder-backup-open", + "back-ups": "folder-backup-open", + ".back-ups": "folder-backup-open", + "_back-ups": "folder-backup-open", + "__back-ups__": "folder-backup-open", + "history": "folder-backup-open", + ".history": "folder-backup-open", + "_history": "folder-backup-open", + "__history__": "folder-backup-open", + "histories": "folder-backup-open", + ".histories": "folder-backup-open", + "_histories": "folder-backup-open", + "__histories__": "folder-backup-open", "batch": "folder-batch-open", ".batch": "folder-batch-open", "_batch": "folder-batch-open", @@ -4761,6 +4873,14 @@ ".constants": "folder-constant-open", "_constants": "folder-constant-open", "__constants__": "folder-constant-open", + "const": "folder-constant-open", + ".const": "folder-constant-open", + "_const": "folder-constant-open", + "__const__": "folder-constant-open", + "consts": "folder-constant-open", + ".consts": "folder-constant-open", + "_consts": "folder-constant-open", + "__consts__": "folder-constant-open", "container": "folder-container-open", ".container": "folder-container-open", "_container": "folder-container-open", @@ -4949,14 +5069,14 @@ ".hooks": "folder-hook-open", "_hooks": "folder-hook-open", "__hooks__": "folder-hook-open", - "trigger": "folder-hook-open", - ".trigger": "folder-hook-open", - "_trigger": "folder-hook-open", - "__trigger__": "folder-hook-open", - "triggers": "folder-hook-open", - ".triggers": "folder-hook-open", - "_triggers": "folder-hook-open", - "__triggers__": "folder-hook-open", + "trigger": "folder-trigger-open", + ".trigger": "folder-trigger-open", + "_trigger": "folder-trigger-open", + "__trigger__": "folder-trigger-open", + "triggers": "folder-trigger-open", + ".triggers": "folder-trigger-open", + "_triggers": "folder-trigger-open", + "__triggers__": "folder-trigger-open", "job": "folder-job-open", ".job": "folder-job-open", "_job": "folder-job-open", @@ -6131,7 +6251,47 @@ "kql": "folder-kusto-open", ".kql": "folder-kusto-open", "_kql": "folder-kusto-open", - "__kql__": "folder-kusto-open" + "__kql__": "folder-kusto-open", + "policy": "folder-policy-open", + ".policy": "folder-policy-open", + "_policy": "folder-policy-open", + "__policy__": "folder-policy-open", + "policies": "folder-policy-open", + ".policies": "folder-policy-open", + "_policies": "folder-policy-open", + "__policies__": "folder-policy-open", + "attachment": "folder-attachment-open", + ".attachment": "folder-attachment-open", + "_attachment": "folder-attachment-open", + "__attachment__": "folder-attachment-open", + "attachments": "folder-attachment-open", + ".attachments": "folder-attachment-open", + "_attachments": "folder-attachment-open", + "__attachments__": "folder-attachment-open", + "bibliography": "folder-bibliography-open", + ".bibliography": "folder-bibliography-open", + "_bibliography": "folder-bibliography-open", + "__bibliography__": "folder-bibliography-open", + "bibliographies": "folder-bibliography-open", + ".bibliographies": "folder-bibliography-open", + "_bibliographies": "folder-bibliography-open", + "__bibliographies__": "folder-bibliography-open", + "book": "folder-bibliography-open", + ".book": "folder-bibliography-open", + "_book": "folder-bibliography-open", + "__book__": "folder-bibliography-open", + "books": "folder-bibliography-open", + ".books": "folder-bibliography-open", + "_books": "folder-bibliography-open", + "__books__": "folder-bibliography-open", + "link": "folder-link-open", + ".link": "folder-link-open", + "_link": "folder-link-open", + "__link__": "folder-link-open", + "links": "folder-link-open", + ".links": "folder-link-open", + "_links": "folder-link-open", + "__links__": "folder-link-open" }, "rootFolderNames": {}, "rootFolderNamesExpanded": {}, @@ -6316,6 +6476,7 @@ "d.ts": "typescript-def", "d.cts": "typescript-def", "d.mts": "typescript-def", + "d.ets": "typescript-def", "mdoc": "markdoc", "markdoc": "markdoc", "markdoc.md": "markdoc", @@ -6468,12 +6629,16 @@ "fish": "console", "exp": "console", "nu": "console", + "xsh": "console", "ps1": "powershell", "psm1": "powershell", "psd1": "powershell", "ps1xml": "powershell", "psc1": "powershell", "pssc": "powershell", + "excalidraw.json": "excalidraw", + "excalidraw.svg": "excalidraw", + "excalidraw.png": "excalidraw", "gradle": "gradle", "doc": "word", "docx": "word", @@ -6508,8 +6673,9 @@ "ntf": "font", "mrf": "font", "lib": "lib", - "bib": "lib", "a": "lib", + "bib": "bibliography", + "bst": "bibtex-style", "dll": "dll", "ilk": "dll", "so": "dll", @@ -6530,10 +6696,13 @@ "containerfile": "docker", "compose.yaml": "docker", "compose.yml": "docker", + "bbx": "bbx", + "cbx": "cbx", + "lbx": "lbx", "tex": "tex", - "sty": "tex", - "dtx": "tex", - "ltx": "tex", + "sty": "sty", + "ltx": "ltx", + "dtx": "dtx", "pptx": "powerpoint", "ppt": "powerpoint", "pptm": "powerpoint", @@ -7209,6 +7378,10 @@ "epub": "epub", "reg": "regedit", "gnu": "gnuplot", + "smk": "snakemake", + "snakemake": "snakemake", + "cpn": "coloredpetrinets", + "pnml": "coloredpetrinets", "yaml-tmlanguage": "yaml", "tmlanguage": "xml", "cljx": "clojure", @@ -7276,8 +7449,6 @@ "babelrc": "jsonc", "jmd": "juliamarkdown", "cls": "tex", - "bbx": "tex", - "cbx": "tex", "ctx": "latex", "mak": "makefile", "mkd": "markdown", @@ -7503,6 +7674,9 @@ "pre-commit": "console", "pre-push": "console", "post-merge": "console", + "excalidraw.json": "excalidraw", + "excalidraw.svg": "excalidraw", + "excalidraw.png": "excalidraw", "gradle.properties": "gradle", "gradlew": "gradle", "gradle-wrapper.properties": "gradle", @@ -7644,6 +7818,8 @@ "compose.ci.yml": "docker", "compose.web.yml": "docker", "compose.worker.yml": "docker", + ".latexmkrc": "latexmk", + "latexmkrc": "latexmk", ".mailmap": "email", ".graphqlrc": "graphql", ".graphqlrc.json": "graphql", @@ -8105,6 +8281,7 @@ ".vars": "tune", ".dev.vars": "tune", "turbo.json": "turborepo", + "turbo.jsonc": "turborepo", ".babelrc": "babel", ".babelrc.json": "babel", ".babelrc.jsonc": "babel", @@ -8873,6 +9050,7 @@ "serverless.js": "serverless", "serverless.ts": "serverless", "supabase.js": "supabase", + "supabase.ts": "supabase", "supabase.py": "supabase", ".ember-cli": "ember", ".ember-cli.js": "ember", @@ -9284,6 +9462,20 @@ "wrangler.json": "wrangler", "wrangler.jsonc": "wrangler", ".clinerules": "cline", + ".packshiprc": "packship", + ".packshiprc.json": "packship", + ".packshiprc.js": "packship", + ".packshiprc.ts": "packship", + "packship.config.js": "packship", + "packship.config.ts": "packship", + "packship.config.mjs": "packship", + "packship.config.mts": "packship", + "packship.config.json": "packship", + "Snakefile": "snakemake", + ".hadolint.yaml": "hadolint", + ".hadolint.yml": "hadolint", + "hadolint.yaml": "hadolint", + "hadolint.yml": "hadolint", ".rhistory": "r", "cname": "http", "sonarqube.analysis.xml": "sonarcloud", @@ -9292,6 +9484,7 @@ "pklproject": "pkl", "pklproject.deps.json": "pkl", ".github/funding.yml": "github-sponsors", + "snakefile": "snakemake", "language-configuration.json": "jsonc", "icon-theme.json": "jsonc", "color-theme.json": "jsonc", @@ -9328,6 +9521,7 @@ "mojo": "mojo", "javascript": "javascript", "typescript": "typescript", + "ets": "typescript", "scala": "scala", "handlebars": "handlebars", "perl": "perl", @@ -9394,13 +9588,14 @@ "reason_lisp": "reason", "sml": "sml", "tex": "tex", - "doctex": "tex", - "latex": "tex", - "latex-expl3": "tex", + "latex": "latex", + "latex-expl3": "latex", + "doctex": "doctex", "apex": "salesforce", "sas": "sas", "dockerfile": "docker", "dockercompose": "docker", + "dockerbake": "docker", "csv": "table", "tsv": "table", "psv": "table", @@ -9427,8 +9622,8 @@ "vue-postcss": "vue", "vue-html": "vue", "lua": "lua", - "bibtex": "lib", - "bibtex-style": "lib", + "bibtex": "bibliography", + "bibtex-style": "bibtex-style", "log": "log", "jupyter": "jupyter", "plaintext": "document", @@ -9539,6 +9734,7 @@ "remix.config.js": "remix_light", "remix.config.ts": "remix_light", "turbo.json": "turborepo_light", + "turbo.jsonc": "turborepo_light", ".autorc": "auto_light", "auto.config.js": "auto_light", "auto.config.ts": "auto_light", diff --git a/options/fileicon/material-icon-svgs.json b/options/fileicon/material-icon-svgs.json index 50bc2d2b8a..5fb182b731 100644 --- a/options/fileicon/material-icon-svgs.json +++ b/options/fileicon/material-icon-svgs.json @@ -50,10 +50,13 @@ "babel": "", "ballerina": "", "bazel": "", + "bbx": "", "beancount": "", "bench-js": "", "bench-jsx": "", "bench-ts": "", + "bibliography": "", + "bibtex-style": "", "bicep": "", "biome": "", "bitbucket": "", @@ -80,6 +83,7 @@ "cake": "", "capacitor": "", "capnp": "", + "cbx": "", "cds": "", "certificate": "", "changelog": "", @@ -88,7 +92,7 @@ "chrome": "", "circleci": "", "circleci_light": "", - "citation": "", + "citation": "", "clangd": "", "cline": "", "clojure": "", @@ -104,6 +108,7 @@ "coderabbit-ai": "", "coffee": "", "coldfusion": "", + "coloredpetrinets": "", "command": "", "commitizen": "", "commitlint": "", @@ -122,8 +127,8 @@ "crystal": "", "crystal_light": "", "csharp": "", - "css-map": "", - "css": "", + "css-map": "", + "css": "", "cucumber": "", "cuda": "", "cypress": "", @@ -144,12 +149,14 @@ "django": "", "dll": "", "docker": "", + "doctex.clone": "", "document": "", "dotjs": "", "drawio": "", "drizzle": "", "drone": "", "drone_light": "", + "dtx.clone": "", "duc": "", "dune": "", "edge": "", @@ -163,6 +170,7 @@ "erlang": "", "esbuild": "", "eslint": "", + "excalidraw": "", "exe": "", "fastlane": "", "favicon": "", @@ -187,10 +195,12 @@ "folder-apollo": "", "folder-app-open": "", "folder-app": "", - "folder-archive-open": "", - "folder-archive": "", + "folder-archive-open": "", + "folder-archive": "", "folder-astro-open": "", "folder-astro": "", + "folder-attachment-open": "", + "folder-attachment": "", "folder-audio-open": "", "folder-audio": "", "folder-aurelia-open": "", @@ -199,12 +209,16 @@ "folder-aws": "", "folder-azure-pipelines-open": "", "folder-azure-pipelines": "", + "folder-backup-open": "", + "folder-backup": "", "folder-base-open": "", "folder-base": "", "folder-batch-open": "", "folder-batch": "", "folder-benchmark-open": "", "folder-benchmark": "", + "folder-bibliography-open": "", + "folder-bibliography": "", "folder-bicep-open": "", "folder-bicep": "", "folder-bloc-open": "", @@ -261,8 +275,8 @@ "folder-core": "", "folder-coverage-open": "", "folder-coverage": "", - "folder-css-open": "", - "folder-css": "", + "folder-css-open": "", + "folder-css": "", "folder-custom-open": "", "folder-custom": "", "folder-cypress-open": "", @@ -361,8 +375,8 @@ "folder-helper": "", "folder-home-open": "", "folder-home": "", - "folder-hook-open": "", - "folder-hook": "", + "folder-hook-open": "", + "folder-hook": "", "folder-husky-open": "", "folder-husky": "", "folder-i18n-open": "", @@ -409,6 +423,8 @@ "folder-less": "", "folder-lib-open": "", "folder-lib": "", + "folder-link-open": "", + "folder-link": "", "folder-linux-open": "", "folder-linux": "", "folder-liquibase-open": "", @@ -490,6 +506,8 @@ "folder-plastic": "", "folder-plugin-open": "", "folder-plugin": "", + "folder-policy-open": "", + "folder-policy": "", "folder-powershell-open": "", "folder-powershell": "", "folder-prisma-open": "", @@ -508,8 +526,8 @@ "folder-quasar": "", "folder-queue-open": "", "folder-queue": "", - "folder-react-components-open": "", - "folder-react-components": "", + "folder-react-components-open": "", + "folder-react-components": "", "folder-redux-actions-open.clone": "", "folder-redux-actions.clone": "", "folder-redux-reducer-open": "", @@ -608,6 +626,8 @@ "folder-tools": "", "folder-trash-open": "", "folder-trash": "", + "folder-trigger-open": "", + "folder-trigger": "", "folder-turborepo-open": "", "folder-turborepo": "", "folder-typescript-open": "", @@ -692,6 +712,7 @@ "gulp": "", "h": "", "hack": "", + "hadolint": "", "haml": "", "handlebars": "", "hardhat": "", @@ -751,6 +772,9 @@ "kusto": "", "label": "", "laravel": "", + "latex.clone": "", + "latexmk": "", + "lbx": "", "lefthook": "", "lerna": "", "less": "", @@ -765,6 +789,7 @@ "log": "", "lolcode": "", "lottie": "", + "ltx.clone": "", "lua": "", "luau": "", "lyric": "", @@ -839,6 +864,7 @@ "openapi": "", "openapi_light": "", "otne": "", + "packship": "", "palette": "", "panda": "", "parcel": "", @@ -955,6 +981,7 @@ "slug": "", "smarty": "", "sml": "", + "snakemake": "", "snapcraft": "", "snowpack": "", "snowpack_light": "", @@ -970,6 +997,7 @@ "stitches_light": "", "storybook": "", "stryker": "", + "sty.clone": "", "stylable": "", "stylelint": "", "stylelint_light": "", @@ -1004,7 +1032,7 @@ "test-js": "", "test-jsx": "", "test-ts": "", - "tex": "", + "tex": "", "textlint": "", "tilt": "", "tldraw": "", diff --git a/options/locale/TRANSLATORS b/options/locale/TRANSLATORS index e67255f2fb..4eee2b26c1 100644 --- a/options/locale/TRANSLATORS +++ b/options/locale/TRANSLATORS @@ -66,6 +66,7 @@ Piotr Orzechowski Richard Bukovansky Robert Nuske Robin Hübner +Ryo Hanafusa SeongJae Park Thiago Avelino Thomas Fanninger diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 0dc7b6e26a..2a3bd3e743 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1285,7 +1285,6 @@ file_copy_permalink=Kopírovat trvalý odkaz view_git_blame=Zobrazit Git Blame video_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5 video. audio_not_supported_in_browser=Váš prohlížeč nepodporuje značku pro HTML5 audio. -stored_lfs=Uloženo pomocí Git LFS symbolic_link=Symbolický odkaz executable_file=Spustitelný soubor vendored=Vendorováno @@ -3668,12 +3667,13 @@ owner.settings.chef.keypair.description=Pro autentizaci do registru Chef je zapo secrets=Tajné klíče description=Tejné klíče budou předány určitým akcím a nelze je přečíst jinak. none=Zatím zde nejsou žádné tajné klíče. -creation=Přidat tajný klíč + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Popis creation.name_placeholder=nerozlišovat velká a malá písmena, pouze alfanumerické znaky nebo podtržítka, nemohou začínat na GITEA_ nebo GITHUB_ creation.value_placeholder=Vložte jakýkoliv obsah. Mezery na začátku a konci budou vynechány. -creation.success=Tajný klíč „%s“ byl přidán. -creation.failed=Nepodařilo se přidat tajný klíč. + + deletion=Odstranit tajný klíč deletion.description=Odstranění tajného klíče je trvalé a nelze ho vrátit zpět. Pokračovat? deletion.success=Tajný klíč byl odstraněn. diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 9a5ae9418a..56dcadd451 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, existiert nicht oder Du bist nicht berechtigt, 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 API-Routen. Lies die Dokumentation 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 .git-blame-ignore-revs werden i blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in .git-blame-ignore-revs. 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 @@ -1209,6 +1228,7 @@ migrate.migrating_issues=Issues werden migriert migrate.migrating_pulls=Pull Requests werden migriert migrate.cancel_migrating_title=Migration abbrechen migrate.cancel_migrating_confirm=Möchtest du diese Migration abbrechen? +migrating_status=Migrationstatus mirror_from=Mirror von forked_from=geforkt von @@ -1233,6 +1253,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. @@ -1286,7 +1307,6 @@ file_copy_permalink=Permalink kopieren view_git_blame=Git Blame ansehen video_not_supported_in_browser=Dein Browser unterstützt das HTML5 'video'-Tag nicht. audio_not_supported_in_browser=Dein Browser unterstützt den HTML5 'audio'-Tag nicht. -stored_lfs=Gespeichert mit Git LFS symbolic_link=Softlink executable_file=Ausführbare Datei vendored=Vendor @@ -1345,6 +1365,8 @@ 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. @@ -1393,6 +1415,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 @@ -1453,6 +1476,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 @@ -1528,11 +1553,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 @@ -1624,12 +1652,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 Bereich/Element um es gegenseitig ausschließend mit anderen Bereich/ 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 @@ -1681,14 +1712,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 %[1]s %[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 diesem Issue gestartet!` +issues.stop_tracking=Timer stoppen +issues.stop_tracking_history=hat für %[1]s %[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 %[1]s %[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 @@ -1848,6 +1883,7 @@ pulls.add_prefix=%s Präfix hinzufügen pulls.remove_prefix=%s 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. @@ -1917,6 +1953,7 @@ 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 %[2]s geschlossen` pulls.reopened_at=`hat diesen Pull-Request %[2]s 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 @@ -1945,6 +1982,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 @@ -2104,6 +2142,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 @@ -2317,6 +2361,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 @@ -2354,6 +2400,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 @@ -2616,6 +2665,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 @@ -2686,6 +2738,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 @@ -2700,6 +2753,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 @@ -2854,7 +2909,11 @@ teams.invite.title=Du wurdest eingeladen, dem Team %s 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 @@ -3355,6 +3414,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 @@ -3529,6 +3590,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 /etc/apk/repositories-Datei hinzufügst: alpine.registry.key=Lade den öffentlichen RSA-Key der Registry in den /etc/apk/keys/-Ordner, um die Signatur zu überprüfen: alpine.registry.info=Wähle $branch und $repository aus der Liste unten. @@ -3558,6 +3620,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 @@ -3660,12 +3724,18 @@ owner.settings.chef.keypair.description=Ein Schlüsselpaar ist notwendig, um mit secrets=Secrets description=Secrets werden an bestimmte Aktionen weitergegeben und können nicht anderweitig ausgelesen werden. none=Noch keine Secrets vorhanden. -creation=Secret hinzufügen + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation 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.success=Das Secret "%s" wurde hinzugefügt. -creation.failed=Secret konnte nicht hinzugefügt werden. +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 +3813,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 +3828,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 +3856,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 a70137f8ac..444fbd26c9 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -1148,7 +1148,6 @@ file_copy_permalink=Αντιγραφή Permalink view_git_blame=Προβολή Git Blame video_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 'video'. audio_not_supported_in_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει την ετικέτα HTML5 'audio'. -stored_lfs=Αποθηκεύτηκε με το Git LFS symbolic_link=Symbolic link executable_file=Εκτελέσιμο Αρχείο commit_graph=Γράφημα Υποβολών @@ -3330,12 +3329,13 @@ owner.settings.chef.keypair.description=Ένα ζεύγος κλειδιών ε secrets=Μυστικά description=Τα μυστικά θα περάσουν σε ορισμένες δράσεις και δεν μπορούν να αναγνωστούν αλλού. none=Δεν υπάρχουν ακόμα μυστικά. -creation=Προσθήκη Μυστικού + +; 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.success=Το μυστικό "%s" προστέθηκε. -creation.failed=Αποτυχία δημιουργίας μυστικού. + + deletion=Αφαίρεση μυστικού deletion.description=Η αφαίρεση ενός μυστικού είναι μόνιμη και δεν μπορεί να αναιρεθεί. Συνέχεια; deletion.success=Το μυστικό έχει αφαιρεθεί. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9928b3588a..6d8aaef4cd 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -130,6 +130,7 @@ pin = Pin unpin = Unpin artifacts = Artifacts +expired = Expired confirm_delete_artifact = Are you sure you want to delete the artifact '%s' ? archived = Archived @@ -450,6 +451,7 @@ use_scratch_code = Use a scratch code twofa_scratch_used = You have used your scratch code. You have been redirected to the two-factor settings page so you may remove your device enrollment or generate a new scratch code. twofa_passcode_incorrect = Your passcode is incorrect. If you misplaced your device, use your scratch code to sign in. twofa_scratch_token_incorrect = Your scratch code is incorrect. +twofa_required = You must setup Two-Factor Authentication to get access to repositories, or try to login again. login_userpass = Sign In login_openid = OpenID oauth_signup_tab = Register New Account @@ -1226,6 +1228,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 mirror_from = mirror of forked_from = forked from @@ -1304,7 +1307,6 @@ file_copy_permalink = Copy Permalink view_git_blame = View Git Blame video_not_supported_in_browser = Your browser does not support the HTML5 'video' tag. audio_not_supported_in_browser = Your browser does not support the HTML5 'audio' tag. -stored_lfs = Stored with Git LFS symbolic_link = Symbolic link executable_file = Executable File vendored = Vendored @@ -1330,7 +1332,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. @@ -1431,7 +1435,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 @@ -1559,6 +1562,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 @@ -1650,6 +1654,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 @@ -3721,13 +3726,18 @@ owner.settings.chef.keypair.description = A key pair is necessary to authenticat secrets = Secrets description = Secrets will be passed to certain actions and cannot be read otherwise. none = There are no secrets yet. -creation = Add Secret + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description = Description creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_ creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted. creation.description_placeholder = Enter short description (optional). -creation.success = The secret "%s" has been added. -creation.failed = Failed to add secret. + +save_success = The secret "%s" has been saved. +save_failed = Failed to save secret. + +add_secret = Add secret +edit_secret = Edit secret deletion = Remove secret deletion.description = Removing a secret is permanent and cannot be undone. Continue? deletion.success = The secret has been removed. @@ -3805,6 +3815,10 @@ runs.no_workflows.documentation = For more information on Gitea Actions, see %s pulls.remove_prefix=Enlever le préfixe %s pulls.data_broken=Cette demande d’ajout est impossible par manque d'informations de bifurcation. pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblée. +pulls.is_checking=Recherche de conflits de fusion… pulls.is_ancestor=Cette branche est déjà présente dans la branche ciblée. Il n'y a rien à fusionner. pulls.is_empty=Les changements sur cette branche sont déjà sur la branche cible. Cette révision sera vide. pulls.required_status_check_failed=Certains contrôles requis n'ont pas réussi. @@ -3719,13 +3725,18 @@ owner.settings.chef.keypair.description=Une paire de clés est nécessaire pour secrets=Secrets description=Les secrets seront transmis à certaines actions et ne pourront pas être lus autrement. none=Il n'y a pas encore de secrets. -creation=Ajouter un secret + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Description creation.name_placeholder=Caractères alphanumériques ou tirets bas uniquement, insensibles à la casse, ne peut commencer par GITEA_ ou GITHUB_. creation.value_placeholder=Entrez n’importe quoi. Les blancs cernant seront taillés. creation.description_placeholder=Décrire brièvement votre dépôt (optionnel). -creation.success=Le secret "%s" a été ajouté. -creation.failed=Impossible d'ajouter le secret. + +save_success=Le secret « %s » a été enregistré. +save_failed=Impossible d’enregistrer le secret. + +add_secret=Ajouter un secret +edit_secret=Modifier le secret deletion=Supprimer le secret deletion.description=La suppression d'un secret est permanente et irréversible. Continuer ? deletion.success=Le secret a été supprimé. @@ -3803,6 +3814,10 @@ runs.no_workflows.documentation=Pour plus d’informations sur les actions Gitea 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.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 8721dd1150..3804339585 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -130,6 +130,7 @@ pin=Bioráin unpin=Díphoráil artifacts=Déantáin +expired=Imithe in éag confirm_delete_artifact=An bhfuil tú cinnte gur mhaith leat an déantán '%s' a scriosadh? archived=Cartlann @@ -450,6 +451,7 @@ use_scratch_code=Úsáid cód scratch twofa_scratch_used=D'úsáid tú do chód scratch. Tá tú atreoraithe chuig an leathanach socruithe dhá fhachtóir ionas gur féidir leat clárú do ghléas a bhaint nó cód scratch nua a ghiniúint. twofa_passcode_incorrect=Tá do phaschód mícheart. Má chuir tú do ghléas míchuir tú, bain úsáid as do chód scratch chun síniú isteach. twofa_scratch_token_incorrect=Tá do chód scratch mícheart. +twofa_required=Ní mór duit Fíordheimhniú Dhá Fhachtóir a shocrú chun rochtain a fháil ar stórtha, nó iarracht a dhéanamh logáil isteach arís. login_userpass=Sínigh isteach login_openid=OpenID oauth_signup_tab=Cláraigh Cuntas Nua @@ -1226,6 +1228,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 mirror_from=scáthán de forked_from=forcailte ó @@ -1304,7 +1307,6 @@ file_copy_permalink=Cóipeáil Buan-nasc view_git_blame=Féach ar Git Blame video_not_supported_in_browser=Ní thacaíonn do bhrabhsálaí leis an gclib 'video' HTML5. audio_not_supported_in_browser=Ní thacaíonn do bhrabhsálaí leis an gclib 'audio' HTML5. -stored_lfs=Stóráilte le Git LFS symbolic_link=Nasc siombalach executable_file=Comhad Infheidhmithe vendored=Díoltóra @@ -1558,6 +1560,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 @@ -1649,6 +1652,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 @@ -1879,6 +1883,7 @@ pulls.add_prefix=Cuir réimír %s leis pulls.remove_prefix=Bain an réimír %s pulls.data_broken=Tá an t-iarratas tarraingthe seo briste mar gheall ar fhaisnéis forc a bheith in easnamh. pulls.files_conflicted=Tá athruithe ag an iarratas tarraingthe seo atá contrártha leis an spriocbhrainse. +pulls.is_checking=Ag seiceáil le haghaidh coinbhleachtaí cumaisc ... pulls.is_ancestor=Tá an brainse seo san áireamh cheana féin sa spriocbhrainse. Níl aon rud le cumasc. pulls.is_empty=Tá na hathruithe ar an mbrainse seo ar an spriocbhrainse cheana féin. Is tiomantas folamh é seo. pulls.required_status_check_failed=Níor éirigh le roinnt seiceálacha riachtanacha. @@ -3719,13 +3724,18 @@ owner.settings.chef.keypair.description=Tá eochairphéire riachtanach le fíord secrets=Rúin description=Cuirfear rúin ar aghaidh chuig gníomhartha áirithe agus ní féidir iad a léamh ar mhalairt. none=Níl aon rúin ann fós. -creation=Cuir Rúnda leis + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Cur síos creation.name_placeholder=carachtair alfanumair nó íoslaghda amháin nach féidir a thosú le GITEA_ nó GITHUB_ creation.value_placeholder=Ionchur ábhar ar bith. Fágfar spás bán ag tús agus ag deireadh ar lár. creation.description_placeholder=Cuir isteach cur síos gairid (roghnach). -creation.success=Tá an rún "%s" curtha leis. -creation.failed=Theip ar an rún a chur leis. + +save_success=Tá an rún "%s" sábháilte. +save_failed=Theip ar an rún a shábháil. + +add_secret=Cuir rún leis +edit_secret=Cuir rún in eagar deletion=Bain rún deletion.description=Is buan rún a bhaint agus ní féidir é a chealú. Lean ort? deletion.success=Tá an rún bainte. @@ -3803,6 +3813,10 @@ runs.no_workflows.documentation=Le haghaidh tuilleadh eolais ar Gitea Actions, f 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.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ú. @@ -3842,6 +3856,8 @@ deleted.display_name=Tionscadal scriosta type-1.display_name=Tionscadal Aonair type-2.display_name=Tionscadal Stórais type-3.display_name=Tionscadal Eagrúcháin +enter_fullscreen=Lánscáileán +exit_fullscreen=Scoir Lánscáileáin [git.filemode] changed_filemode=%[1]s → %[2]s diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 9df43afbfa..ebc6d5c801 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -679,7 +679,6 @@ file_too_large=Ez a fájl túl nagy ahhoz, hogy megjelenítsük. video_not_supported_in_browser=A böngésző nem támogatja a HTML5 video tag-et. audio_not_supported_in_browser=A böngésző nem támogatja a HTML5 audio tag-et. -stored_lfs=Git LFS-el eltárolva symbolic_link=Szimbolikus hivatkozás commit_graph=Commit gráf commit_graph.hide_pr_refs=Pull request-ek elrejtése @@ -1593,8 +1592,12 @@ conan.details.repository=Tároló owner.settings.cleanuprules.enabled=Engedélyezett [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Leírás + + [actions] diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index 9e2244c1ab..54b0499d96 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -685,7 +685,6 @@ file_view_raw=Lihat Mentah file_permalink=Permalink file_too_large=Berkas terlalu besar untuk ditampilkan. -stored_lfs=Tersimpan dengan GIT LFS commit_graph=Grafik Komit blame=Salahkan normal_view=Pandangan Normal @@ -1395,8 +1394,12 @@ conan.details.repository=Repositori owner.settings.cleanuprules.enabled=Aktif [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Deskripsi + + [actions] diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index ee5eb4a7dd..42ecfabe22 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -667,7 +667,6 @@ file_view_source=Skoða Frumkóða file_view_rendered=Skoða Unnið file_copy_permalink=Afrita Varanlega Slóð -stored_lfs=Geymt með Git LFS commit_graph.hide_pr_refs=Fela Sameiningarbeiðnir commit_graph.monochrome=Einlitað commit_graph.color=Litað @@ -1326,8 +1325,12 @@ npm.details.tag=Merki pypi.requires=Þarfnast Python [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Lýsing + + [actions] diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index b89b75b2bc..9cc257029b 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -967,7 +967,6 @@ file_copy_permalink=Copia Permalink view_git_blame=Visualizza Git Blame video_not_supported_in_browser=Il tuo browser non supporta i tag "video" di HTML5. audio_not_supported_in_browser=Il tuo browser non supporta il tag "video" di HTML5. -stored_lfs=Memorizzati con Git LFS symbolic_link=Link Simbolico commit_graph=Grafico dei commit commit_graph.select=Seleziona rami @@ -1191,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 %[2]s` +issues.closed_at=`ha chiuso questo problema %[2]s` issues.reopened_at=`riaperto questo problema %[2]s` issues.commit_ref_at=`ha fatto riferimento a questa issue dal commit %[2]s` issues.ref_issue_from=`ha fatto riferimento a questo problema %[4]s %[2]s` @@ -2783,8 +2782,12 @@ settings.delete.error=Impossibile eliminare il pacchetto. owner.settings.cleanuprules.enabled=Attivo [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Descrizione + + [actions] diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 9121d8c11a..2934384052 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=アーカイブ @@ -450,6 +451,7 @@ use_scratch_code=スクラッチコードを使う twofa_scratch_used=あなたはスクラッチコードを使用しました。 2要素認証の設定ページにリダイレクトしましたので、デバイスの登録を解除するか、新しいスクラッチコードを生成しましょう。 twofa_passcode_incorrect=パスコードが正しくありません。デバイスを紛失した場合は、スクラッチコードを使ってサインインしてください。 twofa_scratch_token_incorrect=スクラッチコードが正しくありません。 +twofa_required=リポジトリにアクセスするには2段階認証を設定するか、再度ログインしてください。 login_userpass=サインイン login_openid=OpenID oauth_signup_tab=新規アカウント登録 @@ -1226,6 +1228,7 @@ migrate.migrating_issues=イシュー移行中 migrate.migrating_pulls=プルリクエスト移行中 migrate.cancel_migrating_title=移行のキャンセル migrate.cancel_migrating_confirm=この移行をキャンセルしますか? +migrating_status=移行状況 mirror_from=ミラー元 forked_from=フォーク元 @@ -1304,7 +1307,6 @@ file_copy_permalink=パーマリンクをコピー view_git_blame=Git Blameを表示 video_not_supported_in_browser=このブラウザはHTML5のvideoタグをサポートしていません。 audio_not_supported_in_browser=このブラウザーはHTML5のaudioタグをサポートしていません。 -stored_lfs=Git LFSで保管されています symbolic_link=シンボリック リンク executable_file=実行ファイル vendored=ベンダーファイル @@ -1879,6 +1881,7 @@ pulls.add_prefix=先頭に %s を追加 pulls.remove_prefix=先頭の %s を除去 pulls.data_broken=このプルリクエストは、フォークの情報が見つからないため壊れています。 pulls.files_conflicted=このプルリクエストは、ターゲットブランチと競合する変更を含んでいます。 +pulls.is_checking=マージの競合を確認しています... pulls.is_ancestor=このブランチは既にターゲットブランチに含まれています。マージするものはありません。 pulls.is_empty=このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。 pulls.required_status_check_failed=いくつかの必要なステータスチェックが成功していません。 @@ -3719,13 +3722,18 @@ owner.settings.chef.keypair.description=Chefレジストリの認証にはキー secrets=シークレット description=シークレットは特定のActionsに渡されます。 それ以外で読み出されることはありません。 none=シークレットはまだありません。 -creation=シークレットを追加 + +; 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.success=シークレット "%s" を追加しました。 -creation.failed=シークレットの追加に失敗しました。 + +save_success=シークレット "%s" を保存しました。 +save_failed=シークレットの保存に失敗しました。 + +add_secret=シークレットを追加 +edit_secret=シークレットを編集 deletion=シークレットの削除 deletion.description=シークレットの削除は恒久的で元に戻すことはできません。 続行しますか? deletion.success=シークレットを削除しました。 @@ -3803,6 +3811,10 @@ runs.no_workflows.documentation=Gitea Actions の詳細については、%s %s` issues.add_project_at=`adicionou esta questão ao planeamento %s %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 %s para %s %s` issues.change_project_at=`modificou o planeamento de %s para %s %s` issues.remove_milestone_at=`removeu esta questão da etapa %s %s` @@ -1558,6 +1561,7 @@ 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 @@ -1649,6 +1653,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 @@ -1879,6 +1884,7 @@ pulls.add_prefix=Adicione o prefixo %s pulls.remove_prefix=Remover o prefixo %s pulls.data_broken=Este pedido de integração está danificado devido à falta de informação da derivação. pulls.files_conflicted=Este pedido de integração contém modificações que entram em conflito com o ramo de destino. +pulls.is_checking=Verificando se existem conflitos na integração... pulls.is_ancestor=Este ramo já está incluído no ramo de destino. Não há nada a integrar. pulls.is_empty=As modificações feitas neste ramo já existem no ramo de destino. Este cometimento ficará vazio. pulls.required_status_check_failed=Algumas das verificações obrigatórias não foram bem sucedidas. @@ -3719,13 +3725,18 @@ owner.settings.chef.keypair.description=É necessário um par de chaves para aut secrets=Segredos description=Os segredos serão transmitidos a certas operações e não poderão ser lidos de outra forma. none=Ainda não há segredos. -creation=Adicionar segredo + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Descrição creation.name_placeholder=Só sublinhados ou alfanuméricos sem distinguir maiúsculas, sem começar com GITEA_ nem GITHUB_ creation.value_placeholder=Insira um conteúdo qualquer. Espaços em branco no início ou no fim serão omitidos. creation.description_placeholder=Escreva uma descrição curta (opcional). -creation.success=O segredo "%s" foi adicionado. -creation.failed=Falhou ao adicionar o segredo. + +save_success=O segredo "%s" foi guardado. +save_failed=Falhou ao guardar o segredo. + +add_secret=Adicionar segredo +edit_secret=Editar segredo deletion=Remover segredo deletion.description=Remover um segredo é permanente e não pode ser revertido. Continuar? deletion.success=O segredo foi removido. @@ -3803,6 +3814,10 @@ runs.no_workflows.documentation=Para mais informação sobre o Gitea Actions vej 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.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 970a46ee65..c65d08a4cf 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1127,7 +1127,6 @@ file_copy_permalink=Копировать постоянную ссылку view_git_blame=Показать git blame video_not_supported_in_browser=Ваш браузер не поддерживает HTML5 'video' тэг. audio_not_supported_in_browser=Ваш браузер не поддерживает HTML5 'audio' тэг. -stored_lfs=Хранится Git LFS symbolic_link=Символическая ссылка executable_file=Исполняемый файл commit_graph=Граф коммитов @@ -3267,12 +3266,13 @@ owner.settings.chef.keypair=Создать пару ключей secrets=Секреты description=Секреты будут передаваться определенным действиям и не могут быть прочитаны иначе. none=Секретов пока нет. -creation=Добавить секрет + +; 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.success=Секрет «%s» добавлен. -creation.failed=Не удалось добавить секрет. + + deletion=Удалить секрет deletion.description=Удаление секрета необратимо, его нельзя отменить. Продолжить? deletion.success=Секрет удалён. diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index f9acaff64f..a209187aff 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -874,7 +874,6 @@ file_too_large=ගොනුව පෙන්වීමට තරම් විශ file_copy_permalink=පිටපත් මාමලින්ක් video_not_supported_in_browser=ඔබගේ බ්රව්සරය HTML5 'වීඩියෝ' ටැගය සඳහා සහය නොදක්වයි. audio_not_supported_in_browser=ඔබගේ බ්රව්සරය HTML5 'ශ්රව්ය' ටැගය සඳහා සහය නොදක්වයි. -stored_lfs=Git LFS සමඟ ගබඩා symbolic_link=සංකේතාත්මක සබැඳිය commit_graph=ප්රස්තාරය කැප commit_graph.select=ශාඛා තෝරන්න @@ -2448,8 +2447,12 @@ conan.details.repository=කෝෂ්ඨය owner.settings.cleanuprules.enabled=සබල කර ඇත [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=සවිස්තරය + + [actions] diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index 20f26db801..e461075e53 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -981,7 +981,6 @@ file_copy_permalink=Kopírovať trvalý odkaz view_git_blame=Zobraziť Git Blame video_not_supported_in_browser=Váš prehliadač nepodporuje HTML5 tag 'video'. audio_not_supported_in_browser=Váš prehliadač nepodporuje HTML5 tag 'audio'. -stored_lfs=Uložené pomocou Git LFS symbolic_link=Symbolický odkaz commit_graph=Graf commitov line=riadok @@ -1322,6 +1321,10 @@ owner.settings.cleanuprules.enabled=Povolené [secrets] +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation + + + [actions] diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 3ed904aca2..04428aeab2 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -739,7 +739,6 @@ file_too_large=Filen är för stor för att visas. video_not_supported_in_browser=Din webbläsare stödjer ej HTML5-taggen 'video'. audio_not_supported_in_browser=Din webbläsare stöder inte taggen 'audio' i HTML5. -stored_lfs=Sparad med Git LFS symbolic_link=Symbolisk länk commit_graph=Commit-Graf commit_graph.monochrome=Mono @@ -1983,8 +1982,12 @@ conan.details.repository=Utvecklingskatalog owner.settings.cleanuprules.enabled=Aktiv [secrets] + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Beskrivning + + [actions] diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 4e389217e9..1f46369fe0 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -113,9 +113,11 @@ copy_type_unsupported=Bu dosya türü kopyalanamaz write=Yaz preview=Önizleme loading=Yükleniyor… +files=Dosyalar error=Hata error404=Ulaşmaya çalıştığınız sayfa mevcut değil veya görüntüleme yetkiniz yok. +error503=Sunucu isteğinizi gerçekleştiremedi. Lütfen daha sonra tekrar deneyin. go_back=Geri Git invalid_data=Geçersiz veri: %v @@ -128,6 +130,7 @@ pin=Sabitle unpin=Sabitlemeyi kaldır artifacts=Yapılar +expired=Süresi doldu confirm_delete_artifact=%s yapısını silmek istediğinizden emin misiniz? archived=Arşivlenmiş @@ -169,6 +172,10 @@ search=Ara... type_tooltip=Arama türü fuzzy=Bulanık fuzzy_tooltip=Arama terimine benzeyen sonuçları da içer +words=Kelimeler +words_tooltip=Sadece arama terimi kelimeleriyle eşleşen sonuçları içer +regexp=Regexp +regexp_tooltip=Sadece regexp arama terimiyle tamamen eşleşen sonuçları içer exact=Tam exact_tooltip=Sadece arama terimiyle tamamen eşleşen sonuçları içer repo_kind=Depoları ara... @@ -235,13 +242,17 @@ network_error=Ağ hatası [startpage] app_desc=Zahmetsiz, kendi sunucunuzda barındırabileceğiniz Git servisi install=Kurulumu kolay +install_desc=Platformunuz için ikili dosyayı çalıştırın, Docker ile yükleyin veya paket olarak edinin. platform=Farklı platformlarda çalışablir +platform_desc=Gitea Go ile derleme yapılabilecek her yerde çalışmaktadır: Windows, macOS, Linux, ARM, vb. Hangisini seviyorsanız onu seçin! lightweight=Hafif lightweight_desc=Gitea'nın minimal gereksinimleri çok düşüktür ve ucuz bir Raspberry Pi üzerinde çalışabilmektedir. Makine enerjinizden tasarruf edin! license=Açık Kaynak +license_desc=Gidin ve code.gitea.io/gitea'yı edinin! Bu projeyi daha da iyi yapmak için katkıda bulunarak bize katılın. Katkıda bulunmaktan çekinmeyin! [install] install=Kurulum +installing_desc=Şimdi kuruluyor, lütfen bekleyin... title=Başlangıç Yapılandırması docker_helper=Eğer Gitea'yı Docker içerisinde çalıştırıyorsanız, lütfen herhangi bir değişiklik yapmadan önce belgeleri okuyun. require_db_desc=Gitea MySQL, PostgreSQL, MSSQL, SQLite3 veya TiDB (MySQL protokolü) gerektirir. @@ -352,6 +363,7 @@ enable_update_checker=Güncelleme Denetleyicisini Etkinleştir enable_update_checker_helper=Düzenli olarak gitea.io'ya bağlanarak yeni yayınlanan sürümleri denetler. env_config_keys=Ortam Yapılandırma env_config_keys_prompt=Aşağıdaki ortam değişkenleri de yapılandırma dosyanıza eklenecektir: +config_write_file_prompt=Bu yapılandırma seçenekleri şuraya yazılacak: %s [home] nav_menu=Gezinti Menüsü @@ -380,6 +392,12 @@ show_only_public=Yalnızca açık olanlar gösteriliyor issues.in_your_repos=Depolarınızda +guide_title=Etkinlik yok +guide_desc=Herhangi bir depo veya kullanıcı takip etmiyorsunuz, bu yüzden görüntülenecek bir içerik yok. Aşağıdaki bağlantıları kullanarak ilgi çekici depo ve kullanıcıları keşfedebilirsiniz. +explore_repos=Depoları keşfet +explore_users=Kullanıcıları keşfet +empty_org=Henüz bir organizasyon yok. +empty_repo=Henüz bir depo yok. [explore] repos=Depolar @@ -433,6 +451,7 @@ use_scratch_code=Bir çizgi kodu kullanınız twofa_scratch_used=Geçici kodunuzu kullandınız. İki aşamalı ayarlar sayfasına yönlendirildiniz, burada aygıt kaydınızı kaldırabilir veya yeni bir geçici kod oluşturabilirsiniz. twofa_passcode_incorrect=Şifreniz yanlış. Aygıtınızı yanlış yerleştirdiyseniz, oturum açmak için çizgi kodunuzu kullanın. twofa_scratch_token_incorrect=Çizgi kodunuz doğru değildir. +twofa_required=Depolara erişmek için iki aşama doğrulama kullanmanız veya tekrar oturum açmayı denemeniz gereklidir. login_userpass=Oturum Aç login_openid=Açık Kimlik oauth_signup_tab=Yeni Hesap Oluştur @@ -441,6 +460,7 @@ oauth_signup_submit=Hesabı Tamamla oauth_signin_tab=Mevcut Hesaba Bağla oauth_signin_title=Bağlantılı Hesabı Yetkilendirmek için Giriş Yapın oauth_signin_submit=Hesabı Bağla +oauth.signin.error.general=Yetkilendirme isteğini işlerken bir hata oluştu: %s. Eğer hata devam ederse lütfen site yöneticisiyle bağlantıya geçin. oauth.signin.error.access_denied=Yetkilendirme isteği reddedildi. oauth.signin.error.temporarily_unavailable=Yetkilendirme sunucusu geçici olarak erişilemez olduğu için yetkilendirme başarısız oldu. Lütfen daha sonra tekrar deneyin. oauth_callback_unable_auto_reg=Otomatik kayıt etkin ancak OAuth2 Sağlayıcı %[1] eksik sahalar döndürdü: %[2]s, otomatik olarak hesap oluşturulamıyor, lütfen bir hesap oluşturun veya bağlantı verin, veya site yöneticisiyle iletişim kurun. @@ -457,10 +477,12 @@ authorize_application=Uygulamayı Yetkilendir authorize_redirect_notice=Bu uygulamayı yetkilendirirseniz %s adresine yönlendirileceksiniz. authorize_application_created_by=Bu uygulama %s tarafından oluşturuldu. authorize_application_description=Erişime izin verirseniz, özel depolar ve organizasyonlar da dahil olmak üzere tüm hesap bilgilerinize erişebilir ve yazabilir. +authorize_application_with_scopes=Kapsamlar: %s authorize_title=Hesabınıza erişmesi için "%s" yetkilendirilsin mi? authorization_failed=Yetkilendirme başarısız oldu authorization_failed_desc=Geçersiz bir istek tespit ettiğimiz için yetkilendirme başarısız oldu. Lütfen izin vermeye çalıştığınız uygulamanın sağlayıcısı ile iletişim kurun. sspi_auth_failed=SSPI kimlik doğrulaması başarısız oldu +password_pwned=Seçtiğiniz parola, daha önce herkese açık veri ihlallerinde açığa çıkan bir çalınan parola listesindedir. Lütfen farklı bir parola ile tekrar deneyin ve başka yerlerde de bu parolayı değiştirmeyi düşünün. password_pwned_err=HaveIBeenPwned'e yapılan istek tamamlanamadı last_admin=Son yöneticiyi silemezsiniz. En azından bir yönetici olmalıdır. signin_passkey=Bir parola anahtarı ile oturum aç @@ -583,6 +605,8 @@ lang_select_error=Listeden bir dil seçin. username_been_taken=Bu kullanıcı adı daha önce alınmış. username_change_not_local_user=Yerel olmayan kullanıcılar kendi kullanıcı adlarını değiştiremezler. +change_username_disabled=Kullanıcı adı değişikliği devre dışıdır. +change_full_name_disabled=Tam ad değişikliği devre dışıdır. username_has_not_been_changed=Kullanıcı adı değişmedi repo_name_been_taken=Depo adı zaten kullanılıyor. repository_force_private=Gizliyi Zorla devrede: gizli depolar herkese açık yapılamaz. @@ -632,6 +656,7 @@ org_still_own_repo=Bu organizasyon hala bir veya daha fazla depoya sahip, önce org_still_own_packages=Bu organizasyon hala bir veya daha fazla pakete sahip, önce onları silin. target_branch_not_exist=Hedef dal mevcut değil. +target_ref_not_exist=Hedef referans mevcut değil %s admin_cannot_delete_self=Yöneticiyken kendinizi silemezsiniz. Lütfen önce yönetici haklarınızı kaldırın. @@ -698,14 +723,18 @@ applications=Uygulamalar orgs=Organizasyonları Yönet repos=Depolar delete=Hesabı Sil +twofa=İki Aşamalı Kimlik Doğrulama (TOTP) account_link=Bağlı Hesaplar organization=Organizasyonlar uid=UID +webauthn=İki-Aşamalı Kimlik Doğrulama (Güvenlik Anahtarları) public_profile=Herkese Açık Profil biography_placeholder=Bize kendiniz hakkında birşeyler söyleyin! (Markdown kullanabilirsiniz) location_placeholder=Yaklaşık konumunuzu başkalarıyla paylaşın profile_desc=Profilinizin başkalarına nasıl gösterildiğini yönetin. Ana e-posta adresiniz bildirimler, parola kurtarma ve web tabanlı Git işlemleri için kullanılacaktır. +password_username_disabled=Yerel olmayan kullanıcılara kullanıcı adlarını değiştirme izni verilmemiştir. Daha fazla bilgi edinmek için lütfen site yöneticisi ile iletişime geçiniz. +password_full_name_disabled=Tam adınızı değiştirme izniniz yoktur. Daha fazla bilgi edinmek için lütfen site yöneticisi ile iletişime geçiniz. full_name=Ad Soyad website=Web Sitesi location=Konum @@ -755,6 +784,7 @@ uploaded_avatar_not_a_image=Yüklenen dosya bir resim dosyası değil. uploaded_avatar_is_too_big=Yüklenen dosyanın boyutu (%d KiB), azami boyutu (%d KiB) aşıyor. update_avatar_success=Profil resminiz değiştirildi. update_user_avatar_success=Kullanıcının avatarı güncellendi. +cropper_prompt=Kaydetmeden önce resmi düzenleyebilirsiniz. Düzenlenen resim PNG biçiminde kaydedilecektir. change_password=Parolayı Güncelle old_password=Mevcut Parola @@ -797,6 +827,7 @@ add_email_success=Yeni e-posta adresi eklendi. email_preference_set_success=E-posta tercihi başarıyla ayarlandı. add_openid_success=Yeni OpenID adresi eklendi. keep_email_private=E-posta Adresini Gizle +keep_email_private_popup=Bu, e-posta adresinizi profilde, değişiklik isteği yaptığınızda veya web arayüzünde dosya düzenlediğinizde gizleyecektir. İtilen işlemeler değişmeyecektir. openid_desc=OpenID, kimlik doğrulama işlemini harici bir sağlayıcıya devretmenize olanak sağlar. manage_ssh_keys=SSH Anahtarlarını Yönet @@ -898,6 +929,9 @@ permission_not_set=Ayarlanmadı permission_no_access=Erişim Yok permission_read=Okunmuş permission_write=Okuma ve Yazma +permission_anonymous_read=Anonim Okuma +permission_everyone_read=Herkes Okuyabilir +permission_everyone_write=Herkes Yazabilir access_token_desc=Seçili token izinleri, yetkilendirmeyi ilgili API yollarıyla sınırlandıracaktır. Daha fazla bilgi için belgeleri okuyun. at_least_one_permission=Bir token oluşturmak için en azından bir izin seçmelisiniz permissions_list=İzinler: @@ -925,6 +959,7 @@ oauth2_client_secret_hint=Bu sayfadan ayrıldıktan veya yeniledikten sonra gizl oauth2_application_edit=Düzenle oauth2_application_create_description=OAuth2 uygulamaları, üçüncü taraf uygulamanıza bu durumda kullanıcı hesaplarına erişim sağlar. oauth2_application_remove_description=Bir OAuth2 uygulamasının kaldırılması, bu sunucudaki yetkili kullanıcı hesaplarına erişmesini önler. Devam edilsin mi? +oauth2_application_locked=Gitea kimi OAuth2 uygulamalarının başlangıçta ön kaydını, yapılandırmada etkinleştirilmişse yapabilir. Beklenmeyen davranışı önlemek için bunlar ne düzenlenmeli ne de kaldırılmalı. Daha fazla bilgi için OAuth2 belgesine bakın. authorized_oauth2_applications=Yetkili OAuth2 Uygulamaları authorized_oauth2_applications_description=Kişisel Gitea hesabınıza bu üçüncü parti uygulamalara erişim izni verdiniz. Lütfen artık ihtiyaç duyulmayan uygulamalara erişimi iptal edin. @@ -933,13 +968,17 @@ revoke_oauth2_grant=Erişimi İptal Et revoke_oauth2_grant_description=Bu üçüncü taraf uygulamasına erişimin iptal edilmesi bu uygulamanın verilerinize erişmesini önleyecektir. Emin misiniz? revoke_oauth2_grant_success=Erişim başarıyla kaldırıldı. +twofa_desc=İki aşamalı kimlik doğrulama, hesabınızın güvenliğini artırır. twofa_recovery_tip=Aygıtınızı kaybetmeniz durumunda, hesabınıza tekrar erişmek için tek kullanımlık kurtarma anahtarını kullanabileceksiniz. twofa_is_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmiş. twofa_not_enrolled=Hesabınız şu anda iki faktörlü kimlik doğrulaması içinde kaydedilmemiş. twofa_disable=İki Aşamalı Doğrulamayı Devre Dışı Bırak +twofa_scratch_token_regenerate=Geçici Kodu Yeniden Üret +twofa_scratch_token_regenerated=Geçici kodunuz şimdi %s. Güvenli bir yerde saklayın, tekrar gösterilmeyecektir. twofa_enroll=İki Faktörlü Kimlik Doğrulamaya Kaydolun twofa_disable_note=Gerekirse iki faktörlü kimlik doğrulamayı devre dışı bırakabilirsiniz. twofa_disable_desc=İki faktörlü kimlik doğrulamayı devre dışı bırakmak hesabınızı daha az güvenli hale getirir. Devam edilsin mi? +regenerate_scratch_token_desc=Geçici kodunuzu kaybettiyseniz veya oturum açmak için kullandıysanız, buradan sıfırlayabilirsiniz. twofa_disabled=İki faktörlü kimlik doğrulama devre dışı bırakıldı. scan_this_image=Kim doğrulama uygulamanızla bu görüntüyü tarayın: or_enter_secret=Veya gizli şeyi girin: %s @@ -993,6 +1032,8 @@ new_repo_helper=Bir depo, sürüm geçmişi dahil tüm proje dosyalarını içer owner=Sahibi owner_helper=Bazı organizasyonlar, en çok depo sayısı sınırı nedeniyle açılır menüde görünmeyebilir. 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_size=Depo Boyutu template=Şablon template_select=Bir şablon seçin. @@ -1011,6 +1052,8 @@ fork_to_different_account=Başka bir hesaba çatalla fork_visibility_helper=Çatallanmış bir deponun görünürlüğü değiştirilemez. fork_branch=Çatala klonlanacak dal all_branches=Tüm dallar +view_all_branches=Tüm dalları görüntüle +view_all_tags=Tüm etiketleri görüntüle fork_no_valid_owners=Geçerli bir sahibi olmadığı için bu depo çatallanamaz. fork.blocked_user=Depo çatallanamıyor, depo sahibi tarafından engellenmişsiniz. use_template=Bu şablonu kullan @@ -1022,6 +1065,8 @@ generate_repo=Depo Oluştur generate_from=Şuradan Oluştur repo_desc=Açıklama repo_desc_helper=Kısa açıklama girin (isteğe bağlı) +repo_no_desc=Hiçbir açıklama sağlanmadı +repo_lang=Dil repo_gitignore_helper=.gitignore şablonlarını seç. repo_gitignore_helper_desc=Sık kullanılan diller için bir şablon listesinden hangi dosyaların izlenmeyeceğini seçin. Her dilin oluşturma araçları tarafından oluşturulan tipik yapılar, varsayılan olarak .gitignore dosyasına dahil edilmiştir. issue_labels=Konu Etiketleri @@ -1029,6 +1074,7 @@ issue_labels_helper=Bir konu etiket seti seçin. license=Lisans license_helper=Bir lisans dosyası seçin. license_helper_desc=Bir lisans, başkalarının kodunuzla neler yapıp yapamayacağını yönetir. Projeniz için hangisinin doğru olduğundan emin değil misiniz? Lisans seçme konusuna bakın +multiple_licenses=Çoklu Lisans object_format=Nesne Biçimi object_format_helper=Deponun nesne biçimi. Daha sonra değiştirilemez. SHA1 en uyumlu olandır. readme=README @@ -1082,15 +1128,20 @@ delete_preexisting_success=%s içindeki kabul edilmeyen dosyalar silindi blame_prior=Bu değişiklikten önceki suçu görüntüle blame.ignore_revs=.git-blame-ignore-revs dosyasındaki sürümler yok sayılıyor. Bunun yerine normal sorumlu görüntüsü için buraya tıklayın. blame.ignore_revs.failed=.git-blame-ignore-revs dosyasındaki sürümler yok sayılamadı. +user_search_tooltip=En fazla 30 kullanıcı görüntüler +tree_path_not_found=%[1] yolu, %[2]s deposunda mevcut değil transfer.accept=Aktarımı Kabul Et +transfer.accept_desc=`"%s" tarafına aktar` transfer.reject=Aktarımı Reddet +transfer.reject_desc=`"%s" tarafına aktarımı iptal et` transfer.no_permission_to_accept=Bu aktarımı kabul etme izniniz yok. transfer.no_permission_to_reject=Bu aktarımı reddetme izniniz yok. desc.private=Özel desc.public=Genel +desc.public_access=Herkese Açık Erişim desc.template=Şablon desc.internal=Dahili desc.archived=Arşivlenmiş @@ -1160,6 +1211,10 @@ 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.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ı +migrate.codecommit.https_git_credentials_password=HTTPS Git Kimliği Parolası migrate.migrating_git=Git Verilerini Taşıma migrate.migrating_topics=Konuları Taşıma migrate.migrating_milestones=Kilometre Taşlarını Taşıma @@ -1193,6 +1248,7 @@ create_new_repo_command=Komut satırında yeni bir depo oluşturuluyor push_exist_repo=Komut satırından mevcut bir depo itiliyor empty_message=Bu depoda herhangi bir içerik yok. broken_message=Bu deponun altındaki Git verisi okunamıyor. Bu sunucunun yöneticisiyle bağlantıya geçin veya bu depoyu silin. +no_branch=Bu deponun hiç bir dalı yok. code=Kod code.desc=Kaynak koda, dosyalara, işlemelere ve dallara eriş. @@ -1244,7 +1300,6 @@ file_copy_permalink=Kalıcı Bağlantıyı Kopyala view_git_blame=Git Suç Görüntüle video_not_supported_in_browser=Tarayıcınız HTML5 'video' etiketini desteklemiyor. audio_not_supported_in_browser=Tarayıcınız HTML5 'audio' etiketini desteklemiyor. -stored_lfs=Git LFS ile depolandı symbolic_link=Sembolik Bağlantı executable_file=Çalıştırılabilir Dosya vendored=Sağlanmış @@ -1303,6 +1358,8 @@ editor.new_branch_name_desc=Yeni dal ismi… editor.cancel=İptal editor.filename_cannot_be_empty=Dosya adı boş olamaz. editor.filename_is_invalid=Dosya adı geçersiz: "%s". +editor.commit_email=İşleme e-postası +editor.invalid_commit_email=İşleme e-postası hatalı. editor.branch_does_not_exist=Bu depoda "%s" dalı yok. 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. @@ -1351,6 +1408,7 @@ commits.signed_by_untrusted_user_unmatched=İşleyici ile eşleşmeyen güvenilm commits.gpg_key_id=GPG Anahtar Kimliği commits.ssh_key_fingerprint=SSH Anahtar Parmak İzi commits.view_path=Geçmişte bu noktayı görüntüle +commits.view_file_diff=Bu dosyanın bu işlemedeki değişikliklerini görüntüle commit.operations=İşlemler commit.revert=Geri Al @@ -1411,6 +1469,8 @@ issues.filter_milestones=Kilometre Taşı Süzgeci issues.filter_projects=Projeyi Süz issues.filter_labels=Etiket Süzgeci issues.filter_reviewers=Gözden Geçiren Süzgeci +issues.filter_no_results=Sonuç yok +issues.filter_no_results_placeholder=Arama filtrelerinizi ayarlamayı deneyin. issues.new=Yeni Konu issues.new.title_empty=Başlık boş olamaz issues.new.labels=Etiketler @@ -1428,6 +1488,7 @@ issues.new.clear_milestone=Kilometre Taşlarını Temizle issues.new.assignees=Atananlar issues.new.clear_assignees=Atamaları Temizle issues.new.no_assignees=Atanan Kişi Yok +issues.new.no_reviewers=Gözden geçiren yok issues.new.blocked_user=Konu oluşturulamıyor, depo sahibi tarafından engellenmişsiniz. issues.edit.already_changed=Konuya yapılan değişiklikler kaydedilemiyor. İçerik başka kullanıcı tarafından değiştirilmiş gözüküyor. Diğerlerinin değişikliklerinin üzerine yazmamak için lütfen sayfayı yenileyin ve tekrar düzenlemeye çalışın issues.edit.blocked_user=İçerik düzenlenemiyor, gönderen veya depo sahibi tarafından engellenmişsiniz. @@ -1484,6 +1545,7 @@ issues.filter_project=Proje 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_poster=Yazar issues.filter_type=Tür issues.filter_type.all_issues=Tüm konular @@ -1628,7 +1690,12 @@ 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_manually_add=Zaman Ekle +issues.time_estimate_set=Tahmini zaman ayarla +issues.time_estimate_display=Tahmin: %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=`başka bir konuda zaten zaman izleyici başlattınız!` issues.cancel_tracking_history=`%s zaman takibini iptal etti` @@ -2030,6 +2097,7 @@ contributors.contribution_type.deletions=Silmeler settings=Ayarlar settings.desc=Ayarlar, deponun ayarlarını yönetebileceğiniz yerdir settings.options=Depo +settings.public_access=Herkese Açık Erişim settings.collaboration=Katkıcılar settings.collaboration.admin=Yönetici settings.collaboration.write=Yazma @@ -3526,12 +3594,13 @@ owner.settings.chef.keypair.description=Chef kütüğünde kimlik doğrulaması secrets=Gizlilikler description=Gizlilikler belirli işlemlere aktarılacaktır, bunun dışında okunamaz. none=Henüz gizlilik yok. -creation=Gizlilik Ekle + +; These keys are also for "edit secret", the keys are kept as-is to avoid unnecessary re-translation creation.description=Açıklama creation.name_placeholder=küçük-büyük harfe duyarlı değil, alfanümerik karakterler veya sadece alt tire, GITEA_ veya GITHUB_ ile başlayamaz creation.value_placeholder=Herhangi bir içerik girin. Baştaki ve sondaki boşluklar ihmal edilecektir. -creation.success=Gizlilik "%s" eklendi. -creation.failed=Gizlilik eklenemedi. + + deletion=Gizliliği kaldır deletion.description=Bir gizliliği kaldırma kalıcıdır ve geri alınamaz. Devam edilsin mi? deletion.success=Gizlilik kaldırıldı. diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index cc9d668bfb..fab4a06ecb 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,445 @@ milestones=Етапи ok=OK cancel=Відмінити +retry=Повторіть спробу +rerun=Перезапустити +rerun_all=Перезапустити всі завдання save=Зберегти add=Додати add_all=Додати все remove=Видалити remove_all=Видалити все +remove_label_str=`Видалити елемент "%s"` edit=Редагувати +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=Сторінка, до якої ви намагаєтеся звернутися або до , не існує або Ви не маєте права на її перегляд. +error404=Сторінка, яку ви намагаєтеся відкрити, не існує або ви не маєте права на її перегляд. +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, будь ласка, спробуйте відшукати відповідну проблему на GitHub або створіть нову, якщо необхідно. +not_found=Ціль не знайдено. network_error=Помилка мережі [startpage] app_desc=Зручний власний сервіс хостингу репозиторіїв Git install=Легко встановити +install_desc=Просто запустіть двійковий файл для вашої платформи, скористайтеся Docker, або встановіть системою керування пакунками. platform=Платформонезалежність +platform_desc=Gitea запускається будь-де, де Go може компілюватись: на 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, будь ласка уважно прочитайте документацію перед тим, як щось змінити на цій сторінці. +docker_helper=Якщо ви запускаєте Gitea у Docker, будь ласка, прочитайте документацію перед тим, як змінювати будь-які налаштування. +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.
    Введіть абсолютний шлях, якщо ви запускаєте Gіtea як сервіс. +sqlite_helper=Шлях до файлу бази даних SQLite3.
    Введіть абсолютний шлях, якщо ви запускаєте 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=Файли журналу будуть записані в цей каталог. +app_url_helper=Базова адреса для URL-адрес клонів HTTP(S) та сповіщень електронною поштою. +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. Введіть звичайну електронну адресу або використовуйте формат: "Ім'я" . -mailer_user=SMTP Ім'я кристувача -mailer_password=SMTP Пароль -register_confirm=Потрібно підтвердити електронну пошту для реєстрації +optional_title=Необов'язкові налаштування +email_title=Налаштування електронної пошти +smtp_addr=Сервер SMTP +smtp_port=Порт SMTP +smtp_from=Відправити листа від імені +smtp_from_invalid=Адреса "Надіслати листа як" недійсна +smtp_from_helper=Адреса електронної пошти, яку буде використовувати Gitea. Введіть звичайну адресу електронної пошти або використовуйте формат «Ім'я» . +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=Показано лише важливі сховища, показати нефільтровані результати. [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=Електронний лист із підтвердженням надіслано %s. Перевірте папку 'Вхідні' в межах наступних %s, щоб завершити процес відновлення облікового запису. +need_account=Потрібен обліковий запис? +sign_up_now=Зареєструватися. +sign_up_successful=Обліковий запис створено успішно. Вітаю! +confirmation_mail_sent_prompt_ex=Новий лист з підтвердженням було надіслано на %s. Будь ласка, перевірте свою поштову скриньку протягом наступних %s, щоб завершити процес реєстрації. Якщо ви вказали невірну адресу електронної пошти, ви можете увійти ще раз і змінити її. +must_change_password=Оновити пароль +allow_password_change=Вимагати від користувача змінити пароль (рекомендовано) +reset_password_mail_sent_prompt=На адресу %s було надіслано лист із підтвердженням. Будь ласка, перевірте свою поштову скриньку протягом наступних %s, щоб завершити процес відновлення облікового запису. active_your_account=Активувати обліковий запис account_activated=Обліковий запис активовано -prohibit_login=Вхід заборонений -resent_limit_prompt=Вибачте, ви вже запросили активацію по електронній пошті нещодавно. Будь ласка, зачекайте 3 хвилини, а потім спробуйте ще раз. -has_unconfirmed_mail=Привіт %s, у вас є непідтверджена електронна адреса (%s ). Якщо ви не отримали електронний лист із підтвердженням або вам потрібно надіслати новий, натисніть на кнопку нижче. -resend_mail=Натисніть тут, щоб вислати лист активації знову -email_not_associate=Ця електронна пошта не пов'язана ні з одним обліковим записом. -send_reset_mail=Надіслати електронний лист для відновлення облікового запису +prohibit_login=Вхід заборонено +prohibit_login_desc=Ваш обліковий запис заблоковано для входу, зверніться до адміністратора сайту. +resent_limit_prompt=Ви вже надсилали запит на активацію нещодавно. Зачекайте 3 хвилини і спробуйте ще раз. +has_unconfirmed_mail= +Привіт %s, у вас непідтверджена адреса електронної пошти (%s). Якщо ви не отримали листа з підтвердженням або вам потрібно надіслати новий, будь ласка, натисніть кнопку нижче. +change_unconfirmed_mail_address=Якщо ваша адреса електронної пошти для реєстрації невірна, ви можете змінити її тут і надіслати новий лист з підтвердженням. +resend_mail=Натисніть тут, щоб повторно надіслати лист з активацією +email_not_associate=Ця адреса електронної пошти не пов'язана з жодним обліковим записом. +send_reset_mail=Надіслати лист для відновлення облікового запису reset_password=Відновлення облікового запису -invalid_code=Цей код підтвердження недійсний або закінчився. +invalid_code=Ваш код підтвердження недійсний або його термін дії закінчився. +invalid_code_forgot_password=Ваш код підтвердження недійсний або термін дії минув. Натисніть тут, щоб почати новий сеанс. +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=Пароль, який ви обрали, знаходиться в списку викрадених паролів, раніше викритих в публічних витоках даних. Спробуйте ще раз з іншим паролем і подумайте про зміну цього пароля деінде. 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=Привіт %s, activate_account=Будь ласка, активуйте ваш обліковий запис activate_account.title=%s, будь ласка, активуйте свій обліковий запис activate_account.text_1=Привіт, %[1]s, дякуємо за реєстрацію на %[2]s! -activate_account.text_2=Перейдіть за цим посиланням, щоб активувати ваш обліковий запис в %s: +activate_account.text_2=Будь ласка, перейдіть за наступним посиланням, щоб активувати свій обліковий запис протягом %s: -activate_email=Підтвердить вашу адресу електронної пошти -activate_email.text=Перейдіть за цим посиланням, щоб підтвердити вашу електронну адресу в %s: +activate_email=Підтвердіть адресу електронної пошти +activate_email.title=%s, будь ласка, підтвердіть вашу адресу електронної пошти +activate_email.text=Будь ласка, перейдіть за наступним посиланням протягом %s, щоб підтвердити свою електронну адресу: +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=Якщо цей обліковий запис було створено для вас, будь ласка, спочатку встановіть свій пароль. +register_notify.text_3=Якщо цей обліковий запис створено для вас, будь ласка, спершу встановіть пароль. -reset_password=Відновлення вашого облікового запису -reset_password.title=%s, ви відправили запит на відновлення облікового запису -reset_password.text=Перейдіть за цим посиланням, щоб відновити ваш обліковий запис в %s: +reset_password=Відновити обліковий запис +reset_password.title=%s, ви надіслали запит на відновлення облікового запису +reset_password.text=Щоб відновити обліковий запис, перейдіть за наступним посиланням протягом %s: 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=@%s згадав вас: -issue.action.force_push=%[1]s force-pushed %[2]s з %[3]s в %[4]s. -issue.action.push_1=@%[1]s надіслав %[3]d коміти %[2]s -issue.action.push_n=@%[1]s відправив %[3]d коміти до %[2]s -issue.action.close=@%[1]s закрито #%[2]d. +issue.action.push_1=@%[1]s надіслав %[3]d коміт в %[2]s +issue.action.push_n=@%[1]s надіслав %[3]d коміти до %[2]s +issue.action.close=@%[1]s закрив #%[2]d. issue.action.reopen=@%[1]s заново відкрив #%[2]d. issue.action.merge=@%[1]s об'єднав #%[2]d до %[3]s. -issue.action.approve=@%[1]s затвердили цей запит на злиття. -issue.action.reject=@%[1]s запитують зміни на цей запит на злиття. -issue.action.review=@%[1]s прокоментували цей запит на злиття. -issue.action.review_dismissed=@%[1]s відхилено останній відгук від %[2]s для цього запиту на злиття. -issue.action.ready_for_review=@%[1]s позначили цей запит на злиття як готовий до розгляду. -issue.action.new=@%[1]s створили #%[2]d. +issue.action.approve=@%[1]s затвердив цей запит на злиття. +issue.action.reject=@%[1]s запросив зміни у цьому запиті на злиття. +issue.action.review=@%[1]s прокоментував цей запит на злиття. +issue.action.review_dismissed=@%[1]s відхилив останній відгук від %[2]s на цей запит на злиття. +issue.action.ready_for_review=@%[1]s позначив цей запит на злиття як готовий до розгляду. +issue.action.new=@%[1]s створив #%[2]d. issue.in_tree_path=В %s: release.new.subject=%s в %s випущено @@ -372,35 +531,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 +574,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=Оберіть мову з переліку. +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=Адреса електронної пошти помилкова. +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=Домен електронної пошти користувача %s конфліктує з 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=Організацію неможливо додати як учасника команди. +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 +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 +703,456 @@ 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=Потрібна допомога? Дивіться гід на GitHub з генерації ключів SSH або виправлення типових неполадок SSH. -gpg_helper= Потрібна допомога? Перегляньте посібник GitHub про GPG . -add_new_key=Додати SSH ключ -add_new_gpg_key=Додати GPG ключ +ssh_desc=Ці публічні ключі SSH пов'язані з вашим обліковим записом. Відповідні приватні ключі надають повний доступ до ваших сховищ. +principal_desc=Ці ідентифікатори сертифікатів SSH прив'язані до вашого облікового запису і надають повний доступ до ваших сховищ. +gpg_desc=Ці публічні ключі GPG пов'язані з вашим обліковим записом. Зберігайте свої приватні ключі в безпеці, оскільки вони дозволяють підтверджувати коміти. +ssh_helper=Потрібна допомога? Ознайомтеся з інструкцією GitHub щодо створення власних ключів SSH або виправлення типових неполадок SSH. +gpg_helper=Потрібна допомога? Перегляньте посібник GitHub про GPG. +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=Назва програми %s вже використовується. Будь ласка, використайте нову. delete_token=Видалити access_token_deletion=Видалити токен доступу access_token_deletion_cancel_action=Відмінити access_token_deletion_confirm_action=Видалити -delete_token_success=Токен був знищений. Програми, що використовують його, більше не мають доступу до вашого облікового запису. -permission_read=Прочитані +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=Обрані дозволи токена обмежують авторизацію лише відповідними маршрутами API. Читайте документацію для отримання додаткової інформації. +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=Створити програму -oauth2_application_name=Назва програми +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=Пропустити авторизацію для публічних клієнтів після надання доступу один раз. Може становити ризик для безпеки. +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=Ваш обліковий запис на даний час використовує двофакторну автентифікацію. -twofa_not_enrolled=Ваш обліковий запис наразі не використовує двофакторну автентифікаціїю. +twofa_desc=Щоб захистити свій обліковий запис від крадіжки пароля, ви можете використовувати смартфон або інший пристрій для отримання одноразових паролів, прив'язаних до часу (TOTP). +twofa_recovery_tip=Якщо ви втратите свій пристрій, ви зможете скористатися одноразовим ключем відновлення, щоб відновити доступ до свого облікового запису. +twofa_is_enrolled=Ваш обліковий запис наразі використовує двофакторну автентифікацію. +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_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=Додайте веб-хуки, які запускатимуться для усіх репозиторіїв, якими ви володієте. -orgs_none=Ви не є учасником будь-якої організації. +orgs_none=Ви не є членом організації. +repos_none=У вас немає сховищ. -delete_account=Видалити ваш обліковий запис -delete_prompt=Ця операція остаточно видалить обліковий запис користувача. Це НЕ МОЖЛИВО відмінити. -delete_with_all_comments=Ваш обліковий запис молодший за %s днів. Щоб уникнути коментарів-привидів, всі запити/PR коментрарі будуть видалені з ним. -confirm_delete_account=Підтвердження видалення -delete_account_title=Видалити цей обліковий запис +delete_account=Видалити обліковий запис +delete_prompt=Ця операція остаточно видалить ваш обліковий запис. Її НЕ МОЖЛИВО скасувати. +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=Сховище містить усі файли проєкту, включно з історією ревізій. Ви вже розміщуєте його деінде? Перенести сховище. 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=Потрібна допомога у клонуванні? Відвідайте сторінку Допомога. 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=Ліцензія регулює те, що інші можуть і не можуть робити з вашим кодом. Не впевнені, що саме підходить для вашого проєкту? Дивіться Виберіть ліцензію. +license_helper=Обрати файл ліцензії. +license_helper_desc=Ліцензія визначає, що інші можуть робити з вашим кодом, а що ні. Не впевнені, яка підходить для вашого проєкту? Дивіться Вибір ліцензії. +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 для клону щоб визначити LFS-сервер. Ви також можете вказати кінцеву точку користувача, якщо дані репозиторію LFS зберігаються в іншому місці. +mirror_lfs_endpoint_desc=Синхронізація спробує використати URL-адресу клону для визначення сервера LFS. Ви також можете вказати власну кінцеву точку, якщо дані 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=Не вдалося проігнорувати ревізії у .git-blame-ignore-revs. +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 віддалено, щоб визначати LFS сервер. Ви також можете вказати свою кінцеву точку, якщо дані репозиторію LFS зберігаються в іншому місці. -migrate_options_lfs_endpoint.description.local=Також підтримуються шляхи на локальному сервері. -migrate_items=Деталі міграції +migrate_options_lfs_endpoint.description=Міграція спробує використати ваш Git віддалено, щоб визначати сервер LFS. Ви також можете вказати власну кінцеву точку, якщо дані сховища LFS зберігаються в іншому місці. +migrate_options_lfs_endpoint.description.local=Також підтримується шлях до локального сервера. +migrate_items=Елементи міграції migrate_items_wiki=Вікі migrate_items_milestones=Етапи migrate_items_labels=Мітки @@ -827,26 +1162,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=Перенесено з %[2]s migrated_from_fake=Перенесено з %[1]s -migrate.migrate=Міграція з %s +migrate.migrate=Мігрувати з %s migrate.migrating=Міграція із %s... migrate.migrating_failed=Міграція із %s не вдалася. +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 +1197,34 @@ migrate.migrating_labels=Міграція міток migrate.migrating_releases=Міграція релізів migrate.migrating_issues=Міграція задач migrate.migrating_pulls=Міграція запитів на злиття +migrate.cancel_migrating_title=Скасувати міграцію +migrate.cancel_migrating_confirm=Ви хочете скасувати міграцію? +migrating_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,97 +1238,149 @@ tags=Теги issues=Задачі pulls=Запити на злиття projects=Проєкти +packages=Пакети +actions=Дії labels=Мітки org_labels_desc=Мітки рівня організації можуть використовуватися в усіх репозиторіях цієї організації 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'. -stored_lfs=Збережено з Git LFS 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.fail_to_apply_patch=`Не вдалося застосувати патч "%s"` +editor.new_patch=Новий патч editor.commit_message_desc=Додати необов'язковий розширений опис… -editor.signoff_desc=Додатиь Signed-off-by комітом в конці повідомлення журналу комітів. -editor.commit_directly_to_this_branch=Зробіть коміт прямо в гілку %s. +editor.signoff_desc=Додати «Підписано комітером» в кінці повідомлення коміту. +editor.commit_directly_to_this_branch=Зробити коміт безпосередньо в гілку %s. editor.create_new_branch=Створити нову гілку для цього коміту та відкрити запит на злиття. editor.create_new_branch_np=Створити нову гілку для цього коміту. 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_deleting_no_longer_exists=Видалений файл '%s' більше не існує в цьому сховищі. editor.file_changed_while_editing=Зміст файлу змінився з моменту початку редагування. Натисніть тут , щоб переглянути що було змінено, або закомітьте зміни ще раз, щоб переписати їх. editor.commit_empty_file_header=Закомітити порожній файл -editor.commit_empty_file_text=Файл, в комміті порожній. Продовжити? -editor.no_changes_to_show=Нема змін для показу. +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=Повне повідомлення про відмову: 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=Гілка вимагає підписаного коміту commits.desc=Переглянути історію зміни коду. commits.commits=Коміти 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.revert=Повернути до попереднього стану +commit.revert-header=Повернути: %s +commit.revert-content=Виберіть гілку, до якої хочете повернутися: commitstatus.error=Помилка +commitstatus.failure=Невдача commitstatus.pending=Очікування +commitstatus.success=Успіх ext_issues=Доступ до зовнішніх задач ext_issues.desc=Посилання на зовнішню систему відстеження задач. @@ -987,23 +1390,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=Фільтр виконавців @@ -1011,8 +1426,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=Очистити мітки @@ -1023,15 +1440,20 @@ 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.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=Нова мітка @@ -1041,7 +1463,7 @@ issues.create_label=Створити мітку issues.label_templates.title=Завантажити визначений набір міток issues.label_templates.info=Ще немає міток. Натисніть 'Нова мітка' або використовуйте попередньо визначений набір міток: issues.label_templates.helper=Оберіть набір міток -issues.label_templates.use=Використовувати набір міток +issues.label_templates.use=Використати набір міток issues.add_label=додано %s з міткою %s issues.add_labels=додано %s з мітками %s issues.remove_label=видалено %s з міткою %s @@ -1067,27 +1489,38 @@ issues.delete_branch_at=`видалена гілка %s %s` issues.filter_label=Мітка issues.filter_label_exclude=`Використовуйте Alt + клік/Enter для виключення міток` 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_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.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=Відкрити @@ -1097,13 +1530,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 відкрито %[3]s -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=`прокоментував(ла) %s` issues.delete_comment_confirm=Ви впевнені, що хочете видалити цей коментар? @@ -1112,33 +1548,53 @@ issues.context.quote_reply=Цитувати відповідь issues.context.reference_issue=Посилання в новій задачі issues.context.edit=Редагувати issues.context.delete=Видалити +issues.close=Закрити задачу +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=`закрив цю задачу %[2]s` -issues.reopened_at=`повторно відкрив цю задачу %[2]s` +issues.comment.blocked_user=Неможливо створити або редагувати коментар, тому що ви заблоковані автором або власником сховища. +issues.closed_at=`закрив(ла) цю задачу %[2]s` +issues.reopened_at=`повторно відкрив(ла) цю задачу %[2]s` issues.commit_ref_at=`згадано цю задачу в коміті %[2]s` -issues.ref_issue_from=`посилання на цю задачу %[4]s %[2]s` +issues.ref_issue_from=` вказав(ла) на цю задачу %[4]s %[2]s` issues.ref_pull_from=`послався на цей запит злиття %[4]s %[2]s` issues.ref_closing_from=`згадав запит на злиття %[4]s, які закриють цю задачу %[2]s` -issues.ref_reopening_from=`згадав запит на злиття %[4]s, які повторно відкриють цю задачу %[2]s` +issues.ref_reopening_from=`згадав запит на злиття %[4]s, який знову відкриє цю задачу %[2]s` issues.ref_closed_from=`закрив цю задачу %[4]s %[2]s` issues.ref_reopened_from=`повторно відкрито цю задачу %[4]s %[2]s` 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=Підпишіться щоб приєднатися до обговорення. +issues.dismiss_review=Відхилити рецензію +issues.dismiss_review_warning=Ви впевнені, що хочете відхилити рецензію? +issues.sign_in_require_desc=Увійдіть, щоб приєднатися до розмови. issues.edit=Редагувати issues.cancel=Відмінити issues.save=Зберегти issues.label_title=Назва мітки issues.label_description=Опис мітки -issues.label_color=Колір мітки +issues.label_color=Колір +issues.label_color_invalid=Недійсний колір +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=Редагувати @@ -1146,9 +1602,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 учасників @@ -1156,17 +1612,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=заблоковано як %s та обмежене обговорення для співавторів %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=- Ви завжди зможете заблокувати цю задачу в майбутньому. @@ -1175,37 +1635,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=змінено приблизний час на %[1]s %[2]s +issues.remove_time_estimate_at=видалено оцінку часу %s +issues.time_estimate_invalid=Невірний формат розрахунку часу issues.tracker_auto_close=Таймер буде автоматично зупинено, коли ця задача буде закрита issues.tracking_already_started=`Ви вже почали відстежувати час для іншої задачі!` +issues.stop_tracking=Зупинити таймер +issues.stop_tracking_history=працював для %[1]s %[2]s +issues.cancel_tracking=Скинути +issues.cancel_tracking_history=`скасував(-ла) відстеження часу %s` issues.del_time=Видалити цей журнал часу +issues.add_time_history=додав(ла) витрачений час %[1]s %[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 з %[2]s до %[4]s %[6]s` +issues.force_push_codes=`примусово надіслано %[1]s з %[2]s до %[4]s %[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=Видалити @@ -1213,39 +1697,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=Показати вирішене @@ -1253,7 +1743,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=видалено @@ -1261,20 +1756,32 @@ 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.nothing_to_compare=Ці гілки однакові. Немає необхідності створювати запитів на злиття. pulls.nothing_to_compare_and_allow_empty_pr=Одинакові гілки. Цей PR буде порожнім. pulls.has_pull_request=`Запит злиття для цих гілок вже існує: %[2]s#%[3]d` @@ -1285,113 +1792,149 @@ pulls.change_target_branch_at=`змінена цільова гілка з %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.closed=Запит на злиття закрито pulls.manually_merged=Ручне злиття -pulls.is_closed=Запит на злиття було закрито. -pulls.title_wip_desc=`Почніть заголовок з %s щоб запобігти випадковому злиттю запитів.` -pulls.cannot_merge_work_in_progress=Цей пулл-реквест позначений як прийнятий в опрацювання. -pulls.still_in_progress=Все ще в процесі? +pulls.merged_info_text=Гілку %s тепер можна видалити. +pulls.is_closed=Запит на злиття закрито. +pulls.title_wip_desc=`Почніть заголовок з %s щоб запобігти випадковому об'єднанню.` +pulls.cannot_merge_work_in_progress=Цей запит на злиття позначено як незавершений. +pulls.still_in_progress=Ще не закінчено? pulls.add_prefix=Додати префікс %s pulls.remove_prefix=Видалити префікс %s -pulls.data_broken=Зміст цього запиту було порушено внаслідок видалення інформації Форком. Цей запит тягнеться через відсутність інформації про вилучення. -pulls.files_conflicted=Цей запит має зміни, що конфліктують з цільовою гілкою. +pulls.data_broken=Збій цього запиту на злиття через відсутність інформації про форк. +pulls.files_conflicted=Цей запит на злиття має зміни, що конфліктують з цільовою гілкою. +pulls.is_checking=Перевірка конфліктів об'єднання (merge) ... 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.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.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=`закрив цей запит на злиття %[2]s` pulls.reopened_at=`повторно відкрив цей запит на злиття %[2]s` +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_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=%d%% завершено 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 @@ -1401,12 +1944,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 дні @@ -1416,25 +1966,25 @@ activity.period.quarterly=3 місяці activity.period.semiyearly=6 місяців activity.period.yearly=1 рік activity.overview=Огляд -activity.active_prs_count_1=%d Активний запити на злиття +activity.active_prs_count_1=%d Активний запит на злиття activity.active_prs_count_n=%d Активні запити на злиття -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=%d Активна задача activity.active_issues_count_n=%d Активні задачі 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 @@ -1444,97 +1994,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=Першу захоплену групу буде використано замість {index}. settings.tracker_url_format_desc=Використовуйте шаблони {user}, {repo} та {index} для імені користувача, репозиторію та номеру задічі. 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=Ця операція перетворить форк на звичайний репозиторій та не може бути скасованою. @@ -1544,89 +2128,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=- Цю операцію НЕ МОЖНА відмінити. -settings.delete_notices_2=- Ця операція остаточно видалить %s репозиторій, включаючи код, задачі, коментарі, вікі та налаштування співавторів. +settings.delete_desc=Видалення сховища є остаточним і не може бути скасоване. +settings.delete_notices_1=- Цю операцію НЕМОЖЛИВО скасувати. +settings.delete_notices_2=- Ця операція назавжди видалить сховище %s, включно з кодом, проблемами, коментарями, даними вікі та налаштуваннями співавторів. 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. Дізнайтеся більше в інструкції по використанню web-хуків . +settings.add_webhook.invalid_channel_name=Назва каналу веб-хука не може бути порожньою і містити лише символ #. +settings.hooks_desc=Веб-хуки автоматично роблять HTTP POST запити до сервера, коли відбуваються певні події Gitea. Дізнайтеся більше в інструкції по використанню веб-хуків. 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 буде відправляти POST запити на вказану URL адресу, з інформацією про події, що відбуваються. Подробиці на сторінці інструкції по використанню web-хуків . +settings.add_webhook_desc=Gitea надішле запити POST із зазначеним типом змісту на цільову URL-адресу. Дізнайтеся більше в інструкції по використанню веб-хуків. 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=Події репозиторію @@ -1635,41 +2222,40 @@ 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_package=Пакет settings.branch_filter=Фільтр гілок +settings.authorization_header=Заголовок авторизації settings.active=Активний settings.active_helper=Інформацію про викликані події буде надіслано за цією веб-хук URL-адресою. settings.add_hook_success=Веб-хук було додано. @@ -1681,71 +2267,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=Чи може цей ключ бути використаний для виконання push в репозиторій? Ключі розгортання завжди мають доступ на 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=Захист гілки '%s' +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=Шаблони назв захищених гілок. Перегляньте документацію щодо синтаксису шаблону. Приклади: 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=Ви можете використовувати єдине ім’я, глобальний шаблон або регулярний вираз, щоб відповідати кільком міткам. Докладніше читайте в посібнику із захищених міток. 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, які зберігаються в цьому репозиторії @@ -1757,9 +2381,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=Відсутнє блокування @@ -1799,7 +2423,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=Висота @@ -1811,6 +2435,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=Додати коментар @@ -1818,81 +2443,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=%d коміт(ів) 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=Назва організації @@ -1902,6 +2576,7 @@ create_org=Створити організацію repo_updated=Оновлено members=Учасники teams=Команди +code=Код lower_members=учасники lower_repositories=репозиторії create_new_team=Нова команда @@ -1911,43 +2586,48 @@ 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_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.change_orgname_prompt=Примітка: Зміна назви організації також змінить URL-адресу вашої організації та звільнить стару назву. +settings.change_orgname_redirect_prompt=Стара назва буде перенаправлятися до тих пір, поки не буде заброньована. settings.update_avatar_success=Аватар організації оновлений. settings.delete=Видалити організацію settings.delete_account=Видалити цю організацію -settings.delete_prompt=Організація буде остаточно видалена. Це НЕ МОЖЛИВО відмінити! -settings.confirm_delete_account=Підтвердіть видалення +settings.delete_prompt=Організацію буде остаточно видалено. Це НЕМОЖЛИВО скасувати! +settings.confirm_delete_account=Підтвердити видалення settings.delete_org_title=Видалити організацію settings.delete_org_desc=Ця організація буде безповоротно видалена. Продовжити? -settings.hooks_desc=Додайте webhooks, який буде викликатися для всіх репозиторіїв якими володіє ця організація. +settings.hooks_desc=Додайте веб-хуки, які спрацьовуватимуть для всіх сховищ у цій організації. -settings.labels_desc=Додати мітки, які можуть бути використані для задач для всіх репозиторіїв в цій організації. +settings.labels_desc=Додайте мітки, які можна використовувати у задачах для усіх сховищ у цій організації. members.membership_visibility=Видимість учасника: members.public=Показувати -members.public_helper=зробити прихованим +members.public_helper=приховати members.private=Прихований members.private_helper=зробити видимим members.member_role=Роль учасника: @@ -1965,22 +2645,27 @@ teams.leave=Покинути teams.leave.detail=Покинути %s? teams.can_create_org_repo=Створити репозиторії teams.can_create_org_repo_helper=Учасники можуть створювати нові репозиторії в організації. Автор отримає доступ адміністратора до нового репозиторію. -teams.read_access=Прочитані +teams.none_access=Немає доступу +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=Власник має повний доступ до усіх репозиторіїв та має права адміністратора організації. +teams.owners_permission_desc=Власники мають повний доступ до усіх репозиторіїв та права адміністратора організації. 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=Ця команда має доступ для читання: учасники можуть переглядати та клонувати репозиторії. +teams.read_permission_desc=Ця команда має доступ на читання: учасники можуть переглядати та клонувати репозиторії. teams.write_permission_desc=Ця команда надає доступ на запис: учасники можуть отримувати й виконувати push команди до репозитрію. teams.admin_permission_desc=Ця команда надає адміністраторський доступ: учасники можуть читати, виконувати push команди та додавати співробітників до репозиторію. teams.create_repo_permission_desc=Крім того, ця команда надає дозвіл Створити репозиторій: учасники можуть створювати нові репозиторії в організації. @@ -1992,6 +2677,7 @@ teams.add_all_repos_desc=Це додасть всі репозиторії ор teams.add_duplicate_users=Користувач уже є членом команди. teams.repos.none=Для команди немає доступних репозиторіїв. teams.members.none=Немає членів в цій команді. +teams.members.blocked_user=Не вдається додати користувача, оскільки він заблокований організацією. teams.specific_repositories=Конкретні репозиторії teams.specific_repositories_helper=Учасники матимуть доступ лише до репозиторіїв, які були явно додані до команди. Вибір цього пункту не призводить до автоматичного видалення репозиторіїв, доданих з Всі репозиторії. teams.all_repositories=Всі репозиторії @@ -1999,17 +2685,35 @@ teams.all_repositories_helper=Команда має доступ до всіх teams.all_repositories_read_permission_desc=Ця команда надає дозвіл Перегляд для всіх репозиторіїв: учасники можуть переглядати та клонувати їх. teams.all_repositories_write_permission_desc=Ця команда надає дозвіл Запис для всіх репозиторіїв: учасники можуть переглядати та виконувати push в репозиторіях. teams.all_repositories_admin_permission_desc=Ця команда надає дозвіл Адміністрування для всіх репозиторіїв: учасники можуть переглядати, виконувати push та додавати співробітників. +teams.invite.title=Вас запросили приєднатися до команди %s в організації %s. +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=Налаштування @@ -2018,8 +2722,11 @@ monitor=Моніторинг first_page=Перша last_page=Остання total=Разом: %d +settings=Адміністративні налаштування +dashboard.new_version_hint=Gitea %s тепер доступна, ви використовуєте %s. Перевірте блог для отримання додаткової інформації. dashboard.statistic=Підсумок +dashboard.maintenance_operations=Операції з технічного обслуговування dashboard.system_status=Статус системи dashboard.operation_name=Назва операції dashboard.operation_switch=Перемкнути @@ -2028,20 +2735,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=Перевірити статистику всіх репозиторіїв @@ -2051,11 +2761,12 @@ 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_actions=Очищення ресурсів прострочених дій +dashboard.server_uptime=Час роботи сервера dashboard.current_goroutine=Поточна кількість Goroutines dashboard.current_memory_usage=Поточне використання пам'яті dashboard.total_memory_allocated=Виділено пам'яті загалом @@ -2084,6 +2795,14 @@ dashboard.total_gc_time=Загальна пауза збирача сміття dashboard.total_gc_pause=Загальна пауза збирача сміття (GC) dashboard.last_gc_pause=Остання пауза збирача сміття (GC) dashboard.gc_times=Кількість запусків збирача сміття (GC) +dashboard.delete_old_actions=Видалити всі старі дії з бази даних +dashboard.update_checker=Перевірка оновлень +dashboard.gc_lfs=Збір сміття мета-об'єктів LFS +dashboard.cancel_abandoned_jobs=Скасувати покинуті завдання +dashboard.start_schedule_tasks=Запуск запланованих завдань +dashboard.sync_branch.started=Розпочато синхронізацію гілок +dashboard.rebuild_issue_indexer=Перебудувати індексатор задач +dashboard.sync_repo_licenses=Синхронізувати ліцензії сховища users.user_manage_panel=Керування обліковими записами користувачів users.new_account=Створити обліковий запис @@ -2092,12 +2811,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=Локальні @@ -2117,8 +2840,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=Фільтр @@ -2129,13 +2854,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=Електронна пошта (зворотна) @@ -2143,8 +2869,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=Назва @@ -2160,12 +2891,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=Додати веб-хук за замовчуванням @@ -2177,7 +2917,7 @@ systemhooks.update_webhook=Оновити системний вебхук auths.auth_manage_panel=Керування джерелом автентифікації auths.new=Додати джерело автентифікації -auths.name=Ім'я +auths.name=Назва auths.type=Тип auths.enabled=Увімкнено auths.syncenabled=Увімкнути синхронізацію користувача @@ -2194,12 +2934,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=Користувацький фільтр @@ -2209,6 +2950,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 хост @@ -2224,7 +2966,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 для автоматизації входу @@ -2236,6 +2978,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 @@ -2249,21 +2992,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 @@ -2271,13 +3026,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 @@ -2287,7 +3043,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=Мінімальні розміри ключів @@ -2299,8 +3055,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=Шлях @@ -2332,16 +3088,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=Увімкнено @@ -2351,6 +3115,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=Провайдер сесії @@ -2379,23 +3147,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=Черги @@ -2405,12 +3182,15 @@ 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=Переглянути деталі повідомлення @@ -2426,11 +3206,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=створив(ла) репозиторій %s rename_repo=репозиторій перейменовано з %[1]s на %[3]s -commit_repo=надіслав зміни (push) до %[3]s о %[4]s +commit_repo=надіслав зміни (push) до %[3]s в %[4]s create_issue=`відкрив задачу %[3]s#%[2]s` close_issue=`закрив задачу %[3]s#%[2]s` reopen_issue=`повторно відкрив задачу %[3]s#%[2]s` @@ -2440,6 +3228,7 @@ reopen_pull_request=`повторно відкрив запит злиття %[3]s#%[2]s` comment_pull=`прокоментував запит злиття %[3]s#%[2]s` merge_pull_request=`прийняв запит злиття %[3]s#%[2]s` +auto_merge_pull_request=`автоматично об'єднано запит на злиття %[3]s#%[2]s` transfer_repo=перенесено репозиторій %s у %s push_tag=створив мітку %[3]s в %[4]s delete_tag=видалено мітку %[2]s з %[3]s @@ -2472,7 +3261,6 @@ seconds=%d секунди minutes=%d хвилини hours=%d години days=%d дні -weeks=%d тижднів months=%d місяці years=%d роки raw_seconds=секунди @@ -2480,6 +3268,7 @@ raw_minutes=хвилини [dropzone] default_message=Перетягніть файли або натисніть тут, щоб завантажити. +invalid_input_type=Неможливо завантажити файли цього типу. file_too_big=Розмір файлу ({{filesize}} MB), що більше ніж максимальний розмір: ({{maxFilesize}} MB). remove_file=Видалити файл @@ -2493,54 +3282,275 @@ 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=Для отримання додаткової інформації про реєстр пакетів, дивіться документацію. +empty.repo=Ви завантажили пакунок, але він тут не відображається? Перейдіть до налаштування пакету та під'єднайте його до цього сховища. +registry.documentation=Докладнішу інформацію про реєстр %s наведено у документації. filter.type=Тип +filter.type.all=Всі +filter.no_result=Ваш фільтр не дав результатів. +filter.container.tagged=З міткою +filter.container.untagged=Без мітки +published_by=%[1]s опубліковано %[3]s +published_by_in=%[1]s опубліковано %[3]s у %[5]s +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.version=Версія +search_in_external_registry=Шукати в %s +alpine.registry=Налаштуйте цей реєстр, додавши URL у ваш файл /etc/apk/repositories: +alpine.registry.key=Завантажте публічний ключ RSA реєстру в теку /etc/apk/keys/ для перевірки підпису індексу: +alpine.registry.info=Виберіть $branch та $repository зі списку нижче. +alpine.install=Щоб встановити пакет, виконайте наступну команду: +alpine.repository=Інформація про сховище alpine.repository.branches=Гілки alpine.repository.repositories=Репозиторії +alpine.repository.architectures=Архітектури +arch.registry=Додати сервер з відповідним сховищем та архітектурою до /etc/pacman.conf: +arch.install=Синхронізувати пакет з pacman: +arch.repository=Інформація про сховище arch.repository.repositories=Репозиторії -conan.details.repository=Репозиторій +arch.repository.architectures=Архітектури +cargo.registry=Налаштуйте цей реєстр у файлі конфігурації Cargo (наприклад, ~/.cargo/config.toml): +cargo.install=Щоб встановити пакет за допомогою Cargo, виконайте наступну команду: +chef.registry=Налаштуйте цей реєстр у вашому файлі ~/.chef/config.rb: +chef.install=Щоб встановити пакет, виконайте наступну команду: +composer.registry=Налаштуйте цей реєстр у вашому файлі ~/.composer/config.json: +composer.install=Щоб встановити пакет за допомогою Composer, виконайте наступну команду: +composer.dependencies=Залежності +composer.dependencies.development=Залежності розробки +conan.details.repository=Сховище +conan.registry=Налаштувати реєстр із командного рядка: +conan.install=Щоб встановити пакет за допомогою Conan, виконайте наступну команду: +conda.registry=Налаштуйте цей реєстр як сховище Conda у власному файлі .condarc: +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=Налаштуйте цей реєстр у вашому файлі Rprofile.site: +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=Налаштуйте цей реєстр у файлі pom.xml вашого проєкту: +maven.install=Щоб використати пакет, додайте наступне до блоку dependencies у файлі pom.xml: +maven.install2=Виконати з командного рядка: +maven.download=Щоб завантажити залежність, виконайте в командному рядку: +nuget.registry=Налаштувати реєстр із командного рядка: +nuget.install=Щоб встановити пакет за допомогою NuGet, запустіть наступну команду: +nuget.dependency.framework=Цільовий фреймворк +npm.registry=Налаштувати цей реєстр у файлі вашого проєкту .npmrc: +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=Додайте пакет у ваш файл Package.swift: +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.success=Успіх +status.failure=Невдача +status.cancelled=Скасовано +status.skipped=Пропущено +status.blocked=Заблоковано +runners.status=Статус 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.no_job=Робочий процес повинен містити принаймні одну задачу +runs.actor=Актор +runs.status=Статус +runs.actors_no_select=Усі актори +runs.status_no_select=Всі статуси +runs.no_results=Збігів немає. +runs.no_workflows.documentation=Для отримання додаткової інформації про Gitea Дії, перегляньте документацію. +runs.empty_commit_message=(порожнє повідомлення коміту) +runs.view_workflow_file=Перегляд файлу робочого процесу + +workflow.disable=Вимкнути робочий процес +workflow.disable_success=Робочий процес '%s' успішно вимкнено. +workflow.enable=Увімкнути робочий процес +workflow.enable_success=Робочий процес '%s' успішно ввімкнено. +workflow.run=Запустити робочий процес +workflow.not_found=Робочий процес '%s' не знайдено. +workflow.from_ref=Використати робочий процес з +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 b15d2c80a1..b88377cce8 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=测试 @@ -117,6 +117,7 @@ files=文件 error=错误 error404=您正尝试访问的页面 不存在您尚未被授权 查看该页面。 +error503=服务器无法完成您的请求,请稍后重试。 go_back=返回 invalid_data=无效数据: %v @@ -128,8 +129,9 @@ rss_feed=RSS 订阅源 pin=固定 unpin=取消置顶 -artifacts=制品 -confirm_delete_artifact=您确定要删除制品'%s'吗? +artifacts=产物 +expired=已过期 +confirm_delete_artifact=您确定要删除产物「%s」吗? archived=已归档 @@ -163,7 +165,7 @@ filter.public=公开 filter.private=私有 no_results_found=未找到结果 -internal_error_skipped=发生内部错误,但已被跳过: %s +internal_error_skipped=发生内部错误,但已跳过: %s [search] search=搜索... @@ -182,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=搜索合并请求... @@ -260,20 +262,20 @@ host=数据库主机 user=用户名 password=数据库用户密码 db_name=数据库名称 -db_schema=Schema +db_schema=架构 db_schema_helper=留空则数据库中默认值为("public")。 ssl_mode=SSL path=数据库文件路径 sqlite_helper=SQLite3 数据库的文件路径。
    如果以服务的方式运行 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=管理员用户名无效 @@ -294,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=日志文件将写入此目录。 @@ -302,12 +304,12 @@ optional_title=可选设置 email_title=电子邮箱设置 smtp_addr=SMTP 主机地址 smtp_port=SMTP 端口 -smtp_from=电子邮件发件人 -smtp_from_invalid=`"发送电子邮件为"地址无效` -smtp_from_helper=请输入一个用于 Gitea 的电子邮件地址,或者使用完整格式:"名称" +smtp_from=邮件发件人 +smtp_from_invalid=「邮件发件人」地址无效 +smtp_from_helper=请输入一个用于 Gitea 的邮箱地址,或者使用完整格式:「名称」。 mailer_user=SMTP 用户名 mailer_password=SMTP 密码 -register_confirm=需要发电子邮件确认注册 +register_confirm=需要邮件确认注册 mail_notify=启用邮件通知提醒 server_service_title=服务器和第三方服务设置 offline_mode=启用本地模式 @@ -332,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 @@ -346,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 算法相当安全,但使用大量内存,因此可能不适合小型系统。 @@ -376,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=显示已归档和未归档的 @@ -412,7 +414,7 @@ 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=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。 @@ -421,34 +423,35 @@ forgot_password=忘记密码? need_account=需要一个帐户? sign_up_now=还没账号?马上注册。 sign_up_successful=帐户创建成功。欢迎! -confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 %s请在下一个 %s 中检查您的收件箱以完成注册过程。 如果您的注册电子邮件地址不正确,您可以重新登录并更改它。 +confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 %s。请在下一个 %s 中检查您的收件箱以完成注册流程。 如果您的注册邮箱地址不正确,您可以重新登录并更改它。 must_change_password=更新您的密码 allow_password_change=要求用户更改密码(推荐) -reset_password_mail_sent_prompt=确认电子邮件已被发送到 %s。请您在 %s 内检查您的收件箱 ,完成密码重置过程。 +reset_password_mail_sent_prompt=确认邮件已被发送到 %s。请您在 %s 内检查您的收件箱 ,完成密码重置流程。 active_your_account=激活您的帐户 account_activated=帐户已激活 prohibit_login=禁止登录 prohibit_login_desc=您的帐户被禁止登录,请与网站管理员联系。 resent_limit_prompt=您请求发送激活邮件过于频繁,请等待 3 分钟后再试! has_unconfirmed_mail=%s 您好,系统检测到您有一封发送至 %s 但未被确认的邮件。如果您未收到激活邮件,或需要重新发送,请单击下方的按钮。 -change_unconfirmed_mail_address=如果您的注册电子邮件地址不正确,您可以在此更改并重新发送新的确认电子邮件。 +change_unconfirmed_mail_address=如果您的注册邮箱地址不正确,您可以在此更改并重新发送新的确认邮件。 resend_mail=单击此处重新发送确认邮件 email_not_associate=您输入的邮箱地址未被关联到任何帐号! send_reset_mail=发送账户恢复邮件 reset_password=账户恢复 invalid_code=此确认密钥无效或已过期。 -invalid_code_forgot_password=你的确认码无效或者已过期,点击 这里 开始新的会话。 +invalid_code_forgot_password=您的确认码无效或已过期,点击 这里 开始新的会话。 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 oauth_signup_tab=注册帐号 @@ -460,21 +463,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=因为检测到无效请求,授权失败。请尝试联系您授权应用的管理员。 @@ -498,17 +501,17 @@ activate_account.text_2=请在 %s 时间内,点击以下链接激活您 activate_email=请验证您的邮箱地址 activate_email.title=%s,请验证您的邮箱 -activate_email.text=请在 %s 时间内,点击以下链接,以验证你的电子邮件地址: +activate_email.text=请在 %s 时间内,点击以下链接,以验证您的邮箱地址: 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=如果此账户已为您创建,请先 设置您的密码。 reset_password=恢复您的账户 reset_password.title=%s,您已请求恢复您的帐户 -reset_password.text=请在 %s 时间内,点击以下链接,恢复你的账户: +reset_password.text=请在 %s 时间内,点击以下链接,以恢复您的账户: register_success=注册成功 @@ -530,26 +533,26 @@ issue.action.ready_for_review=@%[1]s 标记此合并请求已评审通过 issue.action.new=@%[1]s 创建了 #%[2]d. issue.in_tree_path=在 %s 中: -release.new.subject=%[2]s 中的 %[1]s 发布了 +release.new.subject=%[2]s 中的 %[1]s 已发布 release.new.text=@%[1]s 于 %[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=确认操作 @@ -589,9 +592,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` @@ -600,26 +603,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=用户 %s 与EMAIL_DOMAIN_ALLOWLIT 或 EMAIL_DOMAIN_BLOCKLIT 冲突。请确保您的操作是预期的。 -openid_been_used=OpenID 地址 "%s" 已被使用。 +openid_been_used=OpenID 地址「%s」已被使用。 username_password_incorrect=用户名或密码不正确。 password_complexity=密码未达到复杂程度要求: password_lowercase_one=至少一个小写字符 @@ -634,14 +637,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 @@ -674,27 +677,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=打开和评论工单或合并请求 @@ -727,18 +730,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=您的用户名已更改。 @@ -749,7 +752,7 @@ cancel=取消操作 language=界面语言 ui=主题 hidden_comment_types=隐藏的评论类型 -hidden_comment_types_description=此处选中的注释类型不会显示在问题页面中。比如,勾选”标签“删除所有 " 添加/删除的
- + +
+
+
+ + +

{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}

+
@@ -159,13 +166,6 @@ {{end}} -
-
- - -

{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}

-
-
@@ -227,13 +227,6 @@

{{ctx.Locale.Tr "admin.auths.allowed_domains_helper"}}

-
-
- - -

{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}

-
-
{{end}} @@ -247,13 +240,6 @@
-
-
- - -

{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}

-
-
{{end}} @@ -288,13 +274,6 @@ -
-
- - -

{{ctx.Locale.Tr "admin.auths.skip_local_two_fa_helper"}}

-
-
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index c04d332660..879b5cb550 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -9,7 +9,7 @@ {{.CsrfTokenHtml}}
- +
@@ -55,7 +55,7 @@
- +
@@ -63,7 +63,7 @@
- +
diff --git a/templates/base/alert.tmpl b/templates/base/alert.tmpl index 3f6d77a645..5ebe191771 100644 --- a/templates/base/alert.tmpl +++ b/templates/base/alert.tmpl @@ -18,3 +18,8 @@

{{.Flash.WarningMsg | SanitizeHTML}}

{{- end -}} +{{- if .ShowTwoFactorRequiredMessage -}} + +{{- end -}} 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 @@
-

+

{{AppName}}

-

{{ctx.Locale.Tr "startpage.app_desc"}}

+

{{ctx.Locale.Tr "startpage.app_desc"}}

@@ -16,7 +16,7 @@

{{svg "octicon-flame"}} {{ctx.Locale.Tr "startpage.install"}}

-

+

{{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"}}

@@ -24,7 +24,7 @@

{{svg "octicon-device-desktop"}} {{ctx.Locale.Tr "startpage.platform"}}

-

+

{{ctx.Locale.Tr "startpage.platform_desc" "https://go.dev/"}}

@@ -34,7 +34,7 @@

{{svg "octicon-rocket"}} {{ctx.Locale.Tr "startpage.lightweight"}}

-

+

{{ctx.Locale.Tr "startpage.lightweight_desc"}}

@@ -42,7 +42,7 @@

{{svg "octicon-code"}} {{ctx.Locale.Tr "startpage.license"}}

-

+

{{ctx.Locale.Tr "startpage.license_desc" "https://code.gitea.io/gitea" "code.gitea.io/gitea" "https://github.com/go-gitea/gitea"}}

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}}
- {{if .RenderedDescription}}
{{.RenderedDescription}}
{{end}} + {{if .RenderedDescription}}
{{.RenderedDescription}}
{{end}}
{{if .Org.Location}}
{{svg "octicon-location"}} {{.Org.Location}}
{{end}} {{if .Org.Website}}
{{svg "octicon-link"}} {{.Org.Website}}
{{end}} diff --git a/templates/org/settings/options.tmpl b/templates/org/settings/options.tmpl index 76315f3eac..f4583bbe36 100644 --- a/templates/org/settings/options.tmpl +++ b/templates/org/settings/options.tmpl @@ -12,7 +12,7 @@
{{ctx.Locale.Tr "org.settings.change_orgname_prompt"}}
{{ctx.Locale.Tr "org.settings.change_orgname_redirect_prompt"}} - +
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 @@
-
{{range .PackageDescriptor.Files}}{{if eq .File.LowerName "manifest.json"}}{{.Properties.GetByName "container.digest"}}{{end}}{{end}}
+
+
+

+							{{- range .PackageDescriptor.Files -}}
+								{{- if eq .File.LowerName "manifest.json" -}}
+									{{- .Properties.GetByName "container.digest" -}}{{"\n"}}
+								{{- end -}}
+							{{- end -}}
+						
+
+
@@ -39,7 +49,11 @@ {{/* "unknown/unknown" is attestation-manifest, so we should skip it */}} {{if ne .Platform "unknown/unknown"}} - {{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}} + + + {{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}} + + {{.Platform}} {{FileSize .Size}} @@ -55,12 +69,24 @@ {{.PackageDescriptor.Metadata.Description}}
{{end}} - {{if .PackageDescriptor.Metadata.ImageLayers}} -

{{ctx.Locale.Tr "packages.container.layers"}}

+ + {{/* 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}} +

+ {{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}} + + ({{svg "octicon-cpu" 12}} {{.ContainerImageMetadata.Platform}}) + + {{end}} +

- +
- {{range .PackageDescriptor.Metadata.ImageLayers}} + {{range $imageMetadata.ImageLayers}} @@ -69,10 +95,10 @@
{{.}}
{{end}} - {{if .PackageDescriptor.Metadata.Labels}} + {{if $imageMetadata.Labels}}

{{ctx.Locale.Tr "packages.container.labels"}}

- +
@@ -80,7 +106,7 @@ - {{range $key, $value := .PackageDescriptor.Metadata.Labels}} + {{range $key, $value := $imageMetadata.Labels}} 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 @@
-
pip install --index-url  {{.PackageDescriptor.Package.Name}}
+
pip install --index-url  --extra-index-url https://pypi.org/ {{.PackageDescriptor.Package.Name}}
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 @@
+ {{$packageVersionLink := print $.PackageDescriptor.PackageWebLink "/" (PathEscape .PackageDescriptor.Version.LowerVersion)}}

{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})

{{$timeStr := DateUtils.TimeSince .PackageDescriptor.Version.CreatedUnix}} @@ -9,8 +10,8 @@ {{end}}
-
-
+
+
{{template "package/content/alpine" .}} {{template "package/content/arch" .}} {{template "package/content/cargo" .}} @@ -34,7 +35,7 @@ {{template "package/content/swift" .}} {{template "package/content/vagrant" .}}
-
+
{{ctx.Locale.Tr "packages.details"}}
{{svg .PackageDescriptor.Package.Type.SVGName}} {{.PackageDescriptor.Package.Type.Name}}
@@ -74,8 +75,8 @@
{{range .PackageDescriptor.Files}}
- {{.File.Name}} - {{FileSize .Blob.Size}} + {{.File.Name}} + {{FileSize .Blob.Size}}
{{end}}
@@ -98,7 +99,7 @@
{{svg "octicon-issue-opened"}} {{ctx.Locale.Tr "repo.issues"}}
{{end}} {{if .CanWritePackages}} -
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
+
{{svg "octicon-tools"}} {{ctx.Locale.Tr "repo.settings"}}
{{end}}
{{end}} diff --git a/templates/repo/actions/runs_list.tmpl b/templates/repo/actions/runs_list.tmpl index fa1adb3e3b..fdb631f0ee 100644 --- a/templates/repo/actions/runs_list.tmpl +++ b/templates/repo/actions/runs_list.tmpl @@ -5,36 +5,50 @@

{{if $.IsFiltered}}{{ctx.Locale.Tr "actions.runs.no_results"}}{{else}}{{ctx.Locale.Tr "actions.runs.no_runs"}}{{end}}

{{end}} - {{range .Runs}} + {{range $run := .Runs}}
- {{template "repo/actions/status" (dict "status" .Status.String)}} + {{template "repo/actions/status" (dict "status" $run.Status.String)}}
- - {{if .Title}}{{.Title}}{{else}}{{ctx.Locale.Tr "actions.runs.empty_commit_message"}}{{end}} + + {{or $run.Title (ctx.Locale.Tr "actions.runs.empty_commit_message")}}
- {{if not $.CurWorkflow}}{{.WorkflowID}} {{end}}#{{.Index}}: - {{- if .ScheduleID -}} + {{if not $.CurWorkflow}}{{$run.WorkflowID}} {{end}}#{{$run.Index}}: + {{- if $run.ScheduleID -}} {{ctx.Locale.Tr "actions.runs.scheduled"}} {{- else -}} {{ctx.Locale.Tr "actions.runs.commit"}} - {{ShortSha .CommitSHA}} + {{ShortSha $run.CommitSHA}} {{ctx.Locale.Tr "actions.runs.pushed_by"}} - {{.TriggerUser.GetDisplayName}} + {{$run.TriggerUser.GetDisplayName}} {{- end -}}
- {{if .IsRefDeleted}} - {{.PrettyRef}} + {{if $run.IsRefDeleted}} + {{$run.PrettyRef}} {{else}} - {{.PrettyRef}} + {{$run.PrettyRef}} {{end}}
-
{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}
-
{{svg "octicon-stopwatch" 16}}{{.Duration}}
+
{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince $run.Updated}}
+
{{svg "octicon-stopwatch" 16}}{{$run.Duration}}
+
+
diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index 8d1de41f70..d0741cdc0b 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -19,6 +19,7 @@ data-locale-status-skipped="{{ctx.Locale.Tr "actions.status.skipped"}}" data-locale-status-blocked="{{ctx.Locale.Tr "actions.status.blocked"}}" data-locale-artifacts-title="{{ctx.Locale.Tr "artifacts"}}" + data-locale-artifact-expired="{{ctx.Locale.Tr "expired"}}" data-locale-confirm-delete-artifact="{{ctx.Locale.Tr "confirm_delete_artifact"}}" data-locale-show-timestamps="{{ctx.Locale.Tr "show_timestamps"}}" data-locale-show-log-seconds="{{ctx.Locale.Tr "show_log_seconds"}}" diff --git a/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 @@
{{end}}
- + {{/* use autofocus here to prevent the "branch selection" dropdown from getting focus, otherwise it will auto popup */}} +
{{end}} {{range .workflows}} diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 19797229bf..fffe3a08cc 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -20,14 +20,14 @@
{{/* FIXME: here and below, the tw-overflow-visible is not quite right but it is still needed the moment: to show the important buttons when the width is narrow */}}
{{ctx.Locale.Tr "packages.container.labels.key"}}
{{$key}} {{$value}}
- {{.DefaultBranchBranch.DBBranch.Name}} + {{.DefaultBranchBranch.DBBranch.Name}} {{if .DefaultBranchBranch.IsProtected}} {{svg "octicon-shield-lock"}} {{end}} {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DefaultBranchBranch.DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DefaultBranchBranch.DBBranch.CommitID)}}
-

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage (.Repository.ComposeCommentMetas ctx)}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}

+

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DefaultBranchBranch.DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DefaultBranchBranch.DBBranch.CommitMessage .Repository}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DefaultBranchBranch.DBBranch.CommitTime}}{{if .DefaultBranchBranch.DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DefaultBranchBranch.DBBranch.Pusher}}{{template "shared/user/namelink" .DefaultBranchBranch.DBBranch.Pusher}}{{end}}

@@ -90,20 +90,20 @@ {{if .DBBranch.IsDeleted}}
- {{.DBBranch.Name}} + {{.DBBranch.Name}}

{{ctx.Locale.Tr "repo.branch.deleted_by" .DBBranch.DeletedBy.Name}} {{DateUtils.TimeSince .DBBranch.DeletedUnix}}

{{else}}
- {{.DBBranch.Name}} + {{.DBBranch.Name}} {{if .IsProtected}} {{svg "octicon-shield-lock"}} {{end}} {{template "repo/commit_statuses" dict "Status" (index $.CommitStatus .DBBranch.CommitID) "Statuses" (index $.CommitStatuses .DBBranch.CommitID)}}
-

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage ($.Repository.ComposeCommentMetas ctx)}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}}  {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}

+

{{svg "octicon-git-commit" 16 "tw-mr-1"}}{{ShortSha .DBBranch.CommitID}} · {{ctx.RenderUtils.RenderCommitMessage .DBBranch.CommitMessage $.Repository}} · {{ctx.Locale.Tr "org.repo_updated"}} {{DateUtils.TimeSince .DBBranch.CommitTime}}{{if .DBBranch.Pusher}}  {{template "shared/user/avatarlink" dict "user" .DBBranch.Pusher}}  {{template "shared/user/namelink" .DBBranch.Pusher}}{{end}}

{{end}}
diff --git a/templates/repo/branch_dropdown.tmpl b/templates/repo/branch_dropdown.tmpl index 261d0ea409..36dc047c23 100644 --- a/templates/repo/branch_dropdown.tmpl +++ b/templates/repo/branch_dropdown.tmpl @@ -47,16 +47,20 @@ Search "repo/branch_dropdown" in the template directory to find all occurrences. > {{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}