diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 048ca393d1b..0dd9a6687d8 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -16,10 +16,10 @@ parserOptions: parser: "@typescript-eslint/parser" # for vue plugin - https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser settings: - import/extensions: [".js", ".ts"] - import/parsers: + import-x/extensions: [".js", ".ts"] + import-x/parsers: "@typescript-eslint/parser": [".js", ".ts"] - import/resolver: + import-x/resolver: typescript: true plugins: @@ -28,7 +28,7 @@ plugins: - "@typescript-eslint/eslint-plugin" - eslint-plugin-array-func - eslint-plugin-github - - eslint-plugin-i + - eslint-plugin-import-x - eslint-plugin-no-jquery - eslint-plugin-no-use-extend-native - eslint-plugin-regexp @@ -58,15 +58,15 @@ overrides: no-restricted-globals: [2, addEventListener, blur, close, closed, confirm, defaultStatus, defaultstatus, error, event, external, find, focus, frameElement, frames, history, innerHeight, innerWidth, isFinite, isNaN, length, locationbar, menubar, moveBy, moveTo, name, onblur, onerror, onfocus, onload, onresize, onunload, open, opener, opera, outerHeight, outerWidth, pageXOffset, pageYOffset, parent, print, removeEventListener, resizeBy, resizeTo, screen, screenLeft, screenTop, screenX, screenY, scroll, scrollbars, scrollBy, scrollTo, scrollX, scrollY, status, statusbar, stop, toolbar, top] - files: ["*.config.*"] rules: - i/no-unused-modules: [0] + import-x/no-unused-modules: [0] - files: ["**/*.d.ts"] rules: - i/no-unused-modules: [0] + import-x/no-unused-modules: [0] "@typescript-eslint/consistent-type-definitions": [0] "@typescript-eslint/consistent-type-imports": [0] - files: ["web_src/js/types.ts"] rules: - i/no-unused-modules: [0] + import-x/no-unused-modules: [0] - files: ["**/*.test.*", "web_src/js/test/setup.ts"] env: vitest-globals/env: true @@ -394,49 +394,49 @@ rules: id-blacklist: [0] id-length: [0] id-match: [0] - i/consistent-type-specifier-style: [0] - i/default: [0] - i/dynamic-import-chunkname: [0] - i/export: [2] - i/exports-last: [0] - i/extensions: [2, always, {ignorePackages: true}] - i/first: [2] - i/group-exports: [0] - i/max-dependencies: [0] - i/named: [2] - i/namespace: [0] - i/newline-after-import: [0] - i/no-absolute-path: [0] - i/no-amd: [2] - i/no-anonymous-default-export: [0] - i/no-commonjs: [2] - i/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}] - i/no-default-export: [0] - i/no-deprecated: [0] - i/no-dynamic-require: [0] - i/no-empty-named-blocks: [2] - i/no-extraneous-dependencies: [2] - i/no-import-module-exports: [0] - i/no-internal-modules: [0] - i/no-mutable-exports: [0] - i/no-named-as-default-member: [0] - i/no-named-as-default: [0] - i/no-named-default: [0] - i/no-named-export: [0] - i/no-namespace: [0] - i/no-nodejs-modules: [0] - i/no-relative-packages: [0] - i/no-relative-parent-imports: [0] - i/no-restricted-paths: [0] - i/no-self-import: [2] - i/no-unassigned-import: [0] - i/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}] - i/no-unused-modules: [2, {unusedExports: true}] - i/no-useless-path-segments: [2, {commonjs: true}] - i/no-webpack-loader-syntax: [2] - i/order: [0] - i/prefer-default-export: [0] - i/unambiguous: [0] + import-x/consistent-type-specifier-style: [0] + import-x/default: [0] + import-x/dynamic-import-chunkname: [0] + import-x/export: [2] + import-x/exports-last: [0] + import-x/extensions: [2, always, {ignorePackages: true}] + import-x/first: [2] + import-x/group-exports: [0] + import-x/max-dependencies: [0] + import-x/named: [2] + import-x/namespace: [0] + import-x/newline-after-import: [0] + import-x/no-absolute-path: [0] + import-x/no-amd: [2] + import-x/no-anonymous-default-export: [0] + import-x/no-commonjs: [2] + import-x/no-cycle: [2, {ignoreExternal: true, maxDepth: 1}] + import-x/no-default-export: [0] + import-x/no-deprecated: [0] + import-x/no-dynamic-require: [0] + import-x/no-empty-named-blocks: [2] + import-x/no-extraneous-dependencies: [2] + import-x/no-import-module-exports: [0] + import-x/no-internal-modules: [0] + import-x/no-mutable-exports: [0] + import-x/no-named-as-default-member: [0] + import-x/no-named-as-default: [0] + import-x/no-named-default: [0] + import-x/no-named-export: [0] + import-x/no-namespace: [0] + import-x/no-nodejs-modules: [0] + import-x/no-relative-packages: [0] + import-x/no-relative-parent-imports: [0] + import-x/no-restricted-paths: [0] + import-x/no-self-import: [2] + import-x/no-unassigned-import: [0] + import-x/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$"]}] + import-x/no-unused-modules: [2, {unusedExports: true}] + import-x/no-useless-path-segments: [2, {commonjs: true}] + import-x/no-webpack-loader-syntax: [2] + import-x/order: [0] + import-x/prefer-default-export: [0] + import-x/unambiguous: [0] init-declarations: [0] line-comment-position: [0] logical-assignment-operators: [0] @@ -818,7 +818,7 @@ rules: unicorn/consistent-destructuring: [2] unicorn/consistent-empty-array-spread: [2] unicorn/consistent-existence-index-check: [0] - unicorn/consistent-function-scoping: [2] + unicorn/consistent-function-scoping: [0] unicorn/custom-error-definition: [0] unicorn/empty-brace-spaces: [2] unicorn/error-message: [0] diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 023fb05a296..2adea23aa40 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -3,3 +3,5 @@ self-hosted-runner: - actuated-4cpu-8gb - actuated-4cpu-16gb - nscloud + - namespace-profile-gitea-release-docker + - namespace-profile-gitea-release-binary diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index 6e1b6e07584..2264c9e822d 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -10,7 +10,7 @@ concurrency: jobs: nightly-binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -58,7 +58,7 @@ jobs: run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress nightly-docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -95,7 +95,7 @@ jobs: push: true tags: gitea/gitea:${{ steps.clean_name.outputs.branch }} nightly-docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions diff --git a/.github/workflows/release-tag-rc.yml b/.github/workflows/release-tag-rc.yml index 41037df29cb..a406602dc0a 100644 --- a/.github/workflows/release-tag-rc.yml +++ b/.github/workflows/release-tag-rc.yml @@ -11,7 +11,7 @@ concurrency: jobs: binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -68,7 +68,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -99,7 +99,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions diff --git a/.github/workflows/release-tag-version.yml b/.github/workflows/release-tag-version.yml index a23e6982000..f67b76f4087 100644 --- a/.github/workflows/release-tag-version.yml +++ b/.github/workflows/release-tag-version.yml @@ -13,7 +13,7 @@ concurrency: jobs: binary: - runs-on: nscloud + runs-on: namespace-profile-gitea-release-binary steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -70,7 +70,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} docker-rootful: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions @@ -105,7 +105,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} docker-rootless: - runs-on: ubuntu-latest + runs-on: namespace-profile-gitea-release-docker steps: - uses: actions/checkout@v4 # fetch all commits instead of only the last as some branches are long lived and could have many between versions diff --git a/.gitignore b/.gitignore index 55644b17b57..86e6e4fefdc 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,7 @@ _testmain.go *.exe *.test *.prof -*.tsbuildInfo +*.tsbuildinfo *coverage.out coverage.all diff --git a/.golangci.yml b/.golangci.yml index 37617ad3652..c39d7ac5f2f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,6 +19,8 @@ linters: - revive - staticcheck - stylecheck + - tenv + - testifylint - typecheck - unconvert - unused @@ -34,6 +36,10 @@ output: show-stats: true linters-settings: + testifylint: + disable: + - go-require + - require-error stylecheck: checks: ["all", "-ST1005", "-ST1003"] nakedret: diff --git a/CHANGELOG.md b/CHANGELOG.md index 04fffd4a4e3..ab8c2ac2234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,399 @@ 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.22.4](https://github.com/go-gitea/gitea/releases/tag/v1.22.4) - 2024-11-14 + +* SECURITY + * Fix basic auth with webauthn (#32531) (#32536) + * Refactor internal routers (partial backport, auth token const time comparing) (#32473) (#32479) +* PERFORMANCE + * Remove transaction for archive download (#32186) (#32520) +* BUGFIXES + * Fix `missing signature key` error when pulling Docker images with `SERVE_DIRECT` enabled (#32365) (#32397) + * Fix get reviewers fails when selecting user without pull request permissions unit (#32415) (#32616) + * Fix adding index files to tmp directory (#32360) (#32593) + * Fix PR creation on forked repositories via API (#31863) (#32591) + * Fix missing menu tabs in organization project view page (#32313) (#32592) + * Support HTTP POST requests to `/userinfo`, aligning to OpenID Core specification (#32578) (#32594) + * Fix debian package clean up cron job (#32351) (#32590) + * Fix GetInactiveUsers (#32540) (#32588) + * Allow the actions user to login via the jwt token (#32527) (#32580) + * Fix submodule parsing (#32571) (#32577) + * Refactor find forks and fix possible bugs that weaken permissions check (#32528) (#32547) + * Fix some places that don't respect org full name setting (#32243) (#32550) + * Refactor push mirror find and add check for updating push mirror (#32539) (#32549) + * Fix basic auth with webauthn (#32531) (#32536) + * Fix artifact v4 upload above 8MB (#31664) (#32523) + * Fix oauth2 error handle not return immediately (#32514) (#32516) + * Fix action not triggered when commit message is too long (#32498) (#32507) + * Fix `GetRepoLink` nil pointer dereference on dashboard feed page when repo is deleted with actions enabled (#32501) (#32502) + * Fix `missing signature key` error when pulling Docker images with `SERVE_DIRECT` enabled (#32397) (#32397) + * Fix the permission check for user search API and limit the number of returned users for `/user/search` (#32310) + * Fix SearchIssues swagger docs (#32208) (#32298) + * Fix dropdown content overflow (#31610) (#32250) + * Disable Oauth check if oauth disabled (#32368) (#32480) + * Respect renamed dependencies of Cargo registry (#32430) (#32478) + * Fix mermaid diagram height when initially hidden (#32457) (#32464) + * Fix broken releases when re-pushing tags (#32435) (#32449) + * Only provide the commit summary for Discord webhook push events (#32432) (#32447) + * Only query team tables if repository is under org when getting assignees (#32414) (#32426) + * Fix created_unix for mirroring (#32342) (#32406) + * Respect UI.ExploreDefaultSort setting again (#32357) (#32385) + * Fix broken image when editing comment with non-image attachments (#32319) (#32345) + * Fix disable 2fa bug (#32320) (#32330) + * Always update expiration time when creating an artifact (#32281) (#32285) + * Fix null errors on conversation holder (#32258) (#32266) (#32282) + * Only rename a user when they should receive a different name (#32247) (#32249) + * Fix checkbox bug on private/archive filter (#32236) (#32240) + * Add a doctor check to disable the "Actions" unit for mirrors (#32424) (#32497) + * Quick fix milestone deadline 9999 (#32423) + * Make `show stats` work when only one file changed (#32244) (#32268) + * Make `owner/repo/pulls` handlers use "PR reader" permission (#32254) (#32265) + * Update scheduled tasks even if changes are pushed by "ActionsUser" (#32246) (#32252) +* MISC + * Remove unnecessary code: `GetPushMirrorsByRepoID` called on all repo pages (#32560) (#32567) + * Improve some sanitizer rules (#32534) + * Update nix development environment vor v1.22.x (#32495) + * Add warn log when deleting inactive users (#32318) (#32321) + * Update github.com/go-enry/go-enry to v2.9.1 (#32295) (#32296) + * Warn users when they try to use a non-root-url to sign in/up (#32272) (#32273) + +## [1.22.3](https://github.com/go-gitea/gitea/releases/tag/v1.22.3) - 2024-10-08 + +* SECURITY + * Fix bug when a token is given public only (#32204) (#32218) +* PERFORMANCE + * Increase `cacheContextLifetime` to reduce false reports (#32011) (#32023) + * Don't join repository when loading action table data (#32127) (#32143) +* BUGFIXES + * Fix javascript error when an anonymous user visits migration page (#32144) (#32179) + * Don't init signing keys if oauth2 provider is disabled (#32177) + * Fix wrong status of `Set up Job` when first step is skipped (#32120) (#32125) + * Fix bug when deleting a migrated branch (#32075) (#32123) + * Truncate commit message during Discord webhook push events (#31970) (#32121) + * Allow to set branch protection in an empty repository (#32095) (#32119) + * Fix panic when cloning with wrong ssh format. (#32076) (#32118) + * Fix rename branch permission bug (#32066) (#32108) + * Fix: database not update release when using `git push --tags --force` (#32040) (#32074) + * Add missing comment reply handling (#32050) (#32065) + * Do not escape relative path in RPM primary index (#32038) (#32054) + * Fix `/repos/{owner}/{repo}/pulls/{index}/files` endpoint not populating `previous_filename` (#32017) (#32028) + * Support allowed hosts for migrations to work with proxy (#32025) (#32026) + * Fix the logic of finding the latest pull review commit ID (#32139) (#32165) + * Fix bug in getting merged pull request by commit (#32079) (#32117) + * Fix wrong last modify time (#32102) (#32104) + * Fix incorrect `/tokens` api (#32085) (#32092) + * Handle invalid target when creating releases using API (#31841) (#32043) + * Check if the `due_date` is nil when editing issues (#32035) (#32042) + * Fix container parallel upload bugs (#32022) + * Fixed race condition when deleting documents by repoId in ElasticSearch (#32185) (#32188) + * Refactor CSRF protector (#32057) (#32069) + * Fix Bug in Issue/pulls list (#32081) (#32115) + * Include collaboration repositories on dashboard source/forks/mirrors list (#31946) (#32122) + * Add null check for responseData.invalidTopics (#32212) (#32217) +* TESTING + * Fix mssql ci with a new mssql version on ci (#32094) +* MISC + * Upgrade some dependencies include minio-go (#32166) + * Add bin to Composer Metadata (#32099) (#32106) + * Lazy load avatar images (#32051) (#32063) + * Upgrade cache to v0.2.1 (#32003) (#32009) + +## [1.22.2](https://github.com/go-gitea/gitea/releases/tag/v1.22.2) - 2024-08-28 + +* Security + * Replace v-html with v-text in search inputbox (#31966) (#31973) + * Fix nuget/conan/container packages upload bugs (#31967) (#31982) +* PERFORMANCE + * Refactor the usage of batch catfile (#31754) (#31889) +* BUGFIXES + * Fix overflowing content in action run log (#31842) (#31853) + * Scroll images in project issues separately from the remaining issue (#31683) (#31823) + * Add `:focus-visible` style to buttons (#31799) (#31819) + * Fix the display of project type for deleted projects (#31732) (#31734) + * Fix API owner ID should be zero when created repo secret (#31715) (#31811) + * Set owner id to zero when GetRegistrationToken for repo (#31725) (#31729) + * Fix API endpoint for registration-token (#31722) (#31728) + * Add permission check when creating PR (#31033) (#31720) + * Don't return 500 if mirror url contains special chars (#31859) (#31895) + * Fix agit automerge (#31207) (#31881) + * Add CfTurnstileSitekey context data to all captcha templates (#31874) (#31876) + * Avoid returning without written ctx when posting PR (#31843) (#31848) + * Fix raw wiki links (#31825) (#31845) + * Fix panic of ssh public key page after deletion of auth source (#31829) (#31836) + * Fixes for unreachable project issues when transfer repository from organization (#31770) (#31828) + * Show lock owner instead of repo owner on LFS setting page (#31788) (#31817) + * Fix `IsObjectExist` with gogit (#31790) (#31806) + * Fix protected branch files detection on pre_receive hook (#31778) (#31796) + * Add `TAGS` to `TEST_TAGS` and fix bugs found with gogit (#31791) (#31795) + * Rename head branch of pull requests when renaming a branch (#31759) (#31774) + * Fix wiki revision pagination (#31760) (#31772) + * Bump vue-bar-graph (#31705) (#31753) + * Distinguish LFS object errors to ignore missing objects during migration (#31702) (#31745) + * Make GetRepositoryByName more safer (#31712) (#31718) + * Fix a branch divergence cache bug (#31659) (#31661) + * Allow org team names of length 255 in create team form (#31564) (#31603) + * Use old behavior for telegram webhook (#31588) + * Bug fix for translation in ru (#31892) + * Fix actions notify bug (#31866) (#31875) + * Fix the component of access token list not mounted (#31824) (#31868) + * Add missing repository type filter parameters to pager (#31832) (#31837) + * Fix dates displaying in a wrong manner when we're close to the end of… (#31750) + * Fix "Filter by commit" Dropdown (#31695) (#31696) + * Properly filter issue list given no assignees filter (#31522) (#31685) + * Prevent update pull refs manually and will not affect other refs update (#31931)(#31955) + * Fix sort order for organization home and user profile page (#31921) (#31922) + * Fix search team (#31923) (#31942) + * Fix 500 error when state params is set when editing issue/PR by API (#31880) (#31952) + * Fix index too many file names bug (#31903) (#31953) + * Add lock for parallel maven upload (#31851) (#31954) +* MISC + * Remove "dsa-1024" testcases from Test_SSHParsePublicKey and Test_calcFingerprint (#31905) (#31914) + * Upgrade bleve to 2.4.2 (#31894) + * Remove unneccessary uses of `word-break: break-all` (#31637) (#31652) + * Return an empty string when a repo has no avatar in the repo API (#31187) (#31567) + * Upgrade micromatch to 4.0.8 (#31944) + * Update webpack to 5.94.0 (#31941) + +## [1.22.1](https://github.com/go-gitea/gitea/releases/tag/v1.22.1) - 2024-07-04 + +* SECURITY + * Add replacement module for `mholt/archiver` (#31267) (#31270) +* API + * Fix missing images in editor preview due to wrong links (#31299) (#31393) + * Fix duplicate sub-path for avatars (#31365) (#31368) + * Reduce memory usage for chunked artifact uploads to MinIO (#31325) (#31338) + * Remove sub-path from container registry realm (#31293) (#31300) + * Fix NuGet Package API for $filter with Id equality (#31188) (#31242) + * Add an immutable tarball link to archive download headers for Nix (#31139) (#31145) + * Add missed return after `ctx.ServerError` (#31130) (#31133) +* BUGFIXES + * Fix avatar radius problem on the new issue page (#31506) (#31508) + * Fix overflow menu flickering on mobile (#31484) (#31488) + * Fix poor table column width due to breaking words (#31473) (#31477) + * Support relative paths to videos from Wiki pages (#31061) (#31453) + * Fix new issue/pr avatar (#31419) (#31424) + * Increase max length of org team names from 30 to 255 characters (#31410) (#31421) + * Fix line number width in code preview (#31307) (#31316) + * Optimize runner-tags layout to enhance visual experience (#31258) (#31263) + * Fix overflow on push notification (#31179) (#31238) + * Fix overflow on notifications (#31178) (#31237) + * Fix overflow in issue card (#31203) (#31225) + * Split sanitizer functions and fine-tune some tests (#31192) (#31200) + * use correct l10n string (#31487) (#31490) + * Fix dropzone JS error when attachment is disabled (#31486) + * Fix web notification icon not updated once you read all notifications (#31447) (#31466) + * Switch to "Write" tab when edit comment again (#31445) (#31461) + * Fix the link for .git-blame-ignore-revs bypass (#31432) (#31442) + * Fix the wrong line number in the diff view page when expanded twice. (#31431) (#31440) + * Fix labels and projects menu overflow on issue page (#31435) (#31439) + * Fix Account Linking UpdateMigrationsByType (#31428) (#31434) + * Fix markdown math brackets render problem (#31420) (#31430) + * Fix rendered wiki page link (#31398) (#31407) + * Fix natural sort (#31384) (#31394) + * Allow downloading attachments of draft releases (#31369) (#31380) + * Fix repo graph JS (#31377) + * Fix incorrect localization `explorer.go` (#31348) (#31350) + * Fix hash render end with colon (#31319) (#31346) + * Fix line number widths (#31341) (#31343) + * Fix navbar `+` menu flashing on page load (#31281) (#31342) + * Fix adopt repository has empty object name in database (#31333) (#31335) + * Delete legacy cookie before setting new cookie (#31306) (#31317) + * Fix some URLs whose sub-path is missing (#31289) (#31292) + * Fix admin oauth2 custom URL settings (#31246) (#31247) + * Make pasted "img" tag has the same behavior as markdown image (#31235) (#31243) + * Fix agit checkout command line hint & fix ShowMergeInstructions checking (#31219) (#31222) + * Fix the possible migration failure on 286 with postgres 16 (#31209) (#31218) + * Fix branch order (#31174) (#31193) + * Fix markup preview (#31158) (#31166) + * Fix push multiple branches error with tests (#31151) (#31153) + * Fix API repository object format missed (#31118) (#31132) + * Fix missing memcache import (#31105) (#31109) + * Upgrade `github.com/hashicorp/go-retryablehttp` (#31499) + * Fix double border in system status table (#31363) (#31401) + * Fix bug filtering issues which have no project (#31337) (#31367) + * Fix #31185 try fix lfs download from bitbucket failed (#31201) (#31329) + * Add nix flake for dev shell (#30967) (#31310) + * Fix and clean up `ConfirmModal` (#31283) (#31291) + * Optimize repo-list layout to enhance visual experience (#31272) (#31276) + * fixed the dropdown menu for the top New button to expand to the left (#31273) (#31275) + * Fix Activity Page Contributors dropdown (#31264) (#31269) + * fix: allow actions artifacts storage migration to complete succesfully (#31251) (#31257) + * Make blockquote attention recognize more syntaxes (#31240) (#31250) + * Remove .segment from .project-column (#31204) (#31239) + * Ignore FindRecentlyPushedNewBranches err (#31164) (#31171) + * Use vertical layout for multiple code expander buttons (#31122) (#31152) + * Remove duplicate `ProxyPreserveHost` in Apache httpd doc (#31143) (#31147) + * Improve mobile review ui (#31091) (#31136) + * Fix DashboardRepoList margin (#31121) (#31128) + * Update pip related commands for docker (#31106) (#31111) + +## [1.22.0](https://github.com/go-gitea/gitea/releases/tag/v1.22.0) - 2024-05-27 + +This release stands as a monumental milestone in our development journey with a record-breaking incorporation of [1528](https://github.com/go-gitea/gitea/pulls?q=is%3Apr+milestone%3A1.22.0+is%3Amerged) pull requests. It marks the most extensive update in Gitea's history, showcasing a plethora of new features and infrastructure improvements. + +Noteworthy advancements in this release include the introduction of `HTMX` and `Tailwind`, signaling a strategic shift as we gradually phase out `jquery` and `Fomantic UI`. These changes reflect our commitment to embracing modern technologies and enhancing the user experience. + +Key highlights of this release encompass significant changes categorized under `BREAKING`, `FEATURES`, `ENHANCEMENTS`, and `PERFORMANCE`, each contributing to a more robust and efficient Gitea platform. + +* BREAKING + * Improve reverse proxy documents and clarify the AppURL guessing behavior (#31003) (#31020) + * Remember log in for a month by default (#30150) + * Breaking summary for template refactoring (#29395) + * All custom templates need to follow these changes + * Recommend/convert to use case-sensitive collation for MySQL/MSSQL (#28662) + * Make offline mode as default to not connect external avatar service by default (#28548) + * Include public repos in the doer's dashboard for issue search (#28304) + * Use restricted sanitizer for repository description (#28141) + * Support storage base path as prefix (#27827) + * Enhanced auth token / remember me (#27606) + * Rename the default themes to `gitea-light`, `gitea-dark`, `gitea-auto` (#27419) + * If you didn't see the new themes, please remove the `[ui].THEMES` config option from `app.ini` + * Require MySQL 8.0, PostgreSQL 12, MSSQL 2012 (#27337) +* FEATURES + * Allow everyone to read or write a wiki by a repo unit setting (#30495) + * Use raw Wiki links for non-renderable Wiki files (#30273) + * Render embedded code preview by permalink in markdown (#30234) (#30249) + * Support repo code search without setting up an indexer (#29998) + * Support pasting URLs over markdown text (#29566) + * Allow to change primary email before account activation (#29412) + * Customizable "Open with" applications for repository clone (#29320) + * Allow options to disable user deletion from the interface on app.ini (#29275) + * Extend issue template YAML engine (#29274) + * Add support for `linguist-detectable` and `linguist-documentation` (#29267) + * Implement code frequency graph (#29191) + * Show commit status for releases (#29149) + * Add user blocking (#29028) + * Actions Artifacts v4 backend (#28965) + * Add merge style `fast-forward-only` (#28954) + * Retarget depending pulls when the parent branch is deleted (#28686) + * Add global setting on how timestamps should be rendered (#28657) + * Implement actions badge SVGs (#28102) + * Add skip ci functionality (#28075) + * Show latest commit for file (#28067) + * Allow to sync tags from the admin dashboard (#28045) + * Add Profile Readme for Organisations (#27955) + * Implement contributors graph (#27882) + * Artifact deletion in actions ui (#27172) + * Add API routes to get runner registration token (#27144) + * Add support for forking single branch (#25821) + * Add support for sha256 repositories (#23894) + * Add admin API route for managing user's badges (#23106) +* ENHANCEMENTS + * Make gitea webhooks openproject compatible (#28435) (#31081) + * Support using label names when changing issue labels (#30943) (#30958) + * Fix various problems around project board view (#30696) (#30902) + * Improve context popup rendering (#30824) (#30829) + * Allow to save empty comment (#30706) + * Prevent allow/reject reviews on merged/closed PRs (#30686) + * Initial support for colorblindness-friendly themes (#30625) + * Some NuGet package enhancements (#30280) (#30324) + * Markup color and font size fixes (#30282) (#30310) + * Show 12 lines in markup code preview (#30255) (#30257) + * Add `[other].SHOW_FOOTER_POWERED_BY` setting to hide `Powered by` (#30253) + * Pulse page improvements (#30149) + * Render code tags in commit messages (#30146) + * Prevent re-review and dismiss review actions on closed and merged PRs (#30065) + * Cancel previous runs of the same PR automatically (#29961) + * Drag-and-drop improvements for projects and issue pins (#29875) + * Add default board to new projects, remove uncategorized pseudo-board (#29874) + * Prevent layout shift in `` items (#29831) + * Add skip ci support for pull request title (#29774) + * Add more stats tables (#29730) + * Update API to return 'source_id' for users (#29718) + * Determine fuzziness of bleve indexer by keyword length (#29706) + * Expose fuzzy search for issues/pulls (#29701) + * Put an edit file button on pull request files to allow a quick operation (#29697) + * Fix action runner offline label padding (#29691) + * Update allowed attachment types (#29688) + * Completely style the webkit autofill (#29683) + * Highlight archived labels (#29680) + * Add a warning for disallowed email domains (#29658) + * Set user's 24h preference from their current OS locale (#29651) + * Add setting to disable user features when user login type is not plain (#29615) + * Improve natural sort (#29611) + * Make wiki default branch name changeable (#29603) + * Unify search boxes (#29530) + * Add support for API blob upload of release attachments (#29507) + * Detect broken git hooks (#29494) + * Sync branches to DB immediately when handling git hook calling (#29493) + * Allow options to disable user GPG key configuration from the interface on app.ini (#29486) + * Allow options to disable user SSH key configuration from the interface on app.ini (#29447) + * Use relative links for commits, mentions, and issues in markdown (#29427) + * Add ``, rename webcomponents (#29400) + * Include resource state events in Gitlab downloads (#29382) + * Properly migrate target branch change GitLab comment (#29340) + * Recolor dark theme to blue shade (#29283) + * Partially enable MSSQL case-sensitive collation support (#29238) + * Auto-update the system status in the admin dashboard (#29163) + * Integrate alpine `noarch` packages into other architectures index (#29137) + * Document how the TOC election process works (#29135) + * Tweak repo header (#29134) + * Make blockquote border size less aggressive (#29124) + * Downscale pasted PNG images based on metadata (#29123) + * Show `View at this point in history` for every commit (#29122) + * Add support for action artifact serve direct (#29120) + * Change webhook-type in create-view (#29114) + * Drop "@" from the email sender to avoid spam filters (#29109) + * Allow non-admin users to delete review requests (#29057) + * Improve user search display name (#29002) + * Include username in email headers (#28981) + * Show whether a PR is WIP inside popups (#28975) + * Also match weakly validated ETags (#28957) + * Support nuspec manifest download for Nuget packages (#28921) + * Fix hardcoded GitHub icon used as migrated release avatar (#28910) + * Propagate install_if and provider_priority to APKINDEX (#28899) + * Add artifacts v4 JWT to job message and accept it (#28885) + * Enable/disable owner and repo projects independently (#28805) + * Add non-JS fallback for reaction tooltips (#28785) + * Add the ability to see open and closed issues at the same time (#28757) + * Move sign-in labels to be above inputs (#28753) + * Display the latest sync time for pull mirrors on the repo page (#28712) + * Show in Web UI if the file is vendored and generated (#28620) + * Add orphaned topic consistency check (#28507) + * Add branch protection setting for ignoring stale approvals (#28498) + * Add option to set language in admin user view (#28449) + * Fix incorrect run order of action jobs (#28367) + * Add missing exclusive in advanced label options (#28322) + * Added instance-level variables (#28115) + * Add edit option for README.md (#28071) + * Fix link to `Code` tab on wiki commits (#28041) + * Allow to set explore page default sort (#27951) + * Improve PR diff view on mobile (#27883) + * Properly migrate automatic merge GitLab comments (#27873) + * Display issue task list on project cards (#27865) + * Add Index to pull_auto_merge.doer_id (#27811) + * Fix display member unit in the menu bar if there are no hidden members in public org (#27795) + * List all Debian package versions in `Packages` (#27786) + * Allow pull requests Manually Merged option to be used by non-admins (#27780) + * Only show diff file tree when more than one file changed (#27775) + * Show placeholder email in privacy popup (#27770) + * Revamp repo header (#27760) + * Add `must-change-password` command line parameter (#27626) + * Unify password changing and invalidate auth tokens (#27625) + * Add border to file tree 'sub-items' and add padding to 'item-file' (#27593) + * Add slow SQL query warning (#27545) + * Pre-register OAuth application for tea (#27509) + * Differentiate between `push` and `pull` `mirror sync in progress` (#27390) + * Link to file from its history (#27354) + * Add a shortcut to user's profile page to admin user details (#27299) + * Doctor: delete action entries without existing user (#27292) + * Show total TrackedTime on issue/pull/milestone lists (#26672) + * Don't show the new pull request button when the page is not compare pull (#26431) + * Add `Hide/Show all checks` button to commit status check (#26284) + * Improvements of releases list and tags list (#25859) +* PERFORMANCE + * Fix package list performance (#30520) (#30616) + * Add commit status summary table to reduce query from commit status table (#30223) + * Refactor markup/csv: don't read all to memory (#29760) + * Lazy load object format with command line and don't do it in OpenRepository (#29712) + * Add cache for branch divergence on branch list page (#29577) + * Do some performance optimization for issues list and view issue/pull (#29515) + * Cache repository default branch commit status to reduce query on commit status table (#29444) + * Use `crypto/sha256` (#29386) + * Some performance optimization on the dashboard and issues page (#29010) + * Add combined index for issue_user.uid and issue_id (#28080) + ## [1.21.11](https://github.com/go-gitea/gitea/releases/tag/v1.21.11) - 2024-04-07 * SECURITY diff --git a/MAINTAINERS b/MAINTAINERS index 426181cbcf1..ad02ecc7557 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -46,7 +46,6 @@ Wim (@42wim) Jason Song (@wolfogre) Yarden Shoham (@yardenshoham) Yu Tian (@Zettat123) -Eddie Yang <576951401@qq.com> (@yp05327) Dong Ge (@sillyguodong) Xinyi Gong (@HesterG) wxiaoguang (@wxiaoguang) diff --git a/Makefile b/Makefile index 0cd6936b3bb..4889958c3b6 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ XGO_VERSION := go-1.23.x AIR_PACKAGE ?= github.com/air-verse/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0 GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.60.3 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.5.1 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 @@ -377,12 +377,12 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig .PHONY: lint-js lint-js: node_modules npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) -# npx vue-tsc + npx vue-tsc .PHONY: lint-js-fix lint-js-fix: node_modules npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix -# npx vue-tsc + npx vue-tsc .PHONY: lint-css lint-css: node_modules @@ -451,10 +451,6 @@ lint-templates: .venv node_modules lint-yaml: .venv @poetry run yamllint . -.PHONY: tsc -tsc: - npx vue-tsc - .PHONY: watch watch: @bash tools/watch.sh diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 5c23f70d7ca..6377ebf9d2a 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1040,9 +1040,13 @@ LEVEL = Info ;; Don't allow download source archive files from UI ;DISABLE_DOWNLOAD_SOURCE_ARCHIVES = false -;; Allow fork repositories without maximum number limit +;; Allow to fork repositories without maximum number limit ;ALLOW_FORK_WITHOUT_MAXIMUM_LIMIT = true +;; Allow to fork repositories into the same owner (user or organization) +;; This feature is experimental, not fully tested, and may be changed in the future +;ALLOW_FORK_INTO_SAME_OWNER = false + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[repository.editor] diff --git a/go.mod b/go.mod index 80b62ce83f8..671151d4b63 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/ethantkoenig/rupture v1.0.1 github.com/felixge/fgprof v0.9.5 github.com/fsnotify/fsnotify v1.7.0 - github.com/gliderlabs/ssh v0.3.7 + github.com/gliderlabs/ssh v0.3.8 github.com/go-ap/activitypub v0.0.0-20240910141749-b4b8c8aa484c github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 github.com/go-chi/chi/v5 v5.1.0 @@ -121,13 +121,13 @@ require ( github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-meta v1.1.0 - golang.org/x/crypto v0.28.0 + golang.org/x/crypto v0.31.0 golang.org/x/image v0.21.0 golang.org/x/net v0.30.0 golang.org/x/oauth2 v0.23.0 - golang.org/x/sync v0.8.0 - golang.org/x/sys v0.26.0 - golang.org/x/text v0.19.0 + golang.org/x/sync v0.10.0 + golang.org/x/sys v0.28.0 + golang.org/x/text v0.21.0 golang.org/x/tools v0.26.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 diff --git a/go.sum b/go.sum index d1b7890fb68..afa3abece8f 100644 --- a/go.sum +++ b/go.sum @@ -293,8 +293,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-ap/activitypub v0.0.0-20240910141749-b4b8c8aa484c h1:82lzmsy5Nr6JA6HcLRVxGfbdSoWfW45C6jnY3zFS7Ks= @@ -893,8 +893,9 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= @@ -946,8 +947,9 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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= @@ -982,8 +984,9 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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= @@ -996,8 +999,9 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 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= @@ -1009,8 +1013,9 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/models/actions/run.go b/models/actions/run.go index 732fb48bb9a..f40bc1eb3db 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -37,6 +37,7 @@ type ActionRun struct { TriggerUser *user_model.User `xorm:"-"` ScheduleID int64 Ref string `xorm:"index"` // the commit/tag/… that caused the run + IsRefDeleted bool `xorm:"-"` CommitSHA string IsForkPullRequest bool // If this is triggered by a PR from a forked repository or an untrusted user, we need to check if it is approved and limit permissions when running the workflow. NeedApproval bool // may need approval if it's a fork pull request diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 4b8664077dc..de4b6aab667 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -137,7 +137,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col if err != nil { return 0, err } - run.Status = aggregateJobStatus(jobs) + run.Status = AggregateJobStatus(jobs) if run.Started.IsZero() && run.Status.IsRunning() { run.Started = timeutil.TimeStampNow() } @@ -152,29 +152,35 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col return affected, nil } -func aggregateJobStatus(jobs []*ActionRunJob) Status { - allDone := true - allWaiting := true - hasFailure := false +func AggregateJobStatus(jobs []*ActionRunJob) Status { + allSuccessOrSkipped := len(jobs) != 0 + allSkipped := len(jobs) != 0 + var hasFailure, hasCancelled, hasWaiting, hasRunning, hasBlocked bool for _, job := range jobs { - if !job.Status.IsDone() { - allDone = false - } - if job.Status != StatusWaiting && !job.Status.IsDone() { - allWaiting = false - } - if job.Status == StatusFailure || job.Status == StatusCancelled { - hasFailure = true - } + allSuccessOrSkipped = allSuccessOrSkipped && (job.Status == StatusSuccess || job.Status == StatusSkipped) + allSkipped = allSkipped && job.Status == StatusSkipped + hasFailure = hasFailure || job.Status == StatusFailure + hasCancelled = hasCancelled || job.Status == StatusCancelled + hasWaiting = hasWaiting || job.Status == StatusWaiting + hasRunning = hasRunning || job.Status == StatusRunning + hasBlocked = hasBlocked || job.Status == StatusBlocked } - if allDone { - if hasFailure { - return StatusFailure - } + switch { + case allSkipped: + return StatusSkipped + case allSuccessOrSkipped: return StatusSuccess - } - if allWaiting { + case hasCancelled: + return StatusCancelled + case hasFailure: + return StatusFailure + case hasRunning: + return StatusRunning + case hasWaiting: return StatusWaiting + case hasBlocked: + return StatusBlocked + default: + return StatusUnknown // it shouldn't happen } - return StatusRunning } diff --git a/models/actions/run_job_status_test.go b/models/actions/run_job_status_test.go new file mode 100644 index 00000000000..04fd9ceba7b --- /dev/null +++ b/models/actions/run_job_status_test.go @@ -0,0 +1,85 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAggregateJobStatus(t *testing.T) { + testStatuses := func(expected Status, statuses []Status) { + t.Helper() + var jobs []*ActionRunJob + for _, v := range statuses { + jobs = append(jobs, &ActionRunJob{Status: v}) + } + actual := AggregateJobStatus(jobs) + if !assert.Equal(t, expected, actual) { + var statusStrings []string + for _, s := range statuses { + statusStrings = append(statusStrings, s.String()) + } + t.Errorf("AggregateJobStatus(%v) = %v, want %v", statusStrings, statusNames[actual], statusNames[expected]) + } + } + + cases := []struct { + statuses []Status + expected Status + }{ + // unknown cases, maybe it shouldn't happen in real world + {[]Status{}, StatusUnknown}, + {[]Status{StatusUnknown, StatusSuccess}, StatusUnknown}, + {[]Status{StatusUnknown, StatusSkipped}, StatusUnknown}, + {[]Status{StatusUnknown, StatusFailure}, StatusFailure}, + {[]Status{StatusUnknown, StatusCancelled}, StatusCancelled}, + {[]Status{StatusUnknown, StatusWaiting}, StatusWaiting}, + {[]Status{StatusUnknown, StatusRunning}, StatusRunning}, + {[]Status{StatusUnknown, StatusBlocked}, StatusBlocked}, + + // success with other status + {[]Status{StatusSuccess}, StatusSuccess}, + {[]Status{StatusSuccess, StatusSkipped}, StatusSuccess}, // skipped doesn't affect success + {[]Status{StatusSuccess, StatusFailure}, StatusFailure}, + {[]Status{StatusSuccess, StatusCancelled}, StatusCancelled}, + {[]Status{StatusSuccess, StatusWaiting}, StatusWaiting}, + {[]Status{StatusSuccess, StatusRunning}, StatusRunning}, + {[]Status{StatusSuccess, StatusBlocked}, StatusBlocked}, + + // any cancelled, then cancelled + {[]Status{StatusCancelled}, StatusCancelled}, + {[]Status{StatusCancelled, StatusSuccess}, StatusCancelled}, + {[]Status{StatusCancelled, StatusSkipped}, StatusCancelled}, + {[]Status{StatusCancelled, StatusFailure}, StatusCancelled}, + {[]Status{StatusCancelled, StatusWaiting}, StatusCancelled}, + {[]Status{StatusCancelled, StatusRunning}, StatusCancelled}, + {[]Status{StatusCancelled, StatusBlocked}, StatusCancelled}, + + // failure with other status, fail fast + // Should "running" win? Maybe no: old code does make "running" win, but GitHub does fail fast. + {[]Status{StatusFailure}, StatusFailure}, + {[]Status{StatusFailure, StatusSuccess}, StatusFailure}, + {[]Status{StatusFailure, StatusSkipped}, StatusFailure}, + {[]Status{StatusFailure, StatusCancelled}, StatusCancelled}, + {[]Status{StatusFailure, StatusWaiting}, StatusFailure}, + {[]Status{StatusFailure, StatusRunning}, StatusFailure}, + {[]Status{StatusFailure, StatusBlocked}, StatusFailure}, + + // skipped with other status + // TODO: need to clarify whether a PR with "skipped" job status is considered as "mergeable" or not. + {[]Status{StatusSkipped}, StatusSkipped}, + {[]Status{StatusSkipped, StatusSuccess}, StatusSuccess}, + {[]Status{StatusSkipped, StatusFailure}, StatusFailure}, + {[]Status{StatusSkipped, StatusCancelled}, StatusCancelled}, + {[]Status{StatusSkipped, StatusWaiting}, StatusWaiting}, + {[]Status{StatusSkipped, StatusRunning}, StatusRunning}, + {[]Status{StatusSkipped, StatusBlocked}, StatusBlocked}, + } + + for _, c := range cases { + testStatuses(c.expected, c.statuses) + } +} diff --git a/models/actions/runner_token_test.go b/models/actions/runner_token_test.go index e85e99abe53..159805e5f7c 100644 --- a/models/actions/runner_token_test.go +++ b/models/actions/runner_token_test.go @@ -17,7 +17,7 @@ func TestGetLatestRunnerToken(t *testing.T) { token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3}) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) assert.NoError(t, err) - assert.EqualValues(t, token, expectedToken) + assert.EqualValues(t, expectedToken, token) } func TestNewRunnerToken(t *testing.T) { @@ -26,7 +26,7 @@ func TestNewRunnerToken(t *testing.T) { assert.NoError(t, err) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) assert.NoError(t, err) - assert.EqualValues(t, token, expectedToken) + assert.EqualValues(t, expectedToken, token) } func TestUpdateRunnerToken(t *testing.T) { @@ -36,5 +36,5 @@ func TestUpdateRunnerToken(t *testing.T) { assert.NoError(t, UpdateRunnerToken(db.DefaultContext, token)) expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) assert.NoError(t, err) - assert.EqualValues(t, token, expectedToken) + assert.EqualValues(t, expectedToken, token) } diff --git a/models/activities/user_heatmap_test.go b/models/activities/user_heatmap_test.go index b7babcbde1d..a039fd36132 100644 --- a/models/activities/user_heatmap_test.go +++ b/models/activities/user_heatmap_test.go @@ -4,7 +4,6 @@ package activities_test import ( - "fmt" "testing" "time" @@ -91,11 +90,11 @@ func TestGetUserHeatmapDataByUser(t *testing.T) { assert.NoError(t, err) assert.Len(t, actions, contributions, "invalid action count: did the test data became too old?") assert.Equal(t, count, int64(contributions)) - assert.Equal(t, tc.CountResult, contributions, fmt.Sprintf("testcase '%s'", tc.desc)) + assert.Equal(t, tc.CountResult, contributions, "testcase '%s'", tc.desc) // Test JSON rendering jsonData, err := json.Marshal(heatmap) assert.NoError(t, err) - assert.Equal(t, tc.JSONResult, string(jsonData)) + assert.JSONEq(t, tc.JSONResult, string(jsonData)) } } diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go index 0829d31d51b..43daa0b5ec1 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -18,7 +18,7 @@ func TestOAuth2Application_GenerateClientSecret(t *testing.T) { app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) secret, err := app.GenerateClientSecret(db.DefaultContext) assert.NoError(t, err) - assert.True(t, len(secret) > 0) + assert.NotEmpty(t, secret) unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1, ClientSecret: app.ClientSecret}) } @@ -165,7 +165,7 @@ func TestOAuth2Grant_GenerateNewAuthorizationCode(t *testing.T) { code, err := grant.GenerateNewAuthorizationCode(db.DefaultContext, "https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256") assert.NoError(t, err) assert.NotNil(t, code) - assert.True(t, len(code.Code) > 32) // secret length > 32 + assert.Greater(t, len(code.Code), 32) // secret length > 32 } func TestOAuth2Grant_TableName(t *testing.T) { diff --git a/models/db/context_committer_test.go b/models/db/context_committer_test.go index 38e91f22edb..849c5dea411 100644 --- a/models/db/context_committer_test.go +++ b/models/db/context_committer_test.go @@ -4,7 +4,7 @@ package db // it's not db_test, because this file is for testing the private type halfCommitter import ( - "fmt" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -80,7 +80,7 @@ func Test_halfCommitter(t *testing.T) { testWithCommitter(mockCommitter, func(committer Committer) error { defer committer.Close() if true { - return fmt.Errorf("error") + return errors.New("error") } return committer.Commit() }) @@ -94,7 +94,7 @@ func Test_halfCommitter(t *testing.T) { testWithCommitter(mockCommitter, func(committer Committer) error { committer.Close() committer.Commit() - return fmt.Errorf("error") + return errors.New("error") }) mockCommitter.Assert(t) diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go index 0f6ba2cc94a..e9f27906711 100644 --- a/models/db/iterate_test.go +++ b/models/db/iterate_test.go @@ -38,8 +38,6 @@ func TestIterate(t *testing.T) { if !has { return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID} } - assert.EqualValues(t, repoUnit.RepoID, repoUnit.RepoID) - assert.EqualValues(t, repoUnit.CreatedUnix, repoUnit.CreatedUnix) return nil }) assert.NoError(t, err) diff --git a/models/db/search.go b/models/db/search.go index 37565f45e1f..e0a1b6bde9f 100644 --- a/models/db/search.go +++ b/models/db/search.go @@ -26,8 +26,10 @@ const ( SearchOrderByForksReverse SearchOrderBy = "num_forks DESC" ) -const ( - // Which means a condition to filter the records which don't match any id. - // It's different from zero which means the condition could be ignored. - NoConditionID = -1 -) +// NoConditionID means a condition to filter the records which don't match any id. +// eg: "milestone_id=-1" means "find the items without any milestone. +const NoConditionID int64 = -1 + +// NonExistingID means a condition to match no result (eg: a non-existing user) +// It doesn't use -1 or -2 because they are used as builtin users. +const NonExistingID int64 = -1000000 diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index a42ab77ca5b..1db849352f2 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -36,3 +36,41 @@ updated: 1683636626 need_approval: 0 approved_by: 0 +- + id: 793 + title: "job output" + repo_id: 4 + owner_id: 1 + workflow_id: "test.yaml" + index: 189 + trigger_user_id: 1 + ref: "refs/heads/master" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + started: 1683636528 + stopped: 1683636626 + created: 1683636108 + updated: 1683636626 + need_approval: 0 + approved_by: 0 +- + id: 794 + title: "job output" + repo_id: 4 + owner_id: 1 + workflow_id: "test.yaml" + index: 190 + trigger_user_id: 1 + ref: "refs/heads/test" + commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" + event: "push" + is_fork_pull_request: 0 + status: 1 + 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 fd90f4fd5d2..9b6f5b9a887 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -26,3 +26,46 @@ status: 1 started: 1683636528 stopped: 1683636626 +- + id: 194 + run_id: 793 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job1 (1) + attempt: 1 + job_id: job1 + task_id: 49 + status: 1 + started: 1683636528 + stopped: 1683636626 +- + id: 195 + run_id: 793 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job1 (2) + attempt: 1 + job_id: job1 + task_id: 50 + status: 1 + started: 1683636528 + stopped: 1683636626 +- + id: 196 + run_id: 793 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + name: job2 + attempt: 1 + job_id: job2 + needs: [job1] + task_id: 51 + status: 5 + started: 1683636528 + stopped: 1683636626 diff --git a/models/fixtures/action_task.yml b/models/fixtures/action_task.yml index d88a8ed8a91..506a47d8a04 100644 --- a/models/fixtures/action_task.yml +++ b/models/fixtures/action_task.yml @@ -57,3 +57,63 @@ log_length: 707 log_size: 90179 log_expired: 0 +- + id: 49 + job_id: 194 + attempt: 1 + runner_id: 1 + status: 1 # success + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784220 + 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: 50 + job_id: 195 + attempt: 1 + runner_id: 1 + status: 1 # success + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784221 + 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: 51 + job_id: 196 + attempt: 1 + runner_id: 1 + status: 6 # running + started: 1683636528 + stopped: 1683636626 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: 0 + token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc4784222 + 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 diff --git a/models/fixtures/action_task_output.yml b/models/fixtures/action_task_output.yml new file mode 100644 index 00000000000..314e9f7115b --- /dev/null +++ b/models/fixtures/action_task_output.yml @@ -0,0 +1,20 @@ +- + id: 1 + task_id: 49 + output_key: output_a + output_value: abc +- + id: 2 + task_id: 49 + output_key: output_b + output_value: '' +- + id: 3 + task_id: 50 + output_key: output_a + output_value: '' +- + id: 4 + task_id: 50 + output_key: output_b + output_value: bbb diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index c7bdff77339..17b1869ab63 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -81,3 +81,15 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +- + id: 15 + repo_id: 4 + name: 'master' + commit_id: 'c7cd3cd144e6d23c9d6f3d07e52b2c1a956e0338' + commit_message: 'add Readme' + commit_time: 1588147171 + pusher_id: 13 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 diff --git a/models/fixtures/review.yml b/models/fixtures/review.yml index 0438ceadae2..5b8bbceca9e 100644 --- a/models/fixtures/review.yml +++ b/models/fixtures/review.yml @@ -4,6 +4,7 @@ reviewer_id: 1 issue_id: 2 content: "Demo Review" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -12,6 +13,7 @@ reviewer_id: 534543 issue_id: 534543 content: "Invalid Review #1" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -20,6 +22,7 @@ reviewer_id: 1 issue_id: 343545 content: "Invalid Review #2" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -28,6 +31,7 @@ reviewer_id: 1 issue_id: 2 content: "Pending Review" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -36,6 +40,7 @@ reviewer_id: 1 issue_id: 3 content: "New review 1" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 - @@ -61,8 +66,8 @@ type: 1 reviewer_id: 4 issue_id: 3 - original_author_id: 0 content: "New review 5" + original_author_id: 0 commit_id: 8091a55037cd59e47293aca02981b5a67076b364 stale: true updated_unix: 946684813 @@ -73,9 +78,9 @@ reviewer_id: 2 issue_id: 3 content: "New review 3 rejected" + original_author_id: 0 updated_unix: 946684814 created_unix: 946684814 - original_author_id: 0 - id: 10 @@ -83,6 +88,7 @@ reviewer_id: 100 issue_id: 3 content: "a deleted user's review" + original_author_id: 0 official: true updated_unix: 946684815 created_unix: 946684815 @@ -112,6 +118,7 @@ reviewer_id: 5 issue_id: 11 content: "old review from user5" + original_author_id: 0 updated_unix: 946684820 created_unix: 946684820 @@ -121,6 +128,7 @@ reviewer_id: 5 issue_id: 11 content: "duplicate review from user5 (latest)" + original_author_id: 0 updated_unix: 946684830 created_unix: 946684830 @@ -130,6 +138,7 @@ reviewer_id: 6 issue_id: 11 content: "singular review from org6 and final review for this pr" + original_author_id: 0 updated_unix: 946684831 created_unix: 946684831 @@ -139,6 +148,7 @@ reviewer_id: 20 issue_id: 20 content: "review request for user20" + original_author_id: 0 updated_unix: 946684832 created_unix: 946684832 @@ -148,6 +158,7 @@ reviewer_id: 20 issue_id: 20 content: "review approved by user20" + original_author_id: 0 updated_unix: 946684833 created_unix: 946684833 @@ -158,6 +169,7 @@ reviewer_team_id: 5 issue_id: 20 content: "review request for team5" + original_author_id: 0 updated_unix: 946684834 created_unix: 946684834 @@ -168,6 +180,7 @@ reviewer_team_id: 0 issue_id: 20 content: "review request for user15" + original_author_id: 0 updated_unix: 946684835 created_unix: 946684835 @@ -177,6 +190,7 @@ reviewer_id: 1 issue_id: 2 content: "Review Comment" + original_author_id: 0 updated_unix: 946684810 created_unix: 946684810 @@ -186,6 +200,7 @@ reviewer_id: 5 issue_id: 3 content: "reviewed by user5" + original_author_id: 0 commit_id: 4a357436d925b5c974181ff12a994538ddc5a269 updated_unix: 946684816 created_unix: 946684816 @@ -196,5 +211,6 @@ reviewer_id: 5 issue_id: 3 content: "review request for user5" + original_author_id: 0 updated_unix: 946684817 created_unix: 946684817 diff --git a/models/git/branch.go b/models/git/branch.go index ba1ada5517d..e683ce47e65 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -12,6 +12,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -169,9 +170,22 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e return &branch, nil } -func GetBranches(ctx context.Context, repoID int64, branchNames []string) ([]*Branch, error) { +func GetBranches(ctx context.Context, repoID int64, branchNames []string, includeDeleted bool) ([]*Branch, error) { branches := make([]*Branch, 0, len(branchNames)) - return branches, db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames).Find(&branches) + + sess := db.GetEngine(ctx).Where("repo_id=?", repoID).In("name", branchNames) + if !includeDeleted { + sess.And("is_deleted=?", false) + } + return branches, sess.Find(&branches) +} + +func BranchesToNamesSet(branches []*Branch) container.Set[string] { + names := make(container.Set[string], len(branches)) + for _, branch := range branches { + names.Add(branch.Name) + } + return names } func AddBranches(ctx context.Context, branches []*Branch) error { diff --git a/models/git/commit_status_test.go b/models/git/commit_status_test.go index 7ac4da68103..37d785e9385 100644 --- a/models/git/commit_status_test.go +++ b/models/git/commit_status_test.go @@ -34,7 +34,7 @@ func TestGetCommitStatuses(t *testing.T) { SHA: sha1, }) assert.NoError(t, err) - assert.Equal(t, int(maxResults), 5) + assert.Equal(t, 5, int(maxResults)) assert.Len(t, statuses, 5) assert.Equal(t, "ci/awesomeness", statuses[0].Context) @@ -63,7 +63,7 @@ func TestGetCommitStatuses(t *testing.T) { SHA: sha1, }) assert.NoError(t, err) - assert.Equal(t, int(maxResults), 5) + assert.Equal(t, 5, int(maxResults)) assert.Empty(t, statuses) } diff --git a/models/git/protected_branch_test.go b/models/git/protected_branch_test.go index 49d433f845d..e1c91d927d8 100644 --- a/models/git/protected_branch_test.go +++ b/models/git/protected_branch_test.go @@ -4,7 +4,6 @@ package git import ( - "fmt" "testing" "code.gitea.io/gitea/models/db" @@ -76,7 +75,7 @@ func TestBranchRuleMatch(t *testing.T) { infact = " not" } assert.EqualValues(t, kase.ExpectedMatch, pb.Match(kase.BranchName), - fmt.Sprintf("%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact), + "%s should%s match %s but it is%s", kase.BranchName, should, kase.Rule, infact, ) } } diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index c5bbfdedc28..d81f33f953d 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -64,7 +64,7 @@ func TestFetchCodeComments(t *testing.T) { } func TestAsCommentType(t *testing.T) { - assert.Equal(t, issues_model.CommentType(0), issues_model.CommentTypeComment) + assert.Equal(t, issues_model.CommentTypeComment, issues_model.CommentType(0)) assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType("")) assert.Equal(t, issues_model.CommentTypeUndefined, issues_model.AsCommentType("nonsense")) assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment")) diff --git a/models/issues/issue.go b/models/issues/issue.go index 64fc20cc05e..fe347c27156 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -125,8 +125,11 @@ type Issue struct { IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not. PullRequest *PullRequest `xorm:"-"` NumComments int - Ref string - PinOrder int `xorm:"DEFAULT 0"` + + // TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl" + Ref string + + PinOrder int `xorm:"DEFAULT 0"` DeadlineUnix timeutil.TimeStamp `xorm:"INDEX"` diff --git a/models/issues/issue_index.go b/models/issues/issue_index.go index 16274d0ef09..2eb61858bfc 100644 --- a/models/issues/issue_index.go +++ b/models/issues/issue_index.go @@ -18,12 +18,12 @@ func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error { } defer committer.Close() - var max int64 - if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { + var maxIndex int64 + if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&maxIndex); err != nil { return err } - if err = db.SyncMaxResourceIndex(ctx, "issue_index", repoID, max); err != nil { + if err = db.SyncMaxResourceIndex(ctx, "issue_index", repoID, maxIndex); err != nil { return err } diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index 5948a67d4eb..f1cd125d495 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -27,8 +27,8 @@ type IssuesOptions struct { //nolint RepoIDs []int64 // overwrites RepoCond if the length is not 0 AllPublic bool // include also all public repositories RepoCond builder.Cond - AssigneeID int64 - PosterID int64 + AssigneeID optional.Option[int64] + PosterID optional.Option[int64] MentionedID int64 ReviewRequestedID int64 ReviewedID int64 @@ -231,15 +231,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) { sess.And("issue.is_closed=?", opts.IsClosed.Value()) } - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } else if opts.AssigneeID == db.NoConditionID { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") - } - - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } + applyAssigneeCondition(sess, opts.AssigneeID) + applyPosterCondition(sess, opts.PosterID) if opts.MentionedID > 0 { applyMentionedCondition(sess, opts.MentionedID) @@ -359,13 +352,27 @@ func issuePullAccessibleRepoCond(repoIDstr string, userID int64, org *organizati return cond } -func applyAssigneeCondition(sess *xorm.Session, assigneeID int64) { - sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). - And("issue_assignees.assignee_id = ?", assigneeID) +func applyAssigneeCondition(sess *xorm.Session, assigneeID optional.Option[int64]) { + // old logic: 0 is also treated as "not filtering assignee", because the "assignee" was read as FormInt64 + if !assigneeID.Has() || assigneeID.Value() == 0 { + return + } + if assigneeID.Value() == db.NoConditionID { + sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") + } else { + sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id"). + And("issue_assignees.assignee_id = ?", assigneeID.Value()) + } } -func applyPosterCondition(sess *xorm.Session, posterID int64) { - sess.And("issue.poster_id=?", posterID) +func applyPosterCondition(sess *xorm.Session, posterID optional.Option[int64]) { + if !posterID.Has() { + return + } + // poster doesn't need to support db.NoConditionID(-1), so just use the value as-is + if posterID.Has() { + sess.And("issue.poster_id=?", posterID.Value()) + } } func applyMentionedCondition(sess *xorm.Session, mentionedID int64) { diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index 39326616f88..9ef9347a16c 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -151,15 +151,9 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6 applyProjectCondition(sess, opts) - if opts.AssigneeID > 0 { - applyAssigneeCondition(sess, opts.AssigneeID) - } else if opts.AssigneeID == db.NoConditionID { - sess.Where("issue.id NOT IN (SELECT issue_id FROM issue_assignees)") - } + applyAssigneeCondition(sess, opts.AssigneeID) - if opts.PosterID > 0 { - applyPosterCondition(sess, opts.PosterID) - } + applyPosterCondition(sess, opts.PosterID) if opts.MentionedID > 0 { applyMentionedCondition(sess, opts.MentionedID) diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 1bbc0eee564..dbbb1e41790 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -16,6 +16,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -155,7 +156,7 @@ func TestIssues(t *testing.T) { }{ { issues_model.IssuesOptions{ - AssigneeID: 1, + AssigneeID: optional.Some(int64(1)), SortType: "oldest", }, []int64{1, 6}, @@ -433,7 +434,7 @@ func assertCreateIssues(t *testing.T, isPull bool) { owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) - assert.EqualValues(t, milestone.ID, 1) + assert.EqualValues(t, 1, milestone.ID) reaction := &issues_model.Reaction{ Type: "heart", UserID: owner.ID, diff --git a/models/issues/issue_watch_test.go b/models/issues/issue_watch_test.go index d4ce8d8d3d6..fad94e243e6 100644 --- a/models/issues/issue_watch_test.go +++ b/models/issues/issue_watch_test.go @@ -48,17 +48,17 @@ func TestGetIssueWatchers(t *testing.T) { iws, err := issues_model.GetIssueWatchers(db.DefaultContext, 1, db.ListOptions{}) assert.NoError(t, err) // Watcher is inactive, thus 0 - assert.Len(t, iws, 0) + assert.Empty(t, iws) iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{}) assert.NoError(t, err) // Watcher is explicit not watching - assert.Len(t, iws, 0) + assert.Empty(t, iws) iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 5, db.ListOptions{}) assert.NoError(t, err) // Issue has no Watchers - assert.Len(t, iws, 0) + assert.Empty(t, iws) iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 7, db.ListOptions{}) assert.NoError(t, err) diff --git a/models/issues/label_test.go b/models/issues/label_test.go index c2ff084c236..1d4b6f4684c 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -31,12 +31,12 @@ func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) { // First test : with negative and scope label.LoadSelectedLabelsAfterClick([]int64{1, -8}, []string{"", "scope"}) assert.Equal(t, "1", label.QueryString) - assert.Equal(t, true, label.IsSelected) + assert.True(t, label.IsSelected) // Second test : with duplicates label.LoadSelectedLabelsAfterClick([]int64{1, 7, 1, 7, 7}, []string{"", "scope", "", "scope", "scope"}) assert.Equal(t, "1,8", label.QueryString) - assert.Equal(t, false, label.IsSelected) + assert.False(t, label.IsSelected) // Third test : empty set label.LoadSelectedLabelsAfterClick([]int64{}, []string{}) @@ -248,7 +248,7 @@ func TestGetLabelsByIssueID(t *testing.T) { labels, err = issues_model.GetLabelsByIssueID(db.DefaultContext, unittest.NonexistentID) assert.NoError(t, err) - assert.Len(t, labels, 0) + assert.Empty(t, labels) } func TestUpdateLabel(t *testing.T) { @@ -271,7 +271,7 @@ func TestUpdateLabel(t *testing.T) { assert.EqualValues(t, label.Color, newLabel.Color) assert.EqualValues(t, label.Name, newLabel.Name) assert.EqualValues(t, label.Description, newLabel.Description) - assert.EqualValues(t, newLabel.ArchivedUnix, 0) + assert.EqualValues(t, 0, newLabel.ArchivedUnix) unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{}) } diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go index e5f6f15ca2a..28cd0c028b8 100644 --- a/models/issues/milestone_test.go +++ b/models/issues/milestone_test.go @@ -87,7 +87,7 @@ func TestGetMilestonesByRepoID(t *testing.T) { IsClosed: optional.Some(false), }) assert.NoError(t, err) - assert.Len(t, milestones, 0) + assert.Empty(t, milestones) } func TestGetMilestones(t *testing.T) { diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index 70b6e370164..1ddb94e566b 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" + "xorm.io/builder" "xorm.io/xorm" ) @@ -257,6 +258,64 @@ func (prs PullRequestList) GetIssueIDs() []int64 { }) } +func (prs PullRequestList) LoadReviewCommentsCounts(ctx context.Context) (map[int64]int, error) { + issueIDs := prs.GetIssueIDs() + countsMap := make(map[int64]int, len(issueIDs)) + counts := make([]struct { + IssueID int64 + Count int + }, 0, len(issueIDs)) + if err := db.GetEngine(ctx).Select("issue_id, count(*) as count"). + Table("comment").In("issue_id", issueIDs).And("type = ?", CommentTypeReview). + GroupBy("issue_id").Find(&counts); err != nil { + return nil, err + } + for _, c := range counts { + countsMap[c.IssueID] = c.Count + } + return countsMap, nil +} + +func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error) { + issueIDs := prs.GetIssueIDs() + reviews := make([]*Review, 0, len(issueIDs)) + + subQuery := builder.Select("max(id) as id"). + From("review"). + Where(builder.In("issue_id", issueIDs)). + And(builder.In("`type`", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest)). + And(builder.Eq{ + "dismissed": false, + "original_author_id": 0, + "reviewer_team_id": 0, + }). + GroupBy("issue_id, reviewer_id") + // Get latest review of each reviewer, sorted in order they were made + if err := db.GetEngine(ctx).In("id", subQuery).OrderBy("review.updated_unix ASC").Find(&reviews); err != nil { + return nil, err + } + + teamReviewRequests := make([]*Review, 0, 5) + subQueryTeam := builder.Select("max(id) as id"). + From("review"). + Where(builder.In("issue_id", issueIDs)). + And(builder.Eq{ + "original_author_id": 0, + }).And(builder.Neq{ + "reviewer_team_id": 0, + }). + GroupBy("issue_id, reviewer_team_id") + if err := db.GetEngine(ctx).In("id", subQueryTeam).OrderBy("review.updated_unix ASC").Find(&teamReviewRequests); err != nil { + return nil, err + } + + if len(teamReviewRequests) > 0 { + reviews = append(reviews, teamReviewRequests...) + } + + return reviews, nil +} + // HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) { return db.GetEngine(ctx). diff --git a/models/issues/pull_list_test.go b/models/issues/pull_list_test.go new file mode 100644 index 00000000000..c7a898ca4e8 --- /dev/null +++ b/models/issues/pull_list_test.go @@ -0,0 +1,64 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestPullRequestList_LoadAttributes(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + prs := []*issues_model.PullRequest{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), + } + assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes(db.DefaultContext)) + for _, pr := range prs { + assert.NotNil(t, pr.Issue) + assert.Equal(t, pr.IssueID, pr.Issue.ID) + } + + assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(db.DefaultContext)) +} + +func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + prs := []*issues_model.PullRequest{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), + } + reviewComments, err := issues_model.PullRequestList(prs).LoadReviewCommentsCounts(db.DefaultContext) + assert.NoError(t, err) + assert.Len(t, reviewComments, 2) + for _, pr := range prs { + assert.EqualValues(t, 1, reviewComments[pr.IssueID]) + } +} + +func TestPullRequestList_LoadReviews(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + prs := []*issues_model.PullRequest{ + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), + unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), + } + reviewList, err := issues_model.PullRequestList(prs).LoadReviews(db.DefaultContext) + assert.NoError(t, err) + // 1, 7, 8, 9, 10, 22 + assert.Len(t, reviewList, 6) + assert.EqualValues(t, 1, reviewList[0].ID) + assert.EqualValues(t, 7, reviewList[1].ID) + assert.EqualValues(t, 8, reviewList[2].ID) + assert.EqualValues(t, 9, reviewList[3].ID) + assert.EqualValues(t, 10, reviewList[4].ID) + assert.EqualValues(t, 22, reviewList[5].ID) +} diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index 675c90527d8..090659864a8 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -79,11 +79,11 @@ func TestPullRequestsNewest(t *testing.T) { func TestLoadRequestedReviewers(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) assert.NoError(t, pull.LoadIssue(db.DefaultContext)) issue := pull.Issue assert.NoError(t, issue.LoadRepo(db.DefaultContext)) - assert.Len(t, pull.RequestedReviewers, 0) + assert.Empty(t, pull.RequestedReviewers) user1, err := user_model.GetUserByID(db.DefaultContext, 1) assert.NoError(t, err) @@ -93,7 +93,7 @@ func TestLoadRequestedReviewers(t *testing.T) { assert.NotNil(t, comment) assert.NoError(t, pull.LoadRequestedReviewers(db.DefaultContext)) - assert.Len(t, pull.RequestedReviewers, 1) + assert.Len(t, pull.RequestedReviewers, 6) comment, err = issues_model.RemoveReviewRequest(db.DefaultContext, issue, user1, &user_model.User{}) assert.NoError(t, err) @@ -234,22 +234,6 @@ func TestPullRequest_UpdateCols(t *testing.T) { unittest.CheckConsistencyFor(t, pr) } -func TestPullRequestList_LoadAttributes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - prs := []*issues_model.PullRequest{ - unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), - unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), - } - assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes(db.DefaultContext)) - for _, pr := range prs { - assert.NotNil(t, pr.Issue) - assert.Equal(t, pr.IssueID, pr.Issue.ID) - } - - assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(db.DefaultContext)) -} - // TODO TestAddTestPullRequestTask func TestPullRequest_IsWorkInProgress(t *testing.T) { diff --git a/models/issues/review_list.go b/models/issues/review_list.go index a5ceb217911..bc7d7ec0f01 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -47,14 +47,9 @@ func (reviews ReviewList) LoadReviewersTeams(ctx context.Context) error { } } - teamsMap := make(map[int64]*organization_model.Team, 0) - for _, teamID := range reviewersTeamsIDs { - team, err := organization_model.GetTeamByID(ctx, teamID) - if err != nil { - return err - } - - teamsMap[teamID] = team + teamsMap, err := organization_model.GetTeamsByIDs(ctx, reviewersTeamsIDs) + if err != nil { + return err } for _, review := range reviews { diff --git a/models/issues/review_test.go b/models/issues/review_test.go index 942121fd8f2..50330e3ff2c 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -126,42 +126,48 @@ func TestGetReviewersByIssueID(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) expectedReviews := []*issues_model.Review{} expectedReviews = append(expectedReviews, &issues_model.Review{ + ID: 7, Reviewer: org3, Type: issues_model.ReviewTypeReject, UpdatedUnix: 946684812, }, &issues_model.Review{ + ID: 8, Reviewer: user4, Type: issues_model.ReviewTypeApprove, UpdatedUnix: 946684813, }, &issues_model.Review{ + ID: 9, Reviewer: user2, Type: issues_model.ReviewTypeReject, UpdatedUnix: 946684814, - }) + }, + &issues_model.Review{ + ID: 10, + Reviewer: user_model.NewGhostUser(), + Type: issues_model.ReviewTypeReject, + UpdatedUnix: 946684815, + }, + &issues_model.Review{ + ID: 22, + Reviewer: user5, + Type: issues_model.ReviewTypeRequest, + UpdatedUnix: 946684817, + }, + ) allReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) assert.NoError(t, err) for _, review := range allReviews { assert.NoError(t, review.LoadReviewer(db.DefaultContext)) } - if assert.Len(t, allReviews, 3) { - for i, review := range allReviews { - assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) - assert.Equal(t, expectedReviews[i].Type, review.Type) - assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix) - } - } - - allReviews, err = issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID) - assert.NoError(t, err) - assert.NoError(t, allReviews.LoadReviewers(db.DefaultContext)) - if assert.Len(t, allReviews, 3) { + if assert.Len(t, allReviews, 5) { for i, review := range allReviews { assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer) assert.Equal(t, expectedReviews[i].Type, review.Type) diff --git a/models/issues/stopwatch_test.go b/models/issues/stopwatch_test.go index 39958a7f36b..a1bf9dc931f 100644 --- a/models/issues/stopwatch_test.go +++ b/models/issues/stopwatch_test.go @@ -32,7 +32,7 @@ func TestCancelStopwatch(t *testing.T) { _ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID}) - assert.Nil(t, issues_model.CancelStopwatch(db.DefaultContext, user1, issue2)) + assert.NoError(t, issues_model.CancelStopwatch(db.DefaultContext, user1, issue2)) } func TestStopwatchExists(t *testing.T) { diff --git a/models/issues/tracked_time_test.go b/models/issues/tracked_time_test.go index d82bff967a9..44054a1b836 100644 --- a/models/issues/tracked_time_test.go +++ b/models/issues/tracked_time_test.go @@ -50,7 +50,7 @@ func TestGetTrackedTimes(t *testing.T) { times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: -1}) assert.NoError(t, err) - assert.Len(t, times, 0) + assert.Empty(t, times) // by User times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 1}) @@ -60,7 +60,7 @@ func TestGetTrackedTimes(t *testing.T) { times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 3}) assert.NoError(t, err) - assert.Len(t, times, 0) + assert.Empty(t, times) // by Repo times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 2}) @@ -69,7 +69,7 @@ func TestGetTrackedTimes(t *testing.T) { assert.Equal(t, int64(1), times[0].Time) issue, err := issues_model.GetIssueByID(db.DefaultContext, times[0].IssueID) assert.NoError(t, err) - assert.Equal(t, issue.RepoID, int64(2)) + assert.Equal(t, int64(2), issue.RepoID) times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 1}) assert.NoError(t, err) @@ -77,7 +77,7 @@ func TestGetTrackedTimes(t *testing.T) { times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 10}) assert.NoError(t, err) - assert.Len(t, times, 0) + assert.Empty(t, times) } func TestTotalTimesForEachUser(t *testing.T) { diff --git a/models/migrations/v1_16/v193_test.go b/models/migrations/v1_16/v193_test.go index d99bbc29620..b279967a2c0 100644 --- a/models/migrations/v1_16/v193_test.go +++ b/models/migrations/v1_16/v193_test.go @@ -56,8 +56,8 @@ func Test_AddRepoIDForAttachment(t *testing.T) { err := x.Table("attachment").Where("issue_id > 0").Find(&issueAttachments) assert.NoError(t, err) for _, attach := range issueAttachments { - assert.Greater(t, attach.RepoID, int64(0)) - assert.Greater(t, attach.IssueID, int64(0)) + assert.Positive(t, attach.RepoID) + assert.Positive(t, attach.IssueID) var issue Issue has, err := x.ID(attach.IssueID).Get(&issue) assert.NoError(t, err) @@ -69,8 +69,8 @@ func Test_AddRepoIDForAttachment(t *testing.T) { err = x.Table("attachment").Where("release_id > 0").Find(&releaseAttachments) assert.NoError(t, err) for _, attach := range releaseAttachments { - assert.Greater(t, attach.RepoID, int64(0)) - assert.Greater(t, attach.ReleaseID, int64(0)) + assert.Positive(t, attach.RepoID) + assert.Positive(t, attach.ReleaseID) var release Release has, err := x.ID(attach.ReleaseID).Get(&release) assert.NoError(t, err) diff --git a/models/migrations/v1_22/v286_test.go b/models/migrations/v1_22/v286_test.go index a19c9396e2e..1f213ddb6e0 100644 --- a/models/migrations/v1_22/v286_test.go +++ b/models/migrations/v1_22/v286_test.go @@ -107,12 +107,12 @@ func Test_RepositoryFormat(t *testing.T) { repo = new(Repository) ok, err := x.ID(2).Get(repo) assert.NoError(t, err) - assert.EqualValues(t, true, ok) + assert.True(t, ok) assert.EqualValues(t, "sha1", repo.ObjectFormatName) repo = new(Repository) ok, err = x.ID(id).Get(repo) assert.NoError(t, err) - assert.EqualValues(t, true, ok) + assert.True(t, ok) assert.EqualValues(t, "sha256", repo.ObjectFormatName) } diff --git a/models/migrations/v1_22/v294_test.go b/models/migrations/v1_22/v294_test.go index 82a3bcd602e..a1d702cb77d 100644 --- a/models/migrations/v1_22/v294_test.go +++ b/models/migrations/v1_22/v294_test.go @@ -39,7 +39,7 @@ func Test_AddUniqueIndexForProjectIssue(t *testing.T) { tables, err := x.DBMetas() assert.NoError(t, err) - assert.EqualValues(t, 1, len(tables)) + assert.Len(t, tables, 1) found := false for _, index := range tables[0].Indexes { if index.Type == schemas.UniqueType { diff --git a/models/organization/org_list_test.go b/models/organization/org_list_test.go index edc8996f3ec..0f0f8a4bcd0 100644 --- a/models/organization/org_list_test.go +++ b/models/organization/org_list_test.go @@ -40,7 +40,7 @@ func TestFindOrgs(t *testing.T) { IncludePrivate: false, }) assert.NoError(t, err) - assert.Len(t, orgs, 0) + assert.Empty(t, orgs) total, err := db.Count[organization.Organization](db.DefaultContext, organization.FindOrgOptions{ UserID: 4, diff --git a/models/organization/org_test.go b/models/organization/org_test.go index 7159f0fc465..5e99e88689e 100644 --- a/models/organization/org_test.go +++ b/models/organization/org_test.go @@ -283,7 +283,7 @@ func TestGetOrgUsersByOrgID(t *testing.T) { OrgID: unittest.NonexistentID, }) assert.NoError(t, err) - assert.Len(t, orgUsers, 0) + assert.Empty(t, orgUsers) } func TestChangeOrgUserStatus(t *testing.T) { diff --git a/models/organization/team_list.go b/models/organization/team_list.go index cc2a50236a4..4ceb405e310 100644 --- a/models/organization/team_list.go +++ b/models/organization/team_list.go @@ -131,3 +131,8 @@ func GetTeamsByOrgIDs(ctx context.Context, orgIDs []int64) (TeamList, error) { teams := make([]*Team, 0, 10) return teams, db.GetEngine(ctx).Where(builder.In("org_id", orgIDs)).Find(&teams) } + +func GetTeamsByIDs(ctx context.Context, teamIDs []int64) (map[int64]*Team, error) { + teams := make(map[int64]*Team, len(teamIDs)) + return teams, db.GetEngine(ctx).Where(builder.In("`id`", teamIDs)).Find(&teams) +} diff --git a/models/organization/team_list_test.go b/models/organization/team_list_test.go new file mode 100644 index 00000000000..5526446e221 --- /dev/null +++ b/models/organization/team_list_test.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package organization_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + org_model "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func Test_GetTeamsByIDs(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // 1 owner team, 2 normal team + teams, err := org_model.GetTeamsByIDs(db.DefaultContext, []int64{1, 2}) + assert.NoError(t, err) + assert.Len(t, teams, 2) + assert.Equal(t, "Owners", teams[1].Name) + assert.Equal(t, "team1", teams[2].Name) +} diff --git a/models/perm/access_mode_test.go b/models/perm/access_mode_test.go index 982fceee5a5..c4c7d483fb2 100644 --- a/models/perm/access_mode_test.go +++ b/models/perm/access_mode_test.go @@ -15,7 +15,7 @@ func TestAccessMode(t *testing.T) { m := ParseAccessMode(name) assert.Equal(t, AccessMode(i), m) } - assert.Equal(t, AccessMode(4), AccessModeOwner) + assert.Equal(t, AccessModeOwner, AccessMode(4)) assert.Equal(t, "owner", AccessModeOwner.ToString()) assert.Equal(t, AccessModeNone, ParseAccessMode("owner")) assert.Equal(t, AccessModeNone, ParseAccessMode("invalid")) diff --git a/models/project/column_test.go b/models/project/column_test.go index 911649fb726..566667e45d1 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -5,7 +5,6 @@ package project import ( "fmt" - "strings" "testing" "code.gitea.io/gitea/models/db" @@ -66,7 +65,7 @@ func Test_moveIssuesToAnotherColumn(t *testing.T) { issues, err = column1.GetIssues(db.DefaultContext) assert.NoError(t, err) - assert.Len(t, issues, 0) + assert.Empty(t, issues) issues, err = column2.GetIssues(db.DefaultContext) assert.NoError(t, err) @@ -123,5 +122,5 @@ func Test_NewColumn(t *testing.T) { ProjectID: project1.ID, }) assert.Error(t, err) - assert.True(t, strings.Contains(err.Error(), "maximum number of columns reached")) + assert.Contains(t, err.Error(), "maximum number of columns reached") } diff --git a/models/repo/repo_test.go b/models/repo/repo_test.go index 6468e0f6058..6d88d170da3 100644 --- a/models/repo/repo_test.go +++ b/models/repo/repo_test.go @@ -144,8 +144,8 @@ func TestGetRepositoryByURL(t *testing.T) { assert.NotNil(t, repo) assert.NoError(t, err) - assert.Equal(t, repo.ID, int64(2)) - assert.Equal(t, repo.OwnerID, int64(2)) + assert.Equal(t, int64(2), repo.ID) + assert.Equal(t, int64(2), repo.OwnerID) } test(t, "https://try.gitea.io/user2/repo2") @@ -159,8 +159,8 @@ func TestGetRepositoryByURL(t *testing.T) { assert.NotNil(t, repo) assert.NoError(t, err) - assert.Equal(t, repo.ID, int64(2)) - assert.Equal(t, repo.OwnerID, int64(2)) + assert.Equal(t, int64(2), repo.ID) + assert.Equal(t, int64(2), repo.OwnerID) } test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2") @@ -177,8 +177,8 @@ func TestGetRepositoryByURL(t *testing.T) { assert.NotNil(t, repo) assert.NoError(t, err) - assert.Equal(t, repo.ID, int64(2)) - assert.Equal(t, repo.OwnerID, int64(2)) + assert.Equal(t, int64(2), repo.ID) + assert.Equal(t, int64(2), repo.OwnerID) } test(t, "sshuser@try.gitea.io:user2/repo2") diff --git a/models/repo/star_test.go b/models/repo/star_test.go index aaac89d975d..b540f54310c 100644 --- a/models/repo/star_test.go +++ b/models/repo/star_test.go @@ -52,7 +52,7 @@ func TestRepository_GetStargazers2(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0}) assert.NoError(t, err) - assert.Len(t, gazers, 0) + assert.Empty(t, gazers) } func TestClearRepoStars(t *testing.T) { @@ -71,5 +71,5 @@ func TestClearRepoStars(t *testing.T) { gazers, err := repo_model.GetStargazers(db.DefaultContext, repo, db.ListOptions{Page: 0}) assert.NoError(t, err) - assert.Len(t, gazers, 0) + assert.Empty(t, gazers) } diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go index f2abc2ffa01..44ebe5f214c 100644 --- a/models/repo/user_repo_test.go +++ b/models/repo/user_repo_test.go @@ -21,7 +21,7 @@ func TestRepoAssignees(t *testing.T) { users, err := repo_model.GetRepoAssignees(db.DefaultContext, repo2) assert.NoError(t, err) assert.Len(t, users, 1) - assert.Equal(t, users[0].ID, int64(2)) + assert.Equal(t, int64(2), users[0].ID) repo21 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 21}) users, err = repo_model.GetRepoAssignees(db.DefaultContext, repo21) diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index a95a2679616..c39ef607e8b 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_test.go @@ -41,7 +41,7 @@ func TestGetWatchers(t *testing.T) { watches, err = repo_model.GetWatchers(db.DefaultContext, unittest.NonexistentID) assert.NoError(t, err) - assert.Len(t, watches, 0) + assert.Empty(t, watches) } func TestRepository_GetWatchers(t *testing.T) { @@ -58,7 +58,7 @@ func TestRepository_GetWatchers(t *testing.T) { repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 9}) watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) assert.NoError(t, err) - assert.Len(t, watchers, 0) + assert.Empty(t, watchers) } func TestWatchIfAuto(t *testing.T) { diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index 3b0f28d3a60..4ac858e04ea 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -79,7 +79,7 @@ func AssertExistsAndLoadMap(t assert.TestingT, table string, conditions ...any) e := db.GetEngine(db.DefaultContext).Table(table) res, err := whereOrderConditions(e, conditions).Query() assert.NoError(t, err) - assert.True(t, len(res) == 1, + assert.Len(t, res, 1, "Expected to find one row in %s (with conditions %+v), but found %d", table, conditions, len(res), ) diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index c2e010d95b3..d72d873de2c 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -97,8 +97,7 @@ func TestListEmails(t *testing.T) { } emails, count, err := user_model.SearchEmails(db.DefaultContext, opts) assert.NoError(t, err) - assert.NotEqual(t, int64(0), count) - assert.True(t, count > 5) + assert.Greater(t, count, int64(5)) contains := func(match func(s *user_model.SearchEmailResult) bool) bool { for _, v := range emails { diff --git a/models/user/setting_test.go b/models/user/setting_test.go index c56fe930750..c607d9fd008 100644 --- a/models/user/setting_test.go +++ b/models/user/setting_test.go @@ -56,5 +56,5 @@ func TestSettings(t *testing.T) { assert.NoError(t, err) settings, err = user_model.GetUserAllSettings(db.DefaultContext, 99) assert.NoError(t, err) - assert.Len(t, settings, 0) + assert.Empty(t, settings) } diff --git a/models/user/user_test.go b/models/user/user_test.go index 6701be39a55..7ebc64f69e7 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -201,7 +201,7 @@ func TestNewGitSig(t *testing.T) { assert.NotContains(t, sig.Name, "<") assert.NotContains(t, sig.Name, ">") assert.NotContains(t, sig.Name, "\n") - assert.NotEqual(t, len(strings.TrimSpace(sig.Name)), 0) + assert.NotEmpty(t, strings.TrimSpace(sig.Name)) } } @@ -216,7 +216,7 @@ func TestDisplayName(t *testing.T) { if len(strings.TrimSpace(user.FullName)) == 0 { assert.Equal(t, user.Name, displayName) } - assert.NotEqual(t, len(strings.TrimSpace(displayName)), 0) + assert.NotEmpty(t, strings.TrimSpace(displayName)) } } @@ -322,15 +322,15 @@ func TestGetMaileableUsersByIDs(t *testing.T) { assert.NoError(t, err) assert.Len(t, results, 1) if len(results) > 1 { - assert.Equal(t, results[0].ID, 1) + assert.Equal(t, 1, results[0].ID) } results, err = user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, true) assert.NoError(t, err) assert.Len(t, results, 2) if len(results) > 2 { - assert.Equal(t, results[0].ID, 1) - assert.Equal(t, results[1].ID, 4) + assert.Equal(t, 1, results[0].ID) + assert.Equal(t, 4, results[1].ID) } } @@ -499,7 +499,7 @@ func Test_ValidateUser(t *testing.T) { {ID: 2, Visibility: structs.VisibleTypePrivate}: true, } for kase, expected := range kases { - assert.EqualValues(t, expected, nil == user_model.ValidateUser(kase), fmt.Sprintf("case: %+v", kase)) + assert.EqualValues(t, expected, nil == user_model.ValidateUser(kase), "case: %+v", kase) } } @@ -570,11 +570,11 @@ func TestDisabledUserFeatures(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - assert.Len(t, setting.Admin.UserDisabledFeatures.Values(), 0) + assert.Empty(t, setting.Admin.UserDisabledFeatures.Values()) // no features should be disabled with a plain login type assert.LessOrEqual(t, user.LoginType, auth.Plain) - assert.Len(t, user_model.DisabledFeaturesWithLoginType(user).Values(), 0) + assert.Empty(t, user_model.DisabledFeaturesWithLoginType(user).Values()) for _, f := range testValues.Values() { assert.False(t, user_model.IsFeatureDisabledWithLoginType(user, f)) } @@ -600,5 +600,5 @@ func TestGetInactiveUsers(t *testing.T) { interval := time.Now().Unix() - 1730468968 + 3600*24 users, err = user_model.GetInactiveUsers(db.DefaultContext, time.Duration(interval*int64(time.Second))) assert.NoError(t, err) - assert.Len(t, users, 0) + assert.Empty(t, users) } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index f4403776cec..c6c3f40d46e 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -43,7 +43,7 @@ func TestWebhook_History(t *testing.T) { webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2}) tasks, err = webhook.History(db.DefaultContext, 0) assert.NoError(t, err) - assert.Len(t, tasks, 0) + assert.Empty(t, tasks) } func TestWebhook_UpdateEvent(t *testing.T) { @@ -206,7 +206,7 @@ func TestHookTasks(t *testing.T) { hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1) assert.NoError(t, err) - assert.Len(t, hookTasks, 0) + assert.Empty(t, hookTasks) } func TestCreateHookTask(t *testing.T) { diff --git a/modules/activitypub/client_test.go b/modules/activitypub/client_test.go index 65ea8d4d5bf..d0c48454457 100644 --- a/modules/activitypub/client_test.go +++ b/modules/activitypub/client_test.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "net/http/httptest" - "regexp" "testing" "code.gitea.io/gitea/models/db" @@ -28,9 +27,9 @@ func TestActivityPubSignedPost(t *testing.T) { expected := "BODY" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Regexp(t, regexp.MustCompile("^"+setting.Federation.DigestAlgorithm), r.Header.Get("Digest")) + assert.Regexp(t, "^"+setting.Federation.DigestAlgorithm, r.Header.Get("Digest")) assert.Contains(t, r.Header.Get("Signature"), pubID) - assert.Equal(t, r.Header.Get("Content-Type"), ActivityStreamsContentType) + assert.Equal(t, ActivityStreamsContentType, r.Header.Get("Content-Type")) body, err := io.ReadAll(r.Body) assert.NoError(t, err) assert.Equal(t, expected, string(body)) diff --git a/modules/assetfs/layered_test.go b/modules/assetfs/layered_test.go index b82111e745e..03a3ae0d7cc 100644 --- a/modules/assetfs/layered_test.go +++ b/modules/assetfs/layered_test.go @@ -58,7 +58,7 @@ func TestLayered(t *testing.T) { assertRead := func(expected string, expectedErr error, elems ...string) { bs, err := assets.ReadFile(elems...) if err != nil { - assert.ErrorAs(t, err, &expectedErr) + assert.ErrorIs(t, err, expectedErr) } else { assert.NoError(t, err) assert.Equal(t, expected, string(bs)) diff --git a/modules/auth/pam/pam_test.go b/modules/auth/pam/pam_test.go index c277d59c415..7265b5d0c15 100644 --- a/modules/auth/pam/pam_test.go +++ b/modules/auth/pam/pam_test.go @@ -15,5 +15,5 @@ func TestPamAuth(t *testing.T) { result, err := Auth("gitea", "user1", "false-pwd") assert.Error(t, err) assert.EqualError(t, err, "Authentication failure") - assert.Len(t, result, 0) + assert.Len(t, result) } diff --git a/modules/auth/password/hash/dummy_test.go b/modules/auth/password/hash/dummy_test.go index f3b36df6250..e56e3f1a7f7 100644 --- a/modules/auth/password/hash/dummy_test.go +++ b/modules/auth/password/hash/dummy_test.go @@ -18,7 +18,7 @@ func TestDummyHasher(t *testing.T) { password, salt := "password", "ZogKvWdyEx" hash, err := dummy.Hash(password, salt) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, hash, salt+":"+password) assert.True(t, dummy.VerifyPassword(password, hash, salt)) diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index 85f9780709c..c66b62937fd 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -99,10 +99,10 @@ func IsComplexEnough(pwd string) bool { func Generate(n int) (string, error) { NewComplexity() buffer := make([]byte, n) - max := big.NewInt(int64(len(validChars))) + maxInt := big.NewInt(int64(len(validChars))) for { for j := 0; j < n; j++ { - rnd, err := rand.Int(rand.Reader, max) + rnd, err := rand.Int(rand.Reader, maxInt) if err != nil { return "", err } diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go index 86cccdf2092..f63679048e3 100644 --- a/modules/base/tool_test.go +++ b/modules/base/tool_test.go @@ -6,7 +6,6 @@ package base import ( "crypto/sha1" "fmt" - "os" "testing" "time" @@ -157,7 +156,7 @@ func TestStringsToInt64s(t *testing.T) { testSuccess([]string{"1", "4", "16", "64", "256"}, []int64{1, 4, 16, 64, 256}) ints, err := StringsToInt64s([]string{"-1", "a"}) - assert.Len(t, ints, 0) + assert.Empty(t, ints) assert.Error(t, err) } @@ -172,9 +171,9 @@ func TestInt64sToStrings(t *testing.T) { // TODO: Test EntryIcon func TestSetupGiteaRoot(t *testing.T) { - _ = os.Setenv("GITEA_ROOT", "test") + t.Setenv("GITEA_ROOT", "test") assert.Equal(t, "test", SetupGiteaRoot()) - _ = os.Setenv("GITEA_ROOT", "") + t.Setenv("GITEA_ROOT", "") assert.NotEqual(t, "test", SetupGiteaRoot()) } diff --git a/modules/dump/dumper_test.go b/modules/dump/dumper_test.go index b444fa2de53..2db3a598a4c 100644 --- a/modules/dump/dumper_test.go +++ b/modules/dump/dumper_test.go @@ -25,7 +25,7 @@ func TestPrepareFileNameAndType(t *testing.T) { assert.Equal(t, fmt.Sprintf("outFile=%s, outType=%s", expFile, expType), fmt.Sprintf("outFile=%s, outType=%s", outFile, outType), - fmt.Sprintf("argFile=%s, argType=%s", argFile, argType), + "argFile=%s, argType=%s", argFile, argType, ) } diff --git a/modules/git/commit.go b/modules/git/commit.go index 010b56948ef..0ed268e3469 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -474,3 +474,17 @@ func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSetting } return c.repo.GetDefaultPublicGPGKey(forceUpdate) } + +func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool { + minLen := util.OptionalArg(minLength, objFmt.FullLength()) + if len(s) < minLen || len(s) > objFmt.FullLength() { + return false + } + for _, c := range s { + isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') + if !isHex { + return false + } + } + return true +} diff --git a/modules/git/commit_sha256_test.go b/modules/git/commit_sha256_test.go index 3b8b6d3763a..2184a9c47cf 100644 --- a/modules/git/commit_sha256_test.go +++ b/modules/git/commit_sha256_test.go @@ -146,7 +146,7 @@ func TestHasPreviousCommitSha256(t *testing.T) { parentSHA := MustIDFromString("b0ec7af4547047f12d5093e37ef8f1b3b5415ed8ee17894d43a34d7d34212e9c") notParentSHA := MustIDFromString("42e334efd04cd36eea6da0599913333c26116e1a537ca76e5b6e4af4dda00236") assert.Equal(t, objectFormat, parentSHA.Type()) - assert.Equal(t, objectFormat.Name(), "sha256") + assert.Equal(t, "sha256", objectFormat.Name()) haz, err := commit.HasPreviousCommit(parentSHA) assert.NoError(t, err) diff --git a/modules/git/commit_test.go b/modules/git/commit_test.go index bf381a53501..6ac65564dc1 100644 --- a/modules/git/commit_test.go +++ b/modules/git/commit_test.go @@ -343,9 +343,9 @@ func TestGetCommitFileStatusMerges(t *testing.T) { }, } - assert.Equal(t, commitFileStatus.Added, expected.Added) - assert.Equal(t, commitFileStatus.Removed, expected.Removed) - assert.Equal(t, commitFileStatus.Modified, expected.Modified) + assert.Equal(t, expected.Added, commitFileStatus.Added) + assert.Equal(t, expected.Removed, commitFileStatus.Removed) + assert.Equal(t, expected.Modified, commitFileStatus.Modified) } func Test_GetCommitBranchStart(t *testing.T) { diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 6a99f804070..005d5397267 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -73,9 +73,9 @@ func TestGrepSearch(t *testing.T) { res, err = GrepSearch(context.Background(), repo, "no-such-content", GrepOptions{}) assert.NoError(t, err) - assert.Len(t, res, 0) + assert.Empty(t, res) res, err = GrepSearch(context.Background(), &Repository{Path: "no-such-git-repo"}, "no-such-content", GrepOptions{}) assert.Error(t, err) - assert.Len(t, res, 0) + assert.Empty(t, res) } diff --git a/modules/git/parse_nogogit_test.go b/modules/git/parse_nogogit_test.go index 23fddb014c1..a4436ce499a 100644 --- a/modules/git/parse_nogogit_test.go +++ b/modules/git/parse_nogogit_test.go @@ -100,5 +100,5 @@ func TestParseTreeEntriesInvalid(t *testing.T) { // there was a panic: "runtime error: slice bounds out of range" when the input was invalid: #20315 entries, err := ParseTreeEntries([]byte("100644 blob ea0d83c9081af9500ac9f804101b3fd0a5c293af")) assert.Error(t, err) - assert.Len(t, entries, 0) + assert.Empty(t, entries) } diff --git a/modules/git/ref.go b/modules/git/ref.go index 2db630e2ea9..aab4c5d77d7 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -142,7 +142,6 @@ func (ref RefName) RemoteName() string { // ShortName returns the short name of the reference name func (ref RefName) ShortName() string { - refName := string(ref) if ref.IsBranch() { return ref.BranchName() } @@ -158,8 +157,7 @@ func (ref RefName) ShortName() string { if ref.IsFor() { return ref.ForBranchName() } - - return refName + return string(ref) // usually it is a commit ID } // RefGroup returns the group type of the reference diff --git a/modules/git/repo.go b/modules/git/repo.go index 1c223018add..fc6e6e7accb 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -223,7 +223,7 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { if err != nil { if strings.Contains(stderr, "non-fast-forward") { return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err} - } else if strings.Contains(stderr, "! [remote rejected]") { + } else if strings.Contains(stderr, "! [remote rejected]") || strings.Contains(stderr, "! [rejected]") { err := &ErrPushRejected{StdOut: stdout, StdErr: stderr, Err: err} err.GenerateMessage() return err diff --git a/modules/git/repo_branch_test.go b/modules/git/repo_branch_test.go index 009c545832a..5d3b8abb3a8 100644 --- a/modules/git/repo_branch_test.go +++ b/modules/git/repo_branch_test.go @@ -34,7 +34,7 @@ func TestRepository_GetBranches(t *testing.T) { branches, countAll, err = bareRepo1.GetBranchNames(5, 1) assert.NoError(t, err) - assert.Len(t, branches, 0) + assert.Empty(t, branches) assert.EqualValues(t, 3, countAll) assert.ElementsMatch(t, []string{}, branches) } @@ -66,7 +66,7 @@ func TestGetRefsBySha(t *testing.T) { // do not exist branches, err := bareRepo5.GetRefsBySha("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", "") assert.NoError(t, err) - assert.Len(t, branches, 0) + assert.Empty(t, branches) // refs/pull/1/head branches, err = bareRepo5.GetRefsBySha("c83380d7056593c51a699d12b9c00627bd5743e9", PullPrefix) diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 9405634df12..9ffadb833d8 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -465,15 +465,15 @@ func (repo *Repository) getBranches(env []string, commitID string, limit int) ([ refs := strings.Split(stdout, "\n") - var max int + var maxNum int if len(refs) > limit { - max = limit + maxNum = limit } else { - max = len(refs) - 1 + maxNum = len(refs) - 1 } - branches := make([]string, max) - for i, ref := range refs[:max] { + branches := make([]string, maxNum) + for i, ref := range refs[:maxNum] { parts := strings.Fields(ref) branches[i] = parts[len(parts)-1] diff --git a/modules/git/repo_compare.go b/modules/git/repo_compare.go index b6e9d2b44a1..16fcdcf4c8f 100644 --- a/modules/git/repo_compare.go +++ b/modules/git/repo_compare.go @@ -246,18 +246,40 @@ func (repo *Repository) GetDiffOrPatch(base, head string, w io.Writer, patch, bi // GetDiff generates and returns patch data between given revisions, optimized for human readability func (repo *Repository) GetDiff(base, head string, w io.Writer) error { - return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head).Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - }) + stderr := new(bytes.Buffer) + err := NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base + "..." + head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + Stderr: stderr, + }) + if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { + return NewCommand(repo.Ctx, "diff", "-p").AddDynamicArguments(base, head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + }) + } + return err } // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. func (repo *Repository) GetDiffBinary(base, head string, w io.Writer) error { - return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head).Run(&RunOpts{ - Dir: repo.Path, - Stdout: w, - }) + stderr := new(bytes.Buffer) + err := NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base + "..." + head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + Stderr: stderr, + }) + if err != nil && bytes.Contains(stderr.Bytes(), []byte("no merge base")) { + return NewCommand(repo.Ctx, "diff", "-p", "--binary", "--histogram").AddDynamicArguments(base, head). + Run(&RunOpts{ + Dir: repo.Path, + Stdout: w, + }) + } + return err } // GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` diff --git a/modules/git/repo_compare_test.go b/modules/git/repo_compare_test.go index 99838731867..454ed6b9f85 100644 --- a/modules/git/repo_compare_test.go +++ b/modules/git/repo_compare_test.go @@ -72,7 +72,7 @@ func TestReadPatch(t *testing.T) { assert.Empty(t, noFile) assert.Empty(t, noCommit) assert.Len(t, oldCommit, 40) - assert.True(t, oldCommit == "6e8e2a6f9efd71dbe6917816343ed8415ad696c3") + assert.Equal(t, "6e8e2a6f9efd71dbe6917816343ed8415ad696c3", oldCommit) } func TestReadWritePullHead(t *testing.T) { @@ -113,7 +113,7 @@ func TestReadWritePullHead(t *testing.T) { } assert.Len(t, headContents, 40) - assert.True(t, headContents == newCommit) + assert.Equal(t, headContents, newCommit) // Remove file after the test err = repo.RemoveReference(PullPrefix + "1/head") diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go index 8eaa17cb041..850ec655029 100644 --- a/modules/git/repo_ref.go +++ b/modules/git/repo_ref.go @@ -61,3 +61,31 @@ func parseTags(refs []string) []string { } return results } + +// UnstableGuessRefByShortName does the best guess to see whether a "short name" provided by user is a branch, tag or commit. +// It could guess wrongly if the input is already ambiguous. For example: +// * "refs/heads/the-name" vs "refs/heads/refs/heads/the-name" +// * "refs/tags/1234567890" vs commit "1234567890" +// In most cases, it SHOULD AVOID using this function, unless there is an irresistible reason (eg: make API friendly to end users) +// If the function is used, the caller SHOULD CHECK the ref type carefully. +func (repo *Repository) UnstableGuessRefByShortName(shortName string) RefName { + if repo.IsBranchExist(shortName) { + return RefNameFromBranch(shortName) + } + if repo.IsTagExist(shortName) { + return RefNameFromTag(shortName) + } + if strings.HasPrefix(shortName, "refs/") { + if repo.IsReferenceExist(shortName) { + return RefName(shortName) + } + } + commit, err := repo.GetCommit(shortName) + if err == nil { + commitIDString := commit.ID.String() + if strings.HasPrefix(commitIDString, shortName) { + return RefName(commitIDString) + } + } + return "" +} diff --git a/modules/globallock/globallock_test.go b/modules/globallock/globallock_test.go index 88a555c86f3..f14c7d656b6 100644 --- a/modules/globallock/globallock_test.go +++ b/modules/globallock/globallock_test.go @@ -64,7 +64,7 @@ func TestLockAndDo(t *testing.T) { } func testLockAndDo(t *testing.T) { - const concurrency = 1000 + const concurrency = 50 ctx := context.Background() count := 0 diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go index 3f1115066a0..991b2f2b7af 100644 --- a/modules/graceful/manager.go +++ b/modules/graceful/manager.go @@ -218,13 +218,13 @@ func (g *Manager) ServerDone() { g.runningServerWaitGroup.Done() } -func (g *Manager) setStateTransition(old, new state) bool { +func (g *Manager) setStateTransition(oldState, newState state) bool { g.lock.Lock() - if g.state != old { + if g.state != oldState { g.lock.Unlock() return false } - g.state = new + g.state = newState g.lock.Unlock() return true } diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go index 21422b281c4..1b18ca1a779 100644 --- a/modules/indexer/internal/bleve/query.go +++ b/modules/indexer/internal/bleve/query.go @@ -35,18 +35,18 @@ func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery { return q } -func NumericRangeInclusiveQuery(min, max optional.Option[int64], field string) *query.NumericRangeQuery { +func NumericRangeInclusiveQuery(minOption, maxOption optional.Option[int64], field string) *query.NumericRangeQuery { var minF, maxF *float64 var minI, maxI *bool - if min.Has() { + if minOption.Has() { minF = new(float64) - *minF = float64(min.Value()) + *minF = float64(minOption.Value()) minI = new(bool) *minI = true } - if max.Has() { + if maxOption.Has() { maxF = new(float64) - *maxF = float64(max.Value()) + *maxF = float64(maxOption.Value()) maxI = new(bool) *maxI = true } diff --git a/modules/indexer/internal/paginator.go b/modules/indexer/internal/paginator.go index ee204bf0471..f1e19740eb7 100644 --- a/modules/indexer/internal/paginator.go +++ b/modules/indexer/internal/paginator.go @@ -10,12 +10,12 @@ import ( ) // ParsePaginator parses a db.Paginator into a skip and limit -func ParsePaginator(paginator *db.ListOptions, max ...int) (int, int) { +func ParsePaginator(paginator *db.ListOptions, maxNums ...int) (int, int) { // Use a very large number to indicate no limit unlimited := math.MaxInt32 - if len(max) > 0 { + if len(maxNums) > 0 { // Some indexer engines have a limit on the page size, respect that - unlimited = max[0] + unlimited = maxNums[0] } if paginator == nil || paginator.IsListAll() { diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index 7ef370e89c5..bf51bd6c148 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -23,7 +23,7 @@ import ( const ( issueIndexerAnalyzer = "issueIndexer" issueIndexerDocType = "issueIndexerDocType" - issueIndexerLatestVersion = 4 + issueIndexerLatestVersion = 5 ) const unicodeNormalizeName = "unicodeNormalize" @@ -75,6 +75,7 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) { docMapping.AddFieldMappingsAt("is_pull", boolFieldMapping) docMapping.AddFieldMappingsAt("is_closed", boolFieldMapping) + docMapping.AddFieldMappingsAt("is_archived", boolFieldMapping) docMapping.AddFieldMappingsAt("label_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("no_label", boolFieldMapping) docMapping.AddFieldMappingsAt("milestone_id", numberFieldMapping) @@ -185,6 +186,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.IsClosed.Has() { queries = append(queries, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed")) } + if options.IsArchived.Has() { + queries = append(queries, inner_bleve.BoolFieldQuery(options.IsArchived.Value(), "is_archived")) + } if options.NoLabelOnly { queries = append(queries, inner_bleve.BoolFieldQuery(true, "no_label")) diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 875a4ca279d..42834f6e886 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -54,8 +54,8 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m RepoIDs: options.RepoIDs, AllPublic: options.AllPublic, RepoCond: nil, - AssigneeID: convertID(options.AssigneeID), - PosterID: convertID(options.PosterID), + AssigneeID: optional.Some(convertID(options.AssigneeID)), + PosterID: options.PosterID, MentionedID: convertID(options.MentionID), ReviewRequestedID: convertID(options.ReviewRequestedID), ReviewedID: convertID(options.ReviewedID), @@ -72,7 +72,7 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issue_m UpdatedAfterUnix: options.UpdatedAfterUnix.Value(), UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(), PriorityRepoID: 0, - IsArchived: optional.None[bool](), + IsArchived: options.IsArchived, Org: nil, Team: nil, User: nil, diff --git a/modules/indexer/issues/dboptions.go b/modules/indexer/issues/dboptions.go index c1f454eeee5..4f6ad96d222 100644 --- a/modules/indexer/issues/dboptions.go +++ b/modules/indexer/issues/dboptions.go @@ -11,11 +11,12 @@ import ( func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOptions { searchOpt := &SearchOptions{ - Keyword: keyword, - RepoIDs: opts.RepoIDs, - AllPublic: opts.AllPublic, - IsPull: opts.IsPull, - IsClosed: opts.IsClosed, + Keyword: keyword, + RepoIDs: opts.RepoIDs, + AllPublic: opts.AllPublic, + IsPull: opts.IsPull, + IsClosed: opts.IsClosed, + IsArchived: opts.IsArchived, } if len(opts.LabelIDs) == 1 && opts.LabelIDs[0] == 0 { @@ -40,14 +41,14 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp if opts.ProjectID > 0 { searchOpt.ProjectID = optional.Some(opts.ProjectID) - } else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places + } else if opts.ProjectID == db.NoConditionID { // FIXME: this is inconsistent from other places searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0) } - if opts.AssigneeID > 0 { - searchOpt.AssigneeID = optional.Some(opts.AssigneeID) - } else if opts.AssigneeID == -1 { // FIXME: this is inconsistent from other places - searchOpt.AssigneeID = optional.Some[int64](0) + if opts.AssigneeID.Value() == db.NoConditionID { + searchOpt.AssigneeID = optional.Some[int64](0) // FIXME: this is inconsistent from other places, 0 means "no assignee" + } else if opts.AssigneeID.Value() != 0 { + searchOpt.AssigneeID = opts.AssigneeID } // See the comment of issues_model.SearchOptions for the reason why we need to convert @@ -62,7 +63,7 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp } searchOpt.ProjectColumnID = convertID(opts.ProjectColumnID) - searchOpt.PosterID = convertID(opts.PosterID) + searchOpt.PosterID = opts.PosterID searchOpt.MentionID = convertID(opts.MentionedID) searchOpt.ReviewedID = convertID(opts.ReviewedID) searchOpt.ReviewRequestedID = convertID(opts.ReviewRequestedID) diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 6f705150097..4c293f3f2a9 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -18,7 +18,7 @@ import ( ) const ( - issueIndexerLatestVersion = 1 + issueIndexerLatestVersion = 2 // multi-match-types, currently only 2 types are used // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types esMultiMatchTypeBestFields = "best_fields" @@ -58,6 +58,7 @@ const ( "is_pull": { "type": "boolean", "index": true }, "is_closed": { "type": "boolean", "index": true }, + "is_archived": { "type": "boolean", "index": true }, "label_ids": { "type": "integer", "index": true }, "no_label": { "type": "boolean", "index": true }, "milestone_id": { "type": "integer", "index": true }, @@ -168,6 +169,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.IsClosed.Has() { query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value())) } + if options.IsArchived.Has() { + query.Must(elastic.NewTermQuery("is_archived", options.IsArchived.Value())) + } if options.NoLabelOnly { query.Must(elastic.NewTermQuery("no_label", true)) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 0dce6541816..06a6a46c234 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -37,6 +37,7 @@ func TestDBSearchIssues(t *testing.T) { t.Run("search issues by ID", searchIssueByID) t.Run("search issues is pr", searchIssueIsPull) t.Run("search issues is closed", searchIssueIsClosed) + t.Run("search issues is archived", searchIssueIsArchived) t.Run("search issues by milestone", searchIssueByMilestoneID) t.Run("search issues by label", searchIssueByLabelID) t.Run("search issues by time", searchIssueByTime) @@ -191,7 +192,7 @@ func searchIssueByID(t *testing.T) { }, { // NOTE: This tests no assignees filtering and also ToSearchOptions() to ensure it will set AssigneeID to 0 when it is passed as -1. - opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: -1}), + opts: *ToSearchOptions("", &issues.IssuesOptions{AssigneeID: optional.Some(db.NoConditionID)}), expectedIDs: []int64{22, 21, 16, 15, 14, 13, 12, 11, 20, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2}, }, { @@ -298,6 +299,33 @@ func searchIssueIsClosed(t *testing.T) { } } +func searchIssueIsArchived(t *testing.T) { + tests := []struct { + opts SearchOptions + expectedIDs []int64 + }{ + { + SearchOptions{ + IsArchived: optional.Some(false), + }, + []int64{22, 21, 17, 16, 15, 13, 12, 11, 20, 6, 5, 19, 18, 10, 7, 4, 9, 8, 3, 2, 1}, + }, + { + SearchOptions{ + IsArchived: optional.Some(true), + }, + []int64{14}, + }, + } + for _, test := range tests { + issueIDs, _, err := SearchIssues(context.TODO(), &test.opts) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, test.expectedIDs, issueIDs) + } +} + func searchIssueByMilestoneID(t *testing.T) { tests := []struct { opts SearchOptions diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index a43c6be0059..09dcbf4804c 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -25,6 +25,7 @@ type IndexerData struct { // Fields used for filtering IsPull bool `json:"is_pull"` IsClosed bool `json:"is_closed"` + IsArchived bool `json:"is_archived"` LabelIDs []int64 `json:"label_ids"` NoLabel bool `json:"no_label"` // True if LabelIDs is empty MilestoneID int64 `json:"milestone_id"` @@ -81,8 +82,9 @@ type SearchOptions struct { RepoIDs []int64 // repository IDs which the issues belong to AllPublic bool // if include all public repositories - IsPull optional.Option[bool] // if the issues is a pull request - IsClosed optional.Option[bool] // if the issues is closed + IsPull optional.Option[bool] // if the issues is a pull request + IsClosed optional.Option[bool] // if the issues is closed + IsArchived optional.Option[bool] // if the repo is archived IncludedLabelIDs []int64 // labels the issues have ExcludedLabelIDs []int64 // labels the issues don't have diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 16f0a78ec04..94ce8520bf6 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -113,7 +113,7 @@ var cases = []*testIndexerCase{ }, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) assert.Equal(t, len(data), int(result.Total)) }, }, @@ -176,7 +176,7 @@ var cases = []*testIndexerCase{ IsPull: optional.Some(false), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.False(t, data[v.ID].IsPull) } @@ -192,7 +192,7 @@ var cases = []*testIndexerCase{ IsPull: optional.Some(true), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.True(t, data[v.ID].IsPull) } @@ -208,7 +208,7 @@ var cases = []*testIndexerCase{ IsClosed: optional.Some(false), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.False(t, data[v.ID].IsClosed) } @@ -224,7 +224,7 @@ var cases = []*testIndexerCase{ IsClosed: optional.Some(true), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.True(t, data[v.ID].IsClosed) } @@ -274,7 +274,7 @@ var cases = []*testIndexerCase{ MilestoneIDs: []int64{1, 2, 6}, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, []int64{1, 2, 6}, data[v.ID].MilestoneID) } @@ -292,7 +292,7 @@ var cases = []*testIndexerCase{ MilestoneIDs: []int64{0}, }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(0), data[v.ID].MilestoneID) } @@ -310,7 +310,7 @@ var cases = []*testIndexerCase{ ProjectID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(1), data[v.ID].ProjectID) } @@ -328,7 +328,7 @@ var cases = []*testIndexerCase{ ProjectID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(0), data[v.ID].ProjectID) } @@ -346,7 +346,7 @@ var cases = []*testIndexerCase{ ProjectColumnID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(1), data[v.ID].ProjectColumnID) } @@ -364,7 +364,7 @@ var cases = []*testIndexerCase{ ProjectColumnID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(0), data[v.ID].ProjectColumnID) } @@ -382,7 +382,7 @@ var cases = []*testIndexerCase{ PosterID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(1), data[v.ID].PosterID) } @@ -400,7 +400,7 @@ var cases = []*testIndexerCase{ AssigneeID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(1), data[v.ID].AssigneeID) } @@ -418,7 +418,7 @@ var cases = []*testIndexerCase{ AssigneeID: optional.Some(int64(0)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Equal(t, int64(0), data[v.ID].AssigneeID) } @@ -436,7 +436,7 @@ var cases = []*testIndexerCase{ MentionID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, data[v.ID].MentionIDs, int64(1)) } @@ -454,7 +454,7 @@ var cases = []*testIndexerCase{ ReviewedID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, data[v.ID].ReviewedIDs, int64(1)) } @@ -472,7 +472,7 @@ var cases = []*testIndexerCase{ ReviewRequestedID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, data[v.ID].ReviewRequestedIDs, int64(1)) } @@ -490,7 +490,7 @@ var cases = []*testIndexerCase{ SubscriberID: optional.Some(int64(1)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.Contains(t, data[v.ID].SubscriberIDs, int64(1)) } @@ -509,7 +509,7 @@ var cases = []*testIndexerCase{ UpdatedBeforeUnix: optional.Some(int64(30)), }, Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { - assert.Equal(t, 5, len(result.Hits)) + assert.Len(t, result.Hits, 5) for _, v := range result.Hits { assert.GreaterOrEqual(t, data[v.ID].UpdatedUnix, int64(20)) assert.LessOrEqual(t, data[v.ID].UpdatedUnix, int64(30)) diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index 93323193392..1066e962725 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -18,7 +18,7 @@ import ( ) const ( - issueIndexerLatestVersion = 3 + issueIndexerLatestVersion = 4 // TODO: make this configurable if necessary maxTotalHits = 10000 @@ -61,6 +61,7 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer { "is_public", "is_pull", "is_closed", + "is_archived", "label_ids", "no_label", "milestone_id", @@ -145,6 +146,9 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( if options.IsClosed.Has() { query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.Value())) } + if options.IsArchived.Has() { + query.And(inner_meilisearch.NewFilterEq("is_archived", options.IsArchived.Value())) + } if options.NoLabelOnly { query.And(inner_meilisearch.NewFilterEq("no_label", true)) diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index e752ae6f243..deb19adc49d 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -101,6 +101,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD Comments: comments, IsPull: issue.IsPull, IsClosed: issue.IsClosed, + IsArchived: issue.Repo.IsArchived, LabelIDs: labels, NoLabel: len(labels) == 0, MilestoneID: issue.MilestoneID, diff --git a/modules/lfs/http_client.go b/modules/lfs/http_client.go index 3060e25754c..3acd23b8f73 100644 --- a/modules/lfs/http_client.go +++ b/modules/lfs/http_client.go @@ -72,7 +72,10 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin url := fmt.Sprintf("%s/objects/batch", c.endpoint) - request := &BatchRequest{operation, c.transferNames(), nil, objects} + // `ref` is an "optional object describing the server ref that the objects belong to" + // but some (incorrect) lfs servers require it, so maybe adding an empty ref here doesn't break the correct ones. + // https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37 + request := &BatchRequest{operation, c.transferNames(), &Reference{}, objects} payload := new(bytes.Buffer) err := json.NewEncoder(payload).Encode(request) if err != nil { @@ -236,6 +239,7 @@ func createRequest(ctx context.Context, method, url string, headers map[string]s req.Header.Set(key, value) } req.Header.Set("Accept", AcceptHeader) + req.Header.Set("User-Agent", UserAgentHeader) return req, nil } diff --git a/modules/lfs/shared.go b/modules/lfs/shared.go index a4326b57b2f..cd9488e3dbf 100644 --- a/modules/lfs/shared.go +++ b/modules/lfs/shared.go @@ -14,8 +14,12 @@ import ( const ( // MediaType contains the media type for LFS server requests MediaType = "application/vnd.git-lfs+json" - // Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served + // AcceptHeader Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8" + // UserAgentHeader Add User-Agent for gitea's self-implemented lfs client, + // and the version is consistent with the latest version of git lfs can be avoided incompatibilities. + // Some lfs servers will check this + UserAgentHeader = "git-lfs/3.6.0 (Gitea)" ) // BatchRequest contains multiple requests processed in one batch operation. diff --git a/modules/lfs/transferadapter_test.go b/modules/lfs/transferadapter_test.go index 7fec137efe5..a430b71a5f1 100644 --- a/modules/lfs/transferadapter_test.go +++ b/modules/lfs/transferadapter_test.go @@ -96,7 +96,7 @@ func TestBasicTransferAdapter(t *testing.T) { for n, c := range cases { _, err := a.Download(context.Background(), c.link) if len(c.expectederror) > 0 { - assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) + assert.Contains(t, err.Error(), c.expectederror, "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) } else { assert.NoError(t, err, "case %d", n) } @@ -129,7 +129,7 @@ func TestBasicTransferAdapter(t *testing.T) { for n, c := range cases { err := a.Upload(context.Background(), c.link, p, bytes.NewBufferString("dummy")) if len(c.expectederror) > 0 { - assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) + assert.Contains(t, err.Error(), c.expectederror, "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) } else { assert.NoError(t, err, "case %d", n) } @@ -162,7 +162,7 @@ func TestBasicTransferAdapter(t *testing.T) { for n, c := range cases { err := a.Verify(context.Background(), c.link, p) if len(c.expectederror) > 0 { - assert.True(t, strings.Contains(err.Error(), c.expectederror), "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) + assert.Contains(t, err.Error(), c.expectederror, "case %d: '%s' should contain '%s'", n, err.Error(), c.expectederror) } else { assert.NoError(t, err, "case %d", n) } diff --git a/modules/log/event_format.go b/modules/log/event_format.go index d9dbebf8315..0b8d1cec791 100644 --- a/modules/log/event_format.go +++ b/modules/log/event_format.go @@ -110,10 +110,10 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms buf = append(buf, ' ') } if flags&(Ltime|Lmicroseconds) != 0 { - hour, min, sec := t.Clock() + hour, minNum, sec := t.Clock() buf = itoa(buf, hour, 2) buf = append(buf, ':') - buf = itoa(buf, min, 2) + buf = itoa(buf, minNum, 2) buf = append(buf, ':') buf = itoa(buf, sec, 2) if flags&Lmicroseconds != 0 { diff --git a/modules/log/logger_test.go b/modules/log/logger_test.go index 70222f64f5c..0de14eb411e 100644 --- a/modules/log/logger_test.go +++ b/modules/log/logger_test.go @@ -56,7 +56,7 @@ func TestLogger(t *testing.T) { logger := NewLoggerWithWriters(context.Background(), "test") dump := logger.DumpWriters() - assert.EqualValues(t, 0, len(dump)) + assert.Empty(t, dump) assert.EqualValues(t, NONE, logger.GetLevel()) assert.False(t, logger.IsEnabled()) @@ -69,7 +69,7 @@ func TestLogger(t *testing.T) { assert.EqualValues(t, DEBUG, logger.GetLevel()) dump = logger.DumpWriters() - assert.EqualValues(t, 2, len(dump)) + assert.Len(t, dump, 2) logger.Trace("trace-level") // this level is not logged logger.Debug("debug-level") diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 9419350e615..159d7129557 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -278,12 +278,12 @@ func TestRender_AutoLink(t *testing.T) { test := func(input, expected string) { var buffer strings.Builder err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer) - assert.Equal(t, err, nil) + assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) buffer.Reset() err = PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer) - assert.Equal(t, err, nil) + assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String())) } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index f77db9eb38e..a14c0cad597 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -78,26 +78,23 @@ func (r *GlodmarkRender) Renderer() renderer.Renderer { func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) { if entering { - language, _ := c.Language() - if language == nil { - language = []byte("text") - } + languageBytes, _ := c.Language() + languageStr := giteautil.IfZero(string(languageBytes), "text") - languageStr := string(language) - - preClasses := []string{"code-block"} + preClasses := "code-block" if languageStr == "mermaid" || languageStr == "math" { - preClasses = append(preClasses, "is-loading") + preClasses += " is-loading" } - err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `
`, strings.Join(preClasses, " "))
+		err := r.ctx.RenderInternal.FormatWithSafeAttrs(w, `
`, preClasses)
 		if err != nil {
 			return
 		}
 
-		// include language-x class as part of commonmark spec
-		// the "display" class is used by "js/markup/math.js" to render the code element as a block
-		err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``, string(language))
+		// include language-x class as part of commonmark spec, "chroma" class is used to highlight the code
+		// the "display" class is used by "js/markup/math.ts" to render the code element as a block
+		// the "math.ts" strictly depends on the structure: 
...
+ err = r.ctx.RenderInternal.FormatWithSafeAttrs(w, ``, languageStr) if err != nil { return } @@ -128,7 +125,12 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { ), highlighting.WithWrapperRenderer(r.highlightingRenderer), ), - math.NewExtension(&ctx.RenderInternal, math.Enabled(setting.Markdown.EnableMath)), + math.NewExtension(&ctx.RenderInternal, math.Options{ + Enabled: setting.Markdown.EnableMath, + ParseDollarInline: true, + ParseDollarBlock: true, + ParseSquareBlock: true, // TODO: this is a bad syntax, it should be deprecated in the future (by some config options) + }), meta.Meta, ), goldmark.WithParserOptions( diff --git a/modules/markup/markdown/markdown_math_test.go b/modules/markup/markdown/markdown_math_test.go index 0e5adeeac8d..813f050965a 100644 --- a/modules/markup/markdown/markdown_math_test.go +++ b/modules/markup/markdown/markdown_math_test.go @@ -12,31 +12,32 @@ import ( "github.com/stretchr/testify/assert" ) +const nl = "\n" + func TestMathRender(t *testing.T) { - const nl = "\n" testcases := []struct { testcase string expected string }{ { "$a$", - `

a

` + nl, + `

a

` + nl, }, { "$ a $", - `

a

` + nl, + `

a

` + nl, }, { "$a$ $b$", - `

a b

` + nl, + `

a b

` + nl, }, { `\(a\) \(b\)`, - `

a b

` + nl, + `

a b

` + nl, }, { `$a$.`, - `

a.

` + nl, + `

a.

` + nl, }, { `.$a$`, @@ -64,23 +65,39 @@ func TestMathRender(t *testing.T) { }, { "$a$ ($b$) [$c$] {$d$}", - `

a (b) [$c$] {$d$}

` + nl, + `

a (b) [$c$] {$d$}

` + nl, }, { "$$a$$", - `
a
` + nl, + `a` + nl, }, { "$$a$$ test", - `

a test

` + nl, + `

a test

` + nl, }, { "test $$a$$", - `

test a

` + nl, + `

test a

` + nl, }, { - "foo $x=\\$$ bar", - `

foo x=\$ bar

` + nl, + `foo $x=\$$ bar`, + `

foo x=\$ bar

` + nl, + }, + { + `$\text{$b$}$`, + `

\text{$b$}

` + nl, + }, + { + "a$`b`$c", + `

abc

` + nl, + }, + { + "a $`b`$ c", + `

a b c

` + nl, + }, + { + "a$``b``$c x$```y```$z", + `

abc xyz

` + nl, }, } @@ -106,7 +123,7 @@ func TestMathRenderBlockIndent(t *testing.T) { \alpha \] `, - `

+			`

 \alpha
 
`, @@ -118,20 +135,42 @@ func TestMathRenderBlockIndent(t *testing.T) { \alpha \] `, - `

+			`

 \alpha
 
+`, + }, + { + "indent-2-mismatch", + ` + \[ +a + b + c + d + \] +`, + `

+a
+b
+c
+ d
+
`, }, { "indent-2", ` \[ - \alpha + a + b + c \] `, - `

-\alpha
+			`

+a
+ b
+c
 
`, }, @@ -139,7 +178,7 @@ func TestMathRenderBlockIndent(t *testing.T) { "indent-0-oneline", `$$ x $$ foo`, - `
 x 
+ ` x

foo

`, }, @@ -147,10 +186,53 @@ foo`, "indent-3-oneline", ` $$ x $$ foo`, - `
 x 
+ ` x

foo

`, }, + { + "quote-block", + ` +> \[ +> a +> \] +> \[ +> b +> \] +`, + `
+

+a
+
+

+b
+
+
+`, + }, + { + "list-block", + ` +1. a + \[ + x + \] +2. b`, + `
    +
  1. a +
    
    +x
    +
    +
  2. +
  3. b
  4. +
+`, + }, + { + "inline-non-math", + `\[x]`, + `

[x]

` + nl, + }, } for _, test := range testcases { diff --git a/modules/markup/markdown/math/block_node.go b/modules/markup/markdown/math/block_node.go index 10d17ff8d3c..d2293133cda 100644 --- a/modules/markup/markdown/math/block_node.go +++ b/modules/markup/markdown/math/block_node.go @@ -11,6 +11,7 @@ type Block struct { Dollars bool Indent int Closed bool + Inline bool } // KindBlock is the node kind for math blocks diff --git a/modules/markup/markdown/math/block_parser.go b/modules/markup/markdown/math/block_parser.go index f31cfb09ad1..2c5553550a7 100644 --- a/modules/markup/markdown/math/block_parser.go +++ b/modules/markup/markdown/math/block_parser.go @@ -6,6 +6,8 @@ package math import ( "bytes" + giteaUtil "code.gitea.io/gitea/modules/util" + "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" @@ -13,13 +15,19 @@ import ( ) type blockParser struct { - parseDollars bool + parseDollars bool + parseSquare bool + endBytesDollars []byte + endBytesSquare []byte } // NewBlockParser creates a new math BlockParser -func NewBlockParser(parseDollarBlocks bool) parser.BlockParser { +func NewBlockParser(parseDollars, parseSquare bool) parser.BlockParser { return &blockParser{ - parseDollars: parseDollarBlocks, + parseDollars: parseDollars, + parseSquare: parseSquare, + endBytesDollars: []byte{'$', '$'}, + endBytesSquare: []byte{'\\', ']'}, } } @@ -34,7 +42,7 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex var dollars bool if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' { dollars = true - } else if line[pos] == '\\' && line[pos+1] == '[' { + } else if b.parseSquare && line[pos] == '\\' && line[pos+1] == '[' { if len(line[pos:]) >= 3 && line[pos+2] == '!' && bytes.Contains(line[pos:], []byte(`\]`)) { // do not process escaped attention block: "> \[!NOTE\]" return nil, parser.NoChildren @@ -47,13 +55,10 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex node := NewBlock(dollars, pos) // Now we need to check if the ending block is on the segment... - endBytes := []byte{'\\', ']'} - if dollars { - endBytes = []byte{'$', '$'} - } + endBytes := giteaUtil.Iif(dollars, b.endBytesDollars, b.endBytesSquare) idx := bytes.Index(line[pos+2:], endBytes) if idx >= 0 { - // for case $$ ... $$ any other text + // for case: "$$ ... $$ any other text" (this case will be handled by the inline parser) for i := pos + 2 + idx + 2; i < len(line); i++ { if line[i] != ' ' && line[i] != '\n' { return nil, parser.NoChildren @@ -63,9 +68,17 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex segment.Stop = segment.Start + idx node.Lines().Append(segment) node.Closed = true + node.Inline = true return node, parser.Close | parser.NoChildren } + // for case "\[ ... ]" (no close marker on the same line) + for i := pos + 2 + idx + 2; i < len(line); i++ { + if line[i] != ' ' && line[i] != '\n' { + return nil, parser.NoChildren + } + } + segment.Start += pos + 2 node.Lines().Append(segment) return node, parser.NoChildren @@ -79,27 +92,19 @@ func (b *blockParser) Continue(node ast.Node, reader text.Reader, pc parser.Cont } line, segment := reader.PeekLine() - w, pos := util.IndentWidth(line, 0) + w, pos := util.IndentWidth(line, reader.LineOffset()) if w < 4 { - if block.Dollars { - i := pos - for ; i < len(line) && line[i] == '$'; i++ { - } - length := i - pos - if length >= 2 && util.IsBlank(line[i:]) { - reader.Advance(segment.Stop - segment.Start - segment.Padding) - block.Closed = true + endBytes := giteaUtil.Iif(block.Dollars, b.endBytesDollars, b.endBytesSquare) + if bytes.HasPrefix(line[pos:], endBytes) && util.IsBlank(line[pos+len(endBytes):]) { + if util.IsBlank(line[pos+len(endBytes):]) { + newline := giteaUtil.Iif(line[len(line)-1] != '\n', 0, 1) + reader.Advance(segment.Stop - segment.Start - newline + segment.Padding) return parser.Close } - } else if len(line[pos:]) > 1 && line[pos] == '\\' && line[pos+1] == ']' && util.IsBlank(line[pos+2:]) { - reader.Advance(segment.Stop - segment.Start - segment.Padding) - block.Closed = true - return parser.Close } } - - pos, padding := util.IndentPosition(line, 0, block.Indent) - seg := text.NewSegmentPadding(segment.Start+pos, segment.Stop, padding) + start := segment.Start + giteaUtil.Iif(pos > block.Indent, block.Indent, pos) + seg := text.NewSegmentPadding(start, segment.Stop, segment.Padding) node.Lines().Append(seg) return parser.Continue | parser.NoChildren } diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go index 0d2a966102e..c29f0618821 100644 --- a/modules/markup/markdown/math/block_renderer.go +++ b/modules/markup/markdown/math/block_renderer.go @@ -5,12 +5,24 @@ package math import ( "code.gitea.io/gitea/modules/markup/internal" + giteaUtil "code.gitea.io/gitea/modules/util" gast "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/util" ) +// Block render output: +//
...
+// +// Keep in mind that there is another "code block" render in "func (r *GlodmarkRender) highlightingRenderer" +// "highlightingRenderer" outputs the math block with extra "chroma" class: +//
...
+// +// Special classes: +// * "is-loading": show a loading indicator +// * "display": used by JS to decide to render as a block, otherwise render as inline + // BlockRenderer represents a renderer for math Blocks type BlockRenderer struct { renderInternal *internal.RenderInternal @@ -37,10 +49,11 @@ func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) func (r *BlockRenderer) renderBlock(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) { n := node.(*Block) if entering { - _ = r.renderInternal.FormatWithSafeAttrs(w, `
`)
+		code := giteaUtil.Iif(n.Inline, "", `
`) + ``
+		_ = r.renderInternal.FormatWithSafeAttrs(w, code)
 		r.writeLines(w, source, n)
 	} else {
-		_, _ = w.WriteString(`
` + "\n") + _, _ = w.WriteString(`
` + giteaUtil.Iif(n.Inline, "", `
`) + "\n") } return gast.WalkContinue, nil } diff --git a/modules/markup/markdown/math/inline_block_node.go b/modules/markup/markdown/math/inline_block_node.go deleted file mode 100644 index c92d0c8d84b..00000000000 --- a/modules/markup/markdown/math/inline_block_node.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package math - -import ( - "github.com/yuin/goldmark/ast" -) - -// InlineBlock represents inline math e.g. $$...$$ -type InlineBlock struct { - Inline -} - -// InlineBlock implements InlineBlock. -func (n *InlineBlock) InlineBlock() {} - -// KindInlineBlock is the kind for math inline block -var KindInlineBlock = ast.NewNodeKind("MathInlineBlock") - -// Kind returns KindInlineBlock -func (n *InlineBlock) Kind() ast.NodeKind { - return KindInlineBlock -} - -// NewInlineBlock creates a new ast math inline block node -func NewInlineBlock() *InlineBlock { - return &InlineBlock{ - Inline{}, - } -} diff --git a/modules/markup/markdown/math/inline_node.go b/modules/markup/markdown/math/inline_node.go index 2221a251bf1..1e4034d54b9 100644 --- a/modules/markup/markdown/math/inline_node.go +++ b/modules/markup/markdown/math/inline_node.go @@ -8,7 +8,7 @@ import ( "github.com/yuin/goldmark/util" ) -// Inline represents inline math e.g. $...$ or \(...\) +// Inline struct represents inline math e.g. $...$ or \(...\) type Inline struct { ast.BaseInline } diff --git a/modules/markup/markdown/math/inline_parser.go b/modules/markup/markdown/math/inline_parser.go index 56ae3d57eb0..a57abe9f9b0 100644 --- a/modules/markup/markdown/math/inline_parser.go +++ b/modules/markup/markdown/math/inline_parser.go @@ -12,31 +12,25 @@ import ( ) type inlineParser struct { - start []byte - end []byte + trigger []byte + endBytesSingleDollar []byte + endBytesDoubleDollar []byte + endBytesBracket []byte } var defaultInlineDollarParser = &inlineParser{ - start: []byte{'$'}, - end: []byte{'$'}, -} - -var defaultDualDollarParser = &inlineParser{ - start: []byte{'$', '$'}, - end: []byte{'$', '$'}, + trigger: []byte{'$'}, + endBytesSingleDollar: []byte{'$'}, + endBytesDoubleDollar: []byte{'$', '$'}, } func NewInlineDollarParser() parser.InlineParser { return defaultInlineDollarParser } -func NewInlineDualDollarParser() parser.InlineParser { - return defaultDualDollarParser -} - var defaultInlineBracketParser = &inlineParser{ - start: []byte{'\\', '('}, - end: []byte{'\\', ')'}, + trigger: []byte{'\\', '('}, + endBytesBracket: []byte{'\\', ')'}, } func NewInlineBracketParser() parser.InlineParser { @@ -45,7 +39,7 @@ func NewInlineBracketParser() parser.InlineParser { // Trigger triggers this parser on $ or \ func (parser *inlineParser) Trigger() []byte { - return parser.start + return parser.trigger } func isPunctuation(b byte) bool { @@ -64,32 +58,60 @@ func isAlphanumeric(b byte) bool { func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { line, _ := block.PeekLine() - if !bytes.HasPrefix(line, parser.start) { + if !bytes.HasPrefix(line, parser.trigger) { // We'll catch this one on the next time round return nil } - precedingCharacter := block.PrecendingCharacter() - if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) { - // need to exclude things like `a$` from being considered a start - return nil + var startMarkLen int + var stopMark []byte + checkSurrounding := true + if line[0] == '$' { + startMarkLen = 1 + stopMark = parser.endBytesSingleDollar + if len(line) > 1 { + if line[1] == '$' { + startMarkLen = 2 + stopMark = parser.endBytesDoubleDollar + } else if line[1] == '`' { + pos := 1 + for ; pos < len(line) && line[pos] == '`'; pos++ { + } + startMarkLen = pos + stopMark = bytes.Repeat([]byte{'`'}, pos) + stopMark[len(stopMark)-1] = '$' + checkSurrounding = false + } + } + } else { + startMarkLen = 2 + stopMark = parser.endBytesBracket + } + + if checkSurrounding { + precedingCharacter := block.PrecendingCharacter() + if precedingCharacter < 256 && (isAlphanumeric(byte(precedingCharacter)) || isPunctuation(byte(precedingCharacter))) { + // need to exclude things like `a$` from being considered a start + return nil + } } // move the opener marker point at the start of the text - opener := len(parser.start) + opener := startMarkLen // Now look for an ending line + depth := 0 ender := -1 for i := opener; i < len(line); i++ { - if bytes.HasPrefix(line[i:], parser.end) { + if depth == 0 && bytes.HasPrefix(line[i:], stopMark) { succeedingCharacter := byte(0) - if i+len(parser.end) < len(line) { - succeedingCharacter = line[i+len(parser.end)] + if i+len(stopMark) < len(line) { + succeedingCharacter = line[i+len(stopMark)] } // check valid ending character isValidEndingChar := isPunctuation(succeedingCharacter) || isBracket(succeedingCharacter) || succeedingCharacter == ' ' || succeedingCharacter == '\n' || succeedingCharacter == 0 - if !isValidEndingChar { + if checkSurrounding && !isValidEndingChar { break } ender = i @@ -99,6 +121,11 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser. i++ continue } + if line[i] == '{' { + depth++ + } else if line[i] == '}' { + depth-- + } } if ender == -1 { return nil @@ -106,21 +133,12 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser. block.Advance(opener) _, pos := block.Position() - var node ast.Node - if parser == defaultDualDollarParser { - node = NewInlineBlock() - } else { - node = NewInline() - } + node := NewInline() + segment := pos.WithStop(pos.Start + ender - opener) node.AppendChild(node, ast.NewRawTextSegment(segment)) - block.Advance(ender - opener + len(parser.end)) - - if parser == defaultDualDollarParser { - trimBlock(&(node.(*InlineBlock)).Inline, block) - } else { - trimBlock(node.(*Inline), block) - } + block.Advance(ender - opener + len(stopMark)) + trimBlock(node, block) return node } diff --git a/modules/markup/markdown/math/inline_renderer.go b/modules/markup/markdown/math/inline_renderer.go index 0cff4f1e74e..d000a7b317a 100644 --- a/modules/markup/markdown/math/inline_renderer.go +++ b/modules/markup/markdown/math/inline_renderer.go @@ -13,6 +13,9 @@ import ( "github.com/yuin/goldmark/util" ) +// Inline render output: +// ... + // InlineRenderer is an inline renderer type InlineRenderer struct { renderInternal *internal.RenderInternal @@ -25,11 +28,7 @@ func NewInlineRenderer(renderInternal *internal.RenderInternal) renderer.NodeRen func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { if entering { - extraClass := "" - if _, ok := n.(*InlineBlock); ok { - extraClass = "display " - } - _ = r.renderInternal.FormatWithSafeAttrs(w, ``, extraClass) + _ = r.renderInternal.FormatWithSafeAttrs(w, ``) for c := n.FirstChild(); c != nil; c = c.NextSibling() { segment := c.(*ast.Text).Segment value := util.EscapeHTML(segment.Value(source)) @@ -51,5 +50,4 @@ func (r *InlineRenderer) renderInline(w util.BufWriter, source []byte, n ast.Nod // RegisterFuncs registers the renderer for inline math nodes func (r *InlineRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(KindInline, r.renderInline) - reg.Register(KindInlineBlock, r.renderInline) } diff --git a/modules/markup/markdown/math/math.go b/modules/markup/markdown/math/math.go index 7e8defcd4a1..a6ff593d626 100644 --- a/modules/markup/markdown/math/math.go +++ b/modules/markup/markdown/math/math.go @@ -5,6 +5,7 @@ package math import ( "code.gitea.io/gitea/modules/markup/internal" + giteaUtil "code.gitea.io/gitea/modules/util" "github.com/yuin/goldmark" "github.com/yuin/goldmark/parser" @@ -12,70 +13,45 @@ import ( "github.com/yuin/goldmark/util" ) +type Options struct { + Enabled bool + ParseDollarInline bool + ParseDollarBlock bool + ParseSquareBlock bool +} + // Extension is a math extension type Extension struct { - renderInternal *internal.RenderInternal - enabled bool - parseDollarInline bool - parseDollarBlock bool -} - -// Option is the interface Options should implement -type Option interface { - SetOption(e *Extension) -} - -type extensionFunc func(e *Extension) - -func (fn extensionFunc) SetOption(e *Extension) { - fn(e) -} - -// Enabled enables or disables this extension -func Enabled(enable ...bool) Option { - value := true - if len(enable) > 0 { - value = enable[0] - } - return extensionFunc(func(e *Extension) { - e.enabled = value - }) + renderInternal *internal.RenderInternal + options Options } // NewExtension creates a new math extension with the provided options -func NewExtension(renderInternal *internal.RenderInternal, opts ...Option) *Extension { +func NewExtension(renderInternal *internal.RenderInternal, opts ...Options) *Extension { + opt := giteaUtil.OptionalArg(opts) r := &Extension{ - renderInternal: renderInternal, - enabled: true, - parseDollarBlock: true, - parseDollarInline: true, - } - - for _, o := range opts { - o.SetOption(r) + renderInternal: renderInternal, + options: opt, } return r } // Extend extends goldmark with our parsers and renderers func (e *Extension) Extend(m goldmark.Markdown) { - if !e.enabled { + if !e.options.Enabled { return } - m.Parser().AddOptions(parser.WithBlockParsers( - util.Prioritized(NewBlockParser(e.parseDollarBlock), 701), - )) - - inlines := []util.PrioritizedValue{ - util.Prioritized(NewInlineBracketParser(), 501), - } - if e.parseDollarInline { - inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 503), - util.Prioritized(NewInlineDualDollarParser(), 502)) + inlines := []util.PrioritizedValue{util.Prioritized(NewInlineBracketParser(), 501)} + if e.options.ParseDollarInline { + inlines = append(inlines, util.Prioritized(NewInlineDollarParser(), 502)) } m.Parser().AddOptions(parser.WithInlineParsers(inlines...)) + m.Parser().AddOptions(parser.WithBlockParsers( + util.Prioritized(NewBlockParser(e.options.ParseDollarBlock, e.options.ParseSquareBlock), 701), + )) + m.Renderer().AddOptions(renderer.WithNodeRenderers( util.Prioritized(NewBlockRenderer(e.renderInternal), 501), util.Prioritized(NewInlineRenderer(e.renderInternal), 502), diff --git a/modules/packages/conan/conanfile_parser_test.go b/modules/packages/conan/conanfile_parser_test.go index 58015701841..aabafd5f64f 100644 --- a/modules/packages/conan/conanfile_parser_test.go +++ b/modules/packages/conan/conanfile_parser_test.go @@ -40,7 +40,7 @@ class ConanPackageConan(ConanFile): func TestParseConanfile(t *testing.T) { metadata, err := ParseConanfile(strings.NewReader(contentConanfile)) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, license, metadata.License) assert.Equal(t, author, metadata.Author) assert.Equal(t, homepage, metadata.ProjectURL) diff --git a/modules/packages/conan/conaninfo_parser_test.go b/modules/packages/conan/conaninfo_parser_test.go index 556a4b939ee..f6510ca6670 100644 --- a/modules/packages/conan/conaninfo_parser_test.go +++ b/modules/packages/conan/conaninfo_parser_test.go @@ -50,7 +50,7 @@ const ( func TestParseConaninfo(t *testing.T) { info, err := ParseConaninfo(strings.NewReader(contentConaninfo)) assert.NotNil(t, info) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal( t, map[string]string{ diff --git a/modules/queue/base_test.go b/modules/queue/base_test.go index c5bf526ae67..01b52b3c16e 100644 --- a/modules/queue/base_test.go +++ b/modules/queue/base_test.go @@ -46,10 +46,10 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error) assert.NoError(t, err) if !isUnique { assert.EqualValues(t, 2, cnt) - assert.EqualValues(t, false, has) // non-unique queues don't check for duplicates + assert.False(t, has) // non-unique queues don't check for duplicates } else { assert.EqualValues(t, 1, cnt) - assert.EqualValues(t, true, has) + assert.True(t, has) } // push another item @@ -101,7 +101,7 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error) pushBlockTime = 30 * time.Millisecond err = q.PushItem(ctx, []byte("item-full")) assert.ErrorIs(t, err, context.DeadlineExceeded) - assert.True(t, time.Since(timeStart) >= pushBlockTime*2/3) + assert.GreaterOrEqual(t, time.Since(timeStart), pushBlockTime*2/3) pushBlockTime = oldPushBlockTime // remove all diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go index d66253ff664..c0841a1752a 100644 --- a/modules/queue/workerqueue_test.go +++ b/modules/queue/workerqueue_test.go @@ -172,8 +172,8 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett q2() // restart the queue to continue to execute the tasks in it - assert.NotZero(t, len(tasksQ1)) - assert.NotZero(t, len(tasksQ2)) + assert.NotEmpty(t, tasksQ1) + assert.NotEmpty(t, tasksQ2) assert.EqualValues(t, testCount, len(tasksQ1)+len(tasksQ2)) } diff --git a/modules/references/references.go b/modules/references/references.go index 2889430bcf6..6e549cb8758 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -164,9 +164,9 @@ func newKeywords() { }) } -func doNewKeywords(close, reopen []string) { - issueCloseKeywordsPat = makeKeywordsPat(close) - issueReopenKeywordsPat = makeKeywordsPat(reopen) +func doNewKeywords(closeKeywords, reopenKeywords []string) { + issueCloseKeywordsPat = makeKeywordsPat(closeKeywords) + issueReopenKeywordsPat = makeKeywordsPat(reopenKeywords) } // getGiteaHostName returns a normalized string with the local host name, with no scheme or port information diff --git a/modules/references/references_test.go b/modules/references/references_test.go index e5a0d60fe3b..e224c919e92 100644 --- a/modules/references/references_test.go +++ b/modules/references/references_test.go @@ -526,7 +526,7 @@ func TestCustomizeCloseKeywords(t *testing.T) { func TestParseCloseKeywords(t *testing.T) { // Test parsing of CloseKeywords and ReopenKeywords - assert.Len(t, parseKeywords([]string{""}), 0) + assert.Empty(t, parseKeywords([]string{""})) assert.Len(t, parseKeywords([]string{" aa ", " bb ", "99", "#", "", "this is", "cc"}), 3) for _, test := range []struct { diff --git a/modules/repository/fork.go b/modules/repository/fork.go index fbf00087167..d5306340716 100644 --- a/modules/repository/fork.go +++ b/modules/repository/fork.go @@ -9,14 +9,22 @@ import ( "code.gitea.io/gitea/models/organization" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/setting" ) +func CanUserForkBetweenOwners(id1, id2 int64) bool { + if id1 != id2 { + return true + } + return setting.Repository.AllowForkIntoSameOwner +} + // CanUserForkRepo returns true if specified user can fork repository. func CanUserForkRepo(ctx context.Context, user *user_model.User, repo *repo_model.Repository) (bool, error) { if user == nil { return false, nil } - if repo.OwnerID != user.ID && !repo_model.HasForkedRepo(ctx, user.ID, repo.ID) { + if CanUserForkBetweenOwners(repo.OwnerID, user.ID) && !repo_model.HasForkedRepo(ctx, user.ID, repo.ID) { return true, nil } ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, user.ID) diff --git a/modules/repository/fork_test.go b/modules/repository/fork_test.go new file mode 100644 index 00000000000..f8c76d942d1 --- /dev/null +++ b/modules/repository/fork_test.go @@ -0,0 +1,25 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "testing" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestCanUserForkBetweenOwners(t *testing.T) { + defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner) + + setting.Repository.AllowForkIntoSameOwner = true + assert.True(t, CanUserForkBetweenOwners(1, 1)) + assert.True(t, CanUserForkBetweenOwners(1, 2)) + + setting.Repository.AllowForkIntoSameOwner = false + assert.False(t, CanUserForkBetweenOwners(1, 1)) + assert.True(t, CanUserForkBetweenOwners(1, 2)) +} diff --git a/modules/repository/repo_test.go b/modules/repository/repo_test.go index 68980f92f94..f3e7be6d7d7 100644 --- a/modules/repository/repo_test.go +++ b/modules/repository/repo_test.go @@ -62,15 +62,15 @@ func Test_calcSync(t *testing.T) { } inserts, deletes, updates := calcSync(gitTags, dbReleases) - if assert.EqualValues(t, 1, len(inserts), "inserts") { + if assert.Len(t, inserts, 1, "inserts") { assert.EqualValues(t, *gitTags[2], *inserts[0], "inserts equal") } - if assert.EqualValues(t, 1, len(deletes), "deletes") { + if assert.Len(t, deletes, 1, "deletes") { assert.EqualValues(t, 1, deletes[0], "deletes equal") } - if assert.EqualValues(t, 1, len(updates), "updates") { + if assert.Len(t, updates, 1, "updates") { assert.EqualValues(t, *gitTags[1], *updates[0], "updates equal") } } diff --git a/modules/setting/cron_test.go b/modules/setting/cron_test.go index 3187ab18a25..55244d70757 100644 --- a/modules/setting/cron_test.go +++ b/modules/setting/cron_test.go @@ -38,6 +38,6 @@ EXTEND = true _, err = getCronSettings(cfg, "test", extended) assert.NoError(t, err) assert.True(t, extended.Base) - assert.EqualValues(t, extended.Second, "white rabbit") + assert.EqualValues(t, "white rabbit", extended.Second) assert.True(t, extended.Extend) } diff --git a/modules/setting/oauth2_test.go b/modules/setting/oauth2_test.go index 38ee4d248d6..d0e5ccf13d2 100644 --- a/modules/setting/oauth2_test.go +++ b/modules/setting/oauth2_test.go @@ -74,5 +74,5 @@ DEFAULT_APPLICATIONS = tea DEFAULT_APPLICATIONS = `) loadOAuth2From(cfg) - assert.Nil(t, nil, OAuth2.DefaultApplications) + assert.Nil(t, OAuth2.DefaultApplications) } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 14cf5805c02..c5619d0f048 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -53,6 +53,7 @@ var ( AllowDeleteOfUnadoptedRepositories bool DisableDownloadSourceArchives bool AllowForkWithoutMaximumLimit bool + AllowForkIntoSameOwner bool // Repository editor settings Editor struct { diff --git a/modules/setting/storage_test.go b/modules/setting/storage_test.go index 8ee37fd2b6d..afff85537e3 100644 --- a/modules/setting/storage_test.go +++ b/modules/setting/storage_test.go @@ -447,7 +447,7 @@ MINIO_USE_SSL = true assert.NoError(t, loadRepoArchiveFrom(cfg)) assert.EqualValues(t, "my_access_key", RepoArchive.Storage.MinioConfig.AccessKeyID) assert.EqualValues(t, "my_secret_key", RepoArchive.Storage.MinioConfig.SecretAccessKey) - assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL) + assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath) } @@ -464,7 +464,7 @@ MINIO_BASE_PATH = /prefix assert.NoError(t, loadRepoArchiveFrom(cfg)) assert.EqualValues(t, "my_access_key", RepoArchive.Storage.MinioConfig.AccessKeyID) assert.EqualValues(t, "my_secret_key", RepoArchive.Storage.MinioConfig.SecretAccessKey) - assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL) + assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath) cfg, err = NewConfigProviderFromData(` @@ -477,7 +477,7 @@ MINIO_BASE_PATH = /prefix assert.NoError(t, err) assert.NoError(t, loadRepoArchiveFrom(cfg)) assert.EqualValues(t, "127.0.0.1", RepoArchive.Storage.MinioConfig.IamEndpoint) - assert.EqualValues(t, true, RepoArchive.Storage.MinioConfig.UseSSL) + assert.True(t, RepoArchive.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "/prefix/repo-archive/", RepoArchive.Storage.MinioConfig.BasePath) cfg, err = NewConfigProviderFromData(` @@ -495,7 +495,7 @@ MINIO_BASE_PATH = /lfs assert.NoError(t, loadLFSFrom(cfg)) assert.EqualValues(t, "my_access_key", LFS.Storage.MinioConfig.AccessKeyID) assert.EqualValues(t, "my_secret_key", LFS.Storage.MinioConfig.SecretAccessKey) - assert.EqualValues(t, true, LFS.Storage.MinioConfig.UseSSL) + assert.True(t, LFS.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "/lfs", LFS.Storage.MinioConfig.BasePath) cfg, err = NewConfigProviderFromData(` @@ -513,7 +513,7 @@ MINIO_BASE_PATH = /lfs assert.NoError(t, loadLFSFrom(cfg)) assert.EqualValues(t, "my_access_key", LFS.Storage.MinioConfig.AccessKeyID) assert.EqualValues(t, "my_secret_key", LFS.Storage.MinioConfig.SecretAccessKey) - assert.EqualValues(t, true, LFS.Storage.MinioConfig.UseSSL) + assert.True(t, LFS.Storage.MinioConfig.UseSSL) assert.EqualValues(t, "/lfs", LFS.Storage.MinioConfig.BasePath) } diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go index f8e4f569b87..7479cfbd95a 100644 --- a/modules/ssh/ssh.go +++ b/modules/ssh/ssh.go @@ -17,6 +17,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "strconv" "strings" "sync" @@ -33,9 +34,26 @@ import ( gossh "golang.org/x/crypto/ssh" ) -type contextKey string +// The ssh auth overall works like this: +// NewServerConn: +// serverHandshake+serverAuthenticate: +// PublicKeyCallback: +// PublicKeyHandler (our code): +// reset(ctx.Permissions) and set ctx.Permissions.giteaKeyID = keyID +// pubKey.Verify +// return ctx.Permissions // only reaches here, the pub key is really authenticated +// set conn.Permissions from serverAuthenticate +// sessionHandler(conn) +// +// Then sessionHandler should only use the "verified keyID" from the original ssh conn, but not the ctx one. +// Otherwise, if a user provides 2 keys A (a correct one) and B (public key matches but no private key), +// then only A succeeds to authenticate, sessionHandler will see B's keyID +// +// After x/crypto >= 0.31.0 (fix CVE-2024-45337), the PublicKeyCallback will be called again for the verified key, +// it mitigates the misuse for most cases, it's still good for us to make sure we don't rely on that mitigation +// and do not misuse the PublicKeyCallback: we should only use the verified keyID from the verified ssh conn. -const giteaKeyID = contextKey("gitea-key-id") +const giteaPermissionExtensionKeyID = "gitea-perm-ext-key-id" func getExitStatusFromError(err error) int { if err == nil { @@ -61,8 +79,32 @@ func getExitStatusFromError(err error) int { return waitStatus.ExitStatus() } +// sessionPartial is the private struct from "gliderlabs/ssh/session.go" +// We need to read the original "conn" field from "ssh.Session interface" which contains the "*session pointer" +// https://github.com/gliderlabs/ssh/blob/d137aad99cd6f2d9495bfd98c755bec4e5dffb8c/session.go#L109-L113 +// If upstream fixes the problem and/or changes the struct, we need to follow. +// If the struct mismatches, the builtin ssh server will fail during integration tests. +type sessionPartial struct { + sync.Mutex + gossh.Channel + conn *gossh.ServerConn +} + +func ptr[T any](intf any) *T { + // https://pkg.go.dev/unsafe#Pointer + // (1) Conversion of a *T1 to Pointer to *T2. + // Provided that T2 is no larger than T1 and that the two share an equivalent memory layout, + // this conversion allows reinterpreting data of one type as data of another type. + v := reflect.ValueOf(intf) + p := v.UnsafePointer() + return (*T)(p) +} + func sessionHandler(session ssh.Session) { - keyID := fmt.Sprintf("%d", session.Context().Value(giteaKeyID).(int64)) + // here can't use session.Permissions() because it only uses the value from ctx, which might not be the authenticated one. + // so we must use the original ssh conn, which always contains the correct (verified) keyID. + sshSession := ptr[sessionPartial](session) + keyID := sshSession.conn.Permissions.Extensions[giteaPermissionExtensionKeyID] command := session.RawCommand() @@ -164,6 +206,20 @@ func sessionHandler(session ssh.Session) { } func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { + // The publicKeyHandler (PublicKeyCallback) only helps to provide the candidate keys to authenticate, + // It does NOT really verify here, so we could only record the related information here. + // After authentication (Verify), the "Permissions" will be assigned to the ssh conn, + // then we can use it in the "session handler" + + // first, reset the ctx permissions (just like https://github.com/gliderlabs/ssh/pull/243 does) + // it shouldn't be reused across different ssh conn (sessions), each pub key should have its own "Permissions" + ctx.Permissions().Permissions = &gossh.Permissions{} + setPermExt := func(keyID int64) { + ctx.Permissions().Permissions.Extensions = map[string]string{ + giteaPermissionExtensionKeyID: fmt.Sprint(keyID), + } + } + if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary log.Debug("Handle Public Key: Fingerprint: %s from %s", gossh.FingerprintSHA256(key), ctx.RemoteAddr()) } @@ -238,8 +294,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary log.Debug("Successfully authenticated: %s Certificate Fingerprint: %s Principal: %s", ctx.RemoteAddr(), gossh.FingerprintSHA256(key), principal) } - ctx.SetValue(giteaKeyID, pkey.ID) - + setPermExt(pkey.ID) return true } @@ -266,8 +321,7 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { if log.IsDebug() { // <- FingerprintSHA256 is kinda expensive so only calculate it if necessary log.Debug("Successfully authenticated: %s Public Key Fingerprint: %s", ctx.RemoteAddr(), gossh.FingerprintSHA256(key)) } - ctx.SetValue(giteaKeyID, pkey.ID) - + setPermExt(pkey.ID) return true } diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 832ffa8bcc9..fb784bd8b37 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -278,6 +278,16 @@ type CreateBranchRepoOption struct { OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"` } +// UpdateBranchRepoOption options when updating a branch in a repository +// swagger:model +type UpdateBranchRepoOption struct { + // New branch name + // + // required: true + // unique: true + Name string `json:"name" binding:"Required;GitRefName;MaxSize(100)"` +} + // TransferRepoOption options when transfer a repository's ownership // swagger:model type TransferRepoOption struct { diff --git a/modules/templates/helper.go b/modules/templates/helper.go index e6442fa87e9..ff9673ccef7 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -42,6 +42,7 @@ func NewFuncMap() template.FuncMap { "HTMLFormat": htmlutil.HTMLFormat, "HTMLEscape": htmlEscape, "QueryEscape": queryEscape, + "QueryBuild": QueryBuild, "JSEscape": jsEscapeSafe, "SanitizeHTML": SanitizeHTML, "URLJoin": util.URLJoin, @@ -293,6 +294,72 @@ func timeEstimateString(timeSec any) string { return util.TimeEstimateString(v) } +// QueryBuild builds a query string from a list of key-value pairs. +// It omits the nil and empty strings, but it doesn't omit other zero values, +// because the zero value of number types may have a meaning. +func QueryBuild(a ...any) template.URL { + var s string + if len(a)%2 == 1 { + if v, ok := a[0].(string); ok { + if v == "" || (v[0] != '?' && v[0] != '&') { + panic("QueryBuild: invalid argument") + } + s = v + } else if v, ok := a[0].(template.URL); ok { + s = string(v) + } else { + panic("QueryBuild: invalid argument") + } + } + for i := len(a) % 2; i < len(a); i += 2 { + k, ok := a[i].(string) + if !ok { + panic("QueryBuild: invalid argument") + } + var v string + if va, ok := a[i+1].(string); ok { + v = va + } else if a[i+1] != nil { + v = fmt.Sprint(a[i+1]) + } + // pos1 to pos2 is the "k=v&" part, "&" is optional + pos1 := strings.Index(s, "&"+k+"=") + if pos1 != -1 { + pos1++ + } else { + pos1 = strings.Index(s, "?"+k+"=") + if pos1 != -1 { + pos1++ + } else if strings.HasPrefix(s, k+"=") { + pos1 = 0 + } + } + pos2 := len(s) + if pos1 == -1 { + pos1 = len(s) + } else { + pos2 = pos1 + 1 + for pos2 < len(s) && s[pos2-1] != '&' { + pos2++ + } + } + if v != "" { + sep := "" + hasPrefixSep := pos1 == 0 || (pos1 <= len(s) && (s[pos1-1] == '?' || s[pos1-1] == '&')) + if !hasPrefixSep { + sep = "&" + } + s = s[:pos1] + sep + k + "=" + url.QueryEscape(v) + "&" + s[pos2:] + } else { + s = s[:pos1] + s[pos2:] + } + } + if s != "" && s != "&" && s[len(s)-1] == '&' { + s = s[:len(s)-1] + } + return template.URL(s) +} + func panicIfDevOrTesting() { if !setting.IsProd || setting.IsInTesting { panic("legacy template functions are for backward compatibility only, do not use them in new code") diff --git a/modules/templates/util_date.go b/modules/templates/util_date.go index 66f83d23fe5..658691ee40c 100644 --- a/modules/templates/util_date.go +++ b/modules/templates/util_date.go @@ -53,8 +53,8 @@ func parseLegacy(datetime string) time.Time { return t } -func anyToTime(any any) (t time.Time, isZero bool) { - switch v := any.(type) { +func anyToTime(value any) (t time.Time, isZero bool) { + switch v := value.(type) { case nil: // it is zero case *time.Time: @@ -72,7 +72,7 @@ func anyToTime(any any) (t time.Time, isZero bool) { case int64: t = timeutil.TimeStamp(v).AsTime() default: - panic(fmt.Sprintf("Unsupported time type %T", any)) + panic(fmt.Sprintf("Unsupported time type %T", value)) } return t, t.IsZero() || t.Unix() == 0 } diff --git a/modules/templates/util_string.go b/modules/templates/util_string.go index 479b755da1c..382e2de13f1 100644 --- a/modules/templates/util_string.go +++ b/modules/templates/util_string.go @@ -53,10 +53,14 @@ func (su *StringUtils) Cut(s, sep string) []any { return []any{before, after, found} } -func (su *StringUtils) EllipsisString(s string, max int) string { - return base.EllipsisString(s, max) +func (su *StringUtils) EllipsisString(s string, maxLength int) string { + return base.EllipsisString(s, maxLength) } func (su *StringUtils) ToUpper(s string) string { return strings.ToUpper(s) } + +func (su *StringUtils) TrimPrefix(s, prefix string) string { + return strings.TrimPrefix(s, prefix) +} diff --git a/modules/user/user_test.go b/modules/user/user_test.go index 9129ae79a13..372a675d342 100644 --- a/modules/user/user_test.go +++ b/modules/user/user_test.go @@ -4,7 +4,6 @@ package user import ( - "os" "os/exec" "runtime" "strings" @@ -36,7 +35,7 @@ func TestCurrentUsername(t *testing.T) { if user != whoami { t.Errorf("expected %s as user, got: %s", whoami, user) } - os.Setenv("USER", "spoofed") + t.Setenv("USER", "spoofed") user = CurrentUsername() if user != whoami { t.Errorf("expected %s as user, got: %s", whoami, user) diff --git a/modules/util/color_test.go b/modules/util/color_test.go index be6e6b122a5..abd55512184 100644 --- a/modules/util/color_test.go +++ b/modules/util/color_test.go @@ -27,9 +27,9 @@ func Test_HexToRBGColor(t *testing.T) { } for n, c := range cases { r, g, b := HexToRBGColor(c.colorString) - assert.Equal(t, c.expectedR, r, "case %d: error R should match: expected %f, but get %f", n, c.expectedR, r) - assert.Equal(t, c.expectedG, g, "case %d: error G should match: expected %f, but get %f", n, c.expectedG, g) - assert.Equal(t, c.expectedB, b, "case %d: error B should match: expected %f, but get %f", n, c.expectedB, b) + assert.InDelta(t, c.expectedR, r, 0, "case %d: error R should match: expected %f, but get %f", n, c.expectedR, r) + assert.InDelta(t, c.expectedG, g, 0, "case %d: error G should match: expected %f, but get %f", n, c.expectedG, g) + assert.InDelta(t, c.expectedB, b, 0, "case %d: error B should match: expected %f, but get %f", n, c.expectedB, b) } } diff --git a/modules/util/keypair_test.go b/modules/util/keypair_test.go index c6f68c845a4..2bade3bb28a 100644 --- a/modules/util/keypair_test.go +++ b/modules/util/keypair_test.go @@ -10,7 +10,6 @@ import ( "crypto/sha256" "crypto/x509" "encoding/pem" - "regexp" "testing" "github.com/stretchr/testify/assert" @@ -23,8 +22,8 @@ func TestKeygen(t *testing.T) { assert.NotEmpty(t, priv) assert.NotEmpty(t, pub) - assert.Regexp(t, regexp.MustCompile("^-----BEGIN RSA PRIVATE KEY-----.*"), priv) - assert.Regexp(t, regexp.MustCompile("^-----BEGIN PUBLIC KEY-----.*"), pub) + assert.Regexp(t, "^-----BEGIN RSA PRIVATE KEY-----.*", priv) + assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----.*", pub) } func TestSignUsingKeys(t *testing.T) { diff --git a/modules/util/time_str_test.go b/modules/util/time_str_test.go index 67b7978d0bf..8d1de51c8e6 100644 --- a/modules/util/time_str_test.go +++ b/modules/util/time_str_test.go @@ -27,9 +27,9 @@ func TestTimeStr(t *testing.T) { t.Run(test.input, func(t *testing.T) { output, err := TimeEstimateParse(test.input) if test.err { - assert.NotNil(t, err) + assert.Error(t, err) } else { - assert.Nil(t, err) + assert.NoError(t, err) } assert.Equal(t, test.output, output) }) diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 9ce72fb8669..5abce08b41f 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -122,8 +122,8 @@ func Test_NormalizeEOL(t *testing.T) { func Test_RandomInt(t *testing.T) { randInt, err := CryptoRandomInt(255) - assert.True(t, randInt >= 0) - assert.True(t, randInt <= 255) + assert.GreaterOrEqual(t, randInt, int64(0)) + assert.LessOrEqual(t, randInt, int64(255)) assert.NoError(t, err) } @@ -223,22 +223,22 @@ func BenchmarkToUpper(b *testing.B) { } func TestToTitleCase(t *testing.T) { - assert.Equal(t, ToTitleCase(`foo bar baz`), `Foo Bar Baz`) - assert.Equal(t, ToTitleCase(`FOO BAR BAZ`), `Foo Bar Baz`) + assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`foo bar baz`)) + assert.Equal(t, `Foo Bar Baz`, ToTitleCase(`FOO BAR BAZ`)) } func TestToPointer(t *testing.T) { assert.Equal(t, "abc", *ToPointer("abc")) assert.Equal(t, 123, *ToPointer(123)) abc := "abc" - assert.False(t, &abc == ToPointer(abc)) + assert.NotSame(t, &abc, ToPointer(abc)) val123 := 123 - assert.False(t, &val123 == ToPointer(val123)) + assert.NotSame(t, &val123, ToPointer(val123)) } func TestReserveLineBreakForTextarea(t *testing.T) { - assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata"), "test\ndata") - assert.Equal(t, ReserveLineBreakForTextarea("test\r\ndata\r\n"), "test\ndata\n") + assert.Equal(t, "test\ndata", ReserveLineBreakForTextarea("test\r\ndata")) + assert.Equal(t, "test\ndata\n", ReserveLineBreakForTextarea("test\r\ndata\r\n")) } func TestOptionalArg(t *testing.T) { diff --git a/options/gitignore/Alteryx b/options/gitignore/Alteryx index a8e1341ffe4..8fe3c5cd716 100644 --- a/options/gitignore/Alteryx +++ b/options/gitignore/Alteryx @@ -29,7 +29,7 @@ CASS.ini *.gzlc ## gitignore reference sites -# https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#Ignoring-Files +# https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#_ignoring # https://git-scm.com/docs/gitignore # https://help.github.com/articles/ignoring-files/ diff --git a/options/gitignore/ECU-TEST b/options/gitignore/ECU-TEST new file mode 100644 index 00000000000..d25ba945fe3 --- /dev/null +++ b/options/gitignore/ECU-TEST @@ -0,0 +1,62 @@ +# gitignore template for ECU-TEST workspaces - by TraceTronic https://tracetronic.com +# website: https://www.ecu-test.com +# * all directories are related to the default directories, please adapt the .gitignore if you use customized +# directories + +# Dynamic workspace settings +# * We don't recommend to ignore the .workspace directory, because of important project specific settings +# local user settings +.workspace/ETdrive.xml +.workspace/favorites.xml +.workspace/filters.xml +.workspace/generators.xml +.workspace/history.xml +.workspace/parallelExecution.xml +.workspace/signalviewer.xml +.workspace/signalViewerHistory.json +.workspace/signalviewer2layout.xml +.workspace/testeditor.xml +.workspace/tooladapter.xml +.workspace/view.xml +# optional, if your process depends on this file remove exclusion +.workspace/interactiveexecution.xml +.workspace/pythonlibrary.xml +# deprecated, support for older versions +.workspace/traceexplorer.xml + +# Custom file formats and test dependencies +# * you can manage your artifacts also with TEST-GUIDE (https://www.test-guide.info) and reference them via Playbooks +*.arxml +*.a2l +*.dbc +*.hex +*.s19 +[tT]estdata +[tT]estdaten + +# Test results and test execution related content +# * Git is not intended to store and provide test results for all iterations +# * We recommend to use TEST-GUIDE (https://www.test-guide.info) for the test report management +TestReports + +# Report generators and templates +# * if you want to provide (f.e.) your own report generators exclude the directory here and ignore only the +# unnecessary subdirectories +Templates + +# Exclude large binary artifacts +# * you can manage your artifacts also with TEST-GUIDE (https://www.test-guide.info) and reference them via Playbooks +Offline-FIUs +Offline-Models +Offline-SGBDs +*.exe +*.msi +*.zip +*.7z + +# Exclude default and custom temporary directories +Backup_* + +# Python bytecode and cache files +__pycache__/ +*.py[cod] diff --git a/options/gitignore/Kotlin b/options/gitignore/Kotlin index 524f0963bd1..566e06bf990 100644 --- a/options/gitignore/Kotlin +++ b/options/gitignore/Kotlin @@ -22,3 +22,6 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects +.kotlin/ \ No newline at end of file diff --git a/options/gitignore/Laravel b/options/gitignore/Laravel index 297959a19e5..d5673e321c2 100644 --- a/options/gitignore/Laravel +++ b/options/gitignore/Laravel @@ -21,3 +21,10 @@ Homestead.yaml Homestead.json /.vagrant .phpunit.result.cache + +/public/build +/storage/pail +.env.backup +.env.production +.phpactor.json +auth.json diff --git a/options/gitignore/Move b/options/gitignore/Move new file mode 100644 index 00000000000..b7d406e7bb0 --- /dev/null +++ b/options/gitignore/Move @@ -0,0 +1,6 @@ +# Generated by Move +# will have compiled files +build/ + +# Remove possibly saving credentials to the git repository +.aptos/ diff --git a/options/gitignore/OpenTofu b/options/gitignore/OpenTofu new file mode 100644 index 00000000000..0c736af6997 --- /dev/null +++ b/options/gitignore/OpenTofu @@ -0,0 +1,42 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tofu +override.tf.json +override.tofu.json +*_override.tf +*_override.tofu +*_override.tf.json +*_override.tofu.json + +# Ignore transient lock info files created by tofu apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf +# !example_override.tofu + +# Include tfplan files to ignore the plan output of command: tofu plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/options/gitignore/Python b/options/gitignore/Python index 82f927558a3..c2fb773388e 100644 --- a/options/gitignore/Python +++ b/options/gitignore/Python @@ -94,6 +94,12 @@ ipython_config.py # install all needed dependencies. #Pipfile.lock +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more diff --git a/options/gitignore/Unity b/options/gitignore/Unity index 58cbc8256e8..3496b7cda7a 100644 --- a/options/gitignore/Unity +++ b/options/gitignore/Unity @@ -59,6 +59,7 @@ sysinfo.txt *.apk *.aab *.unitypackage +*.unitypackage.meta *.app # Crashlytics generated file diff --git a/options/gitignore/VisualStudio b/options/gitignore/VisualStudio index 8a30d258ed9..a4fe18bdd10 100644 --- a/options/gitignore/VisualStudio +++ b/options/gitignore/VisualStudio @@ -82,6 +82,8 @@ StyleCopReport.xml *.pgc *.pgd *.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp *.sbr *.tlb *.tli diff --git a/options/license/CC-PDM-1.0 b/options/license/CC-PDM-1.0 new file mode 100644 index 00000000000..1dc4e63b874 --- /dev/null +++ b/options/license/CC-PDM-1.0 @@ -0,0 +1,27 @@ +No Copyright + +This work has been identified as being free of known restrictions under +copyright law, including all related and neighboring rights. + + +You can copy, modify, distribute and perform the work, even for commercial +purposes, all without asking permission. See Other Information below. + +Other Information + +The work may not be free of known copyright restrictions in all jurisdictions . + +Persons may have other rights in or related to the work, such as patent or +trademark rights, and others may have rights in how the work is used, such as +publicity or privacy rights. + +In some jurisdictions moral rights of the author may persist beyond the term of +copyright. These rights may include the right to be identified as the author +and the right to object to derogatory treatments. + +Unless expressly stated otherwise, the person who identified the work makes no +warranties about the work, and disclaims liability for all uses of the work, to +the fullest extent permitted by applicable law. + +When using or citing the work, you should not imply endorsement by the author +or the person who identified the work. diff --git a/options/license/CC-SA-1.0 b/options/license/CC-SA-1.0 new file mode 100644 index 00000000000..1a810feaec2 --- /dev/null +++ b/options/license/CC-SA-1.0 @@ -0,0 +1,198 @@ + Creative Commons Legal Code + + ShareAlike 1.0 + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL +SERVICES. DISTRIBUTION OF THIS DRAFT LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT +RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. +CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND +DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE +BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Collective Work" means a work, such as a periodical issue, anthology or + encyclopedia, in which the Work in its entirety in unmodified form, along + with a number of other contributions, constituting separate and independent + works in themselves, are assembled into a collective whole. A work that + constitutes a Collective Work will not be considered a Derivative Work (as + defined below) for the purposes of this License. + b. "Derivative Work" means a work based upon the Work or upon the Work and + other pre-existing works, such as a translation, musical arrangement, + dramatization, fictionalization, motion picture version, sound recording, + art reproduction, abridgment, condensation, or any other form in which the + Work may be recast, transformed, or adapted, except that a work that + constitutes a Collective Work will not be considered a Derivative Work for + the purpose of this License. + c. "Licensor" means the individual or entity that offers the Work under the + terms of this License. + d. "Original Author" means the individual or entity who created the Work. + e. "Work" means the copyrightable work of authorship offered under the terms + of this License. + f. "You" means an individual or entity exercising rights under this License + who has not previously violated the terms of this License with respect to + the Work, or who has received express permission from the Licensor to + exercise rights under this License despite a previous violation. + +2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or +restrict any rights arising from fair use, first sale or other limitations on +the exclusive rights of the copyright owner under copyright law or other +applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, Licensor +hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the +duration of the applicable copyright) license to exercise the rights in the +Work as stated below: + + a. to reproduce the Work, to incorporate the Work into one or more Collective + Works, and to reproduce the Work as incorporated in the Collective Works; + b. to create and reproduce Derivative Works; + c. to distribute copies or phonorecords of, display publicly, perform + publicly, and perform publicly by means of a digital audio transmission the + Work including as incorporated in Collective Works; + d. to distribute copies or phonorecords of, display publicly, perform + publicly, and perform publicly by means of a digital audio transmission + Derivative Works; + +The above rights may be exercised in all media and formats whether now known or +hereafter devised. The above rights include the right to make such +modifications as are technically necessary to exercise the rights in other +media and formats. All rights not expressly granted by Licensor are hereby +reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may distribute, publicly display, publicly perform, or publicly + digitally perform the Work only under the terms of this License, and You + must include a copy of, or the Uniform Resource Identifier for, this + License with every copy or phonorecord of the Work You distribute, publicly + display, publicly perform, or publicly digitally perform. You may not offer + or impose any terms on the Work that alter or restrict the terms of this + License or the recipients' exercise of the rights granted hereunder. You + may not sublicense the Work. You must keep intact all notices that refer to + this License and to the disclaimer of warranties. You may not distribute, + publicly display, publicly perform, or publicly digitally perform the Work + with any technological measures that control access or use of the Work in a + manner inconsistent with the terms of this License Agreement. The above + applies to the Work as incorporated in a Collective Work, but this does not + require the Collective Work apart from the Work itself to be made subject + to the terms of this License. If You create a Collective Work, upon notice + from any Licensor You must, to the extent practicable, remove from the + Collective Work any reference to such Licensor or the Original Author, as + requested. If You create a Derivative Work, upon notice from any Licensor + You must, to the extent practicable, remove from the Derivative Work any + reference to such Licensor or the Original Author, as requested. + b. You may distribute, publicly display, publicly perform, or publicly + digitally perform a Derivative Work only under the terms of this License, + and You must include a copy of, or the Uniform Resource Identifier for, + this License with every copy or phonorecord of each Derivative Work You + distribute, publicly display, publicly perform, or publicly digitally + perform. You may not offer or impose any terms on the Derivative Works that + alter or restrict the terms of this License or the recipients' exercise of + the rights granted hereunder, and You must keep intact all notices that + refer to this License and to the disclaimer of warranties. You may not + distribute, publicly display, publicly perform, or publicly digitally + perform the Derivative Work with any technological measures that control + access or use of the Work in a manner inconsistent with the terms of this + License Agreement. The above applies to the Derivative Work as incorporated + in a Collective Work, but this does not require the Collective Work apart + from the Derivative Work itself to be made subject to the terms of this + License. + +5. Representations, Warranties and Disclaimer + + a. By offering the Work for public release under this License, Licensor + represents and warrants that, to the best of Licensor's knowledge after + reasonable inquiry: + i. Licensor has secured all rights in the Work necessary to grant the + license rights hereunder and to permit the lawful exercise of the + rights granted hereunder without You having any obligation to pay any + royalties, compulsory license fees, residuals or any other payments; + ii. The Work does not infringe the copyright, trademark, publicity rights, + common law rights or any other right of any third party or constitute + defamation, invasion of privacy or other tortious injury to any third + party. + b. EXCEPT AS EXPRESSLY STATED IN THIS LICENSE OR OTHERWISE AGREED IN WRITING + OR REQUIRED BY APPLICABLE LAW, THE WORK IS LICENSED ON AN "AS IS" BASIS, + WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, + WITHOUT LIMITATION, ANY WARRANTIES REGARDING THE CONTENTS OR ACCURACY OF + THE WORK. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, +AND EXCEPT FOR DAMAGES ARISING FROM LIABILITY TO A THIRD PARTY RESULTING FROM +BREACH OF THE WARRANTIES IN SECTION 5, IN NO EVENT WILL LICENSOR BE LIABLE TO +YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR +EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF +LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate automatically + upon any breach by You of the terms of this License. Individuals or + entities who have received Derivative Works or Collective Works from You + under this License, however, will not have their licenses terminated + provided such individuals or entities remain in full compliance with those + licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of + this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the Work + under different license terms or to stop distributing the Work at any time; + provided, however that any such election will not serve to withdraw this + License (or any other license that has been, or is required to be, granted + under the terms of this License), and this License will continue in full + force and effect unless terminated as stated above. + +8. Miscellaneous + + a. Each time You distribute or publicly digitally perform the Work or a + Collective Work, the Licensor offers to the recipient a license to the Work + on the same terms and conditions as the license granted to You under this + License. + b. Each time You distribute or publicly digitally perform a Derivative Work, + Licensor offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of the + remainder of the terms of this License, and without further action by the + parties to this agreement, such provision shall be reformed to the minimum + extent necessary to make such provision valid and enforceable. + d. No term or provision of this License shall be deemed waived and no breach + consented to unless such waiver or consent shall be in writing and signed + by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, agreements + or representations with respect to the Work not specified here. Licensor + shall not be bound by any additional provisions that may appear in any + communication from You. This License may not be modified without the mutual + written agreement of the Licensor and You. + +Creative Commons is not a party to this License, and makes no warranty +whatsoever in connection with the Work. Creative Commons will not be liable to +You or any party on any legal theory for any damages whatsoever, including +without limitation any general, special, incidental or consequential damages +arising in connection to this license. Notwithstanding the foregoing two (2) +sentences, if Creative Commons has expressly identified itself as the Licensor +hereunder, it shall have all rights and obligations of Licensor. + +Except for the limited purpose of indicating to the public that the Work is +licensed under the CCPL, neither party will use the trademark "Creative +Commons" or any related trademark or logo of Creative Commons without the prior +written consent of Creative Commons. Any permitted use will be in compliance +with Creative Commons' then-current trademark usage guidelines, as may be +published on its website or otherwise made available upon request from time to +time. + +Creative Commons may be contacted at http://creativecommons.org/. diff --git a/options/license/CGAL-linking-exception b/options/license/CGAL-linking-exception new file mode 100644 index 00000000000..c6dbd55ca6a --- /dev/null +++ b/options/license/CGAL-linking-exception @@ -0,0 +1,4 @@ +As a special exception, you have permission to link this library +with the CGAL library (http://www.cgal.org) and distribute executables, +as long as you follow the requirements of the GNU GPL in regard to +all of the software in the executable aside from CGAL. diff --git a/options/license/Independent-modules-exception b/options/license/Independent-modules-exception new file mode 100644 index 00000000000..8f66dba6abd --- /dev/null +++ b/options/license/Independent-modules-exception @@ -0,0 +1,18 @@ +This is the file COPYING.FPC, it applies to the Free Pascal Run-Time Library +(RTL) and packages (packages) distributed by members of the Free Pascal +Development Team. + +The source code of the Free Pascal Runtime Libraries and packages are +distributed under the Library GNU General Public License +(see the file COPYING) with the following modification: + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent modules, +and to copy and distribute the resulting executable under terms of your choice, +provided that you also meet, for each linked independent module, the terms +and conditions of the license of that module. An independent module is a module +which is not derived from or based on this library. If you modify this +library, you may extend this exception to your version of the library, but you are +not obligated to do so. If you do not wish to do so, delete this exception +statement from your version. diff --git a/options/license/InnoSetup b/options/license/InnoSetup new file mode 100644 index 00000000000..337584e6d1c --- /dev/null +++ b/options/license/InnoSetup @@ -0,0 +1,27 @@ +Inno Setup License +================== + +Except where otherwise noted, all of the documentation and software included in the Inno Setup +package is copyrighted by Jordan Russell. + +Copyright (C) 1997-2024 Jordan Russell. All rights reserved. +Portions Copyright (C) 2000-2024 Martijn Laan. All rights reserved. + +This software is provided "as-is," without any express or implied warranty. In no event shall the +author be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter and redistribute it, provided that the following conditions are met: + +1. All redistributions of source code files must retain all copyright notices that are currently in + place, and this list of conditions without modification. + +2. All redistributions in binary form must retain all occurrences of the above copyright notice and + web site addresses that are currently in place (for example, in the About boxes). + +3. The origin of this software must not be misrepresented; you must not claim that you wrote the + original software. If you use this software to distribute a product, an acknowledgment in the + product documentation would be appreciated but is not required. + +4. Modified versions in source or binary form must be plainly marked as such, and must not be + misrepresented as being the original software. diff --git a/options/license/MIPS b/options/license/MIPS new file mode 100644 index 00000000000..cf57a056391 --- /dev/null +++ b/options/license/MIPS @@ -0,0 +1,4 @@ +Copyright (c) 1992, 1991, 1990 MIPS Computer Systems, Inc. +MIPS Computer Systems, Inc. grants reproduction and use +rights to all parties, PROVIDED that this comment is +maintained in the copy. diff --git a/options/license/Motosoto b/options/license/Motosoto index 4add8c6a395..a25cff026eb 100644 --- a/options/license/Motosoto +++ b/options/license/Motosoto @@ -1,110 +1,372 @@ MOTOSOTO OPEN SOURCE LICENSE - Version 0.9.1 -This Motosoto Open Source License (the "License") applies to "Community Portal Server" and related software products as well as any updatesor maintenance releases of that software ("Motosoto Products") that are distributed by Motosoto.Com B.V. ("Licensor"). Any Motosoto Product licensed pursuant to this License is a "Licensed Product." Licensed Product, in its entirety, is protected by Dutch copyright law. This License identifies the terms under which you may use, copy, distribute or modify Licensed Product and has been submitted to the Open Software Initiative (OSI) for approval. +This Motosoto Open Source License (the "License") applies to "Community Portal Server" and related +software products as well as any updatesor maintenance releases of that software ("Motosoto +Products") that are distributed by Motosoto.Com B.V. ("Licensor"). Any Motosoto Product licensed +pursuant to this License is a "Licensed Product." Licensed Product, in its entirety, is protected +by Dutch copyright law. This License identifies the terms under which you may use, copy, distribute +or modify Licensed Product and has been submitted to the Open Software Initiative (OSI) for +approval. Preamble -This Preamble is intended to describe, in plain English, the nature and scope of this License. However, this Preamble is not a part of this license. The legal effect of this License is dependent only upon the terms of the License and not this Preamble. This License complies with the Open Source Definition and has been approved by Open Source Initiative. Software distributed under this License may be marked as "OSI Certified Open Source Software." +This Preamble is intended to describe, in plain English, the nature and scope of this License. +However, this Preamble is not a part of this license. The legal effect of this License is dependent +only upon the terms of the License and not this Preamble. This License complies with the Open +Source Definition and has been approved by Open Source Initiative. Software distributed under this +License may be marked as "OSI Certified Open Source Software." This License provides that: -1. You may use, sell or give away the Licensed Product, alone or as a component of an aggregate software distribution containing programs from several different sources. No royalty or other fee is required. +1. You may use, sell or give away the Licensed Product, alone or as a component of an aggregate +software distribution containing programs from several different sources. No royalty or other fee +is required. -2. Both Source Code and executable versions of the Licensed Product, including Modifications made by previous Contributors, are available for your use. (The terms "Licensed Product," "Modifications," "Contributors" and "Source Code" are defined in the License.) +2. Both Source Code and executable versions of the Licensed Product, including Modifications made +by previous Contributors, are available for your use. (The terms "Licensed Product," +"Modifications," "Contributors" and "Source Code" are defined in the License.) -3. You are allowed to make Modifications to the Licensed Product, and you can create Derivative Works from it. (The term "Derivative Works" is defined in the License.) +3. You are allowed to make Modifications to the Licensed Product, and you can create Derivative +Works from it. (The term "Derivative Works" is defined in the License.) -4. By accepting the Licensed Product under the provisions of this License, you agree that any Modifications you make to the Licensed Product and then distribute are governed by the provisions of this License. In particular, you must make the Source Code of your Modifications available to others. +4. By accepting the Licensed Product under the provisions of this License, you agree that any +Modifications you make to the Licensed Product and then distribute are governed by the provisions +of this License. In particular, you must make the Source Code of your Modifications available to +others. -5. You may use the Licensed Product for any purpose, but the Licensor is not providing you any warranty whatsoever, nor is the Licensor accepting any liability in the event that the Licensed Product doesn't work properly or causes you any injury or damages. +5. You may use the Licensed Product for any purpose, but the Licensor is not providing you any +warranty whatsoever, nor is the Licensor accepting any liability in the event that the Licensed +Product doesn't work properly or causes you any injury or damages. -6. If you sublicense the Licensed Product or Derivative Works, you may charge fees for warranty or support, or for accepting indemnity or liability obligations to your customers. You cannot charge for the Source Code. +6. If you sublicense the Licensed Product or Derivative Works, you may charge fees for warranty or +support, or for accepting indemnity or liability obligations to your customers. You cannot charge +for the Source Code. -7. If you assert any patent claims against the Licensor relating to the Licensed Product, or if you breach any terms of the License, your rights to the Licensed Product under this License automatically terminate. +7. If you assert any patent claims against the Licensor relating to the Licensed Product, or if you +breach any terms of the License, your rights to the Licensed Product under this License +automatically terminate. -You may use this License to distribute your own Derivative Works, in which case the provisions of this License will apply to your Derivative Works just as they do to the original Licensed Product. +You may use this License to distribute your own Derivative Works, in which case the provisions of +this License will apply to your Derivative Works just as they do to the original Licensed Product. -Alternatively, you may distribute your Derivative Works under any other OSI-approved Open Source license, or under a proprietary license of your choice. If you use any license other than this License, however, you must continue to fulfill the requirements of this License (including the provisions relating to publishing the Source Code) for those portions of your Derivative Works that consist of the Licensed Product, including the files containing Modifications. +Alternatively, you may distribute your Derivative Works under any other OSI-approved Open Source +license, or under a proprietary license of your choice. If you use any license other than this +License, however, you must continue to fulfill the requirements of this License (including the +provisions relating to publishing the Source Code) for those portions of your Derivative Works that +consist of the Licensed Product, including the files containing Modifications. -New versions of this License may be published from time to time. You may choose to continue to use the license terms in this version of the License or those from the new version. However, only the Licensor has the right to change the License terms as they apply to the Licensed Product. This License relies on precise definitions for certain terms. Those terms are defined when they are first used, and the definitions are repeated for your convenience in a Glossary at the end of the License. +New versions of this License may be published from time to time. You may choose to continue to use +the license terms in this version of the License or those from the new version. However, only the +Licensor has the right to change the License terms as they apply to the Licensed Product. This +License relies on precise definitions for certain terms. Those terms are defined when they are +first used, and the definitions are repeated for your convenience in a Glossary at the end of the +License. License Terms 1. Grant of License From Licensor. -Licensor hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims, to do the following: +Licensor hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third +party intellectual property claims, to do the following: - a. Use, reproduce, modify, display, perform, sublicense and distribute Licensed Product or portions thereof (including Modifications as hereinafter defined), in both Source Code or as an executable program. "Source Code" means the preferred form for making modifications to the Licensed Product, including all modules contained therein, plus any associated interface definition files, scripts used to control compilation and installation of an executable program, or a list of differential comparisons against the Source Code of the Licensed Product. + a. Use, reproduce, modify, display, perform, sublicense and distribute Licensed Product or +portions thereof (including Modifications as hereinafter defined), in both Source Code or as an +executable program. "Source Code" means the preferred form for making modifications to the Licensed +Product, including all modules contained therein, plus any associated interface definition files, +scripts used to control compilation and installation of an executable program, or a list of +differential comparisons against the Source Code of the Licensed Product. - b. Create Derivative Works (as that term is defined under Dutch copyright law) of Licensed Product by adding to or deleting from the substance or structure of said Licensed Product. + b. Create Derivative Works (as that term is defined under Dutch copyright law) of Licensed +Product by adding to or deleting from the substance or structure of said Licensed Product. - c. Under claims of patents now or hereafter owned or controlled by Licensor, to make, use, sell, offer for sale, have made, and/or otherwise dispose of Licensed Product or portions thereof, but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for sale, have made, and/or otherwise dispose of Licensed Product or portions thereof or Derivative Works thereof. + c. Under claims of patents now or hereafter owned or controlled by Licensor, to make, use, +sell, offer for sale, have made, and/or otherwise dispose of Licensed Product or portions thereof, +but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer +for sale, have made, and/or otherwise dispose of Licensed Product or portions thereof or Derivative +Works thereof. 2. Grant of License to Modifications From Contributor. -"Modifications" means any additions to or deletions from the substance or structure of (i) a file containing Licensed Product, or (ii) any new file that contains any part of Licensed Product. Hereinafter in this License, the term "Licensed Product" shall include all previous Modifications that you receive from any Contributor. By application of the provisions in Section 4(a) below, each person or entity who created or contributed to the creation of, and distributed, a Modification (a "Contributor") hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims, to do the following: +"Modifications" means any additions to or deletions from the substance or structure of (i) a file +containing Licensed Product, or (ii) any new file that contains any part of Licensed Product. +Hereinafter in this License, the term "Licensed Product" shall include all previous Modifications +that you receive from any Contributor. By application of the provisions in Section 4(a) below, each +person or entity who created or contributed to the creation of, and distributed, a Modification (a +"Contributor") hereby grants you a world-wide, royalty-free, non-exclusive license, subject to +third party intellectual property claims, to do the following: - a. Use, reproduce, modify, display, perform, sublicense and distribute any Modifications created by such Contributor or portions thereof, in both Source Code or as an executable program, either on an unmodified basis or as part of Derivative Works. + a. Use, reproduce, modify, display, perform, sublicense and distribute any Modifications +created by such Contributor or portions thereof, in both Source Code or as an executable program, +either on an unmodified basis or as part of Derivative Works. - b. Under claims of patents now or hereafter owned or controlled by Contributor, to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof, but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof or Derivative Works thereof. + b. Under claims of patents now or hereafter owned or controlled by Contributor, to make, use, +sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof, but +solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for +sale, have made, and/or otherwise dispose of Modifications or portions thereof or Derivative Works +thereof. 3. Exclusions From License Grant. -Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor or any Contributor except as expressly stated herein. No patent license is granted separate from the Licensed Product, for code that you delete from the Licensed Product, or for combinations of the Licensed Product with other software or hardware. No right is granted to the trademarks of Licensor or any Contributor even if such marks are included in the Licensed Product. Nothing in this License shall be interpreted to prohibit Licensor from licensing under different terms from this License any code that Licensor otherwise would have a right to license. +Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, +trade secrets or any other intellectual property of Licensor or any Contributor except as expressly +stated herein. No patent license is granted separate from the Licensed Product, for code that you +delete from the Licensed Product, or for combinations of the Licensed Product with other software +or hardware. No right is granted to the trademarks of Licensor or any Contributor even if such +marks are included in the Licensed Product. Nothing in this License shall be interpreted to +prohibit Licensor from licensing under different terms from this License any code that Licensor +otherwise would have a right to license. 4. Your Obligations Regarding Distribution. - a. Application of This License to Your Modifications. As an express condition for your use of the Licensed Product, you hereby agree that any Modifications that you create or to which you contribute, and which you distribute, are governed by the terms of this License including, without limitation, Section 2. Any Modifications that you create or to which you contribute may be distributed only under the terms of this License or a future version of this License released under Section 7. You must include a copy of this License with every copy of the Modifications you distribute. You agree not to offer or impose any terms on any Source Code or executable version of the Licensed Product or Modifications that alter or restrict the applicable version of this License or the recipients' rights hereunder. However, you may include an additional document offering the additional rights described in Section 4(e). + a. Application of This License to Your Modifications. As an express condition for your use of +the Licensed Product, you hereby agree that any Modifications that you create or to which you +contribute, and which you distribute, are governed by the terms of this License including, without +limitation, Section 2. Any Modifications that you create or to which you contribute may be +distributed only under the terms of this License or a future version of this License released under +Section 7. You must include a copy of this License with every copy of the Modifications you +distribute. You agree not to offer or impose any terms on any Source Code or executable version of +the Licensed Product or Modifications that alter or restrict the applicable version of this License +or the recipients' rights hereunder. However, you may include an additional document offering the +additional rights described in Section 4(e). - b. Availability of Source Code. You must make available, under the terms of this License, the Source Code of the Licensed Product and any Modifications that you distribute, either on the same media as you distribute any executable or other form of the Licensed Product, or via a mechanism generally accepted in the software development community for the electronic transfer of data (an "Electronic Distribution Mechanism"). The Source Code for any version of Licensed Product or Modifications that you distribute must remain available for at least twelve (12) months after the date it initially became available, or at least six (6) months after a subsequent version of said Licensed Product or Modifications has been made available. You are responsible for ensuring that the Source Code version remains available even if the Electronic Distribution Mechanism is maintained by a third party. + b. Availability of Source Code. You must make available, under the terms of this License, the +Source Code of the Licensed Product and any Modifications that you distribute, either on the same +media as you distribute any executable or other form of the Licensed Product, or via a mechanism +generally accepted in the software development community for the electronic transfer of data (an +"Electronic Distribution Mechanism"). The Source Code for any version of Licensed Product or +Modifications that you distribute must remain available for at least twelve (12) months after the +date it initially became available, or at least six (6) months after a subsequent version of said +Licensed Product or Modifications has been made available. You are responsible for ensuring that +the Source Code version remains available even if the Electronic Distribution Mechanism is +maintained by a third party. - c. Description of Modifications. You must cause any Modifications that you create or to which you contribute, and which you distribute, to contain a file documenting the additions, changes or deletions you made to create or contribute to those Modifications, and the dates of any such additions, changes or deletions. You must include a prominent statement that the Modifications are derived, directly or indirectly, from the Licensed Product and include the names of the Licensor and any Contributor to the Licensed Product in (i) the Source Code and (ii) in any notice displayed by a version of the Licensed Product you distribute or in related documentation in which you describe the origin or ownership of the Licensed Product. You may not modify or delete any preexisting copyright notices in the Licensed Product. + c. Description of Modifications. You must cause any Modifications that you create or to which +you contribute, and which you distribute, to contain a file documenting the additions, changes or +deletions you made to create or contribute to those Modifications, and the dates of any such +additions, changes or deletions. You must include a prominent statement that the Modifications are +derived, directly or indirectly, from the Licensed Product and include the names of the Licensor +and any Contributor to the Licensed Product in (i) the Source Code and (ii) in any notice displayed +by a version of the Licensed Product you distribute or in related documentation in which you +describe the origin or ownership of the Licensed Product. You may not modify or delete any +preexisting copyright notices in the Licensed Product. d. Intellectual Property Matters. - i. Third Party Claims. If you have knowledge that a license to a third party's intellectual property right is required to exercise the rights granted by this License, you must include a text file with the Source Code distribution titled "LEGAL" that describes the claim and the party making the claim in sufficient detail that a recipient will know whom to contact. If you obtain such knowledge after you make any Modifications available as described in Section 4(b), you shall promptly modify the LEGAL file in all copies you make available thereafter and shall take other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to inform those who received the Licensed Product from you that new knowledge has been obtained. + i. Third Party Claims. If you have knowledge that a license to a third party's +intellectual property right is required to exercise the rights granted by this License, you must +include a text file with the Source Code distribution titled "LEGAL" that describes the claim and +the party making the claim in sufficient detail that a recipient will know whom to contact. If you +obtain such knowledge after you make any Modifications available as described in Section 4(b), you +shall promptly modify the LEGAL file in all copies you make available thereafter and shall take +other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to +inform those who received the Licensed Product from you that new knowledge has been obtained. - ii. Contributor APIs. If your Modifications include an application programming interface ("API") and you have knowledge of patent licenses that are reasonably necessary to implement that API, you must also include this information in the LEGAL file. + ii. Contributor APIs. If your Modifications include an application programming interface +("API") and you have knowledge of patent licenses that are reasonably necessary to implement that +API, you must also include this information in the LEGAL file. - iii. Representations. You represent that, except as disclosed pursuant to 4(d)(i) above, you believe that any Modifications you distribute are your original creations and that you have sufficient rights to grant the rights conveyed by this License. + iii. Representations. You represent that, except as disclosed pursuant to 4(d)(i) above, +you believe that any Modifications you distribute are your original creations and that you have +sufficient rights to grant the rights conveyed by this License. - e. Required Notices. You must duplicate this License in any documentation you provide along with the Source Code of any Modifications you create or to which you contribute, and which you distribute, wherever you describe recipients' rights relating to Licensed Product. You must duplicate the notice contained in Exhibit A (the "Notice") in each file of the Source Code of any copy you distribute of the Licensed Product. If you created a Modification, you may add your name as a Contributor to the Notice. If it is not possible to put the Notice in a particular Source Code file due to its structure, then you must include such Notice in a location (such as a relevant directory file) where a user would be likely to look for such a notice. You may choose to offer, and charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Licensed Product. However, you may do so only on your own behalf, and not on behalf of the Licensor or any Contributor. You must make it clear that any such warranty, support, indemnity or liability obligation is offered by you alone, and you hereby agree to indemnify the Licensor and every Contributor for any liability incurred by the Licensor or such Contributor as a result of warranty, support, indemnity or liability terms you offer. + e. Required Notices. You must duplicate this License in any documentation you provide along +with the Source Code of any Modifications you create or to which you contribute, and which you +distribute, wherever you describe recipients' rights relating to Licensed Product. You must +duplicate the notice contained in Exhibit A (the "Notice") in each file of the Source Code of any +copy you distribute of the Licensed Product. If you created a Modification, you may add your name +as a Contributor to the Notice. If it is not possible to put the Notice in a particular Source Code +file due to its structure, then you must include such Notice in a location (such as a relevant +directory file) where a user would be likely to look for such a notice. You may choose to offer, +and charge a fee for, warranty, support, indemnity or liability obligations to one or more +recipients of Licensed Product. However, you may do so only on your own behalf, and not on behalf +of the Licensor or any Contributor. You must make it clear that any such warranty, support, +indemnity or liability obligation is offered by you alone, and you hereby agree to indemnify the +Licensor and every Contributor for any liability incurred by the Licensor or such Contributor as a +result of warranty, support, indemnity or liability terms you offer. - f. Distribution of Executable Versions. You may distribute Licensed Product as an executable program under a license of your choice that may contain terms different from this License provided (i) you have satisfied the requirements of Sections 4(a) through 4(e) for that distribution, (ii) you include a conspicuous notice in the executable version, related documentation and collateral materials stating that the Source Code version of the Licensed Product is available under the terms of this License, including a description of how and where you have fulfilled the obligations of Section 4(b), (iii) you retain all existing copyright notices in the Licensed Product, and (iv) you make it clear that any terms that differ from this License are offered by you alone, not by Licensor or any Contributor. You hereby agree to indemnify the Licensor and every Contributor for any liability incurred by Licensor or such Contributor as a result of any terms you offer. + f. Distribution of Executable Versions. You may distribute Licensed Product as an executable +program under a license of your choice that may contain terms different from this License provided +(i) you have satisfied the requirements of Sections 4(a) through 4(e) for that distribution, (ii) +you include a conspicuous notice in the executable version, related documentation and collateral +materials stating that the Source Code version of the Licensed Product is available under the terms +of this License, including a description of how and where you have fulfilled the obligations of +Section 4(b), (iii) you retain all existing copyright notices in the Licensed Product, and (iv) you +make it clear that any terms that differ from this License are offered by you alone, not by +Licensor or any Contributor. You hereby agree to indemnify the Licensor and every Contributor for +any liability incurred by Licensor or such Contributor as a result of any terms you offer. - g. Distribution of Derivative Works. You may create Derivative Works (e.g., combinations of some or all of the Licensed Product with other code) and distribute the Derivative Works as products under any other license you select, with the proviso that the requirements of this License are fulfilled for those portions of the Derivative Works that consist of the Licensed Product or any Modifications thereto. + g. Distribution of Derivative Works. You may create Derivative Works (e.g., combinations of +some or all of the Licensed Product with other code) and distribute the Derivative Works as +products under any other license you select, with the proviso that the requirements of this License +are fulfilled for those portions of the Derivative Works that consist of the Licensed Product or +any Modifications thereto. 5. Inability to Comply Due to Statute or Regulation. -If it is impossible for you to comply with any of the terms of this License with respect to some or all of the Licensed Product due to statute, judicial order, or regulation, then you must (i) comply with the terms of this License to the maximum extent possible, (ii) cite the statute or regulation that prohibits you from adhering to the License, and (iii) describe the limitations and the code they affect. Such description must be included in the LEGAL file described in Section 4(d), and must be included with all distributions of the Source Code. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill at computer programming to be able to understand it. +If it is impossible for you to comply with any of the terms of this License with respect to some or +all of the Licensed Product due to statute, judicial order, or regulation, then you must (i) comply +with the terms of this License to the maximum extent possible, (ii) cite the statute or regulation +that prohibits you from adhering to the License, and (iii) describe the limitations and the code +they affect. Such description must be included in the LEGAL file described in Section 4(d), and +must be included with all distributions of the Source Code. Except to the extent prohibited by +statute or regulation, such description must be sufficiently detailed for a recipient of ordinary +skill at computer programming to be able to understand it. 6. Application of This License. -This License applies to code to which Licensor or Contributor has attached the Notice in Exhibit A, which is incorporated herein by this reference. +This License applies to code to which Licensor or Contributor has attached the Notice in Exhibit A, +which is incorporated herein by this reference. 7. Versions of This License. - a. Version. The Motosoto Open Source License is derived from the Jabber Open Source License. All changes are related to applicable law and the location of court. + a. Version. The Motosoto Open Source License is derived from the Jabber Open Source License. +All changes are related to applicable law and the location of court. - b. New Versions. Licensor may publish from time to time revised and/or new versions of the License. + b. New Versions. Licensor may publish from time to time revised and/or new versions of the +License. - c. Effect of New Versions. Once Licensed Product has been published under a particular version of the License, you may always continue to use it under the terms of that version. You may also choose to use such Licensed Product under the terms of any subsequent version of the License published by Licensor. No one other than Lic ensor has the right to modify the terms applicable to Licensed Product created under this License. + c. Effect of New Versions. Once Licensed Product has been published under a particular version +of the License, you may always continue to use it under the terms of that version. You may also +choose to use such Licensed Product under the terms of any subsequent version of the License +published by Licensor. No one other than Lic ensor has the right to modify the terms applicable to +Licensed Product created under this License. - d. Derivative Works of this License. If you create or use a modified version of this License, which you may do only in order to apply it to software that is not already a Licensed Product under this License, you must rename your license so that it is not confusingly similar to this License, and must make it clear that your license contains terms that differ from this License. In so naming your license, you may not use any trademark of Licensor or any Contributor. + d. Derivative Works of this License. If you create or use a modified version of this License, +which you may do only in order to apply it to software that is not already a Licensed Product under +this License, you must rename your license so that it is not confusingly similar to this License, +and must make it clear that your license contains terms that differ from this License. In so naming +your license, you may not use any trademark of Licensor or any Contributor. 8. Disclaimer of Warranty. -LICENSED PRODUCT IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE LICENSED PRODUCT IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LICENSED PRODUCT IS WITH YOU. SHOULD LICENSED PRODUCT PROVE DEFECTIVE IN ANY RESPECT, YOU (AND NOT THE LICENSOR OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF LICENSED PRODUCT IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. +LICENSED PRODUCT IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE LICENSED PRODUCT IS +FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE LICENSED PRODUCT IS WITH YOU. SHOULD LICENSED PRODUCT PROVE +DEFECTIVE IN ANY RESPECT, YOU (AND NOT THE LICENSOR OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF +ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL +PART OF THIS LICENSE. NO USE OF LICENSED PRODUCT IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS +DISCLAIMER. 9. Termination. - a. Automatic Termination Upon Breach. This license and the rights granted hereunder will terminate automatically if you fail to comply with the terms herein and fail to cure such breach within thirty (30) days of becoming aware of the breach. All sublicenses to the Licensed Product that are properly granted shall survive any termination of this license. Provisions that, by their nature, must remain in effect beyond the termination of this License, shall survive. + a. Automatic Termination Upon Breach. This license and the rights granted hereunder will +terminate automatically if you fail to comply with the terms herein and fail to cure such breach +within thirty (30) days of becoming aware of the breach. All sublicenses to the Licensed Product +that are properly granted shall survive any termination of this license. Provisions that, by their +nature, must remain in effect beyond the termination of this License, shall survive. - b. Termination Upon Assertion of Patent Infringement. If you initiate litigation by asserting a patent infringement claim (excluding declaratory judgment actions) against Licensor or a Contributor (Licensor or Contributor against whom you file such an action is referred to herein as "Respondent") alleging that Licensed Product directly or indirectly infringes any patent, then any and all rights granted by such Respondent to you under Sections 1 or 2 of this License shall terminate prospectively upon sixty (60) days notice from Respondent (the "Notice Period") unless within that Notice Period you either agree in writing (i) to pay Respondent a mutually agreeable reasonably royalty for your past or future use of Licensed Product made by such Respondent, or (ii) withdraw your litigation claim with respect to Licensed Product against such Respondent. If within said Notice Period a reasonable royalty and payment arrangement are not mutually agreed upon in writing by the parties or the litigation claim is not withdrawn, the rights granted by Licensor to you under Sections 1 and 2 automatically terminate at the expiration of said Notice Period. + b. Termination Upon Assertion of Patent Infringement. If you initiate litigation by asserting +a patent infringement claim (excluding declaratory judgment actions) against Licensor or a +Contributor (Licensor or Contributor against whom you file such an action is referred to herein as +"Respondent") alleging that Licensed Product directly or indirectly infringes any patent, then any +and all rights granted by such Respondent to you under Sections 1 or 2 of this License shall +terminate prospectively upon sixty (60) days notice from Respondent (the "Notice Period") unless +within that Notice Period you either agree in writing (i) to pay Respondent a mutually agreeable +reasonably royalty for your past or future use of Licensed Product made by such Respondent, or (ii) +withdraw your litigation claim with respect to Licensed Product against such Respondent. If within +said Notice Period a reasonable royalty and payment arrangement are not mutually agreed upon in +writing by the parties or the litigation claim is not withdrawn, the rights granted by Licensor to +you under Sections 1 and 2 automatically terminate at the expiration of said Notice Period. - c. Reasonable Value of This License. If you assert a patent infringement claim against Respondent alleging that Licensed Product directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by said Respondent under Sections 1 and 2 shall be taken into account in determining the amount or value of any payment or license. + c. Reasonable Value of This License. If you assert a patent infringement claim against +Respondent alleging that Licensed Product directly or indirectly infringes any patent where such +claim is resolved (such as by license or settlement) prior to the initiation of patent infringement +litigation, then the reasonable value of the licenses granted by said Respondent under Sections 1 +and 2 shall be taken into account in determining the amount or value of any payment or license. - d. No Retroactive Effect of Termination. In the event of termination under Sections 9(a) or 9(b) above, all end user license agreements (excluding licenses to distributors and reselle rs) that have been validly granted by you or any distributor hereunder prior to termination shall survive termination. + d. No Retroactive Effect of Termination. In the event of termination under Sections 9(a) or +9(b) above, all end user license agreements (excluding licenses to distributors and reselle rs) +that have been validly granted by you or any distributor hereunder prior to termination shall +survive termination. 10. Limitation of Liability. -UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL THE LICENSOR, ANY CONTRIBUTOR, OR ANY DISTRIBUTOR OF LICENSED PRODUCT, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY +UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR +OTHERWISE, SHALL THE LICENSOR, ANY CONTRIBUTOR, OR ANY DISTRIBUTOR OF LICENSED PRODUCT, OR ANY +SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR +CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, +WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, +EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF +LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY's +NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE  +EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY  +NOT APPLY TO YOU. + +11. Responsibility for Claims.  + +As between Licensor and Contributors, each party is responsible for claims and damages arising,  +directly or indirectly, out of its utilization of rights under this License. You agree to work with  +Licensor and Contributors to distribute such responsibility on an equitable basis. Nothing herein is  +intended or shall be deemed to constitute any admission of liability. + +12. U.S. Government End Users.  + +The Licensed Product is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995),  +consisting of "commercial computer software" and "commercial computer software documentation,"  +as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and  +48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire  +Licensed Product with only those rights set forth herein. + +13. Miscellaneous.  +This License represents the complete agreement concerning the subject matter hereof. If any  +provision of this License is held to be unenforceable, such provision shall be reformed only  +to the extent necessary to make it enforceable. This License shall be governed by Dutch law  +provisions. The application of the United Nations Convention on Contracts for the International  +Sale of Goods is expressly excluded. You and Licensor expressly waive any rights to a jury trial  +in any litigation concerning Licensed Product or this License. Any law or regulation that provides  +that the language of a contract shall be construed against the drafter shall not apply to this License. + +14. Definition of "You" in This License.  +"You" throughout this License, whether in upper or lower case, means an individual or a legal entity  +exercising rights under, and complying with all of the terms of, this License or a future version of  +this License issued under Section 7. For legal entities, "you" includes any entity that controls, is  +controlled by, or is under common control with you. For purposes of this definition, "control" means  +(i) the power, direct or indirect, to cause the direction or management of such entity, whether by  +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares,  +or (iii) beneficial ownership of such entity. + +15. Glossary. +All defined terms in this License that are used in more than one Section of this License are  +repeated here, in alphabetical order, for the convenience of the reader. The Section of this  +License in which each defined term is first used is shown in parentheses.  + +Contributor: Each person or entity who created or contributed to the creation of, and distributed, a Modification. (See Section 2) + +Derivative Works: That term as used in this License is defined under Dutch copyright law. (See Section 1(b)) + +License: This Motosoto Open Source License. (See first paragraph of License) + +Licensed Product: Any Motosoto Product licensed pursuant to this License. The term +"Licensed Product" includes all previous Modifications from any Contributor that you receive.  +(See first paragraph of License and Section 2) + +Licensor: Motosoto.Com B.V.. (See first paragraph of License) + +Modifications: Any additions to or deletions from the substance or structure of (i) a file  +containing Licensed Product, or (ii) any new file that contains any part of Licensed Product. (See Section 2) + +Notice: The notice contained in Exhibit A. (See Section 4(e)) + +Source Code: The preferred form for making modifications to the Licensed Product, including  +all modules contained therein, plus any associated interface definition files, scripts used  +to control compilation and installation of an executable program, or a list of differential  +comparisons against the Source Code of the Licensed Product. (See Section 1(a)) + +You: This term is defined in Section 14 of this License. +  +EXHIBIT A +The Notice below must appear in each file of the Source Code of any copy you distribute of the Licensed Product or any Modifications thereto. Contributors to any Modifications may add their own copyright notices to identify their own contributions. + +License: +The contents of this file are subject to the Motosoto Open Source License Version 0.9 (the "License"). You may not copy or use this file, in either source code or executable form, except in compliance with the License. You may obtain a copy of the License at http://www.motosoto.com/license/ or at http://www.opensource.org/. + +Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. + +Copyrights: +Portions created by or assigned to Motosoto.com B.V. are Copyright (c) 2000-2001 Motosoto.com B.V. +All Rights Reserved. Contact information for Motosoto.com B.V. is available at http://www.motosoto.com/. + +Acknowledgements +Special thanks to the Motosoto Open Source Contributors for their suggestions and support of Motosoto. + +Modifications: diff --git a/options/license/SMAIL-GPL b/options/license/SMAIL-GPL new file mode 100644 index 00000000000..be799ec39de --- /dev/null +++ b/options/license/SMAIL-GPL @@ -0,0 +1,144 @@ +SMAIL GENERAL PUBLIC LICENSE + (Clarified 11 Feb 1988) + + Copyright (C) 1988 Landon Curt Noll & Ronald S. Karr + Copyright (C) 1992 Ronald S. Karr + Copyleft (GNU) 1988 Landon Curt Noll & Ronald S. Karr + + Everyone is permitted to copy and distribute verbatim copies + of this license, but changing it is not allowed. You can also + use this wording to make the terms for other programs. + + The license agreements of most software companies keep you at the +mercy of those companies. By contrast, our general public license is +intended to give everyone the right to share SMAIL. To make sure that +you get the rights we want you to have, we need to make restrictions +that forbid anyone to deny you these rights or to ask you to surrender +the rights. Hence this license agreement. + + Specifically, we want to make sure that you have the right to give +away copies of SMAIL, that you receive source code or else can get it +if you want it, that you can change SMAIL or use pieces of it in new +free programs, and that you know you can do these things. + + To make sure that everyone has such rights, we have to forbid you to +deprive anyone else of these rights. For example, if you distribute +copies of SMAIL, you must give the recipients all the rights that you +have. You must make sure that they, too, receive or can get the +source code. And you must tell them their rights. + + Also, for our own protection, we must make certain that everyone +finds out that there is no warranty for SMAIL. If SMAIL is modified by +someone else and passed on, we want its recipients to know that what +they have is not what we distributed, so that any problems introduced +by others will not reflect on our reputation. + + Therefore we (Landon Curt Noll and Ronald S. Karr) make the following +terms which say what you must do to be allowed to distribute or change +SMAIL. + + + COPYING POLICIES + + 1. You may copy and distribute verbatim copies of SMAIL source code +as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy a valid copyright notice "Copyright +(C) 1988 Landon Curt Noll & Ronald S. Karr" (or with whatever year is +appropriate); keep intact the notices on all files that refer to this +License Agreement and to the absence of any warranty; and give any +other recipients of the SMAIL program a copy of this License +Agreement along with the program. You may charge a distribution fee +for the physical act of transferring a copy. + + 2. You may modify your copy or copies of SMAIL or any portion of it, +and copy and distribute such modifications under the terms of +Paragraph 1 above, provided that you also do the following: + + a) cause the modified files to carry prominent notices stating + that you changed the files and the date of any change; and + + b) cause the whole of any work that you distribute or publish, + that in whole or in part contains or is a derivative of SMAIL or + any part thereof, to be licensed at no charge to all third + parties on terms identical to those contained in this License + Agreement (except that you may choose to grant more extensive + warranty protection to some or all third parties, at your option). + + c) You may charge a distribution fee for the physical act of + transferring a copy, and you may at your option offer warranty + protection in exchange for a fee. + +Mere aggregation of another unrelated program with this program (or its +derivative) on a volume of a storage or distribution medium does not bring +the other program under the scope of these terms. + + 3. You may copy and distribute SMAIL (or a portion or derivative of it, +under Paragraph 2) in object code or executable form under the terms of +Paragraphs 1 and 2 above provided that you also do one of the following: + + a) accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of + Paragraphs 1 and 2 above; or, + + b) accompany it with a written offer, valid for at least three + years, to give any third party free (except for a nominal + shipping charge) a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of + Paragraphs 1 and 2 above; or, + + c) accompany it with the information you received as to where the + corresponding source code may be obtained. (This alternative is + allowed only for non-commercial distribution and only if you + received the program in object code or executable form alone.) + +For an executable file, complete source code means all the source code for +all modules it contains; but, as a special exception, it need not include +source code for modules which are standard libraries that accompany the +operating system on which the executable file runs. + + 4. You may not copy, sublicense, distribute or transfer SMAIL +except as expressly provided under this License Agreement. Any attempt +otherwise to copy, sublicense, distribute or transfer SMAIL is void and +your rights to use the program under this License agreement shall be +automatically terminated. However, parties who have received computer +software programs from you with this License Agreement will not have +their licenses terminated so long as such parties remain in full compliance. + + 5. If you wish to incorporate parts of SMAIL into other free +programs whose distribution conditions are different, write to Landon +Curt Noll & Ronald S. Karr via the Free Software Foundation at 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. We have not yet +worked out a simple rule that can be stated here, but we will often +permit this. We will be guided by the two goals of preserving the +free status of all derivatives of our free software and of promoting +the sharing and reuse of software. + +Your comments and suggestions about our licensing policies and our +software are welcome! This contract was based on the contract made by +the Free Software Foundation. Please contact the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, +USA, or call (617) 542-5942 for details on copylefted material in +general. + + NO WARRANTY + + BECAUSE SMAIL IS LICENSED FREE OF CHARGE, WE PROVIDE ABSOLUTELY NO +WARRANTY, TO THE EXTENT PERMITTED BY APPLICABLE STATE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING, LANDON CURT NOLL & RONALD S. KARR AND/OR +OTHER PARTIES PROVIDE SMAIL "AS IS" WITHOUT WARRANTY OF ANY KIND, +EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF SMAIL IS WITH +YOU. SHOULD SMAIL PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL +NECESSARY SERVICING, REPAIR OR CORRECTION. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW WILL LANDON CURT NOLL & +RONALD S. KARR AND/OR ANY OTHER PARTY WHO MAY MODIFY AND REDISTRIBUTE +SMAIL AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +LOST PROFITS, LOST MONIES, OR OTHER SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE +(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS) SMAIL, EVEN IF YOU HAVE +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, OR FOR ANY CLAIM BY +ANY OTHER PARTY. diff --git a/options/license/ThirdEye b/options/license/ThirdEye new file mode 100644 index 00000000000..ce75b566e37 --- /dev/null +++ b/options/license/ThirdEye @@ -0,0 +1,7 @@ +(C) Copyright 1984 by Third Eye Software, Inc. + +Third Eye Software, Inc. grants reproduction and use rights to +all parties, PROVIDED that this comment is maintained in the copy. + +Third Eye makes no claims about the applicability of this +symbol table to a particular use. diff --git a/options/license/any-OSI-perl-modules b/options/license/any-OSI-perl-modules new file mode 100644 index 00000000000..108db04581a --- /dev/null +++ b/options/license/any-OSI-perl-modules @@ -0,0 +1,11 @@ +This software may be redistributed under the terms of the GPL, LGPL, +modified BSD, or Artistic license, or any of the other OSI approved +licenses listed at http://www.opensource.org/licenses/alphabetical. +Distribution is allowed under all of these licenses, or any smaller +subset of multiple or just one of these licenses. + +When using a packaged version, please refer to the package metadata to see +under which license terms it was distributed. Alternatively, a distributor +may choose to replace the LICENSE section of the documentation and/or +include a LICENSE file to reflect the license(s) they chose to redistribute +under. diff --git a/options/license/generic-xts b/options/license/generic-xts new file mode 100644 index 00000000000..bf08a2b4216 --- /dev/null +++ b/options/license/generic-xts @@ -0,0 +1,17 @@ +Copyright (C) 2008, Damien Miller +Copyright (C) 2011, Alex Hornung + +Permission to use, copy, and modify this software with or without fee +is hereby granted, provided that this entire notice is included in +all copies of any software which is or includes a copy or +modification of this software. +You may use this code under the GNU public license if you so wish. Please +contribute changes back to the authors under this freer than GPL license +so that we may further the use of strong encryption without limitations to +all. + +THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR +IMPLIED WARRANTY. IN PARTICULAR, NONE OF THE AUTHORS MAKES ANY +REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE +MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR +PURPOSE. diff --git a/options/license/mxml-exception b/options/license/mxml-exception new file mode 100644 index 00000000000..32928e8dd66 --- /dev/null +++ b/options/license/mxml-exception @@ -0,0 +1,16 @@ +Mini-XML + +Copyright © 2003-2024 by Michael R Sweet + + +(Optional) Exceptions to the Apache 2.0 License: +================================================ + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 or LGPLv2 (“Combined Software”) and if +a court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2 or LGPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of the +License, but only in their entirety and only with respect to the Combined +Software. diff --git a/options/license/wwl b/options/license/wwl new file mode 100644 index 00000000000..12486ff6382 --- /dev/null +++ b/options/license/wwl @@ -0,0 +1,5 @@ +db@FreeBSD.ORG wrote this file. As long as you retain this notice you +can do whatever you want with this code, except you may not +license it under any form of the GPL. +A postcard or QSL card showing me you appreciate +this code would be nice. Diane Bruce va3db diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 2bf2a968807..2abe3672cd8 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -104,6 +104,7 @@ copy_url=Kopírovat URL copy_hash=Kopírovat hash copy_content=Kopírovat obsah copy_branch=Kopírovat jméno větve +copy_path=Zkopírovat cestu copy_success=Zkopírováno! copy_error=Kopírování se nezdařilo copy_type_unsupported=Tento typ souboru nelze zkopírovat @@ -159,6 +160,7 @@ filter.public=Veřejná filter.private=Soukromý no_results_found=Nebyly nalezeny žádné výsledky. +internal_error_skipped=Došlo k vnitřní chybě, ale je přeskočena: %s [search] search=Hledat... @@ -177,6 +179,8 @@ code_search_by_git_grep=Aktuální výsledky vyhledávání kódu jsou poskytov package_kind=Hledat balíčky... project_kind=Hledat projekty... branch_kind=Hledat větve... +tag_kind=Prohledat značky... +tag_tooltip=Hledat odpovídající značky. Použijte „%“ pro vyhledání libovolné posloupnosti číslic. commit_kind=Hledat commity... runner_kind=Hledat runnery... no_results=Nebyly nalezeny žádné odpovídající výsledky. @@ -206,6 +210,10 @@ buttons.link.tooltip=Přidat odkaz buttons.list.unordered.tooltip=Přidat seznam odrážek buttons.list.ordered.tooltip=Přidat číslovaný seznam buttons.list.task.tooltip=Přidat seznam úloh +buttons.table.add.tooltip=Přidat tabulku +buttons.table.add.insert=Přidat +buttons.table.rows=Řádky +buttons.table.cols=Sloupce buttons.mention.tooltip=Uveďte uživatele nebo tým buttons.ref.tooltip=Odkaz na issue nebo pull request buttons.switch_to_legacy.tooltip=Místo toho použít starší editor @@ -218,16 +226,20 @@ string.desc=Z – A [error] occurred=Došlo k chybě +report_message=Pokud jste si jisti, že se jedná o chybu Gitea, prosím vyhledejte problémy na GitHub a v případě potřeby založte nový problém. not_found=Cíl nebyl nalezen. network_error=Chyba sítě [startpage] app_desc=Snadno přístupný vlastní Git install=Jednoduchá na instalaci +install_desc=Jednoduše spusťte jako binární program pro vaši platformu, nasaďte jej pomocí Docker, nebo jej stáhněte jako balíček. platform=Multiplatformní +platform_desc=Gitea běží všude, kde Go může kompilovat: Windows, macOS, Linux, ARM, atd. Vyberte si ten, který milujete! lightweight=Lehká lightweight_desc=Gitea má minimální požadavky a může běžet na Raspberry Pi. Šetřete energii vašeho stroje! license=Open Source +license_desc=Vše je na %[2]s! Připojte se tím, že přispějete a uděláte tento projekt ještě lepší. Nestyďte se být přispěvatel! [install] install=Instalace @@ -341,6 +353,7 @@ enable_update_checker=Povolit kontrolu aktualizací enable_update_checker_helper=Kontroluje vydání nových verzí pravidelně připojením ke gitea.io. env_config_keys=Konfigurace prostředí env_config_keys_prompt=Následující proměnné prostředí budou také použity pro váš konfigurační soubor: +config_write_file_prompt=Tyto možnosti konfigurace budou zapsány do: %s [home] nav_menu=Navigační menu @@ -381,6 +394,8 @@ relevant_repositories=Zobrazují se pouze relevantní repositáře, %s. Zkontrolujte prosím svou doručenou poštu během následujících %s a dokončete proces registrace. Pokud je Vaše registrační e-mailová adresa nesprávná, můžete se znovu přihlásit a změnit ji. must_change_password=Aktualizujte své heslo @@ -446,8 +463,11 @@ authorize_title=Autorizovat „%s“ pro přístup k vašemu účtu? authorization_failed=Autorizace selhala authorization_failed_desc=Autorizace selhala, protože jsme detekovali neplatný požadavek. Kontaktujte prosím správce aplikace, kterou jste se pokoušeli autorizovat. sspi_auth_failed=SSPI autentizace selhala +password_pwned=Heslo, které jste zvolili, je na seznamu odcizených hesel dříve odhalených při narušení veřejných dat. Zkuste to prosím znovu s jiným heslem a zvažte změnu tohoto hesla i jinde. password_pwned_err=Nelze dokončit požadavek na HaveIBeenPwned last_admin=Nelze odstranit posledního správce. Musí existovat alespoň jeden správce. +signin_passkey=Přihlásit se pomocí přístupového klíče +back_to_sign_in=Zpět na přihlášení [mail] view_it_on=Zobrazit na %s @@ -464,6 +484,7 @@ activate_email=Ověřte vaši e-mailovou adresu activate_email.title=%s, prosím ověřte vaši e-mailovou adresu activate_email.text=Pro aktivaci vašeho účtu do %s klikněte na následující odkaz: +register_notify=Vítejte v %s register_notify.title=%[1]s vítejte v %[2]s register_notify.text_1=toto je váš potvrzovací e-mail pro %s! register_notify.text_2=Nyní se můžete přihlásit přes uživatelské jméno: %s. @@ -565,6 +586,8 @@ lang_select_error=Vyberte jazyk ze seznamu. username_been_taken=Uživatelské jméno je již obsazeno. username_change_not_local_user=Uživatelé, kteří jsou ověřováni jinak než lokálně, si nemohou změnit uživatelské jméno. +change_username_disabled=Změna uživatelského jména je zakázána. +change_full_name_disabled=Změna celého jména je zakázána. username_has_not_been_changed=Uživatelské jméno nebylo změněno repo_name_been_taken=Název repozitáře je již použit. repository_force_private=Vynucené soukromí je povoleno: soukromé repozitáře nelze zveřejnit. @@ -614,6 +637,7 @@ org_still_own_repo=Organizace stále vlastní jeden nebo více repozitářů. Ne org_still_own_packages=Organizace stále vlastní jeden nebo více balíčků. Nejdříve je smažte. target_branch_not_exist=Cílová větev neexistuje. +target_ref_not_exist=Cílové reference neexistuje %s admin_cannot_delete_self=Nemůžete se smazat, dokud jste správce. Nejdříve prosím odeberte svá administrátorská oprávnění. @@ -689,6 +713,8 @@ public_profile=Veřejný profil biography_placeholder=Řekněte nám něco o sobě! (Můžete použít Markdown) location_placeholder=Sdílejte svou přibližnou polohu s ostatními profile_desc=Nastavte, jak bude váš profil zobrazen ostatním uživatelům. Vaše hlavní e-mailová adresa bude použita pro oznámení, obnovení hesla a operace Git. +password_username_disabled=Nemáte oprávnění měnit jejich uživatelské jméno. Pro více informací kontaktujte svého administrátora. +password_full_name_disabled=Nemáte oprávnění měnit jejich celé jméno. Pro více informací kontaktujte správce webu. full_name=Celé jméno website=Web location=Místo @@ -899,6 +925,7 @@ create_oauth2_application_success=Úspěšně jste vytvořili novou OAuth2 aplik update_oauth2_application_success=Úspěšně jste aktualizovali OAuth2 aplikaci. oauth2_application_name=Název aplikace oauth2_confidential_client=Důvěrný klient. Vyberte aplikace, které zachovávají důvěrnosti v utajení, jako jsou webové aplikace. Nevybírejte pro nativní aplikace včetně stolních a mobilních aplikací. +oauth2_skip_secondary_authorization=Přeskočit autorizaci pro veřejné klienty po udělení přístupu. Může představovat bezpečnostní riziko. oauth2_redirect_uris=Přesměrování URI. Použijte nový řádek pro každou URI. save_application=Uložit oauth2_client_id=ID klienta @@ -918,21 +945,26 @@ revoke_oauth2_grant=Zrušit přístup revoke_oauth2_grant_description=Zrušením přístupu této aplikaci třetí strany ji zabráníte v přístupu k vašim datům. Jste si jisti? revoke_oauth2_grant_success=Přístup byl úspěšně zrušen. +twofa_desc=Chcete-li svůj účet ochránit před krádeží hesla, můžete použít chytrý telefon nebo jiné zařízení pro příjem jednorázových časových hesel („TOTP“). twofa_recovery_tip=Pokud ztratíte své zařízení, budete moci použít jednorázový obnovovací klíč k získání přístupu k vašemu účtu. twofa_is_enrolled=Váš účet aktuálně používá dvoufaktorové ověřování. twofa_not_enrolled=Váš účet aktuálně nepoužívá dvoufaktorové ověřování. twofa_disable=Zakázat dvoufaktorové ověřování +twofa_scratch_token_regenerate=Obnovit jednorázový obnovovací klíč twofa_scratch_token_regenerated=Váš jednorázový obnovovací klíč je nyní %s. Uložte jej na bezpečném místě, protože se znovu nezobrazí. twofa_enroll=Povolit dvoufaktorové ověřování twofa_disable_note=Dvoufaktorové ověřování můžete zakázat, když bude potřeba. twofa_disable_desc=Zakážete-li dvoufaktorové ověřování, bude váš účet méně zabezpečený. Pokračovat? +regenerate_scratch_token_desc=Jestli jste někam založili váš záložní klíč nebo jste jej již použili k přihlášení, můžete jej resetovat zde. twofa_disabled=Dvoufaktorové ověřování bylo zakázáno. scan_this_image=Naskenujte tento obrázek s vaší ověřovací aplikací: or_enter_secret=Nebo zadejte tajný kód: %s then_enter_passcode=A zadejte přístupový kód zobrazený ve vaší aplikaci: passcode_invalid=Přístupový kód není platný. Zkuste to znovu. +twofa_enrolled=Váš účet byl úspěšně zaregistrován. Uložte si jednorázový obnovovací klíč (%s) na bezpečném místě, protože se již nebude zobrazovat. twofa_failed_get_secret=Nepodařilo se získat tajemství. +webauthn_desc=Bezpečnostní klíče jsou hardwarová zařízení obsahující kryptografické klíče. Mohou být použity pro dvoufaktorové ověřování. Bezpečnostní klíče musí podporovat WebAuthn Authenticator standard. webauthn_register_key=Přidat bezpečnostní klíč webauthn_nickname=Přezdívka webauthn_delete_key=Odstranit bezpečnostní klíč @@ -1009,7 +1041,6 @@ generate_repo=Generovat repozitář generate_from=Generovat z repo_desc=Popis repo_desc_helper=Zadejte krátký popis (volitelné) -repo_lang=Jazyk repo_gitignore_helper=Vyberte šablony .gitignore. repo_gitignore_helper_desc=Vyberte soubory, které nechcete sledovat ze seznamu šablon pro běžné jazyky. Typické artefakty generované nástroji pro sestavení každého jazyka jsou ve výchozím stavu součástí .gitignore. issue_labels=Štítky úkolů @@ -1017,6 +1048,7 @@ issue_labels_helper=Vyberte sadu štítků úkolů. license=Licence license_helper=Vyberte licenční soubor. license_helper_desc=Licence řídí, co ostatní mohou a nemohou dělat s vaším kódem. Nejste si jisti, která je pro váš projekt správná? Podívejte se na Zvolte licenci +multiple_licenses=Více licencí object_format=Formát objektu object_format_helper=Objektový formát repozitáře. Nelze později změnit. SHA1 je nejvíce kompatibilní. readme=README @@ -1070,14 +1102,15 @@ delete_preexisting_success=Smazány nepřijaté soubory v %s blame_prior=Zobrazit blame před touto změnou blame.ignore_revs=Ignorování revizí v .git-blame-ignorerevs. Klikněte zde pro obejití a zobrazení normálního pohledu blame. blame.ignore_revs.failed=Nepodařilo se ignorovat revize v .git-blame-ignore-revs. -author_search_tooltip=Zobrazí maximálně 30 uživatelů tree_path_not_found_commit=Cesta %[1]s v commitu %[2]s neexistuje tree_path_not_found_branch=Cesta %[1]s ve větvi %[2]s neexistuje tree_path_not_found_tag=Cesta %[1]s ve značce %[2]s neexistuje transfer.accept=Přijmout převod +transfer.accept_desc=Převést do „%s“ transfer.reject=Odmítnout převod +transfer.reject_desc=Zrušit převod do „%s“ transfer.no_permission_to_accept=Nemáte oprávnění k přijetí tohoto převodu. transfer.no_permission_to_reject=Nemáte oprávnění k odmítnutí tohoto převodu. @@ -1152,6 +1185,11 @@ migrate.gogs.description=Migrovat data z notabug.com nebo jiných Gogs instancí migrate.onedev.description=Migrovat data z code.onedev.io nebo jiných OneDev instancí. migrate.codebase.description=Migrovat data z codebasehq.com. migrate.gitbucket.description=Migrovat data z GitBucket instancí. +migrate.codecommit.description=Přenést data z AWS CodeCommit. +migrate.codecommit.aws_access_key_id=AWS Access Key ID +migrate.codecommit.aws_secret_access_key=AWS Secret Access Key +migrate.codecommit.https_git_credentials_username=HTTPS Git uživatelské jméno +migrate.codecommit.https_git_credentials_password=HTTPS Git heslo migrate.migrating_git=Migrování data gitu migrate.migrating_topics=Migrování témat migrate.migrating_milestones=Migrování milnků @@ -1212,6 +1250,7 @@ releases=Vydání tag=Značka released_this=vydal/a toto tagged_this=označil/a +file.title=%s v %s file_raw=Surový file_history=Historie file_view_source=Zobrazit zdroj @@ -1228,6 +1267,7 @@ ambiguous_runes_header=`Tento soubor obsahuje nejednoznačné znaky Unicode` ambiguous_runes_description=`Tento soubor obsahuje znaky Unicode, které mohou být zaměněny s jinými znaky. Pokud si myslíte, že je to záměrné, můžete toto varování bezpečně ignorovat. Použijte tlačítko Escape sekvence k jejich zobrazení.` invisible_runes_line=`Tento řádek má neviditelné znaky Unicode` ambiguous_runes_line=`Tento řádek má nejednoznačné znaky Unicode` +ambiguous_character=`%[1]c [U+%04[1]X] je zaměnitelný s %[2]c [U+%04[2]X]` escape_control_characters=Escape sekvence unescape_control_characters=Bez escape sekvencí @@ -1416,8 +1456,6 @@ issues.new.no_items=Žádné položky issues.new.milestone=Milník issues.new.no_milestone=Bez milníku issues.new.clear_milestone=Smazat milník -issues.new.open_milestone=Otevřít milník -issues.new.closed_milestone=Zavřené milníky issues.new.assignees=Zpracovatelé issues.new.clear_assignees=Smazat zpracovatele issues.new.no_assignees=Bez zpracovatelů @@ -1450,6 +1488,7 @@ issues.remove_labels=odstranil/a %s štítky %s issues.add_remove_labels=přidáno %s a odebráno %s štítků %s issues.add_milestone_at=`přidal/a toto do milníku %s %s` issues.add_project_at=`přidal/a toto do projektu %s %s` +issues.move_to_column_of_project=`přesunul/a toto do %s v %s na %s` issues.change_milestone_at=`upravil/a milník z %s na %s %s` issues.change_project_at=`upravil/a projekt z %s na %s %s` issues.remove_milestone_at=`odstranil/a toto z milníku %s %s` @@ -1481,7 +1520,6 @@ issues.filter_assignee=Zpracovatel issues.filter_assginee_no_select=Všichni zpracovatelé issues.filter_assginee_no_assignee=Bez zpracovatele issues.filter_poster=Autor -issues.filter_poster_no_select=Všichni autoři issues.filter_type=Typ issues.filter_type.all_issues=Všechny úkoly issues.filter_type.assigned_to_you=Přiřazené vám @@ -1624,27 +1662,20 @@ issues.comment_on_locked=Nemůžete komentovat uzamčený úkol. issues.delete=Smazat issues.delete.title=Smazat tento úkol? issues.delete.text=Opravdu chcete tento úkol smazat? (Tím se trvale odstraní veškerý obsah. Pokud jej hodláte archivovat, zvažte raději jeho uzavření.) + issues.tracker=Sledování času -issues.start_tracking_short=Spustit časovač -issues.start_tracking=Spustit sledování času -issues.start_tracking_history=`započal/a práci %s` + issues.tracker_auto_close=Časovač se automaticky zastaví po zavření tohoto úkolu issues.tracking_already_started=`Již jste spustili sledování času na jiném úkolu!` -issues.stop_tracking=Zastavit časovač -issues.stop_tracking_history=`ukončil/a práci %s` -issues.cancel_tracking=Zahodit issues.cancel_tracking_history=`zrušil/a sledování času %s` -issues.add_time=Přidat čas ručně issues.del_time=Odstranit tento časový záznam -issues.add_time_short=Přidat čas -issues.add_time_cancel=Zrušit -issues.add_time_history=`přidal/a strávený čas %s` issues.del_time_history=`odstranil/a strávený čas %s` issues.add_time_hours=Hodiny issues.add_time_minutes=Minuty issues.add_time_sum_to_small=Čas nebyl zadán. issues.time_spent_total=Celkový strávený čas issues.time_spent_from_all_authors=`Celkový strávený čas: %s` + issues.due_date=Termín dokončení issues.invalid_due_date_format=Termín dokončení musí být ve formátu 'rrrr-mm-dd'. issues.error_modifying_due_date=Změna termínu dokončení selhala. @@ -1698,6 +1729,7 @@ issues.dependency.add_error_dep_not_same_repo=Oba úkoly musí být ve stejném issues.review.self.approval=Nemůžete schválit svůj pull request. issues.review.self.rejection=Nemůžete požadovat změny ve svém vlastním pull requestu. issues.review.approve=schválil tyto změny %s +issues.review.comment=posoudil/a %s issues.review.dismissed=zamítl/a posouzení od %s %s issues.review.dismissed_label=Zamítnuto issues.review.left_comment=zanechal komentář @@ -1705,7 +1737,7 @@ issues.review.content.empty=Je potřeba zanechat poznámku s uvedením požadova issues.review.reject=požadované změny %s issues.review.wait=byl požádán o posouzení %s issues.review.add_review_request=vyžádal posouzení od %s %s -issues.review.remove_review_request=odstranil žádost o posouzení na %s %s +issues.review.remove_review_request=odstranil/a žádost o posouzení na %s %s issues.review.remove_review_request_self=odmítl posoudit %s issues.review.pending=Čekající issues.review.pending.tooltip=Tento komentář není momentálně viditelný pro ostatní uživatele. Chcete-li odeslat Vaše čekající komentáře, vyberte „%s“ → „%s/%s/%s“ v horní části stránky. @@ -1723,6 +1755,11 @@ issues.review.resolve_conversation=Vyřešit konverzaci issues.review.un_resolve_conversation=Nevyřešit konverzaci issues.review.resolved_by=označil tuto konverzaci jako vyřešenou issues.review.commented=Okomentovat +issues.review.official=Schváleno +issues.review.requested=Čeká na posouzení +issues.review.rejected=Požadovány změny +issues.review.stale=Aktualizováno od schválení +issues.review.unofficial=Nezapočtené schválení issues.assignee.error=Ne všichni zpracovatelé byli přidáni z důvodu neočekávané chyby. issues.reference_issue.body=Tělo zprávy issues.content_history.deleted=vymazáno @@ -1739,6 +1776,7 @@ compare.compare_head=porovnat pulls.desc=Povolit pull requesty a posuzování kódu. pulls.new=Nový pull request pulls.new.blocked_user=Nemůžete vytvořit pull request, protože jste zablokování vlastníkem repozitáře. +pulls.new.must_collaborator=Musíte být spolupracovníkem pro vytvoření pull requestu. pulls.edit.already_changed=Nelze uložit změny v pull requestu. Zdá se, že obsah byl již změněn jiným uživatelem. Aktualizujte stránku a zkuste znovu komentář upravit, abyste se vyhnuli přepsání jejich změn pulls.view=Zobrazit pull request pulls.compare_changes=Nový pull request @@ -1795,6 +1833,8 @@ pulls.is_empty=Změny na této větvi jsou již na cílové větvi. Toto bude pr pulls.required_status_check_failed=Některé požadované kontroly nebyly úspěšné. pulls.required_status_check_missing=Některé požadované kontroly chybí. pulls.required_status_check_administrator=Jako administrátor stále můžete sloučit tento pull request. +pulls.blocked_by_approvals=Tento pull request ještě nemá dostatek schválení. Uděleno %d z %d udělených oficiálních schválení. +pulls.blocked_by_approvals_whitelisted=Tento pull request nemá ještě nemá dostatek požadovaných schválení. %d z %d schválení udělených uživateli nebo týmy na seznamu povolených. pulls.blocked_by_rejection=Tento pull request obsahuje změny požadované oficiálním posuzovatelem. pulls.blocked_by_official_review_requests=Tento pull request obsahuje oficiální žádosti o posouzení. pulls.blocked_by_outdated_branch=Tento pull request je zablokován, protože je zastaralý. @@ -1836,7 +1876,9 @@ pulls.unrelated_histories=Sloučení selhalo: Hlavní a základní revize nesdí pulls.merge_out_of_date=Sloučení selhalo: Základ byl aktualizován při generování sloučení. Tip: Zkuste to znovu. pulls.head_out_of_date=Sloučení selhalo: Hlavní revize byla aktualizován při generování sloučení. Tip: Zkuste to znovu. pulls.has_merged=Chyba: Pull request byl sloučen, nelze znovu sloučit nebo změnit cílovou větev. +pulls.push_rejected=Nahrání se nezdařilo: Nahrání bylo zamítnuto. Zkontrolujte háčky Gitu pro tento repozitář. pulls.push_rejected_summary=Úplná zpráva o odmítnutí +pulls.push_rejected_no_message=Nahrání se nezdařilo: Nahrání bylo odmítnuto, ale nebyla nalezena žádná vzdálená zpráva. Zkontrolujte háčky gitu pro tento repozitář pulls.open_unmerged_pull_exists=`Nemůžete provést operaci znovuotevření protože je tu čekající pull request (#%d) s identickými vlastnostmi.` pulls.status_checking=Některé kontroly jsou nedořešeny pulls.status_checks_success=Všechny kontroly byly úspěšné @@ -1860,6 +1902,7 @@ pulls.cmd_instruction_checkout_title=Checkout pulls.cmd_instruction_checkout_desc=Z vašeho repositáře projektu se podívejte na novou větev a vyzkoušejte změny. pulls.cmd_instruction_merge_title=Sloučit pulls.cmd_instruction_merge_desc=Slučte změny a aktualizujte je na Gitea. +pulls.cmd_instruction_merge_warning=Varování: Tato operace nemůže sloučit požadavek na natažení, protože „autodetekce manuálních sloučení“ nebyla povolena pulls.clear_merge_message=Vymazat zprávu o sloučení pulls.clear_merge_message_hint=Vymazání zprávy o sloučení odstraní pouze obsah zprávy a ponechá generované přídavky gitu jako "Co-AuthoreBy …". @@ -1881,6 +1924,7 @@ pulls.delete.text=Opravdu chcete tento pull request smazat? (Tím se trvale odst pulls.recently_pushed_new_branches=Nahráli jste větev %[1]s %[2]s pull.deleted_branch=(odstraněno):%s +pull.agit_documentation=Prohlédněte si dokumentaci o AGit comments.edit.already_changed=Nelze uložit změny v komentáři. Zdá se, že obsah byl již změněn jiným uživatelem. Aktualizujte stránku a zkuste znovu komentář upravit, abyste se vyhnuli přepsání jejich změn @@ -1891,6 +1935,7 @@ milestones.no_due_date=Bez lhůty dokončení milestones.open=Otevřít milestones.close=Zavřít milestones.new_subheader=Milníky vám pomohou organizovat úkoly a sledovat jejich pokrok. +milestones.completeness=%d%% Dokončeno milestones.create=Vytvořit milník milestones.title=Název milestones.desc=Popis @@ -2113,6 +2158,7 @@ settings.pulls.default_delete_branch_after_merge=Ve výchozím nastavení mazat settings.pulls.default_allow_edits_from_maintainers=Ve výchozím nastavení povolit úpravy od správců settings.releases_desc=Povolit vydání v repozitáři settings.packages_desc=Povolit registr balíčků repozitáře +settings.projects_desc=Povolit projekty settings.projects_mode_desc=Režim projektů (druhy projektů k zobrazení) settings.projects_mode_repo=Pouze projekty repozitáře settings.projects_mode_owner=Pouze projekty uživatele nebo organizace @@ -2152,6 +2198,7 @@ settings.transfer_in_progress=V současné době probíhá převod. Zrušte jej, settings.transfer_notices_1=- Ztratíte přístup k repozitáři, pokud jej převedete na uživatele. settings.transfer_notices_2=- Zůstane vám přístup k repozitáři, pokud jej převedete na organizaci kterou (spolu)vlastníte. settings.transfer_notices_3=- Pokud je repozitář soukromý a je předán jednotlivému uživateli, tato akce se ujistí, že uživatel má alespoň oprávnění ke čtení (a v případě potřeby změní oprávnění). +settings.transfer_notices_4=- Pokud repozitář patří organizaci a převádíte ho na jinou organizaci nebo jednotlivce, ztratíte odkazy mezi problémy repositáře a projektovou tabulí organizace. settings.transfer_owner=Nový vlastník settings.transfer_perform=Provést převod settings.transfer_started=Tento repozitář byl označen pro převod a čeká na potvrzení od „%s“ @@ -2251,6 +2298,7 @@ settings.event_wiki_desc=Wiki stránka vytvořena, přejmenována nebo smazána. settings.event_release=Vydání settings.event_release_desc=Vydání v tomto repozitáři bylo publikováno, aktualizováno nebo smazáno. settings.event_push=Nahrát +settings.event_force_push=Vynucené nahrání settings.event_push_desc=Nahrání pomocí Gitu do repozitáře. settings.event_repository=Repozitář settings.event_repository_desc=Repozitář vytvořen nebo smazán. @@ -2287,6 +2335,7 @@ settings.event_pull_request_merge=Sloučení pull requestu settings.event_package=Balíček settings.event_package_desc=Balíček vytvořen nebo odstraněn v repozitáři. settings.branch_filter=Filtr větví +settings.branch_filter_desc=Povolené větve pro události nahrání, vytvoření větve a smazání větve jsou určeny pomocí zástupného vzoru. Pokud je prázdný nebo *, všechny události jsou ohlášeny. Podívejte se na dokumentaci syntaxe na github.com/gobwas/glob. Příklady: master, {master,release*}. settings.authorization_header=Autorizační hlavička settings.authorization_header_desc=Pokud vyplněno, bude připojeno k požadavkům jako autorizační hlavička. Příklady: %s. settings.active=Aktivní @@ -2337,22 +2386,48 @@ settings.protected_branch.save_rule=Uložit pravidlo settings.protected_branch.delete_rule=Odstranit pravidlo settings.protected_branch_can_push=Povolit nahrání? settings.protected_branch_can_push_yes=Můžete nahrávat +settings.protected_branch_can_push_no=Nemůžete nahrávat +settings.branch_protection=Pravidla ochrany větve pro větev „%s“ settings.protect_this_branch=Povolit ochranu větví settings.protect_this_branch_desc=Zabraňuje smazání a omezuje gitu nahrávání a slučování do větve. settings.protect_disable_push=Zakázat nahrávání settings.protect_disable_push_desc=Žádné nahrávání do této větve nebude povoleno. +settings.protect_disable_force_push=Zakázat vynucené nahrání +settings.protect_disable_force_push_desc=Do této větve nebude povoleno žádné vynucené nahrání. settings.protect_enable_push=Povolit nahrávání settings.protect_enable_push_desc=Každý, kdo má přístup k zápisu, bude moci nahrávat do této větve (ale ne vynucená nahrávání). +settings.protect_enable_force_push_all=Povolit vynucené nahrání +settings.protect_enable_force_push_all_desc=Každý, kdo má přístup k nahrávání, bude moci vynutit nahrání do této větve. +settings.protect_enable_force_push_allowlist=Povolit vynucené nahrání jen vyjmenovaným +settings.protect_enable_force_push_allowlist_desc=Pouze uživatelé nebo týmy s přístupem k nahrávání budou moci vynutit nahrání do této větve. settings.protect_enable_merge=Povolit sloučení settings.protect_enable_merge_desc=Každému, kdo má přístup k zápisu, bude povoleno sloučit pull requesty do této větve. +settings.protect_whitelist_committers=Povolit nahrání jen vyjmenovaným +settings.protect_whitelist_committers_desc=Pouze povolení uživatelé budou moci nahrávat do této větve (ale ne vynucení nahrávání). +settings.protect_whitelist_deploy_keys=Povolit nahrání klíčům pro nasazení s přístupem pro zápis. +settings.protect_whitelist_users=Povolení uživatelé pro nahrávání: +settings.protect_whitelist_teams=Povolené týmy pro nahrávání: +settings.protect_force_push_allowlist_users=Povolení uživatelé pro vynucené nahrávání: +settings.protect_force_push_allowlist_teams=Povolené týmy pro vynucené nahrávání: +settings.protect_force_push_allowlist_deploy_keys=Povolte klíče pro nasazení s přístupem pro nahrávání, aby mohly provádět vynucené nahrávání. +settings.protect_merge_whitelist_committers=Povolit vyjmenovaným slučování +settings.protect_merge_whitelist_committers_desc=Povolit pouze vyjmenovaným uživatelům nebo týmům slučovat pull requesty do této větve. +settings.protect_merge_whitelist_users=Povolení uživatelé pro slučování: +settings.protect_merge_whitelist_teams=Povolené týmy pro slučování: settings.protect_check_status_contexts=Povolit kontrolu stavu settings.protect_status_check_patterns=Vzorce kontroly stavu: settings.protect_status_check_patterns_desc=Zadejte vzory pro určení, které kontroly stavu musí projít před sloučením větví do větve, která odpovídá tomuto pravidlu. Každý řádek určuje vzor. Vzory nemohou být prázdné. +settings.protect_check_status_contexts_desc=Požadovat kontrolu stavu před sloučením. Pokud je povoleno, revize musí být nejprve nahrány do jiné větve, projít kontrolou stavu, a následné sloučeny nebo přímo nahrány do větve, která vyhovuje tomuto pravidlu. Pokud nejsou vybrány žádné kontexty, musí být poslední potvrzení úspěšné bez ohledu na kontext. settings.protect_check_status_contexts_list=Kontroly stavu pro tento repozitář zjištěné během posledního týdne settings.protect_status_check_matched=Odpovídá settings.protect_invalid_status_check_pattern=Neplatný vzor kontroly stavu: „%s“. settings.protect_no_valid_status_check_patterns=Žádné platné vzory kontroly stavu. settings.protect_required_approvals=Požadovaná schválení: +settings.protect_required_approvals_desc=Umožňuje sloučit pouze pull requestů s dostatečným počtem požadovaných schválení. Požadovaná schválení jsou buď od uživatelů nebo týmů, které jsou na seznamu povolených, nebo od kohokoli s právem zápisu. +settings.protect_approvals_whitelist_enabled=Omezit schválení na povolené uživatele nebo týmy +settings.protect_approvals_whitelist_enabled_desc=Do požadovaných schválení se započítají pouze posouzení od povolených uživatelů nebo týmů. Bez seznamu povolených se započítává schválení od kohokoli s právem zápisu. +settings.protect_approvals_whitelist_users=Povolení posuzovatelé: +settings.protect_approvals_whitelist_teams=Povolené týmy pro posuzování: settings.dismiss_stale_approvals=Odmítnout nekvalitní schválení settings.dismiss_stale_approvals_desc=Pokud budou do větve nahrány nové revize, které mění obsah tohoto pull requestu, všechna stará schválení budou zamítnuta. settings.ignore_stale_approvals=Ignorovat zastaralá schválení @@ -2360,14 +2435,18 @@ settings.ignore_stale_approvals_desc=Nezapočítávejte schválení, která byla settings.require_signed_commits=Vyžadovat podepsané revize settings.require_signed_commits_desc=Odmítnout nahrání do této větve pokud nejsou podepsaná nebo jsou neověřitelná. settings.protect_branch_name_pattern=Vzor jména chráněných větví +settings.protect_branch_name_pattern_desc=Vzory jmen chráněných větví. Pro vzorovou syntaxi viz dokumentace. Příklady: main, release/** settings.protect_patterns=Vzory settings.protect_protected_file_patterns=Vzory chráněných souborů (oddělené středníkem „;“): +settings.protect_protected_file_patterns_desc=Chráněné soubory, které nemají povoleno být měněny přímo, i když uživatel má právo přidávat, upravovat nebo mazat soubory v této větvi. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na %[2]s dokumentaci pro syntaxi vzoru. Příklady: .drone.yml, /docs/**/*.txt. settings.protect_unprotected_file_patterns=Vzory nechráněných souborů (oddělené středníkem „;“): +settings.protect_unprotected_file_patterns_desc=Nechráněné soubory, které je možné měnit přímo, pokud má uživatel právo zápisu, čímž se obejde omezení push. Více vzorů lze oddělit pomocí středníku („;“). Podívejte se na %[2]s dokumentaci pro syntaxi vzoru. Příklady: .drone.yml, /docs/**/*.txt. settings.add_protected_branch=Zapnout ochranu settings.delete_protected_branch=Vypnout ochranu settings.update_protect_branch_success=Ochrana větví pro větev „%s“ byla aktualizována. settings.remove_protected_branch_success=Ochrana větví pro větev „%s“ byla zakázána. settings.remove_protected_branch_failed=Odstranění ochranného pravidla větve „%s“ se nezdařilo. +settings.protected_branch_deletion=Odstranit ochranu věteve settings.protected_branch_deletion_desc=Zakázání ochrany větví umožní uživatelům s právem zápisu nahrávat do této větve. Pokračovat? settings.block_rejected_reviews=Blokovat sloučení při zamítavých posouzeních settings.block_rejected_reviews_desc=Slučování nebude možné, pokud o změny požádají oficiální posuzovatelé, i když je k dispozici dostatek schválení. @@ -2375,8 +2454,11 @@ settings.block_on_official_review_requests=Blokovat sloučení při oficiální settings.block_on_official_review_requests_desc=Slučování nebude možné, pokud mají oficiální požadavek na posouzení, i když mají k dispozici dostatek schválení. settings.block_outdated_branch=Blokovat sloučení, pokud je pull request zastaralý settings.block_outdated_branch_desc=Slučování nebude možné, pokud je hlavní větev za základní větví. +settings.block_admin_merge_override=Pravidla pro ochranu větví se vztahují i na administrátory +settings.block_admin_merge_override_desc=Pravidla pro ochranu větví se vztahují i na administrátory a nesmějí je obcházet. settings.default_branch_desc=Vybrat výchozí větev repozitáře pro pull requesty a revize kódu: settings.merge_style_desc=Sloučit styly +settings.default_merge_style_desc=Výchozí styl sloučení settings.choose_branch=Vyberte větev… settings.no_protected_branch=Nejsou tu žádné chráněné větve. settings.edit_protected_branch=Upravit @@ -2392,12 +2474,25 @@ settings.tags.protection.allowed.teams=Povolené týmy settings.tags.protection.allowed.noone=Nikdo settings.tags.protection.create=Chránit značku settings.tags.protection.none=Neexistují žádné chráněné značky. +settings.tags.protection.pattern.description=Můžete použít jediné jméno nebo vzor glob nebo regulární výraz, který bude odpovídat více značek. Přečtěte si více v průvodci chráněnými značkami. settings.bot_token=Token pro robota settings.chat_id=ID chatu settings.thread_id=ID vlákna settings.matrix.homeserver_url=URL adresa Homeserveru settings.matrix.room_id=ID místnosti settings.matrix.message_type=Typ zprávy +settings.visibility.private.button=Nastavit jako soukromé +settings.visibility.private.text=Změna viditelnosti na soukromou nejen zviditelní repozitář pouze pro povolené členy, ale může odstranit vztah mezi ním a rozštěpením, sledujícími a oblíbeností. +settings.visibility.private.bullet_title=Změna viditelnosti na soukromou způsobí: +settings.visibility.private.bullet_one=Zviditelnit repozitář pouze pro povolené členy. +settings.visibility.private.bullet_two=Může odstranit vztah mezi ním a rozštěpeními, sledujícímia oblíbeností. +settings.visibility.public.button=Nastavit jako veřejné +settings.visibility.public.text=Změna viditelnosti na veřejné učiní repozitář viditelným pro kohokoliv. +settings.visibility.public.bullet_title=Změna viditelnosti na veřejnou způsobí: +settings.visibility.public.bullet_one=Zviditelnit repozitář pro kohokoliv. +settings.visibility.success=Viditelnost repozitáře se změnila. +settings.visibility.error=Nastala chyba při pokusu o změnu viditelnosti repozitáře. +settings.visibility.fork_error=Viditelnost rozštěpeného repozitáře nelze změnit. settings.archive.button=Archivovat repozitář settings.archive.header=Archivovat tento repozitář settings.archive.text=Archivace repozitáře způsobí, že bude zcela určen pouze pro čtení. Bude skryt z ovládacího panelu. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat nové úkoly nebo pull requesty. @@ -2480,7 +2575,6 @@ diff.generated=vygenerováno diff.vendored=vendorováno diff.comment.add_line_comment=Přidat jednořádkový komentář diff.comment.placeholder=Zanechat komentář -diff.comment.markdown_info=Je podporována úprava vzhledu pomocí markdown. diff.comment.add_single_comment=Přidat jeden komentář diff.comment.add_review_comment=Přidat komentář diff.comment.start_review=Začít posuzování @@ -2594,6 +2688,7 @@ tag.create_success=Značka „%s“ byla vytvořena. topic.manage_topics=Spravovat témata topic.done=Hotovo +topic.count_prompt=Nelze vybrat více než 25 témat topic.format_prompt=Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a tečky („.“) a může být dlouhé až 35 znaků. Písmena musí být malá. find_file.go_to_file=Přejít na soubor @@ -2691,6 +2786,7 @@ teams.leave.detail=Opustit %s? teams.can_create_org_repo=Vytvořit repozitáře teams.can_create_org_repo_helper=Členové mohou vytvářet nové repozitáře v organizaci. Tvůrce získá přístup správce do nového repozitáře. teams.none_access=Bez přístupu +teams.none_access_helper=Členové nemohou tuto jednotku prohlížet ani s ní provádět žádné jiné úkony. Pro veřejné repozitáře to nemá žádný vliv. teams.general_access=Obecný přístup teams.general_access_helper=O oprávnění členů bude rozhodnuto níže uvedenou tabulkou oprávnění. teams.read_access=Čtení @@ -2743,6 +2839,7 @@ self_check=Samokontrola identity_access=Identita a přístup users=Uživatelské účty organizations=Organizace +assets=Prostředky kódu repositories=Repozitáře hooks=Webové háčky integrations=Integrace @@ -2758,6 +2855,7 @@ last_page=Poslední total=Celkem: %d settings=Nastavení správce +dashboard.new_version_hint=Gitea %s je nyní k dispozici, vy používáte %s. Další podrobnosti najdete na blogu. dashboard.statistic=Souhrn dashboard.maintenance_operations=Operace údržby dashboard.system_status=Status systému @@ -2800,6 +2898,7 @@ dashboard.reinit_missing_repos=Znovu inicializovat všechny chybějící repozit dashboard.sync_external_users=Synchronizovat externí uživatelská data dashboard.cleanup_hook_task_table=Vyčistit tabulku hook_task dashboard.cleanup_packages=Vyčistit prošlé balíčky +dashboard.cleanup_actions=Vyčištění prostředků akcí, jejichž platnost vypršela dashboard.server_uptime=Doba provozu serveru dashboard.current_goroutine=Aktuální Goroutines dashboard.current_memory_usage=Aktuální využití paměti @@ -2829,12 +2928,19 @@ dashboard.total_gc_time=Celková pauza GC dashboard.total_gc_pause=Celková pauza GC dashboard.last_gc_pause=Poslední pauza GC dashboard.gc_times=Časy GC +dashboard.delete_old_actions=Odstranit všechny staré aktivity z databáze +dashboard.delete_old_actions.started=Začalo odstraňování všech starých aktivit z databáze. dashboard.update_checker=Kontrola aktualizací dashboard.delete_old_system_notices=Odstranit všechna stará systémová upozornění z databáze dashboard.gc_lfs=Úklid LFS meta objektů +dashboard.stop_zombie_tasks=Zastavit zombie úlohy akcí +dashboard.stop_endless_tasks=Zastavit nekonečné úlohy akcí +dashboard.cancel_abandoned_jobs=Zrušit opuštěné úlohy akcí +dashboard.start_schedule_tasks=Spustit naplánované úlohy akcí dashboard.sync_branch.started=Synchronizace větví byla spuštěna dashboard.sync_tag.started=Synchronizace značek spuštěna dashboard.rebuild_issue_indexer=Znovu sestavit index úkolů +dashboard.sync_repo_licenses=Synchronizovat licence repozitáře users.user_manage_panel=Správa uživatelských účtů users.new_account=Vytvořit uživatelský účet @@ -2906,6 +3012,10 @@ emails.not_updated=Aktualizace požadované e-mailové adresy se nezdařila: %v emails.duplicate_active=Tato e-mailová adresa je již aktivní pro jiného uživatele. emails.change_email_header=Aktualizovat vlastnosti e-mailu emails.change_email_text=Opravdu chcete aktualizovat tuto e-mailovou adresu? +emails.delete=Odstranit e-mail +emails.delete_desc=Opravdu chcete odstranit tuto e-mailovou adresu? +emails.deletion_success=E-mailová adresa byla odstraněna. +emails.delete_primary_email_error=Primární e-mail nelze odstranit. orgs.org_manage_panel=Správa organizací orgs.name=Název @@ -2938,10 +3048,12 @@ packages.size=Velikost packages.published=Publikováno defaulthooks=Výchozí webové háčky +defaulthooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde jsou výchozí a budou zkopírovány do všech nových repozitářů. Přečtěte si více v průvodci webovými háčky. defaulthooks.add_webhook=Přidat výchozí webový háček defaulthooks.update_webhook=Aktualizovat výchozí webový háček systemhooks=Systémové webové háčky +systemhooks.desc=Webové háčky automaticky vytvářejí HTTP POST dotazy na server při určitých Gitea událostech. Webové háčky definované zde budou vykonány na všech repozitářích systému, proto prosím zvažte jakékoli důsledky, které to může mít na výkon. Přečtěte si více v průvodci webovými háčky. systemhooks.add_webhook=Přidat systémový webový háček systemhooks.update_webhook=Aktualizovat systémový webový háček @@ -3036,8 +3148,18 @@ auths.tips=Tipy auths.tips.oauth2.general=Ověřování OAuth2 auths.tips.oauth2.general.tip=Při registraci nové OAuth2 autentizace by URL callbacku/přesměrování měla být: auths.tip.oauth2_provider=Poskytovatel OAuth2 +auths.tip.bitbucket=Vytvořte nového OAuth konzumenta na %s a přidejte oprávnění „Account“ - „Read“ auths.tip.nextcloud=Zaregistrujte nového OAuth konzumenta na vaší instanci pomocí následujícího menu „Nastavení -> Zabezpečení -> OAuth 2.0 klient“ +auths.tip.dropbox=Vytvořte novou aplikaci na %s +auths.tip.facebook=Registrujte novou aplikaci na %s a přidejte produkt „Facebook Login“ +auths.tip.github=Registrujte novou OAuth aplikaci na %s +auths.tip.gitlab_new=Zaregistrujte novou aplikaci na %s +auths.tip.google_plus=Získejte klientské pověření OAuth2 z Google API konzole na %s auths.tip.openid_connect=Použijte OpenID Connect URL pro objevování spojení „https://{server}/.well-known/openid-configuration“ k nastavení koncových bodů +auths.tip.twitter=Jděte na %s, vytvořte aplikaci a ujistěte se, že volba „Allow this application to be used to Sign in with Twitter“ je povolená +auths.tip.discord=Registrujte novou aplikaci na %s +auths.tip.gitea=Registrovat novou Oauth2 aplikaci. Návod naleznete na %s +auths.tip.yandex=Vytvořte novou aplikaci na %s. Vyberte následující oprávnění z „Yandex.Passport API“ sekce: „Přístup k e-mailové adrese“, „Přístup k uživatelskému avataru“ a „Přístup k uživatelskému jménu, jménu a příjmení, pohlaví“ auths.tip.mastodon=Vložte vlastní URL instance pro mastodon, kterou se chcete autentizovat (nebo použijte výchozí) auths.edit=Upravit zdroj ověřování auths.activated=Tento zdroj ověřování je aktivován @@ -3203,6 +3325,7 @@ monitor.next=Příští čas spuštění monitor.previous=Předešlý čas spuštění monitor.execute_times=Vykonání monitor.process=Spuštěné procesy +monitor.stacktrace=Výpisy zásobníku monitor.processes_count=%d procesů monitor.download_diagnosis_report=Stáhnout diagnosttickou zprávu monitor.desc=Popis @@ -3210,6 +3333,8 @@ monitor.start=Čas zahájení monitor.execute_time=Doba provádění monitor.last_execution_result=Výsledek monitor.process.cancel=Zrušit proces +monitor.process.cancel_desc=Zrušení procesu může způsobit ztrátu dat +monitor.process.cancel_notices=Zrušit: %s? monitor.process.children=Potomek monitor.queues=Fronty @@ -3311,6 +3436,7 @@ raw_minutes=minut [dropzone] default_message=Přetáhněte soubory nebo klikněte sem pro nahrání. +invalid_input_type=Nemůžete nahrávat soubory tohoto typu. file_too_big=Velikost souboru ({{filesize}} MB) je vyšší než maximální velikost ({{maxFilesize}} MB). remove_file=Smazat soubor @@ -3383,6 +3509,9 @@ alpine.repository=Informace o repozitáři alpine.repository.branches=Větve alpine.repository.repositories=Repozitáře alpine.repository.architectures=Architektury +arch.repository=Informace o repozitáři +arch.repository.repositories=Repozitáře +arch.repository.architectures=Architektury cargo.registry=Nastavte tento registr v konfiguračním souboru Cargo (například ~/.cargo/config.toml): cargo.install=Chcete-li nainstalovat balíček pomocí Cargo, spusťte následující příkaz: chef.registry=Nastavit tento registr v souboru ~/.chef/config.rb: @@ -3582,12 +3711,18 @@ runs.no_workflows.quick_start=Nevíte jak začít s Gitea Actions? Podívejte se runs.no_workflows.documentation=Další informace o Gitea Actions naleznete v dokumentaci. runs.no_runs=Pracovní postup zatím nebyl spuštěn. runs.empty_commit_message=(prázdná zpráva commitu) +runs.expire_log_message=Logy byly vyčištěny, protože byly příliš staré. workflow.disable=Zakázat pracovní postup workflow.disable_success=Pracovní postup „%s“ byl úspěšně deaktivován. workflow.enable=Povolit pracovní postup workflow.enable_success=Pracovní postup „%s“ byl úspěšně aktivován. workflow.disabled=Pracovní postup je zakázán. +workflow.run=Spustit pracovní postup +workflow.not_found=Pracovní postup „%s“ nebyl nalezen. +workflow.run_success=Pracovní postup „%s“ proběhl úspěšně. +workflow.from_ref=Použít pracovní postup od +workflow.has_workflow_dispatch=Tento pracovní postup má spouštěč události workflow_dispatch. need_approval_desc=Potřebujete schválení pro spuštění pracovních postupů pro rozštěpený pull request. @@ -3608,6 +3743,7 @@ variables.update.failed=Úprava proměnné se nezdařila. variables.update.success=Proměnná byla upravena. [projects] +deleted.display_name=Odstraněný projekt type-1.display_name=Samostatný projekt type-2.display_name=Projekt repozitíře type-3.display_name=Projekt organizace diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 4f011663e88..c8dd3f71a23 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -201,6 +201,7 @@ buttons.link.tooltip=Link hinzufügen buttons.list.unordered.tooltip=Liste hinzufügen buttons.list.ordered.tooltip=Nummerierte Liste hinzufügen buttons.list.task.tooltip=Aufgabenliste hinzufügen +buttons.table.add.insert=Hinzufügen buttons.mention.tooltip=Benutzer oder Team erwähnen buttons.ref.tooltip=Issue oder Pull-Request referenzieren buttons.switch_to_legacy.tooltip=Legacy-Editor verwenden @@ -997,7 +998,6 @@ generate_repo=Repository erstellen generate_from=Erstelle aus repo_desc=Beschreibung repo_desc_helper=Gib eine kurze Beschreibung an (optional) -repo_lang=Sprache repo_gitignore_helper=Wähle eine .gitignore-Vorlage aus. repo_gitignore_helper_desc=Wähle aus einer Liste an Vorlagen für bekannte Sprachen, welche Dateien ignoriert werden sollen. Typische Artefakte, die durch die Build Tools der gewählten Sprache generiert werden, sind standardmäßig Bestandteil der .gitignore. issue_labels=Issue Label @@ -1058,7 +1058,6 @@ delete_preexisting_success=Nicht übernommene Dateien in %s gelöscht blame_prior=Blame vor dieser Änderung anzeigen blame.ignore_revs=Revisionen in .git-blame-ignore-revs werden ignoriert. Klicke hier, um das zu umgehen und die normale Blame-Ansicht zu sehen. blame.ignore_revs.failed=Fehler beim Ignorieren der Revisionen in .git-blame-ignore-revs. -author_search_tooltip=Zeigt maximal 30 Benutzer tree_path_not_found_commit=Pfad %[1]s existiert nicht in Commit%[2]s tree_path_not_found_branch=Pfad %[1]s existiert nicht in Branch %[2]s @@ -1399,12 +1398,9 @@ issues.new.no_items=Keine Einträge issues.new.milestone=Meilenstein issues.new.no_milestone=Kein Meilenstein issues.new.clear_milestone=Meilenstein entfernen -issues.new.open_milestone=Offene Meilensteine -issues.new.closed_milestone=Geschlossene Meilensteine issues.new.assignees=Zuständig issues.new.clear_assignees=Zuständige entfernen issues.new.no_assignees=Niemand zuständig -issues.new.no_reviewers=Keine Reviewer issues.new.blocked_user=Das Issue kann nicht erstellt werden, da du vom Repository-Eigentümer blockiert wurdest. issues.edit.blocked_user=Der Inhalt kann nicht bearbeitet werden, da du vom Repository-Eigentümer blockiert wurdest. issues.choose.get_started=Los geht's @@ -1463,7 +1459,6 @@ issues.filter_assignee=Zuständig issues.filter_assginee_no_select=Alle Zuständigen issues.filter_assginee_no_assignee=Niemand zuständig issues.filter_poster=Autor -issues.filter_poster_no_select=Alle Autoren issues.filter_type=Typ issues.filter_type.all_issues=Alle Issues issues.filter_type.assigned_to_you=Dir zugewiesen @@ -1604,27 +1599,20 @@ issues.comment_on_locked=Du kannst einen gesperrten Issue nicht kommentieren. issues.delete=Löschen issues.delete.title=Dieses Issue löschen? issues.delete.text=Möchtest du dieses Issue wirklich löschen? (Dadurch wird der Inhalt dauerhaft gelöscht. Denke daran, es stattdessen zu schließen, wenn du es archivieren willst) + issues.tracker=Zeiterfassung -issues.start_tracking_short=Zeiterfassung starten -issues.start_tracking=Zeiterfassung starten -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=Zeiterfassung stoppen -issues.stop_tracking_history=hat die Zeiterfassung %s angehalten -issues.cancel_tracking=Verwerfen issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen` -issues.add_time=Zeit manuell hinzufügen issues.del_time=Diese Zeiterfassung löschen -issues.add_time_short=Zeit hinzufügen -issues.add_time_cancel=Abbrechen -issues.add_time_history=`hat %s gearbeitete Zeit hinzugefügt` issues.del_time_history=`hat %s gearbeitete Zeit gelöscht` issues.add_time_hours=Stunden issues.add_time_minutes=Minuten issues.add_time_sum_to_small=Es wurde keine Zeit eingegeben. issues.time_spent_total=Zeitaufwand insgesamt issues.time_spent_from_all_authors=`Aufgewendete Zeit: %s` + issues.due_date=Fällig am issues.invalid_due_date_format=Das Fälligkeitsdatum muss das Format „JJJJ-MM-TT“ haben. issues.error_modifying_due_date=Fehler beim Ändern des Fälligkeitsdatums. @@ -2452,7 +2440,6 @@ diff.generated=generiert diff.vendored=vendored diff.comment.add_line_comment=Einzelnen Kommentar hinzufügen diff.comment.placeholder=Kommentieren... -diff.comment.markdown_info=Styling mit Markdown wird unterstützt. diff.comment.add_single_comment=Einzelnen Kommentar hinzufügen diff.comment.add_review_comment=Kommentar hinzufügen diff.comment.start_review=Review starten @@ -3344,6 +3331,9 @@ alpine.repository=Repository-Informationen alpine.repository.branches=Branches alpine.repository.repositories=Repositories alpine.repository.architectures=Architekturen +arch.repository=Repository-Informationen +arch.repository.repositories=Repositories +arch.repository.architectures=Architekturen cargo.registry=Richte diese Registry in der Cargo-Konfigurationsdatei ein (z.B. ~/.cargo/config.toml): cargo.install=Um das Paket mit Cargo zu installieren, führe den folgenden Befehl aus: chef.registry=Richte diese Registry in deiner ~/.chef/config.rb Datei ein: diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 8a82d9353a3..193441828ae 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -172,6 +172,7 @@ buttons.link.tooltip=Προσθήκη συνδέσμου buttons.list.unordered.tooltip=Προσθήκη απλής λίστας buttons.list.ordered.tooltip=Προσθήκη αριθμημένης λίστας buttons.list.task.tooltip=Προσθήκη λίστας εργασιών +buttons.table.add.insert=Προσθήκη buttons.mention.tooltip=Μνημόνευση ενός χρήστη ή ομάδας buttons.ref.tooltip=Μνημόνευση ενός θέματος ή pull request buttons.switch_to_legacy.tooltip=Χρήση του κλασσικού κειμενογράφου @@ -934,7 +935,6 @@ generate_repo=Δημιουργία Αποθετηρίου generate_from=Δημιουργία Από repo_desc=Περιγραφή repo_desc_helper=Εισάγετε μια σύντομη περιγραφή (προαιρετικό) -repo_lang=Γλώσσα repo_gitignore_helper=Επιλέξτε πρότυπα .gitignore. repo_gitignore_helper_desc=Επιλέξτε ποια αρχεία δεν θα παρακολουθείτε από μια λίστα προτύπων για κοινές γλώσσες προγραμματισμού. Τυπικά αντικείμενα που δημιουργούνται από τα εργαλεία κατασκευής κάθε γλώσσας περιλαμβάνονται ήδη στο .gitignore. issue_labels=Σήματα Ζητήματος @@ -991,7 +991,6 @@ delete_preexisting_success=Διαγράφηκαν τα μη υιοθετημέν blame_prior=Προβολή ευθύνης πριν από αυτή την αλλαγή blame.ignore_revs=Αγνόηση των αναθεωρήσεων στο .git-blame-ignore-revs. Πατήστε εδώ για να το παρακάμψετε και να δείτε την κανονική προβολή ευθυνών. blame.ignore_revs.failed=Αποτυχία αγνόησης των αναθεωρήσεων στο .git-blame-ignore-revs. -author_search_tooltip=Εμφάνιση το πολύ 30 χρηστών tree_path_not_found_commit=Η διαδρομή %[1]s δεν υπάρχει στην υποβολή %[2]s tree_path_not_found_branch=Η διαδρομή %[1]s δεν υπάρχει στον κλάδο %[2]s @@ -1324,12 +1323,9 @@ issues.new.no_items=Δεν υπάρχουν αντικείμενα issues.new.milestone=Ορόσημο issues.new.no_milestone=Χωρίς Ορόσημο issues.new.clear_milestone=Καθαρισμός ορόσημου -issues.new.open_milestone=Ανοιχτά Ορόσημα -issues.new.closed_milestone=Κλειστά Ορόσημα issues.new.assignees=Αποδέκτες issues.new.clear_assignees=Εκκαθάριση αποδεκτών issues.new.no_assignees=Χωρίς Αποδέκτη -issues.new.no_reviewers=Δεν υπάρχουν εξεταστές issues.choose.get_started=Ας Αρχίσουμε issues.choose.open_external_link=Άνοιγμα issues.choose.blank=Προεπιλογή @@ -1386,7 +1382,6 @@ issues.filter_assignee=Αποδέκτης issues.filter_assginee_no_select=Όλοι οι αποδέκτες issues.filter_assginee_no_assignee=Κανένας Αποδέκτης issues.filter_poster=Συγγραφέας -issues.filter_poster_no_select=Όλοι οι συγγραφείς issues.filter_type=Τύπος issues.filter_type.all_issues=Όλα τα ζητήματα issues.filter_type.assigned_to_you=Ανατέθηκαν σε εσάς @@ -1526,27 +1521,20 @@ issues.comment_on_locked=Δεν μπορείτε να σχολιάσετε έν issues.delete=Διαγραφή issues.delete.title=Διαγραφή αυτού του ζητήματος; issues.delete.text=Θέλετε πραγματικά να διαγράψετε αυτό το ζήτημα; (Αυτό θα σβήσει οριστικά όλο το περιεχόμενο του. Εξετάστε αν θέλετε να το κλείσετε, αν σκοπεύεται να το αρχειοθετήσετε) + issues.tracker=Καταγραφή Χρόνου -issues.start_tracking_short=Εκκίνηση Χρονομέτρου -issues.start_tracking=Εκκίνηση Καταγραφής Χρόνου -issues.start_tracking_history=`ξεκίνησε να εργάζεται %s` + issues.tracker_auto_close=Το χρονόμετρο θα σταματήσει αυτόματα όταν κλείσει αυτό το ζήτημα issues.tracking_already_started=`Έχετε ήδη ξεκινήσει την καταγραφή του χρόνου σε ένα άλλο ζήτημα!` -issues.stop_tracking=Διακοπή Χρονομέτρου -issues.stop_tracking_history=`σταμάτησε να εργάζεται %s` -issues.cancel_tracking=Απόρριψη issues.cancel_tracking_history=`ακύρωσε τη παρακολούθηση χρόνου %s` -issues.add_time=Χειροκίνητη Προσθήκη Ώρας issues.del_time=Διαγραφή αυτού του αρχείου χρόνου -issues.add_time_short=Προσθήκη Χρόνου -issues.add_time_cancel=Ακύρωση -issues.add_time_history=`πρόσθεσε χρόνο που δαπανήθηκε %s` issues.del_time_history=`διέγραψε το χρόνο που δαπανήθηκε %s` 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=Η μορφή της ημερομηνίας παράδοσης πρέπει να είναι 'yyyy-mm-dd'. issues.error_modifying_due_date=Αποτυχία τροποποίησης της ημερομηνίας παράδοσης. @@ -2358,7 +2346,6 @@ diff.generated=δημιουργημένο diff.vendored=εξωτερικό diff.comment.add_line_comment=Προσθήκη σχολίου στη γραμμή diff.comment.placeholder=Αφήστε ένα σχόλιο -diff.comment.markdown_info=Υποστηρίζεται στυλ με markdown. diff.comment.add_single_comment=Προσθέστε ένα σχόλιο diff.comment.add_review_comment=Προσθήκη σχολίου diff.comment.start_review=Έναρξη αξιολόγησης @@ -3232,6 +3219,9 @@ alpine.repository=Πληροφορίες Αποθετηρίου alpine.repository.branches=Κλάδοι alpine.repository.repositories=Αποθετήρια alpine.repository.architectures=Αρχιτεκτονικές +arch.repository=Πληροφορίες Αποθετηρίου +arch.repository.repositories=Αποθετήρια +arch.repository.architectures=Αρχιτεκτονικές cargo.registry=Ρυθμίστε αυτό το μητρώο στις ρυθμίσεις του Cargo (για παράδειγμα ~/.cargo/config.toml): cargo.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το Cargo, εκτελέστε την ακόλουθη εντολή: chef.registry=Ρυθμίστε αυτό το μητρώο στο αρχείο ~/.chef/config.rb: diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e5c3cd38c8a..92ce4f2db9f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -145,6 +145,7 @@ confirm_delete_selected = Confirm to delete all selected items? name = Name value = Value +readme = Readme filter = Filter filter.clear = Clear Filter @@ -1045,7 +1046,8 @@ generate_repo = Generate Repository generate_from = Generate From repo_desc = Description repo_desc_helper = Enter short description (optional) -repo_lang = Language +repo_no_desc = No description provided +repo_lang = Languages repo_gitignore_helper = Select .gitignore templates. repo_gitignore_helper_desc = Choose which files not to track from a list of templates for common languages. Typical artifacts generated by each language's build tools are included on .gitignore by default. issue_labels = Issue Labels @@ -1107,7 +1109,7 @@ delete_preexisting_success = Deleted unadopted files in %s blame_prior = View blame prior to this change blame.ignore_revs = Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view. blame.ignore_revs.failed = Failed to ignore revisions in .git-blame-ignore-revs. -author_search_tooltip = Shows a maximum of 30 users +user_search_tooltip = Shows a maximum of 30 users tree_path_not_found_commit = Path %[1]s doesn't exist in commit %[2]s tree_path_not_found_branch = Path %[1]s doesn't exist in branch %[2]s @@ -1527,7 +1529,8 @@ issues.filter_assignee = Assignee issues.filter_assginee_no_select = All assignees issues.filter_assginee_no_assignee = No assignee issues.filter_poster = Author -issues.filter_poster_no_select = All authors +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.assigned_to_you = Assigned to you @@ -1677,7 +1680,6 @@ issues.timetracker_timer_stop = Stop timer issues.timetracker_timer_discard = Discard timer issues.timetracker_timer_manually_add = Add Time -issues.time_estimate_placeholder = 1h 2m issues.time_estimate_set = Set estimated time issues.time_estimate_display = Estimate: %s issues.change_time_estimate_at = changed time estimate to %s %s @@ -1944,6 +1946,10 @@ pulls.delete.title = Delete this pull request? pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) pulls.recently_pushed_new_branches = You pushed on branch %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1 = This branch is %d commit behind %s +pulls.upstream_diverging_prompt_behind_n = This branch is %d commits behind %s +pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes +pulls.upstream_diverging_merge = Sync fork pull.deleted_branch = (deleted):%s pull.agit_documentation = Review documentation about AGit @@ -2626,6 +2632,7 @@ release.new_release = New Release release.draft = Draft release.prerelease = Pre-Release release.stable = Stable +release.latest = Latest release.compare = Compare release.edit = edit release.ahead.commits = %d commits diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index 423f74f8b20..e95513766bb 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -170,6 +170,7 @@ buttons.link.tooltip=Añadir un enlace buttons.list.unordered.tooltip=Añadir una lista buttons.list.ordered.tooltip=Añadir una lista numerada buttons.list.task.tooltip=Añadir una lista de tareas +buttons.table.add.insert=Añadir buttons.mention.tooltip=Mencionar un usuario o equipo buttons.ref.tooltip=Referir a una incidencia o pull request buttons.switch_to_legacy.tooltip=Utilizar el editor antiguo en su lugar @@ -924,7 +925,6 @@ generate_repo=Generar repositorio generate_from=Generar desde repo_desc=Descripción repo_desc_helper=Introduce una descripción corta (opcional) -repo_lang=Idioma repo_gitignore_helper=Seleccionar plantillas de .gitignore. repo_gitignore_helper_desc=Elija qué archivos no rastrear de una lista de plantillas para idiomas comunes. Los artefactos típicos generados por las herramientas de construcción de cada idioma se incluyen por defecto en .gitignore. issue_labels=Etiquetas de incidencia @@ -981,7 +981,6 @@ delete_preexisting_success=Eliminó archivos no adoptados en %s blame_prior=Ver la culpa antes de este cambio blame.ignore_revs=Ignorando revisiones en .git-blame-ignore-revs. Haga clic aquí para saltar y para a la vista normal. blame.ignore_revs.failed=No se pudieron ignorar las revisiones en .git-blame-ignore-revs. -author_search_tooltip=Muestra un máximo de 30 usuarios tree_path_not_found_commit=La ruta %[1]s no existe en el commit %[2]s tree_path_not_found_branch=La ruta %[1]s no existe en la rama %[2]s @@ -1314,12 +1313,9 @@ issues.new.no_items=No hay elementos issues.new.milestone=Milestone issues.new.no_milestone=Sin Milestone issues.new.clear_milestone=Limpiar Milestone -issues.new.open_milestone=Milestones abiertas -issues.new.closed_milestone=Milestones cerradas issues.new.assignees=Asignados issues.new.clear_assignees=Limpiar asignados issues.new.no_assignees=No asignados -issues.new.no_reviewers=No hay revisores issues.choose.get_started=Comenzar issues.choose.open_external_link=Abrir issues.choose.blank=Predeterminado @@ -1376,7 +1372,6 @@ issues.filter_assignee=Asignada a issues.filter_assginee_no_select=Todos los asignados issues.filter_assginee_no_assignee=Sin asignado issues.filter_poster=Autor -issues.filter_poster_no_select=Todos los autores issues.filter_type=Tipo issues.filter_type.all_issues=Todas las incidencias issues.filter_type.assigned_to_you=Asignadas a ti @@ -1516,27 +1511,20 @@ issues.comment_on_locked=No puede comentar una incidencia bloqueada. issues.delete=Eliminar issues.delete.title=¿Eliminar esta incidencia? issues.delete.text=¿Realmente quieres eliminar esta incidencia? (Esto eliminará permanentemente todo el contenido. Considera cerrarlo en su lugar, si quieres mantenerlo archivado) + issues.tracker=Gestor de tiempo -issues.start_tracking_short=Iniciar temporizador -issues.start_tracking=Inicio de seguimiento de tiempo -issues.start_tracking_history=`ha empezado a trabajar %s` + issues.tracker_auto_close=El temporizador se detendrá automáticamente cuando se cierre este problema issues.tracking_already_started=`¡Ya has iniciado el seguimiento de tiempo en otro problema!` -issues.stop_tracking=Detener temporizador -issues.stop_tracking_history=`dejó de trabajar %s` -issues.cancel_tracking=Descartar issues.cancel_tracking_history=`canceló el seguimiento de tiempo %s` -issues.add_time=Añadir tiempo gastado manualmente issues.del_time=Eliminar este registro de tiempo -issues.add_time_short=Añadir tiempo gastado -issues.add_time_cancel=Cancelar -issues.add_time_history=`añadió tiempo gastado %s` issues.del_time_history=`eliminado el tiempo gastado %s` issues.add_time_hours=Horas issues.add_time_minutes=Minutos issues.add_time_sum_to_small=No se ha entrado tiempo. issues.time_spent_total=Tiempo total gastado issues.time_spent_from_all_authors=`Tiempo total gastado: %s` + issues.due_date=Fecha de vencimiento issues.invalid_due_date_format=El formato de la fecha de vencimiento debe ser 'aaaa-mm-dd'. issues.error_modifying_due_date=Fallo al modificar la fecha de vencimiento. @@ -2339,7 +2327,6 @@ diff.generated=generado diff.vendored=vendido diff.comment.add_line_comment=Añadir comentario en línea diff.comment.placeholder=Deja un comentario -diff.comment.markdown_info=Es posible estilizar con markdown. diff.comment.add_single_comment=Añadir solo comentario diff.comment.add_review_comment=Añadir comentario diff.comment.start_review=Comenzar revisión @@ -3211,6 +3198,9 @@ alpine.repository=Información del repositorio alpine.repository.branches=Ramas alpine.repository.repositories=Repositorios alpine.repository.architectures=Arquitecturas +arch.repository=Información del repositorio +arch.repository.repositories=Repositorios +arch.repository.architectures=Arquitecturas cargo.registry=Configurar este registro en el archivo de configuración de Cargo (por ejemplo ~/.cargo/config.toml): cargo.install=Para instalar el paquete usando Cargo, ejecute el siguiente comando: chef.registry=Configura este registro en tu archivo ~/.chef/config.rb: diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 33d72ee7eb7..640592f2bfb 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -114,6 +114,7 @@ filter.private=خصوصی [heatmap] [editor] +buttons.table.add.insert=افزودن [filter] @@ -724,7 +725,6 @@ generate_repo=ساختن مخزن generate_from=ساختن از repo_desc=توضیحات repo_desc_helper=توضیحات مختصر را وارد کنید(اختیاری) -repo_lang=زبان repo_gitignore_helper=یک قالب برای .gitignore انتخاب کنید. repo_gitignore_helper_desc=از فهرست الگوهای زبان‌های رایج، فایل‌هایی را انتخاب کنید که ردیابی نشوند. مصنوعات معمولی تولید شده توسط ابزارهای ساخت هر زبان به طور پیش فرض در gitignore. گنجانده شده است. issue_labels=برچسب‌های مسئله @@ -1013,12 +1013,9 @@ issues.new.no_items=موردی وجود ندارد issues.new.milestone=نقطه عطف issues.new.no_milestone=بدون نقطه عطف issues.new.clear_milestone=پاک‌کردن نقطه عطف -issues.new.open_milestone=نقاط عطف باز -issues.new.closed_milestone=نقاط عطف بسته issues.new.assignees=تخصیص شده issues.new.clear_assignees=پاک کردن تخصیص issues.new.no_assignees=بدون تخصیص -issues.new.no_reviewers=بدون بازبین گر issues.choose.get_started=آغاز کردن issues.choose.open_external_link=باز‌کردن issues.choose.blank=پیشگزیده @@ -1168,26 +1165,19 @@ issues.lock.title=انسداد مکالمه در این مسئله. issues.unlock.title=رفع انسداد مکالمه در این مسئله. issues.comment_on_locked=شما نمی‌توانید در مسئله قفل شده اظهار نظر کنید. issues.delete=حذف + issues.tracker=پیگیری زمان -issues.start_tracking_short=تایمز شروع -issues.start_tracking=شروع به پیگیری زمان -issues.start_tracking_history=`شروع به کار %s` + issues.tracker_auto_close=زمان‌سنج به صورت خودکار متوقف میشود زمانی که مسئله بسته شود issues.tracking_already_started=`شما قبلا رهگیری زمان را روی این مسئله آغاز کرده‌اید!` -issues.stop_tracking=تایمز ایست -issues.stop_tracking_history=`توقف کار در %s` -issues.cancel_tracking=ول کردن -issues.add_time=زمان را به صورت دستی وارد کنید issues.del_time=این لاگ را حذف کنید -issues.add_time_short=افزودن زمان -issues.add_time_cancel=انصراف -issues.add_time_history=`زمان صرف شده اضافه شد %s` issues.del_time_history=`زمان صرف شده حذف شد %s` 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=موعد مقرر، باید به سبک 'yyyy-mm-dd' باشد. issues.error_modifying_due_date=تغییر موعد مقرر با شکست مواجه شد. @@ -1814,7 +1804,6 @@ diff.load=Diff را بارگزاری کن diff.generated=تولید شده diff.vendored=فروخته شده diff.comment.placeholder=اظهار نظر کنید -diff.comment.markdown_info=شیوه markdown پیشتیبانی می‌شود. diff.comment.add_single_comment=افزودن یک دیدگاه به تنهایی diff.comment.add_review_comment=افزودن دیدگاه diff.comment.start_review=شروع بازبینی @@ -2517,6 +2506,7 @@ error.unit_not_allowed=شما اجازه دسترسی به این قسمت مخ filter.type=نوع alpine.repository.branches=شاخه‎ها alpine.repository.repositories=مخازن +arch.repository.repositories=مخازن conan.details.repository=مخزن owner.settings.cleanuprules.enabled=فعال شده diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index d16efb8834d..375c7b11bf9 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -128,6 +128,7 @@ filter.private=Yksityinen [heatmap] [editor] +buttons.table.add.insert=Lisää [filter] @@ -648,7 +649,6 @@ fork_visibility_helper=Forkatun repon näkyvyyttä ei voi muuttaa. download_zip=Lataa ZIP download_tar=Lataa TAR.GZ repo_desc=Kuvaus -repo_lang=Kieli repo_gitignore_helper=Valitse .gitignore mallit. issue_labels=Ongelmien tunnisteet issue_labels_helper=Valitse pohja ongelmien nimilapuille. @@ -806,8 +806,6 @@ issues.new.no_items=Ei kohteita issues.new.milestone=Merkkipaalu issues.new.no_milestone=Ei merkkipaalua issues.new.clear_milestone=Tyhjennä merkkipaalu -issues.new.open_milestone=Avoimet merkkipaalut -issues.new.closed_milestone=Suljetut merkkipaalut issues.new.assignees=Käsittelijä issues.new.clear_assignees=Tyhjennä käsittelijä issues.new.no_assignees=Ei käsittelijää @@ -914,21 +912,15 @@ issues.lock.reason=Lukitsemisen syy issues.lock.title=Lukitse keskustelu tästä ongelmasta. issues.unlock.title=Avaa keskustelu tästä ongelmasta. issues.delete=Poista + issues.tracker=Ajan seuranta -issues.start_tracking_short=Aloita ajanotto -issues.start_tracking=Aloita ajan seuranta -issues.start_tracking_history=`aloitti työskentelyn %s` + issues.tracker_auto_close=Ajan seuranta pysähtyy automaattisesti kun tämä ongelma on suljettu -issues.stop_tracking=Pysäytä ajanotto -issues.stop_tracking_history=`lopetti työskentelyn %s` -issues.add_time=Lisää aika käsin -issues.add_time_short=Lisää aika -issues.add_time_cancel=Peruuta -issues.add_time_history=`lisäsi käytetyn ajan %s` issues.add_time_hours=Tuntia issues.add_time_minutes=Minuuttia issues.add_time_sum_to_small=Aikaa ei syötetty. issues.time_spent_from_all_authors=`Käytetty kokonaisaika: %s` + issues.due_date=Määräpäivä issues.push_commit_1=lisäsi %d commitin %s issues.push_commits_n=lisäsi %d committia %s @@ -1239,7 +1231,6 @@ diff.view_file=Näytä tiedosto diff.file_image_width=Leveys diff.file_image_height=Korkeus diff.file_byte_size=Koko -diff.comment.markdown_info=Muotoilu markdownilla tuettu. diff.comment.add_single_comment=Lisää yksittäinen kommentti diff.comment.add_review_comment=Lisää kommentti diff.comment.start_review=Aloita tarkistus @@ -1694,6 +1685,7 @@ installation=Asennus details.author=Tekijä alpine.repository.branches=Haarat alpine.repository.repositories=Repot +arch.repository.repositories=Repot conan.details.repository=Repo owner.settings.cleanuprules.enabled=Käytössä diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index f58ce74564a..4d4940cff5a 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -104,6 +104,7 @@ copy_url=Copier l'URL copy_hash=Copier le hach copy_content=Copier le contenu copy_branch=Copier le nom de la branche +copy_path=Copier le chemin copy_success=Copié ! copy_error=Échec de la copie copy_type_unsupported=Ce type de fichier ne peut pas être copié @@ -144,6 +145,7 @@ confirm_delete_selected=Êtes-vous sûr de vouloir supprimer tous les éléments name=Nom value=Valeur +readme=Lisez-moi filter=Filtrer filter.clear=Effacer le filtre @@ -209,6 +211,10 @@ buttons.link.tooltip=Ajouter un lien buttons.list.unordered.tooltip=Ajouter une liste à puces buttons.list.ordered.tooltip=Ajouter une liste numérotée buttons.list.task.tooltip=Ajouter une liste de tâches +buttons.table.add.tooltip=Ajouter un tableau +buttons.table.add.insert=Ajouter +buttons.table.rows=Lignes +buttons.table.cols=Colonnes buttons.mention.tooltip=Mentionner un utilisateur ou une équipe buttons.ref.tooltip=Référencer un ticket ou demande d’ajout buttons.switch_to_legacy.tooltip=Utiliser l’ancien éditeur à la place @@ -348,6 +354,7 @@ enable_update_checker=Activer la vérification des mises-à-jour enable_update_checker_helper=Vérifie les mises à jour régulièrement en se connectant à gitea.io. env_config_keys=Configuration de l'environnement env_config_keys_prompt=Les variables d'environnement suivantes seront également ajoutées à votre fichier de configuration : +config_write_file_prompt=Ces options de configuration seront écrites dans : %s [home] nav_menu=Menu de navigation @@ -453,6 +460,7 @@ authorize_application=Autoriser l'application authorize_redirect_notice=Vous serez redirigé vers %s si vous autorisez cette application. authorize_application_created_by=Cette application a été créée par %s. authorize_application_description=Si vous accordez l'accès, il sera en mesure d'accéder et d'écrire toutes les informations de votre compte, y compris les dépôts privés et les organisations. +authorize_application_with_scopes=Avec des contextes : %s authorize_title=Autoriser "%s" à accéder à votre compte ? authorization_failed=L’autorisation a échoué authorization_failed_desc=L'autorisation a échoué car nous avons détecté une demande incorrecte. Veuillez contacter le responsable de l'application que vous avez essayé d'autoriser. @@ -707,6 +715,8 @@ public_profile=Profil public biography_placeholder=Parlez-nous un peu de vous ! (Vous pouvez utiliser Markdown) location_placeholder=Partagez votre position approximative avec d'autres personnes profile_desc=Contrôlez comment votre profil est affiché aux autres utilisateurs. Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et les opérations Git basées sur le Web. +password_username_disabled=Vous n’êtes pas autorisé à modifier leur nom d’utilisateur. Veuillez contacter l’administrateur de votre site pour plus de détails. +password_full_name_disabled=Vous n’êtes pas autorisé à modifier leur nom complet. Veuillez contacter l’administrateur du site pour plus de détails. full_name=Nom complet website=Site Web location=Localisation @@ -756,6 +766,7 @@ uploaded_avatar_not_a_image=Le fichier téléchargé n'est pas une image. uploaded_avatar_is_too_big=La taille du fichier téléversé (%d Kio) dépasse la taille maximale (%d Kio). update_avatar_success=Votre avatar a été mis à jour. update_user_avatar_success=L'avatar de l'utilisateur a été mis à jour. +cropper_prompt=Vous pouvez modifier l’image avant de l’enregistrer. L’image modifiée sera enregistrée en tant que PNG. change_password=Modifier le mot de passe old_password=Mot de passe actuel @@ -1022,6 +1033,8 @@ fork_to_different_account=Créer une bifurcation vers un autre compte fork_visibility_helper=La visibilité d'un dépôt bifurqué ne peut pas être modifiée. fork_branch=Branche à cloner sur la bifurcation all_branches=Toutes les branches +view_all_branches=Voir toutes les branches +view_all_tags=Voir toutes les étiquettes fork_no_valid_owners=Ce dépôt ne peut pas être bifurqué car il n’a pas de propriétaire valide. fork.blocked_user=Impossible de bifurquer le dépôt car vous êtes bloqué par son propriétaire. use_template=Utiliser ce modèle @@ -1033,6 +1046,7 @@ generate_repo=Générer un dépôt generate_from=Générer depuis repo_desc=Description repo_desc_helper=Décrire brièvement votre dépôt +repo_no_desc=Aucune description fournie repo_lang=Langue repo_gitignore_helper=Sélectionner quelques .gitignore prédéfinies repo_gitignore_helper_desc=De nombreux outils et compilateurs génèrent des fichiers résiduels qui n'ont pas besoin d'être supervisés par git. Composez un .gitignore à l’aide de cette liste des languages de programmation courants. @@ -1095,7 +1109,6 @@ delete_preexisting_success=Fichiers dépossédés supprimés dans %s. blame_prior=Voir le blame avant cette modification blame.ignore_revs=Les révisions dans .git-blame-ignore-revs sont ignorées. Vous pouvez quand même voir ces blâmes. blame.ignore_revs.failed=Impossible d'ignorer les révisions dans .git-blame-ignore-revs. -author_search_tooltip=Affiche un maximum de 30 utilisateurs tree_path_not_found_commit=Le chemin %[1]s n’existe pas dans la révision %[2]s. tree_path_not_found_branch=Le chemin %[1]s n’existe pas dans la branche %[2]s. @@ -1450,8 +1463,6 @@ issues.new.no_items=Pas d'élément issues.new.milestone=Jalon issues.new.no_milestone=Sans jalon issues.new.clear_milestone=Effacer le jalon -issues.new.open_milestone=Ouvrir un jalon -issues.new.closed_milestone=Jalons fermés issues.new.assignees=Assignés issues.new.clear_assignees=Supprimer les affectations issues.new.no_assignees=Sans assignation @@ -1516,7 +1527,6 @@ issues.filter_assignee=Assigné issues.filter_assginee_no_select=Tous les assignés issues.filter_assginee_no_assignee=Aucun assigné issues.filter_poster=Auteur -issues.filter_poster_no_select=Tous les auteurs issues.filter_type=Type issues.filter_type.all_issues=Tous les tickets issues.filter_type.assigned_to_you=Qui vous sont assignés @@ -1659,27 +1669,34 @@ issues.comment_on_locked=Vous ne pouvez pas commenter un ticket verrouillé. issues.delete=Supprimer issues.delete.title=Supprimer ce ticket ? issues.delete.text=Voulez-vous vraiment supprimer ce ticket ? (Cette opération supprimera définitivement tout le contenu. Envisagez plutôt de le fermer si vous avez l'intention de l'archiver) + issues.tracker=Minuteur -issues.start_tracking_short=Démarrer la minuteuse -issues.start_tracking=Démarrer le suivi du temps +issues.timetracker_timer_start=Démarrer le minuteur +issues.timetracker_timer_stop=Arrêter le minuteur +issues.timetracker_timer_discard=Annuler le minuteur +issues.timetracker_timer_manually_add=Pointer du temps + +issues.time_estimate_placeholder=1h 2m +issues.time_estimate_set=Définir le temps estimé +issues.time_estimate_display=Estimation : %s +issues.change_time_estimate_at=a changé le temps estimé à %s %s +issues.remove_time_estimate_at=a supprimé le temps estimé %s +issues.time_estimate_invalid=Le format du temps estimé est invalide issues.start_tracking_history=`a commencé son travail %s.` issues.tracker_auto_close=Le minuteur sera automatiquement arrêté quand le ticket sera fermé. issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur un autre ticket !` -issues.stop_tracking=Arrêter la minuteuse -issues.stop_tracking_history=`a fini de travailler %s.` -issues.cancel_tracking=Abandonner le minuteur +issues.stop_tracking_history=`a fini de travailler sur %s %s.` issues.cancel_tracking_history=`a abandonné son minuteur %s.` -issues.add_time=Ajouter du temps manuellement issues.del_time=Supprimer ce minuteur du journal -issues.add_time_short=Pointer du temps -issues.add_time_cancel=Annuler issues.add_time_history=`a pointé du temps de travail %s.` issues.del_time_history=`a supprimé son temps de travail %s.` +issues.add_time_manually=Temps pointé manuellement issues.add_time_hours=Heures issues.add_time_minutes=Minutes issues.add_time_sum_to_small=Aucun minuteur n'a été saisi. issues.time_spent_total=Temps passé total issues.time_spent_from_all_authors=`Temps passé total : %s` + issues.due_date=Échéance issues.invalid_due_date_format=Le format de la date d'échéance est invalide, il doit être comme suit 'aaaa-mm-jj'. issues.error_modifying_due_date=Impossible de modifier l'échéance. @@ -1926,6 +1943,10 @@ pulls.delete.title=Supprimer cette demande d'ajout ? pulls.delete.text=Voulez-vous vraiment supprimer cet demande d'ajout ? (Cela supprimera définitivement tout le contenu. Envisagez de le fermer à la place, si vous avez l'intention de le garder archivé) pulls.recently_pushed_new_branches=Vous avez soumis sur la branche %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Cette branche est en retard de %d révision sur %s +pulls.upstream_diverging_prompt_behind_n=Cette branche est en retard de %d révisions sur %s +pulls.upstream_diverging_prompt_base_newer=La branche de base %s a de nouveaux changements +pulls.upstream_diverging_merge=Synchroniser la bifurcation pull.deleted_branch=(supprimé) : %s pull.agit_documentation=Voir la documentation sur AGit @@ -2458,6 +2479,8 @@ settings.block_on_official_review_requests=Bloquer la fusion en cas de demande d settings.block_on_official_review_requests_desc=La fusion ne sera pas possible tant qu’elle aura des demandes d’évaluations officielles, même s'il y a suffisamment d’approbations. settings.block_outdated_branch=Bloquer la fusion si la demande d'ajout est obsolète settings.block_outdated_branch_desc=La fusion ne sera pas possible lorsque la branche principale est derrière la branche de base. +settings.block_admin_merge_override=Les administrateurs doivent respecter les règles de protection des branches +settings.block_admin_merge_override_desc=Les administrateurs doivent respecter les règles de protection des branches et ne peuvent pas les contourner. settings.default_branch_desc=Sélectionnez une branche par défaut pour les demandes de fusion et les révisions : settings.merge_style_desc=Styles de fusion settings.default_merge_style_desc=Méthode de fusion par défaut @@ -2577,7 +2600,6 @@ diff.generated=générée diff.vendored=externe diff.comment.add_line_comment=Commenter cette ligne diff.comment.placeholder=Laisser un commentaire -diff.comment.markdown_info=Formater avec Markdown. diff.comment.add_single_comment=Commenter (simple) diff.comment.add_review_comment=Commenter diff.comment.start_review=Débuter une évaluation @@ -3512,6 +3534,11 @@ alpine.repository=Informations sur le Dépôt alpine.repository.branches=Branches alpine.repository.repositories=Dépôts alpine.repository.architectures=Architectures +arch.registry=Ajouter un serveur avec un dépôt et une architecture liés dans /etc/pacman.conf : +arch.install=Synchroniser le paquet avec pacman : +arch.repository=Informations sur le Dépôt +arch.repository.repositories=Dépôts +arch.repository.architectures=Architectures cargo.registry=Configurez ce registre dans le fichier de configuration Cargo (par exemple ~/.cargo/config.toml) : cargo.install=Pour installer le paquet en utilisant Cargo, exécutez la commande suivante : chef.registry=Configurer ce registre dans votre fichier ~/.chef/config.rb: diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 0fae28daeae..f40e0037d2c 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -104,8 +104,9 @@ copy_url=Cóipeáil URL copy_hash=Cóipeáil hais copy_content=Cóipeáil ábhair copy_branch=Ainm brainse cóipeáil +copy_path=Cóipeáil cosán copy_success=Cóipeáil! -copy_error=Theip ar an gcóip +copy_error=Theip ar an gcóipeáil copy_type_unsupported=Ní féidir an cineál comhaid seo a chóipeáil write=Scríobh @@ -144,6 +145,7 @@ confirm_delete_selected=Deimhnigh chun gach earra roghnaithe a scriosadh? name=Ainm value=Luach +readme=Readme filter=Scagaire filter.clear=Scagaire Soiléir @@ -209,6 +211,10 @@ buttons.link.tooltip=Cuir nasc leis buttons.list.unordered.tooltip=Cuir liosta piléar leis buttons.list.ordered.tooltip=Cuir liosta uimhrithe buttons.list.task.tooltip=Cuir liosta tascanna leis +buttons.table.add.tooltip=Cuir tábla leis +buttons.table.add.insert=Cuir +buttons.table.rows=Sraitheanna +buttons.table.cols=Colúin buttons.mention.tooltip=Luaigh úsáideoir nó foireann buttons.ref.tooltip=Déan tagairt d'eisiúint nó iarratas tarraingthe buttons.switch_to_legacy.tooltip=Úsáid an eagarthóir oidhreachta ina ionad @@ -348,6 +354,7 @@ enable_update_checker=Cumasaigh Seiceoir Nuashonraithe enable_update_checker_helper=Seiceálacha ar eisiúintí leagan nua go tréimhsiúil trí nascadh le gitea.io. env_config_keys=Cumraíocht Comhshaoil env_config_keys_prompt=Cuirfear na hathróga comhshaoil seo a leanas i bhfeidhm ar do chomhad cumraíochta freisin: +config_write_file_prompt=Scríobhfar na roghanna cumraíochta seo isteach: %s [home] nav_menu=Roghchlár Nascleanúint @@ -453,6 +460,7 @@ authorize_application=Údaraigh an Feidhmchlár authorize_redirect_notice=Déanfar tú a atreorú chuig %s má údaraíonn tú an feidhmchlár seo. authorize_application_created_by=Chruthaigh %s an feidhmchlár seo. authorize_application_description=Má dheonaíonn tú an rochtain, beidh sé in ann rochtain a fháil agus scríobh chuig faisnéis uile do chuntais, lena n-áirítear repos príobháideacha agus eagraíochtaí. +authorize_application_with_scopes=Le scóip: %s authorize_title=Údaraigh "%s" chun rochtain a fháil ar do chuntas? authorization_failed=Theip ar údarú authorization_failed_desc=Theip ar an údarú toisc gur bhraitheamar iarratas neamhbhailí. Téigh i dteagmháil le cothabhálaí an aip a rinne tú iarracht a údarú. @@ -758,6 +766,7 @@ uploaded_avatar_not_a_image=Ní íomhá é an comhad uaslódáilte. uploaded_avatar_is_too_big=Sáraíonn méid an chomhaid uaslódáilte (%d KiB) an méid uasta (%d KiB). update_avatar_success=Tá do avatar nuashonraithe. update_user_avatar_success=Nuashonraíodh avatar an úsáideora. +cropper_prompt=Is féidir leat an íomhá a chur in eagar roimh shábháil. Sábhálfar an íomhá in eagar mar PNG. change_password=Nuashonrú Pasfhocal old_password=Pasfhocal Reatha @@ -1024,6 +1033,8 @@ fork_to_different_account=Forc chuig cuntas difriúil fork_visibility_helper=Ní féidir infheictheacht stór forcailte a athrú. fork_branch=Brainse le clónú chuig an bhforc all_branches=Gach brainse +view_all_branches=Féach ar gach brainse +view_all_tags=Féach ar gach clib fork_no_valid_owners=Ní féidir an stór seo a fhorcáil toisc nach bhfuil úinéirí bailí ann. fork.blocked_user=Ní féidir an stór a fhorcáil toisc go bhfuil úinéir an stórais bac ort. use_template=Úsáid an teimpléad seo @@ -1035,7 +1046,8 @@ generate_repo=Cruthaigh Stóras generate_from=Gin Ó repo_desc=Cur síos repo_desc_helper=Cuir isteach tuairisc ghearr (roghnach) -repo_lang=Teanga +repo_no_desc=Níor tugadh tuairisc +repo_lang=Teangacha repo_gitignore_helper=Roghnaigh teimpléid .gitignore. repo_gitignore_helper_desc=Roghnaigh na comhaid nach bhfuil le rianú ó liosta teimpléid do theangacha coitianta. Cuirtear déantáin tipiciúla a ghineann uirlisí tógála gach teanga san áireamh ar.gitignore de réir réamhshocraithe. issue_labels=Lipéid Eisiúna @@ -1097,7 +1109,6 @@ delete_preexisting_success=Scriosta comhaid neamhghlactha i %s blame_prior=Féach ar an milleán roimh an athrú seo blame.ignore_revs=Ag déanamh neamhairde de leasuithe i .git-blame-ignore-revs. Cliceáil anseo chun seachaint agus an gnáth-amharc milleán a fheiceáil. blame.ignore_revs.failed=Theip ar neamhaird a dhéanamh ar leasuithe i .git-blame-ignore-revs. -author_search_tooltip=Taispeánann 30 úsáideoir ar a mhéad tree_path_not_found_commit=Níl cosán %[1]s ann i dtiomantas %[2]s tree_path_not_found_branch=Níl cosán %[1]s ann i mbrainse %[2]s @@ -1452,12 +1463,10 @@ issues.new.no_items=Gan aon earraí issues.new.milestone=Cloch Mhíle issues.new.no_milestone=Gan Chloch Mhíle issues.new.clear_milestone=Cloch Mhíle soiléir -issues.new.open_milestone=Clocha Míle Oscailte -issues.new.closed_milestone=Clocha Míle Dúnta issues.new.assignees=Sannaitheoirí issues.new.clear_assignees=Ceannaitheoirí soiléir issues.new.no_assignees=Gan aon Sannaitheoirí -issues.new.no_reviewers=Gan athbhreithnithe +issues.new.no_reviewers=Gan Léirmheastóirí issues.new.blocked_user=Ní féidir saincheist a chruthú toisc go bhfuil úinéir an stórais bac ort. issues.edit.already_changed=Ní féidir athruithe a shábháil ar an tsaincheist. Dealraíonn sé gur athraigh úsáideoir eile an t-ábhar cheana féin. Athnuachan an leathanach agus déan iarracht eagarthóireacht arís chun a gcuid athruithe a sheachaint issues.edit.blocked_user=Ní féidir ábhar a chur in eagar toisc go bhfuil an póstaer nó úinéir an stórais bac ort. @@ -1518,7 +1527,6 @@ issues.filter_assignee=Sannaitheoir issues.filter_assginee_no_select=Gach sannaithe issues.filter_assginee_no_assignee=Gan sannaitheoir issues.filter_poster=Údar -issues.filter_poster_no_select=Gach údair issues.filter_type=Cineál issues.filter_type.all_issues=Gach saincheist issues.filter_type.assigned_to_you=Sannta duit @@ -1661,27 +1669,34 @@ issues.comment_on_locked=Ní féidir leat trácht a dhéanamh ar shaincheist fao issues.delete=Scrios issues.delete.title=Scrios an t-eagrán seo? issues.delete.text=An bhfuil tú cinnte gur mhaith leat an cheist seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann) + issues.tracker=Rianaitheoir Ama -issues.start_tracking_short=Tosaigh Uaineoir -issues.start_tracking=Rianú Am Tosaigh -issues.start_tracking_history=`thosaigh sé ag obair %s` +issues.timetracker_timer_start=Amadóir tosaithe +issues.timetracker_timer_stop=Stop an t-amadóir +issues.timetracker_timer_discard=Déan an t-amadóir a scriosadh +issues.timetracker_timer_manually_add=Cuir Am leis + +issues.time_estimate_placeholder=1u 2n +issues.time_estimate_set=Socraigh am measta +issues.time_estimate_display=Meastachán: %s +issues.change_time_estimate_at=d'athraigh an meastachán ama go %s %s +issues.remove_time_estimate_at=baineadh meastachán ama %s +issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailí +issues.start_tracking_history=thosaigh ag obair %s issues.tracker_auto_close=Stopfar ama go huathoibríoch nuair a dhúnfar an tsaincheist seo issues.tracking_already_started=`Tá tús curtha agat cheana féin ag rianú ama ar eagrán eile!` -issues.stop_tracking=Stop Uaineadóir -issues.stop_tracking_history=`stop sé ag obair %s` -issues.cancel_tracking=Caith amach +issues.stop_tracking_history=d'oibrigh do %s %s issues.cancel_tracking_history=`rianú ama curtha ar ceal %s` -issues.add_time=Láimh Cuir Am leis issues.del_time=Scrios an log ama seo -issues.add_time_short=Cuir Am leis -issues.add_time_cancel=Cealaigh -issues.add_time_history=`am caite curtha leis %s` +issues.add_time_history=cuireadh am caite %s %s leis issues.del_time_history=`an t-am caite scriosta %s` +issues.add_time_manually=Cuir Am leis de Láimh issues.add_time_hours=Uaireanta issues.add_time_minutes=Miontuairi issues.add_time_sum_to_small=Níor iontráilíodh aon am. issues.time_spent_total=An t-am iomlán a chaitear issues.time_spent_from_all_authors=`Am Iomlán Caitear: %s` + issues.due_date=Dáta dlite issues.invalid_due_date_format=Ní mór 'bbbb-mm-ll' a bheith i bhformáid an dáta dlite. issues.error_modifying_due_date=Theip ar an dáta dlite a mhodhnú. @@ -1928,6 +1943,10 @@ pulls.delete.title=Scrios an t-iarratas tarraingthe seo? pulls.delete.text=An bhfuil tú cinnte gur mhaith leat an t-iarratas tarraingthe seo a scriosadh? (Bainfidh sé seo an t-inneachar go léir go buan. Smaoinigh ar é a dhúnadh ina ionad sin, má tá sé i gceist agat é a choinneáil i gcartlann) pulls.recently_pushed_new_branches=Bhrúigh tú ar bhrainse %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Tá an brainse seo %d tiomantas taobh thiar de %s +pulls.upstream_diverging_prompt_behind_n=Tá an brainse seo %d geallta taobh thiar de %s +pulls.upstream_diverging_prompt_base_newer=Tá athruithe nua ar an mbunbhrainse %s +pulls.upstream_diverging_merge=Forc sionc pull.deleted_branch=(scriosta): %s pull.agit_documentation=Déan athbhreithniú ar dhoiciméid faoi AGit @@ -2460,6 +2479,8 @@ settings.block_on_official_review_requests=Cuir bac ar chumasc ar iarratais ar a settings.block_on_official_review_requests_desc=Ní bheidh sé indéanta cumasc nuair a bhíonn iarratais oifigiúla ar athbhreithniú aige, fiú má tá go leor ceadaithe ann. settings.block_outdated_branch=Cuir bac ar chumasc má tá an t-iarratas tarraingthe as dáta settings.block_outdated_branch_desc=Ní bheidh cumasc indéanta nuair a bhíonn ceannbhrainse taobh thiar de bhronnbhrainse. +settings.block_admin_merge_override=Ní mór do riarthóirí rialacha cosanta brainse a leanúint +settings.block_admin_merge_override_desc=Ní mór do riarthóirí rialacha cosanta brainse a leanúint agus ní féidir leo dul timpeall air. settings.default_branch_desc=Roghnaigh brainse stóras réamhshocraithe le haghaidh iarratas tarraingte agus geallann an cód: settings.merge_style_desc=Stíleanna Cumaisc settings.default_merge_style_desc=Stíl Cumaisc Réamhshocraithe @@ -2579,7 +2600,6 @@ diff.generated=a ghintear diff.vendored=curtha ar fáil diff.comment.add_line_comment=Cuir trácht líne leis diff.comment.placeholder=Fág trácht -diff.comment.markdown_info=Tacaítear le stíliú le marcáil. diff.comment.add_single_comment=Cuir trácht aonair leis diff.comment.add_review_comment=Cuir trácht leis diff.comment.start_review=Tosaigh athbhreithniú @@ -3514,6 +3534,11 @@ alpine.repository=Eolas Stórais alpine.repository.branches=Brainsí alpine.repository.repositories=Stórais alpine.repository.architectures=Ailtireachtaí +arch.registry=Cuir freastalaí leis an stór agus an ailtireacht ghaolmhar le /etc/pacman.conf: +arch.install=Sioncronaigh pacáiste le pacman: +arch.repository=Eolas Stórais +arch.repository.repositories=Stórais +arch.repository.architectures=Ailtireachtaí cargo.registry=Socraigh an clárlann seo sa chomhad cumraíochta lasta (mar shampla ~/.cargo/config.toml): cargo.install=Chun an pacáiste a shuiteáil ag baint úsáide as Cargo, reáchtáil an t-ordú seo a leanas: chef.registry=Socraigh an clárlann seo i do chomhad ~/.chef/config.rb: diff --git a/options/locale/locale_hu-HU.ini b/options/locale/locale_hu-HU.ini index 69f0e6eab77..88ccc9fac21 100644 --- a/options/locale/locale_hu-HU.ini +++ b/options/locale/locale_hu-HU.ini @@ -103,6 +103,7 @@ filter.private=Privát [heatmap] [editor] +buttons.table.add.insert=Hozzáadás [filter] @@ -577,7 +578,6 @@ fork_visibility_helper=A másolt tárolók láthatósága nem változtatható me use_template=Sablon használata generate_repo=Új repozitórium repo_desc=Leírás -repo_lang=Nyelv repo_gitignore_helper=Válasszon .gitignore sablont. issue_labels=Hibajegy címkék issue_labels_helper=Válasszon hibajegy címkét. @@ -747,12 +747,9 @@ issues.new.no_items=Nincsenek elemek issues.new.milestone=Mérföldkő issues.new.no_milestone=Nincs mérföldkő issues.new.clear_milestone=Mérföldkő eltávolítása -issues.new.open_milestone=Nyitott mérföldkövek -issues.new.closed_milestone=Lezárt mérföldkövek issues.new.assignees=Megbízottak issues.new.clear_assignees=Megbízottak eltávolítása issues.new.no_assignees=Nincsenek megbízottak -issues.new.no_reviewers=Nincs véleményező issues.no_ref=Nincsen ág/címke megadva issues.create=Hibajegy létrehozása issues.new_label=Új címke @@ -860,19 +857,15 @@ issues.lock.title=Beszélgetés lezárása ezen a hibajegyen. issues.unlock.title=Hibajegy újranyitása. issues.comment_on_locked=Egy zárolt hibajegyhez nem lehet hozzászólni. issues.delete=Törlés + issues.tracker=Időzítő -issues.start_tracking=Időmérés elkezdése -issues.start_tracking_history=`elkezdett dolgozni %s` -issues.stop_tracking_history=`abbahagyta a %s` -issues.add_time=Idő kézi hozzáadása -issues.add_time_short=Idő hozzáadása -issues.add_time_cancel=Megszakítva -issues.add_time_history=`hozzáadta %s` + issues.add_time_hours=Óra issues.add_time_minutes=Perc issues.add_time_sum_to_small=Nem volt idő megadva. issues.time_spent_total=Teljes ráfordított idő issues.time_spent_from_all_authors=`Teljes ráfordított idő: %s` + issues.due_date=Határidő issues.invalid_due_date_format=A határidőt 'éééé-hh-nn' formátumban kell megadni. issues.error_modifying_due_date=Határidő módosítása sikertelen. @@ -1114,7 +1107,6 @@ diff.view_file=Fájl megtekintése diff.file_byte_size=Méret diff.file_suppressed=A különbségek nem kerülnek megjelenítésre, mivel a fájl túl nagy diff.comment.placeholder=Hozzászólás létrehozása -diff.comment.markdown_info=Támogatja a markdown formázást. diff.comment.add_single_comment=Egyszerű hozzászólás hozzáadása diff.comment.reply=Válasz diff.review.comment=Hozzászólás @@ -1600,6 +1592,7 @@ error.not_signed_commit=Nem aláírt commit filter.type=Típus alpine.repository.branches=Ágak alpine.repository.repositories=Tárolók +arch.repository.repositories=Tárolók conan.details.repository=Tároló owner.settings.cleanuprules.enabled=Engedélyezett diff --git a/options/locale/locale_id-ID.ini b/options/locale/locale_id-ID.ini index d1b4166e162..237323a0fc4 100644 --- a/options/locale/locale_id-ID.ini +++ b/options/locale/locale_id-ID.ini @@ -76,28 +76,79 @@ loading=Memuat… +archived=Diarsipkan concept_code_repository=Repositori +show_full_screen=Tampilkan layar penuh +download_logs=Unduh Logs +confirm_delete_selected=Konfirmasi untuk menghapus semua item yang dipilih? name=Nama +value=Nilai +readme=Baca saya +filter=Saring +filter.clear=Hapus Filter +filter.is_archived=Diarsipkan +filter.not_archived=Tidak Diarsipkan filter.is_template=Contoh +filter.public=Publik filter.private=Pribadi +no_results_found=Hasil tidak ditemukan. [search] +search=Cari... +type_tooltip=Tipe pencarian +fuzzy_tooltip=Termasuk juga hasil yang mendekati kata pencarian +exact_tooltip=Hanya menampilkan hasil yang cocok dengan istilah pencarian +repo_kind=Cari repo... +user_kind=Telusuri pengguna... +org_kind=Cari organisasi... +team_kind=Cari tim... +code_kind=Cari kode... +code_search_unavailable=Pencarian kode saat ini tidak tersedia. Silahkan hubungi administrator. +branch_kind=Cari cabang... [aria] +navbar=Bar Navigasi +footer=Footer +footer.software=Tentang Software +footer.links=Tautan [heatmap] +number_of_contributions_in_the_last_12_months=%s Kontribusi pada 12 bulan terakhir +no_contributions=Belum ada kontribusi +less=Lebih sedikit +more=Lebih banyak [editor] +buttons.heading.tooltip=Tambahkan heading +buttons.bold.tooltip=Tambahkan teks Tebal +buttons.italic.tooltip=Tambahkan teks Miring +buttons.quote.tooltip=Kutip teks +buttons.code.tooltip=Tambah Kode +buttons.link.tooltip=Tambahkan tautan +buttons.list.unordered.tooltip=Tambah daftar titik +buttons.list.ordered.tooltip=Tambah daftar angka +buttons.list.task.tooltip=Tambahkan daftar tugas +buttons.table.add.insert=Tambah +buttons.mention.tooltip=Tandai pengguna atau tim +buttons.ref.tooltip=Merujuk pada isu atau permintaan tarik +buttons.switch_to_legacy.tooltip=Gunakan editor versi lama +buttons.enable_monospace_font=Aktifkan font monospace +buttons.disable_monospace_font=Non-Aktifkan font monospace [filter] +string.asc=A - Z +string.desc=Z - A [error] +occurred=Terjadi kesalahan +report_message=Jika Anda yakin ini adalah bug Gitea, silakan cari isu di GitHub atau buka isu baru jika diperlukan. +not_found=Target tidak dapat ditemukan. [startpage] app_desc=Sebuah layanan hosting Git sendiri yang tanpa kesulitan @@ -117,8 +168,10 @@ path=Jalur repo_path=Jalur akar repositori +email_title=Pengaturan email smtp_addr=Host SMTP smtp_port=Port SMTP +smtp_from=Kirim Email Sebagai register_confirm=Perlu Konfirmasi Email Saat Pendaftaran mail_notify=Aktifkan Notifikasi Email disable_gravatar=Menonaktifkan Gravatar @@ -139,6 +192,7 @@ my_orgs=Organisasi Saya my_mirrors=Duplikat Saya view_home=Lihat %s +show_archived=Diarsipkan show_private=Pribadi @@ -480,6 +534,7 @@ email_notifications.enable=Aktifkan Pemberitahuan Surel email_notifications.disable=Nonaktifkan Email Notifikasi email_notifications.submit=Pasang Pengaturan Email +visibility.public=Publik visibility.private=Pribadi [repo] @@ -500,7 +555,6 @@ fork_repo=Cabang Gudang penyimpanan fork_from=Cabang Dari generate_repo=Buat Repositori repo_desc=Deskripsi -repo_lang=Bahasa issue_labels=Label Masalah issue_labels_helper=Pilih serangkaian label masalah. license=Lisensi @@ -522,7 +576,9 @@ delete_preexisting_label=Hapus desc.private=Pribadi +desc.public=Publik desc.template=Contoh +desc.archived=Diarsipkan template.webhooks=Webhooks template.topics=Topik @@ -641,8 +697,6 @@ issues.new.clear_labels=Label yang jelas issues.new.milestone=Tolak ukur waktu issues.new.no_milestone=Tidak Ada Milestone issues.new.clear_milestone=Bersihkan milestone -issues.new.open_milestone=Buka Milestone -issues.new.closed_milestone=Tutup Milestone issues.no_ref=Tidak Ada Cabang/Tag Ditentukan issues.create=Buat Masalah issues.new_label=Label Baru @@ -718,12 +772,11 @@ issues.attachment.download=`Klik untuk mengunduh "%s"` issues.subscribe=Berlangganan issues.unsubscribe=Berhenti berlangganan issues.delete=Hapus -issues.start_tracking_history=`mulai bekerja %s` -issues.stop_tracking_history=`berhenti bekerja %s` -issues.add_time_cancel=Batalkan -issues.add_time_history=`tambah menghabiskan waktu %s` + + issues.add_time_hours=Jam issues.add_time_minutes=Menit + issues.due_date_form_edit=Edit issues.due_date_form_remove=Menghapus issues.dependency.cancel=Membatalkan @@ -950,6 +1003,7 @@ settings=Pengaturan settings.full_name=Nama Lengkap settings.website=Situs web settings.location=Lokasi +settings.visibility.public=Publik settings.visibility.private_shortname=Pribadi settings.update_settings=Perbarui Setelan @@ -1036,6 +1090,7 @@ users.created=Dibuat users.edit=Edit users.auth_source=Sumber Otentikasi users.local=Lokal +users.list_status_filter.menu_text=Saring users.list_status_filter.is_admin=Pengelola emails.activated=Diaktifkan @@ -1287,6 +1342,7 @@ error.unit_not_allowed=Anda tidak diizinkan untuk mengunjungi unit repositori in [packages] filter.type=Jenis alpine.repository.repositories=Repositori +arch.repository.repositories=Repositori conan.details.repository=Repositori owner.settings.cleanuprules.enabled=Aktif diff --git a/options/locale/locale_is-IS.ini b/options/locale/locale_is-IS.ini index 0bd4ba68946..0564d49b1c0 100644 --- a/options/locale/locale_is-IS.ini +++ b/options/locale/locale_is-IS.ini @@ -124,6 +124,7 @@ filter.public=Opinbert [heatmap] [editor] +buttons.table.add.insert=Bæta við [filter] @@ -566,7 +567,6 @@ download_zip=Sækja ZIP generate_repo=Mynda Hugbúnaðarsafn repo_desc=Lýsing repo_desc_helper=Sláðu inn stutta lýsingu (valfrjálst) -repo_lang=Tungumál repo_gitignore_helper=Velja .gitignore sniðmát. repo_gitignore_helper_desc=Veldu hvaða skrár á ekki að rekja af lista sniðmáta fyrir algeng tungumál. Dæmagert rusl sem myndast af byggingarverkfærum hvers tungumáls er sjálfgefið í .gitignore. issue_labels=Vandamálslýsingar @@ -811,17 +811,15 @@ issues.unlock_comment=aflæsti þessa umræðu %s issues.lock_confirm=Læsa issues.unlock_confirm=Aflæsa issues.delete=Eyða -issues.start_tracking_short=Ræsa Tímamælir -issues.add_time=Bæta Bið Tíma Handvirkt -issues.add_time_short=Bæta Bið Tíma -issues.add_time_cancel=Hætta við -issues.add_time_history=`bætti við eyddum tíma %s` + + issues.del_time_history=`fjarlægði eyddum tíma %s` issues.add_time_hours=Klukkutímar issues.add_time_minutes=Mínútur issues.add_time_sum_to_small=Enginn tími var sleginn inn. issues.time_spent_total=Heildartíma Eytt issues.time_spent_from_all_authors=`Heildartíma Eytt: %s` + issues.due_date=Eindagi issues.push_commit_1=bætti við %d framlag %s issues.push_commits_n=bætti við %d framlög %s @@ -1313,6 +1311,7 @@ dependency.id=Auðkenni dependency.version=Útgáfa alpine.repository.branches=Greinar alpine.repository.repositories=Hugbúnaðarsöfn +arch.repository.repositories=Hugbúnaðarsöfn conan.details.repository=Hugbúnaðarsafn container.details.platform=Vettvangur container.labels=Lýsingar diff --git a/options/locale/locale_it-IT.ini b/options/locale/locale_it-IT.ini index d82215622fd..567e6acdcef 100644 --- a/options/locale/locale_it-IT.ini +++ b/options/locale/locale_it-IT.ini @@ -130,6 +130,7 @@ filter.private=Privati [heatmap] [editor] +buttons.table.add.insert=Aggiungi [filter] @@ -777,7 +778,6 @@ generate_repo=Genera repository generate_from=Genera da repo_desc=Descrizione repo_desc_helper=Inserisci una breve descrizione (opzionale) -repo_lang=Lingua repo_gitignore_helper=Seleziona i template di .gitignore. repo_gitignore_helper_desc=Scegli di quali file non tenere traccia da un elenco di modelli per le lingue comuni. Gli artefatti tipici generati dagli strumenti di build di ogni lingua sono inclusi su .gitignore per impostazione predefinita. issue_labels=Etichette Issue @@ -1095,12 +1095,9 @@ issues.new.no_items=Nessun elemento issues.new.milestone=Traguardo issues.new.no_milestone=Nessuna milestone issues.new.clear_milestone=Milestone pulita -issues.new.open_milestone=Apri Milestone -issues.new.closed_milestone=Milestone chiuse issues.new.assignees=Assegnatari issues.new.clear_assignees=Cancella assegnatari issues.new.no_assignees=Nessuna assegnatario -issues.new.no_reviewers=Nessun revisore issues.choose.get_started=Inizia issues.choose.open_external_link=Apri issues.choose.blank=Default @@ -1147,7 +1144,6 @@ issues.filter_assignee=Assegnatario issues.filter_assginee_no_select=Tutte le assegnazioni issues.filter_assginee_no_assignee=Nessun assegnatario issues.filter_poster=Autore -issues.filter_poster_no_select=Tutti gli autori issues.filter_type=Tipo issues.filter_type.all_issues=Tutti i problemi issues.filter_type.assigned_to_you=Assegnati a te @@ -1260,26 +1256,19 @@ issues.comment_on_locked=Non puoi commentare un problema bloccato. issues.delete=Elimina issues.delete.title=Eliminare questo problema? issues.delete.text=Vuoi davvero eliminare questo problema? (Questo rimuoverà permanentemente tutti i contenuti. Considera invece di chiuderlo, se vuoi tenerlo archiviato) + issues.tracker=Cronografo -issues.start_tracking_short=Avvia timer -issues.start_tracking=Avvia cronografo -issues.start_tracking_history=ha iniziato a lavorare %s + issues.tracker_auto_close=Il timer verrà interrotto automaticamente una volta che il problema verrá chiuso issues.tracking_already_started=`Hai già avviato il monitoraggio del tempo su un altro problema!` -issues.stop_tracking=Ferma timer -issues.stop_tracking_history=`ha smesso di funzionare %s` -issues.cancel_tracking=Scarta -issues.add_time=Aggiungi Tempo manualmente issues.del_time=Elimina questo registro di tempo -issues.add_time_short=Aggiungi tempo -issues.add_time_cancel=Annulla -issues.add_time_history=`aggiunto tempo trascorso %s` issues.del_time_history=`tempo trascorso eliminato %s` issues.add_time_hours=Ore issues.add_time_minutes=Minuti issues.add_time_sum_to_small=Non è stato inserito alcun tempo. issues.time_spent_total=Tempo totale trascorso issues.time_spent_from_all_authors=`Totale tempo trascorso: %s` + issues.due_date=Data di scadenza issues.invalid_due_date_format=Il formato della data di scadenza deve essere 'yyyy-mm-dd'. issues.error_modifying_due_date=Impossibile modificare la data di scadenza. @@ -1965,7 +1954,6 @@ diff.load=Carica Diff diff.generated=generato diff.vendored=esterno diff.comment.placeholder=Lascia un commento -diff.comment.markdown_info=Lo stile con markdown è supportato. diff.comment.add_single_comment=Aggiungi un commento singolo diff.comment.add_review_comment=Aggiungi commento diff.comment.start_review=Inizio revisione @@ -2734,6 +2722,7 @@ dependency.version=Versione alpine.install=Per installare il pacchetto, eseguire il seguente comando: alpine.repository.branches=Branches alpine.repository.repositories=Repository +arch.repository.repositories=Repository chef.install=Per installare il pacchetto, eseguire il seguente comando: composer.registry=Imposta questo registro nel tuo file ~/.composer/config.json: composer.install=Per installare il pacchetto utilizzando Composer, eseguire il seguente comando: diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 973c1b3761c..1d8b33bef68 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -104,6 +104,7 @@ copy_url=URLをコピー copy_hash=ハッシュをコピー copy_content=内容をコピー copy_branch=ブランチ名をコピー +copy_path=パスをコピー copy_success=コピーされました! copy_error=コピーに失敗しました copy_type_unsupported=このファイルタイプはコピーできません @@ -209,6 +210,10 @@ 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=レガシーエディタを使用する @@ -348,6 +353,7 @@ enable_update_checker=アップデートチェッカーを有効にする enable_update_checker_helper=gitea.ioに接続して定期的に新しいバージョンのリリースを確認します。 env_config_keys=環境設定 env_config_keys_prompt=以下の環境変数も設定ファイルに適用されます: +config_write_file_prompt=これらの設定オプションは %s に書き込まれます [home] nav_menu=ナビゲーションメニュー @@ -453,6 +459,7 @@ authorize_application=アプリケーションを許可 authorize_redirect_notice=このアプリケーションを許可すると %s にリダイレクトします。 authorize_application_created_by=このアプリケーションは %s が作成しました。 authorize_application_description=アクセスを許可すると、このアプリケーションは、プライベート リポジトリや組織を含むあなたのすべてのアカウント情報に対して、アクセスと書き込みができるようになります。 +authorize_application_with_scopes=スコープ: %s authorize_title=`"%s"にあなたのアカウントへのアクセスを許可しますか?` authorization_failed=認可失敗 authorization_failed_desc=無効なリクエストを検出したため認可が失敗しました。 認可しようとしたアプリの開発者に連絡してください。 @@ -580,6 +587,8 @@ lang_select_error=言語をリストから選択してください。 username_been_taken=ユーザー名が既に使用されています。 username_change_not_local_user=非ローカルユーザーのユーザー名は変更できません。 +change_username_disabled=ユーザー名の変更は無効化されています。 +change_full_name_disabled=フルネームの変更は無効化されています。 username_has_not_been_changed=ユーザー名は変更されていません repo_name_been_taken=リポジトリ名が既に使用されています。 repository_force_private=強制プライベートが有効です。プライベートリポジトリはパブリックにできません。 @@ -705,6 +714,8 @@ public_profile=公開プロフィール biography_placeholder=自己紹介してください!(Markdownを使うことができます) location_placeholder=おおよその場所を他の人と共有 profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。 +password_username_disabled=ユーザー名の変更は許可されていません。詳細はサイト管理者にお問い合わせください。 +password_full_name_disabled=フルネームの変更は許可されていません。詳細はサイト管理者にお問い合わせください。 full_name=フルネーム website=Webサイト location=場所 @@ -754,6 +765,7 @@ uploaded_avatar_not_a_image=アップロードしたファイルは画像ファ uploaded_avatar_is_too_big=アップロードされたファイルサイズ(%d KiB) が最大サイズ(%d KiB) を超えています。 update_avatar_success=アバターを更新しました。 update_user_avatar_success=ユーザーのアバターを更新しました。 +cropper_prompt=保存する前に画像を編集できます。 編集した画像はPNGで保存されます。 change_password=パスワードを更新 old_password=現在のパスワード @@ -1031,7 +1043,6 @@ generate_repo=リポジトリの生成 generate_from=他からの生成 repo_desc=説明 repo_desc_helper=簡単な説明を入力してください (オプション) -repo_lang=言語 repo_gitignore_helper=.gitignoreテンプレートを選択してください。 repo_gitignore_helper_desc=一般的な言語のテンプレートリストから、追跡しないファイルの設定を選択します。 各言語のビルドツールが生成する典型的なファイルが、デフォルトで.gitignoreに含まれます。 issue_labels=イシューラベル @@ -1093,7 +1104,6 @@ delete_preexisting_success=%s の未登録ファイルを削除しました blame_prior=この変更より前のBlameを表示 blame.ignore_revs=.git-blame-ignore-revs で指定されたリビジョンは除外しています。 これを迂回して通常のBlame表示を見るには ここをクリック。 blame.ignore_revs.failed=.git-blame-ignore-revs によるリビジョンの無視は失敗しました。 -author_search_tooltip=最大30人までのユーザーを表示 tree_path_not_found_commit=パス %[1]s はコミット %[2]s に存在しません tree_path_not_found_branch=パス %[1]s はブランチ %[2]s に存在しません @@ -1448,8 +1458,6 @@ issues.new.no_items=項目なし issues.new.milestone=マイルストーン issues.new.no_milestone=マイルストーンなし issues.new.clear_milestone=マイルストーンをクリア -issues.new.open_milestone=オープン中のマイルストーン -issues.new.closed_milestone=クローズされたマイルストーン issues.new.assignees=担当者 issues.new.clear_assignees=担当者をクリア issues.new.no_assignees=担当者なし @@ -1514,7 +1522,6 @@ issues.filter_assignee=担当者 issues.filter_assginee_no_select=すべての担当者 issues.filter_assginee_no_assignee=担当者なし issues.filter_poster=作成者 -issues.filter_poster_no_select=すべての作成者 issues.filter_type=タイプ issues.filter_type.all_issues=すべてのイシュー issues.filter_type.assigned_to_you=自分が担当 @@ -1657,27 +1664,20 @@ issues.comment_on_locked=ロックされているイシューにコメントは issues.delete=削除 issues.delete.title=このイシューを削除しますか? issues.delete.text=本当にこのイシューを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください) + issues.tracker=タイムトラッカー -issues.start_tracking_short=タイマー 開始 -issues.start_tracking=タイムトラッキングを開始 -issues.start_tracking_history=`が作業を開始 %s` + issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します issues.tracking_already_started=`別のイシューで既にタイムトラッキングを開始しています!` -issues.stop_tracking=タイマー 終了 -issues.stop_tracking_history=`が作業を終了 %s` -issues.cancel_tracking=中止 issues.cancel_tracking_history=`がタイムトラッキングを中止 %s` -issues.add_time=手で時間を入力 issues.del_time=このタイムログを削除 -issues.add_time_short=時間入力 -issues.add_time_cancel=キャンセル -issues.add_time_history=`が作業時間を追加 %s` issues.del_time_history=`が作業時間を削除 %s` 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=期日は 'yyyy-mm-dd' の形式で入力してください。 issues.error_modifying_due_date=期日を変更できませんでした。 @@ -2456,6 +2456,8 @@ settings.block_on_official_review_requests=公式レビュー依頼でマージ settings.block_on_official_review_requests_desc=公式レビュー依頼があるときは、承認数を満たしていてもマージできないようにします。 settings.block_outdated_branch=遅れているプルリクエストのマージをブロック settings.block_outdated_branch_desc=baseブランチがheadブランチより進んでいる場合、マージできないようにします。 +settings.block_admin_merge_override=管理者もブランチ保護のルールに従う +settings.block_admin_merge_override_desc=管理者はブランチ保護のルールに従う必要があり、回避することはできません。 settings.default_branch_desc=プルリクエストやコミット表示のデフォルトのブランチを選択: settings.merge_style_desc=マージ スタイル settings.default_merge_style_desc=デフォルトのマージスタイル @@ -2575,7 +2577,6 @@ diff.generated=generated diff.vendored=vendored diff.comment.add_line_comment=行コメントを追加 diff.comment.placeholder=コメントを残す -diff.comment.markdown_info=Markdownによる書式設定をサポートしています。 diff.comment.add_single_comment=単独のコメントを追加 diff.comment.add_review_comment=コメントを追加 diff.comment.start_review=レビュー開始 @@ -3510,6 +3511,9 @@ alpine.repository=リポジトリ情報 alpine.repository.branches=Branches alpine.repository.repositories=Repositories alpine.repository.architectures=Architectures +arch.repository=リポジトリ情報 +arch.repository.repositories=リポジトリ +arch.repository.architectures=Architectures cargo.registry=Cargo 設定ファイルでこのレジストリをセットアップします。(例 ~/.cargo/config.toml): cargo.install=Cargo を使用してパッケージをインストールするには、次のコマンドを実行します: chef.registry=あなたの ~/.chef/config.rb ファイルに、このレジストリをセットアップします: diff --git a/options/locale/locale_ko-KR.ini b/options/locale/locale_ko-KR.ini index 91c5b24ab56..48220d5c998 100644 --- a/options/locale/locale_ko-KR.ini +++ b/options/locale/locale_ko-KR.ini @@ -95,6 +95,7 @@ filter.private=비공개 [heatmap] [editor] +buttons.table.add.insert=추가 [filter] @@ -544,7 +545,6 @@ fork_from=원본 프로젝트 : fork_visibility_helper=포크된 저장소의 가시성은 변경하실 수 없습니다. use_template=이 템플릿을 사용 repo_desc=설명 -repo_lang=언어 repo_gitignore_helper=.gitignore 템플릿 선택 license=라이센스 license_helper=라이센스 파일을 선택해주세요. @@ -680,8 +680,6 @@ issues.new.clear_labels=레이블 초기화 issues.new.milestone=마일스톤 issues.new.no_milestone=마일스톤 없음 issues.new.clear_milestone=마일스톤 초기화 -issues.new.open_milestone=마일스톤 생성 -issues.new.closed_milestone=마일스톤 닫기 issues.new.assignees=담당자 issues.new.clear_assignees=담당자 초기화 issues.new.no_assignees=담당자 없음 @@ -773,19 +771,15 @@ issues.attachment.download=' "%s"를 다운로드 하려면 클릭 하십시오 issues.subscribe=구독하기 issues.unsubscribe=구독 취소 issues.delete=삭제 + issues.tracker=타임 트래커 -issues.start_tracking=타임 트래킹 시작 -issues.start_tracking_history=`%s가 작업 시작` -issues.stop_tracking_history=`작업 중단 %s` -issues.add_time=수동으로 시간 입력 -issues.add_time_short=시간 입력 -issues.add_time_cancel=취소 -issues.add_time_history=`사용 시간이 추가됨 %s` + 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="마감일은 반드시 'yyyy-mm-dd' 형식이어야 합니다." issues.error_modifying_due_date="마감일 수정을 실패하였습니다." @@ -1546,6 +1540,7 @@ error.unit_not_allowed=이 저장소 섹션에 접근할 수 없습니다. [packages] filter.type=유형 alpine.repository.repositories=저장소 +arch.repository.repositories=저장소 conan.details.repository=저장소 owner.settings.cleanuprules.enabled=활성화됨 diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index ee6f3911b0d..fd412b95b41 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -175,6 +175,7 @@ buttons.link.tooltip=Pievienot saiti buttons.list.unordered.tooltip=Pievienot sarakstu buttons.list.ordered.tooltip=Pievienot numurētu sarakstu buttons.list.task.tooltip=Pievienot uzdevumu sarakstu +buttons.table.add.insert=Pievienot buttons.mention.tooltip=Pieminēt lietotāju vai komandu buttons.ref.tooltip=Atsaukties uz problēmu vai izmaiņu pieprasījumu buttons.switch_to_legacy.tooltip=Izmantot vēsturisko redaktoru @@ -939,7 +940,6 @@ generate_repo=Ģenerēt repozitoriju generate_from=Ģenerēt no repo_desc=Apraksts repo_desc_helper=Ievadiet īsu aprakstu (neobligāts) -repo_lang=Valoda repo_gitignore_helper=Izvēlieties .gitignore sagatavi. repo_gitignore_helper_desc=Izvēlieties kādi faili netiks glabāti repozitorijā no sagatavēm biežāk lietotājām valodām. Pēc noklusējuma .gitignore iekļauj valodu kompilācijas rīku artifaktus. issue_labels=Problēmu iezīmes @@ -996,7 +996,6 @@ delete_preexisting_success=Dzēst nepārņemtos failus direktorijā %s blame_prior=Aplūkot vainīgo par izmaiņām pirms šīs revīzijas blame.ignore_revs=Neņem vērā izmaiņas no .git-blame-ignore-revs. Nospiediet šeit, lai to apietu un redzētu visu izmaiņu skatu. blame.ignore_revs.failed=Neizdevās neņemt vērā izmaiņas no .git-blam-ignore-revs. -author_search_tooltip=Tiks attēloti ne vairāk kā 30 lietotāji tree_path_not_found_commit=Revīzijā %[2]s neeksistē ceļš %[1]s tree_path_not_found_branch=Atzarā %[2]s nepastāv ceļš %[1]s @@ -1330,12 +1329,9 @@ issues.new.no_items=Nav vienumu issues.new.milestone=Atskaites punkts issues.new.no_milestone=Nav atskaites punktu issues.new.clear_milestone=Notīrīt atskaites punktus -issues.new.open_milestone=Atvērtie atskaites punktus -issues.new.closed_milestone=Aizvērtie atskaites punkti issues.new.assignees=Atbildīgie issues.new.clear_assignees=Noņemt atbildīgo issues.new.no_assignees=Nav atbildīgo -issues.new.no_reviewers=Nav recenzentu issues.choose.get_started=Sākt darbu issues.choose.open_external_link=Atvērt issues.choose.blank=Noklusējuma @@ -1392,7 +1388,6 @@ issues.filter_assignee=Atbildīgais issues.filter_assginee_no_select=Visi atbildīgie issues.filter_assginee_no_assignee=Nav atbildīgā issues.filter_poster=Autors -issues.filter_poster_no_select=Visi autori issues.filter_type=Veids issues.filter_type.all_issues=Visas problēmas issues.filter_type.assigned_to_you=Piešķirtās Jums @@ -1532,27 +1527,20 @@ issues.comment_on_locked=Jūs nevarat komentēt slēgtai problēmai. issues.delete=Dzēst issues.delete.title=Dzēst šo problēmu? issues.delete.text=Vai patiešām vēlaties dzēst šo problemu? (Neatgriezeniski tiks izdzēsts viss saturs. Apsveriet iespēju to aizvērt, ja vēlaties informāciju saglabāt vēsturei) + issues.tracker=Laika uzskaite -issues.start_tracking_short=Uzsākt taimeri -issues.start_tracking=Uzsākt laika uzskaiti -issues.start_tracking_history=` uzsāka darbu %s` + issues.tracker_auto_close=Taimeris tiks automātiski apturēts, kad šī problēma tiks aizvērta issues.tracking_already_started=`Jau ir uzsākta laika uzskaite par citu problēmu!` -issues.stop_tracking=Apturēt taimeri -issues.stop_tracking_history=` beidza strādāt %s` -issues.cancel_tracking=Atmest issues.cancel_tracking_history=`atcēla laika uzskaiti %s` -issues.add_time=Manuāli pievienot laiku issues.del_time=Dzēst šo laika žurnāla ierakstu -issues.add_time_short=Pievienot laiku -issues.add_time_cancel=Atcelt -issues.add_time_history=` pievienoja patērēto laiku %s` issues.del_time_history=`dzēsts patērētais laiks %s` issues.add_time_hours=Stundas issues.add_time_minutes=Minūtes issues.add_time_sum_to_small=Nav norādīts laiks. issues.time_spent_total=Kopējais patērētais laiks issues.time_spent_from_all_authors=`Kopējais patērētais laiks: %s` + issues.due_date=Izpildes termiņš issues.invalid_due_date_format=Izpildes termiņam ir jābūt formāta 'yyyy-mm-dd'. issues.error_modifying_due_date=Neizdevās izmainīt izpildes termiņu. @@ -2358,7 +2346,6 @@ diff.generated=ģenerēts diff.vendored=ārējs diff.comment.add_line_comment=Pievienot rindas komentāru diff.comment.placeholder=Ievadiet komentāru -diff.comment.markdown_info=Tiek atbalstīta formatēšana ar Markdown. diff.comment.add_single_comment=Pievienot vienu komentāru diff.comment.add_review_comment=Pievienot komentāru diff.comment.start_review=Sākt recenziju @@ -2702,7 +2689,6 @@ dashboard.gc_times=GC reizes dashboard.update_checker=Atjauninājumu pārbaudītājs dashboard.delete_old_system_notices=Dzēst vecos sistēmas paziņojumus no datubāzes dashboard.gc_lfs=Veikt atkritumu uzkopšanas darbus LFS meta objektiem -dashboard.sync_branch.started=Sākta atzaru sinhronizācija dashboard.rebuild_issue_indexer=Pārbūvēt problēmu indeksu users.user_manage_panel=Lietotāju kontu pārvaldība @@ -3236,6 +3222,9 @@ alpine.install=Lai uzstādītu pakotni, ir jāizpilda šī komanda: alpine.repository=Repozitorija informācija alpine.repository.repositories=Repozitoriji alpine.repository.architectures=Arhitektūras +arch.repository=Repozitorija informācija +arch.repository.repositories=Repozitoriji +arch.repository.architectures=Arhitektūras cargo.registry=Uzstādiet šo reģistru Cargo konfigurācijas failā, piemēram, ~/.cargo/config.toml: cargo.install=Lai instalētu Cargo pakotni, izpildiet sekojošu komandu: chef.registry=Uzstādiet šo reģistru failā ~/.chef/config.rb: diff --git a/options/locale/locale_nl-NL.ini b/options/locale/locale_nl-NL.ini index 1540cf095b0..a4da8177bce 100644 --- a/options/locale/locale_nl-NL.ini +++ b/options/locale/locale_nl-NL.ini @@ -129,6 +129,7 @@ filter.private=Prive [heatmap] [editor] +buttons.table.add.insert=Toevoegen [filter] @@ -775,7 +776,6 @@ generate_repo=Repository genereren generate_from=Genereer van repo_desc=Omschrijving repo_desc_helper=Voer korte beschrijving in (optioneel) -repo_lang=Taal repo_gitignore_helper=Selecteer .gitignore templates. repo_gitignore_helper_desc=Kies welke bestanden niet bij te houden vanuit een lijst met sjablonen voor alledaagse talen. Gebruikelijke artefacten gegenereerd door de build tools van elke taal zijn standaard inbegrepen met .gitignore. issue_labels=Issuelabels @@ -1093,12 +1093,9 @@ issues.new.no_items=Geen items issues.new.milestone=Mijlpaal issues.new.no_milestone=Geen mijlpaal issues.new.clear_milestone=Verwijder mijlpaal -issues.new.open_milestone=Open mijlpalen -issues.new.closed_milestone=Gesloten mijlpalen issues.new.assignees=Toegewezen aan issues.new.clear_assignees=Verwijder toegewezen aan issues.new.no_assignees=Niet toegewezen -issues.new.no_reviewers=Geen beoordelaars issues.choose.get_started=Ga aan de slag issues.choose.open_external_link=Open issues.choose.blank=Standaard @@ -1145,7 +1142,6 @@ issues.filter_assignee=Aangewezene issues.filter_assginee_no_select=Alle toegewezen personen issues.filter_assginee_no_assignee=Geen verantwoordelijke issues.filter_poster=Auteur -issues.filter_poster_no_select=Alle auteurs issues.filter_type=Type issues.filter_type.all_issues=Alle kwesties issues.filter_type.assigned_to_you=Aan jou toegewezen @@ -1257,26 +1253,19 @@ issues.comment_on_locked=Je kunt geen commentaar geven op een vergrendeld proble issues.delete=Verwijderen issues.delete.title=Deze issue verwijderen? issues.delete.text=Wilt u deze issue echt verwijderen? (Dit is permanent en verwijdert alle inhoud. Overweeg om deze issue te sluiten, als u liever deze als archief wilt bijhouden) + issues.tracker=Tijdregistratie -issues.start_tracking_short=Start timer -issues.start_tracking=Start tijdregistratie -issues.start_tracking_history=`%s is begonnen` + issues.tracker_auto_close=Timer wordt automatisch gestopt wanneer dit probleem wordt gesloten issues.tracking_already_started=`Je houd al tijd bij voor een ander issue!` -issues.stop_tracking=Stop timer -issues.stop_tracking_history=`gestopt met werken aan %s` -issues.cancel_tracking=Weggooien -issues.add_time=Tijd handmatig toevoegen issues.del_time=Verwijder deze tijdlog -issues.add_time_short=Timer toevoegen -issues.add_time_cancel=Annuleren -issues.add_time_history=`heeft besteedde tijd toegevoegd: %s` issues.del_time_history=`heeft besteedde tijd verwijderd: %s` issues.add_time_hours=Uren issues.add_time_minutes=Minuten issues.add_time_sum_to_small=Geen tijd opgegeven. issues.time_spent_total=Totaal besteedde tijd issues.time_spent_from_all_authors=`Totaal besteedde tijd: %s` + issues.due_date=Vervaldatum issues.invalid_due_date_format=Het formaat van de deadline is moet 'jjjj-mm-dd' zijn. issues.error_modifying_due_date=Deadline aanpassen mislukt. @@ -1900,7 +1889,6 @@ diff.load=Laad Diff diff.generated=gegenereerd diff.vendored=vendored diff.comment.placeholder=Opmerking toevoegen -diff.comment.markdown_info=Styling met markdown wordt ondersteund. diff.comment.add_single_comment=Één reactie toevoegen diff.comment.add_review_comment=Voeg commentaar toe diff.comment.start_review=Review starten @@ -2523,6 +2511,7 @@ error.unit_not_allowed=U heeft geen toegang tot deze sectie van de repository. filter.type=Type assets=Assets alpine.repository.repositories=Repositories +arch.repository.repositories=Repositories conan.details.repository=Opslagplaats rubygems.required.ruby=Vereist Ruby versie rubygems.required.rubygems=Vereist RubyGem versie diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 22bdf1bbacb..22f701219d3 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -126,6 +126,7 @@ filter.private=Prywatne [heatmap] [editor] +buttons.table.add.insert=Dodaj [filter] @@ -732,7 +733,6 @@ generate_repo=Generuj repozytorium generate_from=Generuj z repo_desc=Opis repo_desc_helper=Wprowadź krótki opis (opcjonalnie) -repo_lang=Język repo_gitignore_helper=Wybierz szablony pliku .gitignore. issue_labels=Etykiety zgłoszenia issue_labels_helper=Wybierz zestaw etykiet zgłoszeń. @@ -1012,12 +1012,9 @@ issues.new.no_items=Brak elementów issues.new.milestone=Kamień milowy issues.new.no_milestone=Brak kamienia milowego issues.new.clear_milestone=Wyczyść kamień milowy -issues.new.open_milestone=Otwarte kamienie milowe -issues.new.closed_milestone=Zamknięte kamienie milowe issues.new.assignees=Przypisani issues.new.clear_assignees=Usuń przypisanych issues.new.no_assignees=Brak przypisanych -issues.new.no_reviewers=Brak recenzentów issues.choose.get_started=Rozpocznij issues.choose.open_external_link=Otwórz issues.choose.blank=Domyślny @@ -1158,23 +1155,18 @@ issues.lock.title=Zablokuj konwersację w tym zgłoszeniu. issues.unlock.title=Odblokuj konwersację w tym zgłoszeniu. issues.comment_on_locked=Nie możesz umieszczać komentarzy pod zablokowanym zgłoszeniem. issues.delete=Usuń + issues.tracker=Śledzenie czasu -issues.start_tracking=Rozpocznij śledzenie czasu -issues.start_tracking_history=`rozpoczął(-ęła) pracę nad %s` + issues.tracker_auto_close=Licznik czasu zostanie automatycznie zatrzymany w momencie zamknięcia tego zgłoszenia -issues.stop_tracking_history=`zakończył(-a) pracę nad %s` -issues.cancel_tracking=Odrzuć -issues.add_time=Dodaj czas ręcznie issues.del_time=Usuń ten dziennik czasu -issues.add_time_short=Dodaj czas -issues.add_time_cancel=Anuluj -issues.add_time_history=`dodał(-a) spędzony czas %s` issues.del_time_history=usunął(-ęła) spędzony czas %s issues.add_time_hours=Godziny issues.add_time_minutes=Minuty issues.add_time_sum_to_small=Czas nie został wprowadzony. issues.time_spent_total=Całkowity spędzony czas issues.time_spent_from_all_authors=`Całkowity spędzony czas: %s` + issues.due_date=Termin realizacji issues.invalid_due_date_format=Format terminu realizacji musi mieć wartość 'rrrr-mm-dd'. issues.error_modifying_due_date=Nie udało się zmodyfikować terminu realizacji. @@ -1759,7 +1751,6 @@ diff.file_byte_size=Rozmiar diff.file_suppressed=Plik diff jest za duży diff.generated=wygenerowano diff.comment.placeholder=Zostaw komentarz -diff.comment.markdown_info=Formatowanie przy użyciu Markdown jest wspierane. diff.comment.add_single_comment=Dodaj jeden komentarz diff.comment.add_review_comment=Dodaj komentarz diff.comment.start_review=Rozpocznij recenzję @@ -2416,6 +2407,7 @@ error.unit_not_allowed=Nie masz uprawnień do tej sekcji repozytorium. [packages] filter.type=Typ alpine.repository.repositories=Repozytoria +arch.repository.repositories=Repozytoria conan.details.repository=Repozytorium owner.settings.cleanuprules.enabled=Włączone diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index bb185d3de6d..8fb869898b5 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -172,6 +172,7 @@ buttons.link.tooltip=Adicionar um link buttons.list.unordered.tooltip=Adicionar uma lista com marcadores buttons.list.ordered.tooltip=Adicionar uma lista numerada buttons.list.task.tooltip=Adicionar uma lista de tarefas +buttons.table.add.insert=Adicionar buttons.mention.tooltip=Mencionar um usuário ou equipe buttons.ref.tooltip=Referenciar um issue ou um pull request buttons.switch_to_legacy.tooltip=Em vez disso, usar o editor legado @@ -934,7 +935,6 @@ generate_repo=Gerar repositório generate_from=Gerar de repo_desc=Descrição repo_desc_helper=Digite uma breve descrição (opcional) -repo_lang=Linguagem repo_gitignore_helper=Selecione modelos do .gitignore. repo_gitignore_helper_desc=Escolha os arquivos que não serão rastreados da lista de modelos para linguagens comuns. Artefatos típicos gerados pelos compiladores de cada linguagem estão incluídos no .gitignore por padrão. issue_labels=Etiquetas de issue @@ -990,7 +990,6 @@ delete_preexisting=Excluir arquivos pré-existentes delete_preexisting_content=Excluir arquivos em %s delete_preexisting_success=Arquivos órfãos excluídos em %s blame_prior=Ver a responsabilização anterior a esta modificação -author_search_tooltip=Mostra um máximo de 30 usuários transfer.accept=Aceitar transferência @@ -1322,12 +1321,9 @@ issues.new.no_items=Nenhum item issues.new.milestone=Marco issues.new.no_milestone=Sem marco issues.new.clear_milestone=Limpar marco -issues.new.open_milestone=Marcos abertos -issues.new.closed_milestone=Marcos fechados issues.new.assignees=Responsáveis issues.new.clear_assignees=Limpar responsáveis issues.new.no_assignees=Sem responsável -issues.new.no_reviewers=Sem revisor issues.choose.get_started=Primeiros passos issues.choose.open_external_link=Abrir issues.choose.blank=Padrão @@ -1384,7 +1380,6 @@ issues.filter_assignee=Atribuído issues.filter_assginee_no_select=Todos os responsáveis issues.filter_assginee_no_assignee=Sem responsável issues.filter_poster=Autor -issues.filter_poster_no_select=Todos os autores issues.filter_type=Tipo issues.filter_type.all_issues=Todas as issues issues.filter_type.assigned_to_you=Atribuídos a você @@ -1522,27 +1517,20 @@ issues.comment_on_locked=Você não pode comentar em uma issue bloqueada. issues.delete=Apagar issues.delete.title=Apagar esta issue? issues.delete.text=Você realmente deseja excluir esta issue? (Isto irá remover permanentemente todo o conteúdo. Considere fechá-la em vez disso, se você pretende mantê-la arquivado) + issues.tracker=Contador de tempo -issues.start_tracking_short=Iniciar Cronômetro -issues.start_tracking=Iniciar Cronômetro -issues.start_tracking_history=`começou a trabalhar %s` + issues.tracker_auto_close=Contador de tempo será parado automaticamente quando esta issue for fechada issues.tracking_already_started=`Você já iniciou o cronômetro em outra issue!` -issues.stop_tracking=Parar Cronômetro -issues.stop_tracking_history=`parou de trabalhar %s` -issues.cancel_tracking=Descartar issues.cancel_tracking_history=`cronômetro cancelado %s` -issues.add_time=Adicionar tempo manualmente issues.del_time=Apagar este registro de tempo -issues.add_time_short=Adicionar tempo -issues.add_time_cancel=Cancelar -issues.add_time_history=`adicionou tempo gasto %s` issues.del_time_history=`removeu tempo gasto %s` issues.add_time_hours=Horas issues.add_time_minutes=Minutos issues.add_time_sum_to_small=Nenhum tempo foi inserido. issues.time_spent_total=Tempo total gasto issues.time_spent_from_all_authors=`Tempo total gasto: %s` + issues.due_date=Data limite issues.invalid_due_date_format=Formato da data limite inválido, deve ser 'dd/mm/aaaa'. issues.error_modifying_due_date=Falha ao modificar a data limite. @@ -2319,7 +2307,6 @@ diff.load=Carregar Diff diff.generated=gerado diff.vendored=externo diff.comment.placeholder=Deixe um comentário -diff.comment.markdown_info=Estilo com markdown é suportado. diff.comment.add_single_comment=Adicionar um único comentário diff.comment.add_review_comment=Adicionar comentário diff.comment.start_review=Iniciar revisão @@ -3173,6 +3160,9 @@ alpine.install=Para instalar o pacote, execute o seguinte comando: alpine.repository=Informações do repositório alpine.repository.repositories=Repositórios alpine.repository.architectures=Arquiteturas +arch.repository=Informações do repositório +arch.repository.repositories=Repositórios +arch.repository.architectures=Arquiteturas cargo.registry=Configurar este registro no arquivo de configuração de Cargo (por exemplo ~/.cargo/config.toml): cargo.install=Para instalar o pacote usando Cargo, execute o seguinte comando: chef.registry=Configure este registro em seu arquivo ~/.chef/config.rb: diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 41531f7b3de..776d2bdc2bf 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -47,7 +47,7 @@ webauthn_error_unknown=Ocorreu um erro desconhecido. Tente novamente, por favor. webauthn_error_insecure=`WebAuthn apenas suporta conexões seguras. Para testar sobre HTTP, pode usar a origem "localhost" ou "127.0.0.1"` webauthn_error_unable_to_process=O servidor não conseguiu processar o seu pedido. webauthn_error_duplicated=A chave de segurança não é permitida neste pedido. Certifique-se de que a chave não está já registada. -webauthn_error_empty=Você tem que definir um nome para esta chave. +webauthn_error_empty=Tem de definir um nome para esta chave. webauthn_error_timeout=O tempo limite foi atingido antes que a sua chave pudesse ser lida. Recarregue esta página e tente novamente. webauthn_reload=Recarregar @@ -104,6 +104,7 @@ copy_url=Copiar URL copy_hash=Copiar hash copy_content=Copiar conteúdo copy_branch=Copiar nome do ramo +copy_path=Copiar caminho copy_success=Copiado! copy_error=Falha ao copiar copy_type_unsupported=Este tipo de ficheiro não pode ser copiado @@ -144,6 +145,7 @@ confirm_delete_selected=Confirma a exclusão de todos os itens marcados? name=Nome value=Valor +readme=Leia-me filter=Filtro filter.clear=Retirar filtro @@ -209,6 +211,10 @@ buttons.link.tooltip=Adicionar uma ligação buttons.list.unordered.tooltip=Adicionar uma lista de pontos buttons.list.ordered.tooltip=Adicionar uma lista numerada buttons.list.task.tooltip=Adicionar uma lista de tarefas +buttons.table.add.tooltip=Adicionar uma tabela +buttons.table.add.insert=Adicionar +buttons.table.rows=Linhas +buttons.table.cols=Colunas buttons.mention.tooltip=Mencionar um utilizador ou uma equipa buttons.ref.tooltip=Referenciar uma questão ou um pedido de integração buttons.switch_to_legacy.tooltip=Usar o editor clássico @@ -348,6 +354,7 @@ enable_update_checker=Habilitar verificador de novidades enable_update_checker_helper=Verifica, periodicamente, se foi lançada alguma versão nova, fazendo uma ligação ao gitea.io. env_config_keys=Configuração do ambiente env_config_keys_prompt=As seguintes variáveis de ambiente também serão aplicadas ao seu ficheiro de configuração: +config_write_file_prompt=Estas opções de configuração serão escritas em: %s [home] nav_menu=Menu de navegação @@ -453,6 +460,7 @@ authorize_application=Autorizar aplicação authorize_redirect_notice=Irá ser reencaminhado para %s se autorizar esta aplicação. authorize_application_created_by=Esta aplicação foi criada por %s. authorize_application_description=Se conceder acesso, a aplicação terá privilégios para alterar toda a informação da conta, incluindo repositórios e organizações privados. +authorize_application_with_scopes=Com âmbitos: %s authorize_title=Autorizar o acesso de "%s" à sua conta? authorization_failed=A autorização falhou authorization_failed_desc=A autorização falhou porque encontrámos um pedido inválido. Entre em contacto com o responsável pela aplicação que tentou autorizar. @@ -758,6 +766,7 @@ uploaded_avatar_not_a_image=O ficheiro carregado não é uma imagem. uploaded_avatar_is_too_big=O tamanho do ficheiro carregado (%d KiB) excede o tamanho máximo (%d KiB). update_avatar_success=O seu avatar foi substituído. update_user_avatar_success=O avatar do utilizador foi modificado. +cropper_prompt=Pode editar a imagem antes de a guardar. A imagem editada será guardada como PNG. change_password=Substituir a senha old_password=Senha corrente @@ -1024,6 +1033,8 @@ fork_to_different_account=Fazer uma derivação para uma conta diferente fork_visibility_helper=A visibilidade de um repositório derivado não poderá ser alterada posteriormente. fork_branch=Ramo a ser clonado para a derivação all_branches=Todos os ramos +view_all_branches=Ver todos os ramos +view_all_tags=Ver todas as etiquetas fork_no_valid_owners=Não pode fazer uma derivação deste repositório porque não existem proprietários válidos. fork.blocked_user=Não pode derivar o repositório porque foi bloqueado/a pelo/a proprietário/a do repositório. use_template=Usar este modelo @@ -1035,7 +1046,8 @@ generate_repo=Gerar repositório generate_from=Gerar a partir de repo_desc=Descrição repo_desc_helper=Insira uma descrição curta (opcional) -repo_lang=Idioma +repo_no_desc=Descrição não fornecida +repo_lang=Idiomas repo_gitignore_helper=Escolher modelos .gitignore. repo_gitignore_helper_desc=Escolha os ficheiros que não são para rastrear, a partir de uma lista de modelos de linguagens comuns. Serão incluídos no ficheiro .gitignore, logo à partida, artefactos típicos gerados pelas ferramentas de construção de cada uma das linguagens. issue_labels=Rótulos para as questões @@ -1097,7 +1109,7 @@ delete_preexisting_success=Eliminados os ficheiros não adoptados em %s blame_prior=Ver a responsabilização anterior a esta modificação blame.ignore_revs=Ignorando as revisões em .git-blame-ignore-revs. Clique aqui para contornar e ver a vista normal de responsabilização. blame.ignore_revs.failed=Falhou ao ignorar as revisões em .git-blame-ignore-revs. -author_search_tooltip=Mostra um máximo de 30 utilizadores +user_search_tooltip=Mostra um máximo de 30 utilizadores tree_path_not_found_commit=A localização %[1]s não existe no cometimento %[2]s tree_path_not_found_branch=A localização %[1]s não existe no ramo %[2]s @@ -1452,8 +1464,6 @@ issues.new.no_items=Sem itens issues.new.milestone=Etapa issues.new.no_milestone=Sem etapa issues.new.clear_milestone=Limpar etapa -issues.new.open_milestone=Etapas abertas -issues.new.closed_milestone=Etapas fechadas issues.new.assignees=Encarregados issues.new.clear_assignees=Retirar todos os encarregados issues.new.no_assignees=Sem encarregados @@ -1518,7 +1528,8 @@ issues.filter_assignee=Encarregado issues.filter_assginee_no_select=Todos os encarregados issues.filter_assginee_no_assignee=Sem encarregado issues.filter_poster=Autor(a) -issues.filter_poster_no_select=Todos os autores +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.assigned_to_you=Atribuídas a si @@ -1661,27 +1672,34 @@ issues.comment_on_locked=Não pode comentar numa questão bloqueada. issues.delete=Eliminar issues.delete.title=Pretende eliminar esta questão? issues.delete.text=Tem a certeza que quer eliminar esta questão? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-la, se pretender mantê-la em arquivo. + issues.tracker=Gestor de tempo -issues.start_tracking_short=Iniciar cronómetro -issues.start_tracking=Iniciar contagem de tempo -issues.start_tracking_history=`começou a trabalhar %s` +issues.timetracker_timer_start=Iniciar cronómetro +issues.timetracker_timer_stop=Parar cronómetro +issues.timetracker_timer_discard=Descartar cronómetro +issues.timetracker_timer_manually_add=Adicionar tempo + +issues.time_estimate_placeholder=1h 2m +issues.time_estimate_set=Definir tempo estimado +issues.time_estimate_display=Estimativa: %s +issues.change_time_estimate_at=alterou a estimativa de tempo para %s %s +issues.remove_time_estimate_at=removeu a estimativa de tempo %s +issues.time_estimate_invalid=O formato da estimativa de tempo é inválido +issues.start_tracking_history=começou a trabalhar %s issues.tracker_auto_close=O cronómetro será parado automaticamente quando esta questão for fechada issues.tracking_already_started=`Você já iniciou a contagem de tempo noutra questão!` -issues.stop_tracking=Parar cronómetro -issues.stop_tracking_history=`parou de trabalhar %s` -issues.cancel_tracking=Descartar +issues.stop_tracking_history=trabalhou durante %s %s issues.cancel_tracking_history=`cancelou a contagem de tempo %s` -issues.add_time=Adicionar tempo manualmente issues.del_time=Eliminar este registo de tempo -issues.add_time_short=Adicionar tempo -issues.add_time_cancel=Cancelar -issues.add_time_history=`adicionou tempo gasto nesta questão %s` +issues.add_time_history=adicionou %s de tempo gasto %s issues.del_time_history=`eliminou o tempo gasto nesta questão %s` +issues.add_time_manually=Adicionar tempo manualmente issues.add_time_hours=Horas issues.add_time_minutes=Minutos issues.add_time_sum_to_small=Não foi inserido qualquer tempo. issues.time_spent_total=Total de tempo gasto issues.time_spent_from_all_authors=`Total de tempo gasto: %s` + issues.due_date=Data de vencimento issues.invalid_due_date_format=O formato da data de vencimento tem que ser 'aaaa-mm-dd'. issues.error_modifying_due_date=Falhou a modificação da data de vencimento. @@ -1779,7 +1797,7 @@ issues.reference_link=Referência: %s compare.compare_base=base compare.compare_head=comparar -pulls.desc=Habilitar pedidos de integração e revisão de código. +pulls.desc=Habilitar pedidos de integração e revisão de código-fonte. pulls.new=Novo pedido de integração pulls.new.blocked_user=Não pode criar o pedido de integração porque foi bloqueado/a pelo/a proprietário/a do repositório. pulls.new.must_collaborator=Tem de ser um/a colaborador/a para criar um pedido de integração. @@ -1869,7 +1887,7 @@ pulls.rebase_merge_pull_request=Mudar a base e avançar rapidamente pulls.rebase_merge_commit_pull_request=Mudar a base e criar um cometimento de integração pulls.squash_merge_pull_request=Criar cometimento de compactação pulls.fast_forward_only_merge_pull_request=Avançar rapidamente apenas -pulls.merge_manually=Integrado manualmente +pulls.merge_manually=Integrar manualmente pulls.merge_commit_id=O ID de cometimento da integração pulls.require_signed_wont_sign=O ramo requer que os cometimentos sejam assinados mas esta integração não vai ser assinada @@ -1928,6 +1946,10 @@ pulls.delete.title=Eliminar este pedido de integração? pulls.delete.text=Tem a certeza que quer eliminar este pedido de integração? Isso irá remover todo o conteúdo permanentemente. Como alternativa considere fechá-lo, se pretender mantê-lo em arquivo. pulls.recently_pushed_new_branches=Enviou para o ramo %[1]s %[2]s +pulls.upstream_diverging_prompt_behind_1=Este ramo está %d cometimento atrás de %s +pulls.upstream_diverging_prompt_behind_n=Este ramo está %d cometimentos atrás de %s +pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações +pulls.upstream_diverging_merge=Sincronizar derivação pull.deleted_branch=(eliminado):%s pull.agit_documentation=Rever a documentação sobre o AGit @@ -2131,7 +2153,7 @@ settings.branches.update_default_branch=Definir o ramo principal settings.branches.add_new_rule=Adicionar nova regra settings.advanced_settings=Configurações avançadas settings.wiki_desc=Habilitar wiki do repositório -settings.use_internal_wiki=Usar o wiki nativo +settings.use_internal_wiki=Usar o wiki integrado settings.default_wiki_branch_name=Nome do ramo predefinido do wiki settings.default_wiki_everyone_access=Permissão de acesso predefinida para utilizadores registados: settings.failed_to_change_default_wiki_branch=Falhou ao mudar o nome do ramo predefinido do wiki. @@ -2140,7 +2162,7 @@ settings.external_wiki_url=URL do wiki externo settings.external_wiki_url_error=O URL do wiki externo não é um URL válido. settings.external_wiki_url_desc=Os visitantes são encaminhados para o URL do wiki externo ao clicar no separador do wiki. settings.issues_desc=Habilitar o seguidor de questões do repositório -settings.use_internal_issue_tracker=Usar o seguidor de questões nativo +settings.use_internal_issue_tracker=Usar o seguidor de questões integrado settings.use_external_issue_tracker=Usar um seguidor de questões externo settings.external_tracker_url=URL do gestor de questões externo settings.external_tracker_url_error=O URL do gestor de questões externo não é um URL válido. @@ -2230,7 +2252,7 @@ settings.wiki_deletion_success=Os dados do repositório do wiki foram eliminados settings.delete=Eliminar este repositório settings.delete_desc=Eliminar um repositório é permanente e não pode ser revertido. settings.delete_notices_1=- Esta operação NÃO PODERÁ ser revertida. -settings.delete_notices_2=- Esta operação eliminará permanentemente o repositório %s incluindo código, questões, comentários, dados do wiki e configurações dos colaboradores. +settings.delete_notices_2=- Esta operação eliminará permanentemente o repositório %s incluindo código-fonte, questões, comentários, dados do wiki e configurações dos colaboradores. settings.delete_notices_fork_1=- Derivações deste repositório tornar-se-ão independentes, após a eliminação. settings.deletion_success=O repositório foi eliminado. settings.update_settings_success=As configurações do repositório foram modificadas. @@ -2460,6 +2482,8 @@ settings.block_on_official_review_requests=Bloquear integração nos pedidos de settings.block_on_official_review_requests_desc=A integração não será possível quando tiver pedidos de revisão oficiais, mesmo que haja aprovações suficientes. settings.block_outdated_branch=Bloquear integração se o pedido de integração for obsoleto settings.block_outdated_branch_desc=A integração não será possível quando o ramo de topo estiver abaixo do ramo base. +settings.block_admin_merge_override=Os administradores têm de cumprir as regras de salvaguarda dos ramos +settings.block_admin_merge_override_desc=Os administradores têm de cumprir as regras de salvaguarda e não as podem contornar. settings.default_branch_desc=Escolha um ramo do repositório como sendo o predefinido para pedidos de integração e cometimentos: settings.merge_style_desc=Estilos de integração settings.default_merge_style_desc=Tipo de integração predefinido @@ -2579,7 +2603,6 @@ diff.generated=gerado diff.vendored=externo diff.comment.add_line_comment=Adicionar comentário de linha diff.comment.placeholder=Deixar um comentário -diff.comment.markdown_info=A formatação com markdown é suportada. diff.comment.add_single_comment=Adicionar um único comentário diff.comment.add_review_comment=Adicionar comentário diff.comment.start_review=Iniciar revisão @@ -2721,7 +2744,7 @@ create_org=Criar organização repo_updated=Modificado members=Membros teams=Equipas -code=Código +code=Código-fonte lower_members=membros lower_repositories=repositórios create_new_team=Nova equipa @@ -3514,6 +3537,11 @@ alpine.repository=Informação do repositório alpine.repository.branches=Ramos alpine.repository.repositories=Repositórios alpine.repository.architectures=Arquitecturas +arch.registry=Adicionar servidor com repositório e arquitectura relacionados a /etc/pacman.conf: +arch.install=Sincronizar pacote com pacman: +arch.repository=Informação do repositório +arch.repository.repositories=Repositórios +arch.repository.architectures=Arquitecturas cargo.registry=Configurar este registo no ficheiro de configuração do Cargo (por exemplo: ~/.cargo/config.toml): cargo.install=Para instalar o pacote usando o Cargo, execute o seguinte comando: chef.registry=Configure este registo no seu ficheiro ~/.chef/config.rb: diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 7fd73f9d830..735077bd414 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -170,6 +170,7 @@ buttons.link.tooltip=Добавить ссылку buttons.list.unordered.tooltip=Добавить маркированный список buttons.list.ordered.tooltip=Добавить нумерованный список buttons.list.task.tooltip=Добавить список заданий +buttons.table.add.insert=Добавить buttons.mention.tooltip=Упомянуть пользователя или команду buttons.ref.tooltip=Сослаться на задачу или запрос на слияние buttons.switch_to_legacy.tooltip=Использовать старый редактор @@ -924,7 +925,6 @@ generate_repo=Создать репозиторий generate_from=Создать из repo_desc=Описание repo_desc_helper=Добавьте краткое описание (необязательно) -repo_lang=Язык repo_gitignore_helper=Выберите шаблон .gitignore. repo_gitignore_helper_desc=Выберите из списка шаблонов для популярных языков , какие файлы не надо отслеживать. По умолчанию в .gitignore включены типичные артефакты, создаваемые инструментами сборки каждого языка. issue_labels=Метки задач @@ -977,7 +977,6 @@ delete_preexisting=Удалить уже существующие файлы delete_preexisting_content=Удалить файлы из %s delete_preexisting_success=Удалены непринятые файлы в %s blame_prior=Показать авторство предшествующих изменений -author_search_tooltip=Показывает максимум 30 пользователей tree_path_not_found_commit=Путь %[1]s не существует в коммите %[2]s tree_path_not_found_branch=Путь %[1]s не существует в ветке %[2]s @@ -1301,12 +1300,9 @@ issues.new.no_items=Нет элементов issues.new.milestone=Этап issues.new.no_milestone=Нет этапа issues.new.clear_milestone=Очистить этап -issues.new.open_milestone=Открыть этап -issues.new.closed_milestone=Завершенные этапы issues.new.assignees=Назначенные issues.new.clear_assignees=Убрать ответственных issues.new.no_assignees=Нет назначенных лиц -issues.new.no_reviewers=Нет рецензентов issues.choose.get_started=Начать issues.choose.open_external_link=Открыть issues.choose.blank=По умолчанию @@ -1363,7 +1359,6 @@ issues.filter_assignee=Назначено issues.filter_assginee_no_select=Все назначения issues.filter_assginee_no_assignee=Нет ответственного issues.filter_poster=Автор -issues.filter_poster_no_select=Все авторы issues.filter_type=Тип issues.filter_type.all_issues=Все задачи issues.filter_type.assigned_to_you=Назначено вам @@ -1502,27 +1497,20 @@ issues.comment_on_locked=Вы не можете оставить коммент issues.delete=Удалить issues.delete.title=Удалить эту задачу? issues.delete.text=Вы действительно хотите удалить эту задачу? Это навсегда удалит всё содержимое. Возможно лучше закрыть её в архивных целях. + issues.tracker=Отслеживание времени -issues.start_tracking_short=Запустить таймер -issues.start_tracking=Начать отслеживание времени -issues.start_tracking_history=`начал(а) работать %s` + issues.tracker_auto_close=Таймер будет остановлен автоматически, когда эта проблема будет закрыта issues.tracking_already_started=`Вы уже начали отслеживать время для другой задачи!` -issues.stop_tracking=Остановить таймер -issues.stop_tracking_history=`перестал(а) работать %s` -issues.cancel_tracking=Отмена issues.cancel_tracking_history=`отменил(а) отслеживание времени %s` -issues.add_time=Вручную добавить время issues.del_time=Удалить этот журнал времени -issues.add_time_short=Добавить время -issues.add_time_cancel=Отмена -issues.add_time_history=`добавил(а) к затраченному времени %s` issues.del_time_history=`удалил(а) потраченное время %s` 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=Не удалось изменить срок выполнения. @@ -2307,7 +2295,6 @@ diff.load=Загрузить разницу diff.generated=сгенерированный diff.vendored=поставляемый diff.comment.placeholder=Оставить комментарий -diff.comment.markdown_info=Поддерживается синтаксис Markdown. diff.comment.add_single_comment=Добавить простой комментарий diff.comment.add_review_comment=Добавить комментарий diff.comment.start_review=Начать рецензию @@ -3171,6 +3158,9 @@ alpine.install=Чтобы установить пакет, выполните с alpine.repository=О репозитории alpine.repository.repositories=Репозитории alpine.repository.architectures=Архитектуры +arch.repository=О репозитории +arch.repository.repositories=Репозитории +arch.repository.architectures=Архитектуры cargo.registry=Настройте этот реестр в файле конфигурации Cargo (например, ~/.cargo/config.toml): cargo.install=Чтобы установить пакет с помощью Cargo, выполните следующую команду: chef.registry=Настройте этот реестр в своём файле ~/.chef/config.rb: diff --git a/options/locale/locale_si-LK.ini b/options/locale/locale_si-LK.ini index 4d64c46e6ee..506fa5b4925 100644 --- a/options/locale/locale_si-LK.ini +++ b/options/locale/locale_si-LK.ini @@ -114,6 +114,7 @@ filter.private=පෞද්ගලික [heatmap] [editor] +buttons.table.add.insert=එකතු [filter] @@ -713,7 +714,6 @@ generate_repo=ගබඩාව ජනනය කරන්න generate_from=සිට උත්පාදනය repo_desc=සවිස්තරය repo_desc_helper=කෙටි විස්තරයක් ඇතුලත් කරන්න (විකල්ප) -repo_lang=භාෂාව repo_gitignore_helper=.gitignore සැකිලි තෝරන්න. repo_gitignore_helper_desc=පොදු භාෂා සඳහා සැකිලි ලැයිස්තුවෙන් සොයා නොගත යුතු ගොනු තෝරන්න. එක් එක් භාෂාව ගොඩ නැගීමේ මෙවලම් මගින් ජනනය කරන ලද සාමාන්ය කෞතුක වස්තු පෙරනිමියෙන් .gitignore මත ඇතුළත් වේ. issue_labels=නිකුත් ලේබල @@ -986,12 +986,9 @@ issues.new.closed_projects=සංවෘත ව්‍යාපෘති issues.new.milestone=සන්ධිස්ථානය issues.new.no_milestone=සන්ධිස්ථානයක් නැත issues.new.clear_milestone=පැහැදිලි සන්ධිස්ථානයක් -issues.new.open_milestone=විවෘත සන්ධිස්ථාන -issues.new.closed_milestone=සංවෘත සන්ධිස්ථාන issues.new.assignees=සහස්ර issues.new.clear_assignees=පැහැදිලි ඇග්නස් issues.new.no_assignees=කිසිදු සහස්ර -issues.new.no_reviewers=විචාරකයින් නැත issues.choose.get_started=ආරම්භ කරන්න issues.choose.open_external_link=විවෘත issues.choose.blank=පෙරනිමි @@ -1133,26 +1130,19 @@ issues.lock.title=මෙම ගැටළුව පිළිබඳ ලොක් issues.unlock.title=මෙම ගැටළුව පිළිබඳ සංවාදය අගුළු ඇරීමට. issues.comment_on_locked=අගුලු දමා ඇති ගැටළුවක් පිළිබඳව ඔබට අදහස් දැක්විය නොහැක. issues.delete=මකන්න + issues.tracker=වේලාව ට්රැකර් -issues.start_tracking_short=ටයිමරයට ගැලපෙන ලෙස -issues.start_tracking=ආරම්භ වේලාව ට්රැකින් -issues.start_tracking_history=`වැඩ ආරම්භ %s` + issues.tracker_auto_close=මෙම ගැටළුව වසා දැමූ විට ටයිමරයට ස්වයංක්රීයව නතර වේ issues.tracking_already_started=`ඔබ දැනටමත් හි තවත් නිකුතුවක්හි කාලය සොයා ගැනීම ආරම්භ කර ඇත! ` -issues.stop_tracking=ටයිමරයට නවත්වන්න -issues.stop_tracking_history=`නතර වැඩ %s` -issues.cancel_tracking=ඉවතලන්න -issues.add_time=අතින් වේලාව එකතු කරන්න issues.del_time=මෙම කාල ලොග් මකන්න -issues.add_time_short=කාලය එකතු කරන්න -issues.add_time_cancel=අවලංගු කරන්න -issues.add_time_history=`එකතු කළ කාලය %s` issues.del_time_history=`මකාදැමුවා කාලය ගත %s` 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=නියමිත දින ආකෘතිය 'yyy-mm-dd' විය යුතුය. issues.error_modifying_due_date=නියමිත දිනය වෙනස් කිරීමට අපොහොසත් විය. @@ -1776,7 +1766,6 @@ diff.load=බර අඩු diff.generated=ජනනය diff.vendored=අලෙවි diff.comment.placeholder=අදහසක් හැරයන්න -diff.comment.markdown_info=Markdown සමග මෝස්තර සහාය වේ. diff.comment.add_single_comment=තනි අදහස් එක් කරන්න diff.comment.add_review_comment=අදහස එකතු කරන්න diff.comment.start_review=සමාලෝචනය ආරම්භ කරන්න @@ -2458,6 +2447,7 @@ no_read=කියවූ දැනුම්දීම් නැත. filter.type=වර්ගය alpine.repository.branches=ශාඛා alpine.repository.repositories=කෝෂ්ඨ +arch.repository.repositories=කෝෂ්ඨ conan.details.repository=කෝෂ්ඨය owner.settings.cleanuprules.enabled=සබල කර ඇත diff --git a/options/locale/locale_sk-SK.ini b/options/locale/locale_sk-SK.ini index a964b526afa..b4bf6fb552a 100644 --- a/options/locale/locale_sk-SK.ini +++ b/options/locale/locale_sk-SK.ini @@ -169,6 +169,7 @@ buttons.link.tooltip=Pridať odkaz buttons.list.unordered.tooltip=Pridať zoznam buttons.list.ordered.tooltip=Pridať číslovaný zoznam buttons.list.task.tooltip=Pridať zoznam úloh +buttons.table.add.insert=Pridať buttons.mention.tooltip=Spomenúť používateľa alebo tím buttons.ref.tooltip=Odkázať na problém alebo žiadosť o natiahnutie buttons.switch_to_legacy.tooltip=Použiť starší editor namiest toho @@ -832,7 +833,6 @@ generate_repo=Generovať repozitár generate_from=Generovať z repo_desc=Popis repo_desc_helper=Zadajte krátky popis (voliteľné) -repo_lang=Jazyk repo_gitignore_helper=Vyberte .gitignore šablóny. repo_gitignore_helper_desc=Zo zoznamu šablón pre bežné jazyky vyberte, ktoré súbory sa nemajú sledovať. Typické artefakty generované nástrojmi na vytváranie jednotlivých jazykov sú štandardne zahrnuté v .gitignore. license=Licencia @@ -1049,7 +1049,6 @@ issues.filter_reviewers=Filtrovať revidentov issues.new.labels=Štítky issues.new.projects=Projekty issues.new.milestone=Míľnik -issues.new.no_reviewers=Žiadni revidenti issues.choose.open_external_link=Otvoriť issues.create=Vytvoriť úkol issues.filter_label=Štítok @@ -1070,8 +1069,9 @@ issues.dismiss_review=Zamietnuť revíziu issues.dismiss_review_warning=Naozaj chcete zrušiť túto revíziu? issues.cancel=Zrušiť issues.save=Uložiť -issues.cancel_tracking=Zahodiť -issues.add_time_cancel=Zrušiť + + + issues.dependency.cancel=Zrušiť issues.review.dismissed=zamietol revíziu od %s %s issues.review.wait=bol požiadaný o revidovanie %s @@ -1311,6 +1311,7 @@ error.not_signed_commit=Nie je podpísaný commit [packages] alpine.repository.repositories=Repozitáre +arch.repository.repositories=Repozitáre conan.details.repository=Repozitár container.labels=Štítky owner.settings.cleanuprules.enabled=Povolené diff --git a/options/locale/locale_sv-SE.ini b/options/locale/locale_sv-SE.ini index 2993828c22f..fc138381db7 100644 --- a/options/locale/locale_sv-SE.ini +++ b/options/locale/locale_sv-SE.ini @@ -104,6 +104,7 @@ filter.private=Privat [heatmap] [editor] +buttons.table.add.insert=Lägg till [filter] @@ -615,7 +616,6 @@ use_template=Välj den här mallen generate_repo=Skapa utvecklingskatalog generate_from=Generera från repo_desc=Beskrivning -repo_lang=Språk repo_gitignore_helper=Välj .gitignore-mallar. repo_gitignore_helper_desc=Välj vilka filer som inte ska spåras från en lista med mallar för vanliga språk. Typiska artefakter som genereras av varje språk byggverktyg ingår i .gitignore som standard. issue_labels=Ärendeetiketter @@ -837,12 +837,9 @@ issues.new.no_items=Inga objekt issues.new.milestone=Milsten issues.new.no_milestone=Ingen Milsten issues.new.clear_milestone=Rensa milstenar -issues.new.open_milestone=Öppna Milstenar -issues.new.closed_milestone=Stängda Milstenar issues.new.assignees=Tilldelade issues.new.clear_assignees=Rensa tilldelade issues.new.no_assignees=Ingen tilldelad -issues.new.no_reviewers=Inga granskare issues.choose.get_started=Kom igång issues.choose.open_external_link=Öppna issues.choose.blank=Standard @@ -980,22 +977,18 @@ issues.comment_on_locked=Du kan inte kommentera ett låst ärende. issues.delete=Radera issues.delete.title=Radera detta ärende? issues.delete.text=Vill du verkligen ta bort detta ärende? (Detta kommer att permanent ta bort allt innehåll. Överväg att stänga det istället om du avser att hålla det arkiverat) + issues.tracker=Tidsredovisning -issues.start_tracking=Starta tidsredovisning -issues.start_tracking_history=`började arbeta %s` + issues.tracker_auto_close=Timern stoppas automatiskt när ärendet stängs issues.tracking_already_started=`Du har redan påbörjat tidredovisning på ett annat ärende!` -issues.stop_tracking_history=`slutade arbeta %s` -issues.add_time=Lägg till tid manuellt -issues.add_time_short=Lägg till tid -issues.add_time_cancel=Avbryt -issues.add_time_history=`la till tillbringad tid %s` issues.del_time_history=`raderade tillbringad tid %s` issues.add_time_hours=Timmar issues.add_time_minutes=Minuter issues.add_time_sum_to_small=Inge tid har angivits. issues.time_spent_total=Total Tid Spenderad issues.time_spent_from_all_authors=`Total Tid Spenderad: %s` + issues.due_date=Förfallodatum issues.invalid_due_date_format=Datumsformatet för förfallodatum måste följa 'yyyy-MM-dd'. issues.error_modifying_due_date=Det gick inte att ändra förfallodatumet. @@ -1443,7 +1436,6 @@ diff.file_image_height=Höjd diff.file_byte_size=Storlek diff.file_suppressed=Filskillnaden har hållits tillbaka eftersom den är för stor diff.comment.placeholder=Lämna en kommentar -diff.comment.markdown_info=Styling med markdown stöds. diff.comment.add_single_comment=Lägg till en kommentar diff.comment.add_review_comment=Lägg till kommentar diff.comment.start_review=Starta granskning @@ -1990,6 +1982,7 @@ error.unit_not_allowed=Du har inte åtkomst till denna del av utvecklingskatalog filter.type=Typ alpine.repository.branches=Brancher alpine.repository.repositories=Utvecklingskataloger +arch.repository.repositories=Utvecklingskataloger conan.details.repository=Utvecklingskatalog owner.settings.cleanuprules.enabled=Aktiv diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 3c719f84baf..9b7f2cb5c6d 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -206,6 +206,7 @@ buttons.link.tooltip=Bağlantı ekle buttons.list.unordered.tooltip=Maddeli liste ekle buttons.list.ordered.tooltip=Numaralandırılmış liste ekle buttons.list.task.tooltip=Görev listesi ekle +buttons.table.add.insert=Ekle buttons.mention.tooltip=Bir kişiye veya takıma değin buttons.ref.tooltip=Bir konuya veya değişiklik isteğine değin buttons.switch_to_legacy.tooltip=Eski düzenleyiciyi kullan @@ -1012,7 +1013,6 @@ 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_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 @@ -1073,7 +1073,6 @@ 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ı. -author_search_tooltip=En fazla 30 kullanıcı görüntüler tree_path_not_found_commit=%[1] yolu, %[2]s işlemesinde mevcut değil tree_path_not_found_branch=%[1] yolu, %[2]s dalında mevcut değil @@ -1419,12 +1418,9 @@ issues.new.no_items=Öge yok issues.new.milestone=Kilometre Taşı issues.new.no_milestone=Kilometre Taşı Yok issues.new.clear_milestone=Kilometre Taşlarını Temizle -issues.new.open_milestone=Kilometre Taşlarını Aç -issues.new.closed_milestone=Kapanmış Kilometre Taşları 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,7 +1480,6 @@ issues.filter_assignee=Atanan issues.filter_assginee_no_select=Tüm atananlar issues.filter_assginee_no_assignee=Atanan yok issues.filter_poster=Yazar -issues.filter_poster_no_select=Tüm yazarlar issues.filter_type=Tür issues.filter_type.all_issues=Tüm konular issues.filter_type.assigned_to_you=Size atanan @@ -1627,27 +1622,20 @@ issues.comment_on_locked=Kilitli bir konuya yorum yapamazsınız. issues.delete=Sil 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.start_tracking_short=Zamanlayıcıyı Başlat -issues.start_tracking=Zaman İzlemeyi Başlat -issues.start_tracking_history=`%s çalışma başlattı` + 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.stop_tracking=Zamanlayıcıyı Bitir -issues.stop_tracking_history=`%s çalışmayı durdurdu` -issues.cancel_tracking=Yoksay issues.cancel_tracking_history=`%s zaman takibini iptal etti` -issues.add_time=El ile Zaman Ekle issues.del_time=Bu zaman kaydını sil -issues.add_time_short=Zaman Ekle -issues.add_time_cancel=İptal -issues.add_time_history=`%s harcanan zaman eklendi` issues.del_time_history=`%s harcanan zaman silindi` issues.add_time_hours=Saat issues.add_time_minutes=Dakika issues.add_time_sum_to_small=Zaman girilmedi. issues.time_spent_total=Toplam Harcanan Zaman issues.time_spent_from_all_authors=`Toplam Harcanan Zaman: %s` + issues.due_date=Bitiş Tarihi issues.invalid_due_date_format=Bitiş tarihinin biçimi 'yyyy-aa-gg' olmalıdır. issues.error_modifying_due_date=Bitiş tarihi değiştirilemedi. @@ -2503,7 +2491,6 @@ diff.generated=üretilen diff.vendored=sağlanmış diff.comment.add_line_comment=Satır yorum ekle diff.comment.placeholder=Yorum Yap -diff.comment.markdown_info=Markdown ile şekillendirme desteklenir. diff.comment.add_single_comment=Bir yorum ekle diff.comment.add_review_comment=Yorum ekle diff.comment.start_review=İncelemeye başla @@ -3418,6 +3405,9 @@ alpine.repository=Depo Bilgisi alpine.repository.branches=Dallar alpine.repository.repositories=Depolar alpine.repository.architectures=Mimariler +arch.repository=Depo Bilgisi +arch.repository.repositories=Depolar +arch.repository.architectures=Mimariler cargo.registry=Bu kütüğü Cargo yapılandırma dosyasına (örneğin ~/.cargo/config.toml) ayarlayın: cargo.install=Paketi Cargo kullanarak kurmak için, şu komutu çalıştırın: chef.registry=Bu kütüğü ~/.chef/config.rb dosyasında ayarlayın: diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index d94d11207b4..efefeeb436b 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -115,6 +115,7 @@ filter.private=Приватний [heatmap] [editor] +buttons.table.add.insert=Додати [filter] @@ -732,7 +733,6 @@ generate_repo=Згенерувати репозиторій generate_from=Генерувати з repo_desc=Опис repo_desc_helper=Введіть короткий опис (опціонально) -repo_lang=Мова repo_gitignore_helper=Виберіть шаблон .gitignore. repo_gitignore_helper_desc=Оберіть з списку мовних шаблонів файли, які не будуть відстежуватись. Типові артефакти, які генеруються за допомогою інструментів побудови кожної мови, за замовчуванням включені до .gitignor. issue_labels=Мітки задачі @@ -1022,12 +1022,9 @@ issues.new.no_items=Немає елементів issues.new.milestone=Етап issues.new.no_milestone=Етап відсутній issues.new.clear_milestone=Очистити етап -issues.new.open_milestone=Активні етапи -issues.new.closed_milestone=Закриті етапи issues.new.assignees=Виконавці issues.new.clear_assignees=Прибрати виконавців issues.new.no_assignees=Немає виконавця -issues.new.no_reviewers=Немає рецензентів issues.choose.get_started=Початок роботи issues.choose.open_external_link=Відкрити issues.choose.blank=Типово @@ -1178,26 +1175,19 @@ issues.lock.title=Заблокувати обговорення цієї зад issues.unlock.title=Розблокувати обговорення цієї задачі. issues.comment_on_locked=Ви не можете коментувати заблоковану задачу. issues.delete=Видалити + issues.tracker=Відстеження часу -issues.start_tracking_short=Запустити таймер -issues.start_tracking=Почати відстеження часу -issues.start_tracking_history=`почав працювати %s` + issues.tracker_auto_close=Таймер буде автоматично зупинено, коли ця задача буде закрита issues.tracking_already_started=`Ви вже почали відстежувати час для іншої задачі!` -issues.stop_tracking=Зупинити таймер -issues.stop_tracking_history=`перестав(-ла) працювати %s` -issues.cancel_tracking=Скасувати -issues.add_time=Вручну додати час issues.del_time=Видалити цей журнал часу -issues.add_time_short=Додати час -issues.add_time_cancel=Відмінити -issues.add_time_history=`додав(-ла) витрачений час %s` issues.del_time_history=`видалив витрачений час %s` 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=Не вдалося змінити дату завершення. @@ -1824,7 +1814,6 @@ diff.load=Завантажити різницю diff.generated=згенерований diff.vendored=сторонній diff.comment.placeholder=Залишити коментар -diff.comment.markdown_info=Стилізація з markdown підтримується. diff.comment.add_single_comment=Додати простий коментар diff.comment.add_review_comment=Додати коментар diff.comment.start_review=Розпочати рецензію @@ -2526,6 +2515,7 @@ error.unit_not_allowed=У вас немає доступу до жодного filter.type=Тип alpine.repository.branches=Гілки alpine.repository.repositories=Репозиторії +arch.repository.repositories=Репозиторії conan.details.repository=Репозиторій owner.settings.cleanuprules.enabled=Увімкнено diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index 3be7b044e90..3add7f8be38 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -158,6 +158,7 @@ filter.public=公开 filter.private=私有库 no_results_found=未找到结果 +internal_error_skipped=发生内部错误,但已被跳过: %s [search] search=搜索... @@ -176,6 +177,7 @@ code_search_by_git_grep=当前代码搜索结果由“git grep”提供。如果 package_kind=搜索软件包... project_kind=搜索项目... branch_kind=搜索分支... +tag_kind=搜索标签... commit_kind=搜索提交记录... runner_kind=搜索runners... no_results=未找到匹配结果 @@ -205,6 +207,10 @@ 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=使用旧版编辑器 @@ -380,6 +386,7 @@ relevant_repositories=只显示相关的仓库, 显示未过滤 [auth] create_new_account=注册帐号 +sign_in_now=立即登录 disable_register_prompt=对不起,注册功能已被关闭。请联系网站管理员。 disable_register_mail=已禁用注册的电子邮件确认。 manual_activation_only=请联系您的站点管理员来完成激活。 @@ -387,6 +394,7 @@ remember_me=记住此设备 remember_me.compromised=登录令牌不再有效,因为它可能表明帐户已被破坏。请检查您的帐户是否有异常活动。 forgot_password_title=忘记密码 forgot_password=忘记密码? +sign_up_now=还没账号?马上注册。 sign_up_successful=帐户创建成功。欢迎! confirmation_mail_sent_prompt_ex=一封新的确认邮件已经发送到 %s请在下一个 %s 中检查您的收件箱以完成注册过程。 如果您的注册电子邮件地址不正确,您可以重新登录并更改它。 must_change_password=更新您的密码 @@ -447,6 +455,7 @@ authorization_failed_desc=因为检测到无效请求,授权失败。请尝试 sspi_auth_failed=SSPI 认证失败 password_pwned_err=无法完成对 HaveIBeenPwned 的请求 last_admin=您不能删除最后一个管理员。必须至少保留一个管理员。 +back_to_sign_in=返回登录页面 [mail] view_it_on=在 %s 上查看 @@ -463,6 +472,7 @@ 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_2=您现在可以以用户名 %s 登录。 @@ -1003,7 +1013,6 @@ generate_repo=生成仓库 generate_from=生成自 repo_desc=仓库描述 repo_desc_helper=输入简要描述 (可选) -repo_lang=仓库语言 repo_gitignore_helper=选择 .gitignore 模板。 repo_gitignore_helper_desc=从常见语言的模板列表中选择忽略跟踪的文件。默认情况下,由开发或构建工具生成的特殊文件都包含在 .gitignore 中。 issue_labels=工单标签 @@ -1011,6 +1020,7 @@ issue_labels_helper=选择一个工单标签集 license=授权许可 license_helper=选择授权许可文件。 license_helper_desc=许可证说明了其他人可以和不可以用您的代码做什么。不确定哪一个适合你的项目?见 选择一个许可证 +multiple_licenses=多许可证 object_format=对象格式 object_format_helper=仓库的对象格式。之后无法更改。SHA1 是最兼容的。 readme=自述 @@ -1064,14 +1074,15 @@ delete_preexisting_success=删除 %s 中未收录的文件 blame_prior=查看此更改前的 blame blame.ignore_revs=忽略 .git-blame-ignore-revs 的修订。点击 绕过 并查看正常的 Blame 视图。 blame.ignore_revs.failed=忽略 .git-blame-ignore-revs 版本失败。 -author_search_tooltip=最多显示30个用户 tree_path_not_found_commit=路径%[1]s 在提交 %[2]s 中不存在 tree_path_not_found_branch=路径 %[1]s 不存在于分支 %[2]s 中。 tree_path_not_found_tag=路径 %[1]s 不存在于标签 %[2]s 中 transfer.accept=接受转移 +transfer.accept_desc=`转移到 "%s"` transfer.reject=拒绝转移 +transfer.reject_desc=`取消转移到 "%s"` transfer.no_permission_to_accept=您没有权限接受此转让。 transfer.no_permission_to_reject=您没有权限拒绝此转让。 @@ -1146,6 +1157,11 @@ 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=AWS 访问密钥ID +migrate.codecommit.aws_secret_access_key=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=迁移里程碑 @@ -1213,6 +1229,7 @@ file_view_rendered=渲染模式 file_view_raw=查看原始文件 file_permalink=永久链接 file_too_large=文件过大,无法显示。 +file_is_empty=此文件是空的。 code_preview_line_from_to=在 %[3]s 的第 %[1]d 行到 %[2]d 行 code_preview_line_in=在 %[2]s 的第 %[1]d 行 invisible_runes_header=`此文件含有不可见的 Unicode 字符` @@ -1409,12 +1426,9 @@ issues.new.no_items=无可选项 issues.new.milestone=里程碑 issues.new.no_milestone=未选择里程碑 issues.new.clear_milestone=取消选中里程碑 -issues.new.open_milestone=开启中的里程碑 -issues.new.closed_milestone=已关闭的里程碑 issues.new.assignees=指派成员 issues.new.clear_assignees=取消指派成员 issues.new.no_assignees=未指派成员 -issues.new.no_reviewers=无审核者 issues.new.blocked_user=无法创建工单,因为您已被仓库所有者屏蔽。 issues.edit.already_changed=无法保存对工单的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改 issues.edit.blocked_user=无法编辑内容,因为您已被仓库所有者或工单创建者屏蔽。 @@ -1474,7 +1488,6 @@ issues.filter_assignee=指派人筛选 issues.filter_assginee_no_select=所有指派成员 issues.filter_assginee_no_assignee=未指派 issues.filter_poster=作者 -issues.filter_poster_no_select=所有作者 issues.filter_type=类型筛选 issues.filter_type.all_issues=所有工单 issues.filter_type.assigned_to_you=指派给您的 @@ -1615,27 +1628,20 @@ issues.comment_on_locked=您不能对锁定的问题发表评论。 issues.delete=删除 issues.delete.title=是否删除工单? issues.delete.text=您真的要删除这个工单吗?(该操作将会永久删除所有内容。如果您需要保留,请关闭它) + issues.tracker=时间跟踪 -issues.start_tracking_short=启动计时器 -issues.start_tracking=开始时间跟踪 -issues.start_tracking_history=`开始工作 %s` + issues.tracker_auto_close=当此工单关闭时,自动停止计时器 issues.tracking_already_started=`你已经开始对 另一个工单 进行时间跟踪!` -issues.stop_tracking=停止计时器 -issues.stop_tracking_history=`停止工作 %s` -issues.cancel_tracking=放弃 issues.cancel_tracking_history=`取消时间跟踪 %s` -issues.add_time=手动添加时间 issues.del_time=删除此时间跟踪日志 -issues.add_time_short=添加时间 -issues.add_time_cancel=取消 -issues.add_time_history=`添加耗时 %s` issues.del_time_history=`已删除时间 %s` 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=到期时间的格式必须是 'yyyy-mm-dd' 的形式。 issues.error_modifying_due_date=修改到期时间失败。 @@ -1714,6 +1720,7 @@ issues.review.resolve_conversation=已解决问题 issues.review.un_resolve_conversation=未解决问题 issues.review.resolved_by=标记问题为已解决 issues.review.commented=评论 +issues.review.unofficial=非官方审批数 issues.assignee.error=因为未知原因,并非所有的指派都成功。 issues.reference_issue.body=内容 issues.content_history.deleted=删除于 @@ -1876,6 +1883,7 @@ pulls.delete.text=你真的要删除这个合并请求吗? (这将永久删除 pulls.recently_pushed_new_branches=您已经于%[2]s推送了分支 %[1]s pull.deleted_branch=(已删除): %s +pull.agit_documentation=查看有关 AGit 的文档 comments.edit.already_changed=无法保存对评论的更改。其内容似乎已被其他用户更改。 请刷新页面并重新编辑以避免覆盖他们的更改 @@ -1883,7 +1891,7 @@ milestones.new=新的里程碑 milestones.closed=于 %s关闭 milestones.update_ago=已更新 %s milestones.no_due_date=暂无截止日期 -milestones.open=开启中 +milestones.open=开启 milestones.close=关闭 milestones.new_subheader=里程碑可以帮助您组织工单并跟踪其进度。 milestones.create=创建里程碑 @@ -1956,6 +1964,7 @@ 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 天 @@ -2245,6 +2254,7 @@ settings.event_wiki_desc=创建、重命名、编辑或删除了百科页面。 settings.event_release=版本发布 settings.event_release_desc=发布、更新或删除版本时。 settings.event_push=推送 +settings.event_force_push=强制推送 settings.event_push_desc=Git 仓库推送 settings.event_repository=仓库 settings.event_repository_desc=创建或删除仓库 @@ -2470,7 +2480,6 @@ diff.generated=自动生成的 diff.vendored=vendored diff.comment.add_line_comment=添加行内评论 diff.comment.placeholder=留下评论 -diff.comment.markdown_info=支持使用Markdown格式。 diff.comment.add_single_comment=添加单条评论 diff.comment.add_review_comment=添加评论 diff.comment.start_review=开始评审 @@ -3143,6 +3152,7 @@ config.cache_adapter=Cache 适配器 config.cache_interval=Cache 周期 config.cache_conn=Cache 连接字符串 config.cache_item_ttl=缓存项目 TTL +config.cache_test=测试缓存 config.session_config=Session 配置 config.session_provider=Session 提供者 @@ -3368,6 +3378,9 @@ alpine.repository=仓库信息 alpine.repository.branches=分支 alpine.repository.repositories=仓库管理 alpine.repository.architectures=架构 +arch.repository=仓库信息 +arch.repository.repositories=仓库管理 +arch.repository.architectures=架构 cargo.registry=在 Cargo 配置文件中设置此注册中心(例如:~/.cargo/config.toml): cargo.install=要使用 Cargo 安装软件包,请运行以下命令: chef.registry=在您的 ~/.chef/config.rb 文件中设置此注册中心: diff --git a/options/locale/locale_zh-HK.ini b/options/locale/locale_zh-HK.ini index 509be152703..6f37d30efcf 100644 --- a/options/locale/locale_zh-HK.ini +++ b/options/locale/locale_zh-HK.ini @@ -308,7 +308,6 @@ visibility=可見度 fork_repo=複製儲存庫 fork_from=複製自 repo_desc=儲存庫描述 -repo_lang=儲存庫語言 license=授權許可 create_repo=建立儲存庫 default_branch=默認分支 @@ -399,8 +398,6 @@ issues.new.clear_labels=清除已選取標籤 issues.new.milestone=里程碑 issues.new.no_milestone=未選擇里程碑 issues.new.clear_milestone=清除已選取里程碑 -issues.new.open_milestone=開啟中的里程碑 -issues.new.closed_milestone=已關閉的里程碑 issues.create=建立問題 issues.new_label=建立標籤 issues.new_label_desc_placeholder=組織描述 @@ -471,7 +468,9 @@ issues.attachment.open_tab=`在新的標籤頁中查看 '%s'` issues.attachment.download=`點擊下載 '%s'` issues.subscribe=訂閱 issues.unsubscribe=取消訂閱 -issues.add_time_cancel=取消 + + + issues.due_date_form_edit=編輯 issues.due_date_form_remove=移除成員 issues.dependency.cancel=取消 @@ -956,6 +955,7 @@ error.not_signed_commit=未簽名的提交 [packages] filter.type=認證類型 alpine.repository.repositories=儲存庫管理 +arch.repository.repositories=儲存庫管理 conan.details.repository=儲存庫 owner.settings.cleanuprules.enabled=已啟用 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index d755f64dcd5..3b1d37a322a 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -156,6 +156,7 @@ buttons.link.tooltip=新增連結 buttons.list.unordered.tooltip=新增項目符號清單 buttons.list.ordered.tooltip=新增編號清單 buttons.list.task.tooltip=新增工作項目清單 +buttons.table.add.insert=增加 buttons.mention.tooltip=提及使用者或團隊 buttons.ref.tooltip=參考問題或合併請求 buttons.enable_monospace_font=啟用等寬字型 @@ -853,7 +854,6 @@ generate_repo=產生儲存庫 generate_from=產生自 repo_desc=描述 repo_desc_helper=輸入簡介 (選用) -repo_lang=儲存庫語言 repo_gitignore_helper=選擇 .gitignore 範本 repo_gitignore_helper_desc=從常見語言範本清單中挑選忽略追蹤的檔案。預設情況下各種語言建置工具產生的特殊檔案都包含在 .gitignore 中。 issue_labels=問題標籤 @@ -904,7 +904,6 @@ delete_preexisting=刪除既有的檔案 delete_preexisting_content=刪除 %s 中的檔案 delete_preexisting_success=刪除 %s 中未接管的檔案 blame_prior=檢視此變更前的 Blame -author_search_tooltip=最多顯示 30 位使用者 transfer.accept=同意轉移 @@ -1211,12 +1210,9 @@ issues.new.no_items=沒有項目 issues.new.milestone=里程碑 issues.new.no_milestone=未選擇里程碑 issues.new.clear_milestone=清除已選取里程碑 -issues.new.open_milestone=開放中的里程碑 -issues.new.closed_milestone=已關閉的里程碑 issues.new.assignees=負責人 issues.new.clear_assignees=清除負責人 issues.new.no_assignees=沒有負責人 -issues.new.no_reviewers=沒有審核者 issues.choose.get_started=開始 issues.choose.open_external_link=開啟 issues.choose.blank=預設 @@ -1269,7 +1265,6 @@ issues.filter_assignee=負責人 issues.filter_assginee_no_select=所有負責人 issues.filter_assginee_no_assignee=沒有負責人 issues.filter_poster=作者 -issues.filter_poster_no_select=所有作者 issues.filter_type=類型 issues.filter_type.all_issues=所有問題 issues.filter_type.assigned_to_you=指派給您的 @@ -1393,26 +1388,19 @@ issues.comment_on_locked=您無法在已鎖定的問題上留言。 issues.delete=刪除 issues.delete.title=刪除此問題? issues.delete.text=您真的要刪除此問題嗎?(這將會永久移除所有內容。若您還想保留,請考慮改為關閉它。) + issues.tracker=時間追蹤 -issues.start_tracking_short=開始計時 -issues.start_tracking=開始時間追蹤 -issues.start_tracking_history=`開始工作 %s` + issues.tracker_auto_close=當這個問題被關閉時,自動停止計時器 issues.tracking_already_started=`您已在另一個問題上開始時間追蹤!` -issues.stop_tracking=停止計時 -issues.stop_tracking_history=`結束工作 %s` -issues.cancel_tracking=捨棄 -issues.add_time=手動新增時間 issues.del_time=刪除此時間記錄 -issues.add_time_short=新增時間 -issues.add_time_cancel=取消 -issues.add_time_history=`加入了花費時間 %s` issues.del_time_history=`刪除了花費時間 %s` 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=截止日期的格式必須為「yyyy-mm-dd」。 issues.error_modifying_due_date=無法修改截止日期。 @@ -2130,7 +2118,6 @@ diff.load=載入差異 diff.generated=generated diff.vendored=vendored diff.comment.placeholder=留言... -diff.comment.markdown_info=支援 markdown 格式。 diff.comment.add_single_comment=加入單獨的留言 diff.comment.add_review_comment=新增留言 diff.comment.start_review=開始審核 @@ -2961,6 +2948,9 @@ alpine.repository=儲存庫資訊 alpine.repository.branches=分支 alpine.repository.repositories=儲存庫 alpine.repository.architectures=架構 +arch.repository=儲存庫資訊 +arch.repository.repositories=儲存庫 +arch.repository.architectures=架構 cargo.registry=在 Cargo 組態檔設定此註冊中心 (例如: ~/.cargo/config.toml): cargo.install=執行下列命令以使用 Cargo 安裝此套件: chef.registry=在您的 ~/.chef/config.rb 檔設定此註冊中心: diff --git a/package-lock.json b/package-lock.json index e3f7a0116f7..8755cfe06f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", "fast-glob": "3.3.2", - "htmx.org": "2.0.3", + "htmx.org": "2.0.4", "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.11", @@ -67,6 +67,7 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.49.0", + "@silverwind/vue-tsc": "2.1.13", "@stoplight/spectral-cli": "6.14.2", "@stylistic/eslint-plugin-js": "2.11.0", "@stylistic/stylelint-plugin": "3.1.1", @@ -87,7 +88,7 @@ "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.1.3", - "eslint-plugin-i": "2.29.1", + "eslint-plugin-import-x": "4.5.0", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-playwright": "2.1.0", @@ -111,8 +112,7 @@ "type-fest": "4.30.0", "updates": "16.4.0", "vite-string-plugin": "1.3.4", - "vitest": "2.1.8", - "vue-tsc": "2.1.10" + "vitest": "2.1.8" }, "engines": { "node": ">= 18.0.0" @@ -3833,6 +3833,24 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@silverwind/vue-tsc": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@silverwind/vue-tsc/-/vue-tsc-2.1.13.tgz", + "integrity": "sha512-ejFxz1KZiUGAESbC+eURnjqt0N95qkU9eZU7W15wgF9zV+v2FEu3ZLduuXTC7D/Sg6lL1R/QjPfUbxbAbBQOsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~2.4.11", + "@vue/language-core": "2.1.10", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, "node_modules/@silverwind/vue3-calendar-heatmap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@silverwind/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.6.tgz", @@ -5335,30 +5353,30 @@ } }, "node_modules/@volar/language-core": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz", - "integrity": "sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.11.tgz", + "integrity": "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.10" + "@volar/source-map": "2.4.11" } }, "node_modules/@volar/source-map": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.10.tgz", - "integrity": "sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.11.tgz", + "integrity": "sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==", "dev": true, "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "2.4.10", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.10.tgz", - "integrity": "sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==", + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.11.tgz", + "integrity": "sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.10", + "@volar/language-core": "2.4.11", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } @@ -8385,56 +8403,6 @@ "node": "*" } }, - "node_modules/eslint-plugin-i": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-i/-/eslint-plugin-i-2.29.1.tgz", - "integrity": "sha512-ORizX37MelIWLbMyqI7hi8VJMf7A0CskMmYkB+lkCX3aF4pkGV7kwx5bSEb4qx7Yce2rAf9s34HqDRPjGRZPNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "doctrine": "^3.0.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "get-tsconfig": "^4.7.2", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://opencollective.com/unts" - }, - "peerDependencies": { - "eslint": "^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-i/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-i/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-i18n-text": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz", @@ -8479,6 +8447,48 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, + "node_modules/eslint-plugin-import-x": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.5.0.tgz", + "integrity": "sha512-l0OTfnPF8RwmSXfjT75N8d6ZYLVrVYWpaGlgvVkVqFERCI5SyBfDP7QEMr3kt0zWi2sOa9EQ47clbdFsHkF83Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.1.0", + "@typescript-eslint/utils": "^8.1.0", + "debug": "^4.3.4", + "doctrine": "^3.0.0", + "eslint-import-resolver-node": "^0.3.9", + "get-tsconfig": "^4.7.3", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3", + "semver": "^7.6.3", + "stable-hash": "^0.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -10565,9 +10575,9 @@ } }, "node_modules/htmx.org": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.3.tgz", - "integrity": "sha512-AeoJUAjkCVVajbfKX+3sVQBTCt8Ct4lif1T+z/tptTXo8+8yyq3QIMQQe/IT+R8ssfrO1I0DeX4CAronzCL6oA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.4.tgz", + "integrity": "sha512-HLxMCdfXDOJirs3vBZl/ZLoY+c7PfM4Ahr2Ad4YXh6d22T5ltbTXFFkpx9Tgb2vvmWFMbIc3LqN2ToNkZJvyYQ==", "license": "0BSD" }, "node_modules/iconv-lite": { @@ -15788,24 +15798,6 @@ } } }, - "node_modules/vue-tsc": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz", - "integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@volar/typescript": "~2.4.8", - "@vue/language-core": "2.1.10", - "semver": "^7.5.4" - }, - "bin": { - "vue-tsc": "bin/vue-tsc.js" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - } - }, "node_modules/watchpack": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", diff --git a/package.json b/package.json index d30aedc54f5..61e65c1f43e 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", "fast-glob": "3.3.2", - "htmx.org": "2.0.3", + "htmx.org": "2.0.4", "idiomorph": "0.3.0", "jquery": "3.7.1", "katex": "0.16.11", @@ -66,6 +66,7 @@ "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.49.0", + "@silverwind/vue-tsc": "2.1.13", "@stoplight/spectral-cli": "6.14.2", "@stylistic/eslint-plugin-js": "2.11.0", "@stylistic/stylelint-plugin": "3.1.1", @@ -86,7 +87,7 @@ "eslint-import-resolver-typescript": "3.7.0", "eslint-plugin-array-func": "4.0.0", "eslint-plugin-github": "5.1.3", - "eslint-plugin-i": "2.29.1", + "eslint-plugin-import-x": "4.5.0", "eslint-plugin-no-jquery": "3.1.0", "eslint-plugin-no-use-extend-native": "0.5.0", "eslint-plugin-playwright": "2.1.0", @@ -110,8 +111,7 @@ "type-fest": "4.30.0", "updates": "16.4.0", "vite-string-plugin": "1.3.4", - "vitest": "2.1.8", - "vue-tsc": "2.1.10" + "vitest": "2.1.8" }, "browserslist": [ "defaults" diff --git a/routers/api/actions/runner/main_test.go b/routers/api/actions/runner/main_test.go new file mode 100644 index 00000000000..1e80a4f5caf --- /dev/null +++ b/routers/api/actions/runner/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index ff6ec5bd54c..539be8d8890 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -162,28 +162,56 @@ func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[str return nil, fmt.Errorf("FindRunJobs: %w", err) } - ret := make(map[string]*runnerv1.TaskNeed, len(needs)) + jobIDJobs := make(map[string][]*actions_model.ActionRunJob) for _, job := range jobs { - if !needs.Contains(job.JobID) { + jobIDJobs[job.JobID] = append(jobIDJobs[job.JobID], job) + } + + ret := make(map[string]*runnerv1.TaskNeed, len(needs)) + for jobID, jobsWithSameID := range jobIDJobs { + if !needs.Contains(jobID) { continue } - if job.TaskID == 0 || !job.Status.IsDone() { - // it shouldn't happen, or the job has been rerun - continue + var jobOutputs map[string]string + for _, job := range jobsWithSameID { + if job.TaskID == 0 || !job.Status.IsDone() { + // it shouldn't happen, or the job has been rerun + continue + } + got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) + if err != nil { + return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) + } + outputs := make(map[string]string, len(got)) + for _, v := range got { + outputs[v.OutputKey] = v.OutputValue + } + if len(jobOutputs) == 0 { + jobOutputs = outputs + } else { + jobOutputs = mergeTwoOutputs(outputs, jobOutputs) + } } - outputs := make(map[string]string) - got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) - if err != nil { - return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) - } - for _, v := range got { - outputs[v.OutputKey] = v.OutputValue - } - ret[job.JobID] = &runnerv1.TaskNeed{ - Outputs: outputs, - Result: runnerv1.Result(job.Status), + ret[jobID] = &runnerv1.TaskNeed{ + Outputs: jobOutputs, + Result: runnerv1.Result(actions_model.AggregateJobStatus(jobsWithSameID)), } } return ret, nil } + +// mergeTwoOutputs merges two outputs from two different ActionRunJobs +// Values with the same output name may be overridden. The user should ensure the output names are unique. +// See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#using-job-outputs-in-a-matrix-job +func mergeTwoOutputs(o1, o2 map[string]string) map[string]string { + ret := make(map[string]string, len(o1)) + for k1, v1 := range o1 { + if len(v1) > 0 { + ret[k1] = v1 + } else { + ret[k1] = o2[k1] + } + } + return ret +} diff --git a/routers/api/actions/runner/utils_test.go b/routers/api/actions/runner/utils_test.go new file mode 100644 index 00000000000..d7a6f84550f --- /dev/null +++ b/routers/api/actions/runner/utils_test.go @@ -0,0 +1,28 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package runner + +import ( + "context" + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func Test_findTaskNeeds(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 51}) + + ret, err := findTaskNeeds(context.Background(), task) + assert.NoError(t, err) + assert.Len(t, ret, 1) + assert.Contains(t, ret, "job1") + assert.Len(t, ret["job1"].Outputs, 2) + assert.Equal(t, "abc", ret["job1"].Outputs["output_a"]) + assert.Equal(t, "bbb", ret["job1"].Outputs["output_b"]) +} diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 4e194f65fa1..47ea7137b82 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -358,6 +358,7 @@ func CommonRoutes() *web.Router { r.Get("/PACKAGES", cran.EnumerateSourcePackages) r.Get("/PACKAGES{format}", cran.EnumerateSourcePackages) r.Get("/{filename}", cran.DownloadSourcePackageFile) + r.Get("/Archive/{packagename}/{filename}", cran.DownloadSourcePackageFile) }) r.Put("", reqPackageAccess(perm.AccessModeWrite), cran.UploadSourcePackageFile) }) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f28ee980e10..96365e7c14d 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1195,6 +1195,7 @@ func Routes() *web.Router { m.Get("/*", repo.GetBranch) m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch) + m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) m.Group("/branch_protections", func() { m.Get("", repo.ListBranchProtections) diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 2ccc859ecf8..e072d69ea0d 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -386,6 +386,77 @@ func ListBranches(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiBranches) } +// UpdateBranch updates a repository's branch. +func UpdateBranch(ctx *context.APIContext) { + // swagger:operation PATCH /repos/{owner}/{repo}/branches/{branch} repository repoUpdateBranch + // --- + // summary: Update a branch + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: branch + // in: path + // description: name of the branch + // type: string + // required: true + // - name: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateBranchRepoOption" + // responses: + // "204": + // "$ref": "#/responses/empty" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption) + + oldName := ctx.PathParam("*") + repo := ctx.Repo.Repository + + if repo.IsEmpty { + ctx.Error(http.StatusNotFound, "", "Git Repository is empty.") + return + } + + if repo.IsMirror { + ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.") + return + } + + msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name) + if err != nil { + ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + return + } + if msg == "target_exist" { + ctx.Error(http.StatusUnprocessableEntity, "", "Cannot rename a branch using the same name or rename to a branch that already exists.") + return + } + if msg == "from_not_exist" { + ctx.Error(http.StatusNotFound, "", "Branch doesn't exist.") + return + } + + ctx.Status(http.StatusNoContent) +} + // GetBranchProtection gets a branch protection func GetBranchProtection(ctx *context.APIContext) { // swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 38e5330b3ac..1678bc033c6 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -64,22 +64,19 @@ func CompareDiff(ctx *context.APIContext) { } } - _, headGitRepo, ci, _, _ := parseCompareInfo(ctx, api.CreatePullRequestOption{ - Base: infos[0], - Head: infos[1], - }) + compareResult, closer := parseCompareInfo(ctx, api.CreatePullRequestOption{Base: infos[0], Head: infos[1]}) if ctx.Written() { return } - defer headGitRepo.Close() + defer closer() verification := ctx.FormString("verification") == "" || ctx.FormBool("verification") files := ctx.FormString("files") == "" || ctx.FormBool("files") - apiCommits := make([]*api.Commit, 0, len(ci.Commits)) + apiCommits := make([]*api.Commit, 0, len(compareResult.compareInfo.Commits)) userCache := make(map[string]*user_model.User) - for i := 0; i < len(ci.Commits); i++ { - apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, ci.Commits[i], userCache, + for i := 0; i < len(compareResult.compareInfo.Commits); i++ { + apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareResult.compareInfo.Commits[i], userCache, convert.ToCommitOptions{ Stat: true, Verification: verification, @@ -93,7 +90,7 @@ func CompareDiff(ctx *context.APIContext) { } ctx.JSON(http.StatusOK, &api.Compare{ - TotalCommits: len(ci.Commits), + TotalCommits: len(compareResult.compareInfo.Commits), Commits: apiCommits, }) } diff --git a/routers/api/v1/repo/issue_dependency.go b/routers/api/v1/repo/issue_dependency.go index 712c71a6823..ae7502c661b 100644 --- a/routers/api/v1/repo/issue_dependency.go +++ b/routers/api/v1/repo/issue_dependency.go @@ -338,7 +338,7 @@ func GetIssueBlocks(ctx *context.APIContext) { } skip := (page - 1) * limit - max := page * limit + maxNum := page * limit deps, err := issue.BlockingDependencies(ctx) if err != nil { @@ -352,7 +352,7 @@ func GetIssueBlocks(ctx *context.APIContext) { repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission for i, depMeta := range deps { - if i < skip || i >= max { + if i < skip || i >= maxNum { continue } diff --git a/routers/api/v1/repo/issue_label.go b/routers/api/v1/repo/issue_label.go index 2f5ea8931b1..cc517619e97 100644 --- a/routers/api/v1/repo/issue_label.go +++ b/routers/api/v1/repo/issue_label.go @@ -319,6 +319,11 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) return nil, nil, err } + if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { + ctx.Error(http.StatusForbidden, "CanWriteIssuesOrPulls", "write permission is required") + return nil, nil, fmt.Errorf("permission denied") + } + var ( labelIDs []int64 labelNames []string @@ -350,10 +355,5 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) return nil, nil, err } - if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { - ctx.Status(http.StatusForbidden) - return nil, nil, nil - } - return issue, labels, err } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 2e7e9a16c3e..e9e303ac134 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -139,43 +139,12 @@ func ListPullRequests(ctx *context.APIContext) { return } - apiPrs := make([]*api.PullRequest, len(prs)) - // NOTE: load repository first, so that issue.Repo will be filled with pr.BaseRepo - if err := prs.LoadRepositories(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadRepositories", err) - return - } - issueList, err := prs.LoadIssues(ctx) + apiPrs, err := convert.ToAPIPullRequests(ctx, ctx.Repo.Repository, prs, ctx.Doer) if err != nil { - ctx.Error(http.StatusInternalServerError, "LoadIssues", err) + ctx.Error(http.StatusInternalServerError, "ToAPIPullRequests", err) return } - if err := issueList.LoadLabels(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadLabels", err) - return - } - if err := issueList.LoadPosters(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadPoster", err) - return - } - if err := issueList.LoadAttachments(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) - return - } - if err := issueList.LoadMilestones(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadMilestones", err) - return - } - if err := issueList.LoadAssignees(ctx); err != nil { - ctx.Error(http.StatusInternalServerError, "LoadAssignees", err) - return - } - - for i := range prs { - apiPrs[i] = convert.ToAPIPullRequest(ctx, prs[i], ctx.Doer) - } - ctx.SetLinkHeader(int(maxResults), listOptions.PageSize) ctx.SetTotalCountHeader(maxResults) ctx.JSON(http.StatusOK, &apiPrs) @@ -420,8 +389,7 @@ func CreatePullRequest(ctx *context.APIContext) { form := *web.GetForm(ctx).(*api.CreatePullRequestOption) if form.Head == form.Base { - ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", - "Invalid PullRequest: There are no changes between the head and the base") + ctx.Error(http.StatusUnprocessableEntity, "BaseHeadSame", "Invalid PullRequest: There are no changes between the head and the base") return } @@ -432,14 +400,22 @@ func CreatePullRequest(ctx *context.APIContext) { ) // Get repo/branch information - headRepo, headGitRepo, compareInfo, baseBranch, headBranch := parseCompareInfo(ctx, form) + compareResult, closer := parseCompareInfo(ctx, form) if ctx.Written() { return } - defer headGitRepo.Close() + defer closer() + + if !compareResult.baseRef.IsBranch() || !compareResult.headRef.IsBranch() { + ctx.Error(http.StatusUnprocessableEntity, "BaseHeadInvalidRefType", "Invalid PullRequest: base and head must be branches") + return + } // Check if another PR exists with the same targets - existingPr, err := issues_model.GetUnmergedPullRequest(ctx, headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, issues_model.PullRequestFlowGithub) + existingPr, err := issues_model.GetUnmergedPullRequest(ctx, compareResult.headRepo.ID, ctx.Repo.Repository.ID, + compareResult.headRef.ShortName(), compareResult.baseRef.ShortName(), + issues_model.PullRequestFlowGithub, + ) if err != nil { if !issues_model.IsErrPullRequestNotExist(err) { ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err) @@ -515,13 +491,13 @@ func CreatePullRequest(ctx *context.APIContext) { DeadlineUnix: deadlineUnix, } pr := &issues_model.PullRequest{ - HeadRepoID: headRepo.ID, + HeadRepoID: compareResult.headRepo.ID, BaseRepoID: repo.ID, - HeadBranch: headBranch, - BaseBranch: baseBranch, - HeadRepo: headRepo, + HeadBranch: compareResult.headRef.ShortName(), + BaseBranch: compareResult.baseRef.ShortName(), + HeadRepo: compareResult.headRepo, BaseRepo: repo, - MergeBase: compareInfo.MergeBase, + MergeBase: compareResult.compareInfo.MergeBase, Type: issues_model.PullRequestGitea, } @@ -1104,32 +1080,32 @@ func MergePullRequest(ctx *context.APIContext) { ctx.Status(http.StatusOK) } -func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (*repo_model.Repository, *git.Repository, *git.CompareInfo, string, string) { - baseRepo := ctx.Repo.Repository +type parseCompareInfoResult struct { + headRepo *repo_model.Repository + headGitRepo *git.Repository + compareInfo *git.CompareInfo + baseRef git.RefName + headRef git.RefName +} +// parseCompareInfo returns non-nil if it succeeds, it always writes to the context and returns nil if it fails +func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) (result *parseCompareInfoResult, closer func()) { + var err error // Get compared branches information // format: ...[:] // base<-head: master...head:feature // same repo: master...feature + baseRepo := ctx.Repo.Repository + baseRefToGuess := form.Base - // TODO: Validate form first? - - baseBranch := form.Base - - var ( - headUser *user_model.User - headBranch string - isSameRepo bool - err error - ) - - // If there is no head repository, it means pull request between same repository. - headInfos := strings.Split(form.Head, ":") - if len(headInfos) == 1 { - isSameRepo = true - headUser = ctx.Repo.Owner - headBranch = headInfos[0] + headUser := ctx.Repo.Owner + headRefToGuess := form.Head + if headInfos := strings.Split(form.Head, ":"); len(headInfos) == 1 { + // If there is no head repository, it means pull request between same repository. + // Do nothing here because the head variables have been assigned above. } else if len(headInfos) == 2 { + // There is a head repository (the head repository could also be the same base repo) + headRefToGuess = headInfos[1] headUser, err = user_model.GetUserByName(ctx, headInfos[0]) if err != nil { if user_model.IsErrUserNotExist(err) { @@ -1137,38 +1113,29 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) } else { ctx.Error(http.StatusInternalServerError, "GetUserByName", err) } - return nil, nil, nil, "", "" + return nil, nil } - headBranch = headInfos[1] - // The head repository can also point to the same repo - isSameRepo = ctx.Repo.Owner.ID == headUser.ID } else { ctx.NotFound() - return nil, nil, nil, "", "" + return nil, nil } - ctx.Repo.PullRequest.SameRepo = isSameRepo - log.Trace("Repo path: %q, base branch: %q, head branch: %q", ctx.Repo.GitRepo.Path, baseBranch, headBranch) - // Check if base branch is valid. - if !ctx.Repo.GitRepo.IsBranchExist(baseBranch) && !ctx.Repo.GitRepo.IsTagExist(baseBranch) { - ctx.NotFound("BaseNotExist") - return nil, nil, nil, "", "" - } + isSameRepo := ctx.Repo.Owner.ID == headUser.ID // Check if current user has fork of repository or in the same repository. headRepo := repo_model.GetForkedRepo(ctx, headUser.ID, baseRepo.ID) if headRepo == nil && !isSameRepo { - err := baseRepo.GetBaseRepo(ctx) + err = baseRepo.GetBaseRepo(ctx) if err != nil { ctx.Error(http.StatusInternalServerError, "GetBaseRepo", err) - return nil, nil, nil, "", "" + return nil, nil } // Check if baseRepo's base repository is the same as headUser's repository. if baseRepo.BaseRepo == nil || baseRepo.BaseRepo.OwnerID != headUser.ID { log.Trace("parseCompareInfo[%d]: does not have fork or in same repository", baseRepo.ID) ctx.NotFound("GetBaseRepo") - return nil, nil, nil, "", "" + return nil, nil } // Assign headRepo so it can be used below. headRepo = baseRepo.BaseRepo @@ -1178,67 +1145,68 @@ func parseCompareInfo(ctx *context.APIContext, form api.CreatePullRequestOption) if isSameRepo { headRepo = ctx.Repo.Repository headGitRepo = ctx.Repo.GitRepo + closer = func() {} // no need to close the head repo because it shares the base repo } else { headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo) if err != nil { ctx.Error(http.StatusInternalServerError, "OpenRepository", err) - return nil, nil, nil, "", "" + return nil, nil } + closer = func() { _ = headGitRepo.Close() } } + defer func() { + if result == nil && !isSameRepo { + _ = headGitRepo.Close() + } + }() // user should have permission to read baseRepo's codes and pulls, NOT headRepo's permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer) if err != nil { - headGitRepo.Close() ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) - return nil, nil, nil, "", "" - } - if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) { - if log.IsTrace() { - log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", - ctx.Doer, - baseRepo, - permBase) - } - headGitRepo.Close() - ctx.NotFound("Can't read pulls or can't read UnitTypeCode") - return nil, nil, nil, "", "" + return nil, nil } - // user should have permission to read headrepo's codes + if !permBase.CanReadIssuesOrPulls(true) || !permBase.CanRead(unit.TypeCode) { + log.Trace("Permission Denied: User %-v cannot create/read pull requests or cannot read code in Repo %-v\nUser in baseRepo has Permissions: %-+v", ctx.Doer, baseRepo, permBase) + ctx.NotFound("Can't read pulls or can't read UnitTypeCode") + return nil, nil + } + + // user should have permission to read headRepo's codes + // TODO: could the logic be simplified if the headRepo is the same as the baseRepo? Need to think more about it. permHead, err := access_model.GetUserRepoPermission(ctx, headRepo, ctx.Doer) if err != nil { - headGitRepo.Close() ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) - return nil, nil, nil, "", "" + return nil, nil } if !permHead.CanRead(unit.TypeCode) { - if log.IsTrace() { - log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", - ctx.Doer, - headRepo, - permHead) - } - headGitRepo.Close() + log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v", ctx.Doer, headRepo, permHead) ctx.NotFound("Can't read headRepo UnitTypeCode") - return nil, nil, nil, "", "" + return nil, nil } - // Check if head branch is valid. - if !headGitRepo.IsBranchExist(headBranch) && !headGitRepo.IsTagExist(headBranch) { - headGitRepo.Close() + baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefToGuess) + headRef := headGitRepo.UnstableGuessRefByShortName(headRefToGuess) + + log.Trace("Repo path: %q, base ref: %q->%q, head ref: %q->%q", ctx.Repo.GitRepo.Path, baseRefToGuess, baseRef, headRefToGuess, headRef) + + baseRefValid := baseRef.IsBranch() || baseRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(ctx.Repo.Repository.ObjectFormatName), baseRef.ShortName()) + headRefValid := headRef.IsBranch() || headRef.IsTag() || git.IsStringLikelyCommitID(git.ObjectFormatFromName(headRepo.ObjectFormatName), headRef.ShortName()) + // Check if base&head ref are valid. + if !baseRefValid || !headRefValid { ctx.NotFound() - return nil, nil, nil, "", "" + return nil, nil } - compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseBranch, headBranch, false, false) + compareInfo, err := headGitRepo.GetCompareInfo(repo_model.RepoPath(baseRepo.Owner.Name, baseRepo.Name), baseRef.ShortName(), headRef.ShortName(), false, false) if err != nil { - headGitRepo.Close() ctx.Error(http.StatusInternalServerError, "GetCompareInfo", err) - return nil, nil, nil, "", "" + return nil, nil } - return headRepo, headGitRepo, compareInfo, baseBranch, headBranch + result = &parseCompareInfoResult{headRepo: headRepo, headGitRepo: headGitRepo, compareInfo: compareInfo, baseRef: baseRef, headRef: headRef} + return result, closer } // UpdatePullRequest merge PR's baseBranch into headBranch diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index c7065c1d9df..f9906ed250d 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -308,7 +308,7 @@ func ListWikiPages(ctx *context.APIContext) { } skip := (page - 1) * limit - max := page * limit + maxNum := page * limit entries, err := commit.ListEntries() if err != nil { @@ -317,7 +317,7 @@ func ListWikiPages(ctx *context.APIContext) { } pages := make([]*api.WikiPageMetaData, 0, len(entries)) for i, entry := range entries { - if i < skip || i >= max || !entry.IsRegular() { + if i < skip || i >= maxNum || !entry.IsRegular() { continue } c, err := wikiRepo.GetCommitByPath(entry.Name()) diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 39c98c666e5..125605d98f5 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -90,6 +90,8 @@ type swaggerParameterBodies struct { // in:body EditRepoOption api.EditRepoOption // in:body + UpdateBranchRepoOption api.UpdateBranchRepoOption + // in:body TransferRepoOption api.TransferRepoOption // in:body CreateForkOption api.CreateForkOption diff --git a/routers/private/hook_post_receive_test.go b/routers/private/hook_post_receive_test.go index 32c78bd009d..34722f910d9 100644 --- a/routers/private/hook_post_receive_test.go +++ b/routers/private/hook_post_receive_test.go @@ -39,7 +39,7 @@ func TestHandlePullRequestMerging(t *testing.T) { }, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, []*repo_module.PushUpdateOptions{ {NewCommitID: "01234567"}, }) - assert.Equal(t, 0, len(resp.Body.String())) + assert.Empty(t, resp.Body.String()) pr, err = issues_model.GetPullRequestByID(db.DefaultContext, pr.ID) assert.NoError(t, err) assert.True(t, pr.HasMerged) diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 37e94aa8023..46e302d634a 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -26,9 +26,9 @@ func generateMockStepsLog(logCur actions.LogCursor) (stepsLog []*actions.ViewSte "::endgroup::", "message for: step={step}, cursor={cursor}", "message for: step={step}, cursor={cursor}", - "message for: step={step}, cursor={cursor}", - "message for: step={step}, cursor={cursor}", - "message for: step={step}, cursor={cursor}", + "##[group]test group for: step={step}, cursor={cursor}", + "in group msg for: step={step}, cursor={cursor}", + "##[endgroup]", } cur := logCur.Cursor // usually the cursor is the "file offset", but here we abuse it as "line number" to make the mock easier, intentionally for i := 0; i < util.Iif(logCur.Step == 0, 3, 1); i++ { @@ -52,6 +52,10 @@ func MockActionsRunsJobs(ctx *context.Context) { req := web.GetForm(ctx).(*actions.ViewRequest) resp := &actions.ViewResponse{} + resp.State.Run.TitleHTML = `mock run title link` + resp.State.Run.Status = actions_model.StatusRunning.String() + resp.State.Run.CanCancel = true + resp.State.Run.CanDeleteArtifact = true resp.Artifacts = append(resp.Artifacts, &actions.ArtifactsViewItem{ Name: "artifact-a", Size: 100 * 1024, diff --git a/routers/web/org/projects.go b/routers/web/org/projects.go index 6dfefbf68d2..3b9ec2a7b85 100644 --- a/routers/web/org/projects.go +++ b/routers/web/org/projects.go @@ -21,6 +21,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/web/shared/issue" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -334,28 +335,15 @@ func ViewProject(ctx *context.Context) { return } - var labelIDs []int64 - // 1,-2 means including label 1 and excluding label 2 - // 0 means issues with no label - // blank means labels will not be filtered for issues - selectLabels := ctx.FormString("labels") - if selectLabels == "" { - ctx.Data["AllLabels"] = true - } else if selectLabels == "0" { - ctx.Data["NoLabel"] = true + labelIDs := issue.PrepareFilterIssueLabels(ctx, project.RepoID, project.Owner) + if ctx.Written() { + return } - if len(selectLabels) > 0 { - labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) - if err != nil { - ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) - } - } - - assigneeID := ctx.FormInt64("assignee") + assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{ LabelIDs: labelIDs, - AssigneeID: assigneeID, + AssigneeID: optional.Some(assigneeID), }) if err != nil { ctx.ServerError("LoadIssuesOfColumns", err) @@ -431,8 +419,6 @@ func ViewProject(ctx *context.Context) { return } ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - - ctx.Data["SelectLabels"] = selectLabels ctx.Data["AssigneeID"] = assigneeID project.RenderedContent = templates.NewRenderUtils(ctx).MarkdownToHtml(project.Description) diff --git a/routers/web/repo/actions/actions.go b/routers/web/repo/actions/actions.go index ad16b9fb4e4..1de18359365 100644 --- a/routers/web/repo/actions/actions.go +++ b/routers/web/repo/actions/actions.go @@ -5,6 +5,7 @@ package actions import ( "bytes" + stdCtx "context" "fmt" "net/http" "slices" @@ -245,6 +246,10 @@ func List(ctx *context.Context) { return } + if err := loadIsRefDeleted(ctx, ctx.Repo.Repository.ID, runs); err != nil { + log.Error("LoadIsRefDeleted", err) + } + ctx.Data["Runs"] = runs actors, err := actions_model.GetActors(ctx, ctx.Repo.Repository.ID) @@ -267,6 +272,34 @@ func List(ctx *context.Context) { ctx.HTML(http.StatusOK, tplListActions) } +// loadIsRefDeleted loads the IsRefDeleted field for each run in the list. +// TODO: move this function to models/actions/run_list.go but now it will result in a circular import. +func loadIsRefDeleted(ctx stdCtx.Context, repoID int64, runs actions_model.RunList) error { + branches := make(container.Set[string], len(runs)) + for _, run := range runs { + refName := git.RefName(run.Ref) + if refName.IsBranch() { + branches.Add(refName.ShortName()) + } + } + if len(branches) == 0 { + return nil + } + + branchInfos, err := git_model.GetBranches(ctx, repoID, branches.Values(), false) + if err != nil { + return err + } + branchSet := git_model.BranchesToNamesSet(branchInfos) + for _, run := range runs { + refName := git.RefName(run.Ref) + if refName.IsBranch() && !branchSet.Contains(refName.ShortName()) { + run.IsRefDeleted = true + } + } + return nil +} + type WorkflowDispatchInput struct { Name string `yaml:"name"` Description string `yaml:"description"` diff --git a/routers/web/repo/actions/actions_test.go b/routers/web/repo/actions/actions_test.go index 194704d14eb..6a976ed65c0 100644 --- a/routers/web/repo/actions/actions_test.go +++ b/routers/web/repo/actions/actions_test.go @@ -7,6 +7,10 @@ import ( "strings" "testing" + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + unittest "code.gitea.io/gitea/models/unittest" + act_model "github.com/nektos/act/pkg/model" "github.com/stretchr/testify/assert" ) @@ -154,3 +158,21 @@ func TestReadWorkflow_WorkflowDispatchConfig(t *testing.T) { Type: "boolean", }, workflowDispatch.Inputs[2]) } + +func Test_loadIsRefDeleted(t *testing.T) { + unittest.PrepareTestEnv(t) + + runs, total, err := db.FindAndCount[actions_model.ActionRun](db.DefaultContext, + actions_model.FindRunOptions{RepoID: 4, Ref: "refs/heads/test"}) + assert.NoError(t, err) + assert.Len(t, runs, 1) + assert.EqualValues(t, 1, total) + for _, run := range runs { + assert.False(t, run.IsRefDeleted) + } + + assert.NoError(t, loadIsRefDeleted(db.DefaultContext, 4, runs)) + for _, run := range runs { + assert.True(t, run.IsRefDeleted) + } +} diff --git a/routers/web/repo/actions/main_test.go b/routers/web/repo/actions/main_test.go new file mode 100644 index 00000000000..a82f9c6672e --- /dev/null +++ b/routers/web/repo/actions/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" +) + +func TestMain(m *testing.M) { + unittest.MainTest(m) +} diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 0f0d7d1ebdc..b711038da06 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -9,6 +9,7 @@ import ( "context" "errors" "fmt" + "html/template" "io" "net/http" "net/url" @@ -18,6 +19,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -29,6 +31,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" @@ -87,19 +90,20 @@ type ViewResponse struct { State struct { Run struct { - Link string `json:"link"` - Title string `json:"title"` - Status string `json:"status"` - CanCancel bool `json:"canCancel"` - CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve - CanRerun bool `json:"canRerun"` - CanDeleteArtifact bool `json:"canDeleteArtifact"` - Done bool `json:"done"` - WorkflowID string `json:"workflowID"` - WorkflowLink string `json:"workflowLink"` - IsSchedule bool `json:"isSchedule"` - Jobs []*ViewJob `json:"jobs"` - Commit ViewCommit `json:"commit"` + Link string `json:"link"` + Title string `json:"title"` + TitleHTML template.HTML `json:"titleHTML"` + Status string `json:"status"` + CanCancel bool `json:"canCancel"` + CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve + CanRerun bool `json:"canRerun"` + CanDeleteArtifact bool `json:"canDeleteArtifact"` + Done bool `json:"done"` + WorkflowID string `json:"workflowID"` + WorkflowLink string `json:"workflowLink"` + IsSchedule bool `json:"isSchedule"` + Jobs []*ViewJob `json:"jobs"` + Commit ViewCommit `json:"commit"` } `json:"run"` CurrentJob struct { Title string `json:"title"` @@ -133,8 +137,9 @@ type ViewUser struct { } type ViewBranch struct { - Name string `json:"name"` - Link string `json:"link"` + Name string `json:"name"` + Link string `json:"link"` + IsDeleted bool `json:"isDeleted"` } type ViewJobStep struct { @@ -200,7 +205,10 @@ func ViewPost(ctx *context_module.Context) { } } + metas := ctx.Repo.Repository.ComposeMetas(ctx) + resp.State.Run.Title = run.Title + resp.State.Run.TitleHTML = templates.NewRenderUtils(ctx).RenderCommitMessage(run.Title, metas) resp.State.Run.Link = run.Link() resp.State.Run.CanCancel = !run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions) @@ -230,6 +238,16 @@ func ViewPost(ctx *context_module.Context) { Name: run.PrettyRef(), Link: run.RefLink(), } + refName := git.RefName(run.Ref) + if refName.IsBranch() { + b, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, refName.ShortName()) + if err != nil && !git_model.IsErrBranchNotExist(err) { + log.Error("GetBranch: %v", err) + } else if git_model.IsErrBranchNotExist(err) || (b != nil && b.IsDeleted) { + branch.IsDeleted = true + } + } + resp.State.Run.Commit = ViewCommit{ ShortSha: base.ShortSha(run.CommitSHA), Link: fmt.Sprintf("%s/commit/%s", run.Repo.Link(), run.CommitSHA), diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 51da80e4d5b..ad790875136 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -114,12 +114,6 @@ func RefBlame(ctx *context.Context) { ctx.Data["UsesIgnoreRevs"] = result.UsesIgnoreRevs ctx.Data["FaultyIgnoreRevsFile"] = result.FaultyIgnoreRevsFile - // Get Topics of this repo - renderRepoTopics(ctx) - if ctx.Written() { - return - } - commitNames := processBlameParts(ctx, result.Parts) if ctx.Written() { return diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index c56b55bf157..97e7bab0866 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -89,7 +89,6 @@ func Branches(ctx *context.Context) { pager := context.NewPagination(int(branchesCount), pageSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager - ctx.Data["LicenseFileName"] = repo_service.LicenseFileName ctx.HTML(http.StatusOK, tplBranch) } @@ -260,3 +259,20 @@ func CreateBranch(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("repo.branch.create_success", form.NewBranchName)) ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(form.NewBranchName) + "/" + util.PathEscapeSegments(form.CurrentPath)) } + +func MergeUpstream(ctx *context.Context) { + branchName := ctx.FormString("branch") + _, err := repo_service.MergeUpstream(ctx, ctx.Doer, ctx.Repo.Repository, branchName) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.JSONError(ctx.Tr("error.not_found")) + return + } else if models.IsErrMergeConflicts(err) { + ctx.JSONError(ctx.Tr("repo.pulls.merge_conflict")) + return + } + ctx.ServerError("MergeUpstream", err) + return + } + ctx.JSONRedirect("") +} diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index c5652784fa0..6d53df7c109 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -102,7 +102,6 @@ func Commits(ctx *context.Context) { pager := context.NewPagination(int(commitsCount), pageSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager - ctx.Data["LicenseFileName"] = repo_service.LicenseFileName ctx.HTML(http.StatusOK, tplCommits) } @@ -219,8 +218,6 @@ func SearchCommits(ctx *context.Context) { } ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name - ctx.Data["RefName"] = ctx.Repo.RefName - ctx.Data["LicenseFileName"] = repo_service.LicenseFileName ctx.HTML(http.StatusOK, tplCommits) } @@ -266,7 +263,6 @@ func FileHistory(ctx *context.Context) { pager := context.NewPagination(int(commitsCount), setting.Git.CommitsRangeSize, page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager - ctx.Data["LicenseFileName"] = repo_service.LicenseFileName ctx.HTML(http.StatusOK, tplCommits) } diff --git a/routers/web/repo/fork.go b/routers/web/repo/fork.go index 27e42a8f98e..86af7056176 100644 --- a/routers/web/repo/fork.go +++ b/routers/web/repo/fork.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" @@ -48,7 +49,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { ctx.Data["repo_name"] = forkRepo.Name ctx.Data["description"] = forkRepo.Description ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate - canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID) + canForkToUser := repository.CanUserForkBetweenOwners(forkRepo.OwnerID, ctx.Doer.ID) && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID) ctx.Data["ForkRepo"] = forkRepo @@ -66,7 +67,7 @@ func getForkRepository(ctx *context.Context) *repo_model.Repository { traverseParentRepo := forkRepo for { - if ctx.Doer.ID == traverseParentRepo.OwnerID { + if !repository.CanUserForkBetweenOwners(ctx.Doer.ID, traverseParentRepo.OwnerID) { canForkToUser = false } else { for i, org := range orgs { @@ -162,7 +163,7 @@ func ForkPost(ctx *context.Context) { var err error traverseParentRepo := forkRepo for { - if ctxUser.ID == traverseParentRepo.OwnerID { + if !repository.CanUserForkBetweenOwners(ctxUser.ID, traverseParentRepo.OwnerID) { ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form) return } diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 833f59981b4..5397411b59c 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -9,7 +9,6 @@ import ( "fmt" "html/template" "net/http" - "net/url" "strconv" "strings" @@ -114,7 +113,6 @@ func MustAllowPulls(ctx *context.Context) { // User can send pull request if owns a forked repository. if ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) { ctx.Repo.PullRequest.Allowed = true - ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Doer.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName) } } diff --git a/routers/web/repo/issue_list.go b/routers/web/repo/issue_list.go index 50bb6684330..ff98bf8ec8e 100644 --- a/routers/web/repo/issue_list.go +++ b/routers/web/repo/issue_list.go @@ -7,7 +7,6 @@ import ( "bytes" "fmt" "net/http" - "net/url" "strconv" "strings" @@ -18,12 +17,12 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/base" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/web/shared/issue" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" @@ -264,8 +263,10 @@ func getUserIDForFilter(ctx *context.Context, queryName string) int64 { return user.ID } -// ListIssues list the issues of a repository -func ListIssues(ctx *context.Context) { +// SearchRepoIssuesJSON lists the issues of a repository +// This function was copied from API (decouple the web and API routes), +// it is only used by frontend to search some dependency or related issues +func SearchRepoIssuesJSON(ctx *context.Context) { before, since, err := context.GetQueryBeforeSince(ctx.Base) if err != nil { ctx.Error(http.StatusUnprocessableEntity, err.Error()) @@ -287,20 +288,11 @@ func ListIssues(ctx *context.Context) { keyword = "" } - var labelIDs []int64 - if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 { - labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted) - if err != nil { - ctx.Error(http.StatusInternalServerError, err.Error()) - return - } - } - var mileIDs []int64 if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 { for i := range part { // uses names and fall back to ids - // non existent milestones are discarded + // non-existent milestones are discarded mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i]) if err == nil { mileIDs = append(mileIDs, mile.ID) @@ -371,17 +363,8 @@ func ListIssues(ctx *context.Context) { if before != 0 { searchOpt.UpdatedBeforeUnix = optional.Some(before) } - if len(labelIDs) == 1 && labelIDs[0] == 0 { - searchOpt.NoLabelOnly = true - } else { - for _, labelID := range labelIDs { - if labelID > 0 { - searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID) - } else { - searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID) - } - } - } + + // TODO: the "labels" query parameter is never used, so no need to handle it if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID { searchOpt.MilestoneIDs = []int64{0} @@ -505,18 +488,15 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt viewType = "all" } - var ( - assigneeID = ctx.FormInt64("assignee") - posterID = ctx.FormInt64("poster") - mentionedID int64 - reviewRequestedID int64 - reviewedID int64 - ) + assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future + posterUsername := ctx.FormString("poster") + posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername) + var mentionedID, reviewRequestedID, reviewedID int64 if ctx.IsSigned { switch viewType { case "created_by": - posterID = ctx.Doer.ID + posterUserID = optional.Some(ctx.Doer.ID) case "mentioned": mentionedID = ctx.Doer.ID case "assigned": @@ -529,23 +509,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt } repo := ctx.Repo.Repository - var labelIDs []int64 - // 1,-2 means including label 1 and excluding label 2 - // 0 means issues with no label - // blank means labels will not be filtered for issues - selectLabels := ctx.FormString("labels") - if selectLabels == "" { - ctx.Data["AllLabels"] = true - } else if selectLabels == "0" { - ctx.Data["NoLabel"] = true - } - if len(selectLabels) > 0 { - labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) - if err != nil { - ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) - } - } - keyword := strings.Trim(ctx.FormString("q"), " ") if bytes.Contains([]byte(keyword), []byte{0x00}) { keyword = "" @@ -556,15 +519,20 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt mileIDs = []int64{milestoneID} } + labelIDs := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner) + if ctx.Written() { + return + } + var issueStats *issues_model.IssueStats statsOpts := &issues_model.IssuesOptions{ RepoIDs: []int64{repo.ID}, LabelIDs: labelIDs, MilestoneIDs: mileIDs, ProjectID: projectID, - AssigneeID: assigneeID, + AssigneeID: optional.Some(assigneeID), MentionedID: mentionedID, - PosterID: posterID, + PosterID: posterUserID, ReviewRequestedID: reviewRequestedID, ReviewedID: reviewedID, IsPull: isPullOption, @@ -619,8 +587,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt ctx.Data["TotalTrackedTime"] = totalTrackedTime } - archived := ctx.FormBool("archived") - page := ctx.FormInt("page") if page <= 1 { page = 1 @@ -645,8 +611,8 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt PageSize: setting.UI.IssuePagingNum, }, RepoIDs: []int64{repo.ID}, - AssigneeID: assigneeID, - PosterID: posterID, + AssigneeID: optional.Some(assigneeID), + PosterID: posterUserID, MentionedID: mentionedID, ReviewRequestedID: reviewRequestedID, ReviewedID: reviewedID, @@ -720,49 +686,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt return } - labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{}) - if err != nil { - ctx.ServerError("GetLabelsByRepoID", err) - return - } - - if repo.Owner.IsOrganization() { - orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) - if err != nil { - ctx.ServerError("GetLabelsByOrgID", err) - return - } - - ctx.Data["OrgLabels"] = orgLabels - labels = append(labels, orgLabels...) - } - - // Get the exclusive scope for every label ID - labelExclusiveScopes := make([]string, 0, len(labelIDs)) - for _, labelID := range labelIDs { - foundExclusiveScope := false - for _, label := range labels { - if label.ID == labelID || label.ID == -labelID { - labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope()) - foundExclusiveScope = true - break - } - } - if !foundExclusiveScope { - labelExclusiveScopes = append(labelExclusiveScopes, "") - } - } - - for _, l := range labels { - l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes) - } - ctx.Data["Labels"] = labels - ctx.Data["NumLabels"] = len(labels) - - if ctx.FormInt64("assignee") == 0 { - assigneeID = 0 // Reset ID to prevent unexpected selection of assignee. - } - ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink) ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 { @@ -795,29 +718,20 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt return } + showArchivedLabels := ctx.FormBool("archived_labels") + ctx.Data["ShowArchivedLabels"] = showArchivedLabels ctx.Data["PinnedIssues"] = pinned ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) ctx.Data["IssueStats"] = issueStats ctx.Data["OpenCount"] = issueStats.OpenCount ctx.Data["ClosedCount"] = issueStats.ClosedCount - linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t" - ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link, - url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels), - milestoneID, projectID, assigneeID, posterID, archived) - ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link, - url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels), - milestoneID, projectID, assigneeID, posterID, archived) - ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link, - url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels), - milestoneID, projectID, assigneeID, posterID, archived) ctx.Data["SelLabelIDs"] = labelIDs - ctx.Data["SelectLabels"] = selectLabels ctx.Data["ViewType"] = viewType ctx.Data["SortType"] = sortType ctx.Data["MilestoneID"] = milestoneID ctx.Data["ProjectID"] = projectID ctx.Data["AssigneeID"] = assigneeID - ctx.Data["PosterID"] = posterID + ctx.Data["PosterUsername"] = posterUsername ctx.Data["Keyword"] = keyword ctx.Data["IsShowClosed"] = isShowClosed switch { @@ -828,19 +742,7 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption opt default: ctx.Data["State"] = "open" } - ctx.Data["ShowArchivedLabels"] = archived - - pager.AddParamString("q", keyword) - pager.AddParamString("type", viewType) - pager.AddParamString("sort", sortType) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) - pager.AddParamString("labels", fmt.Sprint(selectLabels)) - pager.AddParamString("milestone", fmt.Sprint(milestoneID)) - pager.AddParamString("project", fmt.Sprint(projectID)) - pager.AddParamString("assignee", fmt.Sprint(assigneeID)) - pager.AddParamString("poster", fmt.Sprint(posterID)) - pager.AddParamString("archived", fmt.Sprint(archived)) - + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager } diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index 7eda6e3c736..b536b04d7c6 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -195,7 +195,9 @@ func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) { var reviews issues_model.ReviewList if d.Issue == nil { - posterID = ctx.Doer.ID + if ctx.Doer != nil { + posterID = ctx.Doer.ID + } } else { posterID = d.Issue.PosterID if d.Issue.OriginalAuthorID > 0 { diff --git a/routers/web/repo/issue_view.go b/routers/web/repo/issue_view.go index 54ff36db492..09b57f4e783 100644 --- a/routers/web/repo/issue_view.go +++ b/routers/web/repo/issue_view.go @@ -325,14 +325,6 @@ func ViewIssue(ctx *context.Context) { ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) } - if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { - ctx.Data["IssueDependencySearchType"] = "pulls" - } else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) { - ctx.Data["IssueDependencySearchType"] = "issues" - } else { - ctx.Data["IssueDependencySearchType"] = "all" - } - ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects) ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -349,46 +341,6 @@ func ViewIssue(ctx *context.Context) { ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title)) - iw := new(issues_model.IssueWatch) - if ctx.Doer != nil { - iw.UserID = ctx.Doer.ID - iw.IssueID = issue.ID - iw.IsWatching, err = issues_model.CheckIssueWatch(ctx, ctx.Doer, issue) - if err != nil { - ctx.ServerError("CheckIssueWatch", err) - return - } - } - ctx.Data["IssueWatch"] = iw - rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) - issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - - repo := ctx.Repo.Repository - - // Get more information if it's a pull request. - if issue.IsPull { - if issue.PullRequest.HasMerged { - ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged - PrepareMergedViewPullInfo(ctx, issue) - } else { - PrepareViewPullInfo(ctx, issue) - ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed - } - if ctx.Written() { - return - } - } - - pageMetaData := retrieveRepoIssueMetaData(ctx, repo, issue, issue.IsPull) - if ctx.Written() { - return - } - pageMetaData.LabelsData.SetSelectedLabels(issue.Labels) - if ctx.IsSigned { // Update issue-user. if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil { @@ -397,52 +349,219 @@ func ViewIssue(ctx *context.Context) { } } - var ( - role issues_model.RoleDescriptor - ok bool - marked = make(map[int64]issues_model.RoleDescriptor) - comment *issues_model.Comment - participants = make([]*user_model.User, 1, 10) - latestCloseCommentID int64 - ) - if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { - if ctx.IsSigned { - // Deal with the stopwatch - ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx, ctx.Doer.ID, issue.ID) - if !ctx.Data["IsStopwatchRunning"].(bool) { - var exists bool - var swIssue *issues_model.Issue - if exists, _, swIssue, err = issues_model.HasUserStopwatch(ctx, ctx.Doer.ID); err != nil { - ctx.ServerError("HasUserStopwatch", err) - return - } - ctx.Data["HasUserStopwatch"] = exists - if exists { - // Add warning if the user has already a stopwatch - // Add link to the issue of the already running stopwatch - ctx.Data["OtherStopwatchURL"] = swIssue.Link() - } - } - ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) - } else { - ctx.Data["CanUseTimetracker"] = false - } - if ctx.Data["WorkingUsers"], err = issues_model.TotalTimesForEachUser(ctx, &issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil { - ctx.ServerError("TotalTimesForEachUser", err) + pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, issue, issue.IsPull) + if ctx.Written() { + return + } + pageMetaData.LabelsData.SetSelectedLabels(issue.Labels) + + prepareFuncs := []func(*context.Context, *issues_model.Issue){ + prepareIssueViewContent, + func(ctx *context.Context, issue *issues_model.Issue) { + preparePullViewPullInfo(ctx, issue) + }, + prepareIssueViewCommentsAndSidebarParticipants, + preparePullViewReviewAndMerge, + prepareIssueViewSidebarWatch, + prepareIssueViewSidebarTimeTracker, + prepareIssueViewSidebarDependency, + prepareIssueViewSidebarPin, + } + + for _, prepareFunc := range prepareFuncs { + prepareFunc(ctx, issue) + if ctx.Written() { return } } + // Get more information if it's a pull request. + if issue.IsPull { + if issue.PullRequest.HasMerged { + ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged + } else { + ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed + } + } + + ctx.Data["Reference"] = issue.Ref + ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string)) + ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) + ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) + ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects) + ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) + ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons + ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName() + + tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) + if err != nil { + ctx.ServerError("GetTagNamesByRepoID", err) + return + } + ctx.Data["Tags"] = tags + + ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool { + return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) + } + + ctx.HTML(http.StatusOK, tplIssueView) +} + +func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model.Issue) { + if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) { + ctx.Data["IssueDependencySearchType"] = "pulls" + } else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) { + ctx.Data["IssueDependencySearchType"] = "issues" + } else { + ctx.Data["IssueDependencySearchType"] = "all" + } + // Check if the user can use the dependencies ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, issue.IsPull) // check if dependencies can be created across repositories ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies - if issue.ShowRole, err = roleDescriptor(ctx, repo, issue.Poster, issue, issue.HasOriginalAuthor()); err != nil { - ctx.ServerError("roleDescriptor", err) + // Get Dependencies + blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{}) + if err != nil { + ctx.ServerError("BlockedByDependencies", err) return } + ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy) + if ctx.Written() { + return + } + + blocking, err := issue.BlockingDependencies(ctx) + if err != nil { + ctx.ServerError("BlockingDependencies", err) + return + } + + ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking) +} + +func preparePullViewSigning(ctx *context.Context, issue *issues_model.Issue) { + if !issue.IsPull { + return + } + pull := issue.PullRequest + ctx.Data["WillSign"] = false + if ctx.Doer != nil { + sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName()) + ctx.Data["WillSign"] = sign + ctx.Data["SigningKey"] = key + if err != nil { + if asymkey_service.IsErrWontSign(err) { + ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason + } else { + ctx.Data["WontSignReason"] = "error" + log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err) + } + } + } else { + ctx.Data["WontSignReason"] = "not_signed_in" + } +} + +func prepareIssueViewSidebarWatch(ctx *context.Context, issue *issues_model.Issue) { + iw := new(issues_model.IssueWatch) + if ctx.Doer != nil { + iw.UserID = ctx.Doer.ID + iw.IssueID = issue.ID + var err error + iw.IsWatching, err = issues_model.CheckIssueWatch(ctx, ctx.Doer, issue) + if err != nil { + ctx.ServerError("CheckIssueWatch", err) + return + } + } + ctx.Data["IssueWatch"] = iw +} + +func prepareIssueViewSidebarTimeTracker(ctx *context.Context, issue *issues_model.Issue) { + if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { + return + } + + if ctx.IsSigned { + // Deal with the stopwatch + ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx, ctx.Doer.ID, issue.ID) + if !ctx.Data["IsStopwatchRunning"].(bool) { + exists, _, swIssue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID) + if err != nil { + ctx.ServerError("HasUserStopwatch", err) + return + } + ctx.Data["HasUserStopwatch"] = exists + if exists { + // Add warning if the user has already a stopwatch + // Add link to the issue of the already running stopwatch + ctx.Data["OtherStopwatchURL"] = swIssue.Link() + } + } + ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer) + } else { + ctx.Data["CanUseTimetracker"] = false + } + var err error + if ctx.Data["WorkingUsers"], err = issues_model.TotalTimesForEachUser(ctx, &issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil { + ctx.ServerError("TotalTimesForEachUser", err) + return + } +} + +func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue, canDelete bool) { + if !issue.IsPull { + return + } + pull := issue.PullRequest + isPullBranchDeletable := canDelete && + pull.HeadRepo != nil && + git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.HeadBranch) && + (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"]) + + if isPullBranchDeletable && pull.HasMerged { + exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch) + if err != nil { + ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) + return + } + + isPullBranchDeletable = !exist + } + ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable +} + +func prepareIssueViewSidebarPin(ctx *context.Context, issue *issues_model.Issue) { + var pinAllowed bool + if !issue.IsPinned() { + var err error + pinAllowed, err = issues_model.IsNewPinAllowed(ctx, issue.RepoID, issue.IsPull) + if err != nil { + ctx.ServerError("IsNewPinAllowed", err) + return + } + } else { + pinAllowed = true + } + + ctx.Data["NewPinAllowed"] = pinAllowed + ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0 +} + +func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue *issues_model.Issue) { + var ( + role issues_model.RoleDescriptor + ok bool + marked = make(map[int64]issues_model.RoleDescriptor) + comment *issues_model.Comment + participants = make([]*user_model.User, 1, 10) + latestCloseCommentID int64 + err error + ) + marked[issue.PosterID] = issue.ShowRole // Render comments and fetch participants. @@ -461,7 +580,7 @@ func ViewIssue(ctx *context.Context) { comment.Issue = issue if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview { - rctx = renderhelper.NewRenderContextRepoComment(ctx, repo) + rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo) comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content) if err != nil { ctx.ServerError("RenderString", err) @@ -474,7 +593,7 @@ func ViewIssue(ctx *context.Context) { continue } - comment.ShowRole, err = roleDescriptor(ctx, repo, comment.Poster, issue, comment.HasOriginalAuthor()) + comment.ShowRole, err = roleDescriptor(ctx, issue.Repo, comment.Poster, issue, comment.HasOriginalAuthor()) if err != nil { ctx.ServerError("roleDescriptor", err) return @@ -537,7 +656,7 @@ func ViewIssue(ctx *context.Context) { } } } else if comment.Type.HasContentSupport() { - rctx = renderhelper.NewRenderContextRepoComment(ctx, repo) + rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo) comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content) if err != nil { ctx.ServerError("RenderString", err) @@ -572,7 +691,7 @@ func ViewIssue(ctx *context.Context) { continue } - c.ShowRole, err = roleDescriptor(ctx, repo, c.Poster, issue, c.HasOriginalAuthor()) + c.ShowRole, err = roleDescriptor(ctx, issue.Repo, c.Poster, issue, c.HasOriginalAuthor()) if err != nil { ctx.ServerError("roleDescriptor", err) return @@ -629,237 +748,6 @@ func ViewIssue(ctx *context.Context) { // Combine multiple label assignments into a single comment combineLabelComments(issue) - getBranchData(ctx, issue) - if issue.IsPull { - pull := issue.PullRequest - pull.Issue = issue - canDelete := false - allowMerge := false - canWriteToHeadRepo := false - - if ctx.IsSigned { - if err := pull.LoadHeadRepo(ctx); err != nil { - log.Error("LoadHeadRepo: %v", err) - } else if pull.HeadRepo != nil { - perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return - } - if perm.CanWrite(unit.TypeCode) { - // Check if branch is not protected - if pull.HeadBranch != pull.HeadRepo.DefaultBranch { - if protected, err := git_model.IsBranchProtected(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil { - log.Error("IsProtectedBranch: %v", err) - } else if !protected { - canDelete = true - ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup" - } - } - canWriteToHeadRepo = true - } - } - - if err := pull.LoadBaseRepo(ctx); err != nil { - log.Error("LoadBaseRepo: %v", err) - } - perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return - } - if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it - canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode) - } - allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer) - if err != nil { - ctx.ServerError("IsUserAllowedToMerge", err) - return - } - - if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { - ctx.ServerError("CanMarkConversation", err) - return - } - } - - ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo - ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo - ctx.Data["AllowMerge"] = allowMerge - - prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests) - if err != nil { - ctx.ServerError("GetUnit", err) - return - } - prConfig := prUnit.PullRequestsConfig() - - ctx.Data["AutodetectManualMerge"] = prConfig.AutodetectManualMerge - - var mergeStyle repo_model.MergeStyle - // Check correct values and select default - if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok || - !prConfig.IsMergeStyleAllowed(ms) { - defaultMergeStyle := prConfig.GetDefaultMergeStyle() - if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok { - mergeStyle = defaultMergeStyle - } else if prConfig.AllowMerge { - mergeStyle = repo_model.MergeStyleMerge - } else if prConfig.AllowRebase { - mergeStyle = repo_model.MergeStyleRebase - } else if prConfig.AllowRebaseMerge { - mergeStyle = repo_model.MergeStyleRebaseMerge - } else if prConfig.AllowSquash { - mergeStyle = repo_model.MergeStyleSquash - } else if prConfig.AllowFastForwardOnly { - mergeStyle = repo_model.MergeStyleFastForwardOnly - } else if prConfig.AllowManualMerge { - mergeStyle = repo_model.MergeStyleManuallyMerged - } - } - - ctx.Data["MergeStyle"] = mergeStyle - - defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle) - if err != nil { - ctx.ServerError("GetDefaultMergeMessage", err) - return - } - ctx.Data["DefaultMergeMessage"] = defaultMergeMessage - ctx.Data["DefaultMergeBody"] = defaultMergeBody - - defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash) - if err != nil { - ctx.ServerError("GetDefaultSquashMergeMessage", err) - return - } - ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage - ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody - - pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) - if err != nil { - ctx.ServerError("LoadProtectedBranch", err) - return - } - - if pb != nil { - pb.Repo = pull.BaseRepo - ctx.Data["ProtectedBranch"] = pb - ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull) - ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull) - ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull) - ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull) - ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull) - ctx.Data["RequireSigned"] = pb.RequireSignedCommits - ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles - ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0 - ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles) - ctx.Data["RequireApprovalsWhitelist"] = pb.EnableApprovalsWhitelist - } - ctx.Data["WillSign"] = false - if ctx.Doer != nil { - sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName()) - ctx.Data["WillSign"] = sign - ctx.Data["SigningKey"] = key - if err != nil { - if asymkey_service.IsErrWontSign(err) { - ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason - } else { - ctx.Data["WontSignReason"] = "error" - log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err) - } - } - } else { - ctx.Data["WontSignReason"] = "not_signed_in" - } - - isPullBranchDeletable := canDelete && - pull.HeadRepo != nil && - git.IsBranchExist(ctx, pull.HeadRepo.RepoPath(), pull.HeadBranch) && - (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"]) - - if isPullBranchDeletable && pull.HasMerged { - exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch) - if err != nil { - ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err) - return - } - - isPullBranchDeletable = !exist - } - ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable - - stillCanManualMerge := func() bool { - if pull.HasMerged || issue.IsClosed || !ctx.IsSigned { - return false - } - if pull.CanAutoMerge() || pull.IsWorkInProgress(ctx) || pull.IsChecking() { - return false - } - if allowMerge && prConfig.AllowManualMerge { - return true - } - - return false - } - - ctx.Data["StillCanManualMerge"] = stillCanManualMerge() - - // Check if there is a pending pr merge - ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID) - if err != nil { - ctx.ServerError("GetScheduledMergeByPullID", err) - return - } - } - - // Get Dependencies - blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{}) - if err != nil { - ctx.ServerError("BlockedByDependencies", err) - return - } - ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy) - if ctx.Written() { - return - } - - blocking, err := issue.BlockingDependencies(ctx) - if err != nil { - ctx.ServerError("BlockingDependencies", err) - return - } - - ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking) - if ctx.Written() { - return - } - - var pinAllowed bool - if !issue.IsPinned() { - pinAllowed, err = issues_model.IsNewPinAllowed(ctx, issue.RepoID, issue.IsPull) - if err != nil { - ctx.ServerError("IsNewPinAllowed", err) - return - } - } else { - pinAllowed = true - } - - ctx.Data["Participants"] = participants - ctx.Data["NumParticipants"] = len(participants) - ctx.Data["Issue"] = issue - ctx.Data["Reference"] = issue.Ref - ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string)) - ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID) - ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) - ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects) - ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) - ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons - ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName() - ctx.Data["NewPinAllowed"] = pinAllowed - ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0 - var hiddenCommentTypes *big.Int if ctx.IsSigned { val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes) @@ -872,23 +760,189 @@ func ViewIssue(ctx *context.Context) { ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool { return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0 } - // For sidebar - PrepareBranchList(ctx) + // prepare for sidebar participants + ctx.Data["Participants"] = participants + ctx.Data["NumParticipants"] = len(participants) +} + +func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Issue) { + getBranchData(ctx, issue) + if !issue.IsPull { + return + } + + pull := issue.PullRequest + pull.Issue = issue + canDelete := false + allowMerge := false + canWriteToHeadRepo := false + + if ctx.IsSigned { + if err := pull.LoadHeadRepo(ctx); err != nil { + log.Error("LoadHeadRepo: %v", err) + } else if pull.HeadRepo != nil { + perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + if perm.CanWrite(unit.TypeCode) { + // Check if branch is not protected + if pull.HeadBranch != pull.HeadRepo.DefaultBranch { + if protected, err := git_model.IsBranchProtected(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil { + log.Error("IsProtectedBranch: %v", err) + } else if !protected { + canDelete = true + ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup" + } + } + canWriteToHeadRepo = true + } + } + + if err := pull.LoadBaseRepo(ctx); err != nil { + log.Error("LoadBaseRepo: %v", err) + } + perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it + canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode) + } + allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer) + if err != nil { + ctx.ServerError("IsUserAllowedToMerge", err) + return + } + + if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil { + ctx.ServerError("CanMarkConversation", err) + return + } + } + + ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo + ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo + ctx.Data["AllowMerge"] = allowMerge + + prUnit, err := issue.Repo.GetUnit(ctx, unit.TypePullRequests) + if err != nil { + ctx.ServerError("GetUnit", err) + return + } + prConfig := prUnit.PullRequestsConfig() + + ctx.Data["AutodetectManualMerge"] = prConfig.AutodetectManualMerge + + var mergeStyle repo_model.MergeStyle + // Check correct values and select default + if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok || + !prConfig.IsMergeStyleAllowed(ms) { + defaultMergeStyle := prConfig.GetDefaultMergeStyle() + if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok { + mergeStyle = defaultMergeStyle + } else if prConfig.AllowMerge { + mergeStyle = repo_model.MergeStyleMerge + } else if prConfig.AllowRebase { + mergeStyle = repo_model.MergeStyleRebase + } else if prConfig.AllowRebaseMerge { + mergeStyle = repo_model.MergeStyleRebaseMerge + } else if prConfig.AllowSquash { + mergeStyle = repo_model.MergeStyleSquash + } else if prConfig.AllowFastForwardOnly { + mergeStyle = repo_model.MergeStyleFastForwardOnly + } else if prConfig.AllowManualMerge { + mergeStyle = repo_model.MergeStyleManuallyMerged + } + } + + ctx.Data["MergeStyle"] = mergeStyle + + defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle) + if err != nil { + ctx.ServerError("GetDefaultMergeMessage", err) + return + } + ctx.Data["DefaultMergeMessage"] = defaultMergeMessage + ctx.Data["DefaultMergeBody"] = defaultMergeBody + + defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash) + if err != nil { + ctx.ServerError("GetDefaultSquashMergeMessage", err) + return + } + ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage + ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody + + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch) + if err != nil { + ctx.ServerError("LoadProtectedBranch", err) + return + } + + if pb != nil { + pb.Repo = pull.BaseRepo + ctx.Data["ProtectedBranch"] = pb + ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull) + ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull) + ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull) + ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull) + ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull) + ctx.Data["RequireSigned"] = pb.RequireSignedCommits + ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles + ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0 + ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles) + ctx.Data["RequireApprovalsWhitelist"] = pb.EnableApprovalsWhitelist + } + + preparePullViewSigning(ctx, issue) if ctx.Written() { return } - tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID) - if err != nil { - ctx.ServerError("GetTagNamesByRepoID", err) + preparePullViewDeleteBranch(ctx, issue, canDelete) + if ctx.Written() { return } - ctx.Data["Tags"] = tags - ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool { - return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee) + stillCanManualMerge := func() bool { + if pull.HasMerged || issue.IsClosed || !ctx.IsSigned { + return false + } + if pull.CanAutoMerge() || pull.IsWorkInProgress(ctx) || pull.IsChecking() { + return false + } + if allowMerge && prConfig.AllowManualMerge { + return true + } + + return false } - ctx.HTML(http.StatusOK, tplIssueView) + ctx.Data["StillCanManualMerge"] = stillCanManualMerge() + + // Check if there is a pending pr merge + ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID) + if err != nil { + ctx.ServerError("GetScheduledMergeByPullID", err) + return + } +} + +func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) { + var err error + rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) + issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content) + if err != nil { + ctx.ServerError("RenderString", err) + return + } + if issue.ShowRole, err = roleDescriptor(ctx, issue.Repo, issue.Poster, issue, issue.HasOriginalAuthor()); err != nil { + ctx.ServerError("roleDescriptor", err) + return + } + ctx.Data["Issue"] = issue } diff --git a/routers/web/repo/milestone.go b/routers/web/repo/milestone.go index 3afdcfad8bf..33c15e77678 100644 --- a/routers/web/repo/milestone.go +++ b/routers/web/repo/milestone.go @@ -66,12 +66,6 @@ func Milestones(ctx *context.Context) { } ctx.Data["OpenCount"] = stats.OpenCount ctx.Data["ClosedCount"] = stats.ClosedCount - linkStr := "%s/milestones?state=%s&q=%s&sort=%s" - ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Repo.RepoLink, "open", - url.QueryEscape(keyword), url.QueryEscape(sortType)) - ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Repo.RepoLink, "closed", - url.QueryEscape(keyword), url.QueryEscape(sortType)) - if ctx.Repo.Repository.IsTimetrackerEnabled(ctx) { if err := issues_model.MilestoneList(miles).LoadTotalTrackedTimes(ctx); err != nil { ctx.ServerError("LoadTotalTrackedTimes", err) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 799ce3ad804..3be95786706 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/web/shared/issue" shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" @@ -307,28 +308,13 @@ func ViewProject(ctx *context.Context) { return } - var labelIDs []int64 - // 1,-2 means including label 1 and excluding label 2 - // 0 means issues with no label - // blank means labels will not be filtered for issues - selectLabels := ctx.FormString("labels") - if selectLabels == "" { - ctx.Data["AllLabels"] = true - } else if selectLabels == "0" { - ctx.Data["NoLabel"] = true - } - if len(selectLabels) > 0 { - labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) - if err != nil { - ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) - } - } + labelIDs := issue.PrepareFilterIssueLabels(ctx, ctx.Repo.Repository.ID, ctx.Repo.Owner) - assigneeID := ctx.FormInt64("assignee") + assigneeID := ctx.FormInt64("assignee") // TODO: use "optional" but not 0 in the future issuesMap, err := issues_model.LoadIssuesFromColumnList(ctx, columns, &issues_model.IssuesOptions{ LabelIDs: labelIDs, - AssigneeID: assigneeID, + AssigneeID: optional.Some(assigneeID), }) if err != nil { ctx.ServerError("LoadIssuesOfColumns", err) @@ -414,8 +400,6 @@ func ViewProject(ctx *context.Context) { return } ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers) - - ctx.Data["SelectLabels"] = selectLabels ctx.Data["AssigneeID"] = assigneeID rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository) @@ -425,6 +409,7 @@ func ViewProject(ctx *context.Context) { return } + ctx.Data["Title"] = project.Title ctx.Data["IsProjectsPage"] = true ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects) ctx.Data["Project"] = project diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 4b674e503ef..0f5e3e90407 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -263,8 +263,18 @@ func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) stri return baseCommit } -// PrepareMergedViewPullInfo show meta information for a merged pull request view page -func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo { +func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo { + if !issue.IsPull { + return nil + } + if issue.PullRequest.HasMerged { + return prepareMergedViewPullInfo(ctx, issue) + } + return prepareViewPullInfo(ctx, issue) +} + +// prepareMergedViewPullInfo show meta information for a merged pull request view page +func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo { pull := issue.PullRequest setMergeTarget(ctx, pull) @@ -309,8 +319,8 @@ func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) return compareInfo } -// PrepareViewPullInfo show meta information for a pull request preview page -func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo { +// prepareViewPullInfo show meta information for a pull request preview page +func prepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo { ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes repo := ctx.Repo.Repository @@ -610,15 +620,8 @@ func ViewPullCommits(ctx *context.Context) { if !ok { return } - pull := issue.PullRequest - - var prInfo *git.CompareInfo - if pull.HasMerged { - prInfo = PrepareMergedViewPullInfo(ctx, issue) - } else { - prInfo = PrepareViewPullInfo(ctx, issue) - } + prInfo := preparePullViewPullInfo(ctx, issue) if ctx.Written() { return } else if prInfo == nil { @@ -662,11 +665,12 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi gitRepo = ctx.Repo.GitRepo ) - var prInfo *git.CompareInfo - if pull.HasMerged { - prInfo = PrepareMergedViewPullInfo(ctx, issue) - } else { - prInfo = PrepareViewPullInfo(ctx, issue) + prInfo := preparePullViewPullInfo(ctx, issue) + if ctx.Written() { + return + } else if prInfo == nil { + ctx.NotFound("ViewPullFiles", nil) + return } // Validate the given commit sha to show (if any passed) diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index c178ba24916..b3a91a60707 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -31,7 +31,6 @@ import ( "code.gitea.io/gitea/services/context/upload" "code.gitea.io/gitea/services/forms" releaseservice "code.gitea.io/gitea/services/release" - repo_service "code.gitea.io/gitea/services/repository" ) const ( @@ -153,9 +152,6 @@ func Releases(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.release.releases") ctx.Data["IsViewBranch"] = false ctx.Data["IsViewTag"] = true - // Disable the showCreateNewBranch form in the dropdown on this page. - ctx.Data["CanCreateBranch"] = false - ctx.Data["HideBranchesInDropdown"] = true listOptions := db.ListOptions{ Page: ctx.FormInt("page"), @@ -193,9 +189,6 @@ func Releases(ctx *context.Context) { pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager - - ctx.Data["LicenseFileName"] = repo_service.LicenseFileName - ctx.HTML(http.StatusOK, tplReleasesList) } @@ -205,9 +198,6 @@ func TagsList(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.release.tags") ctx.Data["IsViewBranch"] = false ctx.Data["IsViewTag"] = true - // Disable the showCreateNewBranch form in the dropdown on this page. - ctx.Data["CanCreateBranch"] = false - ctx.Data["HideBranchesInDropdown"] = true ctx.Data["CanCreateRelease"] = ctx.Repo.CanWrite(unit.TypeReleases) && !ctx.Repo.Repository.IsArchived namePattern := ctx.FormTrim("q") @@ -254,8 +244,6 @@ func TagsList(ctx *context.Context) { pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager ctx.Data["PageIsViewCode"] = !ctx.Repo.Repository.UnitEnabled(ctx, unit.TypeReleases) - ctx.Data["LicenseFileName"] = repo_service.LicenseFileName - ctx.HTML(http.StatusOK, tplTagsList) } diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index e30129bb44c..717d7cbce1d 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/indexer/code" + issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/indexer/stats" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" @@ -905,6 +906,9 @@ func SettingsPost(ctx *context.Context) { log.Error("CleanRepoScheduleTasks for archived repo %s/%s: %v", ctx.Repo.Owner.Name, repo.Name, err) } + // update issue indexer + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + ctx.Flash.Success(ctx.Tr("repo.settings.archive.success")) log.Trace("Repository was archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) @@ -929,6 +933,9 @@ func SettingsPost(ctx *context.Context) { } } + // update issue indexer + issue_indexer.UpdateRepoIndexer(ctx, repo.ID) + ctx.Flash.Success(ctx.Tr("repo.settings.unarchive.success")) log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index e6c25d75e93..e43841acd36 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -5,18 +5,13 @@ package repo import ( - "bytes" gocontext "context" - "encoding/base64" "errors" "fmt" "html/template" - "image" "io" "net/http" "net/url" - "path" - "slices" "strings" "time" @@ -29,33 +24,21 @@ import ( asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" - issue_model "code.gitea.io/gitea/models/issues" - access_model "code.gitea.io/gitea/models/perm/access" - "code.gitea.io/gitea/models/renderhelper" repo_model "code.gitea.io/gitea/models/repo" unit_model "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/actions" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/routers/web/feed" "code.gitea.io/gitea/services/context" - issue_service "code.gitea.io/gitea/services/issue" repo_service "code.gitea.io/gitea/services/repository" - files_service "code.gitea.io/gitea/services/repository/files" - - "github.com/nektos/act/pkg/model" _ "golang.org/x/image/bmp" // for processing bmp images _ "golang.org/x/image/webp" // for processing webp images @@ -70,140 +53,6 @@ const ( tplMigrating base.TplName = "repo/migrate/migrating" ) -// locate a README for a tree in one of the supported paths. -// -// entries is passed to reduce calls to ListEntries(), so -// this has precondition: -// -// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries() -// -// FIXME: There has to be a more efficient way of doing this -func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) { - // Create a list of extensions in priority order - // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md - // 2. Txt files - e.g. README.txt - // 3. No extension - e.g. README - exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority - extCount := len(exts) - readmeFiles := make([]*git.TreeEntry, extCount+1) - - docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/) - for _, entry := range entries { - if tryWellKnownDirs && entry.IsDir() { - // as a special case for the top-level repo introduction README, - // fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ... - // (note that docsEntries is ignored unless we are at the root) - lowerName := strings.ToLower(entry.Name()) - switch lowerName { - case "docs": - if entry.Name() == "docs" || docsEntries[0] == nil { - docsEntries[0] = entry - } - case ".gitea": - if entry.Name() == ".gitea" || docsEntries[1] == nil { - docsEntries[1] = entry - } - case ".github": - if entry.Name() == ".github" || docsEntries[2] == nil { - docsEntries[2] = entry - } - } - continue - } - if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok { - log.Debug("Potential readme file: %s", entry.Name()) - if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { - if entry.IsLink() { - target, err := entry.FollowLinks() - if err != nil && !git.IsErrBadLink(err) { - return "", nil, err - } else if target != nil && (target.IsExecutable() || target.IsRegular()) { - readmeFiles[i] = entry - } - } else { - readmeFiles[i] = entry - } - } - } - } - var readmeFile *git.TreeEntry - for _, f := range readmeFiles { - if f != nil { - readmeFile = f - break - } - } - - if ctx.Repo.TreePath == "" && readmeFile == nil { - for _, subTreeEntry := range docsEntries { - if subTreeEntry == nil { - continue - } - subTree := subTreeEntry.Tree() - if subTree == nil { - // this should be impossible; if subTreeEntry exists so should this. - continue - } - childEntries, err := subTree.ListEntries() - if err != nil { - return "", nil, err - } - - subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false) - if err != nil && !git.IsErrNotExist(err) { - return "", nil, err - } - if readmeFile != nil { - return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil - } - } - } - - return "", readmeFile, nil -} - -func renderDirectory(ctx *context.Context) { - entries := renderDirectoryFiles(ctx, 1*time.Second) - if ctx.Written() { - return - } - - if ctx.Repo.TreePath != "" { - ctx.Data["HideRepoInfo"] = true - ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) - } - - subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) - if err != nil { - ctx.ServerError("findReadmeFileInEntries", err) - return - } - - renderReadmeFile(ctx, subfolder, readmeFile) -} - -// localizedExtensions prepends the provided language code with and without a -// regional identifier to the provided extension. -// Note: the language code will always be lower-cased, if a region is present it must be separated with a `-` -// Note: ext should be prefixed with a `.` -func localizedExtensions(ext, languageCode string) (localizedExts []string) { - if len(languageCode) < 1 { - return []string{ext} - } - - lowerLangCode := "." + strings.ToLower(languageCode) - - if strings.Contains(lowerLangCode, "-") { - underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_") - indexOfDash := strings.Index(lowerLangCode, "-") - // e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md] - return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext} - } - - // e.g. [.en.md, .md] - return []string{lowerLangCode + ext, ext} -} - type fileInfo struct { isTextFile bool isLFSFile bool @@ -261,85 +110,6 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte, return buf, dataRc, &fileInfo{st.IsText(), true, meta.Size, &meta.Pointer, st}, nil } -func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) { - target := readmeFile - if readmeFile != nil && readmeFile.IsLink() { - target, _ = readmeFile.FollowLinks() - } - if target == nil { - // if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't) - // simply skip rendering the README - return - } - - ctx.Data["RawFileLink"] = "" - ctx.Data["ReadmeInList"] = true - ctx.Data["ReadmeExist"] = true - ctx.Data["FileIsSymlink"] = readmeFile.IsLink() - - buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob()) - if err != nil { - ctx.ServerError("getFileReader", err) - return - } - defer dataRc.Close() - - ctx.Data["FileIsText"] = fInfo.isTextFile - ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name()) - ctx.Data["FileSize"] = fInfo.fileSize - ctx.Data["IsLFSFile"] = fInfo.isLFSFile - - if fInfo.isLFSFile { - filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name())) - ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64)) - } - - if !fInfo.isTextFile { - return - } - - if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { - // Pretend that this is a normal text file to display 'This file is too large to be shown' - ctx.Data["IsFileTooLarge"] = true - ctx.Data["IsTextFile"] = true - return - } - - rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) - - if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" { - ctx.Data["IsMarkup"] = true - ctx.Data["MarkupType"] = markupType - - rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), - CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder), - }). - WithMarkupType(markupType). - WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). - - ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) - if err != nil { - log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err) - delete(ctx.Data, "IsMarkup") - } - } - - if ctx.Data["IsMarkup"] != true { - ctx.Data["IsPlainText"] = true - content, err := io.ReadAll(rd) - if err != nil { - log.Error("Read readme content failed: %v", err) - } - contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content)) - ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) - } - - if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { - ctx.Data["CanEditReadmeFile"] = true - } -} - func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { // Show latest commit info of repository in table header, // or of directory if not in root directory. @@ -371,287 +141,6 @@ func loadLatestCommitData(ctx *context.Context, latestCommit *git.Commit) bool { return true } -func renderFile(ctx *context.Context, entry *git.TreeEntry) { - ctx.Data["IsViewFile"] = true - ctx.Data["HideRepoInfo"] = true - blob := entry.Blob() - buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) - if err != nil { - ctx.ServerError("getFileReader", err) - return - } - defer dataRc.Close() - - ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) - ctx.Data["FileIsSymlink"] = entry.IsLink() - ctx.Data["FileName"] = blob.Name() - ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - - commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath) - if err != nil { - ctx.ServerError("GetCommitByPath", err) - return - } - - if !loadLatestCommitData(ctx, commit) { - return - } - - if ctx.Repo.TreePath == ".editorconfig" { - _, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) - if editorconfigWarning != nil { - ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error()) - } - if editorconfigErr != nil { - ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error()) - } - } else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) { - _, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit) - if issueConfigErr != nil { - ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error()) - } - } else if actions.IsWorkflow(ctx.Repo.TreePath) { - content, err := actions.GetContentFromEntry(entry) - if err != nil { - log.Error("actions.GetContentFromEntry: %v", err) - } - _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content)) - if workFlowErr != nil { - ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) - } - } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { - if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { - _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) - if len(warnings) > 0 { - ctx.Data["FileWarning"] = strings.Join(warnings, "\n") - } - } - } - - isDisplayingSource := ctx.FormString("display") == "source" - isDisplayingRendered := !isDisplayingSource - - if fInfo.isLFSFile { - ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - } - - isRepresentableAsText := fInfo.st.IsRepresentableAsText() - if !isRepresentableAsText { - // If we can't show plain text, always try to render. - isDisplayingSource = false - isDisplayingRendered = true - } - ctx.Data["IsLFSFile"] = fInfo.isLFSFile - ctx.Data["FileSize"] = fInfo.fileSize - ctx.Data["IsTextFile"] = fInfo.isTextFile - ctx.Data["IsRepresentableAsText"] = isRepresentableAsText - ctx.Data["IsDisplayingSource"] = isDisplayingSource - ctx.Data["IsDisplayingRendered"] = isDisplayingRendered - ctx.Data["IsExecutable"] = entry.IsExecutable() - - isTextSource := fInfo.isTextFile || isDisplayingSource - ctx.Data["IsTextSource"] = isTextSource - if isTextSource { - ctx.Data["CanCopyContent"] = true - } - - // Check LFS Lock - lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) - ctx.Data["LFSLock"] = lfsLock - if err != nil { - ctx.ServerError("GetTreePathLock", err) - return - } - if lfsLock != nil { - u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID) - if err != nil { - ctx.ServerError("GetTreePathLock", err) - return - } - ctx.Data["LFSLockOwner"] = u.Name - ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink() - ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked") - } - - // Assume file is not editable first. - if fInfo.isLFSFile { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") - } else if !isRepresentableAsText { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") - } - - switch { - case isRepresentableAsText: - if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { - ctx.Data["IsFileTooLarge"] = true - break - } - - if fInfo.st.IsSvgImage() { - ctx.Data["IsImageFile"] = true - ctx.Data["CanCopyContent"] = true - ctx.Data["HasSourceRenderedToggle"] = true - } - - rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) - - shouldRenderSource := ctx.FormString("display") == "source" - readmeExist := util.IsReadmeFileName(blob.Name()) - ctx.Data["ReadmeExist"] = readmeExist - - markupType := markup.DetectMarkupTypeByFileName(blob.Name()) - if markupType == "" { - markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf)) - } - if markupType != "" { - ctx.Data["HasSourceRenderedToggle"] = true - } - if markupType != "" && !shouldRenderSource { - ctx.Data["IsMarkup"] = true - ctx.Data["MarkupType"] = markupType - metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) - metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() - rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), - CurrentTreePath: path.Dir(ctx.Repo.TreePath), - }). - WithMarkupType(markupType). - WithRelativePath(ctx.Repo.TreePath). - WithMetas(metas) - - ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) - if err != nil { - ctx.ServerError("Render", err) - return - } - // to prevent iframe load third-party url - ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") - } else { - buf, _ := io.ReadAll(rd) - - // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html - // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line; - // Gitea uses the definition (like most modern editors): - // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines; - // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL. - // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines. - // This NumLines is only used for the display on the UI: "xxx lines" - if len(buf) == 0 { - ctx.Data["NumLines"] = 0 - } else { - ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1 - } - - language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) - if err != nil { - log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) - } - - fileContent, lexerName, err := highlight.File(blob.Name(), language, buf) - ctx.Data["LexerName"] = lexerName - if err != nil { - log.Error("highlight.File failed, fallback to plain text: %v", err) - fileContent = highlight.PlainText(buf) - } - status := &charset.EscapeStatus{} - statuses := make([]*charset.EscapeStatus, len(fileContent)) - for i, line := range fileContent { - statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale) - status = status.Or(statuses[i]) - } - ctx.Data["EscapeStatus"] = status - ctx.Data["FileContent"] = fileContent - ctx.Data["LineEscapeStatus"] = statuses - } - if !fInfo.isLFSFile { - if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.Data["CanEditFile"] = false - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") - } else { - ctx.Data["CanEditFile"] = true - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") - } - } else if !ctx.Repo.IsViewBranch { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") - } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") - } - } - - case fInfo.st.IsPDF(): - ctx.Data["IsPDFFile"] = true - case fInfo.st.IsVideo(): - ctx.Data["IsVideoFile"] = true - case fInfo.st.IsAudio(): - ctx.Data["IsAudioFile"] = true - case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()): - ctx.Data["IsImageFile"] = true - ctx.Data["CanCopyContent"] = true - default: - if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { - ctx.Data["IsFileTooLarge"] = true - break - } - - // TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go" - // It is used by "external renders", markupRender will execute external programs to get rendered content. - if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" { - rd := io.MultiReader(bytes.NewReader(buf), dataRc) - ctx.Data["IsMarkup"] = true - ctx.Data["MarkupType"] = markupType - - rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ - CurrentRefPath: ctx.Repo.BranchNameSubURL(), - CurrentTreePath: path.Dir(ctx.Repo.TreePath), - }). - WithMarkupType(markupType). - WithRelativePath(ctx.Repo.TreePath) - - ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) - if err != nil { - ctx.ServerError("Render", err) - return - } - } - } - - if ctx.Repo.GitRepo != nil { - checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID) - if checker != nil { - defer deferable() - attrs, err := checker.CheckPath(ctx.Repo.TreePath) - if err == nil { - ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value() - ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value() - } - } - } - - if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() { - img, _, err := image.DecodeConfig(bytes.NewReader(buf)) - if err == nil { - // There are Image formats go can't decode - // Instead of throwing an error in that case, we show the size only when we can decode - ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height) - } - } - - if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { - if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { - ctx.Data["CanDeleteFile"] = false - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") - } else { - ctx.Data["CanDeleteFile"] = true - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") - } - } else if !ctx.Repo.IsViewBranch { - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") - } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { - ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") - } -} - func markupRender(ctx *context.Context, renderCtx *markup.RenderContext, input io.Reader) (escaped *charset.EscapeStatus, output template.HTML, err error) { markupRd, markupWr := io.Pipe() defer markupWr.Close() @@ -728,59 +217,6 @@ func checkHomeCodeViewable(ctx *context.Context) { ctx.NotFound("Home", errors.New(ctx.Locale.TrString("units.error.no_unit_allowed_repo"))) } -func checkCitationFile(ctx *context.Context, entry *git.TreeEntry) { - if entry.Name() != "" { - return - } - tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) - if err != nil { - HandleGitError(ctx, "Repo.Commit.SubTree", err) - return - } - allEntries, err := tree.ListEntries() - if err != nil { - ctx.ServerError("ListEntries", err) - return - } - for _, entry := range allEntries { - if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" { - // Read Citation file contents - if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { - log.Error("checkCitationFile: GetBlobContent: %v", err) - } else { - ctx.Data["CitiationExist"] = true - ctx.PageData["citationFileContent"] = content - break - } - } - } -} - -// Home render repository home page -func Home(ctx *context.Context) { - if setting.Other.EnableFeed { - isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":reponame"), ctx.Req) - if isFeed { - switch { - case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): - feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) - case ctx.Repo.TreePath == "": - feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) - case ctx.Repo.TreePath != "": - feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) - } - return - } - } - - checkHomeCodeViewable(ctx) - if ctx.Written() { - return - } - - renderHomeCode(ctx) -} - // LastCommit returns lastCommit data for the provided branch/tag/commit and directory (in url) and filenames in body func LastCommit(ctx *context.Context) { checkHomeCodeViewable(ctx) @@ -877,220 +313,6 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri return allEntries } -func renderLanguageStats(ctx *context.Context) { - langs, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5) - if err != nil { - ctx.ServerError("Repo.GetTopLanguageStats", err) - return - } - - ctx.Data["LanguageStats"] = langs -} - -func renderRepoTopics(ctx *context.Context) { - topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{ - RepoID: ctx.Repo.Repository.ID, - }) - if err != nil { - ctx.ServerError("models.FindTopics", err) - return - } - ctx.Data["Topics"] = topics -} - -func prepareOpenWithEditorApps(ctx *context.Context) { - var tmplApps []map[string]any - apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx) - if len(apps) == 0 { - apps = setting.DefaultOpenWithEditorApps() - } - for _, app := range apps { - schema, _, _ := strings.Cut(app.OpenURL, ":") - var iconHTML template.HTML - if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" { - iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16, "tw-mr-2") - } else { - iconHTML = svg.RenderHTML("gitea-git", 16, "tw-mr-2") // TODO: it could support user's customized icon in the future - } - tmplApps = append(tmplApps, map[string]any{ - "DisplayName": app.DisplayName, - "OpenURL": app.OpenURL, - "IconHTML": iconHTML, - }) - } - ctx.Data["OpenWithEditorApps"] = tmplApps -} - -func renderHomeCode(ctx *context.Context) { - ctx.Data["PageIsViewCode"] = true - ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled - prepareOpenWithEditorApps(ctx) - - if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { - showEmpty := true - var err error - if ctx.Repo.GitRepo != nil { - showEmpty, err = ctx.Repo.GitRepo.IsEmpty() - if err != nil { - log.Error("GitRepo.IsEmpty: %v", err) - ctx.Repo.Repository.Status = repo_model.RepositoryBroken - showEmpty = true - ctx.Flash.Error(ctx.Tr("error.occurred"), true) - } - } - if showEmpty { - ctx.HTML(http.StatusOK, tplRepoEMPTY) - return - } - - // the repo is not really empty, so we should update the modal in database - // such problem may be caused by: - // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually - // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. - // it's possible for a repository to be non-empty by that flag but still 500 - // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. - ctx.Repo.Repository.IsEmpty = false - if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { - ctx.ServerError("UpdateRepositoryCols", err) - return - } - if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { - ctx.ServerError("UpdateRepoSize", err) - return - } - - // the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values - link := ctx.Link - if ctx.Req.URL.RawQuery != "" { - link += "?" + ctx.Req.URL.RawQuery - } - ctx.Redirect(link) - return - } - - title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name - if len(ctx.Repo.Repository.Description) > 0 { - title += ": " + ctx.Repo.Repository.Description - } - ctx.Data["Title"] = title - - // Get Topics of this repo - renderRepoTopics(ctx) - if ctx.Written() { - return - } - - // Get current entry user currently looking at. - entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) - if err != nil { - HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) - return - } - - checkOutdatedBranch(ctx) - - checkCitationFile(ctx, entry) - if ctx.Written() { - return - } - - renderLanguageStats(ctx) - if ctx.Written() { - return - } - - if entry.IsDir() { - renderDirectory(ctx) - } else { - renderFile(ctx, entry) - } - if ctx.Written() { - return - } - - if ctx.Doer != nil { - if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil { - ctx.ServerError("GetBaseRepo", err) - return - } - - opts := &git_model.FindRecentlyPushedNewBranchesOptions{ - Repo: ctx.Repo.Repository, - BaseRepo: ctx.Repo.Repository, - } - if ctx.Repo.Repository.IsFork { - opts.BaseRepo = ctx.Repo.Repository.BaseRepo - } - - baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer) - if err != nil { - ctx.ServerError("GetUserRepoPermission", err) - return - } - - if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror && - opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) && - baseRepoPerm.CanRead(unit_model.TypePullRequests) { - ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) - if err != nil { - log.Error("FindRecentlyPushedNewBranches failed: %v", err) - } - } - } - - var treeNames []string - paths := make([]string, 0, 5) - if len(ctx.Repo.TreePath) > 0 { - treeNames = strings.Split(ctx.Repo.TreePath, "/") - for i := range treeNames { - paths = append(paths, strings.Join(treeNames[:i+1], "/")) - } - - ctx.Data["HasParentPath"] = true - if len(paths)-2 >= 0 { - ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] - } - } - - ctx.Data["Paths"] = paths - - branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() - treeLink := branchLink - if len(ctx.Repo.TreePath) > 0 { - treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) - } - ctx.Data["TreeLink"] = treeLink - ctx.Data["TreeNames"] = treeNames - ctx.Data["BranchLink"] = branchLink - ctx.Data["LicenseFileName"] = repo_service.LicenseFileName - ctx.HTML(http.StatusOK, tplRepoHome) -} - -func checkOutdatedBranch(ctx *context.Context) { - if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) { - return - } - - // get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName` - commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) - if err != nil { - log.Error("GetBranchCommitID: %v", err) - // Don't return an error page, as it can be rechecked the next time the user opens the page. - return - } - - dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName) - if err != nil { - log.Error("GetBranch: %v", err) - // Don't return an error page, as it can be rechecked the next time the user opens the page. - return - } - - if dbBranch.CommitID != commit.ID.String() { - ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true) - } -} - // RenderUserCards render a page show users according the input template func RenderUserCards(ctx *context.Context, total int, getter func(opts db.ListOptions) ([]*user_model.User, error), tpl base.TplName) { page := ctx.FormInt("page") diff --git a/routers/web/repo/view_file.go b/routers/web/repo/view_file.go new file mode 100644 index 00000000000..17c28218243 --- /dev/null +++ b/routers/web/repo/view_file.go @@ -0,0 +1,313 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "bytes" + "fmt" + "image" + "io" + "path" + "slices" + "strings" + + git_model "code.gitea.io/gitea/models/git" + issue_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/renderhelper" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/highlight" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + issue_service "code.gitea.io/gitea/services/issue" + files_service "code.gitea.io/gitea/services/repository/files" + + "github.com/nektos/act/pkg/model" +) + +func prepareToRenderFile(ctx *context.Context, entry *git.TreeEntry) { + ctx.Data["IsViewFile"] = true + ctx.Data["HideRepoInfo"] = true + blob := entry.Blob() + buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, blob) + if err != nil { + ctx.ServerError("getFileReader", err) + return + } + defer dataRc.Close() + + ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) + ctx.Data["FileIsSymlink"] = entry.IsLink() + ctx.Data["FileName"] = blob.Name() + ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + + commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath) + if err != nil { + ctx.ServerError("GetCommitByPath", err) + return + } + + if !loadLatestCommitData(ctx, commit) { + return + } + + if ctx.Repo.TreePath == ".editorconfig" { + _, editorconfigWarning, editorconfigErr := ctx.Repo.GetEditorconfig(ctx.Repo.Commit) + if editorconfigWarning != nil { + ctx.Data["FileWarning"] = strings.TrimSpace(editorconfigWarning.Error()) + } + if editorconfigErr != nil { + ctx.Data["FileError"] = strings.TrimSpace(editorconfigErr.Error()) + } + } else if issue_service.IsTemplateConfig(ctx.Repo.TreePath) { + _, issueConfigErr := issue_service.GetTemplateConfig(ctx.Repo.GitRepo, ctx.Repo.TreePath, ctx.Repo.Commit) + if issueConfigErr != nil { + ctx.Data["FileError"] = strings.TrimSpace(issueConfigErr.Error()) + } + } else if actions.IsWorkflow(ctx.Repo.TreePath) { + content, err := actions.GetContentFromEntry(entry) + if err != nil { + log.Error("actions.GetContentFromEntry: %v", err) + } + _, workFlowErr := model.ReadWorkflow(bytes.NewReader(content)) + if workFlowErr != nil { + ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) + } + } else if slices.Contains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { + if data, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize); err == nil { + _, warnings := issue_model.GetCodeOwnersFromContent(ctx, data) + if len(warnings) > 0 { + ctx.Data["FileWarning"] = strings.Join(warnings, "\n") + } + } + } + + isDisplayingSource := ctx.FormString("display") == "source" + isDisplayingRendered := !isDisplayingSource + + if fInfo.isLFSFile { + ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/media/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + } + + isRepresentableAsText := fInfo.st.IsRepresentableAsText() + if !isRepresentableAsText { + // If we can't show plain text, always try to render. + isDisplayingSource = false + isDisplayingRendered = true + } + ctx.Data["IsLFSFile"] = fInfo.isLFSFile + ctx.Data["FileSize"] = fInfo.fileSize + ctx.Data["IsTextFile"] = fInfo.isTextFile + ctx.Data["IsRepresentableAsText"] = isRepresentableAsText + ctx.Data["IsDisplayingSource"] = isDisplayingSource + ctx.Data["IsDisplayingRendered"] = isDisplayingRendered + ctx.Data["IsExecutable"] = entry.IsExecutable() + + isTextSource := fInfo.isTextFile || isDisplayingSource + ctx.Data["IsTextSource"] = isTextSource + if isTextSource { + ctx.Data["CanCopyContent"] = true + } + + // Check LFS Lock + lfsLock, err := git_model.GetTreePathLock(ctx, ctx.Repo.Repository.ID, ctx.Repo.TreePath) + ctx.Data["LFSLock"] = lfsLock + if err != nil { + ctx.ServerError("GetTreePathLock", err) + return + } + if lfsLock != nil { + u, err := user_model.GetUserByID(ctx, lfsLock.OwnerID) + if err != nil { + ctx.ServerError("GetTreePathLock", err) + return + } + ctx.Data["LFSLockOwner"] = u.Name + ctx.Data["LFSLockOwnerHomeLink"] = u.HomeLink() + ctx.Data["LFSLockHint"] = ctx.Tr("repo.editor.this_file_locked") + } + + // Assume file is not editable first. + if fInfo.isLFSFile { + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_lfs_files") + } else if !isRepresentableAsText { + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.cannot_edit_non_text_files") + } + + switch { + case isRepresentableAsText: + if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { + ctx.Data["IsFileTooLarge"] = true + break + } + + if fInfo.st.IsSvgImage() { + ctx.Data["IsImageFile"] = true + ctx.Data["CanCopyContent"] = true + ctx.Data["HasSourceRenderedToggle"] = true + } + + rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) + + shouldRenderSource := ctx.FormString("display") == "source" + readmeExist := util.IsReadmeFileName(blob.Name()) + ctx.Data["ReadmeExist"] = readmeExist + + markupType := markup.DetectMarkupTypeByFileName(blob.Name()) + if markupType == "" { + markupType = markup.DetectRendererType(blob.Name(), bytes.NewReader(buf)) + } + if markupType != "" { + ctx.Data["HasSourceRenderedToggle"] = true + } + if markupType != "" && !shouldRenderSource { + ctx.Data["IsMarkup"] = true + ctx.Data["MarkupType"] = markupType + metas := ctx.Repo.Repository.ComposeDocumentMetas(ctx) + metas["BranchNameSubURL"] = ctx.Repo.BranchNameSubURL() + rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ + CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentTreePath: path.Dir(ctx.Repo.TreePath), + }). + WithMarkupType(markupType). + WithRelativePath(ctx.Repo.TreePath). + WithMetas(metas) + + ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) + if err != nil { + ctx.ServerError("Render", err) + return + } + // to prevent iframe load third-party url + ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'") + } else { + buf, _ := io.ReadAll(rd) + + // The Open Group Base Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html + // empty: 0 lines; "a": 1 incomplete-line; "a\n": 1 line; "a\nb": 1 line, 1 incomplete-line; + // Gitea uses the definition (like most modern editors): + // empty: 0 lines; "a": 1 line; "a\n": 2 lines; "a\nb": 2 lines; + // When rendering, the last empty line is not rendered in UI, while the line-number is still counted, to tell users that the file contains a trailing EOL. + // To make the UI more consistent, it could use an icon mark to indicate that there is no trailing EOL, and show line-number as the rendered lines. + // This NumLines is only used for the display on the UI: "xxx lines" + if len(buf) == 0 { + ctx.Data["NumLines"] = 0 + } else { + ctx.Data["NumLines"] = bytes.Count(buf, []byte{'\n'}) + 1 + } + + language, err := files_service.TryGetContentLanguage(ctx.Repo.GitRepo, ctx.Repo.CommitID, ctx.Repo.TreePath) + if err != nil { + log.Error("Unable to get file language for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err) + } + + fileContent, lexerName, err := highlight.File(blob.Name(), language, buf) + ctx.Data["LexerName"] = lexerName + if err != nil { + log.Error("highlight.File failed, fallback to plain text: %v", err) + fileContent = highlight.PlainText(buf) + } + status := &charset.EscapeStatus{} + statuses := make([]*charset.EscapeStatus, len(fileContent)) + for i, line := range fileContent { + statuses[i], fileContent[i] = charset.EscapeControlHTML(line, ctx.Locale) + status = status.Or(statuses[i]) + } + ctx.Data["EscapeStatus"] = status + ctx.Data["FileContent"] = fileContent + ctx.Data["LineEscapeStatus"] = statuses + } + if !fInfo.isLFSFile { + if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + ctx.Data["CanEditFile"] = false + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") + } else { + ctx.Data["CanEditFile"] = true + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.edit_this_file") + } + } else if !ctx.Repo.IsViewBranch { + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") + } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + ctx.Data["EditFileTooltip"] = ctx.Tr("repo.editor.fork_before_edit") + } + } + + case fInfo.st.IsPDF(): + ctx.Data["IsPDFFile"] = true + case fInfo.st.IsVideo(): + ctx.Data["IsVideoFile"] = true + case fInfo.st.IsAudio(): + ctx.Data["IsAudioFile"] = true + case fInfo.st.IsImage() && (setting.UI.SVG.Enabled || !fInfo.st.IsSvgImage()): + ctx.Data["IsImageFile"] = true + ctx.Data["CanCopyContent"] = true + default: + if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { + ctx.Data["IsFileTooLarge"] = true + break + } + + // TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go" + // It is used by "external renders", markupRender will execute external programs to get rendered content. + if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" { + rd := io.MultiReader(bytes.NewReader(buf), dataRc) + ctx.Data["IsMarkup"] = true + ctx.Data["MarkupType"] = markupType + + rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ + CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentTreePath: path.Dir(ctx.Repo.TreePath), + }). + WithMarkupType(markupType). + WithRelativePath(ctx.Repo.TreePath) + + ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) + if err != nil { + ctx.ServerError("Render", err) + return + } + } + } + + if ctx.Repo.GitRepo != nil { + checker, deferable := ctx.Repo.GitRepo.CheckAttributeReader(ctx.Repo.CommitID) + if checker != nil { + defer deferable() + attrs, err := checker.CheckPath(ctx.Repo.TreePath) + if err == nil { + ctx.Data["IsVendored"] = git.AttributeToBool(attrs, git.AttributeLinguistVendored).Value() + ctx.Data["IsGenerated"] = git.AttributeToBool(attrs, git.AttributeLinguistGenerated).Value() + } + } + } + + if fInfo.st.IsImage() && !fInfo.st.IsSvgImage() { + img, _, err := image.DecodeConfig(bytes.NewReader(buf)) + if err == nil { + // There are Image formats go can't decode + // Instead of throwing an error in that case, we show the size only when we can decode + ctx.Data["ImageSize"] = fmt.Sprintf("%dx%dpx", img.Width, img.Height) + } + } + + if ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + if lfsLock != nil && lfsLock.OwnerID != ctx.Doer.ID { + ctx.Data["CanDeleteFile"] = false + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.this_file_locked") + } else { + ctx.Data["CanDeleteFile"] = true + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.delete_this_file") + } + } else if !ctx.Repo.IsViewBranch { + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_be_on_a_branch") + } else if !ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, ctx.Repo.BranchName) { + ctx.Data["DeleteFileTooltip"] = ctx.Tr("repo.editor.must_have_write_access") + } +} diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go new file mode 100644 index 00000000000..b318c4a621a --- /dev/null +++ b/routers/web/repo/view_home.go @@ -0,0 +1,378 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "errors" + "fmt" + "html/template" + "net/http" + "path" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" + unit_model "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/svg" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/routers/web/feed" + "code.gitea.io/gitea/services/context" + repo_service "code.gitea.io/gitea/services/repository" +) + +func checkOutdatedBranch(ctx *context.Context) { + if !(ctx.Repo.IsAdmin() || ctx.Repo.IsOwner()) { + return + } + + // get the head commit of the branch since ctx.Repo.CommitID is not always the head commit of `ctx.Repo.BranchName` + commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) + if err != nil { + log.Error("GetBranchCommitID: %v", err) + // Don't return an error page, as it can be rechecked the next time the user opens the page. + return + } + + dbBranch, err := git_model.GetBranch(ctx, ctx.Repo.Repository.ID, ctx.Repo.BranchName) + if err != nil { + log.Error("GetBranch: %v", err) + // Don't return an error page, as it can be rechecked the next time the user opens the page. + return + } + + if dbBranch.CommitID != commit.ID.String() { + ctx.Flash.Warning(ctx.Tr("repo.error.broken_git_hook", "https://docs.gitea.com/help/faq#push-hook--webhook--actions-arent-running"), true) + } +} + +func prepareHomeSidebarRepoTopics(ctx *context.Context) { + topics, err := db.Find[repo_model.Topic](ctx, &repo_model.FindTopicOptions{ + RepoID: ctx.Repo.Repository.ID, + }) + if err != nil { + ctx.ServerError("models.FindTopics", err) + return + } + ctx.Data["Topics"] = topics +} + +func prepareOpenWithEditorApps(ctx *context.Context) { + var tmplApps []map[string]any + apps := setting.Config().Repository.OpenWithEditorApps.Value(ctx) + if len(apps) == 0 { + apps = setting.DefaultOpenWithEditorApps() + } + for _, app := range apps { + schema, _, _ := strings.Cut(app.OpenURL, ":") + var iconHTML template.HTML + if schema == "vscode" || schema == "vscodium" || schema == "jetbrains" { + iconHTML = svg.RenderHTML(fmt.Sprintf("gitea-%s", schema), 16) + } else { + iconHTML = svg.RenderHTML("gitea-git", 16) // TODO: it could support user's customized icon in the future + } + tmplApps = append(tmplApps, map[string]any{ + "DisplayName": app.DisplayName, + "OpenURL": app.OpenURL, + "IconHTML": iconHTML, + }) + } + ctx.Data["OpenWithEditorApps"] = tmplApps +} + +func prepareHomeSidebarCitationFile(entry *git.TreeEntry) func(ctx *context.Context) { + return func(ctx *context.Context) { + if entry.Name() != "" { + return + } + tree, err := ctx.Repo.Commit.SubTree(ctx.Repo.TreePath) + if err != nil { + HandleGitError(ctx, "Repo.Commit.SubTree", err) + return + } + allEntries, err := tree.ListEntries() + if err != nil { + ctx.ServerError("ListEntries", err) + return + } + for _, entry := range allEntries { + if entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" { + // Read Citation file contents + if content, err := entry.Blob().GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil { + log.Error("checkCitationFile: GetBlobContent: %v", err) + } else { + ctx.Data["CitiationExist"] = true + ctx.PageData["citationFileContent"] = content + break + } + } + } + } +} + +func prepareHomeSidebarLicenses(ctx *context.Context) { + repoLicenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository) + if err != nil { + ctx.ServerError("GetRepoLicenses", err) + return + } + ctx.Data["DetectedRepoLicenses"] = repoLicenses.StringList() + ctx.Data["LicenseFileName"] = repo_service.LicenseFileName +} + +func prepareToRenderDirectory(ctx *context.Context) { + entries := renderDirectoryFiles(ctx, 1*time.Second) + if ctx.Written() { + return + } + + if ctx.Repo.TreePath != "" { + ctx.Data["HideRepoInfo"] = true + ctx.Data["Title"] = ctx.Tr("repo.file.title", ctx.Repo.Repository.Name+"/"+path.Base(ctx.Repo.TreePath), ctx.Repo.RefName) + } + + subfolder, readmeFile, err := findReadmeFileInEntries(ctx, entries, true) + if err != nil { + ctx.ServerError("findReadmeFileInEntries", err) + return + } + + prepareToRenderReadmeFile(ctx, subfolder, readmeFile) +} + +func prepareHomeSidebarLanguageStats(ctx *context.Context) { + langs, err := repo_model.GetTopLanguageStats(ctx, ctx.Repo.Repository, 5) + if err != nil { + ctx.ServerError("Repo.GetTopLanguageStats", err) + return + } + + ctx.Data["LanguageStats"] = langs +} + +func prepareHomeSidebarLatestRelease(ctx *context.Context) { + if !ctx.Repo.Repository.UnitEnabled(ctx, unit_model.TypeReleases) { + return + } + + release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID) + if err != nil && !repo_model.IsErrReleaseNotExist(err) { + ctx.ServerError("GetLatestReleaseByRepoID", err) + return + } + + if release != nil { + if err = release.LoadAttributes(ctx); err != nil { + ctx.ServerError("release.LoadAttributes", err) + return + } + ctx.Data["LatestRelease"] = release + } +} + +func prepareUpstreamDivergingInfo(ctx *context.Context) { + if !ctx.Repo.Repository.IsFork || !ctx.Repo.IsViewBranch || ctx.Repo.TreePath != "" { + return + } + upstreamDivergingInfo, err := repo_service.GetUpstreamDivergingInfo(ctx, ctx.Repo.Repository, ctx.Repo.BranchName) + if err != nil { + if !errors.Is(err, util.ErrNotExist) && !errors.Is(err, util.ErrInvalidArgument) { + log.Error("GetUpstreamDivergingInfo: %v", err) + } + return + } + ctx.Data["UpstreamDivergingInfo"] = upstreamDivergingInfo +} + +func prepareRecentlyPushedNewBranches(ctx *context.Context) { + if ctx.Doer != nil { + if err := ctx.Repo.Repository.GetBaseRepo(ctx); err != nil { + ctx.ServerError("GetBaseRepo", err) + return + } + + opts := &git_model.FindRecentlyPushedNewBranchesOptions{ + Repo: ctx.Repo.Repository, + BaseRepo: ctx.Repo.Repository, + } + if ctx.Repo.Repository.IsFork { + opts.BaseRepo = ctx.Repo.Repository.BaseRepo + } + + baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, opts.BaseRepo, ctx.Doer) + if err != nil { + ctx.ServerError("GetUserRepoPermission", err) + return + } + + if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror && + opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) && + baseRepoPerm.CanRead(unit_model.TypePullRequests) { + ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) + if err != nil { + log.Error("FindRecentlyPushedNewBranches failed: %v", err) + } + } + } +} + +func handleRepoEmptyOrBroken(ctx *context.Context) { + showEmpty := true + var err error + if ctx.Repo.GitRepo != nil { + showEmpty, err = ctx.Repo.GitRepo.IsEmpty() + if err != nil { + log.Error("GitRepo.IsEmpty: %v", err) + ctx.Repo.Repository.Status = repo_model.RepositoryBroken + showEmpty = true + ctx.Flash.Error(ctx.Tr("error.occurred"), true) + } + } + if showEmpty { + ctx.HTML(http.StatusOK, tplRepoEMPTY) + return + } + + // the repo is not really empty, so we should update the modal in database + // such problem may be caused by: + // 1) an error occurs during pushing/receiving. 2) the user replaces an empty git repo manually + // and even more: the IsEmpty flag is deeply broken and should be removed with the UI changed to manage to cope with empty repos. + // it's possible for a repository to be non-empty by that flag but still 500 + // because there are no branches - only tags -or the default branch is non-extant as it has been 0-pushed. + ctx.Repo.Repository.IsEmpty = false + if err = repo_model.UpdateRepositoryCols(ctx, ctx.Repo.Repository, "is_empty"); err != nil { + ctx.ServerError("UpdateRepositoryCols", err) + return + } + if err = repo_module.UpdateRepoSize(ctx, ctx.Repo.Repository); err != nil { + ctx.ServerError("UpdateRepoSize", err) + return + } + + // the repo's IsEmpty has been updated, redirect to this page to make sure middlewares can get the correct values + link := ctx.Link + if ctx.Req.URL.RawQuery != "" { + link += "?" + ctx.Req.URL.RawQuery + } + ctx.Redirect(link) +} + +func prepareToRenderDirOrFile(entry *git.TreeEntry) func(ctx *context.Context) { + return func(ctx *context.Context) { + if entry.IsDir() { + prepareToRenderDirectory(ctx) + } else { + prepareToRenderFile(ctx, entry) + } + } +} + +func handleRepoHomeFeed(ctx *context.Context) bool { + if setting.Other.EnableFeed { + isFeed, _, showFeedType := feed.GetFeedType(ctx.PathParam(":reponame"), ctx.Req) + if isFeed { + switch { + case ctx.Link == fmt.Sprintf("%s.%s", ctx.Repo.RepoLink, showFeedType): + feed.ShowRepoFeed(ctx, ctx.Repo.Repository, showFeedType) + case ctx.Repo.TreePath == "": + feed.ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) + case ctx.Repo.TreePath != "": + feed.ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) + } + return true + } + } + return false +} + +// Home render repository home page +func Home(ctx *context.Context) { + if handleRepoHomeFeed(ctx) { + return + } + + // Check whether the repo is viewable: not in migration, and the code unit should be enabled + // Ideally the "feed" logic should be after this, but old code did so, so keep it as-is. + checkHomeCodeViewable(ctx) + if ctx.Written() { + return + } + + title := ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name + if len(ctx.Repo.Repository.Description) > 0 { + title += ": " + ctx.Repo.Repository.Description + } + ctx.Data["Title"] = title + ctx.Data["PageIsViewCode"] = true + ctx.Data["RepositoryUploadEnabled"] = setting.Repository.Upload.Enabled // show New File / Upload File buttons + + if ctx.Repo.Commit == nil || ctx.Repo.Repository.IsEmpty || ctx.Repo.Repository.IsBroken() { + // empty or broken repositories need to be handled differently + handleRepoEmptyOrBroken(ctx) + return + } + + // get the current git entry which doer user is currently looking at. + entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) + if err != nil { + HandleGitError(ctx, "Repo.Commit.GetTreeEntryByPath", err) + return + } + + // prepare the tree path + var treeNames, paths []string + branchLink := ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + treeLink := branchLink + if ctx.Repo.TreePath != "" { + treeLink += "/" + util.PathEscapeSegments(ctx.Repo.TreePath) + treeNames = strings.Split(ctx.Repo.TreePath, "/") + for i := range treeNames { + paths = append(paths, strings.Join(treeNames[:i+1], "/")) + } + ctx.Data["HasParentPath"] = true + if len(paths)-2 >= 0 { + ctx.Data["ParentPath"] = "/" + paths[len(paths)-2] + } + } + ctx.Data["Paths"] = paths + ctx.Data["TreeLink"] = treeLink + ctx.Data["TreeNames"] = treeNames + ctx.Data["BranchLink"] = branchLink + + // some UI components are only shown when the tree path is root + isTreePathRoot := ctx.Repo.TreePath == "" + + prepareFuncs := []func(*context.Context){ + prepareOpenWithEditorApps, + prepareHomeSidebarRepoTopics, + checkOutdatedBranch, + prepareToRenderDirOrFile(entry), + prepareRecentlyPushedNewBranches, + } + + if isTreePathRoot { + prepareFuncs = append(prepareFuncs, + prepareUpstreamDivergingInfo, + prepareHomeSidebarLicenses, + prepareHomeSidebarCitationFile(entry), + prepareHomeSidebarLanguageStats, + prepareHomeSidebarLatestRelease, + ) + } + + for _, prepare := range prepareFuncs { + prepare(ctx) + if ctx.Written() { + return + } + } + + ctx.HTML(http.StatusOK, tplRepoHome) +} diff --git a/routers/web/repo/view_readme.go b/routers/web/repo/view_readme.go new file mode 100644 index 00000000000..5bd39de9631 --- /dev/null +++ b/routers/web/repo/view_readme.go @@ -0,0 +1,218 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "bytes" + "encoding/base64" + "fmt" + "html/template" + "io" + "net/url" + "path" + "strings" + + "code.gitea.io/gitea/models/renderhelper" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/charset" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" +) + +// locate a README for a tree in one of the supported paths. +// +// entries is passed to reduce calls to ListEntries(), so +// this has precondition: +// +// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries() +// +// FIXME: There has to be a more efficient way of doing this +func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, tryWellKnownDirs bool) (string, *git.TreeEntry, error) { + // Create a list of extensions in priority order + // 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md + // 2. Txt files - e.g. README.txt + // 3. No extension - e.g. README + exts := append(localizedExtensions(".md", ctx.Locale.Language()), ".txt", "") // sorted by priority + extCount := len(exts) + readmeFiles := make([]*git.TreeEntry, extCount+1) + + docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/) + for _, entry := range entries { + if tryWellKnownDirs && entry.IsDir() { + // as a special case for the top-level repo introduction README, + // fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ... + // (note that docsEntries is ignored unless we are at the root) + lowerName := strings.ToLower(entry.Name()) + switch lowerName { + case "docs": + if entry.Name() == "docs" || docsEntries[0] == nil { + docsEntries[0] = entry + } + case ".gitea": + if entry.Name() == ".gitea" || docsEntries[1] == nil { + docsEntries[1] = entry + } + case ".github": + if entry.Name() == ".github" || docsEntries[2] == nil { + docsEntries[2] = entry + } + } + continue + } + if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok { + log.Debug("Potential readme file: %s", entry.Name()) + if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) { + if entry.IsLink() { + target, err := entry.FollowLinks() + if err != nil && !git.IsErrBadLink(err) { + return "", nil, err + } else if target != nil && (target.IsExecutable() || target.IsRegular()) { + readmeFiles[i] = entry + } + } else { + readmeFiles[i] = entry + } + } + } + } + var readmeFile *git.TreeEntry + for _, f := range readmeFiles { + if f != nil { + readmeFile = f + break + } + } + + if ctx.Repo.TreePath == "" && readmeFile == nil { + for _, subTreeEntry := range docsEntries { + if subTreeEntry == nil { + continue + } + subTree := subTreeEntry.Tree() + if subTree == nil { + // this should be impossible; if subTreeEntry exists so should this. + continue + } + childEntries, err := subTree.ListEntries() + if err != nil { + return "", nil, err + } + + subfolder, readmeFile, err := findReadmeFileInEntries(ctx, childEntries, false) + if err != nil && !git.IsErrNotExist(err) { + return "", nil, err + } + if readmeFile != nil { + return path.Join(subTreeEntry.Name(), subfolder), readmeFile, nil + } + } + } + + return "", readmeFile, nil +} + +// localizedExtensions prepends the provided language code with and without a +// regional identifier to the provided extension. +// Note: the language code will always be lower-cased, if a region is present it must be separated with a `-` +// Note: ext should be prefixed with a `.` +func localizedExtensions(ext, languageCode string) (localizedExts []string) { + if len(languageCode) < 1 { + return []string{ext} + } + + lowerLangCode := "." + strings.ToLower(languageCode) + + if strings.Contains(lowerLangCode, "-") { + underscoreLangCode := strings.ReplaceAll(lowerLangCode, "-", "_") + indexOfDash := strings.Index(lowerLangCode, "-") + // e.g. [.zh-cn.md, .zh_cn.md, .zh.md, _zh.md, .md] + return []string{lowerLangCode + ext, underscoreLangCode + ext, lowerLangCode[:indexOfDash] + ext, "_" + lowerLangCode[1:indexOfDash] + ext, ext} + } + + // e.g. [.en.md, .md] + return []string{lowerLangCode + ext, ext} +} + +func prepareToRenderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) { + target := readmeFile + if readmeFile != nil && readmeFile.IsLink() { + target, _ = readmeFile.FollowLinks() + } + if target == nil { + // if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't) + // simply skip rendering the README + return + } + + ctx.Data["RawFileLink"] = "" + ctx.Data["ReadmeInList"] = true + ctx.Data["ReadmeExist"] = true + ctx.Data["FileIsSymlink"] = readmeFile.IsLink() + + buf, dataRc, fInfo, err := getFileReader(ctx, ctx.Repo.Repository.ID, target.Blob()) + if err != nil { + ctx.ServerError("getFileReader", err) + return + } + defer dataRc.Close() + + ctx.Data["FileIsText"] = fInfo.isTextFile + ctx.Data["FileName"] = path.Join(subfolder, readmeFile.Name()) + ctx.Data["FileSize"] = fInfo.fileSize + ctx.Data["IsLFSFile"] = fInfo.isLFSFile + + if fInfo.isLFSFile { + filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.Name())) + ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64)) + } + + if !fInfo.isTextFile { + return + } + + if fInfo.fileSize >= setting.UI.MaxDisplayFileSize { + // Pretend that this is a normal text file to display 'This file is too large to be shown' + ctx.Data["IsFileTooLarge"] = true + ctx.Data["IsTextFile"] = true + return + } + + rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{}) + + if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" { + ctx.Data["IsMarkup"] = true + ctx.Data["MarkupType"] = markupType + + rctx := renderhelper.NewRenderContextRepoFile(ctx, ctx.Repo.Repository, renderhelper.RepoFileOptions{ + CurrentRefPath: ctx.Repo.BranchNameSubURL(), + CurrentTreePath: path.Join(ctx.Repo.TreePath, subfolder), + }). + WithMarkupType(markupType). + WithRelativePath(path.Join(ctx.Repo.TreePath, subfolder, readmeFile.Name())) // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path). + + ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, rctx, rd) + if err != nil { + log.Error("Render failed for %s in %-v: %v Falling back to rendering source", readmeFile.Name(), ctx.Repo.Repository, err) + delete(ctx.Data, "IsMarkup") + } + } + + if ctx.Data["IsMarkup"] != true { + ctx.Data["IsPlainText"] = true + content, err := io.ReadAll(rd) + if err != nil { + log.Error("Read readme content failed: %v", err) + } + contentEscaped := template.HTMLEscapeString(util.UnsafeBytesToString(content)) + ctx.Data["EscapeStatus"], ctx.Data["FileContent"] = charset.EscapeControlHTML(template.HTML(contentEscaped), ctx.Locale) + } + + if !fInfo.isLFSFile && ctx.Repo.CanEnableEditor(ctx, ctx.Doer) { + ctx.Data["CanEditReadmeFile"] = true + } +} diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index b2dd846fafd..fec72c92532 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -6,6 +6,7 @@ package repo import ( "bytes" + gocontext "context" "fmt" "io" "net/http" @@ -645,22 +646,32 @@ func WikiPages(ctx *context.Context) { return } - entries, err := commit.ListEntries() + treePath := "" // To support list sub folders' pages in the future + tree, err := commit.SubTree(treePath) + if err != nil { + ctx.ServerError("SubTree", err) + return + } + + allEntries, err := tree.ListEntries() if err != nil { ctx.ServerError("ListEntries", err) return } + allEntries.CustomSort(base.NaturalSortLess) + + entries, _, err := allEntries.GetCommitsInfo(gocontext.Context(ctx), commit, treePath) + if err != nil { + ctx.ServerError("GetCommitsInfo", err) + return + } + pages := make([]PageMeta, 0, len(entries)) for _, entry := range entries { - if !entry.IsRegular() { + if !entry.Entry.IsRegular() { continue } - c, err := wikiRepo.GetCommitByPath(entry.Name()) - if err != nil { - ctx.ServerError("GetCommit", err) - return - } - wikiName, err := wiki_service.GitPathToWebPath(entry.Name()) + wikiName, err := wiki_service.GitPathToWebPath(entry.Entry.Name()) if err != nil { if repo_model.IsErrWikiInvalidFileName(err) { continue @@ -672,8 +683,8 @@ func WikiPages(ctx *context.Context) { pages = append(pages, PageMeta{ Name: displayName, SubURL: wiki_service.WebPathToURLPath(wikiName), - GitEntryName: entry.Name(), - UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()), + GitEntryName: entry.Entry.Name(), + UpdatedUnix: timeutil.TimeStamp(entry.Commit.Author.When.Unix()), }) } ctx.Data["Pages"] = pages diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index b81f2ea02e3..958ff802d40 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -135,7 +135,7 @@ func TestNewWikiPost(t *testing.T) { NewWikiPost(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content) + assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) } } @@ -194,7 +194,7 @@ func TestEditWikiPost(t *testing.T) { EditWikiPost(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content) + assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) if title != "Home" { assertWikiNotExists(t, ctx.Repo.Repository, "Home") } diff --git a/routers/web/shared/issue/issue_label.go b/routers/web/shared/issue/issue_label.go new file mode 100644 index 00000000000..eacea36b024 --- /dev/null +++ b/routers/web/shared/issue/issue_label.go @@ -0,0 +1,71 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issue + +import ( + "strings" + + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/services/context" +) + +// PrepareFilterIssueLabels reads the "labels" query parameter, sets `ctx.Data["Labels"]` and `ctx.Data["SelectLabels"]` +func PrepareFilterIssueLabels(ctx *context.Context, repoID int64, owner *user_model.User) (labelIDs []int64) { + // 1,-2 means including label 1 and excluding label 2 + // 0 means issues with no label + // blank means labels will not be filtered for issues + selectLabels := ctx.FormString("labels") + if selectLabels != "" { + var err error + labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ",")) + if err != nil { + ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true) + } + } + + var allLabels []*issues_model.Label + if repoID != 0 { + repoLabels, err := issues_model.GetLabelsByRepoID(ctx, repoID, "", db.ListOptions{}) + if err != nil { + ctx.ServerError("GetLabelsByRepoID", err) + return nil + } + allLabels = append(allLabels, repoLabels...) + } + + if owner != nil && owner.IsOrganization() { + orgLabels, err := issues_model.GetLabelsByOrgID(ctx, owner.ID, "", db.ListOptions{}) + if err != nil { + ctx.ServerError("GetLabelsByOrgID", err) + return nil + } + allLabels = append(allLabels, orgLabels...) + } + + // Get the exclusive scope for every label ID + labelExclusiveScopes := make([]string, 0, len(labelIDs)) + for _, labelID := range labelIDs { + foundExclusiveScope := false + for _, label := range allLabels { + if label.ID == labelID || label.ID == -labelID { + labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope()) + foundExclusiveScope = true + break + } + } + if !foundExclusiveScope { + labelExclusiveScopes = append(labelExclusiveScopes, "") + } + } + + for _, l := range allLabels { + l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes) + } + ctx.Data["Labels"] = allLabels + ctx.Data["SelectLabels"] = selectLabels + return labelIDs +} diff --git a/routers/web/shared/user/helper.go b/routers/web/shared/user/helper.go index dfd65420c1c..b82181a1df1 100644 --- a/routers/web/shared/user/helper.go +++ b/routers/web/shared/user/helper.go @@ -4,9 +4,13 @@ package user import ( + "context" "slices" + "strconv" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/optional" ) func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { @@ -24,3 +28,25 @@ func MakeSelfOnTop(doer *user.User, users []*user.User) []*user.User { } return users } + +// GetFilterUserIDByName tries to get the user ID from the given username. +// Before, the "issue filter" passes user ID to query the list, but in many cases, it's impossible to pre-fetch the full user list. +// So it's better to make it work like GitHub: users could input username directly. +// Since it only converts the username to ID directly and is only used internally (to search issues), so no permission check is needed. +// Return values: +// * nil: no filter +// * some(id): match the id, the id could be -1 to match the issues without assignee +// * some(NonExistingID): match no issue (due to the user doesn't exist) +func GetFilterUserIDByName(ctx context.Context, name string) optional.Option[int64] { + if name == "" { + return optional.None[int64]() + } + u, err := user.GetUserByName(ctx, name) + if err != nil { + if id, err := strconv.ParseInt(name, 10, 64); err == nil { + return optional.Some(id) + } + return optional.Some(db.NonExistingID) + } + return optional.Some(u.ID) +} diff --git a/routers/web/user/home.go b/routers/web/user/home.go index 0cf932ac03b..befa33b0c0d 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -31,7 +31,10 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/web/feed" + "code.gitea.io/gitea/routers/web/shared/issue" + "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" feed_service "code.gitea.io/gitea/services/feed" issue_service "code.gitea.io/gitea/services/issue" @@ -375,16 +378,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { return } - var ( - viewType string - sortType = ctx.FormString("sort") - filterMode int - ) - // Default to recently updated, unlike repository issues list - if sortType == "" { - sortType = "recentupdate" - } + sortType := util.IfZero(ctx.FormString("sort"), "recentupdate") // -------------------------------------------------------------------------------- // Distinguish User from Organization. @@ -399,7 +394,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // TODO: distinguish during routing - viewType = ctx.FormString("type") + viewType := ctx.FormString("type") + var filterMode int switch viewType { case "assigned": filterMode = issues_model.FilterModeAssign @@ -418,6 +414,13 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { viewType = "your_repositories" } + isPullList := unitType == unit.TypePullRequests + opts := &issues_model.IssuesOptions{ + IsPull: optional.Some(isPullList), + SortType: sortType, + IsArchived: optional.Some(false), + User: ctx.Doer, + } // -------------------------------------------------------------------------- // Build opts (IssuesOptions), which contains filter information. // Will eventually be used to retrieve issues relevant for the overview page. @@ -427,22 +430,24 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // -------------------------------------------------------------------------- // Get repository IDs where User/Org/Team has access. - var team *organization.Team - var org *organization.Organization - if ctx.Org != nil { - org = ctx.Org.Organization - team = ctx.Org.Team - } + if ctx.Org != nil && ctx.Org.Organization != nil { + opts.Org = ctx.Org.Organization + opts.Team = ctx.Org.Team - isPullList := unitType == unit.TypePullRequests - opts := &issues_model.IssuesOptions{ - IsPull: optional.Some(isPullList), - SortType: sortType, - IsArchived: optional.Some(false), - Org: org, - Team: team, - User: ctx.Doer, + issue.PrepareFilterIssueLabels(ctx, 0, ctx.Org.Organization.AsUser()) + if ctx.Written() { + return + } } + // Get filter by author id & assignee id + // the existing "/posters" handlers doesn't work for this case, it is unable to list the related users correctly. + // In the future, we need something like github: "author:user1" to accept usernames directly. + posterUsername := ctx.FormString("poster") + ctx.Data["FilterPosterUsername"] = posterUsername + opts.PosterID = user.GetFilterUserIDByName(ctx, posterUsername) + assigneeUsername := ctx.FormString("assignee") + ctx.Data["FilterAssigneeUsername"] = assigneeUsername + opts.AssigneeID = user.GetFilterUserIDByName(ctx, assigneeUsername) isFuzzy := ctx.FormBool("fuzzy") @@ -468,8 +473,8 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { UnitType: unitType, Archived: optional.Some(false), } - if team != nil { - repoOpts.TeamID = team.ID + if opts.Team != nil { + repoOpts.TeamID = opts.Team.ID } accessibleRepos := container.Set[int64]{} { @@ -497,9 +502,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { case issues_model.FilterModeAll: case issues_model.FilterModeYourRepositories: case issues_model.FilterModeAssign: - opts.AssigneeID = ctx.Doer.ID + opts.AssigneeID = optional.Some(ctx.Doer.ID) case issues_model.FilterModeCreate: - opts.PosterID = ctx.Doer.ID + opts.PosterID = optional.Some(ctx.Doer.ID) case issues_model.FilterModeMention: opts.MentionedID = ctx.Doer.ID case issues_model.FilterModeReviewRequested: @@ -573,8 +578,18 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ------------------------------- // Fill stats to post to ctx.Data. // ------------------------------- - issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( - func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy }, + issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( + func(o *issue_indexer.SearchOptions) { + o.IsFuzzyKeyword = isFuzzy + // If the doer is the same as the context user, which means the doer is viewing his own dashboard, + // it's not enough to show the repos that the doer owns or has been explicitly granted access to, + // because the doer may create issues or be mentioned in any public repo. + // So we need search issues in all public repos. + o.AllPublic = ctx.Doer.ID == ctxUser.ID + o.MentionID = nil + o.ReviewRequestedID = nil + o.ReviewedID = nil + }, )) if err != nil { ctx.ServerError("getUserIssueStats", err) @@ -628,7 +643,6 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { ctx.Data["ViewType"] = viewType ctx.Data["SortType"] = sortType ctx.Data["IsShowClosed"] = isShowClosed - ctx.Data["SelectLabels"] = selectedLabels ctx.Data["IsFuzzy"] = isFuzzy if isShowClosed { @@ -638,12 +652,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { } pager := context.NewPagination(shownIssues, setting.UI.IssuePagingNum, page, 5) - pager.AddParamString("q", keyword) - pager.AddParamString("type", viewType) - pager.AddParamString("sort", sortType) - pager.AddParamString("state", fmt.Sprint(ctx.Data["State"])) - pager.AddParamString("labels", selectedLabels) - pager.AddParamString("fuzzy", fmt.Sprintf("%v", isFuzzy)) + pager.AddParamFromRequest(ctx.Req) ctx.Data["Page"] = pager ctx.HTML(http.StatusOK, tplIssues) @@ -768,27 +777,10 @@ func UsernameSubRoute(ctx *context.Context) { } } -func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (*issues_model.IssueStats, error) { +func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) { + ret = &issues_model.IssueStats{} doerID := ctx.Doer.ID - opts = opts.Copy(func(o *issue_indexer.SearchOptions) { - // If the doer is the same as the context user, which means the doer is viewing his own dashboard, - // it's not enough to show the repos that the doer owns or has been explicitly granted access to, - // because the doer may create issues or be mentioned in any public repo. - // So we need search issues in all public repos. - o.AllPublic = doerID == ctxUser.ID - o.AssigneeID = nil - o.PosterID = nil - o.MentionID = nil - o.ReviewRequestedID = nil - o.ReviewedID = nil - }) - - var ( - err error - ret = &issues_model.IssueStats{} - ) - { openClosedOpts := opts.Copy() switch filterMode { diff --git a/routers/web/web.go b/routers/web/web.go index 85e0fdc41e8..72ee47bb4c9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1208,7 +1208,7 @@ func registerRoutes(m *web.Router) { Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost) m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) }) - m.Get("/search", repo.ListIssues) + m.Get("/search", repo.SearchRepoIssuesJSON) }, context.RepoMustNotBeArchived(), reqRepoIssueReader) // FIXME: should use different URLs but mostly same logic for comments of issue and pull request. @@ -1320,6 +1320,7 @@ func registerRoutes(m *web.Router) { m.Post("/delete", repo.DeleteBranchPost) m.Post("/restore", repo.RestoreBranchPost) m.Post("/rename", web.Bind(forms.RenameBranchForm{}), repo_setting.RenameBranchPost) + m.Post("/merge-upstream", repo.MergeUpstream) }, context.RepoMustNotBeArchived(), reqRepoCodeWriter, repo.MustBeNotEmpty) m.Combo("/fork").Get(repo.Fork).Post(web.Bind(forms.CreateRepoForm{}), repo.ForkPost) diff --git a/services/actions/auth_test.go b/services/actions/auth_test.go index 12db2bae565..85e74091052 100644 --- a/services/actions/auth_test.go +++ b/services/actions/auth_test.go @@ -17,19 +17,19 @@ import ( func TestCreateAuthorizationToken(t *testing.T) { var taskID int64 = 23 token, err := CreateAuthorizationToken(taskID, 1, 2) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotEqual(t, "", token) claims := jwt.MapClaims{} _, err = jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) { return setting.GetGeneralTokenSigningSecret(), nil }) - assert.Nil(t, err) + assert.NoError(t, err) scp, ok := claims["scp"] assert.True(t, ok, "Has scp claim in jwt token") assert.Contains(t, scp, "Actions.Results:1:2") taskIDClaim, ok := claims["TaskID"] assert.True(t, ok, "Has TaskID claim in jwt token") - assert.Equal(t, float64(taskID), taskIDClaim, "Supplied taskid must match stored one") + assert.InDelta(t, float64(taskID), taskIDClaim, 0, "Supplied taskid must match stored one") acClaim, ok := claims["ac"] assert.True(t, ok, "Has ac claim in jwt token") ac, ok := acClaim.(string) @@ -43,14 +43,14 @@ func TestCreateAuthorizationToken(t *testing.T) { func TestParseAuthorizationToken(t *testing.T) { var taskID int64 = 23 token, err := CreateAuthorizationToken(taskID, 1, 2) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotEqual(t, "", token) headers := http.Header{} headers.Set("Authorization", "Bearer "+token) rTaskID, err := ParseAuthorizationToken(&http.Request{ Header: headers, }) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, taskID, rTaskID) } @@ -59,6 +59,6 @@ func TestParseAuthorizationTokenNoAuthHeader(t *testing.T) { rTaskID, err := ParseAuthorizationToken(&http.Request{ Header: headers, }) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, int64(0), rTaskID) } diff --git a/services/auth/oauth2_test.go b/services/auth/oauth2_test.go index 75c231ff7a4..b706847e8e1 100644 --- a/services/auth/oauth2_test.go +++ b/services/auth/oauth2_test.go @@ -28,7 +28,7 @@ func TestUserIDFromToken(t *testing.T) { o := OAuth2{} uid := o.userIDFromToken(context.Background(), token, ds) assert.Equal(t, int64(user_model.ActionsUserID), uid) - assert.Equal(t, ds["IsActionsToken"], true) + assert.Equal(t, true, ds["IsActionsToken"]) assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID)) }) } diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go index dc4cb2c9403..963cdba7c21 100644 --- a/services/auth/source/ldap/source.go +++ b/services/auth/source/ldap/source.go @@ -56,8 +56,7 @@ type Source struct { UserUID string // User Attribute listed in Group SkipLocalTwoFA bool `json:",omitempty"` // Skip Local 2fa for users authenticated with this source - // reference to the authSource - authSource *auth.Source + authSource *auth.Source // reference to the authSource } // FromDB fills up a LDAPConfig from serialized format. @@ -107,7 +106,7 @@ func (source *Source) UseTLS() bool { // ProvidesSSHKeys returns if this source provides SSH Keys func (source *Source) ProvidesSSHKeys() bool { - return len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 + return strings.TrimSpace(source.AttributeSSHPublicKey) != "" } // SetAuthSource sets the related AuthSource diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go index 01cb7437205..020e5784dcf 100644 --- a/services/auth/source/ldap/source_authenticate.go +++ b/services/auth/source/ldap/source_authenticate.go @@ -31,13 +31,13 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u return nil, user_model.ErrUserNotExist{Name: loginName} } // Fallback. - if len(sr.Username) == 0 { + if sr.Username == "" { sr.Username = userName } - if len(sr.Mail) == 0 { + if sr.Mail == "" { sr.Mail = fmt.Sprintf("%s@localhost.local", sr.Username) } - isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 + isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != "" // Update User admin flag if exist if isExist, err := user_model.IsUserExist(ctx, 0, sr.Username); err != nil { @@ -51,11 +51,11 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u } if user != nil && !user.ProhibitLogin { opts := &user_service.UpdateOptions{} - if len(source.AdminFilter) > 0 && user.IsAdmin != sr.IsAdmin { + if source.AdminFilter != "" && user.IsAdmin != sr.IsAdmin { // Change existing admin flag only if AdminFilter option is set opts.IsAdmin = optional.Some(sr.IsAdmin) } - if !sr.IsAdmin && len(source.RestrictedFilter) > 0 && user.IsRestricted != sr.IsRestricted { + if !sr.IsAdmin && source.RestrictedFilter != "" && user.IsRestricted != sr.IsRestricted { // Change existing restricted flag only if RestrictedFilter option is set opts.IsRestricted = optional.Some(sr.IsRestricted) } @@ -99,7 +99,7 @@ func (source *Source) Authenticate(ctx context.Context, user *user_model.User, u return user, err } } - if len(source.AttributeAvatar) > 0 { + if source.AttributeAvatar != "" { if err := user_service.UploadAvatar(ctx, user, sr.Avatar); err != nil { return user, err } diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go index b20c90e791d..fa2c45ce4ad 100644 --- a/services/auth/source/ldap/source_search.go +++ b/services/auth/source/ldap/source_search.go @@ -147,7 +147,7 @@ func bindUser(l *ldap.Conn, userDN, passwd string) error { } func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool { - if len(ls.AdminFilter) == 0 { + if ls.AdminFilter == "" { return false } log.Trace("Checking admin with filter %s and base %s", ls.AdminFilter, userDN) @@ -169,7 +169,7 @@ func checkAdmin(l *ldap.Conn, ls *Source, userDN string) bool { } func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool { - if len(ls.RestrictedFilter) == 0 { + if ls.RestrictedFilter == "" { return false } if ls.RestrictedFilter == "*" { @@ -250,8 +250,17 @@ func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string { // SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult { + if MockedSearchEntry != nil { + return MockedSearchEntry(source, name, passwd, directBind) + } + return realSearchEntry(source, name, passwd, directBind) +} + +var MockedSearchEntry func(source *Source, name, passwd string, directBind bool) *SearchResult + +func realSearchEntry(source *Source, name, passwd string, directBind bool) *SearchResult { // See https://tools.ietf.org/search/rfc4513#section-5.1.2 - if len(passwd) == 0 { + if passwd == "" { log.Debug("Auth. failed for %s, password cannot be empty", name) return nil } @@ -323,17 +332,17 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR return nil } - isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 - isAtributeAvatarSet := len(strings.TrimSpace(source.AttributeAvatar)) > 0 + isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != "" + isAttributeAvatarSet := strings.TrimSpace(source.AttributeAvatar) != "" attribs := []string{source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail} - if len(strings.TrimSpace(source.UserUID)) > 0 { + if strings.TrimSpace(source.UserUID) != "" { attribs = append(attribs, source.UserUID) } if isAttributeSSHPublicKeySet { attribs = append(attribs, source.AttributeSSHPublicKey) } - if isAtributeAvatarSet { + if isAttributeAvatarSet { attribs = append(attribs, source.AttributeAvatar) } @@ -375,7 +384,7 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR isRestricted = checkRestricted(l, source, userDN) } - if isAtributeAvatarSet { + if isAttributeAvatarSet { Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar) } @@ -440,14 +449,14 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) { userFilter := fmt.Sprintf(source.Filter, "*") - isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 - isAtributeAvatarSet := len(strings.TrimSpace(source.AttributeAvatar)) > 0 + isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != "" + isAttributeAvatarSet := strings.TrimSpace(source.AttributeAvatar) != "" attribs := []string{source.AttributeUsername, source.AttributeName, source.AttributeSurname, source.AttributeMail, source.UserUID} if isAttributeSSHPublicKeySet { attribs = append(attribs, source.AttributeSSHPublicKey) } - if isAtributeAvatarSet { + if isAttributeAvatarSet { attribs = append(attribs, source.AttributeAvatar) } @@ -503,7 +512,7 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) { user.SSHPublicKey = v.GetAttributeValues(source.AttributeSSHPublicKey) } - if isAtributeAvatarSet { + if isAttributeAvatarSet { user.Avatar = v.GetRawAttributeValue(source.AttributeAvatar) } diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go index a6d6d2a0f2f..e817bf1fa93 100644 --- a/services/auth/source/ldap/source_sync.go +++ b/services/auth/source/ldap/source_sync.go @@ -25,7 +25,7 @@ import ( func (source *Source) Sync(ctx context.Context, updateExisting bool) error { log.Trace("Doing: SyncExternalUsers[%s]", source.authSource.Name) - isAttributeSSHPublicKeySet := len(strings.TrimSpace(source.AttributeSSHPublicKey)) > 0 + isAttributeSSHPublicKeySet := strings.TrimSpace(source.AttributeSSHPublicKey) != "" var sshKeysNeedUpdate bool // Find all users with this login type - FIXME: Should this be an iterator? @@ -86,26 +86,26 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { return db.ErrCancelledf("During update of %s before completed update of users", source.authSource.Name) default: } - if len(su.Username) == 0 && len(su.Mail) == 0 { + if su.Username == "" && su.Mail == "" { continue } var usr *user_model.User - if len(su.Username) > 0 { + if su.Username != "" { usr = usernameUsers[su.LowerName] } - if usr == nil && len(su.Mail) > 0 { + if usr == nil && su.Mail != "" { usr = mailUsers[strings.ToLower(su.Mail)] } if usr != nil { keepActiveUsers.Add(usr.ID) - } else if len(su.Username) == 0 { + } else if su.Username == "" { // we cannot create the user if su.Username is empty continue } - if len(su.Mail) == 0 { + if su.Mail == "" { su.Mail = fmt.Sprintf("%s@localhost.local", su.Username) } @@ -141,7 +141,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } } - if err == nil && len(source.AttributeAvatar) > 0 { + if err == nil && source.AttributeAvatar != "" { _ = user_service.UploadAvatar(ctx, usr, su.Avatar) } } else if updateExisting { @@ -151,8 +151,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } // Check if user data has changed - if (len(source.AdminFilter) > 0 && usr.IsAdmin != su.IsAdmin) || - (len(source.RestrictedFilter) > 0 && usr.IsRestricted != su.IsRestricted) || + if (source.AdminFilter != "" && usr.IsAdmin != su.IsAdmin) || + (source.RestrictedFilter != "" && usr.IsRestricted != su.IsRestricted) || !strings.EqualFold(usr.Email, su.Mail) || usr.FullName != fullName || !usr.IsActive { @@ -180,7 +180,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error { } if usr.IsUploadAvatarChanged(su.Avatar) { - if err == nil && len(source.AttributeAvatar) > 0 { + if err == nil && source.AttributeAvatar != "" { _ = user_service.UploadAvatar(ctx, usr, su.Avatar) } } diff --git a/services/auth/source/ldap/util.go b/services/auth/source/ldap/util.go index bd11e2d1193..05ae32c0fd2 100644 --- a/services/auth/source/ldap/util.go +++ b/services/auth/source/ldap/util.go @@ -6,11 +6,11 @@ package ldap // composeFullName composes a firstname surname or username func composeFullName(firstname, surname, username string) string { switch { - case len(firstname) == 0 && len(surname) == 0: + case firstname == "" && surname == "": return username - case len(firstname) == 0: + case firstname == "": return surname - case len(surname) == 0: + case surname == "": return firstname default: return firstname + " " + surname diff --git a/services/auth/source/oauth2/source_sync_test.go b/services/auth/source/oauth2/source_sync_test.go index 25408e8727e..893ed625028 100644 --- a/services/auth/source/oauth2/source_sync_test.go +++ b/services/auth/source/oauth2/source_sync_test.go @@ -64,8 +64,8 @@ func TestSource(t *testing.T) { ok, err := user_model.GetExternalLogin(context.Background(), e) assert.NoError(t, err) assert.True(t, ok) - assert.Equal(t, e.RefreshToken, "refresh") - assert.Equal(t, e.AccessToken, "token") + assert.Equal(t, "refresh", e.RefreshToken) + assert.Equal(t, "token", e.AccessToken) u, err := user_model.GetUserByID(context.Background(), user.ID) assert.NoError(t, err) @@ -89,8 +89,8 @@ func TestSource(t *testing.T) { ok, err := user_model.GetExternalLogin(context.Background(), e) assert.NoError(t, err) assert.True(t, ok) - assert.Equal(t, e.RefreshToken, "") - assert.Equal(t, e.AccessToken, "") + assert.Equal(t, "", e.RefreshToken) + assert.Equal(t, "", e.AccessToken) u, err := user_model.GetUserByID(context.Background(), user.ID) assert.NoError(t, err) diff --git a/services/context/pagination.go b/services/context/pagination.go index fb2ef699ce4..42117cf96de 100644 --- a/services/context/pagination.go +++ b/services/context/pagination.go @@ -6,6 +6,7 @@ package context import ( "fmt" "html/template" + "net/http" "net/url" "strings" @@ -32,6 +33,18 @@ func (p *Pagination) AddParamString(key, value string) { p.urlParams = append(p.urlParams, urlParam) } +func (p *Pagination) AddParamFromRequest(req *http.Request) { + for key, values := range req.URL.Query() { + if key == "page" || len(values) == 0 { + continue + } + for _, value := range values { + urlParam := fmt.Sprintf("%s=%v", key, url.QueryEscape(value)) + p.urlParams = append(p.urlParams, urlParam) + } + } +} + // GetParams returns the configured URL params func (p *Pagination) GetParams() template.URL { return template.URL(strings.Join(p.urlParams, "&")) diff --git a/services/context/repo.go b/services/context/repo.go index 1eafb7ca48b..9b544391103 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -39,10 +39,9 @@ import ( // PullRequest contains information to make a pull request type PullRequest struct { - BaseRepo *repo_model.Repository - Allowed bool - SameRepo bool - HeadInfoSubURL string // [:] url segment + BaseRepo *repo_model.Repository + Allowed bool // it only used by the web tmpl: "PullRequestCtx.Allowed" + SameRepo bool // it only used by the web tmpl: "PullRequestCtx.SameRepo" } // Repository contains information to operate a repository @@ -396,18 +395,12 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) { ctx.Repo.Repository = repo ctx.Data["RepoName"] = ctx.Repo.Repository.Name ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty - - repoLicenses, err := repo_model.GetRepoLicenses(ctx, ctx.Repo.Repository) - if err != nil { - ctx.ServerError("GetRepoLicenses", err) - return - } - ctx.Data["DetectedRepoLicenses"] = repoLicenses.StringList() } // RepoAssignment returns a middleware to handle repository assignment func RepoAssignment(ctx *Context) context.CancelFunc { if _, repoAssignmentOnce := ctx.Data["repoAssignmentExecuted"]; repoAssignmentOnce { + // FIXME: it should panic in dev/test modes to have a clear behavior log.Trace("RepoAssignment was exec already, skipping second call ...") return nil } @@ -704,7 +697,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Data["BaseRepo"] = repo.BaseRepo ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo ctx.Repo.PullRequest.Allowed = canPush - ctx.Repo.PullRequest.HeadInfoSubURL = url.PathEscape(ctx.Repo.Owner.Name) + ":" + util.PathEscapeSegments(ctx.Repo.BranchName) } else if repo.AllowsPulls(ctx) { // Or, this is repository accepts pull requests between branches. canCompare = true @@ -712,7 +704,6 @@ func RepoAssignment(ctx *Context) context.CancelFunc { ctx.Repo.PullRequest.BaseRepo = repo ctx.Repo.PullRequest.Allowed = canPush ctx.Repo.PullRequest.SameRepo = true - ctx.Repo.PullRequest.HeadInfoSubURL = util.PathEscapeSegments(ctx.Repo.BranchName) } ctx.Data["CanCompareOrPull"] = canCompare ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest @@ -778,20 +769,6 @@ func getRefNameFromPath(repo *Repository, path string, isExist func(string) bool return "" } -func isStringLikelyCommitID(objFmt git.ObjectFormat, s string, minLength ...int) bool { - minLen := util.OptionalArg(minLength, objFmt.FullLength()) - if len(s) < minLen || len(s) > objFmt.FullLength() { - return false - } - for _, c := range s { - isHex := (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') - if !isHex { - return false - } - } - return true -} - func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) (string, RepoRefType) { extraRef := util.OptionalArg(optionalExtraRef) reqPath := ctx.PathParam("*") @@ -806,7 +783,7 @@ func getRefNameLegacy(ctx *Base, repo *Repository, optionalExtraRef ...string) ( // For legacy support only full commit sha parts := strings.Split(reqPath, "/") - if isStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) { + if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.Repository.ObjectFormatName), parts[0]) { // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists repo.TreePath = strings.Join(parts[1:], "/") return parts[0], RepoRefCommit @@ -856,7 +833,7 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string { return getRefNameFromPath(repo, path, repo.GitRepo.IsTagExist) case RepoRefCommit: parts := strings.Split(path, "/") - if isStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) { + if git.IsStringLikelyCommitID(repo.GetObjectFormat(), parts[0], 7) { // FIXME: this logic is different from other types. Ideally, it should also try to GetCommit to check if it exists repo.TreePath = strings.Join(parts[1:], "/") return parts[0] @@ -992,7 +969,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func return cancel } ctx.Repo.CommitID = ctx.Repo.Commit.ID.String() - } else if isStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { + } else if git.IsStringLikelyCommitID(ctx.Repo.GetObjectFormat(), refName, 7) { ctx.Repo.IsViewCommit = true ctx.Repo.CommitID = refName @@ -1036,7 +1013,7 @@ func RepoRefByType(detectRefType RepoRefType, opts ...RepoRefByTypeOptions) func ctx.Data["IsViewBranch"] = ctx.Repo.IsViewBranch ctx.Data["IsViewTag"] = ctx.Repo.IsViewTag ctx.Data["IsViewCommit"] = ctx.Repo.IsViewCommit - ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() + ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount() if err != nil { diff --git a/services/convert/pull.go b/services/convert/pull.go index 4ec24a8276a..ddaaa300a44 100644 --- a/services/convert/pull.go +++ b/services/convert/pull.go @@ -7,9 +7,11 @@ import ( "context" "fmt" + git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" @@ -259,3 +261,252 @@ func ToAPIPullRequest(ctx context.Context, pr *issues_model.PullRequest, doer *u return apiPullRequest } + +func ToAPIPullRequests(ctx context.Context, baseRepo *repo_model.Repository, prs issues_model.PullRequestList, doer *user_model.User) ([]*api.PullRequest, error) { + for _, pr := range prs { + pr.BaseRepo = baseRepo + if pr.BaseRepoID == pr.HeadRepoID { + pr.HeadRepo = baseRepo + } + } + + // NOTE: load head repositories + if err := prs.LoadRepositories(ctx); err != nil { + return nil, err + } + issueList, err := prs.LoadIssues(ctx) + if err != nil { + return nil, err + } + + if err := issueList.LoadLabels(ctx); err != nil { + return nil, err + } + if err := issueList.LoadPosters(ctx); err != nil { + return nil, err + } + if err := issueList.LoadAttachments(ctx); err != nil { + return nil, err + } + if err := issueList.LoadMilestones(ctx); err != nil { + return nil, err + } + if err := issueList.LoadAssignees(ctx); err != nil { + return nil, err + } + + reviews, err := prs.LoadReviews(ctx) + if err != nil { + return nil, err + } + if err = reviews.LoadReviewers(ctx); err != nil { + return nil, err + } + + reviewersMap := make(map[int64][]*user_model.User) + for _, review := range reviews { + if review.Reviewer != nil { + reviewersMap[review.IssueID] = append(reviewersMap[review.IssueID], review.Reviewer) + } + } + + reviewCounts, err := prs.LoadReviewCommentsCounts(ctx) + if err != nil { + return nil, err + } + + gitRepo, err := gitrepo.OpenRepository(ctx, baseRepo) + if err != nil { + return nil, err + } + defer gitRepo.Close() + + baseRepoPerm, err := access_model.GetUserRepoPermission(ctx, baseRepo, doer) + if err != nil { + log.Error("GetUserRepoPermission[%d]: %v", baseRepo.ID, err) + baseRepoPerm.AccessMode = perm.AccessModeNone + } + + apiRepo := ToRepo(ctx, baseRepo, baseRepoPerm) + baseBranchCache := make(map[string]*git_model.Branch) + apiPullRequests := make([]*api.PullRequest, 0, len(prs)) + for _, pr := range prs { + apiIssue := ToAPIIssue(ctx, doer, pr.Issue) + + apiPullRequest := &api.PullRequest{ + ID: pr.ID, + URL: pr.Issue.HTMLURL(), + Index: pr.Index, + Poster: apiIssue.Poster, + Title: apiIssue.Title, + Body: apiIssue.Body, + Labels: apiIssue.Labels, + Milestone: apiIssue.Milestone, + Assignee: apiIssue.Assignee, + Assignees: apiIssue.Assignees, + State: apiIssue.State, + Draft: pr.IsWorkInProgress(ctx), + IsLocked: apiIssue.IsLocked, + Comments: apiIssue.Comments, + ReviewComments: reviewCounts[pr.IssueID], + HTMLURL: pr.Issue.HTMLURL(), + DiffURL: pr.Issue.DiffURL(), + PatchURL: pr.Issue.PatchURL(), + HasMerged: pr.HasMerged, + MergeBase: pr.MergeBase, + Mergeable: pr.Mergeable(ctx), + Deadline: apiIssue.Deadline, + Created: pr.Issue.CreatedUnix.AsTimePtr(), + Updated: pr.Issue.UpdatedUnix.AsTimePtr(), + PinOrder: apiIssue.PinOrder, + + AllowMaintainerEdit: pr.AllowMaintainerEdit, + + Base: &api.PRBranchInfo{ + Name: pr.BaseBranch, + Ref: pr.BaseBranch, + RepoID: pr.BaseRepoID, + Repository: apiRepo, + }, + Head: &api.PRBranchInfo{ + Name: pr.HeadBranch, + Ref: fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index), + RepoID: -1, + }, + } + + pr.RequestedReviewers = reviewersMap[pr.IssueID] + for _, reviewer := range pr.RequestedReviewers { + apiPullRequest.RequestedReviewers = append(apiPullRequest.RequestedReviewers, ToUser(ctx, reviewer, nil)) + } + + for _, reviewerTeam := range pr.RequestedReviewersTeams { + convertedTeam, err := ToTeam(ctx, reviewerTeam, true) + if err != nil { + log.Error("LoadRequestedReviewersTeams[%d]: %v", pr.ID, err) + return nil, err + } + + apiPullRequest.RequestedReviewersTeams = append(apiPullRequest.RequestedReviewersTeams, convertedTeam) + } + + if pr.Issue.ClosedUnix != 0 { + apiPullRequest.Closed = pr.Issue.ClosedUnix.AsTimePtr() + } + + baseBranch, ok := baseBranchCache[pr.BaseBranch] + if !ok { + baseBranch, err = git_model.GetBranch(ctx, baseRepo.ID, pr.BaseBranch) + if err == nil { + baseBranchCache[pr.BaseBranch] = baseBranch + } else if !git_model.IsErrBranchNotExist(err) { + return nil, err + } + } + + if baseBranch != nil { + apiPullRequest.Base.Sha = baseBranch.CommitID + } + + if pr.Flow == issues_model.PullRequestFlowAGit { + apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitRefName()) + if err != nil { + log.Error("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err) + return nil, err + } + apiPullRequest.Head.RepoID = pr.BaseRepoID + apiPullRequest.Head.Repository = apiPullRequest.Base.Repository + apiPullRequest.Head.Name = "" + } + + var headGitRepo *git.Repository + if pr.HeadRepo != nil && pr.Flow == issues_model.PullRequestFlowGithub { + if pr.HeadRepoID == pr.BaseRepoID { + apiPullRequest.Head.RepoID = pr.HeadRepo.ID + apiPullRequest.Head.Repository = apiRepo + headGitRepo = gitRepo + } else { + p, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, doer) + if err != nil { + log.Error("GetUserRepoPermission[%d]: %v", pr.HeadRepoID, err) + p.AccessMode = perm.AccessModeNone + } + + apiPullRequest.Head.RepoID = pr.HeadRepo.ID + apiPullRequest.Head.Repository = ToRepo(ctx, pr.HeadRepo, p) + + headGitRepo, err = gitrepo.OpenRepository(ctx, pr.HeadRepo) + if err != nil { + log.Error("OpenRepository[%s]: %v", pr.HeadRepo.RepoPath(), err) + return nil, err + } + defer headGitRepo.Close() + } + + headBranch, err := headGitRepo.GetBranch(pr.HeadBranch) + if err != nil && !git.IsErrBranchNotExist(err) { + log.Error("GetBranch[%s]: %v", pr.HeadBranch, err) + return nil, err + } + + // Outer scope variables to be used in diff calculation + var ( + startCommitID string + endCommitID string + ) + + if git.IsErrBranchNotExist(err) { + headCommitID, err := headGitRepo.GetRefCommitID(apiPullRequest.Head.Ref) + if err != nil && !git.IsErrNotExist(err) { + log.Error("GetCommit[%s]: %v", pr.HeadBranch, err) + return nil, err + } + if err == nil { + apiPullRequest.Head.Sha = headCommitID + endCommitID = headCommitID + } + } else { + commit, err := headBranch.GetCommit() + if err != nil && !git.IsErrNotExist(err) { + log.Error("GetCommit[%s]: %v", headBranch.Name, err) + return nil, err + } + if err == nil { + apiPullRequest.Head.Ref = pr.HeadBranch + apiPullRequest.Head.Sha = commit.ID.String() + endCommitID = commit.ID.String() + } + } + + // Calculate diff + startCommitID = pr.MergeBase + + apiPullRequest.ChangedFiles, apiPullRequest.Additions, apiPullRequest.Deletions, err = gitRepo.GetDiffShortStat(startCommitID, endCommitID) + if err != nil { + log.Error("GetDiffShortStat: %v", err) + } + } + + if len(apiPullRequest.Head.Sha) == 0 && len(apiPullRequest.Head.Ref) != 0 { + refs, err := gitRepo.GetRefsFiltered(apiPullRequest.Head.Ref) + if err != nil { + log.Error("GetRefsFiltered[%s]: %v", apiPullRequest.Head.Ref, err) + return nil, err + } else if len(refs) == 0 { + log.Error("unable to resolve PR head ref") + } else { + apiPullRequest.Head.Sha = refs[0].Object.String() + } + } + + if pr.HasMerged { + apiPullRequest.Merged = pr.MergedUnix.AsTimePtr() + apiPullRequest.MergedCommitID = &pr.MergedCommitID + apiPullRequest.MergedBy = ToUser(ctx, pr.Merger, nil) + } + + apiPullRequests = append(apiPullRequests, apiPullRequest) + } + + return apiPullRequests, nil +} diff --git a/services/convert/pull_review_test.go b/services/convert/pull_review_test.go index 68869502802..a1296fafd4e 100644 --- a/services/convert/pull_review_test.go +++ b/services/convert/pull_review_test.go @@ -40,7 +40,7 @@ func Test_ToPullReview(t *testing.T) { user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) prList, err := ToPullReviewList(db.DefaultContext, reviewList, user4) assert.NoError(t, err) - assert.Len(t, prList, 0) + assert.Empty(t, prList) }) t.Run("Admin User", func(t *testing.T) { diff --git a/services/cron/tasks_test.go b/services/cron/tasks_test.go index 979371a0229..ab22403ede7 100644 --- a/services/cron/tasks_test.go +++ b/services/cron/tasks_test.go @@ -12,7 +12,7 @@ import ( ) func TestAddTaskToScheduler(t *testing.T) { - assert.Len(t, scheduler.Jobs(), 0) + assert.Empty(t, scheduler.Jobs()) defer scheduler.Clear() // no seconds diff --git a/services/feed/feed_test.go b/services/feed/feed_test.go index 6f1cb9a969b..1e4d029e18c 100644 --- a/services/feed/feed_test.go +++ b/services/feed/feed_test.go @@ -41,7 +41,7 @@ func TestGetFeeds(t *testing.T) { OnlyPerformedBy: false, }) assert.NoError(t, err) - assert.Len(t, actions, 0) + assert.Empty(t, actions) assert.Equal(t, int64(0), count) } @@ -57,7 +57,7 @@ func TestGetFeedsForRepos(t *testing.T) { IncludePrivate: true, }) assert.NoError(t, err) - assert.Len(t, actions, 0) + assert.Empty(t, actions) assert.Equal(t, int64(0), count) // public repo & no login @@ -119,7 +119,7 @@ func TestGetFeeds2(t *testing.T) { IncludeDeleted: true, }) assert.NoError(t, err) - assert.Len(t, actions, 0) + assert.Empty(t, actions) assert.Equal(t, int64(0), count) } diff --git a/services/feed/notifier.go b/services/feed/notifier.go index a8820aeb777..d941027c352 100644 --- a/services/feed/notifier.go +++ b/services/feed/notifier.go @@ -417,6 +417,12 @@ func (a *actionNotifier) SyncPushCommits(ctx context.Context, pusher *user_model } func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName, refID string) { + // ignore pull sync message for pull requests refs + // TODO: it's better to have a UI to let users chose + if refFullName.IsPull() { + return + } + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), @@ -431,6 +437,12 @@ func (a *actionNotifier) SyncCreateRef(ctx context.Context, doer *user_model.Use } func (a *actionNotifier) SyncDeleteRef(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, refFullName git.RefName) { + // ignore pull sync message for pull requests refs + // TODO: it's better to have a UI to let users chose + if refFullName.IsPull() { + return + } + if err := activities_model.NotifyWatchers(ctx, &activities_model.Action{ ActUserID: repo.OwnerID, ActUser: repo.MustOwner(ctx), diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index adcac355a7b..2351c5da879 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -5,7 +5,6 @@ package gitdiff import ( - "fmt" "strconv" "strings" "testing" @@ -643,9 +642,9 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { MaxFiles: setting.Git.MaxGitDiffFiles, WhitespaceBehavior: behavior, }) - assert.NoError(t, err, fmt.Sprintf("Error when diff with %s", behavior)) + assert.NoError(t, err, "Error when diff with %s", behavior) for _, f := range diffs.Files { - assert.True(t, len(f.Sections) > 0, fmt.Sprintf("%s should have sections", f.Name)) + assert.NotEmpty(t, f.Sections, "%s should have sections", f.Name) } } } diff --git a/services/migrations/gitlab_test.go b/services/migrations/gitlab_test.go index 0b9eeaed540..eccfc4def1f 100644 --- a/services/migrations/gitlab_test.go +++ b/services/migrations/gitlab_test.go @@ -50,7 +50,7 @@ func TestGitlabDownloadRepo(t *testing.T) { topics, err := downloader.GetTopics() assert.NoError(t, err) - assert.True(t, len(topics) == 2) + assert.Len(t, topics, 2) assert.EqualValues(t, []string{"migration", "test"}, topics) milestones, err := downloader.GetMilestones() diff --git a/services/org/team_test.go b/services/org/team_test.go index 58b8e0803c4..98addac8f8a 100644 --- a/services/org/team_test.go +++ b/services/org/team_test.go @@ -121,7 +121,7 @@ func TestDeleteTeam(t *testing.T) { repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) accessMode, err := access_model.AccessLevel(db.DefaultContext, user, repo) assert.NoError(t, err) - assert.True(t, accessMode < perm.AccessModeWrite) + assert.Less(t, accessMode, perm.AccessModeWrite) } func TestAddTeamMember(t *testing.T) { diff --git a/services/pull/reviewer_test.go b/services/pull/reviewer_test.go index 1ff373bafb7..b106e2e89f5 100644 --- a/services/pull/reviewer_test.go +++ b/services/pull/reviewer_test.go @@ -30,7 +30,7 @@ func TestRepoGetReviewers(t *testing.T) { // should not include doer and remove the poster reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 2) assert.NoError(t, err) - assert.Len(t, reviewers, 0) + assert.Empty(t, reviewers) // should not include PR poster, if PR poster would be otherwise eligible reviewers, err = pull_service.GetReviewers(ctx, repo1, 11, 4) @@ -43,7 +43,7 @@ func TestRepoGetReviewers(t *testing.T) { reviewers, err = pull_service.GetReviewers(ctx, repo2, 2, 4) assert.NoError(t, err) assert.Len(t, reviewers, 1) - assert.EqualValues(t, reviewers[0].ID, 2) + assert.EqualValues(t, 2, reviewers[0].ID) // test private org repo repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) diff --git a/services/pull/update.go b/services/pull/update.go index 311ffc2442d..abf7ad45091 100644 --- a/services/pull/update.go +++ b/services/pull/update.go @@ -65,7 +65,9 @@ func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model. return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err) } - // use merge functions but switch repos and branches + // TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment + // ideally in the future the "merge" functions should be refactored to decouple from the PullRequest + // now use a fake reverse PR to switch head&base repos/branches reversePR := &issues_model.PullRequest{ ID: pr.ID, diff --git a/services/release/release_test.go b/services/release/release_test.go index 3d0681f1e17..95a54832b9e 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -228,7 +228,7 @@ func TestRelease_Update(t *testing.T) { IsTag: false, } assert.NoError(t, CreateRelease(gitRepo, release, nil, "")) - assert.Greater(t, release.ID, int64(0)) + assert.Positive(t, release.ID) release.IsDraft = false tagName := release.TagName diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go index 2ab18edf491..1d0c6e513d8 100644 --- a/services/repository/archiver/archiver_test.go +++ b/services/repository/archiver/archiver_test.go @@ -4,7 +4,6 @@ package archiver import ( - "errors" "testing" "time" @@ -121,7 +120,7 @@ func TestArchive_Basic(t *testing.T) { // It's fine to go ahead and set it to nil now. assert.Equal(t, zipReq, zipReq2) - assert.False(t, zipReq == zipReq2) + assert.NotSame(t, zipReq, zipReq2) // Same commit, different compression formats should have different names. // Ideally, the extension would match what we originally requested. @@ -131,5 +130,5 @@ func TestArchive_Basic(t *testing.T) { func TestErrUnknownArchiveFormat(t *testing.T) { err := ErrUnknownArchiveFormat{RequestFormat: "master"} - assert.True(t, errors.Is(err, ErrUnknownArchiveFormat{})) + assert.ErrorIs(t, err, ErrUnknownArchiveFormat{}) } diff --git a/services/repository/branch.go b/services/repository/branch.go index 7443c3859c9..078c55247a2 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -305,7 +305,7 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, } return db.WithTx(ctx, func(ctx context.Context) error { - branches, err := git_model.GetBranches(ctx, repoID, branchNames) + branches, err := git_model.GetBranches(ctx, repoID, branchNames, true) if err != nil { return fmt.Errorf("git_model.GetBranches: %v", err) } diff --git a/services/repository/license_test.go b/services/repository/license_test.go index 39e9738145c..9d3e0f36e36 100644 --- a/services/repository/license_test.go +++ b/services/repository/license_test.go @@ -65,7 +65,7 @@ func Test_detectLicense(t *testing.T) { result, err := detectLicense(strings.NewReader(tests[2].arg + tests[3].arg + tests[4].arg)) assert.NoError(t, err) t.Run("multiple licenses test", func(t *testing.T) { - assert.Equal(t, 3, len(result)) + assert.Len(t, result, 3) assert.Contains(t, result, tests[2].want[0]) assert.Contains(t, result, tests[3].want[0]) assert.Contains(t, result, tests[4].want[0]) diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go new file mode 100644 index 00000000000..85ca8f7e31a --- /dev/null +++ b/services/repository/merge_upstream.go @@ -0,0 +1,115 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + "fmt" + + git_model "code.gitea.io/gitea/models/git" + issue_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/pull" +) + +type UpstreamDivergingInfo struct { + BaseIsNewer bool + CommitsBehind int + CommitsAhead int +} + +func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { + if err = repo.MustNotBeArchived(); err != nil { + return "", err + } + if err = repo.GetBaseRepo(ctx); err != nil { + return "", err + } + err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{ + Remote: repo.RepoPath(), + Branch: fmt.Sprintf("%s:%s", branch, branch), + Env: repo_module.PushingEnvironment(doer, repo), + }) + if err == nil { + return "fast-forward", nil + } + if !git.IsErrPushOutOfDate(err) && !git.IsErrPushRejected(err) { + return "", err + } + + // TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment + // ideally in the future the "merge" functions should be refactored to decouple from the PullRequest + fakeIssue := &issue_model.Issue{ + ID: -1, + RepoID: repo.ID, + Repo: repo, + Index: -1, + PosterID: doer.ID, + Poster: doer, + IsPull: true, + } + fakePR := &issue_model.PullRequest{ + ID: -1, + Status: issue_model.PullRequestStatusMergeable, + IssueID: -1, + Issue: fakeIssue, + Index: -1, + HeadRepoID: repo.ID, + HeadRepo: repo, + BaseRepoID: repo.BaseRepo.ID, + BaseRepo: repo.BaseRepo, + HeadBranch: branch, // maybe HeadCommitID is not needed + BaseBranch: branch, + } + fakeIssue.PullRequest = fakePR + err = pull.Update(ctx, fakePR, doer, "merge upstream", false) + if err != nil { + return "", err + } + return "merge", nil +} + +func GetUpstreamDivergingInfo(ctx context.Context, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) { + if !repo.IsFork { + return nil, util.NewInvalidArgumentErrorf("repo is not a fork") + } + + if repo.IsArchived { + return nil, util.NewInvalidArgumentErrorf("repo is archived") + } + + if err := repo.GetBaseRepo(ctx); err != nil { + return nil, err + } + + forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch) + if err != nil { + return nil, err + } + + baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, branch) + if err != nil { + return nil, err + } + + info := &UpstreamDivergingInfo{} + if forkBranch.CommitID == baseBranch.CommitID { + return info, nil + } + + // TODO: if the fork repo has new commits, this call will fail: + // exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb + // so at the moment, we are not able to handle this case, should be improved in the future + diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID) + if err != nil { + info.BaseIsNewer = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix + return info, nil + } + info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead + return info, nil +} diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go index 67799eddcc3..0401701ba55 100644 --- a/services/repository/transfer_test.go +++ b/services/repository/transfer_test.go @@ -103,7 +103,7 @@ func TestRepositoryTransfer(t *testing.T) { assert.NoError(t, models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, user2, repo.ID, nil)) transfer, err = models.GetPendingRepositoryTransfer(db.DefaultContext, repo) - assert.Nil(t, err) + assert.NoError(t, err) assert.NoError(t, transfer.LoadAttributes(db.DefaultContext)) assert.Equal(t, "user2", transfer.Recipient.Name) diff --git a/services/webhook/packagist_test.go b/services/webhook/packagist_test.go index e9b0695baae..f47807fa6e2 100644 --- a/services/webhook/packagist_test.go +++ b/services/webhook/packagist_test.go @@ -25,7 +25,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Create(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Delete", func(t *testing.T) { @@ -33,7 +33,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Delete(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Fork", func(t *testing.T) { @@ -41,7 +41,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Fork(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Push", func(t *testing.T) { @@ -59,12 +59,12 @@ func TestPackagistPayload(t *testing.T) { p.Action = api.HookIssueOpened pl, err := pc.Issue(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) p.Action = api.HookIssueClosed pl, err = pc.Issue(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("IssueComment", func(t *testing.T) { @@ -72,7 +72,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("PullRequest", func(t *testing.T) { @@ -80,7 +80,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.PullRequest(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("PullRequestComment", func(t *testing.T) { @@ -88,7 +88,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.IssueComment(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Review", func(t *testing.T) { @@ -97,7 +97,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Repository", func(t *testing.T) { @@ -105,7 +105,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Repository(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Package", func(t *testing.T) { @@ -113,7 +113,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Package(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Wiki", func(t *testing.T) { @@ -122,17 +122,17 @@ func TestPackagistPayload(t *testing.T) { p.Action = api.HookWikiCreated pl, err := pc.Wiki(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) p.Action = api.HookWikiEdited pl, err = pc.Wiki(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) p.Action = api.HookWikiDeleted pl, err = pc.Wiki(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) t.Run("Release", func(t *testing.T) { @@ -140,7 +140,7 @@ func TestPackagistPayload(t *testing.T) { pl, err := pc.Release(p) require.NoError(t, err) - require.Equal(t, pl, PackagistPayload{}) + require.Equal(t, PackagistPayload{}, pl) }) } diff --git a/services/webhook/webhook_test.go b/services/webhook/webhook_test.go index 5f5c1462323..63cbce17712 100644 --- a/services/webhook/webhook_test.go +++ b/services/webhook/webhook_test.go @@ -21,11 +21,11 @@ func TestWebhook_GetSlackHook(t *testing.T) { Meta: `{"channel": "foo", "username": "username", "color": "blue"}`, } slackHook := GetSlackHook(w) - assert.Equal(t, *slackHook, SlackMeta{ + assert.Equal(t, SlackMeta{ Channel: "foo", Username: "username", Color: "blue", - }) + }, *slackHook) } func TestPrepareWebhooks(t *testing.T) { diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl index 7361df99eaf..80519361fdb 100644 --- a/templates/org/header.tmpl +++ b/templates/org/header.tmpl @@ -1,6 +1,6 @@
{{ctx.AvatarUtils.Avatar .Org 100 "org-avatar"}} -
+
{{.Org.DisplayName}} @@ -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/labels.tmpl b/templates/org/settings/labels.tmpl index 25a562c975a..21d7c0ef3c0 100644 --- a/templates/org/settings/labels.tmpl +++ b/templates/org/settings/labels.tmpl @@ -1,15 +1,13 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings labels")}} -
-
-
- {{ctx.Locale.Tr "org.settings.labels_desc"}} -
- -
-
- {{template "repo/issue/labels/label_new" .}} - {{template "repo/issue/labels/label_list" .}} -
-{{template "repo/issue/labels/edit_delete_label" .}} +
+
+
+ {{ctx.Locale.Tr "org.settings.labels_desc"}} +
+ +
+
+ {{template "repo/issue/labels/label_list" .}} + {{template "repo/issue/labels/label_edit_modal" .}} +
{{template "org/settings/layout_footer" .}} - diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index aaed25bfbd6..207774bfefd 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -36,11 +36,13 @@ {{range .PackageDescriptor.Metadata.Manifests}} - - {{.Digest}} - {{.Platform}} - {{FileSize .Size}} - + {{if ne .Platform "unknown/unknown"}} + + {{StringUtils.TrimPrefix .Digest "sha256:" | ShortSha}} + {{.Platform}} + {{FileSize .Size}} + + {{end}} {{end}} diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl index b2f48fe2c9c..f5a48f72414 100644 --- a/templates/projects/list.tmpl +++ b/templates/projects/list.tmpl @@ -24,16 +24,19 @@ {{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.project_kind")}} - -
- {{if .RefLink}} - {{.PrettyRef}} + {{if .IsRefDeleted}} + {{.PrettyRef}} {{else}} - {{.PrettyRef}} + {{.PrettyRef}} {{end}}
{{svg "octicon-calendar" 16}}{{DateUtils.TimeSince .Updated}}
diff --git a/templates/repo/actions/status.tmpl b/templates/repo/actions/status.tmpl index a0e02cf8d76..64c25433028 100644 --- a/templates/repo/actions/status.tmpl +++ b/templates/repo/actions/status.tmpl @@ -2,28 +2,22 @@ Please also update the vue file above if this template is modified. action status accepted: success, skipped, waiting, blocked, running, failure, cancelled, unknown --> -{{- $size := 16 -}} -{{- if .size -}} -{{- $size = .size -}} -{{- end -}} - -{{- $className := "" -}} -{{- if .className -}} -{{- $className = .className -}} -{{- end -}} - - +{{- $size := Iif .size .size 16 -}} +{{- $className := Iif .className .className "" -}} + {{if eq .status "success"}} {{svg "octicon-check-circle-fill" $size (printf "text green %s" $className)}} {{else if eq .status "skipped"}} {{svg "octicon-skip" $size (printf "text grey %s" $className)}} +{{else if eq .status "cancelled"}} + {{svg "octicon-stop" $size (printf "text grey %s" $className)}} {{else if eq .status "waiting"}} {{svg "octicon-clock" $size (printf "text yellow %s" $className)}} {{else if eq .status "blocked"}} {{svg "octicon-blocked" $size (printf "text yellow %s" $className)}} {{else if eq .status "running"}} {{svg "octicon-meter" $size (printf "text yellow job-status-rotate %s" $className)}} -{{else if or (eq .status "failure") or (eq .status "cancelled") or (eq .status "unknown")}} +{{else}}{{/*failure, unknown*/}} {{svg "octicon-x-circle-fill" $size (printf "text red %s" $className)}} {{end}} diff --git a/templates/repo/clone_buttons.tmpl b/templates/repo/clone_buttons.tmpl index 91952c8a06d..03b7a561daa 100644 --- a/templates/repo/clone_buttons.tmpl +++ b/templates/repo/clone_buttons.tmpl @@ -1,15 +1,13 @@ - -{{if $.CloneButtonShowHTTPS}} - + {{end}} + {{if $.CloneButtonShowSSH}} + + {{end}} + + -{{end}} -{{if $.CloneButtonShowSSH}} - -{{end}} - - +
diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl new file mode 100644 index 00000000000..8cbeda132dd --- /dev/null +++ b/templates/repo/clone_panel.tmpl @@ -0,0 +1,44 @@ + +
+
{{svg "octicon-terminal"}} Clone
+ +
+ + {{if $.CloneButtonShowHTTPS}} + + {{end}} + {{if $.CloneButtonShowSSH}} + + {{end}} +
+
+ +
+
+ +
+ {{svg "octicon-copy" 14}} +
+
+
+ + {{if not .PageIsWiki}} +
+ {{range .OpenWithEditorApps}} + {{.IconHTML}}{{ctx.Locale.Tr "repo.open_with_editor" .DisplayName}} + {{end}} +
+ + {{if and (not $.DisableDownloadSourceArchives) $.RefName}} +
+ + {{end}} + {{end}} +
diff --git a/templates/repo/clone_script.tmpl b/templates/repo/clone_script.tmpl deleted file mode 100644 index 40dae76dc71..00000000000 --- a/templates/repo/clone_script.tmpl +++ /dev/null @@ -1,50 +0,0 @@ - diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl new file mode 100644 index 00000000000..299ba63e9ef --- /dev/null +++ b/templates/repo/code/upstream_diverging_info.tmpl @@ -0,0 +1,18 @@ +{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseIsNewer .UpstreamDivergingInfo.CommitsBehind)}} +
+
+ {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.BranchName|PathEscapeSegments)}} + {{$upstreamHtml := HTMLFormat `%s:%s` $upstreamLink .Repository.BaseRepo.FullName .BranchName}} + {{if .UpstreamDivergingInfo.CommitsBehind}} + {{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}} + {{else}} + {{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}} + {{end}} +
+ {{if .CanWriteCode}} + + {{end}} +
+{{end}} diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl index 6b8a63fe996..882d8b205cf 100644 --- a/templates/repo/contributors.tmpl +++ b/templates/repo/contributors.tmpl @@ -1,6 +1,7 @@ {{if .Permission.CanRead ctx.Consts.RepoUnitTypeCode}}
{{svg "octicon-kebab-horizontal" 18}}
{{if not (or $file.IsIncomplete $file.IsBin $file.IsSubmodule)}} - - + + {{end}} {{if and (not $file.IsSubmodule) (not $.PageIsWiki)}} {{if $file.IsDeleted}} diff --git a/templates/repo/empty.tmpl b/templates/repo/empty.tmpl index d3a81bc51d2..7170fe36020 100644 --- a/templates/repo/empty.tmpl +++ b/templates/repo/empty.tmpl @@ -37,9 +37,7 @@ {{end}} {{end}} -
- {{template "repo/clone_buttons" .}} -
+ {{template "repo/clone_buttons" .}}
@@ -73,7 +71,6 @@ git push -u origin {{.Repository.DefaultBranch}}
{{ctx.Locale.Tr "repo.empty_message"}} {{end}} - {{template "repo/clone_script" .}} diff --git a/templates/repo/home.tmpl b/templates/repo/home.tmpl index 4b25915c278..d73b7470bc5 100644 --- a/templates/repo/home.tmpl +++ b/templates/repo/home.tmpl @@ -3,35 +3,7 @@ {{template "repo/header" .}}
{{template "base/alert" .}} - {{template "repo/code/recently_pushed_new_branches" .}} - {{if and (not .HideRepoInfo) (not .IsBlame)}} -
- {{- $description := .Repository.DescriptionHTML ctx -}} - {{if $description}}{{$description | RenderCodeBlock}}{{end}} - {{if .Repository.Website}}{{.Repository.Website}}{{end}} -
-
- {{/* it should match the code in issue-home.js */}} - {{range .Topics}}{{.Name}}{{end}} - {{if and .Permission.IsAdmin (not .Repository.IsArchived)}}{{end}} -
- {{end}} - {{if and .Permission.IsAdmin (not .Repository.IsArchived)}} -
- -
- - -
-
- {{end}} + {{if .Repository.IsArchived}}
{{if .Repository.ArchivedUnix.IsZero}} @@ -41,134 +13,127 @@ {{end}}
{{end}} - {{template "repo/sub_menu" .}} - {{$n := len .TreeNames}} - {{$l := Eval $n "-" 1}} - {{$isHomepage := (eq $n 0)}} -
-
- {{$branchDropdownCurrentRefType := "branch"}} - {{$branchDropdownCurrentRefShortName := .BranchName}} - {{if .IsViewTag}} - {{$branchDropdownCurrentRefType = "tag"}} - {{$branchDropdownCurrentRefShortName = .TagName}} - {{end}} - {{template "repo/branch_dropdown" dict - "Repository" .Repository - "ShowTabBranches" true - "ShowTabTags" true - "CurrentRefType" $branchDropdownCurrentRefType - "CurrentRefShortName" $branchDropdownCurrentRefShortName - "CurrentTreePath" .TreePath - "RefLinkTemplate" "{RepoLink}/src/{RefType}/{RefShortName}/{TreePath}" - "AllowCreateNewRef" .CanCreateBranch - "ShowViewAllRefsEntry" true - }} - {{if and .CanCompareOrPull .IsViewBranch (not .Repository.IsArchived)}} - {{$cmpBranch := ""}} - {{if ne .Repository.ID .BaseRepo.ID}} - {{$cmpBranch = printf "%s/%s:" (.Repository.OwnerName|PathEscape) (.Repository.Name|PathEscape)}} - {{end}} - {{$cmpBranch = print $cmpBranch (.BranchName|PathEscapeSegments)}} - {{$compareLink := printf "%s/compare/%s...%s" .BaseRepo.Link (.BaseRepo.DefaultBranch|PathEscapeSegments) $cmpBranch}} - - {{svg "octicon-git-pull-request"}} - - {{end}} - - {{if $isHomepage}} - {{ctx.Locale.Tr "repo.find_file.go_to_file"}} - {{end}} - {{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} - - {{end}} + {{end}} - {{if and $isHomepage (.Repository.IsTemplate)}} - - {{ctx.Locale.Tr "repo.use_template"}} - - {{end}} - {{if $isHomepage}} - {{/* only show the "code search" on the repo home page, it only does global search, - so do not show it when viewing file or directory to avoid misleading users (it doesn't search in a directory) */}} -
-
- - {{template "shared/search/button"}} -
-
- {{else}} - - {{StringUtils.EllipsisString .Repository.Name 30}} - {{- range $i, $v := .TreeNames -}} - / - {{- if eq $i $l -}} - {{$v}} - - {{- else -}} - {{$p := index $.Paths $i}}{{$v}} - {{- end -}} - {{- end -}} - - {{end}} -
-
- - {{if $isHomepage}} -
- {{template "repo/clone_buttons" .}} - - {{template "repo/clone_script" .}}{{/* the script will update `.js-clone-url` and related elements */}} + + {{if $isTreePathRoot}} + {{ctx.Locale.Tr "repo.find_file.go_to_file"}} + {{end}} + + {{if and .CanWriteCode .IsViewBranch (not .Repository.IsMirror) (not .Repository.IsArchived) (not .IsViewFile)}} + + {{end}} + + {{if and $isTreePathRoot .Repository.IsTemplate}} + + {{ctx.Locale.Tr "repo.use_template"}} + + {{end}} + + {{if not $isTreePathRoot}} + {{$treeNameIdxLast := Eval $treeNamesLen "-" 1}} + + {{StringUtils.EllipsisString .Repository.Name 30}} + {{- range $i, $v := .TreeNames -}} + / + {{- if eq $i $treeNameIdxLast -}} + {{$v}} + + {{- else -}} + {{$p := index $.Paths $i}}{{$v}} + {{- end -}} + {{- end -}} + + {{end}}
- {{template "repo/cite/cite_modal" .}} - {{end}} - {{if and (not $isHomepage) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} - - {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} - + +
+ + {{if $isTreePathRoot}} + {{template "repo/clone_panel" .}} + {{end}} + {{if and (not $isTreePathRoot) (not .IsViewFile) (not .IsBlame)}}{{/* IsViewDirectory (not home), TODO: split the templates, avoid using "if" tricks */}} + + {{svg "octicon-history" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.file_history"}} + + {{end}} +
+
+ {{if .IsViewFile}} + {{template "repo/view_file" .}} + {{else if .IsBlame}} + {{template "repo/blame" .}} + {{else}}{{/* IsViewDirectory */}} + {{if $isTreePathRoot}} + {{template "repo/code/upstream_diverging_info" .}} + {{end}} + {{template "repo/view_list" .}} + {{if and .ReadmeExist (or .IsMarkup .IsPlainText)}} + {{template "repo/view_file" .}} + {{end}} {{end}}
+ + {{if $showSidebar}} + {{template "repo/home_sidebar_top" .}} + {{template "repo/home_sidebar_bottom" .}} + {{end}}
- {{if .IsViewFile}} - {{template "repo/view_file" .}} - {{else if .IsBlame}} - {{template "repo/blame" .}} - {{else}}{{/* IsViewDirectory */}} - {{template "repo/view_list" .}} - {{end}} {{template "base/footer" .}} diff --git a/templates/repo/home_sidebar_bottom.tmpl b/templates/repo/home_sidebar_bottom.tmpl new file mode 100644 index 00000000000..f780dc122d5 --- /dev/null +++ b/templates/repo/home_sidebar_bottom.tmpl @@ -0,0 +1,61 @@ +
+
+ {{if .LatestRelease}} +
+
+ +
+
+ {{svg "octicon-tag" 16}} +
+
+
+
+ {{.LatestRelease.Title}} + {{template "repo/release/label" (dict "Release" .LatestRelease "IsLatest" true)}} +
+
+
+ {{DateUtils.TimeSince .LatestRelease.CreatedUnix}} +
+
+
+
+
+ {{end}} + + {{if and (not .IsEmptyRepo) .LanguageStats}} +
+
+
+ {{ctx.Locale.Tr "repo.repo_lang"}} +
+ +
+
+ {{range .LanguageStats}} +
+ {{end}} +
+
+ {{range .LanguageStats}} +
+ + + {{Iif (eq .Language "other") (ctx.Locale.Tr "repo.language_other") .Language}} + + {{.Percentage}}% +
+ {{end}} +
+
+
+
+ {{end}} +
+
diff --git a/templates/repo/home_sidebar_top.tmpl b/templates/repo/home_sidebar_top.tmpl new file mode 100644 index 00000000000..607dc62e2e7 --- /dev/null +++ b/templates/repo/home_sidebar_top.tmpl @@ -0,0 +1,70 @@ +
+
+
+ {{template "shared/search/button"}} +
+
+ +
+
+
+
{{ctx.Locale.Tr "repo.repo_desc"}}
+
+
+
+ {{- $description := .Repository.DescriptionHTML ctx -}} + {{if $description}}{{$description | RenderCodeBlock}}{{else}}{{ctx.Locale.Tr "repo.repo_no_desc"}}{{end}} +
+ + {{if .Repository.Website}} + + {{svg "octicon-link" 16 "tw-text-text"}} {{.Repository.Website}} + + {{end}} + +
+ {{/* !!!! it SHOULD and MUST match the code in repo-home.ts */}} + {{range .Topics}}{{.Name}}{{end}} +
+ {{if and .Permission.IsAdmin (not .Repository.IsArchived)}} + +
+ +
+ + +
+
+ {{end}} + + {{if .ReadmeExist}} + + {{svg "octicon-book"}} {{ctx.Locale.Tr "readme"}} + + {{end}} + + {{if .DetectedRepoLicenses}} + + {{svg "octicon-law"}} {{if eq (len .DetectedRepoLicenses) 1}}{{index .DetectedRepoLicenses 0}}{{else}}{{ctx.Locale.Tr "repo.multiple_licenses"}}{{end}} + + {{end}} + + {{if .CitiationExist}} + {{template "repo/cite/cite_modal" .}} + + {{svg "octicon-cross-reference"}} {{ctx.Locale.Tr "repo.cite_this_repo"}} + + {{end}} +
+
+
+
+
+
diff --git a/templates/repo/issue/branch_selector_field.tmpl b/templates/repo/issue/branch_selector_field.tmpl index 286ac0cd051..9183b7b46ab 100644 --- a/templates/repo/issue/branch_selector_field.tmpl +++ b/templates/repo/issue/branch_selector_field.tmpl @@ -1,3 +1,17 @@ +{{/* TODO: RemoveIssueRef: the Issue.Ref will be removed in 1.24 or 1.25 if no end user really needs it or there could be better alternative then. +PR: https://github.com/go-gitea/gitea/pull/32744 + +The Issue.Ref was added by Add possibility to record branch or tag information in an issue (#780) +After 8 years, this "branch selector" does nothing more than saving the branch/tag name into database and displays it. + +There are still users using it: +* @didim99: it is a really useful feature to specify a branch in which issue found. + +Still needs to figure out: +* Could the "recording branch/tag name" be replaced by other approaches? + * Write the branch name in the issue title/body then it will still be displayed, eg: `[bug] (fix/ui-broken-bug) there is a bug ....` +* Is "GitHub-like development sidebar (`#31899`)" good enough (or better) for your usage? +*/}} {{if and (not .Issue.IsPull) (not .PageIsComparePull)}} {{else}} -
{{ctx.Locale.Tr "no_results_found"}}
+
{{ctx.Locale.Tr "no_results_found"}}
{{end}} diff --git a/templates/repo/issue/filter_item_label.tmpl b/templates/repo/issue/filter_item_label.tmpl new file mode 100644 index 00000000000..927328ba14a --- /dev/null +++ b/templates/repo/issue/filter_item_label.tmpl @@ -0,0 +1,45 @@ +{{/* +* "labels" from query string (needed by JS) +* QueryLink +* Labels +* SupportArchivedLabel, if true, then it needs "archived_labels" from query string +*/}} +{{$queryLink := .QueryLink}} + diff --git a/templates/repo/issue/filter_item_user_assign.tmpl b/templates/repo/issue/filter_item_user_assign.tmpl new file mode 100644 index 00000000000..4f1db71d57f --- /dev/null +++ b/templates/repo/issue/filter_item_user_assign.tmpl @@ -0,0 +1,31 @@ +{{/* This is a user list for filter, the data is provided by a local variable assignment +* QueryParamKey: eg: "poster", "assignee" +* QueryLink +* UserSearchList +* SelectedUserId: 0 or empty means default, -1 means "no user is set" +* TextFilterTitle +* TextZeroValue: the text for "all issues" +* TextNegativeOne: the text for "issues with no assignee" +*/}} +{{$queryLink := .QueryLink}} + diff --git a/templates/repo/issue/filter_item_user_fetch.tmpl b/templates/repo/issue/filter_item_user_fetch.tmpl new file mode 100644 index 00000000000..5fa81423546 --- /dev/null +++ b/templates/repo/issue/filter_item_user_fetch.tmpl @@ -0,0 +1,23 @@ +{{/* This is a user list for filter, the data is provided by a remote "fetch" request +* QueryParamKey: eg: "poster", "assignee" +* QueryLink +* UserSearchUrl +* SelectedUsername +* TextFilterTitle +*/}} +{{$queryLink := .QueryLink}} + diff --git a/templates/repo/issue/filter_list.tmpl b/templates/repo/issue/filter_list.tmpl index d48af5b1506..7612d93b212 100644 --- a/templates/repo/issue/filter_list.tmpl +++ b/templates/repo/issue/filter_list.tmpl @@ -1,57 +1,10 @@ - - +{{$queryLink := QueryBuild "?" "q" $.Keyword "type" $.ViewType "sort" $.SortType "state" $.State "labels" $.SelectLabels "milestone" $.MilestoneID "project" $.ProjectID "assignee" $.AssigneeID "poster" $.PosterUsername "archived_labels" (Iif $.ShowArchivedLabels "true")}} + +{{template "repo/issue/filter_item_label" dict "Labels" .Labels "QueryLink" $queryLink "SupportArchivedLabel" true}} {{if not .Milestone}} - - - +{{/* TODO: the UserSearchUrl is old logic but not right, milestone could also have "pull request" posters */}} +{{template "repo/issue/filter_item_user_fetch" dict + "QueryParamKey" "poster" + "QueryLink" $queryLink + "UserSearchUrl" (Iif .Milestone (print $.RepoLink "/issues/posters") (print $.Link "/posters")) + "SelectedUsername" $.PosterUsername + "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_poster") +}} - - +{{template "repo/issue/filter_item_user_assign" dict + "QueryParamKey" "assignee" + "QueryLink" $queryLink + "UserSearchList" $.Assignees + "SelectedUserId" $.AssigneeID + "TextFilterTitle" (ctx.Locale.Tr "repo.issues.filter_assignee") + "TextZeroValue" (ctx.Locale.Tr "repo.issues.filter_assginee_no_select") + "TextNegativeOne" (ctx.Locale.Tr "repo.issues.filter_assginee_no_assignee") +}} {{if .IsSigned}} - + {{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} + {{template "repo/issue/labels/label_edit_modal" .}} + {{end}} - -{{if and (or .CanWriteIssues .CanWritePulls) (not .Repository.IsArchived)}} - {{template "repo/issue/labels/edit_delete_label" .}} -{{end}} {{template "base/footer" .}} diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/label_edit_modal.tmpl similarity index 79% rename from templates/repo/issue/labels/edit_delete_label.tmpl rename to templates/repo/issue/labels/label_edit_modal.tmpl index fcf69217ea7..f04d499737a 100644 --- a/templates/repo/issue/labels/edit_delete_label.tmpl +++ b/templates/repo/issue/labels/label_edit_modal.tmpl @@ -1,22 +1,13 @@ -