diff --git a/.drone.yml b/.drone.yml index d05a96ce64..3084351770 100644 --- a/.drone.yml +++ b/.drone.yml @@ -154,7 +154,7 @@ steps: when: event: exclude: - - pull_request + - pull_request - name: publish-rootless image: plugins/docker:latest @@ -176,7 +176,7 @@ steps: when: event: exclude: - - pull_request + - pull_request --- kind: pipeline @@ -220,7 +220,7 @@ steps: when: event: exclude: - - pull_request + - pull_request - name: publish-rootless image: plugins/docker:latest @@ -241,7 +241,7 @@ steps: when: event: exclude: - - pull_request + - pull_request --- kind: pipeline @@ -289,7 +289,7 @@ steps: when: event: exclude: - - pull_request + - pull_request - name: publish-rootless image: plugins/docker:latest @@ -311,7 +311,7 @@ steps: when: event: exclude: - - pull_request + - pull_request --- kind: pipeline @@ -355,7 +355,7 @@ steps: when: event: exclude: - - pull_request + - pull_request - name: publish-rootless image: plugins/docker:latest @@ -376,7 +376,7 @@ steps: when: event: exclude: - - pull_request + - pull_request --- kind: pipeline @@ -413,7 +413,7 @@ steps: trigger: ref: - - "refs/tags/**" + - "refs/tags/**" paths: exclude: - "docs/**" diff --git a/.eslintrc.yaml b/.eslintrc.yaml index dd2c32eec0..846823abc7 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -156,7 +156,7 @@ rules: import/no-restricted-paths: [0] import/no-self-import: [2] import/no-unassigned-import: [0] - import/no-unresolved: [2, {commonjs: true, ignore: [\?.+$, ^vitest/]}] + import/no-unresolved: [2, {commonjs: true, ignore: ["\\?.+$", ^vitest/]}] import/no-unused-modules: [2, {unusedExports: true}] import/no-useless-path-segments: [2, {commonjs: true}] import/no-webpack-loader-syntax: [2] diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index 1004c55de3..c482affbbd 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -2,90 +2,90 @@ name: Bug Report description: Found something you weren't expecting? Report it here! labels: ["kind/bug"] body: -- type: markdown - attributes: - value: | - NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue. -- type: markdown - attributes: - value: | - 1. Please speak English, this is the language all maintainers can speak and write. - 2. Please ask questions or configuration/deploy problems on our Discord - server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). - 3. Make sure you are using the latest release and - take a moment to check that your issue hasn't been reported before. - 4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq) - 5. It's really important to provide pertinent details and logs (https://docs.gitea.com/help/support), - incomplete details will be handled as an invalid report. -- type: textarea - id: description - attributes: - label: Description - description: | - Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below) - If you are using a proxy or a CDN (e.g. Cloudflare) in front of Gitea, please disable the proxy/CDN fully and access Gitea directly to confirm the issue still persists without those services. -- type: input - id: gitea-ver - attributes: - label: Gitea Version - description: Gitea version (or commit reference) of your instance - validations: - required: true -- type: dropdown - id: can-reproduce - attributes: - label: Can you reproduce the bug on the Gitea demo site? - description: | - If so, please provide a URL in the Description field - URL of Gitea demo: https://try.gitea.io - options: - - "Yes" - - "No" - validations: - required: true -- type: markdown - attributes: - value: | - It's really important to provide pertinent logs - Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help - In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini -- type: input - id: logs - attributes: - label: Log Gist - description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden -- type: textarea - id: screenshots - attributes: - label: Screenshots - description: If this issue involves the Web Interface, please provide one or more screenshots -- type: input - id: git-ver - attributes: - label: Git Version - description: The version of git running on the server -- type: input - id: os-ver - attributes: - label: Operating System - description: The operating system you are using to run Gitea -- type: textarea - id: run-info - attributes: - label: How are you running Gitea? - description: | - Please include information on whether you built Gitea yourself, used one of our downloads, are using https://try.gitea.io or are using some other package - Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc. - If you are using a package or systemd tell us what distribution you are using - validations: - required: true -- type: dropdown - id: database - attributes: - label: Database - description: What database system are you running? - options: - - PostgreSQL - - MySQL/MariaDB - - MSSQL - - SQLite + - type: markdown + attributes: + value: | + NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue. + - type: markdown + attributes: + value: | + 1. Please speak English, this is the language all maintainers can speak and write. + 2. Please ask questions or configuration/deploy problems on our Discord + server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). + 3. Make sure you are using the latest release and + take a moment to check that your issue hasn't been reported before. + 4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq) + 5. It's really important to provide pertinent details and logs (https://docs.gitea.com/help/support), + incomplete details will be handled as an invalid report. + - type: textarea + id: description + attributes: + label: Description + description: | + Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below) + If you are using a proxy or a CDN (e.g. Cloudflare) in front of Gitea, please disable the proxy/CDN fully and access Gitea directly to confirm the issue still persists without those services. + - type: input + id: gitea-ver + attributes: + label: Gitea Version + description: Gitea version (or commit reference) of your instance + validations: + required: true + - type: dropdown + id: can-reproduce + attributes: + label: Can you reproduce the bug on the Gitea demo site? + description: | + If so, please provide a URL in the Description field + URL of Gitea demo: https://try.gitea.io + options: + - "Yes" + - "No" + validations: + required: true + - type: markdown + attributes: + value: | + It's really important to provide pertinent logs + Please read https://docs.gitea.com/administration/logging-config#collecting-logs-for-help + In addition, if your problem relates to git commands set `RUN_MODE=dev` at the top of app.ini + - type: input + id: logs + attributes: + label: Log Gist + description: Please provide a gist URL of your logs, with any sensitive information (e.g. API keys) removed/hidden + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If this issue involves the Web Interface, please provide one or more screenshots + - type: input + id: git-ver + attributes: + label: Git Version + description: The version of git running on the server + - type: input + id: os-ver + attributes: + label: Operating System + description: The operating system you are using to run Gitea + - type: textarea + id: run-info + attributes: + label: How are you running Gitea? + description: | + Please include information on whether you built Gitea yourself, used one of our downloads, are using https://try.gitea.io or are using some other package + Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc. + If you are using a package or systemd tell us what distribution you are using + validations: + required: true + - type: dropdown + id: database + attributes: + label: Database + description: What database system are you running? + options: + - PostgreSQL + - MySQL/MariaDB + - MSSQL + - SQLite diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml index b481e0c2de..71aaa09423 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -2,23 +2,23 @@ name: Feature Request description: Got an idea for a feature that Gitea doesn't have currently? Submit your idea here! labels: ["kind/proposal"] body: -- type: markdown - attributes: - value: | - 1. Please speak English, this is the language all maintainers can speak and write. - 2. Please ask questions or configuration/deploy problems on our Discord - server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). - 3. Please take a moment to check that your feature hasn't already been suggested. -- type: textarea - id: description - attributes: - label: Feature Description - placeholder: | - I think it would be great if Gitea had... - validations: - required: true -- type: textarea - id: screenshots - attributes: - label: Screenshots - description: If you can, provide screenshots of an implementation on another site e.g. GitHub + - type: markdown + attributes: + value: | + 1. Please speak English, this is the language all maintainers can speak and write. + 2. Please ask questions or configuration/deploy problems on our Discord + server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). + 3. Please take a moment to check that your feature hasn't already been suggested. + - type: textarea + id: description + attributes: + label: Feature Description + placeholder: | + I think it would be great if Gitea had... + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: If you can, provide screenshots of an implementation on another site e.g. GitHub diff --git a/.github/ISSUE_TEMPLATE/ui.bug-report.yaml b/.github/ISSUE_TEMPLATE/ui.bug-report.yaml index d5c41bb836..ef0a1014e5 100644 --- a/.github/ISSUE_TEMPLATE/ui.bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/ui.bug-report.yaml @@ -2,65 +2,65 @@ name: Web Interface Bug Report description: Something doesn't look quite as it should? Report it here! labels: ["kind/bug", "kind/ui"] body: -- type: markdown - attributes: - value: | - NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue. -- type: markdown - attributes: - value: | - 1. Please speak English, this is the language all maintainers can speak and write. - 2. Please ask questions or configuration/deploy problems on our Discord - server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). - 3. Please take a moment to check that your issue doesn't already exist. - 4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq) - 5. Please give all relevant information below for bug reports, because - incomplete details will be handled as an invalid report. - 6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript - error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us - DEBUG level logs. (See https://docs.gitea.com/administration/logging-config#collecting-logs-for-help) -- type: textarea - id: description - attributes: - label: Description - description: | - Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below) - If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please disable the proxy/CDN fully and connect to gitea directly to confirm the issue still persists without those services. -- type: textarea - id: screenshots - attributes: - label: Screenshots - description: Please provide at least 1 screenshot showing the issue. - validations: - required: true -- type: input - id: gitea-ver - attributes: - label: Gitea Version - description: Gitea version (or commit reference) your instance is running - validations: - required: true -- type: dropdown - id: can-reproduce - attributes: - label: Can you reproduce the bug on the Gitea demo site? - description: | - If so, please provide a URL in the Description field - URL of Gitea demo: https://try.gitea.io - options: - - "Yes" - - "No" - validations: - required: true -- type: input - id: os-ver - attributes: - label: Operating System - description: The operating system you are using to access Gitea -- type: input - id: browser-ver - attributes: - label: Browser Version - description: The browser and version that you are using to access Gitea - validations: - required: true + - type: markdown + attributes: + value: | + NOTE: If your issue is a security concern, please send an email to security@gitea.io instead of opening a public issue. + - type: markdown + attributes: + value: | + 1. Please speak English, this is the language all maintainers can speak and write. + 2. Please ask questions or configuration/deploy problems on our Discord + server (https://discord.gg/gitea) or forum (https://discourse.gitea.io). + 3. Please take a moment to check that your issue doesn't already exist. + 4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq) + 5. Please give all relevant information below for bug reports, because + incomplete details will be handled as an invalid report. + 6. In particular it's really important to provide pertinent logs. If you are certain that this is a javascript + error, show us the javascript console. If the error appears to relate to Gitea the server you must also give us + DEBUG level logs. (See https://docs.gitea.com/administration/logging-config#collecting-logs-for-help) + - type: textarea + id: description + attributes: + label: Description + description: | + Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below) + If using a proxy or a CDN (e.g. CloudFlare) in front of gitea, please disable the proxy/CDN fully and connect to gitea directly to confirm the issue still persists without those services. + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Please provide at least 1 screenshot showing the issue. + validations: + required: true + - type: input + id: gitea-ver + attributes: + label: Gitea Version + description: Gitea version (or commit reference) your instance is running + validations: + required: true + - type: dropdown + id: can-reproduce + attributes: + label: Can you reproduce the bug on the Gitea demo site? + description: | + If so, please provide a URL in the Description field + URL of Gitea demo: https://try.gitea.io + options: + - "Yes" + - "No" + validations: + required: true + - type: input + id: os-ver + attributes: + label: Operating System + description: The operating system you are using to access Gitea + - type: input + id: browser-ver + attributes: + label: Browser Version + description: The browser and version that you are using to access Gitea + validations: + required: true diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..ee6c7d6ae8 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,32 @@ +kind/docs: + - "**/*.md" + - "docs/**" + +kind/ui: + - "web_src/**/*" + - all: ["templates/**", "!templates/swagger/v1_json.tmpl"] + +kind/api: + - "templates/swagger/v1_json.tmpl" + - "routers/api/**" + +kind/build: + - "Makefile" + - "Dockerfile" + - "Dockerfile.rootless" + - "docker/**" + - "webpack.config.js" + +theme/package-registry: + - "modules/packages/**" + +kind/cli: + - "cmd/**" + +kind/lint: + - ".eslintrc.yaml" + - ".golangci.yml" + - ".markdownlint.yaml" + - ".spectral.yaml" + - ".stylelintrc.yaml" + - ".yamllint.yaml" diff --git a/.github/stale.yml b/.github/stale.yml index 6a9f341cbf..ebe95acf53 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -9,8 +9,8 @@ daysUntilClose: 14 # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable exemptLabels: - - status/blocked - - kind/security + - status/blocked + - kind/security - lgtm/done - reviewed/confirmed - priority/critical @@ -27,7 +27,7 @@ staleLabel: stale # Comment to post when marking as stale. Set to `false` to disable markComment: > - This issue has been automatically marked as stale because it has not had recent activity. + This issue has been automatically marked as stale because it has not had recent activity. I am here to help clear issues left open even if solved or waiting for more insight. This issue will be closed if no further activity occurs during the next 2 weeks. If the issue is still valid just add a comment to keep it alive. diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 24de4076f2..48db7a732e 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -17,6 +17,8 @@ on: value: ${{ jobs.detect.outputs.docker }} swagger: value: ${{ jobs.detect.outputs.swagger }} + yaml: + value: ${{ jobs.detect.outputs.yaml }} jobs: detect: @@ -30,6 +32,7 @@ jobs: templates: ${{ steps.changes.outputs.templates }} docker: ${{ steps.changes.outputs.docker }} swagger: ${{ steps.changes.outputs.swagger }} + yaml: ${{ steps.changes.outputs.yaml }} steps: - uses: actions/checkout@v3 - uses: dorny/paths-filter@v2 @@ -82,3 +85,8 @@ jobs: - "package.json" - "package-lock.json" - ".spectral.yaml" + + yaml: + - "**/*.yml" + - "**/*.yaml" + - ".yamllint.yaml" diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml index 45dd77fd92..bcbd218846 100644 --- a/.github/workflows/pull-compliance.yml +++ b/.github/workflows/pull-compliance.yml @@ -39,6 +39,19 @@ jobs: - run: make deps-py - run: make lint-templates + lint-yaml: + if: needs.files-changed.outputs.yaml == 'true' + needs: files-changed + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - run: pip install poetry + - run: make deps-py + - run: make lint-yaml + lint-swagger: if: needs.files-changed.outputs.swagger == 'true' needs: files-changed diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml index 50c92a9e9b..bbe589d5c8 100644 --- a/.github/workflows/pull-db-tests.yml +++ b/.github/workflows/pull-db-tests.yml @@ -88,7 +88,7 @@ jobs: mysql: image: mysql:5.7 env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_ALLOW_EMPTY_PASSWORD: true MYSQL_DATABASE: test ports: - "3306:3306" @@ -160,7 +160,7 @@ jobs: mysql: image: mysql:5.7 env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_ALLOW_EMPTY_PASSWORD: true MYSQL_DATABASE: test ports: - "3306:3306" @@ -205,7 +205,7 @@ jobs: mysql8: image: mysql:8 env: - MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_ALLOW_EMPTY_PASSWORD: true MYSQL_DATABASE: testgitea ports: - "3306:3306" diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml new file mode 100644 index 0000000000..c62142b9d2 --- /dev/null +++ b/.github/workflows/pull-labeler.yml @@ -0,0 +1,21 @@ +name: labeler + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + label: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/labeler@v4 + with: + dot: true + sync-labels: true diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 0000000000..c0fce7c301 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,48 @@ +extends: default + +rules: + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + min-spaces-inside-empty: 0 + max-spaces-inside-empty: 0 + + brackets: + min-spaces-inside: 0 + max-spaces-inside: 1 + min-spaces-inside-empty: 0 + max-spaces-inside-empty: 0 + + comments: + require-starting-space: true + ignore-shebangs: true + min-spaces-from-content: 1 + + comments-indentation: + level: error + + document-start: + level: error + present: false + ignore: | + /.drone.yml + + document-end: + present: false + + empty-lines: + max: 1 + + indentation: + spaces: 2 + + line-length: disable + + truthy: + allowed-values: ["true", "false", "on", "off"] + +ignore: | + .venv + node_modules + /models/fixtures + /models/migrations/fixtures diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b4f17b658..8ef677b88c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ This changelog goes through all 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.20.4](https://github.com/go-gitea/gitea/releases/tag/v1.20.4) - 2023-09-08 + +* SECURITY + * Check blocklist for emails when adding them to account (#26812) (#26831) +* ENHANCEMENTS + * Add `branch_filter` to hooks API endpoints (#26599) (#26632) + * Fix incorrect "tabindex" attributes (#26733) (#26734) + * Use line-height: normal by default (#26635) (#26708) + * Fix unable to display individual-level project (#26198) (#26636) +* BUGFIXES + * Fix wrong review requested number (#26784) (#26880) + * Avoid double-unescaping of form value (#26853) (#26863) + * Redirect from `{repo}/issues/new` to `{repo}/issues/new/choose` when blank issues are disabled (#26813) (#26847) + * Sync tags when adopting repos (#26816) (#26834) + * Fix verifyCommits error when push a new branch (#26664) (#26810) + * Include the GITHUB_TOKEN/GITEA_TOKEN secret for fork pull requests (#26759) (#26806) + * Fix some slice append usages (#26778) (#26798) + * Add fix incorrect can_create_org_repo for org owner team (#26683) (#26791) + * Fix bug for ctx usage (#26763) + * Make issue template field template access correct template data (#26698) (#26709) + * Use correct minio error (#26634) (#26639) + * Ignore the trailing slashes when comparing oauth2 redirect_uri (#26597) (#26618) + * Set errwriter for urfave/cli v1 (#26616) + * Fix reopen logic for agit flow pull request (#26399) (#26613) + * Fix context filter has no effect in dashboard (#26695) (#26811) + * Fix being unable to use a repo that prohibits accepting PRs as a PR source. (#26785) (#26790) + * Fix Page Not Found error (#26768) + ## [1.20.3](https://github.com/go-gitea/gitea/releases/tag/v1.20.3) - 2023-08-20 * BREAKING diff --git a/Makefile b/Makefile index 908ee7a337..fd852cbaf9 100644 --- a/Makefile +++ b/Makefile @@ -218,6 +218,7 @@ help: @echo " - lint-md lint markdown files" @echo " - lint-swagger lint swagger files" @echo " - lint-templates lint template files" + @echo " - lint-yaml lint yaml files" @echo " - checks run various consistency checks" @echo " - checks-frontend check frontend files" @echo " - checks-backend check backend files" @@ -427,6 +428,10 @@ lint-actions: lint-templates: .venv @poetry run djlint $(shell find templates -type f -iname '*.tmpl') +.PHONY: lint-yaml +lint-yaml: .venv + @poetry run yamllint . + .PHONY: watch watch: @bash build/watch.sh diff --git a/cmd/migrate_storage.go b/cmd/migrate_storage.go index 7f94e11ea9..acc3ba16ba 100644 --- a/cmd/migrate_storage.go +++ b/cmd/migrate_storage.go @@ -185,7 +185,7 @@ func runMigrateStorage(ctx *cli.Context) error { case string(setting.LocalStorageType): p := ctx.String("path") if p == "" { - log.Fatal("Path must be given when storage is loal") + log.Fatal("Path must be given when storage is local") return nil } dstStorage, err = storage.NewLocalStorage( diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index dd673190aa..f0b0cc298b 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -759,6 +759,8 @@ LEVEL = Info ;; ;; More detail: https://github.com/gogits/gogs/issues/165 ;ENABLE_REVERSE_PROXY_AUTHENTICATION = false +; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible. +;ENABLE_REVERSE_PROXY_AUTHENTICATION_API = false ;ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = false ;ENABLE_REVERSE_PROXY_EMAIL = false ;ENABLE_REVERSE_PROXY_FULL_NAME = false @@ -1744,8 +1746,8 @@ LEVEL = Info ;; Session cookie name ;COOKIE_NAME = i_like_gitea ;; -;; If you use session in https only, default is false -;COOKIE_SECURE = false +;; If you use session in https only: true or false. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL. +;COOKIE_SECURE = ;; ;; Session GC time interval in seconds, default is 86400 (1 day) ;GC_INTERVAL_TIME = 86400 @@ -2564,6 +2566,8 @@ LEVEL = Info ;; ;; Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. ;DEFAULT_ACTIONS_URL = github +;; Default artifact retention time in days, default is 90 days +;ARTIFACT_RETENTION_DAYS = 90 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/docs/content/administration/config-cheat-sheet.en-us.md b/docs/content/administration/config-cheat-sheet.en-us.md index 4158f14cb1..acfb6ce77e 100644 --- a/docs/content/administration/config-cheat-sheet.en-us.md +++ b/docs/content/administration/config-cheat-sheet.en-us.md @@ -446,7 +446,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a - `SQLITE_JOURNAL_MODE`: **""**: Change journal mode for SQlite3. Can be used to enable [WAL mode](https://www.sqlite.org/wal.html) when high load causes write congestion. See [SQlite3 docs](https://www.sqlite.org/pragma.html#pragma_journal_mode) for possible values. Defaults to the default for the database file, often DELETE. - `ITERATE_BUFFER_SIZE`: **50**: Internal buffer size for iterating. - `PATH`: **data/gitea.db**: For SQLite3 only, the database file path. -- `LOG_SQL`: **true**: Log the executed SQL. +- `LOG_SQL`: **false**: Log the executed SQL. - `DB_RETRIES`: **10**: How many ORM init / DB connect attempts allowed. - `DB_RETRY_BACKOFF`: **3s**: time.Duration to wait before trying another ORM init / DB connect attempt, if failure occurred. - `MAX_OPEN_CONNS` **0**: Database maximum open connections - default is 0, meaning there is no limit. @@ -621,7 +621,8 @@ And the following unique queues: BASIC and the user's password. Please note if you disable this you will not be able to access the tokens API endpoints using a password. Further, this only disables BASIC authentication using the password - not tokens or OAuth Basic. -- `ENABLE_REVERSE_PROXY_AUTHENTICATION`: **false**: Enable this to allow reverse proxy authentication. +- `ENABLE_REVERSE_PROXY_AUTHENTICATION`: **false**: Enable this to allow reverse proxy authentication for web requests +- `ENABLE_REVERSE_PROXY_AUTHENTICATION_API`: **false**: Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible. - `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION`: **false**: Enable this to allow auto-registration for reverse authentication. - `ENABLE_REVERSE_PROXY_EMAIL`: **false**: Enable this to allow to auto-registration with a @@ -776,7 +777,7 @@ and - `PROVIDER`: **memory**: Session engine provider \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]. Setting `db` will reuse the configuration in `[database]` - `PROVIDER_CONFIG`: **data/sessions**: For file, the root path; for db, empty (database config will be used); for others, the connection string. Relative paths will be made absolute against _`AppWorkPath`_. -- `COOKIE_SECURE`: **false**: Enable this to force using HTTPS for all session access. +- `COOKIE_SECURE`:**_empty_**: `true` or `false`. Enable this to force using HTTPS for all session access. If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL. - `COOKIE_NAME`: **i\_like\_gitea**: The name of the cookie used for the session ID. - `GC_INTERVAL_TIME`: **86400**: GC interval in seconds. - `SESSION_LIFE_TIME`: **86400**: Session life time in seconds, default is 86400 (1 day) @@ -955,6 +956,12 @@ Default templates for project boards: - `SCHEDULE`: **@midnight** : Interval as a duration between each synchronization, it will always attempt synchronization when the instance starts. - `UPDATE_EXISTING`: **true**: Create new users, update existing user data and disable users that are not in external source anymore (default) or only create new users if UPDATE_EXISTING is set to false. +## Cron - Cleanup Expired Actions Assets (`cron.cleanup_actions`) + +- `ENABLED`: **true**: Enable cleanup expired actions assets job. +- `RUN_AT_START`: **true**: Run job at start time (if ENABLED). +- `SCHEDULE`: **@midnight** : Cron syntax for the job. + ### Extended cron tasks (not enabled by default) #### Cron - Garbage collect all repositories (`cron.git_gc_repos`) @@ -1381,6 +1388,7 @@ PROXY_HOSTS = *.github.com - `DEFAULT_ACTIONS_URL`: **github**: Default platform to get action plugins, `github` for `https://github.com`, `self` for the current Gitea instance. - `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]` - `MINIO_BASE_PATH`: **actions_log/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio` +- `ARTIFACT_RETENTION_DAYS`: **90**: Number of days to keep artifacts. Set to 0 to disable artifact retention. Default is 90 days if not set. `DEFAULT_ACTIONS_URL` indicates where the Gitea Actions runners should find the actions with relative path. For example, `uses: actions/checkout@v3` means `https://github.com/actions/checkout@v3` since the value of `DEFAULT_ACTIONS_URL` is `github`. diff --git a/docs/content/administration/config-cheat-sheet.zh-cn.md b/docs/content/administration/config-cheat-sheet.zh-cn.md index 328c269aac..b3e7d1024a 100644 --- a/docs/content/administration/config-cheat-sheet.zh-cn.md +++ b/docs/content/administration/config-cheat-sheet.zh-cn.md @@ -436,7 +436,7 @@ menu: - `SQLITE_JOURNAL_MODE`:**""**:更改 SQlite3 的日志模式。可以用于在高负载导致写入拥塞时启用 [WAL 模式](https://www.sqlite.org/wal.html)。有关可能的值,请参阅 [SQlite3 文档](https://www.sqlite.org/pragma.html#pragma_journal_mode)。默认为数据库文件的默认值,通常为 DELETE。 - `ITERATE_BUFFER_SIZE`:**50**:用于迭代的内部缓冲区大小。 - `PATH`:**data/gitea.db**:仅适用于 SQLite3 的数据库文件路径。 -- `LOG_SQL`:**true**:记录已执行的 SQL。 +- `LOG_SQL`:**false**:记录已执行的 SQL。 - `DB_RETRIES`:**10**:允许多少次 ORM 初始化 / DB 连接尝试。 - `DB_RETRY_BACKOFF`:**3s**:如果发生故障,等待另一个 ORM 初始化 / DB 连接尝试的 time.Duration。 - `MAX_OPEN_CONNS`:**0**:数据库最大打开连接数 - 默认为 0,表示没有限制。 @@ -742,7 +742,7 @@ Gitea 创建以下非唯一队列: - `PROVIDER`: **memory**:会话存储引擎 \[memory, file, redis, redis-cluster, db, mysql, couchbase, memcache, postgres\]。设置为 `db` 将会重用 `[database]` 的配置信息。 - `PROVIDER_CONFIG`: **data/sessions**:对于文件,为根路径;对于 db,为空(将使用数据库配置);对于其他引擎,为连接字符串。相对路径将根据 _`AppWorkPath`_ 绝对化。 -- `COOKIE_SECURE`: **false**:启用此选项以强制在所有会话访问中使用 HTTPS。 +- `COOKIE_SECURE`: **_empty_**:`true` 或 `false`。启用此选项以强制在所有会话访问中使用 HTTPS。如果没有设置,当 ROOT_URL 是 https 链接的时候默认设置为 true。 - `COOKIE_NAME`: **i\_like\_gitea**:用于会话 ID 的 cookie 名称。 - `GC_INTERVAL_TIME`: **86400**:GC 间隔时间,以秒为单位。 - `SESSION_LIFE_TIME`: **86400**:会话生命周期,以秒为单位,默认为 86400(1 天)。 diff --git a/docs/content/administration/reverse-proxies.en-us.md b/docs/content/administration/reverse-proxies.en-us.md index ca06636469..c141483700 100644 --- a/docs/content/administration/reverse-proxies.en-us.md +++ b/docs/content/administration/reverse-proxies.en-us.md @@ -29,6 +29,8 @@ server { location / { client_max_body_size 512M; proxy_pass http://localhost:3000; + proxy_set_header Connection $http_connection; + proxy_set_header Upgrade $http_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/docs/content/contributing/guidelines-frontend.en-us.md b/docs/content/contributing/guidelines-frontend.en-us.md index dc9eef1303..921c2b0233 100644 --- a/docs/content/contributing/guidelines-frontend.en-us.md +++ b/docs/content/contributing/guidelines-frontend.en-us.md @@ -92,6 +92,12 @@ it's recommended to use `const _promise = asyncFoo()` to tell readers that this is done by purpose, we want to call the async function and ignore the Promise. Some lint rules and IDEs also have warnings if the returned Promise is not handled. +### Fetching data + +To fetch data, use the wrapper functions `GET`, `POST` etc. from `modules/fetch.js`. They +accept a `data` option for the content, will automatically set CSFR token and return a +Promise for a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). + ### HTML Attributes and `dataset` The usage of `dataset` is forbidden, its camel-casing behaviour makes it hard to grep for attributes. diff --git a/docs/content/installation/windows-service.en-us.md b/docs/content/installation/windows-service.en-us.md index 201681bc03..90332b7c69 100644 --- a/docs/content/installation/windows-service.en-us.md +++ b/docs/content/installation/windows-service.en-us.md @@ -51,6 +51,15 @@ Open "Windows Services", search for the service named "gitea", right-click it an "Run". If everything is OK, Gitea will be reachable on `http://localhost:3000` (or the port that was configured). +## Service startup type + +It was observed that on loaded systems during boot Gitea service may fail to start with timeout records in Windows Event Log. +In that case change startup type to `Automatic-Delayed`. This can be done during service creation, or by running config command + +``` +sc.exe config gitea start= delayed-auto +``` + ## Adding startup dependencies To add a startup dependency to the Gitea Windows service (eg Mysql, Mariadb), as an Administrator, then run the following command: diff --git a/docs/content/usage/profile-readme.en-us.md b/docs/content/usage/profile-readme.en-us.md index fbe175eed9..045d33d1c1 100644 --- a/docs/content/usage/profile-readme.en-us.md +++ b/docs/content/usage/profile-readme.en-us.md @@ -15,6 +15,6 @@ menu: # Profile READMEs -To display a markdown file in your Gitea profile page, simply make a repository named ".profile" and edit the README.md file inside. Gitea will automatically pull this file in and display it above your repositories. +To display a Markdown file in your Gitea profile page, simply create a repository named `.profile` and add a new file called `README.md`. Gitea will automatically display the contents of the file on your profile, above your repositories. -Note. You are welcome to make this repository private. Doing so will hide your source files from public viewing and allow you to privitize certain files. However, the README.md file will be the only file present on your profile. If you wish to have an entirely private .profile repository, remove or rename the README.md file. +Making the `.profile` repository private will hide the Profile README. diff --git a/go.mod b/go.mod index f15b3c3234..a6e83df05a 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/NYTimes/gziphandler v1.1.1 github.com/PuerkitoBio/goquery v1.8.1 - github.com/alecthomas/chroma/v2 v2.8.0 + github.com/alecthomas/chroma/v2 v2.9.1 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blevesearch/bleve/v2 v2.3.9 github.com/bufbuild/connect-go v1.10.0 diff --git a/go.sum b/go.sum index 3e2bc9a754..5e32daf0c1 100644 --- a/go.sum +++ b/go.sum @@ -119,8 +119,8 @@ github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= -github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= +github.com/alecthomas/chroma/v2 v2.9.1 h1:0O3lTQh9FxazJ4BYE/MOi/vDGuHn7B+6Bu902N2UZvU= +github.com/alecthomas/chroma/v2 v2.9.1/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 800dcd0d50..849a90fd10 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -9,19 +9,21 @@ package actions import ( "context" "errors" + "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) +// ArtifactStatus is the status of an artifact, uploading, expired or need-delete +type ArtifactStatus int64 + const ( - // ArtifactStatusUploadPending is the status of an artifact upload that is pending - ArtifactStatusUploadPending = 1 - // ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed - ArtifactStatusUploadConfirmed = 2 - // ArtifactStatusUploadError is the status of an artifact upload that is errored - ArtifactStatusUploadError = 3 + ArtifactStatusUploadPending ArtifactStatus = iota + 1 // 1, ArtifactStatusUploadPending is the status of an artifact upload that is pending + ArtifactStatusUploadConfirmed // 2, ArtifactStatusUploadConfirmed is the status of an artifact upload that is confirmed + ArtifactStatusUploadError // 3, ArtifactStatusUploadError is the status of an artifact upload that is errored + ArtifactStatusExpired // 4, ArtifactStatusExpired is the status of an artifact that is expired ) func init() { @@ -45,9 +47,10 @@ type ActionArtifact struct { Status int64 `xorm:"index"` // The status of the artifact, uploading, expired or need-delete CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` + ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired } -func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string) (*ActionArtifact, error) { +func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) { if err := t.LoadJob(ctx); err != nil { return nil, err } @@ -61,7 +64,8 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa RepoID: t.RepoID, OwnerID: t.OwnerID, CommitSHA: t.CommitSHA, - Status: ArtifactStatusUploadPending, + Status: int64(ArtifactStatusUploadPending), + ExpiredUnix: timeutil.TimeStamp(time.Now().Unix() + 3600*24*expiredDays), } if _, err := db.GetEngine(ctx).Insert(artifact); err != nil { return nil, err @@ -126,15 +130,16 @@ func ListUploadedArtifactsByRunID(ctx context.Context, runID int64) ([]*ActionAr type ActionArtifactMeta struct { ArtifactName string FileSize int64 + Status int64 } // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) { arts := make([]*ActionArtifactMeta, 0, 10) return arts, db.GetEngine(ctx).Table("action_artifact"). - Where("run_id=? AND status=?", runID, ArtifactStatusUploadConfirmed). + Where("run_id=? AND (status=? OR status=?)", runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). GroupBy("artifact_name"). - Select("artifact_name, sum(file_size) as file_size"). + Select("artifact_name, sum(file_size) as file_size, max(status) as status"). Find(&arts) } @@ -149,3 +154,16 @@ func ListArtifactsByRunIDAndName(ctx context.Context, runID int64, name string) arts := make([]*ActionArtifact, 0, 10) return arts, db.GetEngine(ctx).Where("run_id=? AND artifact_name=?", runID, name).Find(&arts) } + +// ListNeedExpiredArtifacts returns all need expired artifacts but not deleted +func ListNeedExpiredArtifacts(ctx context.Context) ([]*ActionArtifact, error) { + arts := make([]*ActionArtifact, 0, 10) + return arts, db.GetEngine(ctx). + Where("expired_unix < ? AND status = ?", timeutil.TimeStamp(time.Now().Unix()), ArtifactStatusUploadConfirmed).Find(&arts) +} + +// SetArtifactExpired sets an artifact to expired +func SetArtifactExpired(ctx context.Context, artifactID int64) error { + _, err := db.GetEngine(ctx).Where("id=? AND status = ?", artifactID, ArtifactStatusUploadConfirmed).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusExpired)}) + return err +} diff --git a/models/actions/run.go b/models/actions/run.go index 18ed447e80..8078613fb8 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -6,6 +6,7 @@ package actions import ( "context" "fmt" + "slices" "strings" "time" @@ -34,7 +35,8 @@ type ActionRun struct { Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository TriggerUserID int64 `xorm:"index"` TriggerUser *user_model.User `xorm:"-"` - Ref string `xorm:"index"` // the commit/tag/… that caused the run + ScheduleID int64 + Ref string `xorm:"index"` // the commit/tag/… that caused the run 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 @@ -350,7 +352,7 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { // It's impossible that the run is not found, since Gitea never deletes runs. } - if run.Status != 0 || util.SliceContains(cols, "status") { + if run.Status != 0 || slices.Contains(cols, "status") { if run.RepoID == 0 { run, err = GetRunByID(ctx, run.ID) if err != nil { diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 1da58bb659..4b8664077d 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -6,6 +6,7 @@ package actions import ( "context" "fmt" + "slices" "time" "code.gitea.io/gitea/models/db" @@ -107,11 +108,11 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col return 0, err } - if affected == 0 || (!util.SliceContains(cols, "status") && job.Status == 0) { + if affected == 0 || (!slices.Contains(cols, "status") && job.Status == 0) { return affected, nil } - if affected != 0 && util.SliceContains(cols, "status") && job.Status.IsWaiting() { + if affected != 0 && slices.Contains(cols, "status") && job.Status.IsWaiting() { // if the status of job changes to waiting again, increase tasks version. if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil { return 0, err diff --git a/models/activities/action.go b/models/activities/action.go index 432bf8bf3f..07c8053425 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -126,6 +126,15 @@ func (at ActionType) String() string { } } +func (at ActionType) InActions(actions ...string) bool { + for _, action := range actions { + if action == at.String() { + return true + } + } + return false +} + // Action represents user operation type and other information to // repository. It implemented interface base.Actioner so that can be // used in template render. diff --git a/models/activities/main_test.go b/models/activities/main_test.go index a8740f53c4..6be54db658 100644 --- a/models/activities/main_test.go +++ b/models/activities/main_test.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/unittest" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" ) func TestMain(m *testing.M) { diff --git a/models/auth/main_test.go b/models/auth/main_test.go index 3205d8816f..f8cbf3bd54 100644 --- a/models/auth/main_test.go +++ b/models/auth/main_test.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/models/unittest" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" _ "code.gitea.io/gitea/models/activities" _ "code.gitea.io/gitea/models/auth" _ "code.gitea.io/gitea/models/perm/access" diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go index 265ee6428e..e40aa3f542 100644 --- a/models/avatars/avatar.go +++ b/models/avatars/avatar.go @@ -153,7 +153,12 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final return DefaultAvatarLink() } - enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar) + disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, + setting.GetDefaultDisableGravatar(), + ) + + enableFederatedAvatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureEnableFederatedAvatar, + setting.GetDefaultEnableFederatedAvatar(disableGravatar)) var err error if enableFederatedAvatar && system_model.LibravatarService != nil { @@ -174,7 +179,6 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final return urlStr } - disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar) if !disableGravatar { // copy GravatarSourceURL, because we will modify its Path. avatarURLCopy := *system_model.GravatarSourceURL diff --git a/models/fixtures/system_setting.yml b/models/fixtures/system_setting.yml index 6c960168fc..30542bc82a 100644 --- a/models/fixtures/system_setting.yml +++ b/models/fixtures/system_setting.yml @@ -1,6 +1,6 @@ - id: 1 - setting_key: 'disable_gravatar' + setting_key: 'picture.disable_gravatar' setting_value: 'false' version: 1 created: 1653533198 @@ -8,7 +8,7 @@ - id: 2 - setting_key: 'enable_federated_avatar' + setting_key: 'picture.enable_federated_avatar' setting_value: 'false' version: 1 created: 1653533198 diff --git a/models/git/main_test.go b/models/git/main_test.go index 5ef9cde607..a8658d70c4 100644 --- a/models/git/main_test.go +++ b/models/git/main_test.go @@ -10,6 +10,8 @@ import ( "code.gitea.io/gitea/models/unittest" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" ) func TestMain(m *testing.M) { diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index eef7e3935a..5ed1003749 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -435,7 +436,7 @@ func updateTeamWhitelist(ctx context.Context, repo *repo_model.Repository, curre whitelist = make([]int64, 0, len(teams)) for i := range teams { - if util.SliceContains(newWhitelist, teams[i].ID) { + if slices.Contains(newWhitelist, teams[i].ID) { whitelist = append(whitelist, teams[i].ID) } } diff --git a/models/issues/comment.go b/models/issues/comment.go index 17e579b455..e045b71d23 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -17,6 +17,7 @@ import ( project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -1247,3 +1248,44 @@ func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) { func (c *Comment) HasOriginalAuthor() bool { return c.OriginalAuthor != "" && c.OriginalAuthorID != 0 } + +// InsertIssueComments inserts many comments of issues. +func InsertIssueComments(comments []*Comment) error { + if len(comments) == 0 { + return nil + } + + issueIDs := make(container.Set[int64]) + for _, comment := range comments { + issueIDs.Add(comment.IssueID) + } + + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + for _, comment := range comments { + if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil { + return err + } + + for _, reaction := range comment.Reactions { + reaction.IssueID = comment.IssueID + reaction.CommentID = comment.ID + } + if len(comment.Reactions) > 0 { + if err := db.Insert(ctx, comment.Reactions); err != nil { + return err + } + } + } + + for issueID := range issueIDs { + if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", + issueID, CommentTypeComment, issueID); err != nil { + return err + } + } + return committer.Commit() +} diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index d766625be3..90db476571 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -70,3 +70,30 @@ func TestAsCommentType(t *testing.T) { assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment")) assert.Equal(t, issues_model.CommentTypePRUnScheduledToAutoMerge, issues_model.AsCommentType("pull_cancel_scheduled_merge")) } + +func TestMigrate_InsertIssueComments(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + _ = issue.LoadRepo(db.DefaultContext) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}) + reaction := &issues_model.Reaction{ + Type: "heart", + UserID: owner.ID, + } + + comment := &issues_model.Comment{ + PosterID: owner.ID, + Poster: owner, + IssueID: issue.ID, + Issue: issue, + Reactions: []*issues_model.Reaction{reaction}, + } + + err := issues_model.InsertIssueComments([]*issues_model.Comment{comment}) + assert.NoError(t, err) + + issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments) + + unittest.CheckConsistencyFor(t, &issues_model.Issue{}) +} diff --git a/models/issues/issue.go b/models/issues/issue.go index f000f4c660..341ec8547a 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "regexp" + "slices" "code.gitea.io/gitea/models/db" project_model "code.gitea.io/gitea/models/project" @@ -605,7 +606,7 @@ func IsUserParticipantsOfIssue(user *user_model.User, issue *Issue) bool { log.Error(err.Error()) return false } - return util.SliceContains(userIDs, user.ID) + return slices.Contains(userIDs, user.ID) } // DependencyInfo represents high level information about an issue which is a dependency of another issue. @@ -630,7 +631,7 @@ func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, erro Find(&userIDs); err != nil { return nil, fmt.Errorf("get poster IDs: %w", err) } - if !util.SliceContains(userIDs, issue.PosterID) { + if !slices.Contains(userIDs, issue.PosterID) { return append(userIDs, issue.PosterID), nil } return userIDs, nil @@ -891,3 +892,50 @@ func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, erro func IsErrIssueMaxPinReached(err error) bool { return err == ErrIssueMaxPinReached } + +// InsertIssues insert issues to database +func InsertIssues(issues ...*Issue) error { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + for _, issue := range issues { + if err := insertIssue(ctx, issue); err != nil { + return err + } + } + return committer.Commit() +} + +func insertIssue(ctx context.Context, issue *Issue) error { + sess := db.GetEngine(ctx) + if _, err := sess.NoAutoTime().Insert(issue); err != nil { + return err + } + issueLabels := make([]IssueLabel, 0, len(issue.Labels)) + for _, label := range issue.Labels { + issueLabels = append(issueLabels, IssueLabel{ + IssueID: issue.ID, + LabelID: label.ID, + }) + } + if len(issueLabels) > 0 { + if _, err := sess.Insert(issueLabels); err != nil { + return err + } + } + + for _, reaction := range issue.Reactions { + reaction.IssueID = issue.ID + } + + if len(issue.Reactions) > 0 { + if _, err := sess.Insert(issue.Reactions); err != nil { + return err + } + } + + return nil +} diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 0f2ceadc6b..f1bccc0cf8 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -573,3 +573,45 @@ func TestIssueLoadAttributes(t *testing.T) { } } } + +func assertCreateIssues(t *testing.T, isPull bool) { + assert.NoError(t, unittest.PrepareTestDatabase()) + reponame := "repo1" + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) + 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) + reaction := &issues_model.Reaction{ + Type: "heart", + UserID: owner.ID, + } + + title := "issuetitle1" + is := &issues_model.Issue{ + RepoID: repo.ID, + MilestoneID: milestone.ID, + Repo: repo, + Title: title, + Content: "issuecontent1", + IsPull: isPull, + PosterID: owner.ID, + Poster: owner, + IsClosed: true, + Labels: []*issues_model.Label{label}, + Reactions: []*issues_model.Reaction{reaction}, + } + err := issues_model.InsertIssues(is) + assert.NoError(t, err) + + i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID}) +} + +func TestMigrate_CreateIssuesIsPullFalse(t *testing.T) { + assertCreateIssues(t, false) +} + +func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) { + assertCreateIssues(t, true) +} diff --git a/models/issues/main_test.go b/models/issues/main_test.go index 9fbe294f70..190df027f4 100644 --- a/models/issues/main_test.go +++ b/models/issues/main_test.go @@ -11,6 +11,8 @@ import ( "code.gitea.io/gitea/models/unittest" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" _ "code.gitea.io/gitea/models/repo" _ "code.gitea.io/gitea/models/user" diff --git a/models/issues/milestone.go b/models/issues/milestone.go index 1418e0869d..c15b2a41fe 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -10,7 +10,6 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -323,261 +322,6 @@ func DeleteMilestoneByRepoID(repoID, id int64) error { return committer.Commit() } -// MilestoneList is a list of milestones offering additional functionality -type MilestoneList []*Milestone - -func (milestones MilestoneList) getMilestoneIDs() []int64 { - ids := make([]int64, 0, len(milestones)) - for _, ms := range milestones { - ids = append(ids, ms.ID) - } - return ids -} - -// GetMilestonesOption contain options to get milestones -type GetMilestonesOption struct { - db.ListOptions - RepoID int64 - State api.StateType - Name string - SortType string -} - -func (opts GetMilestonesOption) toCond() builder.Cond { - cond := builder.NewCond() - if opts.RepoID != 0 { - cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) - } - - switch opts.State { - case api.StateClosed: - cond = cond.And(builder.Eq{"is_closed": true}) - case api.StateAll: - break - // api.StateOpen: - default: - cond = cond.And(builder.Eq{"is_closed": false}) - } - - if len(opts.Name) != 0 { - cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name)) - } - - return cond -} - -// GetMilestones returns milestones filtered by GetMilestonesOption's -func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) { - sess := db.GetEngine(db.DefaultContext).Where(opts.toCond()) - - if opts.Page != 0 { - sess = db.SetSessionPagination(sess, &opts) - } - - switch opts.SortType { - case "furthestduedate": - sess.Desc("deadline_unix") - case "leastcomplete": - sess.Asc("completeness") - case "mostcomplete": - sess.Desc("completeness") - case "leastissues": - sess.Asc("num_issues") - case "mostissues": - sess.Desc("num_issues") - case "id": - sess.Asc("id") - default: - sess.Asc("deadline_unix").Asc("id") - } - - miles := make([]*Milestone, 0, opts.PageSize) - total, err := sess.FindAndCount(&miles) - return miles, total, err -} - -// GetMilestoneIDsByNames returns a list of milestone ids by given names. -// It doesn't filter them by repo, so it could return milestones belonging to different repos. -// It's used for filtering issues via indexer, otherwise it would be useless. -// Since it could return milestones with the same name, so the length of returned ids could be more than the length of names. -func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) { - var ids []int64 - return ids, db.GetEngine(ctx).Table("milestone"). - Where(db.BuildCaseInsensitiveIn("name", names)). - Cols("id"). - Find(&ids) -} - -// SearchMilestones search milestones -func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) { - miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) - sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) - if len(keyword) > 0 { - sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) - } - if repoCond.IsValid() { - sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) - } - if page > 0 { - sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) - } - - switch sortType { - case "furthestduedate": - sess.Desc("deadline_unix") - case "leastcomplete": - sess.Asc("completeness") - case "mostcomplete": - sess.Desc("completeness") - case "leastissues": - sess.Asc("num_issues") - case "mostissues": - sess.Desc("num_issues") - default: - sess.Asc("deadline_unix") - } - return miles, sess.Find(&miles) -} - -// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status. -func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) { - return SearchMilestones( - builder.In("repo_id", repoIDs), - page, - isClosed, - sortType, - "", - ) -} - -// MilestonesStats represents milestone statistic information. -type MilestonesStats struct { - OpenCount, ClosedCount int64 -} - -// Total returns the total counts of milestones -func (m MilestonesStats) Total() int64 { - return m.OpenCount + m.ClosedCount -} - -// GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions. -func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) { - var err error - stats := &MilestonesStats{} - - sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) - if repoCond.IsValid() { - sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) - } - stats.OpenCount, err = sess.Count(new(Milestone)) - if err != nil { - return nil, err - } - - sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) - if repoCond.IsValid() { - sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) - } - stats.ClosedCount, err = sess.Count(new(Milestone)) - if err != nil { - return nil, err - } - - return stats, nil -} - -// GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword. -func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*MilestonesStats, error) { - var err error - stats := &MilestonesStats{} - - sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) - if len(keyword) > 0 { - sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) - } - if repoCond.IsValid() { - sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) - } - stats.OpenCount, err = sess.Count(new(Milestone)) - if err != nil { - return nil, err - } - - sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) - if len(keyword) > 0 { - sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) - } - if repoCond.IsValid() { - sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) - } - stats.ClosedCount, err = sess.Count(new(Milestone)) - if err != nil { - return nil, err - } - - return stats, nil -} - -// CountMilestones returns number of milestones in given repository with other options -func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) { - return db.GetEngine(ctx). - Where(opts.toCond()). - Count(new(Milestone)) -} - -// CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options` -func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) { - sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) - if repoCond.IsValid() { - sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) - } - - countsSlice := make([]*struct { - RepoID int64 - Count int64 - }, 0, 10) - if err := sess.GroupBy("repo_id"). - Select("repo_id AS repo_id, COUNT(*) AS count"). - Table("milestone"). - Find(&countsSlice); err != nil { - return nil, err - } - - countMap := make(map[int64]int64, len(countsSlice)) - for _, c := range countsSlice { - countMap[c.RepoID] = c.Count - } - return countMap, nil -} - -// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options` -func CountMilestonesByRepoCondAndKw(repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) { - sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) - if len(keyword) > 0 { - sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) - } - if repoCond.IsValid() { - sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) - } - - countsSlice := make([]*struct { - RepoID int64 - Count int64 - }, 0, 10) - if err := sess.GroupBy("repo_id"). - Select("repo_id AS repo_id, COUNT(*) AS count"). - Table("milestone"). - Find(&countsSlice); err != nil { - return nil, err - } - - countMap := make(map[int64]int64, len(countsSlice)) - for _, c := range countsSlice { - countMap[c.RepoID] = c.Count - } - return countMap, nil -} - func updateRepoMilestoneNum(ctx context.Context, repoID int64) error { _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?", repoID, @@ -588,53 +332,6 @@ func updateRepoMilestoneNum(ctx context.Context, repoID int64) error { return err } -// _____ _ _ _____ _ -// |_ _| __ __ _ ___| | _____ __| |_ _(_)_ __ ___ ___ ___ -// | || '__/ _` |/ __| |/ / _ \/ _` | | | | | '_ ` _ \ / _ \/ __| -// | || | | (_| | (__| < __/ (_| | | | | | | | | | | __/\__ \ -// |_||_| \__,_|\___|_|\_\___|\__,_| |_| |_|_| |_| |_|\___||___/ -// - -func (milestones MilestoneList) loadTotalTrackedTimes(ctx context.Context) error { - type totalTimesByMilestone struct { - MilestoneID int64 - Time int64 - } - if len(milestones) == 0 { - return nil - } - trackedTimes := make(map[int64]int64, len(milestones)) - - // Get total tracked time by milestone_id - rows, err := db.GetEngine(ctx).Table("issue"). - Join("INNER", "milestone", "issue.milestone_id = milestone.id"). - Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id"). - Where("tracked_time.deleted = ?", false). - Select("milestone_id, sum(time) as time"). - In("milestone_id", milestones.getMilestoneIDs()). - GroupBy("milestone_id"). - Rows(new(totalTimesByMilestone)) - if err != nil { - return err - } - - defer rows.Close() - - for rows.Next() { - var totalTime totalTimesByMilestone - err = rows.Scan(&totalTime) - if err != nil { - return err - } - trackedTimes[totalTime.MilestoneID] = totalTime.Time - } - - for _, milestone := range milestones { - milestone.TotalTrackedTime = trackedTimes[milestone.ID] - } - return nil -} - func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error { type totalTimesByMilestone struct { MilestoneID int64 @@ -658,12 +355,33 @@ func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error { return nil } -// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request -func (milestones MilestoneList) LoadTotalTrackedTimes() error { - return milestones.loadTotalTrackedTimes(db.DefaultContext) -} - // LoadTotalTrackedTime loads the tracked time for the milestone func (m *Milestone) LoadTotalTrackedTime() error { return m.loadTotalTrackedTime(db.DefaultContext) } + +// InsertMilestones creates milestones of repository. +func InsertMilestones(ms ...*Milestone) (err error) { + if len(ms) == 0 { + return nil + } + + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + // to return the id, so we should not use batch insert + for _, m := range ms { + if _, err = sess.NoAutoTime().Insert(m); err != nil { + return err + } + } + + if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil { + return err + } + return committer.Commit() +} diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go new file mode 100644 index 0000000000..b0c29106a0 --- /dev/null +++ b/models/issues/milestone_list.go @@ -0,0 +1,315 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + + "xorm.io/builder" +) + +// MilestoneList is a list of milestones offering additional functionality +type MilestoneList []*Milestone + +func (milestones MilestoneList) getMilestoneIDs() []int64 { + ids := make([]int64, 0, len(milestones)) + for _, ms := range milestones { + ids = append(ids, ms.ID) + } + return ids +} + +// GetMilestonesOption contain options to get milestones +type GetMilestonesOption struct { + db.ListOptions + RepoID int64 + State api.StateType + Name string + SortType string +} + +func (opts GetMilestonesOption) toCond() builder.Cond { + cond := builder.NewCond() + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + + switch opts.State { + case api.StateClosed: + cond = cond.And(builder.Eq{"is_closed": true}) + case api.StateAll: + break + // api.StateOpen: + default: + cond = cond.And(builder.Eq{"is_closed": false}) + } + + if len(opts.Name) != 0 { + cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name)) + } + + return cond +} + +// GetMilestones returns milestones filtered by GetMilestonesOption's +func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) { + sess := db.GetEngine(db.DefaultContext).Where(opts.toCond()) + + if opts.Page != 0 { + sess = db.SetSessionPagination(sess, &opts) + } + + switch opts.SortType { + case "furthestduedate": + sess.Desc("deadline_unix") + case "leastcomplete": + sess.Asc("completeness") + case "mostcomplete": + sess.Desc("completeness") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + case "id": + sess.Asc("id") + default: + sess.Asc("deadline_unix").Asc("id") + } + + miles := make([]*Milestone, 0, opts.PageSize) + total, err := sess.FindAndCount(&miles) + return miles, total, err +} + +// GetMilestoneIDsByNames returns a list of milestone ids by given names. +// It doesn't filter them by repo, so it could return milestones belonging to different repos. +// It's used for filtering issues via indexer, otherwise it would be useless. +// Since it could return milestones with the same name, so the length of returned ids could be more than the length of names. +func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) { + var ids []int64 + return ids, db.GetEngine(ctx).Table("milestone"). + Where(db.BuildCaseInsensitiveIn("name", names)). + Cols("id"). + Find(&ids) +} + +// SearchMilestones search milestones +func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) { + miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) + sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) + if len(keyword) > 0 { + sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) + } + if repoCond.IsValid() { + sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) + } + if page > 0 { + sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) + } + + switch sortType { + case "furthestduedate": + sess.Desc("deadline_unix") + case "leastcomplete": + sess.Asc("completeness") + case "mostcomplete": + sess.Desc("completeness") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + default: + sess.Asc("deadline_unix") + } + return miles, sess.Find(&miles) +} + +// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status. +func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) { + return SearchMilestones( + builder.In("repo_id", repoIDs), + page, + isClosed, + sortType, + "", + ) +} + +func (milestones MilestoneList) loadTotalTrackedTimes(ctx context.Context) error { + type totalTimesByMilestone struct { + MilestoneID int64 + Time int64 + } + if len(milestones) == 0 { + return nil + } + trackedTimes := make(map[int64]int64, len(milestones)) + + // Get total tracked time by milestone_id + rows, err := db.GetEngine(ctx).Table("issue"). + Join("INNER", "milestone", "issue.milestone_id = milestone.id"). + Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id"). + Where("tracked_time.deleted = ?", false). + Select("milestone_id, sum(time) as time"). + In("milestone_id", milestones.getMilestoneIDs()). + GroupBy("milestone_id"). + Rows(new(totalTimesByMilestone)) + if err != nil { + return err + } + + defer rows.Close() + + for rows.Next() { + var totalTime totalTimesByMilestone + err = rows.Scan(&totalTime) + if err != nil { + return err + } + trackedTimes[totalTime.MilestoneID] = totalTime.Time + } + + for _, milestone := range milestones { + milestone.TotalTrackedTime = trackedTimes[milestone.ID] + } + return nil +} + +// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request +func (milestones MilestoneList) LoadTotalTrackedTimes() error { + return milestones.loadTotalTrackedTimes(db.DefaultContext) +} + +// CountMilestones returns number of milestones in given repository with other options +func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) { + return db.GetEngine(ctx). + Where(opts.toCond()). + Count(new(Milestone)) +} + +// CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options` +func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) { + sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) + if repoCond.IsValid() { + sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) + } + + countsSlice := make([]*struct { + RepoID int64 + Count int64 + }, 0, 10) + if err := sess.GroupBy("repo_id"). + Select("repo_id AS repo_id, COUNT(*) AS count"). + Table("milestone"). + Find(&countsSlice); err != nil { + return nil, err + } + + countMap := make(map[int64]int64, len(countsSlice)) + for _, c := range countsSlice { + countMap[c.RepoID] = c.Count + } + return countMap, nil +} + +// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options` +func CountMilestonesByRepoCondAndKw(repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) { + sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) + if len(keyword) > 0 { + sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) + } + if repoCond.IsValid() { + sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) + } + + countsSlice := make([]*struct { + RepoID int64 + Count int64 + }, 0, 10) + if err := sess.GroupBy("repo_id"). + Select("repo_id AS repo_id, COUNT(*) AS count"). + Table("milestone"). + Find(&countsSlice); err != nil { + return nil, err + } + + countMap := make(map[int64]int64, len(countsSlice)) + for _, c := range countsSlice { + countMap[c.RepoID] = c.Count + } + return countMap, nil +} + +// MilestonesStats represents milestone statistic information. +type MilestonesStats struct { + OpenCount, ClosedCount int64 +} + +// Total returns the total counts of milestones +func (m MilestonesStats) Total() int64 { + return m.OpenCount + m.ClosedCount +} + +// GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions. +func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) { + var err error + stats := &MilestonesStats{} + + sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) + if repoCond.IsValid() { + sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) + } + stats.OpenCount, err = sess.Count(new(Milestone)) + if err != nil { + return nil, err + } + + sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) + if repoCond.IsValid() { + sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) + } + stats.ClosedCount, err = sess.Count(new(Milestone)) + if err != nil { + return nil, err + } + + return stats, nil +} + +// GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword. +func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*MilestonesStats, error) { + var err error + stats := &MilestonesStats{} + + sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) + if len(keyword) > 0 { + sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) + } + if repoCond.IsValid() { + sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) + } + stats.OpenCount, err = sess.Count(new(Milestone)) + if err != nil { + return nil, err + } + + sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) + if len(keyword) > 0 { + sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) + } + if repoCond.IsValid() { + sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) + } + stats.ClosedCount, err = sess.Count(new(Milestone)) + if err != nil { + return nil, err + } + + return stats, nil +} diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go index 5db5655906..e85d77ebc8 100644 --- a/models/issues/milestone_test.go +++ b/models/issues/milestone_test.go @@ -351,3 +351,21 @@ func TestUpdateMilestoneCounters(t *testing.T) { assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) } + +func TestMigrate_InsertMilestones(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + reponame := "repo1" + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) + name := "milestonetest1" + ms := &issues_model.Milestone{ + RepoID: repo.ID, + Name: name, + } + err := issues_model.InsertMilestones(ms) + assert.NoError(t, err) + unittest.AssertExistsAndLoadBean(t, ms) + repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones) + + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) +} diff --git a/models/issues/pull.go b/models/issues/pull.go index 676224a3d6..1c163ecca4 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -1105,3 +1105,23 @@ func TokenizeCodeOwnersLine(line string) []string { return tokens } + +// InsertPullRequests inserted pull requests +func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + for _, pr := range prs { + if err := insertIssue(ctx, pr.Issue); err != nil { + return err + } + pr.IssueID = pr.Issue.ID + if _, err := sess.NoAutoTime().Insert(pr); err != nil { + return err + } + } + return committer.Commit() +} diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index fa1f551adb..83977560ae 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -337,3 +338,31 @@ func TestGetApprovers(t *testing.T) { expected := "Reviewed-by: User Five \nReviewed-by: User Six \n" assert.EqualValues(t, expected, approvers) } + +func TestMigrate_InsertPullRequests(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + reponame := "repo1" + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + i := &issues_model.Issue{ + RepoID: repo.ID, + Repo: repo, + Title: "title1", + Content: "issuecontent1", + IsPull: true, + PosterID: owner.ID, + Poster: owner, + } + + p := &issues_model.PullRequest{ + Issue: i, + } + + err := issues_model.InsertPullRequests(db.DefaultContext, p) + assert.NoError(t, err) + + _ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID}) + + unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{}) +} diff --git a/models/main_test.go b/models/main_test.go index d490507649..c09b661d2c 100644 --- a/models/main_test.go +++ b/models/main_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + _ "code.gitea.io/gitea/models/actions" _ "code.gitea.io/gitea/models/system" "github.com/stretchr/testify/assert" diff --git a/models/migrate.go b/models/migrate.go deleted file mode 100644 index 9705d0ad04..0000000000 --- a/models/migrate.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/structs" -) - -// InsertMilestones creates milestones of repository. -func InsertMilestones(ms ...*issues_model.Milestone) (err error) { - if len(ms) == 0 { - return nil - } - - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - // to return the id, so we should not use batch insert - for _, m := range ms { - if _, err = sess.NoAutoTime().Insert(m); err != nil { - return err - } - } - - if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil { - return err - } - return committer.Commit() -} - -// InsertIssues insert issues to database -func InsertIssues(issues ...*issues_model.Issue) error { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - for _, issue := range issues { - if err := insertIssue(ctx, issue); err != nil { - return err - } - } - return committer.Commit() -} - -func insertIssue(ctx context.Context, issue *issues_model.Issue) error { - sess := db.GetEngine(ctx) - if _, err := sess.NoAutoTime().Insert(issue); err != nil { - return err - } - issueLabels := make([]issues_model.IssueLabel, 0, len(issue.Labels)) - for _, label := range issue.Labels { - issueLabels = append(issueLabels, issues_model.IssueLabel{ - IssueID: issue.ID, - LabelID: label.ID, - }) - } - if len(issueLabels) > 0 { - if _, err := sess.Insert(issueLabels); err != nil { - return err - } - } - - for _, reaction := range issue.Reactions { - reaction.IssueID = issue.ID - } - - if len(issue.Reactions) > 0 { - if _, err := sess.Insert(issue.Reactions); err != nil { - return err - } - } - - return nil -} - -// InsertIssueComments inserts many comments of issues. -func InsertIssueComments(comments []*issues_model.Comment) error { - if len(comments) == 0 { - return nil - } - - issueIDs := make(container.Set[int64]) - for _, comment := range comments { - issueIDs.Add(comment.IssueID) - } - - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - for _, comment := range comments { - if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil { - return err - } - - for _, reaction := range comment.Reactions { - reaction.IssueID = comment.IssueID - reaction.CommentID = comment.ID - } - if len(comment.Reactions) > 0 { - if err := db.Insert(ctx, comment.Reactions); err != nil { - return err - } - } - } - - for issueID := range issueIDs { - if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", - issueID, issues_model.CommentTypeComment, issueID); err != nil { - return err - } - } - return committer.Commit() -} - -// InsertPullRequests inserted pull requests -func InsertPullRequests(ctx context.Context, prs ...*issues_model.PullRequest) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - for _, pr := range prs { - if err := insertIssue(ctx, pr.Issue); err != nil { - return err - } - pr.IssueID = pr.Issue.ID - if _, err := sess.NoAutoTime().Insert(pr); err != nil { - return err - } - } - return committer.Commit() -} - -// InsertReleases migrates release -func InsertReleases(rels ...*repo_model.Release) error { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - for _, rel := range rels { - if _, err := sess.NoAutoTime().Insert(rel); err != nil { - return err - } - - if len(rel.Attachments) > 0 { - for i := range rel.Attachments { - rel.Attachments[i].ReleaseID = rel.ID - } - - if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil { - return err - } - } - } - - return committer.Commit() -} - -// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID -func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error { - if err := issues_model.UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil { - return err - } - - if err := issues_model.UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil { - return err - } - - if err := repo_model.UpdateReleasesMigrationsByType(tp, externalUserID, userID); err != nil { - return err - } - - if err := issues_model.UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil { - return err - } - return issues_model.UpdateReviewsMigrationsByType(tp, externalUserID, userID) -} diff --git a/models/migrate_test.go b/models/migrate_test.go deleted file mode 100644 index 74736a2849..0000000000 --- a/models/migrate_test.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "testing" - - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" - repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - - "github.com/stretchr/testify/assert" -) - -func TestMigrate_InsertMilestones(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - reponame := "repo1" - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) - name := "milestonetest1" - ms := &issues_model.Milestone{ - RepoID: repo.ID, - Name: name, - } - err := InsertMilestones(ms) - assert.NoError(t, err) - unittest.AssertExistsAndLoadBean(t, ms) - repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) - assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones) - - unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) -} - -func assertCreateIssues(t *testing.T, isPull bool) { - assert.NoError(t, unittest.PrepareTestDatabase()) - reponame := "repo1" - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) - 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) - reaction := &issues_model.Reaction{ - Type: "heart", - UserID: owner.ID, - } - - title := "issuetitle1" - is := &issues_model.Issue{ - RepoID: repo.ID, - MilestoneID: milestone.ID, - Repo: repo, - Title: title, - Content: "issuecontent1", - IsPull: isPull, - PosterID: owner.ID, - Poster: owner, - IsClosed: true, - Labels: []*issues_model.Label{label}, - Reactions: []*issues_model.Reaction{reaction}, - } - err := InsertIssues(is) - assert.NoError(t, err) - - i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title}) - unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID}) -} - -func TestMigrate_CreateIssuesIsPullFalse(t *testing.T) { - assertCreateIssues(t, false) -} - -func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) { - assertCreateIssues(t, true) -} - -func TestMigrate_InsertIssueComments(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - _ = issue.LoadRepo(db.DefaultContext) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}) - reaction := &issues_model.Reaction{ - Type: "heart", - UserID: owner.ID, - } - - comment := &issues_model.Comment{ - PosterID: owner.ID, - Poster: owner, - IssueID: issue.ID, - Issue: issue, - Reactions: []*issues_model.Reaction{reaction}, - } - - err := InsertIssueComments([]*issues_model.Comment{comment}) - assert.NoError(t, err) - - issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments) - - unittest.CheckConsistencyFor(t, &issues_model.Issue{}) -} - -func TestMigrate_InsertPullRequests(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - reponame := "repo1" - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - i := &issues_model.Issue{ - RepoID: repo.ID, - Repo: repo, - Title: "title1", - Content: "issuecontent1", - IsPull: true, - PosterID: owner.ID, - Poster: owner, - } - - p := &issues_model.PullRequest{ - Issue: i, - } - - err := InsertPullRequests(db.DefaultContext, p) - assert.NoError(t, err) - - _ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID}) - - unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{}) -} - -func TestMigrate_InsertReleases(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - a := &repo_model.Attachment{ - UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12", - } - r := &repo_model.Release{ - Attachments: []*repo_model.Attachment{a}, - } - - err := InsertReleases(r) - assert.NoError(t, err) -} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 9f4acda236..f0a8b05d53 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -528,6 +528,10 @@ var migrations = []Migration{ NewMigration("Add Version to ActionRun table", v1_21.AddVersionToActionRunTable), // v273 -> v274 NewMigration("Add Action Schedule Table", v1_21.AddActionScheduleTable), + // v274 -> v275 + NewMigration("Add Actions artifacts expiration date", v1_21.AddExpiredUnixColumnInActionArtifactTable), + // v275 -> v276 + NewMigration("Add ScheduleID for ActionRun", v1_21.AddScheduleIDForActionRun), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_21/v274.go b/models/migrations/v1_21/v274.go new file mode 100644 index 0000000000..df5994f159 --- /dev/null +++ b/models/migrations/v1_21/v274.go @@ -0,0 +1,36 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint +import ( + "time" + + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func AddExpiredUnixColumnInActionArtifactTable(x *xorm.Engine) error { + type ActionArtifact struct { + ExpiredUnix timeutil.TimeStamp `xorm:"index"` // time when the artifact will be expired + } + if err := x.Sync(new(ActionArtifact)); err != nil { + return err + } + return updateArtifactsExpiredUnixTo90Days(x) +} + +func updateArtifactsExpiredUnixTo90Days(x *xorm.Engine) error { + sess := x.NewSession() + defer sess.Close() + + if err := sess.Begin(); err != nil { + return err + } + expiredTime := time.Now().AddDate(0, 0, 90).Unix() + if _, err := sess.Exec(`UPDATE action_artifact SET expired_unix=? WHERE status='2' AND expired_unix is NULL`, expiredTime); err != nil { + return err + } + + return sess.Commit() +} diff --git a/models/migrations/v1_21/v275.go b/models/migrations/v1_21/v275.go new file mode 100644 index 0000000000..78804a59d6 --- /dev/null +++ b/models/migrations/v1_21/v275.go @@ -0,0 +1,15 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint + +import ( + "xorm.io/xorm" +) + +func AddScheduleIDForActionRun(x *xorm.Engine) error { + type ActionRun struct { + ScheduleID int64 + } + return x.Sync(new(ActionRun)) +} diff --git a/models/org_team.go b/models/org_team.go index be0c859a4b..cf3680990d 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -151,85 +151,6 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error return nil } -// HasRepository returns true if given repository belong to team. -func HasRepository(t *organization.Team, repoID int64) bool { - return organization.HasTeamRepo(db.DefaultContext, t.OrgID, t.ID, repoID) -} - -// removeRepository removes a repository from a team and recalculates access -// Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted) -func removeRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository, recalculate bool) (err error) { - e := db.GetEngine(ctx) - if err = organization.RemoveTeamRepo(ctx, t.ID, repo.ID); err != nil { - return err - } - - t.NumRepos-- - if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil { - return err - } - - // Don't need to recalculate when delete a repository from organization. - if recalculate { - if err = access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil { - return err - } - } - - teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID) - if err != nil { - return fmt.Errorf("getTeamUsersByTeamID: %w", err) - } - for _, teamUser := range teamUsers { - has, err := access_model.HasAccess(ctx, teamUser.UID, repo) - if err != nil { - return err - } else if has { - continue - } - - if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil { - return err - } - - // Remove all IssueWatches a user has subscribed to in the repositories - if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil { - return err - } - } - - return nil -} - -// RemoveRepository removes repository from team of organization. -// If the team shall include all repositories the request is ignored. -func RemoveRepository(t *organization.Team, repoID int64) error { - if !HasRepository(t, repoID) { - return nil - } - - if t.IncludesAllRepositories { - return nil - } - - repo, err := repo_model.GetRepositoryByID(db.DefaultContext, repoID) - if err != nil { - return err - } - - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if err = removeRepository(ctx, t, repo, true); err != nil { - return err - } - - return committer.Commit() -} - // NewTeam creates a record of new team. // It's caller's responsibility to assign organization ID. func NewTeam(t *organization.Team) (err error) { @@ -564,12 +485,12 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64 } // Remove watches from now unaccessible - if err := reconsiderWatches(ctx, repo, userID); err != nil { + if err := ReconsiderWatches(ctx, repo, userID); err != nil { return err } // Remove issue assignments from now unaccessible - if err := reconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { + if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { return err } } @@ -602,3 +523,33 @@ func RemoveTeamMember(team *organization.Team, userID int64) error { } return committer.Commit() } + +func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error { + user, err := user_model.GetUserByID(ctx, uid) + if err != nil { + return err + } + + if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { + return err + } + + if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). + In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). + Delete(&issues_model.IssueAssignees{}); err != nil { + return fmt.Errorf("Could not delete assignee[%d] %w", uid, err) + } + return nil +} + +func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error { + if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has { + return err + } + if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repository + return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) +} diff --git a/models/org_team_test.go b/models/org_team_test.go index 446084c815..4978f8ef99 100644 --- a/models/org_team_test.go +++ b/models/org_team_test.go @@ -51,36 +51,6 @@ func TestTeam_RemoveMember(t *testing.T) { assert.True(t, organization.IsErrLastOrgOwner(err)) } -func TestTeam_HasRepository(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - test := func(teamID, repoID int64, expected bool) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.Equal(t, expected, HasRepository(team, repoID)) - } - test(1, 1, false) - test(1, 3, true) - test(1, 5, true) - test(1, unittest.NonexistentID, false) - - test(2, 3, true) - test(2, 5, false) -} - -func TestTeam_RemoveRepository(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - testSuccess := func(teamID, repoID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, RemoveRepository(team, repoID)) - unittest.AssertNotExistsBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repoID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &repo_model.Repository{ID: repoID}) - } - testSuccess(2, 3) - testSuccess(2, 5) - testSuccess(1, unittest.NonexistentID) -} - func TestIsUsableTeamName(t *testing.T) { assert.NoError(t, organization.IsUsableTeamName("usable")) assert.True(t, db.IsErrNameReserved(organization.IsUsableTeamName("new"))) diff --git a/models/organization/main_test.go b/models/organization/main_test.go index 7ccf8c8efd..bc5bde2565 100644 --- a/models/organization/main_test.go +++ b/models/organization/main_test.go @@ -10,6 +10,8 @@ import ( "code.gitea.io/gitea/models/unittest" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" _ "code.gitea.io/gitea/models/organization" _ "code.gitea.io/gitea/models/repo" _ "code.gitea.io/gitea/models/user" diff --git a/models/packages/package_test.go b/models/packages/package_test.go index 735688a731..525a9f08d6 100644 --- a/models/packages/package_test.go +++ b/models/packages/package_test.go @@ -13,6 +13,8 @@ import ( user_model "code.gitea.io/gitea/models/user" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" "github.com/stretchr/testify/assert" ) diff --git a/models/perm/access/main_test.go b/models/perm/access/main_test.go index 837a9db437..8102cae496 100644 --- a/models/perm/access/main_test.go +++ b/models/perm/access/main_test.go @@ -10,6 +10,8 @@ import ( "code.gitea.io/gitea/models/unittest" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" _ "code.gitea.io/gitea/models/repo" _ "code.gitea.io/gitea/models/user" ) diff --git a/models/repo.go b/models/repo.go index 74a88d4c48..de0f7ee345 100644 --- a/models/repo.go +++ b/models/repo.go @@ -11,28 +11,15 @@ import ( _ "image/jpeg" // Needed for jpeg support - actions_model "code.gitea.io/gitea/models/actions" - activities_model "code.gitea.io/gitea/models/activities" - admin_model "code.gitea.io/gitea/models/admin" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" - git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" - "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" - project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" - secret_model "code.gitea.io/gitea/models/secret" system_model "code.gitea.io/gitea/models/system" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/models/webhook" - actions_module "code.gitea.io/gitea/modules/actions" - "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/storage" - - "xorm.io/builder" ) // Init initialize model @@ -43,319 +30,6 @@ func Init(ctx context.Context) error { return system_model.Init(ctx) } -// DeleteRepository deletes a repository for a user or organization. -// make sure if you call this func to close open sessions (sqlite will otherwise get a deadlock) -func DeleteRepository(doer *user_model.User, uid, repoID int64) error { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - // Query the action tasks of this repo, they will be needed after they have been deleted to remove the logs - tasks, err := actions_model.FindTasks(ctx, actions_model.FindTaskOptions{RepoID: repoID}) - if err != nil { - return fmt.Errorf("find actions tasks of repo %v: %w", repoID, err) - } - - // Query the artifacts of this repo, they will be needed after they have been deleted to remove artifacts files in ObjectStorage - artifacts, err := actions_model.ListArtifactsByRepoID(ctx, repoID) - if err != nil { - return fmt.Errorf("list actions artifacts of repo %v: %w", repoID, err) - } - - // In case is a organization. - org, err := user_model.GetUserByID(ctx, uid) - if err != nil { - return err - } - - repo := &repo_model.Repository{OwnerID: uid} - has, err := sess.ID(repoID).Get(repo) - if err != nil { - return err - } else if !has { - return repo_model.ErrRepoNotExist{ - ID: repoID, - UID: uid, - OwnerName: "", - Name: "", - } - } - - // Delete Deploy Keys - deployKeys, err := asymkey_model.ListDeployKeys(ctx, &asymkey_model.ListDeployKeysOptions{RepoID: repoID}) - if err != nil { - return fmt.Errorf("listDeployKeys: %w", err) - } - needRewriteKeysFile := len(deployKeys) > 0 - for _, dKey := range deployKeys { - if err := DeleteDeployKey(ctx, doer, dKey.ID); err != nil { - return fmt.Errorf("deleteDeployKeys: %w", err) - } - } - - if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil { - return err - } else if cnt != 1 { - return repo_model.ErrRepoNotExist{ - ID: repoID, - UID: uid, - OwnerName: "", - Name: "", - } - } - - if org.IsOrganization() { - teams, err := organization.FindOrgTeams(ctx, org.ID) - if err != nil { - return err - } - for _, t := range teams { - if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID) { - continue - } else if err = removeRepository(ctx, t, repo, false); err != nil { - return err - } - } - } - - attachments := make([]*repo_model.Attachment, 0, 20) - if err = sess.Join("INNER", "`release`", "`release`.id = `attachment`.release_id"). - Where("`release`.repo_id = ?", repoID). - Find(&attachments); err != nil { - return err - } - releaseAttachments := make([]string, 0, len(attachments)) - for i := 0; i < len(attachments); i++ { - releaseAttachments = append(releaseAttachments, attachments[i].RelativePath()) - } - - if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars=num_stars-1 WHERE id IN (SELECT `uid` FROM `star` WHERE repo_id = ?)", repo.ID); err != nil { - return err - } - - if _, err := db.GetEngine(ctx).In("hook_id", builder.Select("id").From("webhook").Where(builder.Eq{"webhook.repo_id": repo.ID})). - Delete(&webhook.HookTask{}); err != nil { - return err - } - - if err := db.DeleteBeans(ctx, - &access_model.Access{RepoID: repo.ID}, - &activities_model.Action{RepoID: repo.ID}, - &repo_model.Collaboration{RepoID: repoID}, - &issues_model.Comment{RefRepoID: repoID}, - &git_model.CommitStatus{RepoID: repoID}, - &git_model.Branch{RepoID: repoID}, - &git_model.LFSLock{RepoID: repoID}, - &repo_model.LanguageStat{RepoID: repoID}, - &issues_model.Milestone{RepoID: repoID}, - &repo_model.Mirror{RepoID: repoID}, - &activities_model.Notification{RepoID: repoID}, - &git_model.ProtectedBranch{RepoID: repoID}, - &git_model.ProtectedTag{RepoID: repoID}, - &repo_model.PushMirror{RepoID: repoID}, - &repo_model.Release{RepoID: repoID}, - &repo_model.RepoIndexerStatus{RepoID: repoID}, - &repo_model.Redirect{RedirectRepoID: repoID}, - &repo_model.RepoUnit{RepoID: repoID}, - &repo_model.Star{RepoID: repoID}, - &admin_model.Task{RepoID: repoID}, - &repo_model.Watch{RepoID: repoID}, - &webhook.Webhook{RepoID: repoID}, - &secret_model.Secret{RepoID: repoID}, - &actions_model.ActionTaskStep{RepoID: repoID}, - &actions_model.ActionTask{RepoID: repoID}, - &actions_model.ActionRunJob{RepoID: repoID}, - &actions_model.ActionRun{RepoID: repoID}, - &actions_model.ActionRunner{RepoID: repoID}, - &actions_model.ActionScheduleSpec{RepoID: repoID}, - &actions_model.ActionSchedule{RepoID: repoID}, - &actions_model.ActionArtifact{RepoID: repoID}, - ); err != nil { - return fmt.Errorf("deleteBeans: %w", err) - } - - // Delete Labels and related objects - if err := issues_model.DeleteLabelsByRepoID(ctx, repoID); err != nil { - return err - } - - // Delete Pulls and related objects - if err := issues_model.DeletePullsByBaseRepoID(ctx, repoID); err != nil { - return err - } - - // Delete Issues and related objects - var attachmentPaths []string - if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil { - return err - } - - // Delete issue index - if err := db.DeleteResourceIndex(ctx, "issue_index", repoID); err != nil { - return err - } - - if repo.IsFork { - if _, err := db.Exec(ctx, "UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil { - return fmt.Errorf("decrease fork count: %w", err) - } - } - - if _, err := db.Exec(ctx, "UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", uid); err != nil { - return err - } - - if len(repo.Topics) > 0 { - if err := repo_model.RemoveTopicsFromRepo(ctx, repo.ID); err != nil { - return err - } - } - - if err := project_model.DeleteProjectByRepoID(ctx, repoID); err != nil { - return fmt.Errorf("unable to delete projects for repo[%d]: %w", repoID, err) - } - - // Remove LFS objects - var lfsObjects []*git_model.LFSMetaObject - if err = sess.Where("repository_id=?", repoID).Find(&lfsObjects); err != nil { - return err - } - - lfsPaths := make([]string, 0, len(lfsObjects)) - for _, v := range lfsObjects { - count, err := db.CountByBean(ctx, &git_model.LFSMetaObject{Pointer: lfs.Pointer{Oid: v.Oid}}) - if err != nil { - return err - } - if count > 1 { - continue - } - - lfsPaths = append(lfsPaths, v.RelativePath()) - } - - if _, err := db.DeleteByBean(ctx, &git_model.LFSMetaObject{RepositoryID: repoID}); err != nil { - return err - } - - // Remove archives - var archives []*repo_model.RepoArchiver - if err = sess.Where("repo_id=?", repoID).Find(&archives); err != nil { - return err - } - - archivePaths := make([]string, 0, len(archives)) - for _, v := range archives { - archivePaths = append(archivePaths, v.RelativePath()) - } - - if _, err := db.DeleteByBean(ctx, &repo_model.RepoArchiver{RepoID: repoID}); err != nil { - return err - } - - if repo.NumForks > 0 { - if _, err = sess.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil { - log.Error("reset 'fork_id' and 'is_fork': %v", err) - } - } - - // Get all attachments with both issue_id and release_id are zero - var newAttachments []*repo_model.Attachment - if err := sess.Where(builder.Eq{ - "repo_id": repo.ID, - "issue_id": 0, - "release_id": 0, - }).Find(&newAttachments); err != nil { - return err - } - - newAttachmentPaths := make([]string, 0, len(newAttachments)) - for _, attach := range newAttachments { - newAttachmentPaths = append(newAttachmentPaths, attach.RelativePath()) - } - - if _, err := sess.Where("repo_id=?", repo.ID).Delete(new(repo_model.Attachment)); err != nil { - return err - } - - if err = committer.Commit(); err != nil { - return err - } - - committer.Close() - - if needRewriteKeysFile { - if err := asymkey_model.RewriteAllPublicKeys(); err != nil { - log.Error("RewriteAllPublicKeys failed: %v", err) - } - } - - // We should always delete the files after the database transaction succeed. If - // we delete the file but the database rollback, the repository will be broken. - - // Remove repository files. - repoPath := repo.RepoPath() - system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository files", repoPath) - - // Remove wiki files - if repo.HasWiki() { - system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath()) - } - - // Remove archives - for _, archive := range archivePaths { - system_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archive) - } - - // Remove lfs objects - for _, lfsObj := range lfsPaths { - system_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsObj) - } - - // Remove issue attachment files. - for _, attachment := range attachmentPaths { - system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachment) - } - - // Remove release attachment files. - for _, releaseAttachment := range releaseAttachments { - system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachment) - } - - // Remove attachment with no issue_id and release_id. - for _, newAttachment := range newAttachmentPaths { - system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachment) - } - - if len(repo.Avatar) > 0 { - if err := storage.RepoAvatars.Delete(repo.CustomAvatarRelativePath()); err != nil { - return fmt.Errorf("Failed to remove %s: %w", repo.Avatar, err) - } - } - - // Finally, delete action logs after the actions have already been deleted to avoid new log files - for _, task := range tasks { - err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename) - if err != nil { - log.Error("remove log file %q: %v", task.LogFilename, err) - // go on - } - } - - // delete actions artifacts in ObjectStorage after the repo have already been deleted - for _, art := range artifacts { - if err := storage.ActionsArtifacts.Delete(art.StoragePath); err != nil { - log.Error("remove artifact file %q: %v", art.StoragePath, err) - // go on - } - } - - return nil -} - type repoChecker struct { querySQL func(ctx context.Context) ([]map[string][]byte, error) correctSQL func(ctx context.Context, id int64) error diff --git a/models/repo/main_test.go b/models/repo/main_test.go index bb9be54b9c..ff97c7ac9e 100644 --- a/models/repo/main_test.go +++ b/models/repo/main_test.go @@ -9,7 +9,9 @@ import ( "code.gitea.io/gitea/models/unittest" - _ "code.gitea.io/gitea/models" // register table model + _ "code.gitea.io/gitea/models" // register table model + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" _ "code.gitea.io/gitea/models/perm/access" // register table model _ "code.gitea.io/gitea/models/repo" // register table model _ "code.gitea.io/gitea/models/user" // register table model diff --git a/models/repo/release.go b/models/repo/release.go index 191475d541..0e92474365 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -552,3 +552,31 @@ func (r *Release) GetExternalName() string { return r.OriginalAuthor } // ExternalID ExternalUserRemappable interface func (r *Release) GetExternalID() int64 { return r.OriginalAuthorID } + +// InsertReleases migrates release +func InsertReleases(rels ...*Release) error { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + for _, rel := range rels { + if _, err := sess.NoAutoTime().Insert(rel); err != nil { + return err + } + + if len(rel.Attachments) > 0 { + for i := range rel.Attachments { + rel.Attachments[i].ReleaseID = rel.ID + } + + if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil { + return err + } + } + } + + return committer.Commit() +} diff --git a/models/repo/release_test.go b/models/repo/release_test.go new file mode 100644 index 0000000000..2a45ab32f3 --- /dev/null +++ b/models/repo/release_test.go @@ -0,0 +1,26 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestMigrate_InsertReleases(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + a := &Attachment{ + UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12", + } + r := &Release{ + Attachments: []*Attachment{a}, + } + + err := InsertReleases(r) + assert.NoError(t, err) +} diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index cf9ff93d32..b8804c6df1 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -6,6 +6,7 @@ package repo import ( "context" "fmt" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -176,7 +177,7 @@ func (cfg *ActionsConfig) ToString() string { } func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { - return util.SliceContains(cfg.DisabledWorkflows, file) + return slices.Contains(cfg.DisabledWorkflows, file) } func (cfg *ActionsConfig) DisableWorkflow(file string) { diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go deleted file mode 100644 index b85880eab9..0000000000 --- a/models/repo_collaboration.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2016 The Gogs Authors. All rights reserved. -// Copyright 2020 The Gitea Authors. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - "fmt" - - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" - 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" - - "xorm.io/builder" -) - -// DeleteCollaboration removes collaboration relation between the user and repository. -func DeleteCollaboration(repo *repo_model.Repository, uid int64) (err error) { - collaboration := &repo_model.Collaboration{ - RepoID: repo.ID, - UserID: uid, - } - - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if has, err := db.GetEngine(ctx).Delete(collaboration); err != nil || has == 0 { - return err - } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { - return err - } - - if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { - return err - } - - if err = reconsiderWatches(ctx, repo, uid); err != nil { - return err - } - - // Unassign a user from any issue (s)he has been assigned to in the repository - if err := reconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil { - return err - } - - return committer.Commit() -} - -func reconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error { - user, err := user_model.GetUserByID(ctx, uid) - if err != nil { - return err - } - - if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { - return err - } - - if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). - In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). - Delete(&issues_model.IssueAssignees{}); err != nil { - return fmt.Errorf("Could not delete assignee[%d] %w", uid, err) - } - return nil -} - -func reconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error { - if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has { - return err - } - if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { - return err - } - - // Remove all IssueWatches a user has subscribed to in the repository - return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) -} diff --git a/models/system/main_test.go b/models/system/main_test.go index 94e2906447..e074abc155 100644 --- a/models/system/main_test.go +++ b/models/system/main_test.go @@ -9,7 +9,9 @@ import ( "code.gitea.io/gitea/models/unittest" - _ "code.gitea.io/gitea/models" // register models + _ "code.gitea.io/gitea/models" // register models + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" _ "code.gitea.io/gitea/models/system" // register models of system ) diff --git a/models/system/setting.go b/models/system/setting.go index 6af759dc8a..9fffa74e94 100644 --- a/models/system/setting.go +++ b/models/system/setting.go @@ -94,11 +94,14 @@ func GetSetting(ctx context.Context, key string) (*Setting, error) { const contextCacheKey = "system_setting" // GetSettingWithCache returns the setting value via the key -func GetSettingWithCache(ctx context.Context, key string) (string, error) { +func GetSettingWithCache(ctx context.Context, key, defaultVal string) (string, error) { return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) { return cache.GetString(genSettingCacheKey(key), func() (string, error) { res, err := GetSetting(ctx, key) if err != nil { + if IsErrSettingIsNotExist(err) { + return defaultVal, nil + } return "", err } return res.SettingValue, nil @@ -108,17 +111,21 @@ func GetSettingWithCache(ctx context.Context, key string) (string, error) { // GetSettingBool return bool value of setting, // none existing keys and errors are ignored and result in false -func GetSettingBool(ctx context.Context, key string) bool { - s, _ := GetSetting(ctx, key) - if s == nil { - return false +func GetSettingBool(ctx context.Context, key string, defaultVal bool) (bool, error) { + s, err := GetSetting(ctx, key) + switch { + case err == nil: + v, _ := strconv.ParseBool(s.SettingValue) + return v, nil + case IsErrSettingIsNotExist(err): + return defaultVal, nil + default: + return false, err } - v, _ := strconv.ParseBool(s.SettingValue) - return v } -func GetSettingWithCacheBool(ctx context.Context, key string) bool { - s, _ := GetSettingWithCache(ctx, key) +func GetSettingWithCacheBool(ctx context.Context, key string, defaultVal bool) bool { + s, _ := GetSettingWithCache(ctx, key, strconv.FormatBool(defaultVal)) v, _ := strconv.ParseBool(s) return v } @@ -259,52 +266,41 @@ var ( ) func Init(ctx context.Context) error { - var disableGravatar bool - disableGravatarSetting, err := GetSetting(ctx, KeyPictureDisableGravatar) - if IsErrSettingIsNotExist(err) { - disableGravatar = setting_module.GetDefaultDisableGravatar() - disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)} - } else if err != nil { + disableGravatar, err := GetSettingBool(ctx, KeyPictureDisableGravatar, setting_module.GetDefaultDisableGravatar()) + if err != nil { return err - } else { - disableGravatar = disableGravatarSetting.GetValueBool() } - var enableFederatedAvatar bool - enableFederatedAvatarSetting, err := GetSetting(ctx, KeyPictureEnableFederatedAvatar) - if IsErrSettingIsNotExist(err) { - enableFederatedAvatar = setting_module.GetDefaultEnableFederatedAvatar(disableGravatar) - enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)} - } else if err != nil { + enableFederatedAvatar, err := GetSettingBool(ctx, KeyPictureEnableFederatedAvatar, setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)) + if err != nil { return err - } else { - enableFederatedAvatar = disableGravatarSetting.GetValueBool() } if setting_module.OfflineMode { - disableGravatar = true - enableFederatedAvatar = false - if !GetSettingBool(ctx, KeyPictureDisableGravatar) { + if !disableGravatar { if err := SetSettingNoVersion(ctx, KeyPictureDisableGravatar, "true"); err != nil { - return fmt.Errorf("Failed to set setting %q: %w", KeyPictureDisableGravatar, err) + return fmt.Errorf("failed to set setting %q: %w", KeyPictureDisableGravatar, err) } } - if GetSettingBool(ctx, KeyPictureEnableFederatedAvatar) { + disableGravatar = true + + if enableFederatedAvatar { if err := SetSettingNoVersion(ctx, KeyPictureEnableFederatedAvatar, "false"); err != nil { - return fmt.Errorf("Failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err) + return fmt.Errorf("failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err) } } + enableFederatedAvatar = false } if enableFederatedAvatar || !disableGravatar { var err error GravatarSourceURL, err = url.Parse(setting_module.GravatarSource) if err != nil { - return fmt.Errorf("Failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err) + return fmt.Errorf("failed to parse Gravatar URL(%s): %w", setting_module.GravatarSource, err) } } - if GravatarSourceURL != nil && enableFederatedAvatarSetting.GetValueBool() { + if GravatarSourceURL != nil && enableFederatedAvatar { LibravatarService = libravatar.New() if GravatarSourceURL.Scheme == "https" { LibravatarService.SetUseHTTPS(true) diff --git a/models/user/avatar.go b/models/user/avatar.go index 3d1e2ed20b..7f996fa79a 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -67,7 +67,9 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { useLocalAvatar := false autoGenerateAvatar := false - disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar) + disableGravatar := system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, + setting.GetDefaultDisableGravatar(), + ) switch { case u.UseCustomAvatar: diff --git a/models/user/main_test.go b/models/user/main_test.go index 0d76aacd5f..8833cc7386 100644 --- a/models/user/main_test.go +++ b/models/user/main_test.go @@ -10,6 +10,8 @@ import ( "code.gitea.io/gitea/models/unittest" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" _ "code.gitea.io/gitea/models/user" ) diff --git a/modules/activitypub/client_test.go b/modules/activitypub/client_test.go index 0ab512c5ba..83000b96d5 100644 --- a/modules/activitypub/client_test.go +++ b/modules/activitypub/client_test.go @@ -15,8 +15,6 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" - _ "code.gitea.io/gitea/models" // https://discourse.gitea.io/t/testfixtures-could-not-clean-table-access-no-such-table-access/4137/4 - "github.com/stretchr/testify/assert" ) diff --git a/modules/activitypub/main_test.go b/modules/activitypub/main_test.go index 15399ca380..dcd57bb59e 100644 --- a/modules/activitypub/main_test.go +++ b/modules/activitypub/main_test.go @@ -8,6 +8,10 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" ) func TestMain(m *testing.M) { diff --git a/modules/context/org.go b/modules/context/org.go index 2d7cf5185c..7638ffdd3f 100644 --- a/modules/context/org.go +++ b/modules/context/org.go @@ -250,6 +250,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { return } } + ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["CanReadProjects"] = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) ctx.Data["CanReadPackages"] = ctx.Org.CanReadUnit(ctx, unit.TypePackages) diff --git a/modules/context/repo.go b/modules/context/repo.go index f5c56cf833..8a16d311b1 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -471,6 +471,7 @@ func RepoAssignment(ctx *Context) context.CancelFunc { } ctx.Repo.Owner = owner ctx.ContextUser = owner + ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["Username"] = ctx.Repo.Owner.Name // redirect link to wiki diff --git a/modules/git/command.go b/modules/git/command.go index c38fd04696..f095bb18be 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -221,8 +221,18 @@ type RunOpts struct { Dir string Stdout, Stderr io.Writer - Stdin io.Reader - PipelineFunc func(context.Context, context.CancelFunc) error + + // Stdin is used for passing input to the command + // The caller must make sure the Stdin writer is closed properly to finish the Run function. + // Otherwise, the Run function may hang for long time or forever, especially when the Git's context deadline is not the same as the caller's. + // Some common mistakes: + // * `defer stdinWriter.Close()` then call `cmd.Run()`: the Run() would never return if the command is killed by timeout + // * `go { case <- parentContext.Done(): stdinWriter.Close() }` with `cmd.Run(DefaultTimeout)`: the command would have been killed by timeout but the Run doesn't return until stdinWriter.Close() + // * `go { if stdoutReader.Read() err != nil: stdinWriter.Close() }` with `cmd.Run()`: the stdoutReader may never return error if the command is killed by timeout + // In the future, ideally the git module itself should have full control of the stdin, to avoid such problems and make it easier to refactor to a better architecture. + Stdin io.Reader + + PipelineFunc func(context.Context, context.CancelFunc) error } func commonBaseEnvs() []string { diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index 13d06874c9..019773fe51 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -7,6 +7,7 @@ import ( "context" "os" "runtime/pprof" + "slices" "sync/atomic" "time" @@ -20,7 +21,6 @@ import ( "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) var ( @@ -54,22 +54,22 @@ func index(ctx context.Context, indexer internal.Indexer, repoID int64) error { } // skip forks from being indexed if unit is not present - if !util.SliceContains(repoTypes, "forks") && repo.IsFork { + if !slices.Contains(repoTypes, "forks") && repo.IsFork { return nil } // skip mirrors from being indexed if unit is not present - if !util.SliceContains(repoTypes, "mirrors") && repo.IsMirror { + if !slices.Contains(repoTypes, "mirrors") && repo.IsMirror { return nil } // skip templates from being indexed if unit is not present - if !util.SliceContains(repoTypes, "templates") && repo.IsTemplate { + if !slices.Contains(repoTypes, "templates") && repo.IsTemplate { return nil } // skip regular repos from being indexed if unit is not present - if !util.SliceContains(repoTypes, "sources") && !repo.IsFork && !repo.IsMirror && !repo.IsTemplate { + if !slices.Contains(repoTypes, "sources") && !repo.IsFork && !repo.IsMirror && !repo.IsTemplate { return nil } @@ -122,21 +122,6 @@ func Init() { indexer := *globalIndexer.Load() for _, indexerData := range items { log.Trace("IndexerData Process Repo: %d", indexerData.RepoID) - - // FIXME: it seems there is a bug in `CatFileBatch` or `nio.Pipe`, which will cause the process to hang forever in rare cases - /* - sync.(*Cond).Wait(cond.go:70) - github.com/djherbis/nio/v3.(*PipeReader).Read(sync.go:106) - bufio.(*Reader).fill(bufio.go:106) - bufio.(*Reader).ReadSlice(bufio.go:372) - bufio.(*Reader).collectFragments(bufio.go:447) - bufio.(*Reader).ReadString(bufio.go:494) - code.gitea.io/gitea/modules/git.ReadBatchLine(batch_reader.go:149) - code.gitea.io/gitea/modules/indexer/code.(*BleveIndexer).addUpdate(bleve.go:214) - code.gitea.io/gitea/modules/indexer/code.(*BleveIndexer).Index(bleve.go:296) - code.gitea.io/gitea/modules/indexer/code.(*wrappedIndexer).Index(wrapped.go:74) - code.gitea.io/gitea/modules/indexer/code.index(indexer.go:105) - */ if err := index(ctx, indexer, indexerData.RepoID); err != nil { unhandled = append(unhandled, indexerData) if !setting.IsInTesting { diff --git a/modules/indexer/code/indexer_test.go b/modules/indexer/code/indexer_test.go index 55616a0361..2a650646bd 100644 --- a/modules/indexer/code/indexer_test.go +++ b/modules/indexer/code/indexer_test.go @@ -16,6 +16,8 @@ import ( "code.gitea.io/gitea/modules/indexer/code/internal" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" "github.com/stretchr/testify/assert" ) diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index d6812f714e..a4e1c899fc 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -15,6 +15,8 @@ import ( "code.gitea.io/gitea/modules/setting" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" "github.com/stretchr/testify/assert" ) diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 93d38a0b37..06fddeb65b 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -10,6 +10,7 @@ package tests import ( "context" "fmt" + "slices" "testing" "time" @@ -457,7 +458,7 @@ var cases = []*testIndexerCase{ assert.Contains(t, data[v.ID].MentionIDs, int64(1)) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return util.SliceContains(v.MentionIDs, 1) + return slices.Contains(v.MentionIDs, 1) }), result.Total) }, }, @@ -478,7 +479,7 @@ var cases = []*testIndexerCase{ assert.Contains(t, data[v.ID].ReviewedIDs, int64(1)) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return util.SliceContains(v.ReviewedIDs, 1) + return slices.Contains(v.ReviewedIDs, 1) }), result.Total) }, }, @@ -499,7 +500,7 @@ var cases = []*testIndexerCase{ assert.Contains(t, data[v.ID].ReviewRequestedIDs, int64(1)) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return util.SliceContains(v.ReviewRequestedIDs, 1) + return slices.Contains(v.ReviewRequestedIDs, 1) }), result.Total) }, }, @@ -520,7 +521,7 @@ var cases = []*testIndexerCase{ assert.Contains(t, data[v.ID].SubscriberIDs, int64(1)) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return util.SliceContains(v.SubscriberIDs, 1) + return slices.Contains(v.SubscriberIDs, 1) }), result.Total) }, }, diff --git a/modules/indexer/stats/indexer_test.go b/modules/indexer/stats/indexer_test.go index 2d9844f8c9..c031515434 100644 --- a/modules/indexer/stats/indexer_test.go +++ b/modules/indexer/stats/indexer_test.go @@ -16,6 +16,8 @@ import ( "code.gitea.io/gitea/modules/setting" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" "github.com/stretchr/testify/assert" ) diff --git a/modules/queue/queue.go b/modules/queue/queue.go index 0ab8dd4ae4..1ad7320e31 100644 --- a/modules/queue/queue.go +++ b/modules/queue/queue.go @@ -1,27 +1,62 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Package queue implements a specialized queue system for Gitea. +// Package queue implements a specialized concurrent queue system for Gitea. // -// There are two major kinds of concepts: +// Terminology: // -// * The "base queue": channel, level, redis: -// - They have the same abstraction, the same interface, and they are tested by the same testing code. -// - The dummy(immediate) queue is special, it's not a real queue, it's only used as a no-op queue or a testing queue. +// 1. Item: +// - An item can be a simple value, such as an integer, or a more complex structure that has multiple fields. +// Usually a item serves as a task or a message. Sets of items will be sent to a queue handler to be processed. +// - It's represented as a JSON-marshaled binary slice in the queue // -// * The WorkerPoolQueue: it uses the "base queue" to provide "worker pool" function. -// - It calls the "handler" to process the data in the base queue. -// - Its "Push" function doesn't block forever, -// it will return an error if the queue is full after the timeout. +// 2. Batch: +// - A collection of items that are grouped together for processing. Each worker receives a batch of items. +// +// 3. Worker: +// - Individual unit of execution designed to process items from the queue. It's a goroutine that calls the Handler. +// - Workers will get new items through a channel (WorkerPoolQueue is responsible for the distribution). +// - Workers operate in parallel. The default value of max workers is determined by the setting system. +// +// 4. Handler (represented by HandlerFuncT type): +// - It's the function responsible for processing items. Each active worker will call it. +// - If an item or some items are not psuccessfully rocessed, the handler could return them as "unhandled items". +// In such scenarios, the queue system ensures these unhandled items are returned to the base queue after a brief delay. +// This mechanism is particularly beneficial in cases where the processing entity (like a document indexer) is +// temporarily unavailable. It ensures that no item is skipped or lost due to transient failures in the processing +// mechanism. +// +// 5. Base queue: +// - Represents the underlying storage mechanism for the queue. There are several implementations: +// - Channel: Uses Go's native channel constructs to manage the queue, suitable for in-memory queuing. +// - LevelDB: Especially useful in persistent queues for single instances. +// - Redis: Suitable for clusters, where we may have multiple nodes. +// - Dummy: This is special, it's not a real queue, it's a immediate no-op queue, which is useful for tests. +// - They all have the same abstraction, the same interface, and they are tested by the same testing code. +// +// 6. WorkerPoolQueue: +// - It's responsible to glue all together, using the "base queue" to provide "worker pool" functionality. It creates +// new workers if needed and can flush the queue, running all the items synchronously till it finishes. +// - Its "Push" function doesn't block forever, it will return an error if the queue is full after the timeout. +// +// 7. Manager: +// - The purpose of it is to serve as a centralized manager for multiple WorkerPoolQueue instances. Whenever we want +// to create a new queue, flush, or get a specific queue, we could use it. // // A queue can be "simple" or "unique". A unique queue will try to avoid duplicate items. // Unique queue's "Has" function can be used to check whether an item is already in the queue, -// although it's not 100% reliable due to there is no proper transaction support. +// although it's not 100% reliable due to the lack of proper transaction support. // Simple queue's "Has" function always returns "has=false". // -// The HandlerFuncT function is called by the WorkerPoolQueue to process the data in the base queue. -// If the handler returns "unhandled" items, they will be re-queued to the base queue after a slight delay, -// in case the item processor (eg: document indexer) is not available. +// A WorkerPoolQueue is a generic struct; this means it will work with any type but just for that type. +// If you want another kind of items to run, you would have to call the manager to create a new WorkerPoolQueue for you +// with a different handler that works with this new type of item. As an example of this: +// +// func Init() error { +// itemQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "queue-name", handler) +// ... +// } +// func handler(items ...*mypkg.QueueItem) []*mypkg.QueueItem { ... } package queue import "code.gitea.io/gitea/modules/util" diff --git a/modules/repository/commits.go b/modules/repository/commits.go index 96844d5b1d..ede60429a1 100644 --- a/modules/repository/commits.go +++ b/modules/repository/commits.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models/avatars" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -34,42 +35,36 @@ type PushCommits struct { HeadCommit *PushCommit CompareURL string Len int - - avatars map[string]string - emailUsers map[string]*user_model.User } // NewPushCommits creates a new PushCommits object. func NewPushCommits() *PushCommits { - return &PushCommits{ - avatars: make(map[string]string), - emailUsers: make(map[string]*user_model.User), - } + return &PushCommits{} } // toAPIPayloadCommit converts a single PushCommit to an api.PayloadCommit object. -func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) { +func (pc *PushCommits) toAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.User, repoPath, repoLink string, commit *PushCommit) (*api.PayloadCommit, error) { var err error authorUsername := "" - author, ok := pc.emailUsers[commit.AuthorEmail] + author, ok := emailUsers[commit.AuthorEmail] if !ok { author, err = user_model.GetUserByEmail(ctx, commit.AuthorEmail) if err == nil { authorUsername = author.Name - pc.emailUsers[commit.AuthorEmail] = author + emailUsers[commit.AuthorEmail] = author } } else { authorUsername = author.Name } committerUsername := "" - committer, ok := pc.emailUsers[commit.CommitterEmail] + committer, ok := emailUsers[commit.CommitterEmail] if !ok { committer, err = user_model.GetUserByEmail(ctx, commit.CommitterEmail) if err == nil { // TODO: check errors other than email not found. committerUsername = committer.Name - pc.emailUsers[commit.CommitterEmail] = committer + emailUsers[commit.CommitterEmail] = committer } } else { committerUsername = committer.Name @@ -107,11 +102,10 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi commits := make([]*api.PayloadCommit, len(pc.Commits)) var headCommit *api.PayloadCommit - if pc.emailUsers == nil { - pc.emailUsers = make(map[string]*user_model.User) - } + emailUsers := make(map[string]*user_model.User) + for i, commit := range pc.Commits { - apiCommit, err := pc.toAPIPayloadCommit(ctx, repoPath, repoLink, commit) + apiCommit, err := pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, commit) if err != nil { return nil, nil, err } @@ -123,7 +117,7 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi } if pc.HeadCommit != nil && headCommit == nil { var err error - headCommit, err = pc.toAPIPayloadCommit(ctx, repoPath, repoLink, pc.HeadCommit) + headCommit, err = pc.toAPIPayloadCommit(ctx, emailUsers, repoPath, repoLink, pc.HeadCommit) if err != nil { return nil, nil, err } @@ -134,35 +128,21 @@ func (pc *PushCommits) ToAPIPayloadCommits(ctx context.Context, repoPath, repoLi // AvatarLink tries to match user in database with e-mail // in order to show custom avatar, and falls back to general avatar link. func (pc *PushCommits) AvatarLink(ctx context.Context, email string) string { - if pc.avatars == nil { - pc.avatars = make(map[string]string) - } - avatar, ok := pc.avatars[email] - if ok { - return avatar - } - size := avatars.DefaultAvatarPixelSize * setting.Avatar.RenderedSizeFactor - u, ok := pc.emailUsers[email] - if !ok { - var err error - u, err = user_model.GetUserByEmail(ctx, email) + v, _ := cache.GetWithContextCache(ctx, "push_commits", email, func() (string, error) { + u, err := user_model.GetUserByEmail(ctx, email) if err != nil { - pc.avatars[email] = avatars.GenerateEmailAvatarFastLink(ctx, email, size) if !user_model.IsErrUserNotExist(err) { log.Error("GetUserByEmail: %v", err) - return "" + return "", err } - } else { - pc.emailUsers[email] = u + return avatars.GenerateEmailAvatarFastLink(ctx, email, size), nil } - } - if u != nil { - pc.avatars[email] = u.AvatarLinkWithSize(ctx, size) - } + return u.AvatarLinkWithSize(ctx, size), nil + }) - return pc.avatars[email] + return v } // CommitToPushCommit transforms a git.Commit to PushCommit type. @@ -189,7 +169,5 @@ func GitToPushCommits(gitCommits []*git.Commit) *PushCommits { HeadCommit: nil, CompareURL: "", Len: len(commits), - avatars: make(map[string]string), - emailUsers: make(map[string]*user_model.User), } } diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index b6ad967d4c..0931092597 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -103,11 +103,9 @@ func TestPushCommits_ToAPIPayloadCommits(t *testing.T) { assert.EqualValues(t, []string{"readme.md"}, headCommit.Modified) } -func enableGravatar(t *testing.T) { - err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "false") - assert.NoError(t, err) +func initGravatarSource(t *testing.T) { setting.GravatarSource = "https://secure.gravatar.com/avatar" - err = system_model.Init(db.DefaultContext) + err := system_model.Init(db.DefaultContext) assert.NoError(t, err) } @@ -134,7 +132,7 @@ func TestPushCommits_AvatarLink(t *testing.T) { }, } - enableGravatar(t) + initGravatarSource(t) assert.Equal(t, "https://secure.gravatar.com/avatar/ab53a2911ddf9b4817ac01ddcd3d975f?d=identicon&s="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), diff --git a/modules/repository/create.go b/modules/repository/create.go index 10a1e872df..2dac35224e 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/models/webhook" - "code.gitea.io/gitea/modules/git" issue_indexer "code.gitea.io/gitea/modules/indexer/issues" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -156,142 +155,6 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re return nil } -// CreateRepoOptions contains the create repository options -type CreateRepoOptions struct { - Name string - Description string - OriginalURL string - GitServiceType api.GitServiceType - Gitignores string - IssueLabels string - License string - Readme string - DefaultBranch string - IsPrivate bool - IsMirror bool - IsTemplate bool - AutoInit bool - Status repo_model.RepositoryStatus - TrustModel repo_model.TrustModelType - MirrorInterval string -} - -// CreateRepository creates a repository for the user/organization. -func CreateRepository(doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { - if !doer.IsAdmin && !u.CanCreateRepo() { - return nil, repo_model.ErrReachLimitOfRepo{ - Limit: u.MaxRepoCreation, - } - } - - if len(opts.DefaultBranch) == 0 { - opts.DefaultBranch = setting.Repository.DefaultBranch - } - - // Check if label template exist - if len(opts.IssueLabels) > 0 { - if _, err := LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil { - return nil, err - } - } - - repo := &repo_model.Repository{ - OwnerID: u.ID, - Owner: u, - OwnerName: u.Name, - Name: opts.Name, - LowerName: strings.ToLower(opts.Name), - Description: opts.Description, - OriginalURL: opts.OriginalURL, - OriginalServiceType: opts.GitServiceType, - IsPrivate: opts.IsPrivate, - IsFsckEnabled: !opts.IsMirror, - IsTemplate: opts.IsTemplate, - CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, - Status: opts.Status, - IsEmpty: !opts.AutoInit, - TrustModel: opts.TrustModel, - IsMirror: opts.IsMirror, - DefaultBranch: opts.DefaultBranch, - } - - var rollbackRepo *repo_model.Repository - - if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { - if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { - return err - } - - // No need for init mirror. - if opts.IsMirror { - return nil - } - - repoPath := repo_model.RepoPath(u.Name, repo.Name) - isExist, err := util.IsExist(repoPath) - if err != nil { - log.Error("Unable to check if %s exists. Error: %v", repoPath, err) - return err - } - if isExist { - // repo already exists - We have two or three options. - // 1. We fail stating that the directory exists - // 2. We create the db repository to go with this data and adopt the git repo - // 3. We delete it and start afresh - // - // Previously Gitea would just delete and start afresh - this was naughty. - // So we will now fail and delegate to other functionality to adopt or delete - log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) - return repo_model.ErrRepoFilesAlreadyExist{ - Uname: u.Name, - Name: repo.Name, - } - } - - if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { - if err2 := util.RemoveAll(repoPath); err2 != nil { - log.Error("initRepository: %v", err) - return fmt.Errorf( - "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) - } - return fmt.Errorf("initRepository: %w", err) - } - - // Initialize Issue Labels if selected - if len(opts.IssueLabels) > 0 { - if err = InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { - rollbackRepo = repo - rollbackRepo.OwnerID = u.ID - return fmt.Errorf("InitializeLabels: %w", err) - } - } - - if err := CheckDaemonExportOK(ctx, repo); err != nil { - return fmt.Errorf("checkDaemonExportOK: %w", err) - } - - if stdout, _, err := git.NewCommand(ctx, "update-server-info"). - SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). - RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { - log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) - rollbackRepo = repo - rollbackRepo.OwnerID = u.ID - return fmt.Errorf("CreateRepository(git update-server-info): %w", err) - } - return nil - }); err != nil { - if rollbackRepo != nil { - if errDelete := models.DeleteRepository(doer, rollbackRepo.OwnerID, rollbackRepo.ID); errDelete != nil { - log.Error("Rollback deleteRepository: %v", errDelete) - } - } - - return nil, err - } - - return repo, nil -} - const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular // getDirectorySize returns the disk consumption for a given path diff --git a/modules/repository/create_test.go b/modules/repository/create_test.go index e620422bcb..6a2f4deaff 100644 --- a/modules/repository/create_test.go +++ b/modules/repository/create_test.go @@ -4,151 +4,16 @@ package repository import ( - "fmt" "testing" - "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" 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/structs" "github.com/stretchr/testify/assert" ) -func TestIncludesAllRepositoriesTeams(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - testTeamRepositories := func(teamID int64, repoIds []int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name) - assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) - assert.Len(t, team.Repos, len(repoIds), "%s: repo count", team.Name) - for i, rid := range repoIds { - if rid > 0 { - assert.True(t, models.HasRepository(team, rid), "%s: HasRepository(%d) %d", rid, i) - } - } - } - - // Get an admin user. - user, err := user_model.GetUserByID(db.DefaultContext, 1) - assert.NoError(t, err, "GetUserByID") - - // Create org. - org := &organization.Organization{ - Name: "All_repo", - IsActive: true, - Type: user_model.UserTypeOrganization, - Visibility: structs.VisibleTypePublic, - } - assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") - - // Check Owner team. - ownerTeam, err := org.GetOwnerTeam(db.DefaultContext) - assert.NoError(t, err, "GetOwnerTeam") - assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") - - // Create repos. - repoIds := make([]int64, 0) - for i := 0; i < 3; i++ { - r, err := CreateRepository(user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}) - assert.NoError(t, err, "CreateRepository %d", i) - if r != nil { - repoIds = append(repoIds, r.ID) - } - } - // Get fresh copy of Owner team after creating repos. - ownerTeam, err = org.GetOwnerTeam(db.DefaultContext) - assert.NoError(t, err, "GetOwnerTeam") - - // Create teams and check repositories. - teams := []*organization.Team{ - ownerTeam, - { - OrgID: org.ID, - Name: "team one", - AccessMode: perm.AccessModeRead, - IncludesAllRepositories: true, - }, - { - OrgID: org.ID, - Name: "team 2", - AccessMode: perm.AccessModeRead, - IncludesAllRepositories: false, - }, - { - OrgID: org.ID, - Name: "team three", - AccessMode: perm.AccessModeWrite, - IncludesAllRepositories: true, - }, - { - OrgID: org.ID, - Name: "team 4", - AccessMode: perm.AccessModeWrite, - IncludesAllRepositories: false, - }, - } - teamRepos := [][]int64{ - repoIds, - repoIds, - {}, - repoIds, - {}, - } - for i, team := range teams { - if i > 0 { // first team is Owner. - assert.NoError(t, models.NewTeam(team), "%s: NewTeam", team.Name) - } - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Update teams and check repositories. - teams[3].IncludesAllRepositories = false - teams[4].IncludesAllRepositories = true - teamRepos[4] = repoIds - for i, team := range teams { - assert.NoError(t, models.UpdateTeam(team, false, true), "%s: UpdateTeam", team.Name) - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Create repo and check teams repositories. - r, err := CreateRepository(user, org.AsUser(), CreateRepoOptions{Name: "repo-last"}) - assert.NoError(t, err, "CreateRepository last") - if r != nil { - repoIds = append(repoIds, r.ID) - } - teamRepos[0] = repoIds - teamRepos[1] = repoIds - teamRepos[4] = repoIds - for i, team := range teams { - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Remove repo and check teams repositories. - assert.NoError(t, models.DeleteRepository(user, org.ID, repoIds[0]), "DeleteRepository") - teamRepos[0] = repoIds[1:] - teamRepos[1] = repoIds[1:] - teamRepos[3] = repoIds[1:3] - teamRepos[4] = repoIds[1:] - for i, team := range teams { - testTeamRepositories(team.ID, teamRepos[i]) - } - - // Wipe created items. - for i, rid := range repoIds { - if i > 0 { // first repo already deleted. - assert.NoError(t, models.DeleteRepository(user, org.ID, rid), "DeleteRepository %d", i) - } - } - assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") -} - func TestUpdateRepositoryVisibilityChanged(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) diff --git a/modules/repository/generate.go b/modules/repository/generate.go index 2e0b7600a5..4055029d22 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -241,7 +241,7 @@ func generateRepoCommit(ctx context.Context, repo, templateRepo, generateRepo *r defaultBranch = templateRepo.DefaultBranch } - return initRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch) + return InitRepoCommit(ctx, tmpDir, repo, repo.Owner, defaultBranch) } func generateGitContent(ctx context.Context, repo, templateRepo, generateRepo *repo_model.Repository) (err error) { @@ -356,7 +356,7 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ } } - if err = checkInitRepository(ctx, owner.Name, generateRepo.Name); err != nil { + if err = CheckInitRepository(ctx, owner.Name, generateRepo.Name); err != nil { return generateRepo, err } diff --git a/modules/repository/hooks.go b/modules/repository/hooks.go index a95b9c2e99..daab7c3091 100644 --- a/modules/repository/hooks.go +++ b/modules/repository/hooks.go @@ -108,12 +108,7 @@ done } // CreateDelegateHooks creates all the hooks scripts for the repo -func CreateDelegateHooks(repoPath string) error { - return createDelegateHooks(repoPath) -} - -// createDelegateHooks creates all the hooks scripts for the repo -func createDelegateHooks(repoPath string) (err error) { +func CreateDelegateHooks(repoPath string) (err error) { hookNames, hookTpls, giteaHookTpls := getHookTemplates() hookDir := filepath.Join(repoPath, "hooks") diff --git a/modules/repository/init.go b/modules/repository/init.go index 84648f45eb..6f791f742b 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -4,7 +4,6 @@ package repository import ( - "bytes" "context" "fmt" "os" @@ -21,7 +20,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/templates/vars" "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" ) @@ -126,95 +124,8 @@ func LoadRepoConfig() error { return nil } -func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { - commitTimeStr := time.Now().Format(time.RFC3339) - authorSig := repo.Owner.NewGitSig() - - // Because this may call hooks we should pass in the environment - env := append(os.Environ(), - "GIT_AUTHOR_NAME="+authorSig.Name, - "GIT_AUTHOR_EMAIL="+authorSig.Email, - "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+authorSig.Name, - "GIT_COMMITTER_EMAIL="+authorSig.Email, - "GIT_COMMITTER_DATE="+commitTimeStr, - ) - - // Clone to temporary path and do the init commit. - if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). - SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). - RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { - log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) - return fmt.Errorf("git clone: %w", err) - } - - // README - data, err := options.Readme(opts.Readme) - if err != nil { - return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) - } - - cloneLink := repo.CloneLink() - match := map[string]string{ - "Name": repo.Name, - "Description": repo.Description, - "CloneURL.SSH": cloneLink.SSH, - "CloneURL.HTTPS": cloneLink.HTTPS, - "OwnerName": repo.OwnerName, - } - res, err := vars.Expand(string(data), match) - if err != nil { - // here we could just log the error and continue the rendering - log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err) - } - if err = os.WriteFile(filepath.Join(tmpDir, "README.md"), - []byte(res), 0o644); err != nil { - return fmt.Errorf("write README.md: %w", err) - } - - // .gitignore - if len(opts.Gitignores) > 0 { - var buf bytes.Buffer - names := strings.Split(opts.Gitignores, ",") - for _, name := range names { - data, err = options.Gitignore(name) - if err != nil { - return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) - } - buf.WriteString("# ---> " + name + "\n") - buf.Write(data) - buf.WriteString("\n") - } - - if buf.Len() > 0 { - if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil { - return fmt.Errorf("write .gitignore: %w", err) - } - } - } - - // LICENSE - if len(opts.License) > 0 { - data, err = getLicense(opts.License, &licenseValues{ - Owner: repo.OwnerName, - Email: authorSig.Email, - Repo: repo.Name, - Year: time.Now().Format("2006"), - }) - if err != nil { - return fmt.Errorf("getLicense[%s]: %w", opts.License, err) - } - - if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil { - return fmt.Errorf("write LICENSE: %w", err) - } - } - - return nil -} - -// initRepoCommit temporarily changes with work directory. -func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) { +// InitRepoCommit temporarily changes with work directory. +func InitRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Repository, u *user_model.User, defaultBranch string) (err error) { commitTimeStr := time.Now().Format(time.RFC3339) sig := u.NewGitSig() @@ -277,7 +188,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi return nil } -func checkInitRepository(ctx context.Context, owner, name string) (err error) { +func CheckInitRepository(ctx context.Context, owner, name string) (err error) { // Somehow the directory could exist. repoPath := repo_model.RepoPath(owner, name) isExist, err := util.IsExist(repoPath) @@ -295,77 +206,12 @@ func checkInitRepository(ctx context.Context, owner, name string) (err error) { // Init git bare new repository. if err = git.InitRepository(ctx, repoPath, true); err != nil { return fmt.Errorf("git.InitRepository: %w", err) - } else if err = createDelegateHooks(repoPath); err != nil { + } else if err = CreateDelegateHooks(repoPath); err != nil { return fmt.Errorf("createDelegateHooks: %w", err) } return nil } -// InitRepository initializes README and .gitignore if needed. -func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) { - if err = checkInitRepository(ctx, repo.OwnerName, repo.Name); err != nil { - return err - } - - // Initialize repository according to user's choice. - if opts.AutoInit { - tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name) - if err != nil { - return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err) - } - defer func() { - if err := util.RemoveAll(tmpDir); err != nil { - log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err) - } - }() - - if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil { - return fmt.Errorf("prepareRepoCommit: %w", err) - } - - // Apply changes and commit. - if err = initRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil { - return fmt.Errorf("initRepoCommit: %w", err) - } - } - - // Re-fetch the repository from database before updating it (else it would - // override changes that were done earlier with sql) - if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { - return fmt.Errorf("getRepositoryByID: %w", err) - } - - if !opts.AutoInit { - repo.IsEmpty = true - } - - repo.DefaultBranch = setting.Repository.DefaultBranch - - if len(opts.DefaultBranch) > 0 { - repo.DefaultBranch = opts.DefaultBranch - gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) - if err != nil { - return fmt.Errorf("openRepository: %w", err) - } - defer gitRepo.Close() - if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { - return fmt.Errorf("setDefaultBranch: %w", err) - } - - if !repo.IsEmpty { - if _, err := SyncRepoBranches(ctx, repo.ID, u.ID); err != nil { - return fmt.Errorf("SyncRepoBranches: %w", err) - } - } - } - - if err = UpdateRepository(ctx, repo, false); err != nil { - return fmt.Errorf("updateRepository: %w", err) - } - - return nil -} - // InitializeLabels adds a label set to a repository using a template func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error { list, err := LoadTemplateLabelsByDisplayName(labelTemplate) diff --git a/modules/repository/license.go b/modules/repository/license.go index 5b188a041e..6ac3547e7b 100644 --- a/modules/repository/license.go +++ b/modules/repository/license.go @@ -13,14 +13,14 @@ import ( "code.gitea.io/gitea/modules/options" ) -type licenseValues struct { +type LicenseValues struct { Owner string Email string Repo string Year string } -func getLicense(name string, values *licenseValues) ([]byte, error) { +func GetLicense(name string, values *LicenseValues) ([]byte, error) { data, err := options.License(name) if err != nil { return nil, fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) @@ -28,7 +28,7 @@ func getLicense(name string, values *licenseValues) ([]byte, error) { return fillLicensePlaceholder(name, values, data), nil } -func fillLicensePlaceholder(name string, values *licenseValues, origin []byte) []byte { +func fillLicensePlaceholder(name string, values *LicenseValues, origin []byte) []byte { placeholder := getLicensePlaceholder(name) scanner := bufio.NewScanner(bytes.NewReader(origin)) diff --git a/modules/repository/license_test.go b/modules/repository/license_test.go index 13c865693c..3b0cfa1eed 100644 --- a/modules/repository/license_test.go +++ b/modules/repository/license_test.go @@ -13,7 +13,7 @@ import ( func Test_getLicense(t *testing.T) { type args struct { name string - values *licenseValues + values *LicenseValues } tests := []struct { name string @@ -25,7 +25,7 @@ func Test_getLicense(t *testing.T) { name: "regular", args: args{ name: "MIT", - values: &licenseValues{Owner: "Gitea", Year: "2023"}, + values: &LicenseValues{Owner: "Gitea", Year: "2023"}, }, want: `MIT License @@ -49,11 +49,11 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getLicense(tt.args.name, tt.args.values) - if !tt.wantErr(t, err, fmt.Sprintf("getLicense(%v, %v)", tt.args.name, tt.args.values)) { + got, err := GetLicense(tt.args.name, tt.args.values) + if !tt.wantErr(t, err, fmt.Sprintf("GetLicense(%v, %v)", tt.args.name, tt.args.values)) { return } - assert.Equalf(t, tt.want, string(got), "getLicense(%v, %v)", tt.args.name, tt.args.values) + assert.Equalf(t, tt.want, string(got), "GetLicense(%v, %v)", tt.args.name, tt.args.values) }) } } @@ -61,7 +61,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI func Test_fillLicensePlaceholder(t *testing.T) { type args struct { name string - values *licenseValues + values *LicenseValues origin string } tests := []struct { @@ -73,7 +73,7 @@ func Test_fillLicensePlaceholder(t *testing.T) { name: "owner", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` @@ -104,7 +104,7 @@ Gitea name: "email", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` [EMAIL] `, @@ -117,7 +117,7 @@ teabot@gitea.io name: "repo", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` @@ -132,7 +132,7 @@ gitea name: "year", args: args{ name: "regular", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` [YEAR] @@ -155,7 +155,7 @@ gitea name: "0BSD", args: args{ name: "0BSD", - values: &licenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, + values: &LicenseValues{Year: "2023", Owner: "Gitea", Email: "teabot@gitea.io", Repo: "gitea"}, origin: ` Copyright (C) YEAR by AUTHOR EMAIL diff --git a/modules/repository/main_test.go b/modules/repository/main_test.go index 007790f2a9..abaae69866 100644 --- a/modules/repository/main_test.go +++ b/modules/repository/main_test.go @@ -8,6 +8,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/actions" ) func TestMain(m *testing.M) { diff --git a/modules/repository/repo.go b/modules/repository/repo.go index 6a11315cc4..6bf88e7752 100644 --- a/modules/repository/repo.go +++ b/modules/repository/repo.go @@ -256,11 +256,11 @@ func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { // CleanUpMigrateInfo finishes migrating repository and/or wiki with things that don't need to be done for mirrors. func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo_model.Repository, error) { repoPath := repo.RepoPath() - if err := createDelegateHooks(repoPath); err != nil { + if err := CreateDelegateHooks(repoPath); err != nil { return repo, fmt.Errorf("createDelegateHooks: %w", err) } if repo.HasWiki() { - if err := createDelegateHooks(repo.WikiPath()); err != nil { + if err := CreateDelegateHooks(repo.WikiPath()); err != nil { return repo, fmt.Errorf("createDelegateHooks.(wiki): %w", err) } } diff --git a/modules/setting/actions.go b/modules/setting/actions.go index a13330dcd1..bfc502c0cb 100644 --- a/modules/setting/actions.go +++ b/modules/setting/actions.go @@ -13,10 +13,11 @@ import ( // Actions settings var ( Actions = struct { - LogStorage *Storage // how the created logs should be stored - ArtifactStorage *Storage // how the created artifacts should be stored - Enabled bool - DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` + LogStorage *Storage // how the created logs should be stored + ArtifactStorage *Storage // how the created artifacts should be stored + ArtifactRetentionDays int64 `ini:"ARTIFACT_RETENTION_DAYS"` + Enabled bool + DefaultActionsURL defaultActionsURL `ini:"DEFAULT_ACTIONS_URL"` }{ Enabled: false, DefaultActionsURL: defaultActionsURLGitHub, @@ -76,5 +77,10 @@ func loadActionsFrom(rootCfg ConfigProvider) error { Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec) + // default to 90 days in Github Actions + if Actions.ArtifactRetentionDays <= 0 { + Actions.ArtifactRetentionDays = 90 + } + return err } diff --git a/modules/setting/config_provider.go b/modules/setting/config_provider.go index d00164df9e..8d64286288 100644 --- a/modules/setting/config_provider.go +++ b/modules/setting/config_provider.go @@ -174,9 +174,16 @@ func (s *iniConfigSection) ChildSections() (sections []ConfigSection) { return sections } +func configProviderLoadOptions() ini.LoadOptions { + return ini.LoadOptions{ + KeyValueDelimiterOnWrite: " = ", + IgnoreContinuation: true, + } +} + // NewConfigProviderFromData this function is mainly for testing purpose func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { - cfg, err := ini.Load(strings.NewReader(configContent)) + cfg, err := ini.LoadSources(configProviderLoadOptions(), strings.NewReader(configContent)) if err != nil { return nil, err } @@ -190,7 +197,7 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) { // NewConfigProviderFromFile load configuration from file. // NOTE: do not print any log except error. func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) { - cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "}) + cfg := ini.Empty(configProviderLoadOptions()) loadedFromEmpty := true if file != "" { @@ -339,6 +346,7 @@ func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, erro iniFile, err := ini.LoadSources(ini.LoadOptions{ IgnoreInlineComment: true, UnescapeValueCommentSymbols: true, + IgnoreContinuation: true, }, source, others...) if err != nil { return nil, fmt.Errorf("unable to load locale ini: %w", err) diff --git a/modules/setting/config_provider_test.go b/modules/setting/config_provider_test.go index 7e7c6be2bb..a666d124c7 100644 --- a/modules/setting/config_provider_test.go +++ b/modules/setting/config_provider_test.go @@ -30,6 +30,16 @@ key = 123 secSub := cfg.Section("foo.bar.xxx") assert.Equal(t, "123", secSub.Key("key").String()) }) + t.Run("TrailingSlash", func(t *testing.T) { + cfg, _ := NewConfigProviderFromData(` +[foo] +key = E:\ +xxx = yyy +`) + sec := cfg.Section("foo") + assert.Equal(t, "E:\\", sec.Key("key").String()) + assert.Equal(t, "yyy", sec.Key("xxx").String()) + }) } func TestConfigProviderHelper(t *testing.T) { diff --git a/modules/setting/service.go b/modules/setting/service.go index 74a7e90f7c..3ea1501236 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -46,6 +46,7 @@ var Service = struct { EnableNotifyMail bool EnableBasicAuth bool EnableReverseProxyAuth bool + EnableReverseProxyAuthAPI bool EnableReverseProxyAutoRegister bool EnableReverseProxyEmail bool EnableReverseProxyFullName bool @@ -157,6 +158,7 @@ func loadServiceFrom(rootCfg ConfigProvider) { Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool() + Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() Service.EnableReverseProxyEmail = sec.Key("ENABLE_REVERSE_PROXY_EMAIL").MustBool() Service.EnableReverseProxyFullName = sec.Key("ENABLE_REVERSE_PROXY_FULL_NAME").MustBool() diff --git a/modules/setting/session.go b/modules/setting/session.go index d0bc938973..664c66f869 100644 --- a/modules/setting/session.go +++ b/modules/setting/session.go @@ -50,7 +50,7 @@ func loadSessionFrom(rootCfg ConfigProvider) { } SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea") SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash - SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(false) + SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://")) SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400) SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400) SessionConfig.Domain = sec.Key("DOMAIN").String() diff --git a/modules/templates/base.go b/modules/templates/base.go index ef28cc03f4..2c2f35bbed 100644 --- a/modules/templates/base.go +++ b/modules/templates/base.go @@ -4,11 +4,11 @@ package templates import ( + "slices" "strings" "code.gitea.io/gitea/modules/assetfs" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) func AssetFS() *assetfs.LayeredFS { @@ -24,7 +24,7 @@ func ListWebTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) { if err != nil { return nil, err } - return util.SliceRemoveAllFunc(files, func(file string) bool { + return slices.DeleteFunc(files, func(file string) bool { return strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl") }), nil } @@ -34,7 +34,7 @@ func ListMailTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) { if err != nil { return nil, err } - return util.SliceRemoveAllFunc(files, func(file string) bool { + return slices.DeleteFunc(files, func(file string) bool { return !strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl") }), nil } diff --git a/modules/util/slice.go b/modules/util/slice.go index 74356f5496..6d63ab4a77 100644 --- a/modules/util/slice.go +++ b/modules/util/slice.go @@ -1,37 +1,21 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -// Most of the functions in this file can have better implementations with "golang.org/x/exp/slices". -// However, "golang.org/x/exp" is experimental and unreliable, we shouldn't use it. -// So lets waiting for the "slices" has be promoted to the main repository one day. - package util -import "strings" - -// SliceContains returns true if the target exists in the slice. -func SliceContains[T comparable](slice []T, target T) bool { - return SliceContainsFunc(slice, func(t T) bool { return t == target }) -} - -// SliceContainsFunc returns true if any element in the slice satisfies the targetFunc. -func SliceContainsFunc[T any](slice []T, targetFunc func(T) bool) bool { - for _, v := range slice { - if targetFunc(v) { - return true - } - } - return false -} +import ( + "slices" + "strings" +) // SliceContainsString sequential searches if string exists in slice. func SliceContainsString(slice []string, target string, insensitive ...bool) bool { if len(insensitive) != 0 && insensitive[0] { target = strings.ToLower(target) - return SliceContainsFunc(slice, func(t string) bool { return strings.ToLower(t) == target }) + return slices.ContainsFunc(slice, func(t string) bool { return strings.ToLower(t) == target }) } - return SliceContains(slice, target) + return slices.Contains(slice, target) } // SliceSortedEqual returns true if the two slices will be equal when they get sorted. @@ -57,34 +41,7 @@ func SliceSortedEqual[T comparable](s1, s2 []T) bool { return true } -// SliceEqual returns true if the two slices are equal. -func SliceEqual[T comparable](s1, s2 []T) bool { - if len(s1) != len(s2) { - return false - } - - for i, v := range s1 { - if s2[i] != v { - return false - } - } - return true -} - // SliceRemoveAll removes all the target elements from the slice. func SliceRemoveAll[T comparable](slice []T, target T) []T { - return SliceRemoveAllFunc(slice, func(t T) bool { return t == target }) -} - -// SliceRemoveAllFunc removes all elements which satisfy the targetFunc from the slice. -func SliceRemoveAllFunc[T comparable](slice []T, targetFunc func(T) bool) []T { - idx := 0 - for _, v := range slice { - if targetFunc(v) { - continue - } - slice[idx] = v - idx++ - } - return slice[:idx] + return slices.DeleteFunc(slice, func(t T) bool { return t == target }) } diff --git a/modules/util/slice_test.go b/modules/util/slice_test.go index 373c1a3b7b..a910f5edfe 100644 --- a/modules/util/slice_test.go +++ b/modules/util/slice_test.go @@ -9,20 +9,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSliceContains(t *testing.T) { - assert.True(t, SliceContains([]int{2, 0, 2, 3}, 2)) - assert.True(t, SliceContains([]int{2, 0, 2, 3}, 0)) - assert.True(t, SliceContains([]int{2, 0, 2, 3}, 3)) - - assert.True(t, SliceContains([]string{"2", "0", "2", "3"}, "0")) - assert.True(t, SliceContains([]float64{2, 0, 2, 3}, 0)) - assert.True(t, SliceContains([]bool{false, true, false}, true)) - - assert.False(t, SliceContains([]int{2, 0, 2, 3}, 4)) - assert.False(t, SliceContains([]int{}, 4)) - assert.False(t, SliceContains(nil, 4)) -} - func TestSliceContainsString(t *testing.T) { assert.True(t, SliceContainsString([]string{"c", "b", "a", "b"}, "a")) assert.True(t, SliceContainsString([]string{"c", "b", "a", "b"}, "b")) @@ -54,25 +40,6 @@ func TestSliceSortedEqual(t *testing.T) { assert.False(t, SliceSortedEqual([]int{2, 0, 0, 3}, []int{2, 0, 2, 3})) } -func TestSliceEqual(t *testing.T) { - assert.True(t, SliceEqual([]int{2, 0, 2, 3}, []int{2, 0, 2, 3})) - assert.True(t, SliceEqual([]int{}, []int{})) - assert.True(t, SliceEqual([]int(nil), nil)) - assert.True(t, SliceEqual([]int(nil), []int{})) - assert.True(t, SliceEqual([]int{}, []int{})) - - assert.True(t, SliceEqual([]string{"2", "0", "2", "3"}, []string{"2", "0", "2", "3"})) - assert.True(t, SliceEqual([]float64{2, 0, 2, 3}, []float64{2, 0, 2, 3})) - assert.True(t, SliceEqual([]bool{false, true, false}, []bool{false, true, false})) - - assert.False(t, SliceEqual([]int{3, 0, 2, 2}, []int{2, 0, 2, 3})) - assert.False(t, SliceEqual([]int{2, 0, 2}, []int{2, 0, 2, 3})) - assert.False(t, SliceEqual([]int{}, []int{2, 0, 2, 3})) - assert.False(t, SliceEqual(nil, []int{2, 0, 2, 3})) - assert.False(t, SliceEqual([]int{2, 0, 2, 4}, []int{2, 0, 2, 3})) - assert.False(t, SliceEqual([]int{2, 0, 0, 3}, []int{2, 0, 2, 3})) -} - func TestSliceRemoveAll(t *testing.T) { assert.ElementsMatch(t, []int{2, 2, 3}, SliceRemoveAll([]int{2, 0, 2, 3}, 0)) assert.ElementsMatch(t, []int{0, 3}, SliceRemoveAll([]int{2, 0, 2, 3}, 2)) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 9f7f5a24b4..d9773c0f5e 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -4,6 +4,7 @@ explore=Erkunden help=Hilfe logo=Logo sign_in=Anmelden +sign_in_with_provider=Anmelden mit %s sign_in_or=oder sign_out=Abmelden sign_up=Registrieren @@ -79,6 +80,7 @@ milestones=Meilensteine ok=OK cancel=Abbrechen +retry=Erneut versuchen rerun=Neu starten rerun_all=Alle Jobs neu starten save=Speichern @@ -91,6 +93,7 @@ edit=Bearbeiten enabled=Aktiviert disabled=Deaktiviert +locked=Gesperrt copy=Kopieren copy_url=URL kopieren @@ -128,7 +131,9 @@ concept_user_organization=Organisation show_timestamps=Zeitstempel anzeigen show_log_seconds=Sekunden anzeigen show_full_screen=Vollbild anzeigen +download_logs=Logs herunterladen +confirm_delete_selected=Alle ausgewählten Elemente löschen? name=Name value=Wert @@ -167,6 +172,7 @@ string.desc=Z–A [error] occurred=Ein Fehler ist aufgetreten +report_message=Wenn du glaubst, dass dies ein Fehler von Gitea ist, suche bitte auf GitHub nach diesem Fehler und erstelle gegebenenfalls einen neuen Bugreport. missing_csrf=Fehlerhafte Anfrage: Kein CSRF Token verfügbar invalid_csrf=Fehlerhafte Anfrage: Ungültiger CSRF Token not_found=Das Ziel konnte nicht gefunden werden. @@ -220,6 +226,7 @@ repo_path_helper=Remote-Git-Repositories werden in diesem Verzeichnis gespeicher lfs_path=Git-LFS-Wurzelpfad lfs_path_helper=In diesem Verzeichnis werden die Dateien von Git LFS abgespeichert. Leer lassen, um LFS zu deaktivieren. run_user=Ausführen als +run_user_helper=Der Nutzer unter dem Gitea ausgeführt wird. Beachte, dass dieser Nutzer Zugriff auf das Repository-Wurzelverzeichnis haben muss. domain=Server-Domain domain_helper=Domain oder Host-Adresse für den Server. ssh_port=SSH-Server-Port @@ -291,6 +298,8 @@ invalid_password_algorithm=Ungültiger Passwort-Hash-Algorithmus password_algorithm_helper=Lege einen Passwort-Hashing-Algorithmus fest. Algorithmen haben unterschiedliche Anforderungen und Stärken. Der argon2-Algorithmus ist ziemlich sicher, aber er verbraucht viel Speicher und kann für kleine Systeme ungeeignet sein. enable_update_checker=Aktualisierungsprüfung aktivieren enable_update_checker_helper=Stellt regelmäßig eine Verbindung zu gitea.io her, um nach neuen Versionen zu prüfen. +env_config_keys=Umgebungskonfiguration +env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet: [home] uname_holder=E-Mail-Adresse oder Benutzername @@ -353,6 +362,7 @@ remember_me=Dieses Gerät speichern forgot_password_title=Passwort vergessen forgot_password=Passwort vergessen? sign_up_now=Noch kein Konto? Jetzt registrieren. +sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen! confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an %s gesendet. Bitte überprüfe dein Postfach innerhalb der nächsten %s, um die Registrierung abzuschließen. must_change_password=Aktualisiere dein Passwort allow_password_change=Verlange vom Benutzer das Passwort zu ändern (empfohlen) @@ -587,6 +597,7 @@ user_bio=Biografie disabled_public_activity=Dieser Benutzer hat die öffentliche Sichtbarkeit der Aktivität deaktiviert. email_visibility.limited=Ihre E-Mail-Adresse ist für alle authentifizierten Benutzer sichtbar email_visibility.private=Deine E-Mail-Adresse ist nur für Dich und Administratoren sichtbar +settings=Benutzereinstellungen form.name_reserved=Der Benutzername "%s" ist reserviert. form.name_pattern_not_allowed=Das Muster "%s" ist nicht in einem Benutzernamen erlaubt. @@ -662,6 +673,7 @@ update_user_avatar_success=Der Avatar des Benutzers wurde aktualisiert. change_password=Passwort aktualisieren old_password=Aktuelles Passwort new_password=Neues Passwort +retype_new_password=Neues Passwort bestätigen password_incorrect=Das aktuelle Passwort ist falsch. change_password_success=Dein Passwort wurde aktualisiert. Bitte verwende dieses beim nächsten Einloggen. password_change_disabled=Benutzer, die nicht von Gitea verwaltet werden, können ihr Passwort im Web-Interface nicht ändern. @@ -1330,6 +1342,7 @@ issues.delete_branch_at=`löschte die Branch %s %s` issues.filter_label=Label issues.filter_label_exclude=„Alt + Klick/Enter verwenden, um Label auszuschließen” issues.filter_label_no_select=Alle Label +issues.filter_label_select_no_label=Kein Label issues.filter_milestone=Meilenstein issues.filter_milestone_all=Alle Meilensteine issues.filter_milestone_none=Keine Meilensteine @@ -1383,6 +1396,7 @@ issues.next=Nächste issues.open_title=Offen issues.closed_title=Geschlossen issues.draft_title=Entwurf +issues.num_comments_1=%d Kommentar issues.num_comments=%d Kommentare issues.commented_at=`hat %s kommentiert` issues.delete_comment_confirm=Bist du sicher dass du diesen Kommentar löschen möchtest? @@ -1391,6 +1405,7 @@ issues.context.quote_reply=Antwort zitieren issues.context.reference_issue=In neuem Issue referenzieren issues.context.edit=Bearbeiten issues.context.delete=Löschen +issues.no_content=Keine Beschreibung angegeben. issues.close=Issue schließen issues.comment_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s gemerged issues.comment_manually_pull_merged_at=hat Commit %[1]s in %[2]s %[3]s manuell gemerged @@ -1557,6 +1572,8 @@ issues.review.pending.tooltip=Dieser Kommentar ist derzeit nicht für andere Ben issues.review.review=Review issues.review.reviewers=Reviewer issues.review.outdated=Veraltet +issues.review.option.show_outdated_comments=Veraltete Kommentare anzeigen +issues.review.option.hide_outdated_comments=Veraltete Kommentare ausblenden issues.review.show_outdated=Veraltete anzeigen issues.review.hide_outdated=Veraltete ausblenden issues.review.show_resolved=Gelöste anzeigen @@ -1702,6 +1719,7 @@ pulls.delete.title=Diesen Pull-Request löschen? pulls.delete.text=Willst du diesen Pull-Request wirklich löschen? (Dies wird den Inhalt unwiderruflich löschen. Überlege, ob du ihn nicht lieber schließen willst, um ihn zu archivieren) +pull.deleted_branch=(gelöscht):%s milestones.new=Neuer Meilenstein milestones.closed=Geschlossen %s diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 08aa320cc2..39da4be179 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -681,7 +681,7 @@ choose_new_avatar = Choose new avatar update_avatar = Update Avatar delete_current_avatar = Delete Current Avatar uploaded_avatar_not_a_image = The uploaded file is not an image. -uploaded_avatar_is_too_big = The uploaded file has exceeded the maximum size. +uploaded_avatar_is_too_big = The uploaded file size (%d KiB) exceeds the maximum size (%d KiB). update_avatar_success = Your avatar has been updated. update_user_avatar_success = The user's avatar has been updated. @@ -1764,6 +1764,7 @@ pulls.rebase_conflict_summary = Error Message pulls.unrelated_histories = Merge Failed: The merge head and base do not share a common history. Hint: Try a different strategy pulls.merge_out_of_date = Merge Failed: Whilst generating the merge, the base was updated. Hint: Try again. pulls.head_out_of_date = Merge Failed: Whilst generating the merge, the head was updated. Hint: Try again. +pulls.has_merged = Failed: The pull request has been merged, you cannot merge again or change the target branch. pulls.push_rejected = Merge Failed: The push was rejected. Review the Git Hooks for this repository. pulls.push_rejected_summary = Full Rejection Message pulls.push_rejected_no_message = Merge Failed: The push was rejected but there was no remote message.
Review the Git Hooks for this repository @@ -2730,6 +2731,7 @@ dashboard.reinit_missing_repos = Reinitialize all missing Git repositories for w dashboard.sync_external_users = Synchronize external user data dashboard.cleanup_hook_task_table = Cleanup hook_task table dashboard.cleanup_packages = Cleanup expired packages +dashboard.cleanup_actions = Cleanup actions expired logs and artifacts dashboard.server_uptime = Server Uptime dashboard.current_goroutine = Current Goroutines dashboard.current_memory_usage = Current Memory Usage @@ -3502,6 +3504,7 @@ runners.reset_registration_token_success = Runner registration token reset succe runs.all_workflows = All Workflows runs.commit = Commit +runs.scheduled = Scheduled runs.pushed_by = pushed by runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s runs.no_matching_runner_helper = No matching runner: %s diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 00228c5f62..19e2d97dc6 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -4,6 +4,7 @@ explore=Explorateur help=Aide logo=Logo sign_in=Connexion +sign_in_with_provider=Se connecter avec %s sign_in_or=ou sign_out=Déconnexion sign_up=S'inscrire @@ -25,7 +26,7 @@ licenses=Licences return_to_gitea=Revenir à Gitea username=Nom d'utilisateur -email=Adresse e-mail +email=Courriel password=Mot de passe access_token=Jeton d’accès re_type=Confirmez le mot de passe @@ -79,6 +80,7 @@ milestones=Jalons ok=OK cancel=Annuler +retry=Réessayez rerun=Relancer rerun_all=Relancer toutes les tâches save=Enregistrer @@ -91,6 +93,7 @@ edit=Éditer enabled=Activé disabled=Désactivé +locked=Verrouillée copy=Copier copy_url=Copier l'URL @@ -125,9 +128,12 @@ concept_user_individual=Individuel concept_code_repository=Dépôt concept_user_organization=Organisation +show_timestamps=Afficher les dates show_log_seconds=Afficher les secondes show_full_screen=Affichez en plein écran +download_logs=Télécharger les logs +confirm_delete_selected=Êtes-vous sûr de vouloir supprimer tous les éléments sélectionnés ? name=Nom value=Valeur @@ -166,6 +172,7 @@ string.desc=Z - A [error] occurred=Une erreur s’est produite +report_message=Si vous pensez qu'il s'agit d'un bug Gitea, veuillez consulter notre board GitHub ou ouvrir un nouveau ticket si nécessaire. missing_csrf=Requête incorrecte: aucun jeton CSRF présent invalid_csrf=Requête incorrecte : jeton CSRF invalide not_found=La cible n'a pu être trouvée. @@ -206,7 +213,7 @@ reinstall_confirm_check_3=Vous confirmez : vous êtes absolument certain que ce err_empty_db_path=Le chemin de la base de données SQLite3 ne peut être vide. no_admin_and_disable_registration=Vous ne pouvez pas désactiver la création de nouveaux utilisateurs avant d'avoir créé un compte administrateur. err_empty_admin_password=Le mot de passe administrateur ne peut pas être vide. -err_empty_admin_email=L'adresse e-mail de l'administrateur ne peut pas être vide. +err_empty_admin_email=L’adresse courriel de l'administrateur ne peut être vide. err_admin_name_is_reserved=Le nom d'utilisateur de l'administrateur est invalide, le nom d'utilisateur est réservé err_admin_name_pattern_not_allowed=Le nom d'utilisateur de l'administrateur est invalide, le nom d'utilisateur est réservé err_admin_name_is_invalid=Le nom d'utilisateur de l'administrateur est invalide @@ -219,6 +226,7 @@ repo_path_helper=Les dépôts Git distants seront stockés dans ce répertoire. lfs_path=Répertoire racine Git LFS lfs_path_helper=Les fichiers suivis par Git LFS seront stockés dans ce dossier. Laissez vide pour désactiver LFS. run_user=Exécuter avec le compte d'un autre utilisateur +run_user_helper=Le nom d'utilisateur du système d'exploitation sous lequel Gitea fonctionne. Notez que cet utilisateur doit avoir accès au dossier racine du dépôt. domain=Domaine du serveur domain_helper=Domaine ou adresse d'hôte pour le serveur. ssh_port=Port du serveur SSH @@ -226,20 +234,20 @@ ssh_port_helper=Port d'écoute du serveur SSH. Laissez le vide pour le désactiv http_port=Port d'écoute HTTP de Gitea http_port_helper=Port sur lequel le serveur web Gitea attendra des requêtes. app_url=URL de base de Gitea -app_url_helper=Adresse HTTP(S) de base pour les clones git et les notifications par e-mail. +app_url_helper=Adresse HTTP(S) de base pour les clones git et les notifications par courriel. log_root_path=Chemin des journaux log_root_path_helper=Les fichiers de journalisation seront écrits dans ce répertoire. optional_title=Paramètres facultatifs -email_title=Paramètres E-mail +email_title=Paramètres de Messagerie smtp_addr=Hôte SMTP smtp_port=Port SMTP -smtp_from=Envoyer les e-mails en tant que -smtp_from_helper=Adresse e-mail utilisée par Gitea. Veuillez entrer votre e-mail directement ou sous la forme . +smtp_from=Envoyer les courriels en tant que +smtp_from_helper=Adresse courriel utilisée par Gitea. Utilisez directement votre adresse ou la forme « Nom  ». mailer_user=Utilisateur SMTP mailer_password=Mot de passe SMTP -register_confirm=Exiger la confirmation de l'e-mail lors de l'inscription -mail_notify=Activer les notifications par e-mail +register_confirm=Exiger la confirmation du courriel lors de l'inscription +mail_notify=Activer les notifications par courriel server_service_title=Paramètres Serveur et Tierce Parties offline_mode=Activer le mode hors-ligne offline_mode_popup=Désactiver l'utilisation de CDNs, et servir toutes les ressources localement. @@ -263,7 +271,7 @@ admin_title=Paramètres de compte administrateur admin_name=Nom d’utilisateur administrateur admin_password=Mot de passe confirm_password=Confirmez le mot de passe -admin_email=Adresse e-mail +admin_email=Courriel install_btn_confirm=Installer Gitea test_git_failed=Le test de la commande "git" a échoué : %v sqlite3_not_available=Cette version de Gitea ne supporte pas SQLite3. Veuillez télécharger la version binaire officielle de %s (pas la version 'gobuild'). @@ -277,22 +285,24 @@ secret_key_failed=Impossible de générer la clé secrète : %v save_config_failed=L'enregistrement de la configuration %v a échoué invalid_admin_setting=Paramètres du compte administrateur invalides : %v invalid_log_root_path=Le répertoire des fichiers de journalisation est invalide : %v -default_keep_email_private=Masquer les adresses e-mail par défaut -default_keep_email_private_popup=Masquer les adresses e-mail des nouveaux comptes utilisateurs par défaut. +default_keep_email_private=Masquer les adresses courriels par défaut +default_keep_email_private_popup=Masquer par défaut les adresses courriels des nouveaux utilisateurs. default_allow_create_organization=Autoriser la création d'organisations par défaut default_allow_create_organization_popup=Permettre aux nouveaux comptes utilisateurs de créer des organisations par défaut. default_enable_timetracking=Activer le suivi de temps par défaut default_enable_timetracking_popup=Activer le suivi du temps pour les nouveaux dépôts par défaut. -no_reply_address=Domaine pour les e-mails cachés -no_reply_address_helper=Nom de domaine pour les utilisateurs possédant une adresse email cachée. Par exemple, le nom d’utilisateur « joe » sera enregistré dans Git comme « joe@noreply.example.org » si le domaine pour les e-mails cachés a la valeur « noreply.example.org ». +no_reply_address=Domaine pour les courriels cachés +no_reply_address_helper=Nom de domaine pour les utilisateurs ayant une adresse courriel cachée. Par exemple, l’utilisateur « fred » sera associé à « fred@noreply.example.org » par Git si le domaine est « noreply.example.org ». password_algorithm=Algorithme de hachage du mot de passe invalid_password_algorithm=Algorithme de hachage du mot de passe invalide password_algorithm_helper=Définissez l’algorithme de hachage du mot de passe. Les algorithmes ont des exigences matérielles et une résistance différentes. L’algorithme argon2 est bien sécurisé mais utilise beaucoup de mémoire et peut être inapproprié pour les systèmes limités en ressources. 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 : [home] -uname_holder=Nom d'utilisateur ou adresse e-mail +uname_holder=Nom d’utilisateur ou adresse courriel password_holder=Mot de passe switch_dashboard_context=Basculer le contexte du tableau de bord my_repos=Dépôts @@ -346,12 +356,13 @@ create_new_account=Créer un compte register_helper_msg=Déjà enregistré ? Connectez-vous ! social_register_helper_msg=Déjà inscrit ? Connectez-vous ! disable_register_prompt=Les inscriptions sont désactivées. Veuillez contacter l'administrateur du site. -disable_register_mail=La confirmation par e-mail à l'inscription est désactivée. +disable_register_mail=La confirmation par courriel à l’inscription est désactivée. manual_activation_only=Contactez l'administrateur de votre site pour terminer l'activation. remember_me=Mémoriser cet appareil forgot_password_title=Mot de passe oublié forgot_password=Mot de passe oublié ? sign_up_now=Pas de compte ? Inscrivez-vous maintenant. +sign_up_successful=Le compte a été créé avec succès. Bienvenue ! confirmation_mail_sent_prompt=Un nouveau mail de confirmation a été envoyé à %s. Veuillez vérifier votre boîte de réception dans les prochaines %s pour valider votre enregistrement. must_change_password=Réinitialisez votre mot de passe allow_password_change=Demande à l'utilisateur de changer son mot de passe (recommandé) @@ -359,15 +370,17 @@ reset_password_mail_sent_prompt=Un mail de confirmation a été envoyé à %s active_your_account=Activer votre compte account_activated=Le compte a été activé prohibit_login=Connexion interdite -resent_limit_prompt=Désolé, vous avez récemment demandé un e-mail d'activation. Veuillez réessayer dans 3 minutes. -has_unconfirmed_mail=Bonjour %s, votre adresse e-mail (%s) n'a pas été confirmée. Si vous n'avez reçu aucun mail de confirmation ou souhaitez renouveler l'envoi, cliquez sur le bouton ci-dessous. +prohibit_login_desc=Votre compte n'autorise pas la connexion, veuillez contacter l'administrateur de votre site. +resent_limit_prompt=Désolé, vous avez récemment demandé un courriel d'activation. Veuillez réessayer dans 3 minutes. +has_unconfirmed_mail=Bonjour %s, votre adresse courriel (%s) n’a pas été confirmée. Si vous n’avez reçu aucun mail de confirmation ou souhaitez renouveler l’envoi, cliquez sur le bouton ci-dessous. resend_mail=Cliquez ici pour renvoyer un mail de confirmation -email_not_associate=L'adresse e-mail n'est associée à aucun compte. -send_reset_mail=Envoyer un e-mail de récupération du compte +email_not_associate=L’adresse courriel n’est associée à aucun compte. +send_reset_mail=Envoyer un courriel de récupération du compte reset_password=Récupération du compte invalid_code=Votre code de confirmation est invalide ou a expiré. invalid_password=Votre mot de passe ne correspond pas à celui utilisé pour créer le compte. reset_password_helper=Récupérer un compte +reset_password_wrong_user=Vous êtes connecté en tant que %s, mais le lien de récupération est pour %s password_too_short=Le mot de passe doit contenir %d caractères minimum. non_local_account=Les mots de passes des comptes utilisateurs externes ne peuvent pas être modifiées depuis l'interface web Gitea. verify=Vérifier @@ -392,21 +405,24 @@ openid_connect_title=Se connecter à un compte existant openid_connect_desc=L'URI OpenID choisie est inconnue. Associez-le à un nouveau compte ici. openid_register_title=Créer un nouveau compte openid_register_desc=L'URI OpenID choisie est inconnue. Associez-le à un nouveau compte ici. -disable_forgot_password_mail=La récupération du compte est désactivée car aucune adresse courriel n'est configurée. Veuillez contacter l'administrateur de votre site. -disable_forgot_password_mail_admin=La récupération du compte est disponible uniquement lorsque l'adresse courriel est configurée. Veuillez configurer l'adresse courriel pour activer la récupération du compte. -email_domain_blacklisted=Vous ne pouvez pas vous enregistrer avec votre adresse e-mail. +openid_signin_desc=Entrez l'URI de votre OpenID. Par exemple : alice.openid.example.org ou https://openid.example.org/alice. +disable_forgot_password_mail=La récupération du compte est désactivée car aucune adresse courriel n’est configurée. Veuillez contacter l'administrateur de votre site. +disable_forgot_password_mail_admin=La récupération du compte est disponible uniquement lorsque l’adresse courriel est configurée. Veuillez configurer l’adresse courriel pour activer la récupération du compte. +email_domain_blacklisted=Vous ne pouvez pas vous enregistrer avec votre adresse courriel. 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_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. sspi_auth_failed=Échec de l'authentification SSPI +password_pwned=Le mot de passe que vous avez choisi se trouve sur la liste des mots de passe ayant fuité sur internet. Veuillez réessayer avec un mot de passe différent et considérer remplacer ce mot de passe si vous l'utilisez ailleurs. password_pwned_err=Impossible d'envoyer la demande à HaveIBeenPwned [mail] view_it_on=Voir sur %s -reply=ou répondez directement à cet e-mail +reply=ou répondez directement à ce courriel link_not_working_do_paste=Le lien ne fonctionne pas ? Essayez de le copier-coller dans votre navigateur. hi_user_x=Bonjour %s, @@ -415,8 +431,9 @@ activate_account.title=%s, veuillez activer votre compte activate_account.text_1=Bonjour %[1]s, merci de votre inscription chez %[2]s! activate_account.text_2=Veuillez cliquer sur ce lien pour activer votre compte chez %s: -activate_email=Veuillez vérifier votre adresse e-mail -activate_email.text=Veuillez cliquer sur le lien suivant pour vérifier votre adresse de courriel dans %s: +activate_email=Veuillez vérifier votre adresse courriel +activate_email.title=%s, veuillez vérifier votre adresse courriel +activate_email.text=Veuillez cliquer sur le lien suivant pour vérifier votre adresse courriel dans %s: register_notify=Bienvenue sur Gitea register_notify.title=%[1]s, bienvenue à %[2]s @@ -435,16 +452,16 @@ issue_assigned.issue=@%[1]s vous a assigné le ticket %[2]s dans le dépôt %[3] issue.x_mentioned_you=@%s vous a mentionné: issue.action.force_push=%[1]s a forcé la mise à jour de %[2]s depuis %[3]s vers %[4]s. -issue.action.push_1=@%[1]s a soumis %[3]d validation sur %[2]s -issue.action.push_n=@%[1]s a soumis %[3]d validations sur %[2]s +issue.action.push_1=@%[1]s a soumis %[3]d révision sur %[2]s +issue.action.push_n=@%[1]s a soumis %[3]d révisions sur %[2]s issue.action.close=@%[1]s a fermé #%[2]d. issue.action.reopen=@%[1]s a réouvert #%[2]d. issue.action.merge=@%[1]s a fusionné de #%[2]d vers %[3]s. issue.action.approve=@%[1]s a approuvé cette demande d'ajout. issue.action.reject=@%[1]s a demandé des modifications sur cette demande d'ajout. issue.action.review=@%[1]s a commenté sur cette demande d'ajout. -issue.action.review_dismissed=@%[1]s a rejeté la dernière révision de %[2]s pour cette demande d'ajout. -issue.action.ready_for_review=@%[1]s a marqué cette demande d'ajout prête à être revue. +issue.action.review_dismissed=@%[1]s a révoqué la dernière évaluation de %[2]s pour cette demande d'ajout. +issue.action.ready_for_review=La demande d’ajout de @%[1]s est prête à être évaluée. issue.action.new=@%[1]s a créé #%[2]d. issue.in_tree_path=Dans %s: @@ -467,7 +484,7 @@ repo.collaborator.added.text=Vous avez été ajouté en tant que collaborateur d team_invite.subject=%[1]s vous a invité à rejoindre l’organisation %[2]s team_invite.text_1=%[1]s vous a invité à rejoindre l’équipe %[2]s dans l’organisation %[3]s. team_invite.text_2=Veuillez cliquer sur le lien suivant pour rejoindre l'équipe : -team_invite.text_3=Remarque : Cette invitation était destinée à %[1]s. Si vous n’attendiez pas cette invitation, vous pouvez ignorer cet e-mail. +team_invite.text_3=Remarque : Cette invitation était destinée à %[1]s. Si vous n’attendiez pas cette invitation, vous pouvez ignorer ce courriel. [modal] yes=Oui @@ -479,7 +496,7 @@ modify=Mettre à jour [form] UserName=Nom d'utilisateur RepoName=Nom du dépôt -Email=Adresse e-mail +Email=Courriel Password=Mot de passe Retype=Confirmez le mot de passe SSHTitle=Nom de la clé SSH @@ -487,31 +504,32 @@ HttpsUrl=URL HTTPS PayloadUrl=URL des données utiles TeamName=Nom de l'équipe AuthName=Nom d'autorisation -AdminEmail=E-mail de l'administrateur +AdminEmail=Courriel de l’administrateur NewBranchName=Nouveau nom de la branche CommitSummary=Résumé de la révision -CommitMessage=Message de révision -CommitChoice=Choix de révision +CommitMessage=Message de la révision +CommitChoice=Choix de la révision TreeName=Chemin du fichier Content=Contenu SSPISeparatorReplacement=Séparateur SSPIDefaultLanguage=Langue par défaut -require_error=` ne peut pas être vide.` -alpha_dash_error=` ne doit contenir que des caractères alphanumériques, des tirets ("-") et des tirets bas (" _ ").` -alpha_dash_dot_error=` ne doit contenir que des caractères alphanumériques, des tirets ("-"), des tirets bas ("_"), et des points. (".").` -git_ref_name_error=` doit être un nom de référence Git bien formé.` -size_error=` doit être à la taille de %s.` -min_size_error=` %s caractères minimum ` -max_size_error=` %s caractères maximum ` -email_error=` adresse e-mail invalide ` -url_error=`"%s" n'est pas une URL valide.` -include_error=` doit contenir la sous-chaîne "%s".` -glob_pattern_error=` le motif de développement est invalide : %s.` -regex_pattern_error=` le motif regex est invalide : %s.` -username_error=` ne peut contenir que des caractères alphanumériques ('0-9','a-z','A-Z'), tiret ('-'), tiret de soulignement ('_') et point ('.'). Il ne peut pas commencer ou se finir par des caractères non alphanumériques. Les caractères non alphanumériques consécutifs sont également interdits.` +require_error=` ne peut être vide.` +alpha_dash_error=` ne peut contenir que des caractères alphanumériques, trait d'union « - » et tiret bas « _ ».` +alpha_dash_dot_error=` ne peut contenir que des caractères alphanumériques, trait d'union « - », tiret bas « _ » et point « . »` +git_ref_name_error=` n'est pas une référence Git correcte.` +size_error=` doit mesurer %s caractères exactement.` +min_size_error=` doit mesurer %s caractères au minimum.` +max_size_error=` doit mesurer %s caractères au maximum.` +email_error=` n’est pas une adresse courriel valide.` +url_error=`« %s » n'est pas une URL valide.` +include_error=` doit contenir "%s".` +glob_pattern_error=` a un motif glob invalide : %s.` +regex_pattern_error=` a un motif regex invalide : %s.` +username_error=` ne peut contenir que des caractères alphanumériques, trait d'union « - », tiret bas « _ » et point « . », ne peux commencer que par des caractères alphanumériques et avoir des symboles consécutifs.` +invalid_group_team_map_error=` a une cartographie invalide : %s` unknown_error=Erreur inconnue : captcha_incorrect=Le code CAPTCHA est incorrect. password_not_match=Les mots de passe ne correspondent pas. @@ -523,16 +541,16 @@ username_has_not_been_changed=Le nom d'utilisateur n'a pas été modifié repo_name_been_taken=Ce nom de dépôt est déjà utilisé. repository_force_private=Force Private est activé : les dépôts privés ne peuvent pas être rendus publics. repository_files_already_exist=Les fichiers existent déjà pour ce dépôt. Contactez l'administrateur système. -repository_files_already_exist.adopt=Des fichiers existent déjà pour ce dépôt et peuvent seulement être adoptés. +repository_files_already_exist.adopt=Des fichiers existent déjà dans ce dépôt et ne peuvent être qu’adoptés. repository_files_already_exist.delete=Des fichiers existent déjà pour ce dépôt. Vous devez les supprimer. -repository_files_already_exist.adopt_or_delete=Des fichiers existent déjà pour ce dépôt. Veuillez les adopter ou les supprimer. +repository_files_already_exist.adopt_or_delete=Des fichiers existent déjà dans ce dépôt. Veuillez les adopter ou les supprimer. visit_rate_limit=Le taux d'appel à distance autorisé a été dépassé. 2fa_auth_required=L'accès à distance requiert une authentification à deux facteurs. org_name_been_taken=Ce nom d'organisation est déjà pris. team_name_been_taken=Le nom d'équipe est déjà pris. team_no_units_error=Autoriser l’accès à au moins une section du dépôt. -email_been_used=Cette adresse e-mail est déjà utilisée. -email_invalid=L'adresse e-mail est invalide. +email_been_used=Cette adresse courriel est déjà utilisée. +email_invalid=Cette adresse courriel est invalide. openid_been_used=Adresse OpenID "%s" déjà utilisée. username_password_incorrect=Identifiant ou mot de passe invalide. password_complexity=Le mot de passe ne respecte pas les exigences de complexité: @@ -583,8 +601,10 @@ unfollow=Ne plus suivre heatmap.loading=Chargement de la Heatmap… user_bio=Biographie disabled_public_activity=Cet utilisateur a désactivé la visibilité publique de l'activité. -email_visibility.limited=Votre adresse e-mail est visible pour tous les utilisateurs authentifiés -email_visibility.private=Votre adresse e-mail n'est visible que pour vous et les administrateurs +email_visibility.limited=Votre adresse courriel est visible pour tous les utilisateurs authentifiés +email_visibility.private=Votre adresse courriel n'est visible que pour vous et les administrateurs +show_on_map=Afficher ce lieu sur une carte +settings=Paramètres utilisateur form.name_reserved=Le nom d’utilisateur "%s" est réservé. form.name_pattern_not_allowed=Le motif "%s" n'est pas autorisé dans un nom de d'utilisateur. @@ -606,11 +626,15 @@ delete=Supprimer le compte twofa=Authentification à deux facteurs account_link=Comptes liés organization=Organisations +uid=UID webauthn=Clés de sécurité 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=Les utilisateurs externes ne sont pas autorisés à modifier leur nom d'utilisateur. Veuillez contacter l'administrateur de votre site pour plus de détails. -full_name=Non Complet +full_name=Nom complet website=Site Web location=Localisation update_theme=Modifier le thème @@ -620,16 +644,18 @@ update_language_not_found=La langue "%s" n'est pas disponible. update_language_success=La langue a été mise à jour. update_profile_success=Votre profil a été mis à jour. change_username=Votre nom d'utilisateur a été modifié. +change_username_prompt=Remarque : La modification de votre nom d'utilisateur modifie également l'URL de votre compte. +change_username_redirect_prompt=L’ancien nom d'utilisateur redirigera vers le nouveau, jusqu’à ce qu'il soit réclamé. continue=Continuer cancel=Annuler language=Langue ui=Thème -hidden_comment_types=Types de commentaires masqués -hidden_comment_types_description=Les types de commentaires sélectionnés ici ne seront pas affichés dans les pages de tickets. Par exemple, sélectionner « Étiquette » retire tous les commentaires du genre « as ajouté l'étiquette <étiquette> .». -hidden_comment_types.ref_tooltip=Commentaires dont le ticket est référencé ailleurs (message de révision, autre tickets, …) +hidden_comment_types=Catégories de commentaires masqués +hidden_comment_types_description=Les catégories de commentaires cochées masqueront les commentaires respectifs des tickets. Par exemple, « Étiquette » retire tous les commentaires du genre « as ajouté l'étiquette <étiquette> .». +hidden_comment_types.ref_tooltip=Commentaires où ce ticket a été référencé sur un autre ticket, révision, etc. hidden_comment_types.issue_ref_tooltip=Commentaires où l’utilisateur change la branche/étiquette associée au ticket comment_type_group_reference=Référence -comment_type_group_label=Libellé +comment_type_group_label=Étiquette comment_type_group_milestone=Jalon comment_type_group_assignee=Assigné à comment_type_group_title=Titre @@ -638,15 +664,16 @@ comment_type_group_time_tracking=Minuteur comment_type_group_deadline=Échéance comment_type_group_dependency=Dépendance comment_type_group_lock=Verrouiller le statut -comment_type_group_review_request=Demande de revue +comment_type_group_review_request=Demande d’évaluation comment_type_group_pull_request_push=Révisions ajoutées comment_type_group_project=Projet comment_type_group_issue_ref=Référence du ticket saved_successfully=Vos paramètres ont été enregistrés avec succès. privacy=Confidentialité +keep_activity_private=Masquer l'activité de la page de profil keep_activity_private_popup=Rend l'activité visible uniquement pour vous et les administrateurs -lookup_avatar_by_mail=Rechercher un avatar par adresse e-mail +lookup_avatar_by_mail=Rechercher un avatar par courriel federated_avatar_lookup=Recherche d'avatars fédérés enable_custom_avatar=Utiliser un avatar personnalisé choose_new_avatar=Sélectionner un nouvel avatar @@ -660,14 +687,16 @@ update_user_avatar_success=L'avatar de l'utilisateur a été mis à jour. change_password=Modifier le mot de passe old_password=Mot de passe actuel new_password=Nouveau mot de passe +retype_new_password=Confirmer le nouveau mot de passe password_incorrect=Le mot de passe actuel est incorrect. change_password_success=Votre mot de passe a été mis à jour. Désormais, connectez-vous avec votre nouveau mot de passe. password_change_disabled=Les mots de passes des comptes utilisateurs externes ne peuvent pas être modifiées depuis l'interface web Gitea. -emails=Adresses e-mail -manage_emails=Gérer les adresses e-mail +emails=Adresses courriels +manage_emails=Gérer les adresses courriels manage_themes=Sélectionner le thème par défaut manage_openid=Gérer les adresses OpenID +email_desc=Votre adresse courriel principale sera utilisée pour les notifications, la récupération de mot de passe et, à condition qu'elle ne soit pas cachée, les opérations Git basées sur le Web. theme_desc=Ce sera votre thème par défaut sur le site. primary=Principale activated=Activé @@ -675,6 +704,7 @@ requires_activation=Nécessite une activation primary_email=Faire de cette adresse votre adresse principale activate_email=Envoyer l’activation activations_pending=Activations en attente +can_not_add_email_activations_pending=Il y a une activation en attente, réessayez dans quelques minutes si vous souhaitez ajouter un nouvel e-mail. delete_email=Exclure email_deletion=Supprimer l'adresse e-mail email_deletion_desc=L’adresse e-mail et les informations associées seront retirées de votre compte. Les révisions Git effectuées par cette adresse resteront inchangées. Continuer ? @@ -693,6 +723,7 @@ add_email_success=La nouvelle adresse e-mail a été ajoutée. email_preference_set_success=L'e-mail de préférence a été défini avec succès. add_openid_success=La nouvelle adresse OpenID a été ajoutée. keep_email_private=Cacher l'adresse e-mail +keep_email_private_popup=Ceci masquera votre adresse e-mail de votre profil, de vos demandes d'ajout et des fichiers modifiés depuis l'interface Web. Les révisions déjà soumises ne seront pas modifiés. openid_desc=OpenID vous permet de confier l'authentification à une tierce partie. manage_ssh_keys=Gérer les clés SSH @@ -701,7 +732,7 @@ manage_gpg_keys=Gérer les clés GPG add_key=Ajouter une clé ssh_desc=Ces clefs SSH publiques sont associées à votre compte. Les clefs privées correspondantes permettent l'accès complet à vos repos. principal_desc=Ces Principaux de certificats SSH sont associés à votre compte et permettent un accès complet à vos dépôts. -gpg_desc=Ces clefs GPG sont associées avec votre compte. Conservez-les en lieu sûr, car elles permettent la vérification de vos commits. +gpg_desc=Ces clés GPG sont associées à votre compte. Conservez-les en lieu sûr, car elles permettent de vérifier vos révisions. ssh_helper=Besoin d'aide ? Consultez le guide de GitHub pour créer vos propres clés SSH ou résoudre les problèmes courants que vous pourriez rencontrer en utilisant SSH. gpg_helper=Besoin d'aide ? Consultez le guide de GitHub sur GPG. add_new_key=Ajouter une clé SSH @@ -715,9 +746,9 @@ ssh_principal_been_used=Ce principal a déjà été ajouté au serveur. gpg_key_id_used=Une clé publique GPG avec le même ID existe déjà. gpg_no_key_email_found=Cette clé GPG ne correspond à aucune adresse e-mail activée associée à votre compte. Elle peut toujours être ajoutée si vous signez le jeton fourni. gpg_key_matched_identities=Identités correspondantes : -gpg_key_matched_identities_long=Les identités intégrées dans cette clé correspondent aux adresses e-mail activées suivantes pour cet utilisateur. Les commits correspondant à ces adresses e-mail peuvent être vérifiés avec cette clé. +gpg_key_matched_identities_long=Les identités intégrées dans cette clé correspondent aux adresses e-mail activées suivantes pour cet utilisateur. Les révisions correspondant à ces adresses e-mail peuvent être vérifiés avec cette clé. gpg_key_verified=Clé vérifiée -gpg_key_verified_long=La clé a été vérifiée avec un jeton et peut être utilisée pour vérifier les révisions correspondant à toutes les adresses e-mails pour cet utilisateur en plus de toutes les identités pour cette clé. +gpg_key_verified_long=Cette clé a été vérifiée à l’aide d’un jeton et peut dorénavant être utilisée pour authentifier vos révisions lorsqu’elles contiennent l’un de vos courriels actifs ou des identités associées à cette clé. gpg_key_verify=Vérifier gpg_invalid_token_signature=La clé GPG, la signature et le jeton fournis ne correspondent pas ou le jeton n'est pas à jour. gpg_token_required=Vous devez fournir une signature pour le jeton ci-dessous @@ -728,7 +759,7 @@ gpg_token_signature=Signature GPG renforcée key_signature_gpg_placeholder=Commence par '-----BEGIN PGP SIGNATURE-----' verify_gpg_key_success=La clé GPG "%s" a été vérifiée. ssh_key_verified=Clé vérifiée -ssh_key_verified_long=La clé a été vérifiée avec un jeton et peut être utilisée pour vérifier les commits correspondant à toutes les adresses mail activées pour cet utilisateur. +ssh_key_verified_long=La clé a été vérifiée avec un jeton et peut dorénavant être utilisée pour vérifier les révisions comportant l'une des adresses e-mails activées de cet utilisateur. ssh_key_verify=Vérifier ssh_invalid_token_signature=La clé SSH, la signature ou le jeton fournis ne correspondent pas ou le jeton est périmé. ssh_token_required=Vous devez fournir une signature pour le jeton ci-dessous @@ -750,7 +781,7 @@ ssh_key_deletion=Retirer la clé SSH gpg_key_deletion=Retirer la clé GPG ssh_principal_deletion=Retirer le Principal de certificat SSH ssh_key_deletion_desc=Le retrait d'une clé SSH révoque son accès à votre compte. Continuer ? -gpg_key_deletion_desc=Supprimer une clé GPG renie les révisions signées par celle-ci. Continuer ? +gpg_key_deletion_desc=Supprimer une clé GPG discrédite les révisions signées par celle-ci. Continuer ? ssh_principal_deletion_desc=Le retrait d'un Principal de certificat SSH révoque son accès à votre compte. Poursuivre ? ssh_key_deletion_success=La clé SSH a été retirée. gpg_key_deletion_success=La clé GPG a été retirée. @@ -768,10 +799,12 @@ principal_state_desc=Ce Principal a été utilisé au cours des 7 derniers jours show_openid=Afficher sur le profil hide_openid=Masquer du profil ssh_disabled=SSH désactivé -ssh_signonly=Le SSH est actuellement désactivé, donc ces clés ne sont utilisées que pour la vérification de la signature de livraison. +ssh_signonly=SSH étant désactivé, ces clés ne servent qu'à vérifier la signature des révisions. ssh_externally_managed=Cette clé SSH est gérée de manière externe pour cet utilisateur manage_social=Gérer les réseaux sociaux associés +social_desc=Ces comptes sociaux peuvent être utilisés pour vous connecter à votre compte. Assurez-vous de les reconnaître tous. unbind=Dissocier +unbind_success=Le compte social a été supprimé avec succès. manage_access_token=Gérer les jetons d'accès generate_new_token=Générer un nouveau jeton @@ -792,6 +825,8 @@ permissions_access_all=Tout (public, privé et limité) select_permissions=Sélectionner les autorisations permission_no_access=Aucun accès permission_read=Lue(s) +permission_write=Lecture et écriture +access_token_desc=Les autorisations des jetons sélectionnées se limitent aux routes API correspondantes. Lisez la documentation pour plus d’informations. at_least_one_permission=Vous devez sélectionner au moins une permission pour créer un jeton. permissions_list=Autorisations : @@ -803,6 +838,8 @@ remove_oauth2_application_desc=La suppression d'une application OAuth2 révoquer remove_oauth2_application_success=L'application a été supprimée. create_oauth2_application=Créer une nouvelle application OAuth2 create_oauth2_application_button=Créer une application +create_oauth2_application_success=Vous avez créé une nouvelle application OAuth2 avec succès. +update_oauth2_application_success=Vous avez mis à jour l'application OAuth2 avec succès. oauth2_application_name=Nom de l'Application oauth2_confidential_client=Client confidentiel. Sélectionnez cette option pour les applications qui préservent la confidentialité du secret, telles que les applications web. Ne la sélectionnez pas pour les applications natives, y compris les applications de bureau et les applications mobiles. oauth2_redirect_uris=URI de redirection. Veuillez utiliser une nouvelle ligne pour chaque URI. @@ -811,19 +848,25 @@ oauth2_client_id=ID du client oauth2_client_secret=Secret du client oauth2_regenerate_secret=Regénérer le secret oauth2_regenerate_secret_hint=Avez-vous perdu votre secret ? +oauth2_client_secret_hint=Le secret ne sera plus affiché après avoir quitté ou actualisé cette page. Veuillez vous assurer que vous l'avez enregistré. oauth2_application_edit=Éditer oauth2_application_create_description=Les applications OAuth2 permettent à votre application tierce d'accéder aux comptes d'utilisateurs de cette instance. +oauth2_application_remove_description=La suppression d'une application OAuth2 l'empêchera d'accéder aux comptes d'utilisateurs autorisés sur cette instance. Poursuivre ? +oauth2_application_locked=Gitea préinstalle des applications OAuth2 au démarrage si elles sont activées dans la configuration. Pour éviter des comportements inattendus, celles-ci ne peuvent ni être éditées ni supprimées. Veuillez vous référer à la documentation OAuth2 pour plus d'informations. authorized_oauth2_applications=Applications OAuth2 autorisées +authorized_oauth2_applications_description=Vous avez autorisé l'accès à votre compte personnel Gitea à ces applications tierces. Veuillez révoquer l'accès aux applications dont vous n'avez plus besoin. revoke_key=Révoquer revoke_oauth2_grant=Révoquer l'accès revoke_oauth2_grant_description=La révocation de l'accès à cette application tierce l'empêchera d'accéder à vos données. Vous êtes sûr ? +revoke_oauth2_grant_success=Accès révoqué avec succès. twofa_desc=L'authentification à deux facteurs améliore la sécurité de votre compte. twofa_is_enrolled=Votre compte est inscrit à l'authentification à deux facteurs. twofa_not_enrolled=Votre compte n'est pas inscrit à l'authentification à deux facteurs. twofa_disable=Désactiver l'authentification à deux facteurs twofa_scratch_token_regenerate=Régénérer un jeton de secours +twofa_scratch_token_regenerated=Votre jeton de secours est désormais « %s ». Stockez-le dans un endroit sûr, il ne sera plus jamais affiché. twofa_enroll=Activer l'authentification à deux facteurs twofa_disable_note=Vous pouvez désactiver l'authentification à deux facteurs si nécessaire. twofa_disable_desc=Désactiver l'authentification à deux facteurs rendra votre compte plus vulnérable. Confirmer ? @@ -850,8 +893,10 @@ remove_account_link=Supprimer un compte lié remove_account_link_desc=La suppression d'un compte lié révoquera son accès à votre compte Gitea. Continuer ? remove_account_link_success=Le compte lié a été supprimé. +hooks.desc=Ajouter des déclencheurs Web qui seront amorçés pour tous les dépôts que vous possédez. orgs_none=Vous n'êtes membre d'aucune organisation. +repos_none=Vous ne possédez aucun dépôt. delete_account=Supprimer votre compte delete_prompt=Cette opération supprimera définitivement votre compte d'utilisateur. Cette action est IRRÉVERSIBLE. @@ -870,9 +915,12 @@ visibility=Visibilité de l'utilisateur visibility.public=Public visibility.public_tooltip=Visible par tout le monde visibility.limited=Limité +visibility.limited_tooltip=Visible uniquement pour les utilisateurs authentifiés visibility.private=Privé +visibility.private_tooltip=Visible uniquement aux membres des organisations que vous avez rejointes [repo] +new_repo_helper=Un dépôt contient tous les fichiers d'un projet, ainsi que l'historique de leurs modifications. Vous avez déjà ça ailleurs ? Migrez-le ici. owner=Propriétaire owner_helper=Certaines organisations peuvent ne pas apparaître dans la liste déroulante en raison d'une limite maximale du nombre de dépôts. repo_name=Nom du dépôt @@ -884,6 +932,7 @@ template_helper=Faire de ce dépôt un modèle template_description=Les référentiels de modèles permettent aux utilisateurs de générer de nouveaux référentiels avec la même structure de répertoire, fichiers et paramètres optionnels. visibility=Visibilité visibility_description=Seuls le propriétaire ou les membres de l'organisation, s'ils ont des droits, seront en mesure de le voir. +visibility_helper=Rendre le dépôt privé visibility_helper_forced=L'administrateur de votre serveur impose que les nouveaux dépôts soient privés. visibility_fork_helper=(Changer ceci affectera toutes les bifurcations.) clone_helper=Besoin d'aide pour dupliquer ? Visitez l'aide. @@ -892,6 +941,7 @@ fork_from=Bifurquer depuis already_forked=Vous avez déjà forké %s fork_to_different_account=Créer un embranchement vers un autre compte fork_visibility_helper=La visibilité d'un dépôt bifurqué ne peut pas être modifiée. +fork_no_valid_owners=Ce dépôt ne peut pas être bifurqué car il n’a pas de propriétaire valide. use_template=Utiliser ce modèle clone_in_vsc=Cloner dans VS Code download_zip=Télécharger le ZIP @@ -913,11 +963,11 @@ readme=LISEZMOI readme_helper=Choisissez un modèle de fichier LISEZMOI. readme_helper_desc=Le README est l'endroit idéal pour décrire votre projet et accueillir des contributeurs. auto_init=Initialiser le dépôt (avec un .gitignore, une Licence et un README.md) -trust_model_helper=Choisissez, parmi les éléments suivants, la manière dont Gitea contrôle les signatures paraphant les révisions : -trust_model_helper_collaborator=Collaborateur : ne se fier qu'aux signatures correspondant à des collaborateurs du dépôt -trust_model_helper_committer=Auteur : ne se fier qu'aux signatures correspondant à des utilisateurs de Gitea -trust_model_helper_collaborator_committer=Collaborateur et Auteur : ne se fier qu'aux signatures et auteurs correspondants à des collaborateurs du dépôt -trust_model_helper_default=Par défaut : utiliser le niveau de confiance par défaut pour ce dépôt +trust_model_helper=Choisissez, parmi les éléments suivants, les règles de confiance des signatures paraphant les révisions : +trust_model_helper_collaborator=Collaborateur : ne se fier qu'aux signatures des collaborateurs du dépôt +trust_model_helper_committer=Auteur : ne se fier qu'aux signatures des auteurs de révisions +trust_model_helper_collaborator_committer=Collaborateur et Auteur : ne se fier qu'aux signatures des auteurs collaborant au dépôt +trust_model_helper_default=Par défaut : valeur configurée par défaut pour cette instance Gitea create_repo=Créer un dépôt default_branch=Branche par défaut default_branch_helper=La branche par défaut est la branche de base pour les demandes d'ajout et les révisions de code. @@ -925,9 +975,11 @@ mirror_prune=Purger mirror_prune_desc=Supprimer les références externes obsolètes mirror_interval=Intervalle de synchronisation (les unités de temps valides sont 'h', 'm' et 's'). 0 pour désactiver la synchronisation automatique. (Intervalle minimum : %s) mirror_interval_invalid=L'intervalle de synchronisation est invalide. -mirror_sync_on_commit=Synchroniser quand les commits sont poussés +mirror_sync_on_commit=Synchroniser quand les révisions sont soumis mirror_address=Cloner depuis une URL mirror_address_desc=Insérez tous les identifiants requis dans la section Autorisation. +mirror_address_url_invalid=L’URL fournie est invalide. Vous devez échapper tous les composants de l'URL correctement. +mirror_address_protocol_invalid=L'URL fournie est invalide. Seuls les protocoles http(s):// ou git:// peuvent référencer un miroir. mirror_lfs=Stockage de fichiers volumineux (LFS) mirror_lfs_desc=Activer la mise en miroir des données LFS. mirror_lfs_endpoint=Point d'accès LFS @@ -943,15 +995,15 @@ forks=Bifurcations reactions_more=et %d de plus unit_disabled=L'administrateur du site a désactivé cette section du dépôt. language_other=Autre -adopt_search=Entrez le nom d'utilisateur pour rechercher les dépôts non adoptés... (laissez vide pour tous les trouver) +adopt_search=Entrez un nom d’utilisateur pour rechercher les dépôts dépossédés… (laissez vide pour tous trouver) adopt_preexisting_label=Adopter les fichiers adopt_preexisting=Adopter les fichiers préexistants -adopt_preexisting_content=Créer un dépôt à partir de %s +adopt_preexisting_content=Créer un dépôt à partir de %s. adopt_preexisting_success=Fichiers adoptés et dépôt créé depuis %s delete_preexisting_label=Supprimer delete_preexisting=Supprimer les fichiers préexistants delete_preexisting_content=Supprimer les fichiers dans %s -delete_preexisting_success=Supprimer les fichiers non adoptés dans %s +delete_preexisting_success=Fichiers dépossédés supprimés dans %s. blame_prior=Voir le blame avant cette modification author_search_tooltip=Affiche un maximum de 30 utilisateurs @@ -959,6 +1011,8 @@ transfer.accept=Accepter le transfert transfer.accept_desc=`Transférer à "%s"` transfer.reject=Refuser le transfert transfer.reject_desc=`Annuler le transfert à "%s"` +transfer.no_permission_to_accept=Vous n’êtes pas autorisé à accepter ce transfert. +transfer.no_permission_to_reject=Vous n’êtes pas autorisé à rejeter ce transfert. desc.private=Privé desc.public=Publique @@ -979,6 +1033,8 @@ template.issue_labels=Étiquettes de ticket template.one_item=Vous devez sélectionner au moins un élément du modèle template.invalid=Vous devez sélectionner un modèle de dépôt +archive.title=Ce dépôt est archivé. Vous pouvez voir ses fichiers ou le cloner, mais pas ouvrir de ticket ou de demandes d'ajout, ni soumettre de changements. +archive.title_date=Ce dépôt a été archivé le %s. Vous pouvez voir ses fichiers ou le cloner, mais pas ouvrir de ticket ou de demandes d'ajout, ni soumettre de changements. archive.issue.nocomment=Ce dépôt est archivé. Vous ne pouvez pas commenter de tickets. archive.pull.nocomment=Ce dépôt est archivé. Vous ne pouvez pas commenter de demande d'ajout. @@ -993,7 +1049,9 @@ migrate_service=Service de migration migrate_options_mirror_helper=Rendre ce dépôt mirroir migrate_options_lfs=Migrer les fichiers LFS migrate_options_lfs_endpoint.label=Point d'accès LFS +migrate_options_lfs_endpoint.description=La migration va tenter d'utiliser votre dépôt Git distant pour déterminer le serveur LFS. Vous pouvez également spécifier un point d'accès personnalisé si les données LFS du dépôt sont stockées ailleurs. migrate_options_lfs_endpoint.description.local=Un chemin de serveur local est également pris en charge. +migrate_options_lfs_endpoint.placeholder=Si laissé vide, le point de terminaison sera dérivé de l'URL du clone migrate_items=Éléments à migrer migrate_items_wiki=Wiki migrate_items_milestones=Jalons @@ -1009,6 +1067,7 @@ migrate.github_token_desc=Vous pouvez mettre un ou plusieurs jetons séparés pa migrate.clone_local_path=ou un chemin serveur local migrate.permission_denied=Vous n'êtes pas autorisé à importer des dépôts locaux. migrate.permission_denied_blocked=Vous ne pouvez pas importer depuis des hôtes interdits, veuillez demander à l'administrateur de vérifier les paramètres ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS. +migrate.invalid_local_path=Le chemin local n’est pas valide, n’existe pas ou n’est pas un dossier. migrate.invalid_lfs_endpoint=Le point d'accès LFS n'est pas valide. migrate.failed=Echec de migration: %v migrate.migrate_items_options=Un jeton d'accès est requis pour migrer des éléments supplémentaires @@ -1017,6 +1076,7 @@ migrated_from_fake=Migré de %[1]s migrate.migrate=Migrer depuis %s migrate.migrating=Migration de %s ... migrate.migrating_failed=La migration de %s a échoué. +migrate.migrating_failed.error=Échec de la migration : %s migrate.migrating_failed_no_addr=Échec de la migration. migrate.github.description=Migrer les données depuis github.com ou d’autres instances de GitHub. migrate.git.description=Migrer uniquement un dépôt depuis n’importe quel service Git. @@ -1034,6 +1094,7 @@ migrate.migrating_releases=Migration des versions migrate.migrating_issues=Migration des tickets migrate.migrating_pulls=Migration des demandes d'ajout migrate.cancel_migrating_title=Annuler la migration +migrate.cancel_migrating_confirm=Voulez-vous abandonner cette migration ? mirror_from=miroir de forked_from=bifurqué depuis @@ -1079,7 +1140,7 @@ org_labels_desc_manage=gérer milestones=Jalons commits=Révisions -commit=Commit +commit=Révision release=Versions releases=Versions tag=Étiquette @@ -1093,9 +1154,13 @@ file_view_rendered=Voir le rendu file_view_raw=Voir le Raw file_permalink=Lien permanent file_too_large=Le fichier est trop gros pour être affiché. -invisible_runes_line=`Cette ligne contient des caractères Unicode invisibles` -ambiguous_runes_line=`Cette ligne contient des caractères Unicode ambigus` -ambiguous_character=`%[1]c [U+%04[1]X] peut être confondu avec %[2]c [U+%04[2]X]` +invisible_runes_header=`Ce fichier contient des caractères Unicode invisibles.` +invisible_runes_description=`Ce fichier contient des caractères Unicode invisibles à l'œil nu, mais peuvent être traités différemment par un ordinateur. Si vous pensez que c'est intentionnel, vous pouvez ignorer cet avertissement. Utilisez le bouton Échappe pour les dévoiler.` +ambiguous_runes_header=`Ce fichier contient des caractères Unicode ambigus.` +ambiguous_runes_description=`Ce fichier contient des caractères Unicode qui peuvent être confondus avec d'autres caractères. Si vous pensez que c'est intentionnel, vous pouvez ignorer cet avertissement. Utilisez le bouton Échappe pour les dévoiler.` +invisible_runes_line=`Cette ligne contient des caractères Unicode invisibles.` +ambiguous_runes_line=`Cette ligne contient des caractères Unicode ambigus.` +ambiguous_character=`%[1]c [U+%04[1]X] peut être confondu avec %[2]c [U+%04[2]X].` escape_control_characters=Échapper unescape_control_characters=Annuler l'échappement @@ -1105,11 +1170,15 @@ video_not_supported_in_browser=Votre navigateur ne supporte pas la balise « vi audio_not_supported_in_browser=Votre navigateur ne supporte pas la balise « audio » HTML5. stored_lfs=Stocké avec Git LFS symbolic_link=Lien symbolique -commit_graph=Graphique des révisions +executable_file=Fichiers exécutables +commit_graph=Graphe des révisions commit_graph.select=Sélectionner les branches commit_graph.hide_pr_refs=Masquer les demandes d'ajout commit_graph.monochrome=Monochrome commit_graph.color=Couleur +commit.contained_in=Cette révision appartient à : +commit.contained_in_default_branch=Cette révision appartient à la branche par défaut +commit.load_referencing_branches_and_tags=Charger les branches et étiquettes référençant cette révision blame=Annotations download_file=Télécharger le fichier normal_view=Vue normale @@ -1135,8 +1204,8 @@ editor.name_your_file=Nommez votre fichier… editor.filename_help=Ajoutez un dossier en entrant son nom suivi d'une barre oblique ('/'). Supprimez un dossier avec un retour arrière au début du champ. editor.or=ou editor.cancel_lower=Annuler -editor.commit_signed_changes=Valider les révisions signées -editor.commit_changes=Enregistrer les modifications +editor.commit_signed_changes=Réviser les changements (signé) +editor.commit_changes=Réviser les changements editor.add_tmpl=Ajouter '' editor.add=Ajouter %s editor.update=Actualiser %s @@ -1146,9 +1215,9 @@ editor.patching=Correction: editor.fail_to_apply_patch=`Impossible d'appliquer le correctif "%s"` editor.new_patch=Nouveau correctif editor.commit_message_desc=Ajouter une description détaillée facultative… -editor.signoff_desc=Ajout d'un trailer Signed-off-by par le committeur à la fin du message du journal de commit. -editor.commit_directly_to_this_branch=Soumettre directement dans la branche %s. -editor.create_new_branch=Créer une nouvelle branche pour cette révision et envoyer une nouvelle demande d'ajout. +editor.signoff_desc=Créditer l'auteur "Signed-off-by:" en pied de révision. +editor.commit_directly_to_this_branch=Réviser directement dans la branche %s. +editor.create_new_branch=Créer une nouvelle branche pour cette révision et initier une demande d'ajout. editor.create_new_branch_np=Créer une nouvelle branche pour cette révision. editor.propose_file_change=Proposer une modification du fichier editor.new_branch_name=Nommer la nouvelle branche pour cette révision @@ -1159,10 +1228,14 @@ editor.filename_is_invalid=Le nom du fichier est invalide : "%s". editor.branch_does_not_exist=La branche "%s" n'existe pas dans ce dépôt. editor.branch_already_exists=La branche "%s" existe déjà dans ce dépôt. editor.directory_is_a_file=Le nom de dossier "%s" est déjà utilisé comme nom de fichier dans ce dépôt. +editor.file_is_a_symlink=`« %s » est un lien symbolique. Ce type de fichiers ne peut être modifié dans l'éditeur web.` +editor.filename_is_a_directory=« %s » est déjà utilisé comme nom de dossier dans ce dépôt. +editor.file_editing_no_longer_exists=Impossible de modifier le fichier « %s » car il n’existe plus dans ce dépôt. +editor.file_deleting_no_longer_exists=Impossible de supprimer le fichier « %s » car il n’existe plus dans ce dépôt. editor.file_changed_while_editing=Le contenu du fichier a changé depuis que vous avez commencé à éditer. Cliquez ici pour voir les changements ou soumettez de nouveau pour les écraser. editor.file_already_exists=Un fichier nommé "%s" existe déjà dans ce dépôt. -editor.commit_empty_file_header=Commiter un fichier vide -editor.commit_empty_file_text=Le fichier que vous allez commiter est vide. Continuer ? +editor.commit_empty_file_header=Réviser un fichier vide +editor.commit_empty_file_text=Le fichier que vous allez réviser est vide. Continuer ? editor.no_changes_to_show=Il n’y a aucune modification à afficher. editor.fail_to_update_file=Impossible de mettre à jour/créer le fichier "%s". editor.fail_to_update_file_summary=Message d'erreur : @@ -1174,7 +1247,7 @@ editor.unable_to_upload_files=Impossible d'envoyer le fichier "%s" : %v editor.upload_file_is_locked=Le fichier "%s" est verrouillé par %s. editor.upload_files_to_dir=`Téléverser les fichiers vers "%s"` editor.cannot_commit_to_protected_branch=Impossible de créer une révision sur la branche protégée "%s". -editor.no_commit_to_branch=Impossible d'enregistrer la révisions directement sur la branche parce que : +editor.no_commit_to_branch=Impossible d'enregistrer la révision directement sur la branche parce que : editor.user_no_push_to_branch=L'utilisateur ne peut pas pousser vers la branche editor.require_signed_commit=Cette branche nécessite une révision signée editor.cherry_pick=Picorer %s vers: @@ -1185,7 +1258,7 @@ commits.commits=Révisions commits.no_commits=Pas de révisions en commun. "%s" et "%s" ont des historiques entièrement différents. commits.nothing_to_compare=Ces branches sont égales. commits.search=Rechercher des révisions… -commits.search.tooltip=Vous pouvez préfixer les mots-clés avec "author:", "committer:", "after:", ou "before:", par exemple "revert author:Alice before:2019-01-13". +commits.search.tooltip=Vous pouvez utiliser les mots-clés "author:", "committer:", "after:", ou "before:" pour filtrer votre recherche, ex.: "revert author:Alice before:2019-01-13". commits.find=Chercher commits.search_all=Toutes les branches commits.author=Auteur @@ -1194,8 +1267,8 @@ commits.date=Date commits.older=Précédemment commits.newer=Récemment commits.signed_by=Signé par -commits.signed_by_untrusted_user=Signé par l'utilisateur non fiable -commits.signed_by_untrusted_user_unmatched=Signé par un utilisateur non fiable qui ne correspond pas à un auteur connu +commits.signed_by_untrusted_user=Signature provenant d'un utilisateur dilletant +commits.signed_by_untrusted_user_unmatched=Signature discordante de l'auteur de la révision et provenant d'un utilisateur dilletant commits.gpg_key_id=ID de la clé GPG commits.ssh_key_fingerprint=Empreinte numérique de la clé SSH @@ -1238,7 +1311,9 @@ projects.column.new_title=Nom projects.column.new_submit=Créer une colonne projects.column.new=Nouvelle colonne projects.column.set_default=Définir par défaut -projects.column.set_default_desc=Définir cette colonne par défaut pour les tickets et demande d'ajouts non catégorisés +projects.column.set_default_desc=Missionne cette colonne d'accueillir les tickets et demande d'ajouts non catégorisés. +projects.column.unset_default=Défaire par défaut +projects.column.unset_default_desc=Décharge cette colonne d'accueillir les tickets et demandes d'ajouts non catégorisées. Ceux-ci iront dans une colonne idoine. projects.column.delete=Supprimer la colonne projects.column.deletion_desc=La suppression d'une colonne de projet déplace tous les tickets liés à 'Non catégorisé'. Continuer ? projects.column.color=Couleur @@ -1254,32 +1329,33 @@ issues.filter_assignees=Filtrer par assignation issues.filter_milestones=Filtrer le jalon issues.filter_projects=Filtrer par projet issues.filter_labels=Filtrer une étiquette -issues.filter_reviewers=Filtrer par réviseur +issues.filter_reviewers=Filtrer par évaluateur issues.new=Nouveau ticket issues.new.title_empty=Le titre ne peut pas être vide issues.new.labels=Étiquettes -issues.new.no_label=Pas d'étiquette +issues.new.no_label=Sans étiquette issues.new.clear_labels=Effacer les étiquettes issues.new.projects=Projets issues.new.clear_projects=Effacer les projets -issues.new.no_projects=Pas de projet +issues.new.no_projects=Sans projet issues.new.open_projects=Projets ouverts issues.new.closed_projects=Projets clôturés issues.new.no_items=Pas d'élément issues.new.milestone=Jalon -issues.new.no_milestone=Aucun 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=Affecté à +issues.new.assignees=Assignés issues.new.clear_assignees=Supprimer les affectations -issues.new.no_assignees=Pas d'assignataires -issues.new.no_reviewers=Aucune évaluation +issues.new.no_assignees=Sans assignation +issues.new.no_reviewers=Sans évaluateur issues.choose.get_started=Démarrons issues.choose.open_external_link=Ouvrir issues.choose.blank=Par défaut issues.choose.blank_about=Créer un ticket à partir du modèle par défaut. issues.choose.ignore_invalid_templates=Les modèles invalides ont été ignorés +issues.choose.invalid_templates=%v modèle(s) invalide(s) trouvé(s) issues.choose.invalid_config=La configuration du ticket contient des erreurs : issues.no_ref=Aucune branche/étiquette spécifiées issues.create=Créer un ticket @@ -1291,30 +1367,33 @@ issues.label_templates.title=Charger un ensemble prédéfini d'étiquettes issues.label_templates.info=Il n'existe pas encore d'étiquettes. Créez une étiquette avec 'Nouvelle étiquette' ou utilisez un jeu d'étiquettes prédéfini : issues.label_templates.helper=Sélectionnez un ensemble d'étiquettes issues.label_templates.use=Utiliser le jeu de labels -issues.label_templates.fail_to_load_file=Impossible de charger le fichier de modèle de libellé "%s" : %v +issues.label_templates.fail_to_load_file=Impossible de charger le fichier de modèle étiquette "%s" : %v issues.add_label=a ajouté l'étiquette %s %s issues.add_labels=a ajouté les étiquettes %s %s issues.remove_label=a supprimé l'étiquette %s %s issues.remove_labels=a supprimé les étiquettes %s %s issues.add_remove_labels=a ajouté %s et supprimé les étiquettes %s %s -issues.add_milestone_at=`a ajouté cela au jalon %s %s` -issues.add_project_at=`a ajouté au projet %s %s` -issues.change_milestone_at=`a modifié le jalon de %s à %s %s` -issues.change_project_at=modification du projet de %s à %s %s -issues.remove_milestone_at=`a supprimé cela du jalon %s %s` -issues.remove_project_at=`supprimer du projet %s %s` -issues.deleted_milestone=`(supprimée)` +issues.add_milestone_at=`a ajouté ça au jalon %s %s.` +issues.add_project_at=`a ajouté ça au projet %s %s.` +issues.change_milestone_at=`a remplacé le jalon %s par %s %s.` +issues.change_project_at=`a remplacé le projet %s par %s %s.` +issues.remove_milestone_at=`a supprimé ça du jalon %s %s.` +issues.remove_project_at=`a supprimé ça du projet %s %s.` +issues.deleted_milestone=`(supprimé)` issues.deleted_project=`(supprimé)` -issues.self_assign_at=`s'est assigné cela %s` -issues.add_assignee_at=`s'est vu assigner cela par %s %s` -issues.remove_assignee_at=`mis en non assigné par %s %s` -issues.remove_self_assignment=`a retiré son assignation %s` -issues.change_title_at=`a modifié le titre %s pour %s %s` -issues.change_ref_at=`à modifiée la référence %s pour %s%s` -issues.delete_branch_at=`a supprimé la branche %s %s` +issues.self_assign_at=`s'est assigné ça %s.` +issues.add_assignee_at=`a été assigné par %s %s.` +issues.remove_assignee_at=`à été désassigné par %s %s.` +issues.remove_self_assignment=`s'est désassignée ça %s.` +issues.change_title_at=`a remplacé le titre %s par %s %s.` +issues.change_ref_at=`a remplacé la référence %s par %s%s.` +issues.remove_ref_at=`a supprimé la référence %s %s.` +issues.add_ref_at=`a ajouté la référence %s %s.` +issues.delete_branch_at=`a supprimé la branche %s %s.` issues.filter_label=Étiquette -issues.filter_label_exclude=`Utiliser alt + clic/entrée pour exclure les étiquettes` +issues.filter_label_exclude=`Utiliser Alt + Clic/entrée pour exclure les étiquettes` issues.filter_label_no_select=Toutes les étiquettes +issues.filter_label_select_no_label=Aucune étiquette issues.filter_milestone=Jalon issues.filter_milestone_all=Tous les jalons issues.filter_milestone_none=Aucun jalon @@ -1322,10 +1401,10 @@ issues.filter_milestone_open=Jalons ouverts issues.filter_milestone_closed=Jalons fermés issues.filter_project=Projet issues.filter_project_all=Tous les projets -issues.filter_project_none=Pas de projet +issues.filter_project_none=Aucun projet issues.filter_assignee=Assigné -issues.filter_assginee_no_select=Toutes les affectations -issues.filter_assginee_no_assignee=Pas d'assignataire +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 @@ -1333,13 +1412,13 @@ issues.filter_type.all_issues=Tous les tickets issues.filter_type.assigned_to_you=Qui vous sont assignés issues.filter_type.created_by_you=Créés par vous issues.filter_type.mentioning_you=Vous mentionnant -issues.filter_type.review_requested=Revue demandée -issues.filter_type.reviewed_by_you=Revu par vous +issues.filter_type.review_requested=Évaluation demandée +issues.filter_type.reviewed_by_you=Évaluée par vous issues.filter_sort=Trier issues.filter_sort.latest=Plus récent -issues.filter_sort.oldest=Plus ancien/ne +issues.filter_sort.oldest=Plus ancien issues.filter_sort.recentupdate=Mis à jour récemment -issues.filter_sort.leastupdate=Moins récemment mis à jour +issues.filter_sort.leastupdate=Mis à jour jadis issues.filter_sort.mostcomment=Les plus commentés issues.filter_sort.leastcomment=Les moins commentés issues.filter_sort.nearduedate=Date d'échéance la plus proche @@ -1348,6 +1427,7 @@ issues.filter_sort.moststars=Favoris (décroissant) issues.filter_sort.feweststars=Favoris (croissant) issues.filter_sort.mostforks=Bifurcations (décroissant) issues.filter_sort.fewestforks=Bifurcations (croissant) +issues.keyword_search_unavailable=La recherche par mot clé n'est pas disponible. Veuillez contacter l'administrateur de votre instance Gitea. issues.action_open=Ouvrir issues.action_close=Fermer issues.action_label=Étiquette @@ -1358,44 +1438,52 @@ issues.action_assignee_no_select=Pas d'assignataire issues.action_check=Cocher/Décocher issues.action_check_all=Cocher/Décocher tous les éléments issues.opened_by=créé %[1]s par %[3]s +pulls.merged_by=par %[3]s fusionné %[1]s. +pulls.merged_by_fake=par %[2]s fusionné %[1]s. +issues.closed_by=de %[3]s, clôt %[1]s issues.opened_by_fake=%[1]s ouvert par %[2]s -issues.previous=Page Précédente -issues.next=Page Suivante +issues.closed_by_fake=de %[2]s, clôt %[1]s +issues.previous=Précédent +issues.next=Suivant issues.open_title=Ouvert issues.closed_title=Fermé issues.draft_title=Brouillon +issues.num_comments_1=%d commentaire issues.num_comments=%d commentaires -issues.commented_at=`a commenté %s` +issues.commented_at=`a commenté %s.` issues.delete_comment_confirm=Êtes-vous certain de vouloir supprimer ce commentaire? issues.context.copy_link=Copier le lien issues.context.quote_reply=Citer et répondre issues.context.reference_issue=Référencer dans un nouveau ticket issues.context.edit=Éditer issues.context.delete=Supprimer +issues.no_content=Sans contenu. issues.close=Fermer le ticket +issues.comment_pull_merged_at=a fusionné la révision %[1]s dans %[2]s %[3]s +issues.comment_manually_pull_merged_at=a fusionné manuellement la révision %[1]s dans %[2]s %[3]s issues.close_comment_issue=Commenter et Fermer -issues.reopen_issue=Réouvrir +issues.reopen_issue=Rouvrir issues.reopen_comment_issue=Commenter et Réouvrir -issues.create_comment=Créer un commentaire -issues.closed_at=`a fermé ce ticket %[2]s` -issues.reopened_at=`a réouvert ce ticket %[2]s` -issues.commit_ref_at=`a référencé ce ticket depuis une révision %[2]s` -issues.ref_issue_from=`a référencé ce ticket %[4]s %[2]s` -issues.ref_pull_from=`a référencé cette pull request %[4]s %[2]s` -issues.ref_closing_from=`a référence une pull request %[4]s qui va fermer ce ticket %[2]s` +issues.create_comment=Commenter +issues.closed_at=`a fermé ce ticket %[2]s.` +issues.reopened_at=`a réouvert ce ticket %[2]s.` +issues.commit_ref_at=`a référencé ce ticket depuis une révision %[2]s.` +issues.ref_issue_from=`a fait référence à %[4]s ce ticket %[2]s.` +issues.ref_pull_from=`a fait référence à cette demande d'ajout %[4]s %[2]s.` +issues.ref_closing_from=`a fait référence à une demande d'ajout %[4]s qui clora ce ticket, %[2]s.` issues.ref_reopening_from=`a référencé une pull request %[4]s qui va réouvrir ce ticket %[2]s` issues.ref_closed_from=`a fermé ce ticket %[4]s %[2]s` -issues.ref_reopened_from=`a réouvert ce ticket %[4]s %[2]s` +issues.ref_reopened_from=`a rouvert ce ticket %[4]s %[2]s.` issues.ref_from=`de %[1]s` issues.poster=Éditeur issues.collaborator=Collaborateur issues.owner=Propriétaire -issues.re_request_review=Redemander la revue -issues.is_stale=Il y a eu des modifications à cette PR depuis cette révision -issues.remove_request_review=Retirer la demande de revue -issues.remove_request_review_block=Ne peut pas retirer la demande de revue -issues.dismiss_review=Rejeter la revue -issues.dismiss_review_warning=Êtes-vous sûr de vouloir rejeter la revue ? +issues.re_request_review=Redemander une évaluation +issues.is_stale=Cette demande d’ajout a été corrigée depuis sa dernière évaluation. +issues.remove_request_review=Retirer la demande d’évaluation +issues.remove_request_review_block=Impossible de retirer la demande d’évaluation +issues.dismiss_review=Révoquer l’évaluation +issues.dismiss_review_warning=Êtes-vous sûr de vouloir révoquer cette évaluation ? issues.sign_in_require_desc=Connectez-vous pour rejoindre cette conversation. issues.edit=Modifier issues.cancel=Annuler @@ -1404,8 +1492,10 @@ issues.label_title=Nom de l'étiquette issues.label_description=Description de l’étiquette issues.label_color=Couleur de l'étiquette issues.label_exclusive=Exclusif -issues.label_exclusive_desc=Nommez le libellé périmètre/élément pour qu'il soit mutuellement exclusif avec d'autres libellés du périmètre. -issues.label_exclusive_warning=Tout libellé conflictuel sera supprimé lors de l'édition des libellés d'un ticket ou d'une demande de tirage. +issues.label_archive=Archivé +issues.label_archive_tooltip=En archivant une étiquette, celle-ci devient inutilisable. Cependant, pour ne pas dégrader les tickets ou demandes d'ajouts, de telles étiquettes ne leur sont pas retirée. +issues.label_exclusive_desc=Remarque: Pour rendre des étiquettes mutuellement exclusives, préfixez leur nom avec une portée de votre choix de la façon suivante : portée/étiquette +issues.label_exclusive_warning=Toute étiquette d'une portée en conflit sera retirée lors de la modification des étiquettes d’un ticket ou d’une demande d’ajout. issues.label_count=%d étiquettes issues.label_open_issues=%d tickets ouverts issues.label_edit=Éditer @@ -1419,10 +1509,14 @@ issues.label.filter_sort.reverse_alphabetically=Par ordre alphabétique inversé issues.label.filter_sort.by_size=Plus petite taille issues.label.filter_sort.reverse_by_size=Plus grande taille issues.num_participants=%d participants -issues.attachment.open_tab=`Cliquez ici pour voir '%s' dans un nouvel onglet` -issues.attachment.download=`Cliquez pour télécharger "%s"` +issues.attachment.open_tab=`Cliquez ici pour voir « %s » dans un nouvel onglet.` +issues.attachment.download=`Cliquez pour télécharger « %s ».` issues.subscribe=S’abonner issues.unsubscribe=Se désabonner +issues.unpin_issue=Désépingler le ticket +issues.max_pinned=Vous ne pouvez pas épingler plus de tickets +issues.pin_comment=a épinglé ça %s. +issues.unpin_comment=a désépinglé ça %s. issues.lock=Verrouiller la conversation issues.unlock=Déverrouiller la conversation issues.lock.unknown_reason=Impossible de verrouiller un ticket avec une raison inconnue. @@ -1445,21 +1539,22 @@ 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=Suivi du temps -issues.start_tracking_short=Démarrer le suivi de temps +issues.tracker=Minuteur +issues.start_tracking_short=Démarrer la minuteuse issues.start_tracking=Démarrer le suivi du temps -issues.start_tracking_history=`a démarré il y a %s` -issues.tracker_auto_close=Le suivi de temps sera automatiquement arrêté quand le ticket sera fermé -issues.tracking_already_started=`Vous avez déjà commencé à suivre le temps sur un autre ticket!` -issues.stop_tracking=Arrêter le suivi de temps -issues.stop_tracking_history=`a fini de travaillé pour %s` -issues.cancel_tracking=Annuler -issues.add_time=Ajouter un minuteur manuellement -issues.del_time=Supprimer ce journal des temps -issues.add_time_short=Ajouter un minuteur +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.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=` temps passé ajouté %s` -issues.del_time_history=`a supprimé le temps passé %s` +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_hours=Heures issues.add_time_minutes=Minutes issues.add_time_sum_to_small=Aucun minuteur n'a été saisi. @@ -1471,12 +1566,13 @@ issues.error_modifying_due_date=Impossible de modifier l'échéance. issues.error_removing_due_date=Impossible de supprimer l'échéance. issues.push_commit_1=a ajouté %d révision %s issues.push_commits_n=a ajouté %d révisions %s -issues.force_push_codes=`a soumit de force %[1]s de %[2]s à %[4]s %[6]s.` +issues.force_push_codes=`a forcé la soumission de %[1]s depuis %[2]s à %[4]s %[6]s.` issues.force_push_compare=Comparer issues.due_date_form=aaaa-mm-jj issues.due_date_form_add=Ajouter une échéance issues.due_date_form_edit=Éditer issues.due_date_form_remove=Supprimer +issues.due_date_not_writer=Vous avez besoin d’un accès en écriture à ce dépôt pour modifier l’échéance de ses tickets. issues.due_date_not_set=Aucune échéance n'a été définie. issues.due_date_added=a ajouté l'échéance %s %s issues.due_date_modified=a modifié l'échéance de %[2]s à %[1]s %[3]s @@ -1493,13 +1589,14 @@ issues.dependency.add=Ajouter une dépendance… issues.dependency.cancel=Annuler issues.dependency.remove=Supprimer issues.dependency.remove_info=Supprimer cette dépendance -issues.dependency.added_dependency=`a ajouté une nouvelle dépendance %s` -issues.dependency.removed_dependency=`a supprimé une dépendance %s` +issues.dependency.added_dependency=`a créé une dépendance %s.` +issues.dependency.removed_dependency=`a supprimé une dépendance %s.` issues.dependency.pr_closing_blockedby=La fermeture de cette demande d’ajout est bloquée par les tickets suivants issues.dependency.issue_closing_blockedby=La fermeture de ce ticket est bloquée par les tickets suivants issues.dependency.issue_close_blocks=Cette demande d'ajout empêche la clôture des tickets suivants issues.dependency.pr_close_blocks=Cette demande d'ajout empêche la clôture des tickets suivants issues.dependency.issue_close_blocked=Vous devez fermer tous les tickets qui bloquent ce ticket avant de pouvoir le fermer. +issues.dependency.issue_batch_close_blocked=Impossible de fermer tous les tickets que vous avez choisis, car le ticket #%d a toujours des dépendances ouvertes. issues.dependency.pr_close_blocked=Vous devez fermer tous les tickets qui bloquent cette demande d'ajout avant de pouvoir la fusionner. issues.dependency.blocks_short=Bloque issues.dependency.blocked_by_short=Dépend de @@ -1516,28 +1613,31 @@ issues.dependency.add_error_dep_not_same_repo=Les deux tickets doivent être dan issues.review.self.approval=Vous ne pouvez approuver vos propres demandes d'ajout. issues.review.self.rejection=Vous ne pouvez demander de changements sur vos propres demandes de changement. issues.review.approve=ces changements ont été approuvés %s -issues.review.comment=révisé %s -issues.review.dismissed=a rejeté la revue de %s %s -issues.review.dismissed_label=Rejeté +issues.review.comment=a évalué cette demande d’ajout %s. +issues.review.dismissed=a révoqué l’évaluation de %s %s. +issues.review.dismissed_label=Révoquée issues.review.left_comment=laisser un commentaire issues.review.content.empty=Vous devez laisser un commentaire indiquant le(s) changement(s) demandé(s). issues.review.reject=a requis les changements %s -issues.review.wait=a été sollicité pour une révision %s -issues.review.add_review_request=a demandé une révision de %s %s -issues.review.remove_review_request=a supprimé la demande de révision pour %s %s -issues.review.remove_review_request_self=a refusé la revue %s +issues.review.wait=a été sollicité pour évaluer cette demande d’ajout %s. +issues.review.add_review_request=a demandé à %s une évaluation %s. +issues.review.remove_review_request=a retiré la demande d’évaluation pour %s %s. +issues.review.remove_review_request_self=a refusé d’évaluer cette demande d’ajout %s. issues.review.pending=En attente issues.review.pending.tooltip=Ce commentaire n'est pas encore visible par les autres utilisateurs. Pour soumettre vos commentaires en attente, sélectionnez "%s" → "%s/%s/%s" en haut de la page. -issues.review.review=Révision -issues.review.reviewers=Relecteurs +issues.review.review=Évaluation +issues.review.reviewers=Évaluateurs issues.review.outdated=Périmé +issues.review.outdated_description=Le contenu a changé depuis que ce commentaire a été fait. +issues.review.option.show_outdated_comments=Afficher les commentaires obsolètes +issues.review.option.hide_outdated_comments=Masquer les commentaires obsolètes issues.review.show_outdated=Afficher les révisions périmées issues.review.hide_outdated=Cacher les révisions périmées -issues.review.show_resolved=Montrer les résolus -issues.review.hide_resolved=Cacher les résolus -issues.review.resolve_conversation=Conversation résolue -issues.review.un_resolve_conversation=Conversation non résolue -issues.review.resolved_by=marquer cette conversation comme résolue +issues.review.show_resolved=Développer +issues.review.hide_resolved=Réduire +issues.review.resolve_conversation=Clore la conversation +issues.review.un_resolve_conversation=Rouvrir la conversation +issues.review.resolved_by=a marqué cette conversation comme résolue. issues.assignee.error=Tous les assignés n'ont pas été ajoutés en raison d'une erreur inattendue. issues.reference_issue.body=Corps issues.content_history.deleted=supprimé @@ -1551,16 +1651,16 @@ issues.reference_link=Référence : %s compare.compare_base=base compare.compare_head=comparer -pulls.desc=Activer les demandes de fusion et la revue de code. +pulls.desc=Active les demandes d’ajouts et l’évaluation du code. pulls.new=Nouvelle demande d'ajout pulls.view=Voir la demande d'ajout -pulls.compare_changes=Nouvelle demande de fusion +pulls.compare_changes=Nouvelle demande d’ajout pulls.allow_edits_from_maintainers=Autoriser les modifications des mainteneurs pulls.allow_edits_from_maintainers_desc=Les utilisateurs ayant un accès en écriture à la branche de base peuvent également soumettre sur cette branche pulls.allow_edits_from_maintainers_err=La mise à jour à échoué pulls.compare_changes_desc=Sélectionnez la branche dans laquelle fusionner et la branche depuis laquelle tirer les modifications. pulls.has_viewed_file=Consulté -pulls.has_changed_since_last_review=Modifiée depuis votre dernière revue +pulls.has_changed_since_last_review=Modifié depuis votre dernier passage pulls.viewed_files_label=%[1]d / %[2]d fichiers vus pulls.expand_files=Développer tous les fichiers pulls.collapse_files=Réduire tous les fichiers @@ -1570,13 +1670,20 @@ pulls.switch_comparison_type=Changer le type de comparaison pulls.switch_head_and_base=Passez de head à base pulls.filter_branch=Filtre de branche pulls.no_results=Aucun résultat trouvé. -pulls.nothing_to_compare=Ces branches sont identiques. Il n'y a pas besoin de créer une demande de fusion. +pulls.show_all_commits=Afficher toutes les révisions +pulls.show_changes_since_your_last_review=Affiche les modifications depuis votre dernière évaluation. +pulls.showing_only_single_commit=Affiche uniquement les changements de la révision %[1]s +pulls.showing_specified_commit_range=Affichage des changements filtré entre %[1]s..%[2]s +pulls.select_commit_hold_shift_for_range=Maintenir Maj et cliquer sur des révisions pour faire un intervalle +pulls.review_only_possible_for_full_diff=Une évaluation n'est possible que lorsque vous affichez le différentiel complet. +pulls.filter_changes_by_commit=Filtrer par révision +pulls.nothing_to_compare=Ces branches sont identiques. Il n’y a pas besoin de créer une demande d'ajout. pulls.nothing_to_compare_and_allow_empty_pr=Ces branches sont égales. Cette demande d'ajout sera vide. pulls.has_pull_request='Il existe déjà une demande d'ajout entre ces deux branches : %[2]s#%[3]d' pulls.create=Créer une demande d'ajout -pulls.title_desc=veut fusionner %[1]d révision(s) depuis %[2]s vers %[3]s +pulls.title_desc=souhaite fusionner %[1]d révision(s) depuis %[2]s vers %[3]s pulls.merged_title_desc=a fusionné %[1]d révision(s) à partir de %[2]s vers %[3]s %[4]s -pulls.change_target_branch_at=`a modifié la branche cible %s pour %s %s` +pulls.change_target_branch_at=`a remplacée la branche cible %s par %s %s.` pulls.tab_conversation=Discussion pulls.tab_commits=Révisions pulls.tab_files=Fichiers Modifiés @@ -1587,13 +1694,13 @@ pulls.merged_success=Demande d’ajout fusionnée et fermée avec succès pulls.closed=Demande d’ajout fermée pulls.manually_merged=Fusionné manuellement pulls.merged_info_text=La branche %s peut maintenant être supprimée. -pulls.is_closed=La demande de fusion a été fermée. +pulls.is_closed=La demande d’ajout a été fermée. pulls.title_wip_desc=`Préfixer le titre par %s pour empêcher cette demande d'ajout d'être fusionnée par erreur.` pulls.cannot_merge_work_in_progress=Cette demande d'ajout est marquée comme en cours de chantier. pulls.still_in_progress=Toujours en cours ? pulls.add_prefix=Ajouter le préfixe %s pulls.remove_prefix=Enlever le préfixe %s -pulls.data_broken=Cette demande de fusion est impossible par manque d'informations de bifurcation. +pulls.data_broken=Cette demande d’ajout est impossible par manque d'informations de bifurcation. pulls.files_conflicted=Cette demande d'ajout contient des modifications en conflit avec la branche ciblée. pulls.is_checking=Vérification des conflits de fusion en cours. Réessayez dans quelques instants. pulls.is_ancestor=Cette branche est déjà présente dans la branche ciblée. Il n'y a rien à fusionner. @@ -1601,8 +1708,14 @@ pulls.is_empty=Les changements sur cette branche sont déjà sur la branche cibl pulls.required_status_check_failed=Certains contrôles requis n'ont pas réussi. pulls.required_status_check_missing=Certains contrôles requis sont manquants. pulls.required_status_check_administrator=En tant qu'administrateur, vous pouvez toujours fusionner cette requête de pull. +pulls.blocked_by_approvals=Cette demande d'ajout n'a pas encore suffisamment été approuvée. %d approbations obtenues sur %d. +pulls.blocked_by_rejection=Cette demande d’ajout nécessite des corrections sollicitées par un évaluateur officiel. +pulls.blocked_by_official_review_requests=Cette demande d’ajout a des sollicitations officielles d’évaluation. +pulls.blocked_by_outdated_branch=Cette demande d’ajout est bloquée car elle est obsolète. +pulls.blocked_by_changed_protected_files_1=Cette demande d'ajout est bloquée car elle modifie un fichier protégé : +pulls.blocked_by_changed_protected_files_n=Cette demande d'ajout est bloquée car elle modifie des fichiers protégés : pulls.can_auto_merge_desc=Cette demande d'ajout peut être fusionnée automatiquement. -pulls.cannot_auto_merge_desc=Cette demande de fusion ne peut être appliquée automatiquement en raison de conflits de fusion. +pulls.cannot_auto_merge_desc=Cette demande d’ajout ne peut être fusionnée automatiquement en raison de conflits. pulls.cannot_auto_merge_helper=Fusionner manuellement pour résoudre les conflits. pulls.num_conflicting_files_1=%d fichier en conflit pulls.num_conflicting_files_n=%d fichiers en conflit @@ -1610,14 +1723,14 @@ pulls.approve_count_1=%d approuvé pulls.approve_count_n=%d approuvés pulls.reject_count_1=%d changement requis pulls.reject_count_n=%d changements requis -pulls.waiting_count_1=%d en attente de revue -pulls.waiting_count_n=%d en attente de revues +pulls.waiting_count_1=%d évaluation en attente +pulls.waiting_count_n=%d évaluations en attente pulls.wrong_commit_id=l'ID de la révision doit être un ID de révision sur la branche cible -pulls.no_merge_desc=Cette demande de fusion ne peut être appliquée directement car toutes les options de fusion du dépôt sont désactivées. +pulls.no_merge_desc=Cette demande d’ajout ne peut être fusionnée car toutes les options de fusion du dépôt sont désactivées. pulls.no_merge_helper=Activez des options de fusion dans les paramètres du dépôt ou fusionnez la demande manuellement. pulls.no_merge_wip=Cette demande d'ajout ne peut pas être fusionnée car elle est marquée comme en cours de chantier. -pulls.no_merge_not_ready=Cette demande d'ajout n'est pas prête à être fusionnée, vérifiez l'état de la revue et les vérifications. +pulls.no_merge_not_ready=Cette demande d’ajout n’est pas prête à être fusionnée, vérifiez les évaluations en cours et le contrôle qualité. pulls.no_merge_access=Vous n'êtes pas autorisé⋅e à fusionner cette demande d'ajout. pulls.merge_pull_request=Créer une révision de fusion pulls.rebase_merge_pull_request=Rebaser puis avancer rapidement @@ -1630,13 +1743,15 @@ pulls.require_signed_wont_sign=La branche nécessite des révisions signées mai pulls.invalid_merge_option=Vous ne pouvez pas utiliser cette option de fusion pour cette demande. pulls.merge_conflict=Échec de la fusion : il y a eu un conflit lors de la fusion. Indice : Essayez une autre stratégie pulls.merge_conflict_summary=Message d'erreur -pulls.rebase_conflict=Fusion échouée : il y a eu un conflit lors du rebase du commit: %[1]s. Astuce : Essayez une stratégie différente +pulls.rebase_conflict=Fusion échouée : il y a eu un conflit lors du rebasage de la révision %[1]s. Astuce : Essayez une stratégie différente pulls.rebase_conflict_summary=Message d'erreur pulls.unrelated_histories=Échec de la fusion: La tête de fusion et la base ne partagent pas d'historique commun. Indice : Essayez une stratégie différente pulls.merge_out_of_date=Échec de la fusion: La base a été mise à jour en cours de fusion. Indice : Réessayez. +pulls.head_out_of_date=Échec de la fusion : L’en-tête a été mis à jour pendant la fusion. Conseil : réessayez. +pulls.push_rejected=Échec de la fusion : la soumission a été rejetée. Revoyez les déclencheurs Git pour ce dépôt. pulls.push_rejected_summary=Message de rejet complet -pulls.push_rejected_no_message=Échec de la fusion : La soumission a été rejetée sans raison.
Revoyez les Déclencheurs de ce dépot -pulls.open_unmerged_pull_exists=`Vous ne pouvez pas ré-ouvrir cette demande de fusion car il y a une demande de fusion (#%d) en attente avec des propriétés identiques.` +pulls.push_rejected_no_message=Échec de la fusion : la soumission a été rejetée sans raison. Revoyez les déclencheurs Git pour ce dépôt. +pulls.open_unmerged_pull_exists=`Vous ne pouvez pas rouvrir ceci car la demande d’ajout #%d, en attente, a des propriétés identiques.` pulls.status_checking=Certains contrôles sont en attente pulls.status_checks_success=Tous les contrôles ont réussi pulls.status_checks_warning=Quelques vérifications ont signalé des avertissements @@ -1650,14 +1765,16 @@ pulls.update_branch_success=La mise à jour de la branche a réussi pulls.update_not_allowed=Vous n'êtes pas autorisé à mettre à jour la branche pulls.outdated_with_base_branch=Cette branche est désynchronisée avec la branche de base pulls.close=Fermer la demande d’ajout -pulls.closed_at=`a fermé cette pull request %[2]s` -pulls.reopened_at=`a réouvert cette pull request %[2]s` +pulls.closed_at=`a fermé cette demande d'ajout %[2]s.` +pulls.reopened_at=`a rouvert cette demande d'ajout %[2]s.` pulls.merge_instruction_hint=`Vous pouvez également voir les instructions en ligne de commande.` pulls.merge_instruction_step1_desc=Depuis le dépôt de votre projet, sélectionnez une nouvelle branche et testez les modifications. pulls.merge_instruction_step2_desc=Fusionner les modifications et mettre à jour sur Gitea. pulls.clear_merge_message=Effacer le message de fusion +pulls.clear_merge_message_hint=Effacer le message de fusion ne supprimera que le message de la révision, mais pas les pieds de révision générés tels que "Co-Authored-By:". pulls.auto_merge_button_when_succeed=(Lorsque les vérifications ont réussi) +pulls.auto_merge_when_succeed=Fusionner automatiquement si toutes les vérifications passent. pulls.auto_merge_newly_scheduled=La demande d'ajout était programmée pour fusionner lorsque toutes les vérifications aurait réussi. pulls.auto_merge_has_pending_schedule=%[1]s Ont planifié cette demande d'ajout pour fusionner automatiquement lorsque toutes les vérifications réussissent %[2]s. @@ -1665,13 +1782,15 @@ pulls.auto_merge_cancel_schedule=Annuler la fusion automatique pulls.auto_merge_not_scheduled=Cette demande d'ajout n'est pas planifiée pour fusionner automatiquement. pulls.auto_merge_canceled_schedule=La fusion automatique a été annulée pour cette demande d'ajout. -pulls.auto_merge_newly_scheduled_comment=`a programmé cette demande de fusion automatique lorsque toutes les vérifications réussissent %[1]s` -pulls.auto_merge_canceled_schedule_comment=`a annulé la fusion automatique de cette demande d'ajout lorsque toutes les vérifications réussissent %[1]s` +pulls.auto_merge_newly_scheduled_comment=`a programmé la fusion automatique de cette demande d’ajout, si toutes les vérifications passent, %[1]s.` +pulls.auto_merge_canceled_schedule_comment=`a annulé la fusion automatique de cette demande d'ajout %[1]s.` 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 +pull.deleted_branch=(supprimé) : %s milestones.new=Nouveau jalon milestones.closed=%s fermé @@ -1679,6 +1798,7 @@ milestones.update_ago=Actualisé il y a %s milestones.no_due_date=Aucune date d'échéance milestones.open=Ouvrir milestones.close=Fermer +milestones.new_subheader=Les jalons peuvent vous aider à organiser vos tickets et à suivre leurs progrès. milestones.completeness=%d%% complété milestones.create=Créer un Jalon milestones.title=Titre @@ -1702,6 +1822,19 @@ milestones.filter_sort.most_complete=Le plus complété milestones.filter_sort.most_issues=Le plus de tickets milestones.filter_sort.least_issues=Le moins de tickets +signing.will_sign=Cette révision sera signée avec la clé « %s ». +signing.wont_sign.error=Impossible de vérifier la signature de la révision. +signing.wont_sign.nokey=Aucune clé n’est disponible pour signer cette révision. +signing.wont_sign.never=Les révisions ne sont jamais signées. +signing.wont_sign.always=Les révisions sont toujours signées. +signing.wont_sign.pubkey=La révision ne sera pas signée car vous votre compte ne possède pas de clé publique. +signing.wont_sign.twofa=Vous devez activer l'authentification à deux facteurs pour signer vos révisions. +signing.wont_sign.parentsigned=Cette révision ne sera pas signée car son parent n’est pas signée. +signing.wont_sign.basesigned=La fusion ne sera pas signée car la première révision n’est pas signée. +signing.wont_sign.headsigned=La fusion ne sera pas signée car la dernière révision n’est pas signée. +signing.wont_sign.commitssigned=La fusion ne sera pas signée car ses révisions ne sont pas signées. +signing.wont_sign.approved=La fusion ne sera pas signée car la demande d'ajout n'a pas été approuvée. +signing.wont_sign.not_signed_in=Vous n'êtes pas connecté. ext_wiki=Accès au wiki externe ext_wiki.desc=Lier un wiki externe. @@ -1730,6 +1863,7 @@ wiki.page_already_exists=Une page de wiki avec le même nom existe déjà. wiki.reserved_page=Le nom de page de wiki "%s" est réservé. wiki.pages=Pages wiki.last_updated=Dernière mise à jour: %s +wiki.page_name_desc=Entrez un nom pour cette page Wiki. Certains noms spéciaux sont « Home », « _Sidebar » et « _Footer ». wiki.original_git_entry_tooltip=Voir le fichier Git original au lieu d'utiliser un lien convivial. activity=Activité @@ -1768,7 +1902,7 @@ activity.closed_issue_label=Fermé activity.new_issues_count_1=Nouveau ticket activity.new_issues_count_n=Nouveaux tickets activity.new_issue_label=Ouvert -activity.title.unresolved_conv_1=%d conversations non résolues +activity.title.unresolved_conv_1=%d conversation non résolue activity.title.unresolved_conv_n=%d conversations non résolues activity.unresolved_conv_desc=Ces tickets et demandes de fusion récemment mis à jour n'ont pas encore été résolus. activity.unresolved_conv_label=Ouvrir @@ -1825,14 +1959,24 @@ settings.mirror_settings=Réglages Miroir settings.mirror_settings.docs=Configurez votre dépôt pour synchroniser automatiquement les révisions, étiquettes et branches avec un autre dépôt. settings.mirror_settings.docs.disabled_pull_mirror.instructions=Configurez votre projet pour soumettre automatiquement les révisions, étiquettes et branches vers un autre dépôt. Les miroirs ont été désactivés par l'administrateur de votre site. settings.mirror_settings.docs.disabled_push_mirror.instructions=Configurez votre projet pour synchroniser automatiquement les révisions, étiquettes et branches d'un autre dépôt. +settings.mirror_settings.docs.disabled_push_mirror.pull_mirror_warning=Pour l’instant, cela ne peut être fait que dans le menu « Nouvelle migration ». Pour plus d’informations, veuillez consulter : +settings.mirror_settings.docs.disabled_push_mirror.info=Les miroirs push ont été désactivés par l’administrateur de votre site. +settings.mirror_settings.docs.no_new_mirrors=Votre dépôt se synchronise avec un dépôt distant. Vous ne pouvez pas créer de nouveaux miroirs pour le moment. +settings.mirror_settings.docs.can_still_use=Bien que vous ne puissiez pas modifier les miroirs ou en créer de nouveaux, vous pouvez toujours utiliser le(s) miroir(s) existant(s). +settings.mirror_settings.docs.pull_mirror_instructions=Pour configurer un miroir pull, veuillez consulter : +settings.mirror_settings.docs.more_information_if_disabled=Vous pouvez en savoir plus sur les miroirs push et pull ici : settings.mirror_settings.docs.doc_link_title=Comment mettre en miroir les dépôts ? +settings.mirror_settings.docs.doc_link_pull_section=la section « Pulling from a remote repository » de la documentation. settings.mirror_settings.docs.pulling_remote_title=Tirer depuis un dépôt distant settings.mirror_settings.mirrored_repository=Dépôt en miroir settings.mirror_settings.direction=Direction settings.mirror_settings.direction.pull=Tirer settings.mirror_settings.direction.push=Soumission settings.mirror_settings.last_update=Dernière mise à jour +settings.mirror_settings.push_mirror.none=Aucun miroir push configuré settings.mirror_settings.push_mirror.remote_url=URL du dépôt distant Git +settings.mirror_settings.push_mirror.add=Ajouter un miroir push +settings.mirror_settings.push_mirror.edit_sync_time=Modifier la fréquence de réflexion settings.sync_mirror=Synchroniser maintenant settings.mirror_sync_in_progress=La synchronisation est en cours. Revenez dans une minute. @@ -1902,6 +2046,7 @@ settings.transfer.rejected=Le transfert du dépôt a été rejeté. settings.transfer.success=Le transfert du dépôt a réussi. settings.transfer_abort=Annuler le transfert settings.transfer_abort_invalid=Vous ne pouvez pas annuler un transfert de dépôt inexistant. +settings.transfer_abort_success=Le transfert du dépôt vers %s a bien été stoppé. settings.transfer_desc=Transférer ce dépôt à un autre utilisateur ou une organisation dont vous possédez des droits d'administrateur. settings.transfer_form_title=Entrez le nom du dépôt pour confirmer : settings.transfer_in_progress=Il y a actuellement un transfert en cours. Veuillez l'annuler si vous souhaitez transférer ce dépôt à un autre utilisateur. @@ -1915,16 +2060,16 @@ settings.transfer_succeed=Le dépôt a été transféré. settings.signing_settings=Paramètres de vérification de la signature settings.trust_model=Niveau de confiance settings.trust_model.default=Par défaut -settings.trust_model.default.desc=Utiliser le niveau de confiance par défaut pour ce dépôt. +settings.trust_model.default.desc=Utiliser le niveau de confiance configuré par défaut pour cette instance Gitea. settings.trust_model.collaborator=Collaborateur -settings.trust_model.collaborator.long=Collaborateur : ne se fier qu'aux signatures correspondant à des collaborateurs -settings.trust_model.collaborator.desc=La signature d'une révision doit correspondre à celle d'un collaborateur du dépôt pour être « fiable » (indépendamment de l'auteur de la révision). Si elle correspond à un autre utilisateur de Gitea, elle sera « non fiable », et sinon sera « non reconnue ». +settings.trust_model.collaborator.long=Collaborateur : ne se fier qu'aux signatures des collaborateurs du dépôt +settings.trust_model.collaborator.desc=La signature d'une révision est dite « fiable » si elle correspond à un collaborateur du dépôt, indépendamment de son auteur. À défaut, si elle correspond à l'auteur de la révision, elle sera « dilettante », et « discordante » sinon. settings.trust_model.committer=Auteur -settings.trust_model.committer.long=Auteur : ne se fier qu'aux signatures correspondant à des utilisateurs de Gitea (imite GitHub en forçant Gitea cosigner ses propres révisions) -settings.trust_model.committer.desc=La signature d'une révision doit correspondre à celle d'un utilisateur de Gitea pour être « fiable », autrement elle est « non-reconnue ». Pour les révisions déléguées à Gitea, elles seront signées par Gitea et l'utilisateur sera crédité "Co-authored-by:" et "Co-committed-by:" en pied de révision. Pour cela, la clé par défaut de Gitea doit correspondre à un utilisateur dans la base de données. +settings.trust_model.committer.long=Auteur : ne se fier qu'aux signatures des auteurs des révisions (mimique GitHub en forçant Gitea à co-signer ses révisions) +settings.trust_model.committer.desc=La signature d'une révision est dite « fiable » si elle corresponds à son auteur, autrement elle est « discordante ». Pour les révisions déléguées à Gitea, elles seront signées par Gitea et l'auteur original sera crédité "Co-authored-by:" et "Co-committed-by:" en pied de révision. Pour cela, la clé configurée par défaut de Gitea doit correspondre à celle d'un utilisateur. settings.trust_model.collaboratorcommitter=Collaborateur et Auteur -settings.trust_model.collaboratorcommitter.long=Collaborateur et Auteur : ne se fier qu'aux signatures des auteurs identifiés comme collaborateurs -settings.trust_model.collaboratorcommitter.desc=La signature et l'auteur d'une révision doivent correspondre strictement à un collaborateur du dépôt pour être « fiable ». S'ils correspondent à un autre utilisateur de Gitea, elle sera « non-fiable », et sinon « non-reconnue ». Pour les révisions déléguées à Gitea, elles seront signées par Gitea et l'utilisateur sera crédité "Co-authored-by:" et "Co-committed-by:" en pied de révision. Pour cela, la clé par défaut de Gitea doit correspondre à un utilisateur dans la base de données. +settings.trust_model.collaboratorcommitter.long=Collaborateur et Auteur : ne se fier qu'aux signatures des auteurs collaborant au dépôt +settings.trust_model.collaboratorcommitter.desc=La signature d'une révision est dite « fiable » si elle correponds à l'auteur collaborant au dépôt. Elle est « dilettante » si elle ne correponds qu'à l'auteur, et autrement « discordante ». Pour les révisions déléguées à Gitea, elles seront signées par Gitea et l'auteur original sera crédité "Co-authored-by:" et "Co-committed-by:" en pied de révision. Pour cela, la clé configurée par défaut de Gitea doit correspondre à celle d'un utilisateur. settings.wiki_delete=Supprimer les données du Wiki settings.wiki_delete_desc=Supprimer les données du wiki d'un dépôt est permanent. Cette action est irréversible. settings.wiki_delete_notices_1=- Ceci supprimera de manière permanente et désactivera le wiki de dépôt pour %s. @@ -1974,6 +2119,7 @@ settings.webhook.headers=Entêtes settings.webhook.payload=Contenu settings.webhook.body=Corps settings.webhook.replay.description=Rejouer ce déclencheur. +settings.webhook.delivery.success=Un événement a été ajouté à la file d'attente. Cela peut prendre quelques secondes avant qu'il n'apparaisse dans l'historique de livraison. settings.githooks_desc=Les déclencheurs Git sont lancés par Git lui-même. Ils sont modifiables dans la liste ci-dessous afin de configurer des opérations personnalisées. settings.githook_edit_desc=Si un Hook est inactif, un exemple de contenu vous sera proposé. Un contenu laissé vide signifie un Hook inactif. settings.githook_name=Nom du Hook @@ -2003,42 +2149,45 @@ settings.event_fork_desc=Dépôt bifurqué. settings.event_wiki=Wiki settings.event_wiki_desc=Page wiki créée, renommée, modifiée ou supprimée. settings.event_release=Version -settings.event_release_desc=Version publiée, mise à jour ou supprimée dans un dépôt. -settings.event_push=Poussée -settings.event_push_desc=Git push vers un dépôt. +settings.event_release_desc=Version publiée, mise à jour ou supprimée. +settings.event_push=Soumission +settings.event_push_desc=Soumission Git. settings.event_repository=Dépôt settings.event_repository_desc=Dépôt créé ou supprimé. -settings.event_header_issue=Événements des tickets -settings.event_issues=Tickets -settings.event_issues_desc=Ticket ouvert, fermé, ré-ouvert ou modifié. -settings.event_issue_assign=Ticket assigné -settings.event_issue_assign_desc=Ticket assigné ou non assigné. -settings.event_issue_label=Étiquettes des tickets -settings.event_issue_label_desc=Étiquettes de ticket mises à jour ou effacées. -settings.event_issue_milestone=Ticket jalonnée +settings.event_header_issue=Événements de ticket +settings.event_issues=Ticket +settings.event_issues_desc=Ticket ouvert, rouvert, fermé ou modifié. +settings.event_issue_assign=Assignation +settings.event_issue_assign_desc=Ticket assigné ou dé-assigné. +settings.event_issue_label=Étiquetage +settings.event_issue_label_desc=Étiquette attribuée ou retirée. +settings.event_issue_milestone=Jalon settings.event_issue_milestone_desc=Ticket jalonné ou dé-jalonné. -settings.event_issue_comment=Commentaire du ticket -settings.event_issue_comment_desc=Commentaire du ticket créé, modifié, ou supprimé. +settings.event_issue_comment=Commentaire +settings.event_issue_comment_desc=Commentaire créé, modifié ou supprimé. settings.event_header_pull_request=Événements de demande d'ajout settings.event_pull_request=Demande d'ajout -settings.event_pull_request_desc=Demande d'ajout ouverte, fermée, réouverte ou modifiée. -settings.event_pull_request_assign=Demande d'ajout assignée +settings.event_pull_request_desc=Demande d’ajout ouverte, rouverte, fermée ou modifiée. +settings.event_pull_request_assign=Assignation settings.event_pull_request_assign_desc=Demande d'ajout assignée ou non assignée. -settings.event_pull_request_label=Demande d'ajout étiquetée -settings.event_pull_request_label_desc=Étiquettes de la demande d'ajout mises à jour ou effacées. -settings.event_pull_request_milestone=Demande d'ajout jalonnée +settings.event_pull_request_label=Étiquetage +settings.event_pull_request_label_desc=Étiquette attribuée ou retirée. +settings.event_pull_request_milestone=Jalon settings.event_pull_request_milestone_desc=Demande d'ajout jalonnée ou dé-jalonnée. -settings.event_pull_request_comment=Commentaire sur la demande d'ajout -settings.event_pull_request_comment_desc=Commentaire de la demande d'ajout créé, modifié ou supprimé. -settings.event_pull_request_review=Demande d'ajout révisée -settings.event_pull_request_review_desc=Demande d'ajout approvée, rejetée ou commentaire de révision. -settings.event_pull_request_sync=Demande d'ajout synchronisée +settings.event_pull_request_comment=Commentaire +settings.event_pull_request_comment_desc=Commentaire créé, modifié ou supprimé. +settings.event_pull_request_review=Évaluation +settings.event_pull_request_review_desc=Demande d’ajout approuvée, rejetée ou commentée. +settings.event_pull_request_sync=Synchronisation settings.event_pull_request_sync_desc=Demande d'ajout synchronisée. +settings.event_pull_request_review_request=Demande d’évaluation +settings.event_pull_request_review_request_desc=Création ou suppresion de demandes d’évaluation. settings.event_pull_request_approvals=Approbations de demande d'ajout settings.event_pull_request_merge=Fusion de demande d'ajout settings.event_package=Paquet +settings.event_package_desc=Paquet créé ou supprimé. settings.branch_filter=Filtre de branche -settings.branch_filter_desc=Liste blanche pour la soumission sur, création et suppression de branches, spécifiée par un glob. Si vide ou *, les événements pour toutes les branches sont prit en compte. Syntaxe détaillée sur github.com/gobwas/glob. Exemples: master, {master,release*}. +settings.branch_filter_desc=Liste de branches et motifs globs autorisant la soumission, la création et suppression de branches. Laisser vide ou utiliser * englobent toutes les branches. Voir la syntaxe Glob. Exemples : master, {master,release*}. settings.authorization_header=En-tête « Authorization » settings.authorization_header_desc=Si présent, sera ajouté aux requêtes comme en-tête d’authentification. Exemples : %s. settings.active=Actif @@ -2090,8 +2239,8 @@ settings.protected_branch.delete_rule=Supprimer la règle settings.protected_branch_can_push=Autoriser la soumission ? settings.protected_branch_can_push_yes=Vous pouvez pousser settings.protected_branch_can_push_no=Vous ne pouvez pas pousser -settings.branch_protection=`Protection de la branche "%s"` -settings.protect_this_branch=Protection de la branche +settings.branch_protection=Paramètres de protection pour les branches du motif %s +settings.protect_this_branch=Activer la protection de branche settings.protect_this_branch_desc=Empêche les suppressions et limite les poussées et fusions sur cette branche. settings.protect_disable_push=Désactiver la soumission settings.protect_disable_push_desc=Aucune soumission ne sera possible sur cette branche. @@ -2111,25 +2260,29 @@ settings.protect_merge_whitelist_committers_desc=N'autoriser que les utilisateur settings.protect_merge_whitelist_users=Utilisateurs en liste blanche de fusion : settings.protect_merge_whitelist_teams=Équipes en liste blanche de fusion : settings.protect_check_status_contexts=Activer le Contrôle Qualité +settings.protect_status_check_patterns=Schémas de vérification des statuts : +settings.protect_status_check_patterns_desc=Entrez des schémas pour spécifier quelles vérifications doivent réussir avant que des branches puissent être fusionnées. Un schéma par ligne. Un schéma ne peuvent être vide. settings.protect_check_status_contexts_desc=Exiger le status « succès » avant de fusionner. Quand activée, une branche protégée ne peux accepter que des soumissions ou des fusions ayant le status « succès ». Lorsqu'il n'y a pas de contexte, la dernière révision fait foi. settings.protect_check_status_contexts_list=Contrôles qualité trouvés au cours de la semaine dernière pour ce dépôt settings.protect_status_check_matched=Correspondant -settings.protect_required_approvals=Agréments nécessaires : -settings.protect_required_approvals_desc=Permettre uniquement de fusionner les demandes d'ajout avec suffisamment de commentaires positifs. +settings.protect_invalid_status_check_pattern=Shéma de contrôle de status incorrect : "%s". +settings.protect_no_valid_status_check_patterns=Aucun schéma de contrôle de statut valide. +settings.protect_required_approvals=Minimum d'approbations requis : +settings.protect_required_approvals_desc=Permet de fusionner les demandes d’ajout lorsque suffisamment d’évaluation sont positives. settings.protect_approvals_whitelist_enabled=Restreindre les approbations aux utilisateurs ou aux équipes en liste blanche -settings.protect_approvals_whitelist_enabled_desc=Seuls les avis des utilisateurs ou des équipes sur la liste autorisée compteront pour les approbations requises. Sans liste blanche d'approbation, les avis de toute personne ayant un accès en écriture aux approbations requises. -settings.protect_approvals_whitelist_users=Réviseurs sur liste blanche : -settings.protect_approvals_whitelist_teams=Équipes en liste blanche pour les révisions : -settings.dismiss_stale_approvals=Rejeter les approbations obsolètes -settings.dismiss_stale_approvals_desc=Quand de nouvelles révisions qui changent le contenu de la demande d'ajout sont soumises vers la branche, les anciennes approbations seront révoquées. +settings.protect_approvals_whitelist_enabled_desc=Seuls les évaluations des utilisateurs ou des équipes suivantes compteront dans les approbations requises. Si laissé vide, les évaluations de toute personne ayant un accès en écriture seront comptabilisées à la place. +settings.protect_approvals_whitelist_users=Évaluateurs autorisés : +settings.protect_approvals_whitelist_teams=Équipes d’évaluateurs autorisés : +settings.dismiss_stale_approvals=Révoquer automatiquement les approbations périmées +settings.dismiss_stale_approvals_desc=Lorsque des nouvelles révisions changent le contenu de la demande d’ajout, les approbations existantes sont révoquées. settings.require_signed_commits=Exiger des révisions signées settings.require_signed_commits_desc=Rejeter les soumissions sur cette branche lorsqu'ils ne sont pas signés ou vérifiables. settings.protect_branch_name_pattern=Motif de nom de branche protégé settings.protect_patterns=Motifs -settings.protect_protected_file_patterns=Fichier protégés -settings.protect_protected_file_patterns_desc=Liste de fichiers et de motifs, séparés par un point-virgule, qui ne pourront pas être modifiés même si les utilisateurs disposent des droits sur la branche. Syntaxe détaillée sur github.com/gobwas/glob. Exemples: .drone.yml, /docs/**/*.txt. -settings.protect_unprotected_file_patterns=Fichiers non protégés -settings.protect_unprotected_file_patterns_desc=Liste de fichiers et de motifs, séparés par un point-virgule, qui pourront être modifiés malgré la protection de branche, par les utilisateurs autorisés. Syntaxe détaillée sur github.com/gobwas/glob. Exemples: .drone.yml, /docs/**/*.txt. +settings.protect_protected_file_patterns=Liste des fichiers et motifs protégés +settings.protect_protected_file_patterns_desc=Liste de fichiers et de motifs, séparés par un point-virgule « ; », qui ne pourront pas être modifiés même si les utilisateurs disposent des droits sur la branche. Voir la syntaxe glob. Exemples : .drone.yml ; /docs/**/*.txt. +settings.protect_unprotected_file_patterns=Liste des fichiers et motifs exclus +settings.protect_unprotected_file_patterns_desc=Liste de fichiers et de motifs globs, séparés par un point-virgule « ; », qui pourront être modifiés malgré la protection de branche, par les utilisateurs autorisés. Voir la syntaxe Glob. Exemples : .drone.yml ; /docs/**/*.txt. settings.add_protected_branch=Activer la protection settings.delete_protected_branch=Désactiver la protection settings.update_protect_branch_success=La règle de protection de branche "%s" a été mise à jour. @@ -2137,10 +2290,10 @@ settings.remove_protected_branch_success=La règle de protection de branche "%s" settings.remove_protected_branch_failed=Impossible de retirer la règle de protection de branche "%s". settings.protected_branch_deletion=Désactiver la protection de branche settings.protected_branch_deletion_desc=Désactiver la protection de branche permet aux utilisateurs ayant accès en écriture de pousser des modifications sur la branche. Continuer ? -settings.block_rejected_reviews=Bloquer la fusion quand il y a des avis de rejet -settings.block_rejected_reviews_desc=La fusion ne sera pas possible lorsque des modifications sont demandées par les réviseurs officiels, même si les approbations sont suffisantes. -settings.block_on_official_review_requests=Bloquer la fusion en cas de demande de revue officielle -settings.block_on_official_review_requests_desc=La fusion ne sera pas possible quand elle aura des demandes de revues officielles, même si le nombre d'approbations est suffisant. +settings.block_rejected_reviews=Bloquer la fusion en cas d’évaluations négatives +settings.block_rejected_reviews_desc=La fusion ne sera pas possible lorsque des modifications sont demandées par les évaluateurs officiels, même s'il y a suffisamment d’approbations. +settings.block_on_official_review_requests=Bloquer la fusion en cas de demande d’évaluation officielle +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.default_branch_desc=Sélectionnez une branche par défaut pour les demandes de fusion et les révisions : @@ -2164,21 +2317,28 @@ settings.tags.protection.none=Il n'y a pas d'étiquettes protégées. settings.tags.protection.pattern.description=Vous pouvez utiliser soit un nom unique, soit un motif de glob ou une expression régulière qui correspondront à plusieurs étiquettes. Pour plus d'informations, veuillez vous reporter au guide sur les étiquettes protégées. settings.bot_token=Jeton de Bot settings.chat_id=ID de conversation +settings.thread_id=ID du fil settings.matrix.homeserver_url=URL du serveur d'accueil settings.matrix.room_id=ID de la salle settings.matrix.message_type=Type de message settings.archive.button=Archiver ce dépôt settings.archive.header=Archiver ce dépôt +settings.archive.text=Archiver un dépôt le place en lecture seule et le cache des tableaux de bord. Personne ne pourra faire de nouvelles révisions, d'ouvrir des tickets ou des demandes d'ajouts (pas même vous!). settings.archive.success=Ce dépôt a été archivé avec succès. settings.archive.error=Une erreur s'est produite lors de l'archivage du dépôt. Voir le journal pour plus de détails. settings.archive.error_ismirror=Vous ne pouvez pas archiver un dépôt en miroir. settings.archive.branchsettings_unavailable=Le paramétrage des branches n'est pas disponible quand le dépôt est archivé. settings.archive.tagsettings_unavailable=Le paramétrage des étiquettes n'est pas disponible si le dépôt est archivé. +settings.unarchive.button=Réhabiliter +settings.unarchive.header=Réhabiliter ce dépôt +settings.unarchive.text=Réhabiliter un dépôt dégèle les actions de révisions et de soumissions, la gestion des tickets et des demandes d'ajouts. +settings.unarchive.success=Le dépôt a bien été réhabilité. +settings.unarchive.error=Une erreur est survenue en essayant deréhabiliter ce dépôt. Voir le journal pour plus de détails. settings.update_avatar_success=L'avatar du dépôt a été mis à jour. settings.lfs=LFS settings.lfs_filelist=Fichiers LFS stockés dans ce dépôt settings.lfs_no_lfs_files=Aucun fichier LFS stocké dans ce dépôt -settings.lfs_findcommits=Trouver des commits +settings.lfs_findcommits=Trouver des révisions settings.lfs_lfs_file_no_commits=Aucune révision trouvée pour ce fichier LFS settings.lfs_noattribute=Ce chemin n'a pas l'attribut verrouillable dans la branche par défaut settings.lfs_delete=Supprimer le fichier LFS possédant l'OID %s @@ -2240,21 +2400,22 @@ diff.show_more=Voir plus diff.load=Voir la Diff 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=Mise en page avec markdown est prise en charge. -diff.comment.add_single_comment=Ajouter un commentaire -diff.comment.add_review_comment=Ajouter un commentaire -diff.comment.start_review=Démarrer la révision +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 diff.comment.reply=Répondre -diff.review=Révision -diff.review.header=Soumettre une révision -diff.review.placeholder=Commentaire de révision +diff.review=Évaluation +diff.review.header=Évaluer +diff.review.placeholder=Commenter cette évaluation diff.review.comment=Commenter diff.review.approve=Approuver diff.review.self_reject=Les auteurs d’une demande d’ajout ne peuvent pas demander des changements sur leur propre demande d’ajout diff.review.reject=Demander des changements diff.review.self_approve=Les auteurs d’une demande d’ajout ne peuvent pas approuver leur propre demande d’ajout -diff.committed_by=commité par +diff.committed_by=révisé par diff.protected=Protégé diff.image.side_by_side=Côte à côte diff.image.swipe=Glisser @@ -2296,6 +2457,7 @@ release.edit_release=Actualiser la version release.delete_release=Supprimer cette version release.delete_tag=Supprimer l'étiquette release.deletion=Supprimer cette version +release.deletion_desc=Supprimer une version ne la supprime que de Gitea. Cela n’affectera pas les étiquettes Git, le contenu de votre dépôt ou son historique. Continuer ? release.deletion_success=Cette livraison a été supprimée. release.deletion_tag_desc=Ceci supprimera cette étiquette du dépôt. Le contenu du dépôt et l'historique resteront inchangés. Continuer ? release.deletion_tag_success=L'étiquette a été supprimée. @@ -2316,6 +2478,7 @@ branch.already_exists=Une branche nommée "%s" existe déjà. branch.delete_head=Supprimer branch.delete=`Supprimer la branche "%s"` branch.delete_html=Supprimer la branche +branch.delete_desc=La suppression d’une branche est permanente. Bien qu’une branche supprimée puisse temporairement subsister, elle NE PEUT PAS être facilement restaurée. Continuer ? branch.deletion_success=La branche "%s" a été supprimée. branch.deletion_failed=Impossible de supprimer la branche "%s". branch.delete_branch_has_new_commits=La branche "%s" ne peut être supprimé, car de nouvelles révisions ont été ajoutées après la fusion. @@ -2355,6 +2518,7 @@ tag.create_success=L'étiquette "%s" a été créée. topic.manage_topics=Gérer les sujets topic.done=Terminé topic.count_prompt=Vous ne pouvez pas sélectionner plus de 25 sujets +topic.format_prompt=Les sujets doivent commencer par un caractère alphanumérique, peuvent inclure des traits d’union « - » et des points « . », et mesurer jusqu'à 35 caractères. Les lettres doivent être en minuscules. find_file.go_to_file=Aller au fichier find_file.no_matching=Aucun fichier correspondant trouvé @@ -2393,6 +2557,7 @@ form.create_org_not_allowed=Vous n'êtes pas autorisé à créer une organisatio settings=Paramètres settings.options=Organisation settings.full_name=Non Complet +settings.email=Courriel de contact settings.website=Site Web settings.location=Localisation settings.permission=Autorisations @@ -2406,6 +2571,7 @@ settings.visibility.private_shortname=Privé settings.update_settings=Appliquer les paramètres settings.update_setting_success=Les paramètres de l'organisation ont été mis à jour. +settings.change_orgname_prompt=Remarque : Changer le nom de l'organisation changera également l'URL de votre organisation et libèrera l'ancien nom. settings.change_orgname_redirect_prompt=L'ancien nom d'utilisateur redirigera jusqu'à ce qu'il soit réclamé. settings.update_avatar_success=L'avatar de l'organisation a été mis à jour. settings.delete=Supprimer l'organisation @@ -2481,23 +2647,28 @@ teams.all_repositories_helper=L'équipe a accès à tous les dépôts. Sélectio teams.all_repositories_read_permission_desc=Cette équipe accorde l'accès en lecture à tous les dépôts : les membres peuvent voir et cloner les dépôts. teams.all_repositories_write_permission_desc=Cette équipe accorde l'accès en écriture à tous les dépôts : les membres peuvent lire et écrire dans les dépôts. teams.all_repositories_admin_permission_desc=Cette équipe accorde l'accès administrateur à tous les dépôts : les membres peuvent lire, écrire dans et ajouter des collaborateurs aux dépôts. +teams.invite.title=Vous avez été invité à rejoindre l'équipe %s dans l'organisation %s. teams.invite.by=Invité par %s teams.invite.description=Veuillez cliquer sur le bouton ci-dessous pour rejoindre l’équipe. [admin] dashboard=Tableau de bord +identity_access=Identité et accès users=Comptes utilisateurs organizations=Organisations +assets=Ressources de code repositories=Dépôts hooks=Déclencheurs web +integrations=Intégrations authentication=Sources d'authentification -emails=Courriels de l'utilisateur +emails=Emails de l'utilisateur config=Configuration notices=Informations monitor=Surveillance first_page=Première last_page=Dernière total=Total : %d +settings=Paramètres administrateur dashboard.new_version_hint=Gitea %s est maintenant disponible, vous utilisez %s. Consultez le blog pour plus de détails. dashboard.statistic=Résumé @@ -2524,13 +2695,17 @@ dashboard.delete_repo_archives.started=Tâche de suppression de toutes les archi dashboard.delete_missing_repos=Supprimer tous les dépôts dont les fichiers Git sont manquants dashboard.delete_missing_repos.started=Tâche de suppression de tous les dépôts sans fichiers Git démarrée. dashboard.delete_generated_repository_avatars=Supprimer les avatars de dépôt générés +dashboard.sync_repo_branches=Synchroniser les branches manquantes depuis Git vers la base de donnée. dashboard.update_mirrors=Actualiser les miroirs dashboard.repo_health_check=Vérifier l'état de santé de tous les dépôts dashboard.check_repo_stats=Voir les statistiques de tous les dépôts dashboard.archive_cleanup=Supprimer les archives des vieux dépôts dashboard.deleted_branches_cleanup=Nettoyer les branches supprimées -dashboard.git_gc_repos=Collecter les déchets des dépôts +dashboard.update_migration_poster_id=Actualiser les ID des affiches de migration +dashboard.git_gc_repos=Exécuter le ramasse-miette des dépôts +dashboard.resync_all_sshkeys=Mettre à jour le fichier « ssh/authorized_keys » avec les clés SSH Gitea. dashboard.resync_all_sshkeys.desc=(Inutile pour le serveur SSH intégré.) +dashboard.resync_all_sshprincipals=Mettre à jour le fichier « .ssh/authorized_principals » avec les principaux de Gitea SSH. dashboard.resync_all_sshprincipals.desc=(Inutile pour le serveur SSH intégré.) dashboard.resync_all_hooks=Re-synchroniser les déclencheurs Git pre-receive, update et post-receive de tous les dépôts. dashboard.reinit_missing_repos=Réinitialiser tous les dépôts Git manquants pour lesquels un enregistrement existe @@ -2570,9 +2745,11 @@ dashboard.delete_old_actions=Supprimer toutes les anciennes actions de la base d dashboard.delete_old_actions.started=Suppression de toutes les anciennes actions de la base de données démarrée. dashboard.update_checker=Vérificateur de mise à jour dashboard.delete_old_system_notices=Supprimer toutes les anciennes observations de la base de données +dashboard.gc_lfs=Épousseter les métaobjets LFS dashboard.stop_zombie_tasks=Arrêter les tâches zombies dashboard.stop_endless_tasks=Arrêter les tâches sans fin dashboard.cancel_abandoned_jobs=Annuler les jobs abandonnés +dashboard.sync_branch.started=Début de la synchronisation des branches users.user_manage_panel=Gestion du compte utilisateur users.new_account=Créer un compte @@ -2628,15 +2805,15 @@ users.list_status_filter.not_prohibit_login=Autorisé à se connecter users.list_status_filter.is_2fa_enabled=2FA Activé users.list_status_filter.not_2fa_enabled=2FA désactivé -emails.email_manage_panel=Gestion des courriels des utilisateurs +emails.email_manage_panel=Gestion des emails des utilisateurs emails.primary=Principale emails.activated=Activée emails.filter_sort.email=Courriel -emails.filter_sort.email_reverse=Courriel (inverse) +emails.filter_sort.email_reverse=Courriel (inversé) emails.filter_sort.name=Nom d'utilisateur emails.filter_sort.name_reverse=Nom d'utilisateur (inverse) emails.updated=Courriel mis à jour -emails.not_updated=Impossible de mettre à jour l'adresse courriel demandée : %v +emails.not_updated=Impossible de mettre à jour l’adresse courriel demandée : %v emails.duplicate_active=Cette adresse courriel est déjà active pour un autre utilisateur. emails.change_email_header=Mettre à jour les propriétés du courriel emails.change_email_text=Êtes-vous sûr de vouloir mettre à jour cette adresse courriel ? @@ -2648,7 +2825,8 @@ orgs.members=Membres orgs.new_orga=Nouvelle organisation repos.repo_manage_panel=Gestion des dépôts -repos.unadopted=Dépôts non adoptés +repos.unadopted=Dépôts dépossédés +repos.unadopted.no_more=Aucun dépôt dépossédé trouvé. repos.owner=Propriétaire repos.name=Nom repos.private=Privé @@ -2657,10 +2835,12 @@ repos.stars=Votes repos.forks=Bifurcations repos.issues=Tickets repos.size=Taille +repos.lfs_size=Taille LFS packages.package_manage_panel=Gestion des paquets packages.total_size=Taille totale : %s packages.unreferenced_size=Taille non référencée : %s +packages.cleanup=Purger les données expirées packages.owner=Propriétaire packages.creator=Créateur packages.name=Nom @@ -2712,6 +2892,11 @@ auths.filter=Filtre utilisateur auths.admin_filter=Filtre administrateur auths.restricted_filter=Filtre restrictif auths.restricted_filter_helper=Laisser vide pour ne définir aucun utilisateur comme restreint. Utilisez un astérisque ('*') pour définir tous les utilisateurs qui ne correspondent pas au filtre Admin comme restreint. +auths.verify_group_membership=Vérifier l’appartenance au groupe LDAP (laisser vide pour ignorer) +auths.group_search_base=DN de recherche du groupe +auths.group_attribute_list_users=Attribut de groupe contenant la liste des utilisateurs +auths.user_attribute_in_group=Attribut utilisateur listé dans le groupe +auths.map_group_to_team=Associer les groupes LDAP aux équipes d'organisation (laissez vide pour ignorer) auths.map_group_to_team_removal=Retirer les utilisateurs des équipes synchronisées si l'utilisateur n'appartient pas au groupe LDAP correspondant auths.enable_ldap_groups=Activer les groupes LDAP auths.ms_ad_sa=Rechercher les attributs MS AD @@ -2741,9 +2926,15 @@ auths.oauth2_emailURL=URL de l'e-mail auths.skip_local_two_fa=Ignorer l’authentification à deux facteurs locale auths.skip_local_two_fa_helper=Laisser indéfini signifie que les utilisateurs locaux avec l’authentification à deux facteurs activée devront tout de même s’y soumettre pour se connecter auths.oauth2_tenant=Locataire +auths.oauth2_scopes=Champs d'application supplémentaires auths.oauth2_required_claim_name=Nom de réclamation requis auths.oauth2_required_claim_name_helper=Définissez ce nom pour restreindre la connexion depuis cette source aux utilisateurs ayant une réclamation avec ce nom auths.oauth2_required_claim_value=Valeur de réclamation requise +auths.oauth2_required_claim_value_helper=Restreindre la connexion depuis cette source aux utilisateurs ayant réclamé cette valeur. +auths.oauth2_group_claim_name=Réclamer le nom fournissant les noms de groupe pour cette source. (facultatif) +auths.oauth2_admin_group=Valeur de réclamation de groupe pour les administrateurs. (Optionnel, nécessite un nom de réclamation) +auths.oauth2_restricted_group=Valeur de réclamation de groupe pour les utilisateurs restreints. (Optionnel, nécessite un nom de réclamation) +auths.oauth2_map_group_to_team=Associe les groupes réclamés avec les équipes de l'organisation. (Optionnel, nécessite un nom de réclamation) auths.oauth2_map_group_to_team_removal=Supprimer les utilisateurs des équipes synchronisées si l'utilisateur n'appartient pas au groupe correspondant. auths.enable_auto_register=Connexion Automatique auths.sspi_auto_create_users=Créer automatiquement des utilisateurs @@ -2758,6 +2949,7 @@ auths.sspi_default_language=Langue par défaut de l'utilisateur auths.sspi_default_language_helper=Langue par défaut pour les utilisateurs créés automatiquement par la méthode d'authentification SSPI. Laissez vide si vous préférez que la langue soit déterminée automatiquement. auths.tips=Conseils auths.tips.oauth2.general=Authentification OAuth2 +auths.tips.oauth2.general.tip=Lors de l'enregistrement d'une nouvelle authentification OAuth2, l'URL de rappel/redirection doit être : auths.tip.oauth2_provider=Fournisseur OAuth2 auths.tip.bitbucket=`Créez un nouveau jeton OAuth sur https://bitbucket.org/account/user//oauth-consumers/new et ajoutez la permission "Compte"-"Lecture"` auths.tip.nextcloud=`Enregistrez un nouveau consommateur OAuth sur votre instance en utilisant le menu "Paramètres -> Sécurité -> Client OAuth 2.0"` @@ -2799,6 +2991,7 @@ config.disable_router_log=Désactiver la Journalisation du Routeur config.run_user=Exécuter avec l'utilisateur config.run_mode=Mode d'Éxécution config.git_version=Version de Git +config.app_data_path=Chemin App Data config.repo_root_path=Emplacement des Dépôts config.lfs_root_path=Répertoire racine LFS config.log_file_root_path=Chemin des fichiers logs @@ -2955,6 +3148,7 @@ monitor.queue.numberinqueue=Position dans la queue monitor.queue.review=Revoir la configuration monitor.queue.review_add=Réviser/Ajouter des processus monitor.queue.settings.title=Paramètres du réservoir +monitor.queue.settings.desc=Les bassins croissent proportionnellement au besoin de leurs exécuteurs. monitor.queue.settings.maxnumberworkers=Nombre maximale de processus monitor.queue.settings.maxnumberworkers.placeholder=Actuellement %[1]d monitor.queue.settings.maxnumberworkers.error=Le nombre de processus doit être un nombre @@ -2991,6 +3185,7 @@ reopen_pull_request=`a réouvert la demande d'ajout %[3]s#%[2]s< comment_issue=`a commenté le ticket %[3]s#%[2]s` comment_pull=`a commenté la demande d'ajout %[3]s#%[2]s` merge_pull_request=`a fusionné la demande d'ajout %[3]s#%[2]s` +auto_merge_pull_request=`a fusionné automatiquement la demande d’ajout %[3]s#%[2]s.` transfer_repo=a transféré le dépôt %s à %s push_tag=a poussé l'étiquette %[3]s vers %[4]s delete_tag=étiquette supprimée %[2]s de %[3]s @@ -2998,11 +3193,17 @@ delete_branch=branche %[2]s supprimée de %[3]s compare_branch=Comparer compare_commits=Comparer %d révisions compare_commits_general=Comparer les révisions +mirror_sync_push=a synchronisé les révisions de %[3]s d’un miroir vers %[4]s. +mirror_sync_create=a synchronisé la nouvelle référence %[3]s d’un miroir vers %[4]s. mirror_sync_delete=a synchronisé puis supprimé la nouvelle référence %[2]s vers %[3]s depuis le miroir -approve_pull_request=`a approuvé %[3]s#%[2]s` +approve_pull_request=`a approuvé %[3]s#%[2]s.` reject_pull_request=`a suggérés des changements pour %[3]s#%[2]s` publish_release=`a publié "%[4]s" à %[3]s` +review_dismissed=`a révoqué l’évaluation de %[4]s sur %[3]s#%[2]s.` review_dismissed_reason=Raison : +create_branch=a créé la branche %[3]s dans %[4]s. +starred_repo=est fan de %[2]s. +watched_repo=observe %[2]s. [tool] now=maintenant @@ -3036,7 +3237,7 @@ unread=Non lue(s) read=Lue(s) no_unread=Aucune notification non lue. no_read=Aucune notification lue. -pin=Epingler la notification +pin=Épingler la notification mark_as_read=Marquer comme lu mark_as_unread=Marquer comme non lue mark_all_as_read=Tout marquer comme lu @@ -3049,7 +3250,7 @@ default_key=Signé avec la clé par défaut error.extract_sign=Impossible d'extraire la signature error.generate_hash=Impossible de générer la chaine de hachage de la révision error.no_committer_account=Aucun compte lié à l'adresse e-mail de l'auteur -error.no_gpg_keys_found=Aucune clé connue n'a été trouvée dans la base pour cette signature +error.no_gpg_keys_found=Signature inconnue de Gitea error.not_signed_commit=Révision non signée error.failed_retrieval_gpg_keys=Impossible de récupérer la clé liée au compte de l'auteur error.probable_bad_signature=AVERTISSEMENT ! Bien qu'il y ait une clé avec cet ID dans la base de données, il ne vérifie pas cette livraison ! Cette livraison est SUSPECTE. @@ -3066,6 +3267,7 @@ desc=Gérer les paquets du dépôt. empty=Il n'y pas de paquet pour le moment. empty.documentation=Pour plus d'informations sur le registre de paquets, voir la documentation. empty.repo=Avez-vous téléchargé un paquet, mais il n'est pas affiché ici? Allez dans les paramètres du paquet et liez le à ce dépôt. +registry.documentation=Pour plus d’informations sur le registre %s, voir la documentation. filter.type=Type filter.type.all=Tous filter.no_result=Votre filtre n'affiche aucun résultat. @@ -3089,12 +3291,15 @@ versions=Versions versions.view_all=Voir tout dependency.id=ID dependency.version=Version +alpine.registry=Configurez ce registre en ajoutant l’URL dans votre fichier /etc/apk/repositories : +alpine.registry.key=Téléchargez la clé RSA publique du registre dans le dossier /etc/apk/keys/ pour vérifier la signature de l'index : alpine.registry.info=Choisissez $branch et $repository dans la liste ci-dessous. alpine.install=Pour installer le paquet, exécutez la commande suivante : alpine.repository=Informations sur le Dépôt alpine.repository.branches=Branches alpine.repository.repositories=Dépôts alpine.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 : cargo.details.repository_site=Site du dépôt cargo.details.documentation_site=Site de documentation @@ -3107,6 +3312,8 @@ composer.dependencies.development=Dépendances de développement conan.details.repository=Dépôt conan.registry=Configurez ce registre à partir d'un terminal : conan.install=Pour installer le paquet en utilisant Conan, exécutez la commande suivante : +conda.registry=Configurez ce registre en tant que dépôt Conda dans le fichier .condarc : +conda.install=Pour installer le paquet en utilisant Conda, exécutez la commande suivante : conda.details.repository_site=Site du dépôt conda.details.documentation_site=Site de documentation container.details.type=Type d'image @@ -3118,13 +3325,17 @@ container.layers=Calques d'image container.labels=Étiquettes container.labels.key=Clé container.labels.value=Valeur +cran.registry=Configurez ce registre dans le fichier Rprofile.site : cran.install=Pour installer le paquet, exécutez la commande suivante : debian.registry=Configurez ce registre à partir d'un terminal : +debian.registry.info=Choisissez $distribution et $component dans la liste ci-dessous. debian.install=Pour installer le paquet, exécutez la commande suivante : debian.repository=Infos sur le Dépôt +debian.repository.distributions=Distributions debian.repository.components=Composants debian.repository.architectures=Architectures generic.download=Télécharger le paquet depuis un terminal : +go.install=Installer le paquet à partir de la ligne de commande : helm.registry=Configurer ce registre à partir d'un terminal : helm.install=Pour installer le paquet, exécutez la commande suivante : maven.registry=Configurez ce registre dans le fichier pom.xml de votre projet : @@ -3146,6 +3357,8 @@ pub.install=Pour installer le paquet en utilisant Dart, exécutez la commande su pypi.requires=Nécessite Python pypi.install=Pour installer le paquet en utilisant pip, exécutez la commande suivante : rpm.registry=Configurez ce registre à partir d'un terminal : +rpm.distros.redhat=sur les distributions basées sur RedHat +rpm.distros.suse=sur les distributions basées sur SUSE rpm.install=Pour installer le paquet, exécutez la commande suivante : rubygems.install=Pour installer le paquet en utilisant gem, exécutez la commande suivante : rubygems.install2=ou ajoutez-le au Gemfile : @@ -3170,14 +3383,17 @@ settings.delete.success=Le paquet a été supprimé. settings.delete.error=Impossible de supprimer le paquet. owner.settings.cargo.title=Index du Registre Cargo owner.settings.cargo.initialize=Initialiser l'index +owner.settings.cargo.initialize.description=Un dépôt Git d’index spécial est nécessaire pour utiliser le registre Cargo. Utiliser cette option va (re)créer le dépôt et le configurer automatiquement. owner.settings.cargo.initialize.error=Impossible d'initialiser l'index de Cargo : %v owner.settings.cargo.initialize.success=L'index Cargo a été créé avec succès. owner.settings.cargo.rebuild=Reconstruire l'index +owner.settings.cargo.rebuild.description=La reconstruction peut être utile si l'index n'est pas synchronisé avec les paquets Cargo stockés. owner.settings.cargo.rebuild.error=Impossible de reconstruire l'index Cargo : %v owner.settings.cargo.rebuild.success=L'index Cargo a été reconstruit avec succès. owner.settings.cleanuprules.title=Gérer les règles de nettoyage owner.settings.cleanuprules.add=Ajouter une règle de nettoyage owner.settings.cleanuprules.edit=Modifier la règle de nettoyage +owner.settings.cleanuprules.none=Aucune règle de nettoyage disponible. Veuillez consulter la documentation. owner.settings.cleanuprules.preview=Aperçu des règles de nettoyage owner.settings.cleanuprules.preview.overview=%d paquets sont programmés pour être supprimés. owner.settings.cleanuprules.preview.none=La règle de nettoyage ne correspond à aucun paquet. @@ -3196,6 +3412,7 @@ owner.settings.cleanuprules.success.update=La règle de nettoyage a été mise owner.settings.cleanuprules.success.delete=La règle de nettoyage a été supprimée. owner.settings.chef.title=Dépôt Chef owner.settings.chef.keypair=Générer une paire de clés +owner.settings.chef.keypair.description=Une paire de clés est nécessaire pour s'authentifier au registre Chef. Si vous avez déjà généré une paire de clés, la génération d'une nouvelle paire de clés supprimera l'ancienne. [secrets] secrets=Secrets @@ -3222,6 +3439,7 @@ status.waiting=En attente status.running=En cours d'exécution status.success=Succès status.failure=Échec +status.cancelled=Annulé status.skipped=Ignoré status.blocked=Bloqué @@ -3238,10 +3456,11 @@ runners.labels=Étiquettes runners.last_online=Dernière fois en ligne runners.runner_title=Exécuteur runners.task_list=Tâches récentes sur cet exécuteur +runners.task_list.no_tasks=Il n'y a pas de tâche ici. runners.task_list.run=Exécuter runners.task_list.status=Statut runners.task_list.repository=Dépôt -runners.task_list.commit=Commit +runners.task_list.commit=Révision runners.task_list.done_at=Fait à runners.edit_runner=Éditer l'Exécuteur runners.update_runner=Appliquer les modifications @@ -3251,6 +3470,7 @@ runners.delete_runner=Supprimer cet exécuteur runners.delete_runner_success=Exécuteur supprimé avec succès runners.delete_runner_failed=Impossible de supprimer l'Exécuteur runners.delete_runner_header=Êtes-vous sûr de vouloir supprimer cet exécuteur ? +runners.delete_runner_notice=Si une tâche est en cours sur cet exécuteur, elle sera terminée et marquée comme échouée. Cela risque d’interrompre le flux de travail. runners.none=Aucun exécuteur disponible runners.status.unspecified=Inconnu runners.status.idle=Inactif @@ -3259,13 +3479,40 @@ runners.status.offline=Hors-ligne runners.version=Version runners.reset_registration_token_success=Le jeton d’inscription de l’exécuteur a été réinitialisé avec succès -runs.commit=Commit +runs.all_workflows=Tous les flux de travail +runs.commit=Révision +runs.pushed_by=soumis par +runs.invalid_workflow_helper=La configuration du flux de travail est invalide. Veuillez vérifier votre fichier %s. runs.no_matching_runner_helper=Aucun exécuteur correspondant : %s +runs.actor=Acteur runs.status=Statut +runs.actors_no_select=Tous les acteurs +runs.status_no_select=Touts les statuts +runs.no_results=Aucun résultat correspondant. +runs.no_runs=Le flux de travail n'a pas encore d'exécution. +workflow.disable=Désactiver le flux de travail +workflow.disable_success=Le flux de travail « %s » a bien été désactivé. +workflow.enable=Activer le flux de travail +workflow.enable_success=Le flux de travail « %s » a bien été activé. -need_approval_desc=Besoin d'approbation pour exécuter des workflows pour une demande de fusion de bifurcation. +need_approval_desc=Besoin d’approbation pour exécuter des flux de travail pour une demande d’ajout de bifurcation. +variables=Variables +variables.management=Gestion des variables +variables.creation=Ajouter une variable +variables.none=Il n'y a pas encore de variables. +variables.deletion=Retirer la variable +variables.deletion.description=La suppression d’une variable est permanente et ne peut être défaite. Continuer ? +variables.description=Les variables sont passées aux actions et ne peuvent être lues autrement. +variables.id_not_exist=La variable numéro %d n’existe pas. +variables.edit=Modifier la variable +variables.deletion.failed=Impossible de retirer la variable. +variables.deletion.success=La variable a bien été retirée. +variables.creation.failed=Impossible d'ajouter la variable. +variables.creation.success=La variable « %s » a été ajoutée. +variables.update.failed=Impossible d’éditer la variable. +variables.update.success=La variable a bien été modifiée. [projects] type-1.display_name=Projet personnel @@ -3273,5 +3520,10 @@ type-2.display_name=Projet de dépôt type-3.display_name=Projet d’organisation [git.filemode] +changed_filemode=%[1]s → %[2]s +directory=Dossier +normal_file=Fichier normal +executable_file=Fichier exécutable symbolic_link=Lien symbolique +submodule=Sous-module diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index 51480b7e62..07670c678c 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -4,6 +4,7 @@ explore=エクスプローラー help=ヘルプ logo=ロゴ sign_in=サインイン +sign_in_with_provider=%s でサインイン sign_in_or=または sign_out=サインアウト sign_up=登録 @@ -91,6 +92,7 @@ edit=編集 enabled=有効 disabled=無効 +locked=ロック済み copy=コピー copy_url=URLをコピー @@ -128,7 +130,9 @@ concept_user_organization=組織 show_timestamps=タイムスタンプを表示 show_log_seconds=秒数を表示 show_full_screen=フルスクリーン表示 +download_logs=ログをダウンロード +confirm_delete_selected=選択したすべてのアイテムを削除してよろしいですか? name=名称 value=値 @@ -221,6 +225,7 @@ repo_path_helper=リモートGitリポジトリはこのディレクトリに保 lfs_path=Git LFSルートパス lfs_path_helper=Git LFSで管理するファイルが、このディレクトリに保存されます。 空欄にするとGit LFSを無効にします。 run_user=実行ユーザー名 +run_user_helper=オペレーティングシステム上のユーザー名です。 Giteaをこのユーザーとして実行します。 このユーザーはリポジトリルートパスへのアクセス権を持っている必要があります。 domain=サーバードメイン domain_helper=サーバーのドメインまたはホストアドレス。 ssh_port=SSHサーバーのポート @@ -292,6 +297,8 @@ invalid_password_algorithm=無効なパスワードハッシュアルゴリズ password_algorithm_helper=パスワードハッシュアルゴリズムを設定します。 アルゴリズムにより動作要件と強度が異なります。 argon2アルゴリズムはかなり安全ですが、多くのメモリを使用するため小さなシステムには適さない場合があります。 enable_update_checker=アップデートチェッカーを有効にする enable_update_checker_helper=gitea.ioに接続して定期的に新しいバージョンのリリースを確認します。 +env_config_keys=環境設定 +env_config_keys_prompt=以下の環境変数も設定ファイルに適用されます: [home] uname_holder=ユーザー名またはメールアドレス @@ -407,6 +414,7 @@ authorize_application_created_by=このアプリケーションは %s が作成 authorize_application_description=アクセスを許可すると、このアプリケーションは、プライベート リポジトリや組織を含むあなたのすべてのアカウント情報に対して、アクセスと書き込みができるようになります。 authorize_title=`"%s"にあなたのアカウントへのアクセスを許可しますか?` authorization_failed=認可失敗 +authorization_failed_desc=無効なリクエストを検出したため認可が失敗しました。 認可しようとしたアプリの開発者に連絡してください。 sspi_auth_failed=SSPI認証に失敗しました password_pwned=あなたが選択したパスワードは、過去の情報漏洩事件で流出した盗まれたパスワードのリストに含まれています。 別のパスワードでもう一度試してください。 また他の登録でもこのパスワードからの変更を検討してください。 password_pwned_err=HaveIBeenPwnedへのリクエストを完了できませんでした @@ -594,6 +602,8 @@ user_bio=経歴 disabled_public_activity=このユーザーはアクティビティ表示を公開していません。 email_visibility.limited=あなたのメールアドレスはすべての認証済みユーザーに表示されています email_visibility.private=あなたのメールアドレスは、あなたと管理者のみに表示されます +show_on_map=地図上にこの場所を表示 +settings=ユーザー設定 form.name_reserved=ユーザー名 "%s" は予約されています。 form.name_pattern_not_allowed=`"%s" の形式はユーザー名に使用できません。` @@ -620,6 +630,7 @@ webauthn=セキュリティキー public_profile=公開プロフィール biography_placeholder=自己紹介してください!(Markdownを使うことができます) +location_placeholder=おおよその場所を他の人と共有 profile_desc=あなたのプロフィールが他のユーザーにどのように表示されるかを制御します。あなたのプライマリメールアドレスは、通知、パスワードの回復、WebベースのGit操作に使用されます。 password_username_disabled=非ローカルユーザーのユーザー名は変更できません。詳細はサイト管理者にお問い合わせください。 full_name=フルネーム @@ -632,6 +643,8 @@ update_language_not_found=言語 "%s" は利用できません。 update_language_success=言語が更新されました。 update_profile_success=プロフィールを更新しました。 change_username=ユーザー名を変更しました。 +change_username_prompt=注意: ユーザー名を変更するとアカウントのURLも変更されます。 +change_username_redirect_prompt=古いユーザー名は、誰かが再使用するまではリダイレクトします。 continue=続行 cancel=キャンセル language=言語 @@ -656,6 +669,7 @@ comment_type_group_project=プロジェクト comment_type_group_issue_ref=イシューの参照先 saved_successfully=設定は正常に保存されました。 privacy=プライバシー +keep_activity_private=プロフィールページのアクティビティ表示を隠す keep_activity_private_popup=アクティビティを、あなたと管理者にのみ表示します lookup_avatar_by_mail=メールアドレスでアバターを見つける @@ -689,6 +703,7 @@ requires_activation=アクティベーションが必要 primary_email=プライマリーにする activate_email=アクティベーションを送信 activations_pending=アクティベーション待ち +can_not_add_email_activations_pending=保留中のアクティベーションがあります。新しいメールを追加する場合は、数分後にもう一度お試しください。 delete_email=削除 email_deletion=メールアドレスの削除 email_deletion_desc=メールアドレスと関連情報をアカウントから削除します。 このメールアドレスを使ったGitのコミットはそのまま残ります。 続行しますか? @@ -807,8 +822,10 @@ repo_and_org_access=リポジトリと組織へのアクセス permissions_public_only=公開のみ permissions_access_all=すべて (公開、プライベート、限定) select_permissions=許可の選択 -permission_no_access=アクセスなし -permission_read=既読 +permission_no_access=アクセス不可 +permission_read=読み取り +permission_write=読み取りと書き込み +access_token_desc=選択したトークン権限に応じて、関連するAPIルートのみに許可が制限されます。 詳細はドキュメントを参照してください。 at_least_one_permission=トークンを作成するには、少なくともひとつの許可を選択する必要があります permissions_list=許可: @@ -834,6 +851,7 @@ oauth2_client_secret_hint=このページから移動したりページを更新 oauth2_application_edit=編集 oauth2_application_create_description=OAuth2アプリケーションで、サードパーティアプリケーションがこのインスタンス上のユーザーアカウントにアクセスできるようになります。 oauth2_application_remove_description=OAuth2アプリケーションを削除すると、このインスタンス上の許可されたユーザーアカウントへのアクセスができなくなります。 続行しますか? +oauth2_application_locked=設定で有効にされた場合、Giteaは起動時にいくつかのOAuth2アプリケーションを事前登録します。 想定されていない動作を防ぐため、これらは編集も削除もできません。 詳細についてはOAuth2のドキュメントを参照してください。 authorized_oauth2_applications=許可済みOAuth2アプリケーション authorized_oauth2_applications_description=これらのサードパーティ アプリケーションに、あなたのGiteaアカウントへのアクセスを許可しています。 不要になったアプリケーションはアクセス権を取り消すようにしてください。 @@ -922,6 +940,7 @@ fork_from=フォーク元 already_forked=%s はフォーク済み fork_to_different_account=別のアカウントにフォークする fork_visibility_helper=フォークしたリポジトリの公開/非公開は変更できません。 +fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。 use_template=このテンプレートを使用 clone_in_vsc=VSCodeでクローン download_zip=ZIPファイルをダウンロード @@ -2522,6 +2541,7 @@ settings.visibility.private_shortname=プライベート settings.update_settings=設定の更新 settings.update_setting_success=組織の設定を更新しました。 +settings.change_orgname_prompt=注意: 組織名を変更すると組織のURLも変更され、古い名前は解放されます。 settings.change_orgname_redirect_prompt=古い名前は、再使用されていない限りリダイレクトします。 settings.update_avatar_success=組織のアバターを更新しました。 settings.delete=組織を削除 @@ -2615,6 +2635,7 @@ monitor=モニタリング first_page=最初 last_page=最後 total=合計: %d +settings=管理設定 dashboard.new_version_hint=Gitea %s が入手可能になりました。 現在実行しているのは %s です。 詳細は ブログ を確認してください。 dashboard.statistic=サマリー @@ -3378,6 +3399,7 @@ status.waiting=待機中 status.running=実行中 status.success=成功 status.failure=失敗 +status.cancelled=キャンセル status.skipped=スキップ status.blocked=ブロックされた @@ -3432,5 +3454,10 @@ type-2.display_name=リポジトリ プロジェクト type-3.display_name=組織プロジェクト [git.filemode] -symbolic_link=シンボリック リンク +changed_filemode=%[1]s → %[2]s +directory=ディレクトリ +normal_file=ノーマルファイル +executable_file=実行可能ファイル +symbolic_link=シンボリックリンク +submodule=サブモジュール diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index a5d62f4588..2fb17ada73 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -20,7 +20,7 @@ active_stopwatch=Cronómetro em andamento create_new=Criar… user_profile_and_more=Perfil e configurações… signed_in_as=Sessão iniciada como -enable_javascript=Este website requer JavaScript. +enable_javascript=Este sítio Web requer JavaScript. toc=Índice licenses=Licenças return_to_gitea=Retornar ao Gitea diff --git a/package-lock.json b/package-lock.json index dda23bcbd4..db0480a5e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "escape-goat": "4.0.0", "fast-glob": "3.3.1", "jquery": "3.7.1", - "jquery.are-you-sure": "1.9.0", "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "lightningcss-loader": "2.1.0", @@ -6466,17 +6465,6 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, - "node_modules/jquery.are-you-sure": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/jquery.are-you-sure/-/jquery.are-you-sure-1.9.0.tgz", - "integrity": "sha512-2r0uFx8CyAopjeHGOdvvwpFP921TnW1+v1uJXcAWQYHYGB1tryTDhQY+5u6HsVeMwbWiRTKVZFWnLaFpDvIqZQ==", - "dependencies": { - "jquery": ">=1.4.2" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/js-levenshtein-esm": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/js-levenshtein-esm/-/js-levenshtein-esm-1.2.0.tgz", diff --git a/package.json b/package.json index e4f1743feb..224a1422ec 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "escape-goat": "4.0.0", "fast-glob": "3.3.1", "jquery": "3.7.1", - "jquery.are-you-sure": "1.9.0", "katex": "0.16.8", "license-checker-webpack-plugin": "0.2.1", "lightningcss-loader": "2.1.0", diff --git a/poetry.lock b/poetry.lock index 69fc27b107..c8f6cd04ea 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,14 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "click" -version = "8.1.6" +version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.6-py3-none-any.whl", hash = "sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"}, - {file = "click-8.1.6.tar.gz", hash = "sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -149,6 +149,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -156,8 +157,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -174,6 +182,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -181,6 +190,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -188,99 +198,99 @@ files = [ [[package]] name = "regex" -version = "2023.6.3" +version = "2023.8.8" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.6" files = [ - {file = "regex-2023.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:824bf3ac11001849aec3fa1d69abcb67aac3e150a933963fb12bda5151fe1bfd"}, - {file = "regex-2023.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05ed27acdf4465c95826962528f9e8d41dbf9b1aa8531a387dee6ed215a3e9ef"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b49c764f88a79160fa64f9a7b425620e87c9f46095ef9c9920542ab2495c8bc"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e3f1316c2293e5469f8f09dc2d76efb6c3982d3da91ba95061a7e69489a14ef"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43e1dd9d12df9004246bacb79a0e5886b3b6071b32e41f83b0acbf293f820ee8"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4959e8bcbfda5146477d21c3a8ad81b185cd252f3d0d6e4724a5ef11c012fb06"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af4dd387354dc83a3bff67127a124c21116feb0d2ef536805c454721c5d7993d"}, - {file = "regex-2023.6.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2239d95d8e243658b8dbb36b12bd10c33ad6e6933a54d36ff053713f129aa536"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:890e5a11c97cf0d0c550eb661b937a1e45431ffa79803b942a057c4fb12a2da2"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a8105e9af3b029f243ab11ad47c19b566482c150c754e4c717900a798806b222"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:25be746a8ec7bc7b082783216de8e9473803706723b3f6bef34b3d0ed03d57e2"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3676f1dd082be28b1266c93f618ee07741b704ab7b68501a173ce7d8d0d0ca18"}, - {file = "regex-2023.6.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:10cb847aeb1728412c666ab2e2000ba6f174f25b2bdc7292e7dd71b16db07568"}, - {file = "regex-2023.6.3-cp310-cp310-win32.whl", hash = "sha256:dbbbfce33cd98f97f6bffb17801b0576e653f4fdb1d399b2ea89638bc8d08ae1"}, - {file = "regex-2023.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:c5f8037000eb21e4823aa485149f2299eb589f8d1fe4b448036d230c3f4e68e0"}, - {file = "regex-2023.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c123f662be8ec5ab4ea72ea300359023a5d1df095b7ead76fedcd8babbedf969"}, - {file = "regex-2023.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9edcbad1f8a407e450fbac88d89e04e0b99a08473f666a3f3de0fd292badb6aa"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcba6dae7de533c876255317c11f3abe4907ba7d9aa15d13e3d9710d4315ec0e"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29cdd471ebf9e0f2fb3cac165efedc3c58db841d83a518b082077e612d3ee5df"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12b74fbbf6cbbf9dbce20eb9b5879469e97aeeaa874145517563cca4029db65c"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c29ca1bd61b16b67be247be87390ef1d1ef702800f91fbd1991f5c4421ebae8"}, - {file = "regex-2023.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77f09bc4b55d4bf7cc5eba785d87001d6757b7c9eec237fe2af57aba1a071d9"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ea353ecb6ab5f7e7d2f4372b1e779796ebd7b37352d290096978fea83c4dba0c"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:10590510780b7541969287512d1b43f19f965c2ece6c9b1c00fc367b29d8dce7"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e2fbd6236aae3b7f9d514312cdb58e6494ee1c76a9948adde6eba33eb1c4264f"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:6b2675068c8b56f6bfd5a2bda55b8accbb96c02fd563704732fd1c95e2083461"}, - {file = "regex-2023.6.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74419d2b50ecb98360cfaa2974da8689cb3b45b9deff0dcf489c0d333bcc1477"}, - {file = "regex-2023.6.3-cp311-cp311-win32.whl", hash = "sha256:fb5ec16523dc573a4b277663a2b5a364e2099902d3944c9419a40ebd56a118f9"}, - {file = "regex-2023.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:09e4a1a6acc39294a36b7338819b10baceb227f7f7dbbea0506d419b5a1dd8af"}, - {file = "regex-2023.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0654bca0cdf28a5956c83839162692725159f4cda8d63e0911a2c0dc76166525"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:463b6a3ceb5ca952e66550a4532cef94c9a0c80dc156c4cc343041951aec1697"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87b2a5bb5e78ee0ad1de71c664d6eb536dc3947a46a69182a90f4410f5e3f7dd"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6343c6928282c1f6a9db41f5fd551662310e8774c0e5ebccb767002fcf663ca9"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6192d5af2ccd2a38877bfef086d35e6659566a335b1492786ff254c168b1693"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74390d18c75054947e4194019077e243c06fbb62e541d8817a0fa822ea310c14"}, - {file = "regex-2023.6.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:742e19a90d9bb2f4a6cf2862b8b06dea5e09b96c9f2df1779e53432d7275331f"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8abbc5d54ea0ee80e37fef009e3cec5dafd722ed3c829126253d3e22f3846f1e"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c2b867c17a7a7ae44c43ebbeb1b5ff406b3e8d5b3e14662683e5e66e6cc868d3"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d831c2f8ff278179705ca59f7e8524069c1a989e716a1874d6d1aab6119d91d1"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ee2d1a9a253b1729bb2de27d41f696ae893507c7db224436abe83ee25356f5c1"}, - {file = "regex-2023.6.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:61474f0b41fe1a80e8dfa70f70ea1e047387b7cd01c85ec88fa44f5d7561d787"}, - {file = "regex-2023.6.3-cp36-cp36m-win32.whl", hash = "sha256:0b71e63226e393b534105fcbdd8740410dc6b0854c2bfa39bbda6b0d40e59a54"}, - {file = "regex-2023.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bbb02fd4462f37060122e5acacec78e49c0fbb303c30dd49c7f493cf21fc5b27"}, - {file = "regex-2023.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b862c2b9d5ae38a68b92e215b93f98d4c5e9454fa36aae4450f61dd33ff48487"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976d7a304b59ede34ca2921305b57356694f9e6879db323fd90a80f865d355a3"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:83320a09188e0e6c39088355d423aa9d056ad57a0b6c6381b300ec1a04ec3d16"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9427a399501818a7564f8c90eced1e9e20709ece36be701f394ada99890ea4b3"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178bbc1b2ec40eaca599d13c092079bf529679bf0371c602edaa555e10b41c3"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:837328d14cde912af625d5f303ec29f7e28cdab588674897baafaf505341f2fc"}, - {file = "regex-2023.6.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2d44dc13229905ae96dd2ae2dd7cebf824ee92bc52e8cf03dcead37d926da019"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d54af539295392611e7efbe94e827311eb8b29668e2b3f4cadcfe6f46df9c777"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7117d10690c38a622e54c432dfbbd3cbd92f09401d622902c32f6d377e2300ee"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bb60b503ec8a6e4e3e03a681072fa3a5adcbfa5479fa2d898ae2b4a8e24c4591"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:65ba8603753cec91c71de423a943ba506363b0e5c3fdb913ef8f9caa14b2c7e0"}, - {file = "regex-2023.6.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:271f0bdba3c70b58e6f500b205d10a36fb4b58bd06ac61381b68de66442efddb"}, - {file = "regex-2023.6.3-cp37-cp37m-win32.whl", hash = "sha256:9beb322958aaca059f34975b0df135181f2e5d7a13b84d3e0e45434749cb20f7"}, - {file = "regex-2023.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fea75c3710d4f31389eed3c02f62d0b66a9da282521075061ce875eb5300cf23"}, - {file = "regex-2023.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f56fcb7ff7bf7404becdfc60b1e81a6d0561807051fd2f1860b0d0348156a07"}, - {file = "regex-2023.6.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d2da3abc88711bce7557412310dfa50327d5769a31d1c894b58eb256459dc289"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a99b50300df5add73d307cf66abea093304a07eb017bce94f01e795090dea87c"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5708089ed5b40a7b2dc561e0c8baa9535b77771b64a8330b684823cfd5116036"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:687ea9d78a4b1cf82f8479cab23678aff723108df3edeac098e5b2498879f4a7"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d3850beab9f527f06ccc94b446c864059c57651b3f911fddb8d9d3ec1d1b25d"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8915cc96abeb8983cea1df3c939e3c6e1ac778340c17732eb63bb96247b91d2"}, - {file = "regex-2023.6.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:841d6e0e5663d4c7b4c8099c9997be748677d46cbf43f9f471150e560791f7ff"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9edce5281f965cf135e19840f4d93d55b3835122aa76ccacfd389e880ba4cf82"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b956231ebdc45f5b7a2e1f90f66a12be9610ce775fe1b1d50414aac1e9206c06"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:36efeba71c6539d23c4643be88295ce8c82c88bbd7c65e8a24081d2ca123da3f"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:cf67ca618b4fd34aee78740bea954d7c69fdda419eb208c2c0c7060bb822d747"}, - {file = "regex-2023.6.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b4598b1897837067a57b08147a68ac026c1e73b31ef6e36deeeb1fa60b2933c9"}, - {file = "regex-2023.6.3-cp38-cp38-win32.whl", hash = "sha256:f415f802fbcafed5dcc694c13b1292f07fe0befdb94aa8a52905bd115ff41e88"}, - {file = "regex-2023.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:d4f03bb71d482f979bda92e1427f3ec9b220e62a7dd337af0aa6b47bf4498f72"}, - {file = "regex-2023.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ccf91346b7bd20c790310c4147eee6ed495a54ddb6737162a36ce9dbef3e4751"}, - {file = "regex-2023.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b28f5024a3a041009eb4c333863d7894d191215b39576535c6734cd88b0fcb68"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0bb18053dfcfed432cc3ac632b5e5e5c5b7e55fb3f8090e867bfd9b054dbcbf"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5bfb3004f2144a084a16ce19ca56b8ac46e6fd0651f54269fc9e230edb5e4a"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c6b48d0fa50d8f4df3daf451be7f9689c2bde1a52b1225c5926e3f54b6a9ed1"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:051da80e6eeb6e239e394ae60704d2b566aa6a7aed6f2890a7967307267a5dc6"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4c3b7fa4cdaa69268748665a1a6ff70c014d39bb69c50fda64b396c9116cf77"}, - {file = "regex-2023.6.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:457b6cce21bee41ac292d6753d5e94dcbc5c9e3e3a834da285b0bde7aa4a11e9"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aad51907d74fc183033ad796dd4c2e080d1adcc4fd3c0fd4fd499f30c03011cd"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0385e73da22363778ef2324950e08b689abdf0b108a7d8decb403ad7f5191938"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c6a57b742133830eec44d9b2290daf5cbe0a2f1d6acee1b3c7b1c7b2f3606df7"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3e5219bf9e75993d73ab3d25985c857c77e614525fac9ae02b1bebd92f7cecac"}, - {file = "regex-2023.6.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e5087a3c59eef624a4591ef9eaa6e9a8d8a94c779dade95d27c0bc24650261cd"}, - {file = "regex-2023.6.3-cp39-cp39-win32.whl", hash = "sha256:20326216cc2afe69b6e98528160b225d72f85ab080cbdf0b11528cbbaba2248f"}, - {file = "regex-2023.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:bdff5eab10e59cf26bc479f565e25ed71a7d041d1ded04ccf9aee1d9f208487a"}, - {file = "regex-2023.6.3.tar.gz", hash = "sha256:72d1a25bf36d2050ceb35b517afe13864865268dfb45910e2e17a84be6cbfeb0"}, + {file = "regex-2023.8.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:88900f521c645f784260a8d346e12a1590f79e96403971241e64c3a265c8ecdb"}, + {file = "regex-2023.8.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3611576aff55918af2697410ff0293d6071b7e00f4b09e005d614686ac4cd57c"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a0ccc8f2698f120e9e5742f4b38dc944c38744d4bdfc427616f3a163dd9de5"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c662a4cbdd6280ee56f841f14620787215a171c4e2d1744c9528bed8f5816c96"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf0633e4a1b667bfe0bb10b5e53fe0d5f34a6243ea2530eb342491f1adf4f739"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:551ad543fa19e94943c5b2cebc54c73353ffff08228ee5f3376bd27b3d5b9800"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54de2619f5ea58474f2ac211ceea6b615af2d7e4306220d4f3fe690c91988a61"}, + {file = "regex-2023.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ec4b3f0aebbbe2fc0134ee30a791af522a92ad9f164858805a77442d7d18570"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ae646c35cb9f820491760ac62c25b6d6b496757fda2d51be429e0e7b67ae0ab"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca339088839582d01654e6f83a637a4b8194d0960477b9769d2ff2cfa0fa36d2"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d9b6627408021452dcd0d2cdf8da0534e19d93d070bfa8b6b4176f99711e7f90"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:bd3366aceedf274f765a3a4bc95d6cd97b130d1dda524d8f25225d14123c01db"}, + {file = "regex-2023.8.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7aed90a72fc3654fba9bc4b7f851571dcc368120432ad68b226bd593f3f6c0b7"}, + {file = "regex-2023.8.8-cp310-cp310-win32.whl", hash = "sha256:80b80b889cb767cc47f31d2b2f3dec2db8126fbcd0cff31b3925b4dc6609dcdb"}, + {file = "regex-2023.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:b82edc98d107cbc7357da7a5a695901b47d6eb0420e587256ba3ad24b80b7d0b"}, + {file = "regex-2023.8.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1e7d84d64c84ad97bf06f3c8cb5e48941f135ace28f450d86af6b6512f1c9a71"}, + {file = "regex-2023.8.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce0f9fbe7d295f9922c0424a3637b88c6c472b75eafeaff6f910494a1fa719ef"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06c57e14ac723b04458df5956cfb7e2d9caa6e9d353c0b4c7d5d54fcb1325c46"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7a9aaa5a1267125eef22cef3b63484c3241aaec6f48949b366d26c7250e0357"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7408511fca48a82a119d78a77c2f5eb1b22fe88b0d2450ed0756d194fe7a9a"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14dc6f2d88192a67d708341f3085df6a4f5a0c7b03dec08d763ca2cd86e9f559"}, + {file = "regex-2023.8.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48c640b99213643d141550326f34f0502fedb1798adb3c9eb79650b1ecb2f177"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0085da0f6c6393428bf0d9c08d8b1874d805bb55e17cb1dfa5ddb7cfb11140bf"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:964b16dcc10c79a4a2be9f1273fcc2684a9eedb3906439720598029a797b46e6"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7ce606c14bb195b0e5108544b540e2c5faed6843367e4ab3deb5c6aa5e681208"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:40f029d73b10fac448c73d6eb33d57b34607f40116e9f6e9f0d32e9229b147d7"}, + {file = "regex-2023.8.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3b8e6ea6be6d64104d8e9afc34c151926f8182f84e7ac290a93925c0db004bfd"}, + {file = "regex-2023.8.8-cp311-cp311-win32.whl", hash = "sha256:942f8b1f3b223638b02df7df79140646c03938d488fbfb771824f3d05fc083a8"}, + {file = "regex-2023.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:51d8ea2a3a1a8fe4f67de21b8b93757005213e8ac3917567872f2865185fa7fb"}, + {file = "regex-2023.8.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e951d1a8e9963ea51efd7f150450803e3b95db5939f994ad3d5edac2b6f6e2b4"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704f63b774218207b8ccc6c47fcef5340741e5d839d11d606f70af93ee78e4d4"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22283c769a7b01c8ac355d5be0715bf6929b6267619505e289f792b01304d898"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91129ff1bb0619bc1f4ad19485718cc623a2dc433dff95baadbf89405c7f6b57"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de35342190deb7b866ad6ba5cbcccb2d22c0487ee0cbb251efef0843d705f0d4"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b993b6f524d1e274a5062488a43e3f9f8764ee9745ccd8e8193df743dbe5ee61"}, + {file = "regex-2023.8.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3026cbcf11d79095a32d9a13bbc572a458727bd5b1ca332df4a79faecd45281c"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:293352710172239bf579c90a9864d0df57340b6fd21272345222fb6371bf82b3"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d909b5a3fff619dc7e48b6b1bedc2f30ec43033ba7af32f936c10839e81b9217"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:3d370ff652323c5307d9c8e4c62efd1956fb08051b0e9210212bc51168b4ff56"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:b076da1ed19dc37788f6a934c60adf97bd02c7eea461b73730513921a85d4235"}, + {file = "regex-2023.8.8-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e9941a4ada58f6218694f382e43fdd256e97615db9da135e77359da257a7168b"}, + {file = "regex-2023.8.8-cp36-cp36m-win32.whl", hash = "sha256:a8c65c17aed7e15a0c824cdc63a6b104dfc530f6fa8cb6ac51c437af52b481c7"}, + {file = "regex-2023.8.8-cp36-cp36m-win_amd64.whl", hash = "sha256:aadf28046e77a72f30dcc1ab185639e8de7f4104b8cb5c6dfa5d8ed860e57236"}, + {file = "regex-2023.8.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:423adfa872b4908843ac3e7a30f957f5d5282944b81ca0a3b8a7ccbbfaa06103"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ae594c66f4a7e1ea67232a0846649a7c94c188d6c071ac0210c3e86a5f92109"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e51c80c168074faa793685656c38eb7a06cbad7774c8cbc3ea05552d615393d8"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:09b7f4c66aa9d1522b06e31a54f15581c37286237208df1345108fcf4e050c18"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e73e5243af12d9cd6a9d6a45a43570dbe2e5b1cdfc862f5ae2b031e44dd95a8"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:941460db8fe3bd613db52f05259c9336f5a47ccae7d7def44cc277184030a116"}, + {file = "regex-2023.8.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f0ccf3e01afeb412a1a9993049cb160d0352dba635bbca7762b2dc722aa5742a"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2e9216e0d2cdce7dbc9be48cb3eacb962740a09b011a116fd7af8c832ab116ca"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5cd9cd7170459b9223c5e592ac036e0704bee765706445c353d96f2890e816c8"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4873ef92e03a4309b3ccd8281454801b291b689f6ad45ef8c3658b6fa761d7ac"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:239c3c2a339d3b3ddd51c2daef10874410917cd2b998f043c13e2084cb191684"}, + {file = "regex-2023.8.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1005c60ed7037be0d9dea1f9c53cc42f836188227366370867222bda4c3c6bd7"}, + {file = "regex-2023.8.8-cp37-cp37m-win32.whl", hash = "sha256:e6bd1e9b95bc5614a7a9c9c44fde9539cba1c823b43a9f7bc11266446dd568e3"}, + {file = "regex-2023.8.8-cp37-cp37m-win_amd64.whl", hash = "sha256:9a96edd79661e93327cfeac4edec72a4046e14550a1d22aa0dd2e3ca52aec921"}, + {file = "regex-2023.8.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2181c20ef18747d5f4a7ea513e09ea03bdd50884a11ce46066bb90fe4213675"}, + {file = "regex-2023.8.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a2ad5add903eb7cdde2b7c64aaca405f3957ab34f16594d2b78d53b8b1a6a7d6"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9233ac249b354c54146e392e8a451e465dd2d967fc773690811d3a8c240ac601"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920974009fb37b20d32afcdf0227a2e707eb83fe418713f7a8b7de038b870d0b"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2b6c5dfe0929b6c23dde9624483380b170b6e34ed79054ad131b20203a1a63"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96979d753b1dc3b2169003e1854dc67bfc86edf93c01e84757927f810b8c3c93"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ae54a338191e1356253e7883d9d19f8679b6143703086245fb14d1f20196be9"}, + {file = "regex-2023.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2162ae2eb8b079622176a81b65d486ba50b888271302190870b8cc488587d280"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c884d1a59e69e03b93cf0dfee8794c63d7de0ee8f7ffb76e5f75be8131b6400a"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cf9273e96f3ee2ac89ffcb17627a78f78e7516b08f94dc435844ae72576a276e"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:83215147121e15d5f3a45d99abeed9cf1fe16869d5c233b08c56cdf75f43a504"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:3f7454aa427b8ab9101f3787eb178057c5250478e39b99540cfc2b889c7d0586"}, + {file = "regex-2023.8.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0640913d2c1044d97e30d7c41728195fc37e54d190c5385eacb52115127b882"}, + {file = "regex-2023.8.8-cp38-cp38-win32.whl", hash = "sha256:0c59122ceccb905a941fb23b087b8eafc5290bf983ebcb14d2301febcbe199c7"}, + {file = "regex-2023.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:c12f6f67495ea05c3d542d119d270007090bad5b843f642d418eb601ec0fa7be"}, + {file = "regex-2023.8.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:82cd0a69cd28f6cc3789cc6adeb1027f79526b1ab50b1f6062bbc3a0ccb2dbc3"}, + {file = "regex-2023.8.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bb34d1605f96a245fc39790a117ac1bac8de84ab7691637b26ab2c5efb8f228c"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:987b9ac04d0b38ef4f89fbc035e84a7efad9cdd5f1e29024f9289182c8d99e09"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dd6082f4e2aec9b6a0927202c85bc1b09dcab113f97265127c1dc20e2e32495"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7eb95fe8222932c10d4436e7a6f7c99991e3fdd9f36c949eff16a69246dee2dc"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7098c524ba9f20717a56a8d551d2ed491ea89cbf37e540759ed3b776a4f8d6eb"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b694430b3f00eb02c594ff5a16db30e054c1b9589a043fe9174584c6efa8033"}, + {file = "regex-2023.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2aeab3895d778155054abea5238d0eb9a72e9242bd4b43f42fd911ef9a13470"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:988631b9d78b546e284478c2ec15c8a85960e262e247b35ca5eaf7ee22f6050a"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:67ecd894e56a0c6108ec5ab1d8fa8418ec0cff45844a855966b875d1039a2e34"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:14898830f0a0eb67cae2bbbc787c1a7d6e34ecc06fbd39d3af5fe29a4468e2c9"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:f2200e00b62568cfd920127782c61bc1c546062a879cdc741cfcc6976668dfcf"}, + {file = "regex-2023.8.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9691a549c19c22d26a4f3b948071e93517bdf86e41b81d8c6ac8a964bb71e5a6"}, + {file = "regex-2023.8.8-cp39-cp39-win32.whl", hash = "sha256:6ab2ed84bf0137927846b37e882745a827458689eb969028af8032b1b3dac78e"}, + {file = "regex-2023.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5543c055d8ec7801901e1193a51570643d6a6ab8751b1f7dd9af71af467538bb"}, + {file = "regex-2023.8.8.tar.gz", hash = "sha256:fcbdc5f2b0f1cd0f6a56cdb46fe41d2cce1e644e3b68832f3eeebc5fb0f7712e"}, ] [[package]] @@ -307,25 +317,43 @@ files = [ [[package]] name = "tqdm" -version = "4.65.0" +version = "4.66.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"}, - {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"}, + {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, + {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["py-make (>=0.1.0)", "twine", "wheel"] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "yamllint" +version = "1.32.0" +description = "A linter for YAML files." +optional = false +python-versions = ">=3.7" +files = [ + {file = "yamllint-1.32.0-py3-none-any.whl", hash = "sha256:d97a66e48da820829d96077d76b8dfbe6c6140f106e558dae87e81ac4e6b30b7"}, + {file = "yamllint-1.32.0.tar.gz", hash = "sha256:d01dde008c65de5b235188ab3110bebc59d18e5c65fc8a58267cd211cd9df34a"}, +] + +[package.dependencies] +pathspec = ">=0.5.3" +pyyaml = "*" + +[package.extras] +dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] + [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "1b154f70c35b75d47c843959af9df0e7343f3bb579835825ca889ec9350afc41" +content-hash = "d2c60da5736ff899675088560244f086265a777e8057eed9e347367373e1937c" diff --git a/pyproject.toml b/pyproject.toml index f4cd2e6744..0c7e782a97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ python = "^3.8" [tool.poetry.group.dev.dependencies] djlint = "1.32.1" +yamllint = "1.32.0" [tool.djlint] profile="golang" diff --git a/routers/api/actions/artifacts.go b/routers/api/actions/artifacts.go index 946ea11e75..c45dc667af 100644 --- a/routers/api/actions/artifacts.go +++ b/routers/api/actions/artifacts.go @@ -170,8 +170,9 @@ func (ar artifactRoutes) buildArtifactURL(runID int64, artifactHash, suffix stri } type getUploadArtifactRequest struct { - Type string - Name string + Type string + Name string + RetentionDays int64 } type getUploadArtifactResponse struct { @@ -192,10 +193,16 @@ func (ar artifactRoutes) getUploadArtifactURL(ctx *ArtifactContext) { return } + // set retention days + retentionQuery := "" + if req.RetentionDays > 0 { + retentionQuery = fmt.Sprintf("?retentionDays=%d", req.RetentionDays) + } + // use md5(artifact_name) to create upload url artifactHash := fmt.Sprintf("%x", md5.Sum([]byte(req.Name))) resp := getUploadArtifactResponse{ - FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"), + FileContainerResourceURL: ar.buildArtifactURL(runID, artifactHash, "upload"+retentionQuery), } log.Debug("[artifact] get upload url: %s", resp.FileContainerResourceURL) ctx.JSON(http.StatusOK, resp) @@ -219,8 +226,21 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) { return } + // get artifact retention days + expiredDays := setting.Actions.ArtifactRetentionDays + if queryRetentionDays := ctx.Req.URL.Query().Get("retentionDays"); queryRetentionDays != "" { + expiredDays, err = strconv.ParseInt(queryRetentionDays, 10, 64) + if err != nil { + log.Error("Error parse retention days: %v", err) + ctx.Error(http.StatusBadRequest, "Error parse retention days") + return + } + } + log.Debug("[artifact] upload chunk, name: %s, path: %s, size: %d, retention days: %d", + artifactName, artifactPath, fileRealTotalSize, expiredDays) + // create or get artifact with name and path - artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactPath) + artifact, err := actions.CreateArtifact(ctx, task, artifactName, artifactPath, expiredDays) if err != nil { log.Error("Error create or get artifact: %v", err) ctx.Error(http.StatusInternalServerError, "Error create or get artifact") diff --git a/routers/api/actions/artifacts_chunks.go b/routers/api/actions/artifacts_chunks.go index 30d31b4d75..458d671cff 100644 --- a/routers/api/actions/artifacts_chunks.go +++ b/routers/api/actions/artifacts_chunks.go @@ -179,7 +179,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st // save storage path to artifact log.Debug("[artifact] merge chunks to artifact: %d, %s", artifact.ID, storagePath) artifact.StoragePath = storagePath - artifact.Status = actions.ArtifactStatusUploadConfirmed + artifact.Status = int64(actions.ArtifactStatusUploadConfirmed) if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil { return fmt.Errorf("update artifact error: %v", err) } diff --git a/routers/api/actions/runner/runner.go b/routers/api/actions/runner/runner.go index 6de5964cb7..cb206f5685 100644 --- a/routers/api/actions/runner/runner.go +++ b/routers/api/actions/runner/runner.go @@ -202,8 +202,14 @@ func (s *Service) UpdateTask( if err := task.LoadJob(ctx); err != nil { return nil, status.Errorf(codes.Internal, "load job: %v", err) } + if err := task.Job.LoadRun(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "load run: %v", err) + } - actions_service.CreateCommitStatus(ctx, task.Job) + // don't create commit status for cron job + if task.Job.Run.ScheduleID == 0 { + actions_service.CreateCommitStatus(ctx, task.Job) + } if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED { if err := actions_service.EmitJobsIfReady(task.Job.RunID); err != nil { diff --git a/routers/api/packages/chef/auth.go b/routers/api/packages/chef/auth.go index d895640894..53055bb682 100644 --- a/routers/api/packages/chef/auth.go +++ b/routers/api/packages/chef/auth.go @@ -16,6 +16,7 @@ import ( "net/http" "path" "regexp" + "slices" "strconv" "strings" "time" @@ -265,7 +266,7 @@ func verifyDataOld(signature, data []byte, pub *rsa.PublicKey) error { } } - if !util.SliceEqual(out[skip:], data) { + if !slices.Equal(out[skip:], data) { return fmt.Errorf("could not verify signature") } diff --git a/routers/api/v1/admin/adopt.go b/routers/api/v1/admin/adopt.go index ccd8be9171..bf030eb222 100644 --- a/routers/api/v1/admin/adopt.go +++ b/routers/api/v1/admin/adopt.go @@ -9,7 +9,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers/api/v1/utils" repo_service "code.gitea.io/gitea/services/repository" @@ -109,7 +108,7 @@ func AdoptRepository(ctx *context.APIContext) { ctx.NotFound() return } - if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{ + if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: repoName, IsPrivate: true, }); err != nil { diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 3904c61639..cb25f3559a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -333,8 +333,11 @@ func reqExploreSignIn() func(ctx *context.APIContext) { } } -func reqBasicAuth() func(ctx *context.APIContext) { +func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { + if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { + return + } if !ctx.IsBasicAuth { ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required") return @@ -698,6 +701,9 @@ func buildAuthGroup() *auth.Group { &auth.HTTPSign{}, &auth.Basic{}, // FIXME: this should be removed once we don't allow basic auth in API ) + if setting.Service.EnableReverseProxyAuthAPI { + group.Add(&auth.ReverseProxy{}) + } specialAdd(group) return group @@ -800,7 +806,7 @@ func Routes() *web.Route { m.Combo("").Get(user.ListAccessTokens). Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken) m.Combo("/{id}").Delete(reqToken(), user.DeleteAccessToken) - }, reqBasicAuth()) + }, reqBasicOrRevProxyAuth()) m.Get("/activities/feeds", user.ListUserActivityFeeds) }, context_service.UserAssignmentAPI()) diff --git a/routers/api/v1/org/team.go b/routers/api/v1/org/team.go index 0e11acc901..4b52fb8987 100644 --- a/routers/api/v1/org/team.go +++ b/routers/api/v1/org/team.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/convert" org_service "code.gitea.io/gitea/services/org" + repo_service "code.gitea.io/gitea/services/repository" ) // ListTeams list all the teams of an organization @@ -726,7 +727,7 @@ func RemoveTeamRepository(ctx *context.APIContext) { ctx.Error(http.StatusForbidden, "", "Must have admin-level access to the repository") return } - if err := models.RemoveRepository(ctx.Org.Team, repo.ID); err != nil { + if err := repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, repo.ID); err != nil { ctx.Error(http.StatusInternalServerError, "RemoveRepository", err) return } diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 942d4c799f..66e7577a4b 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -8,7 +8,6 @@ import ( "errors" "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -19,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/convert" + repo_service "code.gitea.io/gitea/services/repository" ) // ListCollaborators list a repository's collaborators @@ -228,7 +228,7 @@ func DeleteCollaborator(ctx *context.APIContext) { return } - if err := models.DeleteCollaboration(ctx.Repo.Repository, collaborator.ID); err != nil { + if err := repo_service.DeleteCollaboration(ctx.Repo.Repository, collaborator.ID); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err) return } diff --git a/routers/api/v1/repo/migrate.go b/routers/api/v1/repo/migrate.go index dfc9004620..4ddd452372 100644 --- a/routers/api/v1/repo/migrate.go +++ b/routers/api/v1/repo/migrate.go @@ -22,7 +22,6 @@ import ( "code.gitea.io/gitea/modules/lfs" "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -31,6 +30,7 @@ import ( "code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/migrations" notify_service "code.gitea.io/gitea/services/notify" + repo_service "code.gitea.io/gitea/services/repository" ) // Migrate migrate remote git repository to gitea @@ -170,7 +170,7 @@ func Migrate(ctx *context.APIContext) { opts.Releases = false } - repo, err := repo_module.CreateRepository(ctx.Doer, repoOwner, repo_module.CreateRepoOptions{ + repo, err := repo_service.CreateRepositoryDirectly(ctx, ctx.Doer, repoOwner, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, OriginalURL: form.CloneAddr, @@ -200,7 +200,7 @@ func Migrate(ctx *context.APIContext) { } if repo != nil { - if errDelete := models.DeleteRepository(ctx.Doer, repoOwner.ID, repo.ID); errDelete != nil { + if errDelete := repo_service.DeleteRepositoryDirectly(ctx, ctx.Doer, repoOwner.ID, repo.ID); errDelete != nil { log.Error("DeleteRepository: %v", errDelete) } } diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 7b0c954a73..e86743d55a 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -7,6 +7,7 @@ package repo import ( "fmt" "net/http" + "slices" "strings" "time" @@ -235,12 +236,12 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre } // If the readme template does not exist, a 400 will be returned. - if opt.AutoInit && len(opt.Readme) > 0 && !util.SliceContains(repo_module.Readmes, opt.Readme) { + if opt.AutoInit && len(opt.Readme) > 0 && !slices.Contains(repo_module.Readmes, opt.Readme) { ctx.Error(http.StatusBadRequest, "", fmt.Errorf("readme template does not exist, available templates: %v", repo_module.Readmes)) return } - repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_module.CreateRepoOptions{ + repo, err := repo_service.CreateRepository(ctx, ctx.Doer, owner, repo_service.CreateRepoOptions{ Name: opt.Name, Description: opt.Description, IssueLabels: opt.IssueLabels, diff --git a/routers/api/v1/repo/teams.go b/routers/api/v1/repo/teams.go index 01292f18d8..d1be619cac 100644 --- a/routers/api/v1/repo/teams.go +++ b/routers/api/v1/repo/teams.go @@ -7,11 +7,11 @@ import ( "fmt" "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/services/convert" org_service "code.gitea.io/gitea/services/org" + repo_service "code.gitea.io/gitea/services/repository" ) // ListTeams list a repository's teams @@ -97,7 +97,7 @@ func IsTeam(ctx *context.APIContext) { return } - if models.HasRepository(team, ctx.Repo.Repository.ID) { + if repo_service.HasRepository(team, ctx.Repo.Repository.ID) { apiTeam, err := convert.ToTeam(ctx, team) if err != nil { ctx.InternalServerError(err) @@ -192,7 +192,7 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { return } - repoHasTeam := models.HasRepository(team, ctx.Repo.Repository.ID) + repoHasTeam := repo_service.HasRepository(team, ctx.Repo.Repository.ID) var err error if add { if repoHasTeam { @@ -205,7 +205,7 @@ func changeRepoTeam(ctx *context.APIContext, add bool) { ctx.Error(http.StatusUnprocessableEntity, "notAdded", fmt.Errorf("team '%s' was not added to repo", team.Name)) return } - err = models.RemoveRepository(team, ctx.Repo.Repository.ID) + err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID) } if err != nil { ctx.InternalServerError(err) diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go index d1d0abca02..45c280ef73 100644 --- a/routers/web/admin/repos.go +++ b/routers/web/admin/repos.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "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/util" "code.gitea.io/gitea/routers/web/explore" @@ -144,7 +143,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { if has || !isDir { // Fallthrough to failure mode } else if action == "adopt" { - if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{ + if _, err := repo_service.AdoptRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: dirSplit[1], IsPrivate: true, }); err != nil { diff --git a/routers/web/admin/users.go b/routers/web/admin/users.go index c83d652c3d..47dff6e852 100644 --- a/routers/web/admin/users.go +++ b/routers/web/admin/users.go @@ -314,7 +314,9 @@ func EditUser(ctx *context.Context) { ctx.Data["DisableRegularOrgCreation"] = setting.Admin.DisableRegularOrgCreation ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() - ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar) + ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, + setting.GetDefaultDisableGravatar(), + ) prepareUserInfo(ctx) if ctx.Written() { @@ -331,7 +333,8 @@ func EditUserPost(ctx *context.Context) { ctx.Data["PageIsAdminUsers"] = true ctx.Data["DisableMigrations"] = setting.Repository.DisableMigrations ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() - ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar) + ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, + setting.GetDefaultDisableGravatar()) u := prepareUserInfo(ctx) if ctx.Written() { diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 613dff2182..a69fdedba4 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -152,7 +152,6 @@ func Home(ctx *context.Context) { pager.SetDefaultParams(ctx) pager.AddParam(ctx, "language", "Language") ctx.Data["Page"] = pager - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0 diff --git a/routers/web/org/members.go b/routers/web/org/members.go index fae8b48128..f963ad55ef 100644 --- a/routers/web/org/members.go +++ b/routers/web/org/members.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + shared_user "code.gitea.io/gitea/routers/web/shared/user" ) const ( @@ -52,6 +53,12 @@ func Members(ctx *context.Context) { return } + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + pager := context.NewPagination(int(total), setting.UI.MembersPagingNum, page, 5) opts.ListOptions.Page = page opts.ListOptions.PageSize = setting.UI.MembersPagingNum @@ -62,7 +69,6 @@ func Members(ctx *context.Context) { } ctx.Data["Page"] = pager ctx.Data["Members"] = members - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["MembersIsPublicMember"] = membersIsPublic ctx.Data["MembersIsUserOrgOwner"] = organization.IsUserOrgOwner(members, org.ID) ctx.Data["MembersTwoFaStatus"] = members.GetTwoFaStatus() diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index 957daab646..0f082a70df 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -20,6 +20,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" "code.gitea.io/gitea/services/forms" org_service "code.gitea.io/gitea/services/org" @@ -46,6 +47,13 @@ func Settings(ctx *context.Context) { ctx.Data["CurrentVisibility"] = ctx.Org.Organization.Visibility ctx.Data["RepoAdminChangeTeamAccess"] = ctx.Org.Organization.RepoAdminChangeTeamAccess ctx.Data["ContextUser"] = ctx.ContextUser + + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsOptions) } @@ -189,6 +197,12 @@ func SettingsDelete(ctx *context.Context) { return } + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsDelete) } @@ -207,6 +221,12 @@ func Webhooks(ctx *context.Context) { return } + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.Data["Webhooks"] = ws ctx.HTML(http.StatusOK, tplSettingsHooks) } @@ -228,5 +248,12 @@ func Labels(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsOrgSettingsLabels"] = true ctx.Data["LabelTemplateFiles"] = repo_module.LabelTemplateFiles + + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsLabels) } diff --git a/routers/web/org/setting_oauth2.go b/routers/web/org/setting_oauth2.go index 9bf4280b07..0045bce4c9 100644 --- a/routers/web/org/setting_oauth2.go +++ b/routers/web/org/setting_oauth2.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" + shared_user "code.gitea.io/gitea/routers/web/shared/user" user_setting "code.gitea.io/gitea/routers/web/user/setting" ) @@ -41,6 +42,12 @@ func Applications(ctx *context.Context) { } ctx.Data["Applications"] = apps + err = shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplSettingsApplications) } diff --git a/routers/web/org/setting_packages.go b/routers/web/org/setting_packages.go index 21d25bd90a..796829d34e 100644 --- a/routers/web/org/setting_packages.go +++ b/routers/web/org/setting_packages.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" shared "code.gitea.io/gitea/routers/web/shared/packages" + shared_user "code.gitea.io/gitea/routers/web/shared/user" ) const ( @@ -24,6 +25,12 @@ func Packages(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetPackagesContext(ctx, ctx.ContextUser) ctx.HTML(http.StatusOK, tplSettingsPackages) @@ -34,6 +41,12 @@ func PackagesRuleAdd(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetRuleAddContext(ctx) ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit) @@ -44,6 +57,12 @@ func PackagesRuleEdit(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetRuleEditContext(ctx, ctx.ContextUser) ctx.HTML(http.StatusOK, tplSettingsPackagesRuleEdit) @@ -80,6 +99,12 @@ func PackagesRulePreview(ctx *context.Context) { ctx.Data["PageIsOrgSettings"] = true ctx.Data["PageIsSettingsPackages"] = true + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + shared.SetRulePreviewContext(ctx, ctx.ContextUser) ctx.HTML(http.StatusOK, tplSettingsPackagesRulePreview) diff --git a/routers/web/org/teams.go b/routers/web/org/teams.go index 3b07bba713..2fd2a681af 100644 --- a/routers/web/org/teams.go +++ b/routers/web/org/teams.go @@ -25,9 +25,11 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/utils" + shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/convert" "code.gitea.io/gitea/services/forms" org_service "code.gitea.io/gitea/services/org" + repo_service "code.gitea.io/gitea/services/repository" ) const ( @@ -56,7 +58,12 @@ func Teams(ctx *context.Context) { } } ctx.Data["Teams"] = ctx.Org.Teams - ctx.Data["ContextUser"] = ctx.ContextUser + + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } ctx.HTML(http.StatusOK, tplTeams) } @@ -242,7 +249,7 @@ func TeamsRepoAction(ctx *context.Context) { } err = org_service.TeamAddRepository(ctx.Org.Team, repo) case "remove": - err = models.RemoveRepository(ctx.Org.Team, ctx.FormInt64("repoid")) + err = repo_service.RemoveRepositoryFromTeam(ctx, ctx.Org.Team, ctx.FormInt64("repoid")) case "addall": err = models.AddAllRepositories(ctx.Org.Team) case "removeall": @@ -364,6 +371,12 @@ func TeamMembers(ctx *context.Context) { ctx.Data["Title"] = ctx.Org.Team.Name ctx.Data["PageIsOrgTeams"] = true ctx.Data["PageIsOrgTeamMembers"] = true + + if err := shared_user.LoadHeaderCount(ctx); err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + if err := ctx.Org.Team.LoadMembers(ctx); err != nil { ctx.ServerError("GetMembers", err) return @@ -386,6 +399,12 @@ func TeamRepositories(ctx *context.Context) { ctx.Data["Title"] = ctx.Org.Team.Name ctx.Data["PageIsOrgTeams"] = true ctx.Data["PageIsOrgTeamRepos"] = true + + if err := shared_user.LoadHeaderCount(ctx); err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + if err := ctx.Org.Team.LoadRepositories(ctx); err != nil { ctx.ServerError("GetRepositories", err) return diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index e4ca6a7198..a9c2858303 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -486,8 +486,9 @@ type ArtifactsViewResponse struct { } type ArtifactsViewItem struct { - Name string `json:"name"` - Size int64 `json:"size"` + Name string `json:"name"` + Size int64 `json:"size"` + Status string `json:"status"` } func ArtifactsView(ctx *context_module.Context) { @@ -510,9 +511,14 @@ func ArtifactsView(ctx *context_module.Context) { Artifacts: make([]*ArtifactsViewItem, 0, len(artifacts)), } for _, art := range artifacts { + status := "completed" + if art.Status == int64(actions_model.ArtifactStatusExpired) { + status = "expired" + } artifactsResponse.Artifacts = append(artifactsResponse.Artifacts, &ArtifactsViewItem{ - Name: art.ArtifactName, - Size: art.FileSize, + Name: art.ArtifactName, + Size: art.FileSize, + Status: status, }) } ctx.JSON(http.StatusOK, artifactsResponse) diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index f5e8f80ddf..c95d54532a 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -12,6 +12,7 @@ import ( "math/big" "net/http" "net/url" + "slices" "sort" "strconv" "strings" @@ -3628,7 +3629,7 @@ func issuePosters(ctx *context.Context, isPullList bool) { if search == "" && ctx.Doer != nil { // the returned posters slice only contains limited number of users, // to make the current user (doer) can quickly filter their own issues, always add doer to the posters slice - if !util.SliceContainsFunc(posters, func(user *user_model.User) bool { return user.ID == ctx.Doer.ID }) { + if !slices.ContainsFunc(posters, func(user *user_model.User) bool { return user.ID == ctx.Doer.ID }) { posters = append(posters, ctx.Doer) } } diff --git a/routers/web/repo/packages.go b/routers/web/repo/packages.go index 6ad2f71b5c..ac9e64d774 100644 --- a/routers/web/repo/packages.go +++ b/routers/web/repo/packages.go @@ -58,7 +58,6 @@ func Packages(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("packages.title") ctx.Data["IsPackagesPage"] = true - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["Query"] = query ctx.Data["PackageType"] = packageType ctx.Data["AvailableTypes"] = packages.TypeList diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index 4409381bc5..3ed7ca1c91 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net/http" + "slices" "strings" "code.gitea.io/gitea/models" @@ -275,7 +276,7 @@ func CreatePost(ctx *context.Context) { return } } else { - repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_module.CreateRepoOptions{ + repo, err = repo_service.CreateRepository(ctx, ctx.Doer, ctxUser, repo_service.CreateRepoOptions{ Name: form.RepoName, Description: form.Description, Gitignores: form.Gitignores, @@ -378,10 +379,6 @@ func RedirectDownload(ctx *context.Context) { curRepo := ctx.Repo.Repository releases, err := repo_model.GetReleasesByRepoIDAndNames(ctx, curRepo.ID, tagNames) if err != nil { - if repo_model.IsErrAttachmentNotExist(err) { - ctx.Error(http.StatusNotFound) - return - } ctx.ServerError("RedirectDownload", err) return } @@ -396,6 +393,23 @@ func RedirectDownload(ctx *context.Context) { ServeAttachment(ctx, att.UUID) return } + } else if len(releases) == 0 && vTag == "latest" { + // GitHub supports the alias "latest" for the latest release + // We only fetch the latest release if the tag is "latest" and no release with the tag "latest" exists + release, err := repo_model.GetLatestReleaseByRepoID(ctx.Repo.Repository.ID) + if err != nil { + ctx.Error(http.StatusNotFound) + return + } + att, err := repo_model.GetAttachmentByReleaseIDFileName(ctx, release.ID, fileName) + if err != nil { + ctx.Error(http.StatusNotFound) + return + } + if att != nil { + ServeAttachment(ctx, att.UUID) + return + } } ctx.Error(http.StatusNotFound) } @@ -646,7 +660,7 @@ func GetBranchesList(ctx *context.Context) { } resp := &branchTagSearchResponse{} // always put default branch on the top if it exists - if util.SliceContains(branches, ctx.Repo.Repository.DefaultBranch) { + if slices.Contains(branches, ctx.Repo.Repository.DefaultBranch) { branches = util.SliceRemoveAll(branches, ctx.Repo.Repository.DefaultBranch) branches = append([]string{ctx.Repo.Repository.DefaultBranch}, branches...) } @@ -680,7 +694,7 @@ func PrepareBranchList(ctx *context.Context) { return } // always put default branch on the top if it exists - if util.SliceContains(brs, ctx.Repo.Repository.DefaultBranch) { + if slices.Contains(brs, ctx.Repo.Repository.DefaultBranch) { brs = util.SliceRemoveAll(brs, ctx.Repo.Repository.DefaultBranch) brs = append([]string{ctx.Repo.Repository.DefaultBranch}, brs...) } diff --git a/routers/web/repo/setting/avatar.go b/routers/web/repo/setting/avatar.go index ae80f1db01..02c807b775 100644 --- a/routers/web/repo/setting/avatar.go +++ b/routers/web/repo/setting/avatar.go @@ -38,7 +38,7 @@ func UpdateAvatarSetting(ctx *context.Context, form forms.AvatarForm) error { defer r.Close() if form.Avatar.Size > setting.Avatar.MaxFileSize { - return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big")) + return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) } data, err := io.ReadAll(r) diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index b708422cbd..212b0346bc 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -7,7 +7,6 @@ import ( "net/http" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" @@ -21,6 +20,7 @@ import ( "code.gitea.io/gitea/routers/utils" "code.gitea.io/gitea/services/mailer" org_service "code.gitea.io/gitea/services/org" + repo_service "code.gitea.io/gitea/services/repository" ) // Collaboration render a repository's collaboration page @@ -127,7 +127,7 @@ func ChangeCollaborationAccessMode(ctx *context.Context) { // DeleteCollaboration delete a collaboration for a repository func DeleteCollaboration(ctx *context.Context) { - if err := models.DeleteCollaboration(ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { + if err := repo_service.DeleteCollaboration(ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { ctx.Flash.Error("DeleteCollaboration: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) @@ -196,7 +196,7 @@ func DeleteTeam(ctx *context.Context) { return } - if err = models.RemoveRepository(team, ctx.Repo.Repository.ID); err != nil { + if err = repo_service.RemoveRepositoryFromTeam(ctx, team, ctx.Repo.Repository.ID); err != nil { ctx.ServerError("team.RemoveRepositorys", err) return } diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go index 51d127bfc2..55f292f143 100644 --- a/routers/web/repo/setting/settings_test.go +++ b/routers/web/repo/setting/settings_test.go @@ -7,7 +7,6 @@ import ( "net/http" "testing" - "code.gitea.io/gitea/models" asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" @@ -19,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/forms" + repo_service "code.gitea.io/gitea/services/repository" "github.com/stretchr/testify/assert" ) @@ -248,7 +248,7 @@ func TestAddTeamPost(t *testing.T) { AddTeamPost(ctx) - assert.True(t, models.HasRepository(team, re.ID)) + assert.True(t, repo_service.HasRepository(team, re.ID)) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.Empty(t, ctx.Flash.ErrorMsg) } @@ -288,7 +288,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) { AddTeamPost(ctx) - assert.False(t, models.HasRepository(team, re.ID)) + assert.False(t, repo_service.HasRepository(team, re.ID)) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -329,7 +329,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) { AddTeamPost(ctx) AddTeamPost(ctx) - assert.True(t, models.HasRepository(team, re.ID)) + assert.True(t, repo_service.HasRepository(team, re.ID)) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -402,5 +402,5 @@ func TestDeleteTeam(t *testing.T) { DeleteTeam(ctx) - assert.False(t, models.HasRepository(team, re.ID)) + assert.False(t, repo_service.HasRepository(team, re.ID)) } diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 15c85f6427..f0fe6140df 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -14,6 +14,7 @@ import ( "net/http" "net/url" "path" + "slices" "strings" "time" @@ -370,7 +371,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st if workFlowErr != nil { ctx.Data["FileError"] = ctx.Locale.Tr("actions.runs.invalid_workflow_helper", workFlowErr.Error()) } - } else if util.SliceContains([]string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}, ctx.Repo.TreePath) { + } 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 { diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 9b1918ed16..6273e11fc5 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -22,7 +22,6 @@ import ( func prepareContextForCommonProfile(ctx *context.Context) { ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled - ctx.Data["ContextUser"] = ctx.ContextUser ctx.Data["EnableFeed"] = setting.Other.EnableFeed ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink() } diff --git a/routers/web/user/code.go b/routers/web/user/code.go index 033f65c9c0..29b8b91c89 100644 --- a/routers/web/user/code.go +++ b/routers/web/user/code.go @@ -30,7 +30,6 @@ func CodeSearch(ctx *context.Context) { ctx.Data["IsPackageEnabled"] = setting.Packages.Enabled ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled ctx.Data["Title"] = ctx.Tr("explore.code") - ctx.Data["ContextUser"] = ctx.ContextUser language := ctx.FormTrim("l") keyword := ctx.FormTrim("q") diff --git a/routers/web/user/home.go b/routers/web/user/home.go index a7f6a52f1b..a88479e129 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "regexp" + "slices" "sort" "strconv" "strings" @@ -290,7 +291,7 @@ func Milestones(ctx *context.Context) { if len(repoIDs) == 0 { repoIDs = showRepoIds.Values() } - repoIDs = util.SliceRemoveAllFunc(repoIDs, func(v int64) bool { + repoIDs = slices.DeleteFunc(repoIDs, func(v int64) bool { return !showRepoIds.Contains(v) }) @@ -534,7 +535,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // Gets set when clicking filters on the issues overview page. selectedRepoIDs := getRepoIDs(ctx.FormString("repos")) // Remove repo IDs that are not accessible to the user. - selectedRepoIDs = util.SliceRemoveAllFunc(selectedRepoIDs, func(v int64) bool { + selectedRepoIDs = slices.DeleteFunc(selectedRepoIDs, func(v int64) bool { return !accessibleRepos.Contains(v) }) if len(selectedRepoIDs) > 0 { diff --git a/routers/web/user/package.go b/routers/web/user/package.go index d44638d48b..57770b2b1a 100644 --- a/routers/web/user/package.go +++ b/routers/web/user/package.go @@ -390,6 +390,12 @@ func PackageSettings(ctx *context.Context) { ctx.Data["Repos"] = repos ctx.Data["CanWritePackages"] = ctx.Package.AccessMode >= perm.AccessModeWrite || ctx.IsUserSiteAdmin() + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + ctx.HTML(http.StatusOK, tplPackagesSettings) } diff --git a/routers/web/user/setting/adopt.go b/routers/web/user/setting/adopt.go index 01668c3954..decb35c1e1 100644 --- a/routers/web/user/setting/adopt.go +++ b/routers/web/user/setting/adopt.go @@ -9,7 +9,6 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/context" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" repo_service "code.gitea.io/gitea/services/repository" @@ -45,7 +44,7 @@ func AdoptOrDeleteRepository(ctx *context.Context) { if has || !isDir { // Fallthrough to failure mode } else if action == "adopt" && allowAdopt { - if _, err := repo_service.AdoptRepository(ctx, ctxUser, ctxUser, repo_module.CreateRepoOptions{ + if _, err := repo_service.AdoptRepository(ctx, ctxUser, ctxUser, repo_service.CreateRepoOptions{ Name: dir, IsPrivate: true, }); err != nil { diff --git a/routers/web/user/setting/oauth2_common.go b/routers/web/user/setting/oauth2_common.go index 641cc1fd9f..5786118f50 100644 --- a/routers/web/user/setting/oauth2_common.go +++ b/routers/web/user/setting/oauth2_common.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" + shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/forms" ) @@ -25,6 +26,15 @@ type OAuth2CommonHandlers struct { func (oa *OAuth2CommonHandlers) renderEditPage(ctx *context.Context) { app := ctx.Data["App"].(*auth.OAuth2Application) ctx.Data["FormActionPath"] = fmt.Sprintf("%s/%d", oa.BasePathEditPrefix, app.ID) + + if ctx.ContextUser.IsOrganization() { + err := shared_user.LoadHeaderCount(ctx) + if err != nil { + ctx.ServerError("LoadHeaderCount", err) + return + } + } + ctx.HTML(http.StatusOK, oa.TplAppEdit) } diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index 61089d0947..0321a5759b 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -44,7 +44,9 @@ func Profile(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings.profile") ctx.Data["PageIsSettingsProfile"] = true ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() - ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar) + ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, + setting.GetDefaultDisableGravatar(), + ) ctx.HTML(http.StatusOK, tplSettingsProfile) } @@ -86,7 +88,9 @@ func ProfilePost(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsProfile"] = true ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice() - ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar) + ctx.Data["DisableGravatar"] = system_model.GetSettingWithCacheBool(ctx, system_model.KeyPictureDisableGravatar, + setting.GetDefaultDisableGravatar(), + ) if ctx.HasError() { ctx.HTML(http.StatusOK, tplSettingsProfile) @@ -146,7 +150,7 @@ func UpdateAvatarSetting(ctx *context.Context, form *forms.AvatarForm, ctxUser * defer fr.Close() if form.Avatar.Size > setting.Avatar.MaxFileSize { - return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big")) + return errors.New(ctx.Tr("settings.uploaded_avatar_is_too_big", form.Avatar.Size/1024, setting.Avatar.MaxFileSize/1024)) } data, err := io.ReadAll(fr) diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go new file mode 100644 index 0000000000..785eeb5838 --- /dev/null +++ b/services/actions/cleanup.go @@ -0,0 +1,42 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + "time" + + "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/storage" +) + +// Cleanup removes expired actions logs, data and artifacts +func Cleanup(taskCtx context.Context, olderThan time.Duration) error { + // TODO: clean up expired actions logs + + // clean up expired artifacts + return CleanupArtifacts(taskCtx) +} + +// CleanupArtifacts removes expired artifacts and set records expired status +func CleanupArtifacts(taskCtx context.Context) error { + artifacts, err := actions.ListNeedExpiredArtifacts(taskCtx) + if err != nil { + return err + } + log.Info("Found %d expired artifacts", len(artifacts)) + for _, artifact := range artifacts { + if err := storage.ActionsArtifacts.Delete(artifact.StoragePath); err != nil { + log.Error("Cannot delete artifact %d: %v", artifact.ID, err) + continue + } + if err := actions.SetArtifactExpired(taskCtx, artifact.ID); err != nil { + log.Error("Cannot set artifact %d expired: %v", artifact.ID, err) + continue + } + log.Info("Artifact %d set expired", artifact.ID) + } + return nil +} diff --git a/services/actions/schedule_tasks.go b/services/actions/schedule_tasks.go index 87131e0aab..40b606c777 100644 --- a/services/actions/schedule_tasks.go +++ b/services/actions/schedule_tasks.go @@ -10,6 +10,7 @@ import ( actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -44,6 +45,10 @@ func startTasks(ctx context.Context) error { return fmt.Errorf("find specs: %w", err) } + if err := specs.LoadRepos(); err != nil { + return fmt.Errorf("LoadRepos: %w", err) + } + // Loop through each spec and create a schedule task for it for _, row := range specs { // cancel running jobs if the event is push @@ -59,6 +64,11 @@ func startTasks(ctx context.Context) error { } } + cfg := row.Repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if cfg.IsWorkflowDisabled(row.Schedule.WorkflowID) { + continue + } + if err := CreateScheduleTask(ctx, row.Schedule); err != nil { log.Error("CreateScheduleTask: %v", err) return err @@ -103,6 +113,7 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) CommitSHA: cron.CommitSHA, Event: cron.Event, EventPayload: cron.EventPayload, + ScheduleID: cron.ID, Status: actions_model.StatusWaiting, } @@ -117,19 +128,6 @@ func CreateScheduleTask(ctx context.Context, cron *actions_model.ActionSchedule) return err } - // Retrieve the jobs for the newly created action run - jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: run.ID}) - if err != nil { - return err - } - - // Create commit statuses for each job - for _, job := range jobs { - if err := createCommitStatus(ctx, job); err != nil { - return err - } - } - // Return nil if no errors occurred return nil } diff --git a/services/asymkey/main_test.go b/services/asymkey/main_test.go index 3fa88340fd..e7a03861b9 100644 --- a/services/asymkey/main_test.go +++ b/services/asymkey/main_test.go @@ -8,6 +8,9 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" ) func TestMain(m *testing.M) { diff --git a/services/attachment/attachment_test.go b/services/attachment/attachment_test.go index 1b9af34427..35fcef7445 100644 --- a/services/attachment/attachment_test.go +++ b/services/attachment/attachment_test.go @@ -13,6 +13,8 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + _ "code.gitea.io/gitea/models/actions" + "github.com/stretchr/testify/assert" ) diff --git a/services/context/user.go b/services/context/user.go index 62d2dc0aa2..81c2746819 100644 --- a/services/context/user.go +++ b/services/context/user.go @@ -27,6 +27,7 @@ func UserAssignmentWeb() func(ctx *context.Context) { } } ctx.ContextUser = userAssignment(ctx.Base, ctx.Doer, errorFn) + ctx.Data["ContextUser"] = ctx.ContextUser } } diff --git a/services/convert/main_test.go b/services/convert/main_test.go index 4c8e57bf79..c2298dcb74 100644 --- a/services/convert/main_test.go +++ b/services/convert/main_test.go @@ -8,6 +8,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/actions" ) func TestMain(m *testing.M) { diff --git a/services/cron/tasks_basic.go b/services/cron/tasks_basic.go index 2a213ae515..3869382d22 100644 --- a/services/cron/tasks_basic.go +++ b/services/cron/tasks_basic.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/models/webhook" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/services/actions" "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/migrations" mirror_service "code.gitea.io/gitea/services/mirror" @@ -156,6 +157,20 @@ func registerCleanupPackages() { }) } +func registerActionsCleanup() { + RegisterTaskFatal("cleanup_actions", &OlderThanConfig{ + BaseConfig: BaseConfig{ + Enabled: true, + RunAtStart: true, + Schedule: "@midnight", + }, + OlderThan: 24 * time.Hour, + }, func(ctx context.Context, _ *user_model.User, config Config) error { + realConfig := config.(*OlderThanConfig) + return actions.Cleanup(ctx, realConfig.OlderThan) + }) +} + func initBasicTasks() { if setting.Mirror.Enabled { registerUpdateMirrorTask() @@ -172,4 +187,7 @@ func initBasicTasks() { if setting.Packages.Enabled { registerCleanupPackages() } + if setting.Actions.Enabled { + registerActionsCleanup() + } } diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go index 87d2e02b48..3da5af3486 100644 --- a/services/externalaccount/user.go +++ b/services/externalaccount/user.go @@ -6,8 +6,9 @@ package externalaccount import ( "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/auth" + issues_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/structs" @@ -62,7 +63,7 @@ func LinkAccountToUser(user *user_model.User, gothUser goth.User) error { } if tp.Name() != "" { - return models.UpdateMigrationsByType(tp, externalID, user.ID) + return UpdateMigrationsByType(tp, externalID, user.ID) } return nil @@ -77,3 +78,23 @@ func UpdateExternalUser(user *user_model.User, gothUser goth.User) error { return user_model.UpdateExternalUserByExternalID(externalLoginUser) } + +// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID +func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error { + if err := issues_model.UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + + if err := issues_model.UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + + if err := repo_model.UpdateReleasesMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + + if err := issues_model.UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + return issues_model.UpdateReviewsMigrationsByType(tp, externalUserID, userID) +} diff --git a/services/feed/action_test.go b/services/feed/action_test.go index 0d725d532d..fd84bb675b 100644 --- a/services/feed/action_test.go +++ b/services/feed/action_test.go @@ -14,6 +14,8 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" + _ "code.gitea.io/gitea/models/actions" + "github.com/stretchr/testify/assert" ) diff --git a/services/gitdiff/main_test.go b/services/gitdiff/main_test.go index a5ac274b8f..7f4243576c 100644 --- a/services/gitdiff/main_test.go +++ b/services/gitdiff/main_test.go @@ -10,6 +10,8 @@ import ( "code.gitea.io/gitea/models/unittest" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/activities" ) func TestMain(m *testing.M) { diff --git a/services/issue/main_test.go b/services/issue/main_test.go index 0f2427122f..6bce694cca 100644 --- a/services/issue/main_test.go +++ b/services/issue/main_test.go @@ -8,6 +8,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/actions" ) func TestMain(m *testing.M) { diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go index 1403923c79..88ad0c9836 100644 --- a/services/mailer/mail_team_invite.go +++ b/services/mailer/mail_team_invite.go @@ -46,8 +46,8 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod inviteRedirect := url.QueryEscape(fmt.Sprintf("/org/invite/%s", invite.Token)) inviteURL := fmt.Sprintf("%suser/sign_up?redirect_to=%s", setting.AppURL, inviteRedirect) - if err == nil && user != nil { - // user account exists + if (err == nil && user != nil) || setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration { + // user account exists or registration disabled inviteURL = fmt.Sprintf("%suser/login?redirect_to=%s", setting.AppURL, inviteRedirect) } diff --git a/services/mailer/main_test.go b/services/mailer/main_test.go index 16a6a26545..e906f4cb6e 100644 --- a/services/mailer/main_test.go +++ b/services/mailer/main_test.go @@ -8,6 +8,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/actions" ) func TestMain(m *testing.M) { diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index ee7fc57851..4c21efae44 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -31,6 +31,7 @@ import ( "code.gitea.io/gitea/modules/uri" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/pull" + repo_service "code.gitea.io/gitea/services/repository" "github.com/google/uuid" ) @@ -99,7 +100,7 @@ func (g *GiteaLocalUploader) CreateRepo(repo *base.Repository, opts base.Migrate var r *repo_model.Repository if opts.MigrateToRepoID <= 0 { - r, err = repo_module.CreateRepository(g.doer, owner, repo_module.CreateRepoOptions{ + r, err = repo_service.CreateRepositoryDirectly(g.ctx, g.doer, owner, repo_service.CreateRepoOptions{ Name: g.repoName, Description: repo.Description, OriginalURL: repo.OriginalURL, @@ -204,7 +205,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err mss = append(mss, &ms) } - err := models.InsertMilestones(mss...) + err := issues_model.InsertMilestones(mss...) if err != nil { return err } @@ -349,7 +350,7 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { rels = append(rels, &rel) } - return models.InsertReleases(rels...) + return repo_model.InsertReleases(rels...) } // SyncTags syncs releases with tags in the database @@ -429,7 +430,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { } if len(iss) > 0 { - if err := models.InsertIssues(iss...); err != nil { + if err := issues_model.InsertIssues(iss...); err != nil { return err } @@ -509,7 +510,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { if len(cms) == 0 { return nil } - return models.InsertIssueComments(cms) + return issues_model.InsertIssueComments(cms) } // CreatePullRequests creates pull requests @@ -528,7 +529,7 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error gprs = append(gprs, gpr) } - if err := models.InsertPullRequests(ctx, gprs...); err != nil { + if err := issues_model.InsertPullRequests(ctx, gprs...); err != nil { return err } for _, pr := range gprs { diff --git a/services/migrations/update.go b/services/migrations/update.go index 48b61885e8..2adca01dfe 100644 --- a/services/migrations/update.go +++ b/services/migrations/update.go @@ -6,11 +6,11 @@ package migrations import ( "context" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/externalaccount" ) // UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID @@ -62,7 +62,7 @@ func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServ default: } externalUserID := user.ExternalID - if err := models.UpdateMigrationsByType(tp, externalUserID, user.UserID); err != nil { + if err := externalaccount.UpdateMigrationsByType(tp, externalUserID, user.UserID); err != nil { log.Error("UpdateMigrationsByType type %s external user id %v to local user id %v failed: %v", tp.Name(), user.ExternalID, user.UserID, err) } } diff --git a/services/org/repo.go b/services/org/repo.go index 179249c7a8..0edbf2d464 100644 --- a/services/org/repo.go +++ b/services/org/repo.go @@ -17,7 +17,7 @@ import ( func TeamAddRepository(t *organization.Team, repo *repo_model.Repository) (err error) { if repo.OwnerID != t.OrgID { return errors.New("repository does not belong to organization") - } else if models.HasRepository(t, repo.ID) { + } else if organization.HasTeamRepo(db.DefaultContext, t.OrgID, t.ID, repo.ID) { return nil } diff --git a/services/packages/cargo/index.go b/services/packages/cargo/index.go index 867cd796d3..0561f168e1 100644 --- a/services/packages/cargo/index.go +++ b/services/packages/cargo/index.go @@ -19,10 +19,10 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" cargo_module "code.gitea.io/gitea/modules/packages/cargo" - 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/util" + repo_service "code.gitea.io/gitea/services/repository" files_service "code.gitea.io/gitea/services/repository/files" ) @@ -206,7 +206,7 @@ func getOrCreateIndexRepository(ctx context.Context, doer, owner *user_model.Use repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner.Name, IndexRepositoryName) if err != nil { if errors.Is(err, util.ErrNotExist) { - repo, err = repo_module.CreateRepository(doer, owner, repo_module.CreateRepoOptions{ + repo, err = repo_service.CreateRepositoryDirectly(ctx, doer, owner, repo_service.CreateRepoOptions{ Name: IndexRepositoryName, }) if err != nil { diff --git a/services/pull/main_test.go b/services/pull/main_test.go index 2014b19275..f5297354d6 100644 --- a/services/pull/main_test.go +++ b/services/pull/main_test.go @@ -9,6 +9,8 @@ import ( "testing" "code.gitea.io/gitea/models/unittest" + + _ "code.gitea.io/gitea/models/actions" ) func TestMain(m *testing.M) { diff --git a/services/release/release_test.go b/services/release/release_test.go index 805269413d..0732dbc54d 100644 --- a/services/release/release_test.go +++ b/services/release/release_test.go @@ -16,6 +16,8 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/services/attachment" + _ "code.gitea.io/gitea/models/actions" + "github.com/stretchr/testify/assert" ) diff --git a/services/repository/adopt.go b/services/repository/adopt.go index f225538faf..00dce7295e 100644 --- a/services/repository/adopt.go +++ b/services/repository/adopt.go @@ -27,7 +27,7 @@ import ( ) // AdoptRepository adopts pre-existing repository files for the user/organization. -func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) { +func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { if !doer.IsAdmin && !u.CanCreateRepo() { return nil, repo_model.ErrReachLimitOfRepo{ Limit: u.MaxRepoCreation, diff --git a/services/repository/archiver/archiver_test.go b/services/repository/archiver/archiver_test.go index aa4140d3ec..a1bc355d4e 100644 --- a/services/repository/archiver/archiver_test.go +++ b/services/repository/archiver/archiver_test.go @@ -12,6 +12,8 @@ import ( "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/contexttest" + _ "code.gitea.io/gitea/models/actions" + "github.com/stretchr/testify/assert" ) diff --git a/services/repository/check.go b/services/repository/check.go index 84fdb7159b..6ad644561e 100644 --- a/services/repository/check.go +++ b/services/repository/check.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" system_model "code.gitea.io/gitea/models/system" @@ -165,7 +164,7 @@ func DeleteMissingRepositories(ctx context.Context, doer *user_model.User) error default: } log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) - if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { + if err := DeleteRepositoryDirectly(ctx, doer, repo.OwnerID, repo.ID); err != nil { log.Error("Failed to DeleteRepository %-v: Error: %v", repo, err) if err2 := system_model.CreateRepositoryNotice("Failed to DeleteRepository %s [%d]: Error: %v", repo.FullName(), repo.ID, err); err2 != nil { log.Error("CreateRepositoryNotice: %v", err) diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go new file mode 100644 index 0000000000..28824d83f5 --- /dev/null +++ b/services/repository/collaboration.go @@ -0,0 +1,47 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" +) + +// DeleteCollaboration removes collaboration relation between the user and repository. +func DeleteCollaboration(repo *repo_model.Repository, uid int64) (err error) { + collaboration := &repo_model.Collaboration{ + RepoID: repo.ID, + UserID: uid, + } + + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if has, err := db.GetEngine(ctx).Delete(collaboration); err != nil || has == 0 { + return err + } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { + return err + } + + if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { + return err + } + + if err = models.ReconsiderWatches(ctx, repo, uid); err != nil { + return err + } + + // Unassign a user from any issue (s)he has been assigned to in the repository + if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/repo_collaboration_test.go b/services/repository/collaboration_test.go similarity index 97% rename from models/repo_collaboration_test.go rename to services/repository/collaboration_test.go index 95fb35fe6d..08159af7bc 100644 --- a/models/repo_collaboration_test.go +++ b/services/repository/collaboration_test.go @@ -1,7 +1,7 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package models +package repository import ( "testing" diff --git a/services/repository/create.go b/services/repository/create.go new file mode 100644 index 0000000000..09956b74d4 --- /dev/null +++ b/services/repository/create.go @@ -0,0 +1,314 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "code.gitea.io/gitea/models/db" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/options" + repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/templates/vars" + "code.gitea.io/gitea/modules/util" +) + +// CreateRepoOptions contains the create repository options +type CreateRepoOptions struct { + Name string + Description string + OriginalURL string + GitServiceType api.GitServiceType + Gitignores string + IssueLabels string + License string + Readme string + DefaultBranch string + IsPrivate bool + IsMirror bool + IsTemplate bool + AutoInit bool + Status repo_model.RepositoryStatus + TrustModel repo_model.TrustModelType + MirrorInterval string +} + +func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir, repoPath string, opts CreateRepoOptions) error { + commitTimeStr := time.Now().Format(time.RFC3339) + authorSig := repo.Owner.NewGitSig() + + // Because this may call hooks we should pass in the environment + env := append(os.Environ(), + "GIT_AUTHOR_NAME="+authorSig.Name, + "GIT_AUTHOR_EMAIL="+authorSig.Email, + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME="+authorSig.Name, + "GIT_COMMITTER_EMAIL="+authorSig.Email, + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + + // Clone to temporary path and do the init commit. + if stdout, _, err := git.NewCommand(ctx, "clone").AddDynamicArguments(repoPath, tmpDir). + SetDescription(fmt.Sprintf("prepareRepoCommit (git clone): %s to %s", repoPath, tmpDir)). + RunStdString(&git.RunOpts{Dir: "", Env: env}); err != nil { + log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err) + return fmt.Errorf("git clone: %w", err) + } + + // README + data, err := options.Readme(opts.Readme) + if err != nil { + return fmt.Errorf("GetRepoInitFile[%s]: %w", opts.Readme, err) + } + + cloneLink := repo.CloneLink() + match := map[string]string{ + "Name": repo.Name, + "Description": repo.Description, + "CloneURL.SSH": cloneLink.SSH, + "CloneURL.HTTPS": cloneLink.HTTPS, + "OwnerName": repo.OwnerName, + } + res, err := vars.Expand(string(data), match) + if err != nil { + // here we could just log the error and continue the rendering + log.Error("unable to expand template vars for repo README: %s, err: %v", opts.Readme, err) + } + if err = os.WriteFile(filepath.Join(tmpDir, "README.md"), + []byte(res), 0o644); err != nil { + return fmt.Errorf("write README.md: %w", err) + } + + // .gitignore + if len(opts.Gitignores) > 0 { + var buf bytes.Buffer + names := strings.Split(opts.Gitignores, ",") + for _, name := range names { + data, err = options.Gitignore(name) + if err != nil { + return fmt.Errorf("GetRepoInitFile[%s]: %w", name, err) + } + buf.WriteString("# ---> " + name + "\n") + buf.Write(data) + buf.WriteString("\n") + } + + if buf.Len() > 0 { + if err = os.WriteFile(filepath.Join(tmpDir, ".gitignore"), buf.Bytes(), 0o644); err != nil { + return fmt.Errorf("write .gitignore: %w", err) + } + } + } + + // LICENSE + if len(opts.License) > 0 { + data, err = repo_module.GetLicense(opts.License, &repo_module.LicenseValues{ + Owner: repo.OwnerName, + Email: authorSig.Email, + Repo: repo.Name, + Year: time.Now().Format("2006"), + }) + if err != nil { + return fmt.Errorf("getLicense[%s]: %w", opts.License, err) + } + + if err = os.WriteFile(filepath.Join(tmpDir, "LICENSE"), data, 0o644); err != nil { + return fmt.Errorf("write LICENSE: %w", err) + } + } + + return nil +} + +// InitRepository initializes README and .gitignore if needed. +func initRepository(ctx context.Context, repoPath string, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) { + if err = repo_module.CheckInitRepository(ctx, repo.OwnerName, repo.Name); err != nil { + return err + } + + // Initialize repository according to user's choice. + if opts.AutoInit { + tmpDir, err := os.MkdirTemp(os.TempDir(), "gitea-"+repo.Name) + if err != nil { + return fmt.Errorf("Failed to create temp dir for repository %s: %w", repo.RepoPath(), err) + } + defer func() { + if err := util.RemoveAll(tmpDir); err != nil { + log.Warn("Unable to remove temporary directory: %s: Error: %v", tmpDir, err) + } + }() + + if err = prepareRepoCommit(ctx, repo, tmpDir, repoPath, opts); err != nil { + return fmt.Errorf("prepareRepoCommit: %w", err) + } + + // Apply changes and commit. + if err = repo_module.InitRepoCommit(ctx, tmpDir, repo, u, opts.DefaultBranch); err != nil { + return fmt.Errorf("initRepoCommit: %w", err) + } + } + + // Re-fetch the repository from database before updating it (else it would + // override changes that were done earlier with sql) + if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil { + return fmt.Errorf("getRepositoryByID: %w", err) + } + + if !opts.AutoInit { + repo.IsEmpty = true + } + + repo.DefaultBranch = setting.Repository.DefaultBranch + + if len(opts.DefaultBranch) > 0 { + repo.DefaultBranch = opts.DefaultBranch + gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) + if err != nil { + return fmt.Errorf("openRepository: %w", err) + } + defer gitRepo.Close() + if err = gitRepo.SetDefaultBranch(repo.DefaultBranch); err != nil { + return fmt.Errorf("setDefaultBranch: %w", err) + } + + if !repo.IsEmpty { + if _, err := repo_module.SyncRepoBranches(ctx, repo.ID, u.ID); err != nil { + return fmt.Errorf("SyncRepoBranches: %w", err) + } + } + } + + if err = UpdateRepository(ctx, repo, false); err != nil { + return fmt.Errorf("updateRepository: %w", err) + } + + return nil +} + +// CreateRepositoryDirectly creates a repository for the user/organization. +func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { + if !doer.IsAdmin && !u.CanCreateRepo() { + return nil, repo_model.ErrReachLimitOfRepo{ + Limit: u.MaxRepoCreation, + } + } + + if len(opts.DefaultBranch) == 0 { + opts.DefaultBranch = setting.Repository.DefaultBranch + } + + // Check if label template exist + if len(opts.IssueLabels) > 0 { + if _, err := repo_module.LoadTemplateLabelsByDisplayName(opts.IssueLabels); err != nil { + return nil, err + } + } + + repo := &repo_model.Repository{ + OwnerID: u.ID, + Owner: u, + OwnerName: u.Name, + Name: opts.Name, + LowerName: strings.ToLower(opts.Name), + Description: opts.Description, + OriginalURL: opts.OriginalURL, + OriginalServiceType: opts.GitServiceType, + IsPrivate: opts.IsPrivate, + IsFsckEnabled: !opts.IsMirror, + IsTemplate: opts.IsTemplate, + CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, + Status: opts.Status, + IsEmpty: !opts.AutoInit, + TrustModel: opts.TrustModel, + IsMirror: opts.IsMirror, + DefaultBranch: opts.DefaultBranch, + } + + var rollbackRepo *repo_model.Repository + + if err := db.WithTx(ctx, func(ctx context.Context) error { + if err := repo_module.CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil { + return err + } + + // No need for init mirror. + if opts.IsMirror { + return nil + } + + repoPath := repo_model.RepoPath(u.Name, repo.Name) + isExist, err := util.IsExist(repoPath) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", repoPath, err) + return err + } + if isExist { + // repo already exists - We have two or three options. + // 1. We fail stating that the directory exists + // 2. We create the db repository to go with this data and adopt the git repo + // 3. We delete it and start afresh + // + // Previously Gitea would just delete and start afresh - this was naughty. + // So we will now fail and delegate to other functionality to adopt or delete + log.Error("Files already exist in %s and we are not going to adopt or delete.", repoPath) + return repo_model.ErrRepoFilesAlreadyExist{ + Uname: u.Name, + Name: repo.Name, + } + } + + if err = initRepository(ctx, repoPath, doer, repo, opts); err != nil { + if err2 := util.RemoveAll(repoPath); err2 != nil { + log.Error("initRepository: %v", err) + return fmt.Errorf( + "delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2) + } + return fmt.Errorf("initRepository: %w", err) + } + + // Initialize Issue Labels if selected + if len(opts.IssueLabels) > 0 { + if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil { + rollbackRepo = repo + rollbackRepo.OwnerID = u.ID + return fmt.Errorf("InitializeLabels: %w", err) + } + } + + if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil { + return fmt.Errorf("checkDaemonExportOK: %w", err) + } + + if stdout, _, err := git.NewCommand(ctx, "update-server-info"). + SetDescription(fmt.Sprintf("CreateRepository(git update-server-info): %s", repoPath)). + RunStdString(&git.RunOpts{Dir: repoPath}); err != nil { + log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err) + rollbackRepo = repo + rollbackRepo.OwnerID = u.ID + return fmt.Errorf("CreateRepository(git update-server-info): %w", err) + } + return nil + }); err != nil { + if rollbackRepo != nil { + if errDelete := DeleteRepositoryDirectly(ctx, doer, rollbackRepo.OwnerID, rollbackRepo.ID); errDelete != nil { + log.Error("Rollback deleteRepository: %v", errDelete) + } + } + + return nil, err + } + + return repo, nil +} diff --git a/services/repository/create_test.go b/services/repository/create_test.go new file mode 100644 index 0000000000..78be93bf12 --- /dev/null +++ b/services/repository/create_test.go @@ -0,0 +1,148 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "fmt" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/structs" + + "github.com/stretchr/testify/assert" +) + +func TestIncludesAllRepositoriesTeams(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testTeamRepositories := func(teamID int64, repoIds []int64) { + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) + assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name) + assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name) + assert.Len(t, team.Repos, len(repoIds), "%s: repo count", team.Name) + for i, rid := range repoIds { + if rid > 0 { + assert.True(t, HasRepository(team, rid), "%s: HasRepository(%d) %d", rid, i) + } + } + } + + // Get an admin user. + user, err := user_model.GetUserByID(db.DefaultContext, 1) + assert.NoError(t, err, "GetUserByID") + + // Create org. + org := &organization.Organization{ + Name: "All_repo", + IsActive: true, + Type: user_model.UserTypeOrganization, + Visibility: structs.VisibleTypePublic, + } + assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization") + + // Check Owner team. + ownerTeam, err := org.GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err, "GetOwnerTeam") + assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories") + + // Create repos. + repoIds := make([]int64, 0) + for i := 0; i < 3; i++ { + r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)}) + assert.NoError(t, err, "CreateRepository %d", i) + if r != nil { + repoIds = append(repoIds, r.ID) + } + } + // Get fresh copy of Owner team after creating repos. + ownerTeam, err = org.GetOwnerTeam(db.DefaultContext) + assert.NoError(t, err, "GetOwnerTeam") + + // Create teams and check repositories. + teams := []*organization.Team{ + ownerTeam, + { + OrgID: org.ID, + Name: "team one", + AccessMode: perm.AccessModeRead, + IncludesAllRepositories: true, + }, + { + OrgID: org.ID, + Name: "team 2", + AccessMode: perm.AccessModeRead, + IncludesAllRepositories: false, + }, + { + OrgID: org.ID, + Name: "team three", + AccessMode: perm.AccessModeWrite, + IncludesAllRepositories: true, + }, + { + OrgID: org.ID, + Name: "team 4", + AccessMode: perm.AccessModeWrite, + IncludesAllRepositories: false, + }, + } + teamRepos := [][]int64{ + repoIds, + repoIds, + {}, + repoIds, + {}, + } + for i, team := range teams { + if i > 0 { // first team is Owner. + assert.NoError(t, models.NewTeam(team), "%s: NewTeam", team.Name) + } + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Update teams and check repositories. + teams[3].IncludesAllRepositories = false + teams[4].IncludesAllRepositories = true + teamRepos[4] = repoIds + for i, team := range teams { + assert.NoError(t, models.UpdateTeam(team, false, true), "%s: UpdateTeam", team.Name) + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Create repo and check teams repositories. + r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: "repo-last"}) + assert.NoError(t, err, "CreateRepository last") + if r != nil { + repoIds = append(repoIds, r.ID) + } + teamRepos[0] = repoIds + teamRepos[1] = repoIds + teamRepos[4] = repoIds + for i, team := range teams { + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Remove repo and check teams repositories. + assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, org.ID, repoIds[0]), "DeleteRepository") + teamRepos[0] = repoIds[1:] + teamRepos[1] = repoIds[1:] + teamRepos[3] = repoIds[1:3] + teamRepos[4] = repoIds[1:] + for i, team := range teams { + testTeamRepositories(team.ID, teamRepos[i]) + } + + // Wipe created items. + for i, rid := range repoIds { + if i > 0 { // first repo already deleted. + assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, org.ID, rid), "DeleteRepository %d", i) + } + } + assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization") +} diff --git a/services/repository/delete.go b/services/repository/delete.go new file mode 100644 index 0000000000..8e28c9b255 --- /dev/null +++ b/services/repository/delete.go @@ -0,0 +1,424 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "context" + "fmt" + + "code.gitea.io/gitea/models" + actions_model "code.gitea.io/gitea/models/actions" + activities_model "code.gitea.io/gitea/models/activities" + admin_model "code.gitea.io/gitea/models/admin" + asymkey_model "code.gitea.io/gitea/models/asymkey" + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/organization" + access_model "code.gitea.io/gitea/models/perm/access" + project_model "code.gitea.io/gitea/models/project" + repo_model "code.gitea.io/gitea/models/repo" + secret_model "code.gitea.io/gitea/models/secret" + system_model "code.gitea.io/gitea/models/system" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/models/webhook" + actions_module "code.gitea.io/gitea/modules/actions" + "code.gitea.io/gitea/modules/lfs" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/storage" + + "xorm.io/builder" +) + +// DeleteRepository deletes a repository for a user or organization. +// make sure if you call this func to close open sessions (sqlite will otherwise get a deadlock) +func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, uid, repoID int64) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + // Query the action tasks of this repo, they will be needed after they have been deleted to remove the logs + tasks, err := actions_model.FindTasks(ctx, actions_model.FindTaskOptions{RepoID: repoID}) + if err != nil { + return fmt.Errorf("find actions tasks of repo %v: %w", repoID, err) + } + + // Query the artifacts of this repo, they will be needed after they have been deleted to remove artifacts files in ObjectStorage + artifacts, err := actions_model.ListArtifactsByRepoID(ctx, repoID) + if err != nil { + return fmt.Errorf("list actions artifacts of repo %v: %w", repoID, err) + } + + // In case is a organization. + org, err := user_model.GetUserByID(ctx, uid) + if err != nil { + return err + } + + repo := &repo_model.Repository{OwnerID: uid} + has, err := sess.ID(repoID).Get(repo) + if err != nil { + return err + } else if !has { + return repo_model.ErrRepoNotExist{ + ID: repoID, + UID: uid, + OwnerName: "", + Name: "", + } + } + + // Delete Deploy Keys + deployKeys, err := asymkey_model.ListDeployKeys(ctx, &asymkey_model.ListDeployKeysOptions{RepoID: repoID}) + if err != nil { + return fmt.Errorf("listDeployKeys: %w", err) + } + needRewriteKeysFile := len(deployKeys) > 0 + for _, dKey := range deployKeys { + if err := models.DeleteDeployKey(ctx, doer, dKey.ID); err != nil { + return fmt.Errorf("deleteDeployKeys: %w", err) + } + } + + if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil { + return err + } else if cnt != 1 { + return repo_model.ErrRepoNotExist{ + ID: repoID, + UID: uid, + OwnerName: "", + Name: "", + } + } + + if org.IsOrganization() { + teams, err := organization.FindOrgTeams(ctx, org.ID) + if err != nil { + return err + } + for _, t := range teams { + if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID) { + continue + } else if err = removeRepositoryFromTeam(ctx, t, repo, false); err != nil { + return err + } + } + } + + attachments := make([]*repo_model.Attachment, 0, 20) + if err = sess.Join("INNER", "`release`", "`release`.id = `attachment`.release_id"). + Where("`release`.repo_id = ?", repoID). + Find(&attachments); err != nil { + return err + } + releaseAttachments := make([]string, 0, len(attachments)) + for i := 0; i < len(attachments); i++ { + releaseAttachments = append(releaseAttachments, attachments[i].RelativePath()) + } + + if _, err := db.Exec(ctx, "UPDATE `user` SET num_stars=num_stars-1 WHERE id IN (SELECT `uid` FROM `star` WHERE repo_id = ?)", repo.ID); err != nil { + return err + } + + if _, err := db.GetEngine(ctx).In("hook_id", builder.Select("id").From("webhook").Where(builder.Eq{"webhook.repo_id": repo.ID})). + Delete(&webhook.HookTask{}); err != nil { + return err + } + + if err := db.DeleteBeans(ctx, + &access_model.Access{RepoID: repo.ID}, + &activities_model.Action{RepoID: repo.ID}, + &repo_model.Collaboration{RepoID: repoID}, + &issues_model.Comment{RefRepoID: repoID}, + &git_model.CommitStatus{RepoID: repoID}, + &git_model.Branch{RepoID: repoID}, + &git_model.LFSLock{RepoID: repoID}, + &repo_model.LanguageStat{RepoID: repoID}, + &issues_model.Milestone{RepoID: repoID}, + &repo_model.Mirror{RepoID: repoID}, + &activities_model.Notification{RepoID: repoID}, + &git_model.ProtectedBranch{RepoID: repoID}, + &git_model.ProtectedTag{RepoID: repoID}, + &repo_model.PushMirror{RepoID: repoID}, + &repo_model.Release{RepoID: repoID}, + &repo_model.RepoIndexerStatus{RepoID: repoID}, + &repo_model.Redirect{RedirectRepoID: repoID}, + &repo_model.RepoUnit{RepoID: repoID}, + &repo_model.Star{RepoID: repoID}, + &admin_model.Task{RepoID: repoID}, + &repo_model.Watch{RepoID: repoID}, + &webhook.Webhook{RepoID: repoID}, + &secret_model.Secret{RepoID: repoID}, + &actions_model.ActionTaskStep{RepoID: repoID}, + &actions_model.ActionTask{RepoID: repoID}, + &actions_model.ActionRunJob{RepoID: repoID}, + &actions_model.ActionRun{RepoID: repoID}, + &actions_model.ActionRunner{RepoID: repoID}, + &actions_model.ActionScheduleSpec{RepoID: repoID}, + &actions_model.ActionSchedule{RepoID: repoID}, + &actions_model.ActionArtifact{RepoID: repoID}, + ); err != nil { + return fmt.Errorf("deleteBeans: %w", err) + } + + // Delete Labels and related objects + if err := issues_model.DeleteLabelsByRepoID(ctx, repoID); err != nil { + return err + } + + // Delete Pulls and related objects + if err := issues_model.DeletePullsByBaseRepoID(ctx, repoID); err != nil { + return err + } + + // Delete Issues and related objects + var attachmentPaths []string + if attachmentPaths, err = issues_model.DeleteIssuesByRepoID(ctx, repoID); err != nil { + return err + } + + // Delete issue index + if err := db.DeleteResourceIndex(ctx, "issue_index", repoID); err != nil { + return err + } + + if repo.IsFork { + if _, err := db.Exec(ctx, "UPDATE `repository` SET num_forks=num_forks-1 WHERE id=?", repo.ForkID); err != nil { + return fmt.Errorf("decrease fork count: %w", err) + } + } + + if _, err := db.Exec(ctx, "UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", uid); err != nil { + return err + } + + if len(repo.Topics) > 0 { + if err := repo_model.RemoveTopicsFromRepo(ctx, repo.ID); err != nil { + return err + } + } + + if err := project_model.DeleteProjectByRepoID(ctx, repoID); err != nil { + return fmt.Errorf("unable to delete projects for repo[%d]: %w", repoID, err) + } + + // Remove LFS objects + var lfsObjects []*git_model.LFSMetaObject + if err = sess.Where("repository_id=?", repoID).Find(&lfsObjects); err != nil { + return err + } + + lfsPaths := make([]string, 0, len(lfsObjects)) + for _, v := range lfsObjects { + count, err := db.CountByBean(ctx, &git_model.LFSMetaObject{Pointer: lfs.Pointer{Oid: v.Oid}}) + if err != nil { + return err + } + if count > 1 { + continue + } + + lfsPaths = append(lfsPaths, v.RelativePath()) + } + + if _, err := db.DeleteByBean(ctx, &git_model.LFSMetaObject{RepositoryID: repoID}); err != nil { + return err + } + + // Remove archives + var archives []*repo_model.RepoArchiver + if err = sess.Where("repo_id=?", repoID).Find(&archives); err != nil { + return err + } + + archivePaths := make([]string, 0, len(archives)) + for _, v := range archives { + archivePaths = append(archivePaths, v.RelativePath()) + } + + if _, err := db.DeleteByBean(ctx, &repo_model.RepoArchiver{RepoID: repoID}); err != nil { + return err + } + + if repo.NumForks > 0 { + if _, err = sess.Exec("UPDATE `repository` SET fork_id=0,is_fork=? WHERE fork_id=?", false, repo.ID); err != nil { + log.Error("reset 'fork_id' and 'is_fork': %v", err) + } + } + + // Get all attachments with both issue_id and release_id are zero + var newAttachments []*repo_model.Attachment + if err := sess.Where(builder.Eq{ + "repo_id": repo.ID, + "issue_id": 0, + "release_id": 0, + }).Find(&newAttachments); err != nil { + return err + } + + newAttachmentPaths := make([]string, 0, len(newAttachments)) + for _, attach := range newAttachments { + newAttachmentPaths = append(newAttachmentPaths, attach.RelativePath()) + } + + if _, err := sess.Where("repo_id=?", repo.ID).Delete(new(repo_model.Attachment)); err != nil { + return err + } + + if err = committer.Commit(); err != nil { + return err + } + + committer.Close() + + if needRewriteKeysFile { + if err := asymkey_model.RewriteAllPublicKeys(); err != nil { + log.Error("RewriteAllPublicKeys failed: %v", err) + } + } + + // We should always delete the files after the database transaction succeed. If + // we delete the file but the database rollback, the repository will be broken. + + // Remove repository files. + repoPath := repo.RepoPath() + system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository files", repoPath) + + // Remove wiki files + if repo.HasWiki() { + system_model.RemoveAllWithNotice(db.DefaultContext, "Delete repository wiki", repo.WikiPath()) + } + + // Remove archives + for _, archive := range archivePaths { + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.RepoArchives, "Delete repo archive file", archive) + } + + // Remove lfs objects + for _, lfsObj := range lfsPaths { + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.LFS, "Delete orphaned LFS file", lfsObj) + } + + // Remove issue attachment files. + for _, attachment := range attachmentPaths { + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", attachment) + } + + // Remove release attachment files. + for _, releaseAttachment := range releaseAttachments { + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete release attachment", releaseAttachment) + } + + // Remove attachment with no issue_id and release_id. + for _, newAttachment := range newAttachmentPaths { + system_model.RemoveStorageWithNotice(db.DefaultContext, storage.Attachments, "Delete issue attachment", newAttachment) + } + + if len(repo.Avatar) > 0 { + if err := storage.RepoAvatars.Delete(repo.CustomAvatarRelativePath()); err != nil { + return fmt.Errorf("Failed to remove %s: %w", repo.Avatar, err) + } + } + + // Finally, delete action logs after the actions have already been deleted to avoid new log files + for _, task := range tasks { + err := actions_module.RemoveLogs(ctx, task.LogInStorage, task.LogFilename) + if err != nil { + log.Error("remove log file %q: %v", task.LogFilename, err) + // go on + } + } + + // delete actions artifacts in ObjectStorage after the repo have already been deleted + for _, art := range artifacts { + if err := storage.ActionsArtifacts.Delete(art.StoragePath); err != nil { + log.Error("remove artifact file %q: %v", art.StoragePath, err) + // go on + } + } + + return nil +} + +// removeRepositoryFromTeam removes a repository from a team and recalculates access +// Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted) +func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *repo_model.Repository, recalculate bool) (err error) { + e := db.GetEngine(ctx) + if err = organization.RemoveTeamRepo(ctx, t.ID, repo.ID); err != nil { + return err + } + + t.NumRepos-- + if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil { + return err + } + + // Don't need to recalculate when delete a repository from organization. + if recalculate { + if err = access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil { + return err + } + } + + teamUsers, err := organization.GetTeamUsersByTeamID(ctx, t.ID) + if err != nil { + return fmt.Errorf("getTeamUsersByTeamID: %w", err) + } + for _, teamUser := range teamUsers { + has, err := access_model.HasAccess(ctx, teamUser.UID, repo) + if err != nil { + return err + } else if has { + continue + } + + if err = repo_model.WatchRepo(ctx, teamUser.UID, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repositories + if err := issues_model.RemoveIssueWatchersByRepoID(ctx, teamUser.UID, repo.ID); err != nil { + return err + } + } + + return nil +} + +// HasRepository returns true if given repository belong to team. +func HasRepository(t *organization.Team, repoID int64) bool { + return organization.HasTeamRepo(db.DefaultContext, t.OrgID, t.ID, repoID) +} + +// RemoveRepositoryFromTeam removes repository from team of organization. +// If the team shall include all repositories the request is ignored. +func RemoveRepositoryFromTeam(ctx context.Context, t *organization.Team, repoID int64) error { + if !HasRepository(t, repoID) { + return nil + } + + if t.IncludesAllRepositories { + return nil + } + + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + return err + } + + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if err = removeRepositoryFromTeam(ctx, t, repo, true); err != nil { + return err + } + + return committer.Commit() +} diff --git a/services/repository/delete_test.go b/services/repository/delete_test.go new file mode 100644 index 0000000000..2905c01868 --- /dev/null +++ b/services/repository/delete_test.go @@ -0,0 +1,45 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestTeam_HasRepository(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + test := func(teamID, repoID int64, expected bool) { + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) + assert.Equal(t, expected, HasRepository(team, repoID)) + } + test(1, 1, false) + test(1, 3, true) + test(1, 5, true) + test(1, unittest.NonexistentID, false) + + test(2, 3, true) + test(2, 5, false) +} + +func TestTeam_RemoveRepository(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + testSuccess := func(teamID, repoID int64) { + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) + assert.NoError(t, RemoveRepositoryFromTeam(db.DefaultContext, team, repoID)) + unittest.AssertNotExistsBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repoID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &repo_model.Repository{ID: repoID}) + } + testSuccess(2, 3) + testSuccess(2, 5) + testSuccess(1, unittest.NonexistentID) +} diff --git a/services/repository/files/content_test.go b/services/repository/files/content_test.go index 3e4c1e2c70..d591c46839 100644 --- a/services/repository/files/content_test.go +++ b/services/repository/files/content_test.go @@ -13,6 +13,8 @@ import ( "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" + _ "code.gitea.io/gitea/models/actions" + "github.com/stretchr/testify/assert" ) diff --git a/services/repository/repository.go b/services/repository/repository.go index 47e96bd5e5..60f9568b54 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -7,7 +7,6 @@ import ( "context" "fmt" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -40,8 +39,8 @@ type WebSearchResults struct { } // CreateRepository creates a repository for the user/organization. -func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts repo_module.CreateRepoOptions) (*repo_model.Repository, error) { - repo, err := repo_module.CreateRepository(doer, owner, opts) +func CreateRepository(ctx context.Context, doer, owner *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) { + repo, err := CreateRepositoryDirectly(ctx, doer, owner, opts) if err != nil { // No need to rollback here we should do this in CreateRepository... return nil, err @@ -63,7 +62,7 @@ func DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_mod notify_service.DeleteRepository(ctx, doer, repo) } - if err := models.DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { + if err := DeleteRepositoryDirectly(ctx, doer, repo.OwnerID, repo.ID); err != nil { return err } @@ -84,7 +83,7 @@ func PushCreateRepo(ctx context.Context, authUser, owner *user_model.User, repoN } } - repo, err := CreateRepository(ctx, authUser, owner, repo_module.CreateRepoOptions{ + repo, err := CreateRepository(ctx, authUser, owner, CreateRepoOptions{ Name: repoName, IsPrivate: setting.Repository.DefaultPushCreatePrivate, }) diff --git a/services/task/task.go b/services/task/task.go index db5c1dd3f8..3a40faef90 100644 --- a/services/task/task.go +++ b/services/task/task.go @@ -7,6 +7,7 @@ import ( "fmt" admin_model "code.gitea.io/gitea/models/admin" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/graceful" @@ -14,12 +15,12 @@ import ( "code.gitea.io/gitea/modules/log" base "code.gitea.io/gitea/modules/migration" "code.gitea.io/gitea/modules/queue" - repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/secret" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + repo_service "code.gitea.io/gitea/services/repository" ) // taskQueue is a global queue of tasks @@ -100,7 +101,7 @@ func CreateMigrateTask(doer, u *user_model.User, opts base.MigrateOptions) (*adm return nil, err } - repo, err := repo_module.CreateRepository(doer, u, repo_module.CreateRepoOptions{ + repo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, doer, u, repo_service.CreateRepoOptions{ Name: opts.RepoName, Description: opts.Description, OriginalURL: opts.OriginalURL, diff --git a/services/user/user.go b/services/user/user.go index bb3dd002ea..72bea0b468 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/services/agit" "code.gitea.io/gitea/services/packages" container_service "code.gitea.io/gitea/services/packages/container" + repo_service "code.gitea.io/gitea/services/repository" ) // RenameUser renames a user @@ -174,7 +175,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error { break } for _, repo := range repos { - if err := models.DeleteRepository(u, u.ID, repo.ID); err != nil { + if err := repo_service.DeleteRepositoryDirectly(ctx, u, u.ID, repo.ID); err != nil { return fmt.Errorf("unable to delete repository %s for %s[%d]. Error: %w", repo.Name, u.Name, u.ID, err) } } diff --git a/services/webhook/main_test.go b/services/webhook/main_test.go index 0189e17840..cd34c02b5c 100644 --- a/services/webhook/main_test.go +++ b/services/webhook/main_test.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/setting" _ "code.gitea.io/gitea/models" + _ "code.gitea.io/gitea/models/actions" ) func TestMain(m *testing.M) { diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index 85d99806fe..0621456f3e 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -14,6 +14,8 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" + _ "code.gitea.io/gitea/models/actions" + "github.com/stretchr/testify/assert" ) diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index e72e7bba62..f565318b8b 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -1,33 +1,13 @@ {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin settings new webhook")}}
-

- {{if .PageIsAdminDefaultHooksNew}} - {{.locale.Tr "admin.defaulthooks.add_webhook"}} - {{else if .PageIsAdminSystemHooksNew}} - {{.locale.Tr "admin.systemhooks.add_webhook"}} - {{else if .Webhook.IsSystemWebhook}} - {{.locale.Tr "admin.systemhooks.update_webhook"}} - {{else}} - {{.locale.Tr "admin.defaulthooks.update_webhook"}} - {{end}} -
- {{template "shared/webhook/icon" .}} -
-

-
- {{template "repo/settings/webhook/gitea" .}} - {{template "repo/settings/webhook/gogs" .}} - {{template "repo/settings/webhook/slack" .}} - {{template "repo/settings/webhook/discord" .}} - {{template "repo/settings/webhook/dingtalk" .}} - {{template "repo/settings/webhook/telegram" .}} - {{template "repo/settings/webhook/msteams" .}} - {{template "repo/settings/webhook/feishu" .}} - {{template "repo/settings/webhook/matrix" .}} - {{template "repo/settings/webhook/wechatwork" .}} - {{template "repo/settings/webhook/packagist" .}} -
- - {{template "repo/settings/webhook/history" .}} + {{$CustomHeaderTitle := .locale.Tr "admin.defaulthooks.update_webhook"}} + {{if .PageIsAdminDefaultHooksNew}} + {{$CustomHeaderTitle = .locale.Tr "admin.defaulthooks.add_webhook"}} + {{else if .PageIsAdminSystemHooksNew}} + {{$CustomHeaderTitle = .locale.Tr "admin.systemhooks.add_webhook"}} + {{else if .Webhook.IsSystemWebhook}} + {{$CustomHeaderTitle = .locale.Tr "admin.systemhooks.update_webhook"}} + {{end}} + {{template "webhook/new" (dict "ctxData" . "CustomHeaderTitle" $CustomHeaderTitle)}}
{{template "admin/layout_footer" .}} diff --git a/templates/devtest/fomantic-modal.tmpl b/templates/devtest/fomantic-modal.tmpl index b91b29f3cc..ec7d2cd215 100644 --- a/templates/devtest/fomantic-modal.tmpl +++ b/templates/devtest/fomantic-modal.tmpl @@ -30,6 +30,16 @@ + +