diff --git a/.deadcode-out b/.deadcode-out index 97093ce93b..b70be3e800 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -19,10 +19,10 @@ forgejo.org/models/auth forgejo.org/models/db TruncateBeans TruncateBeansCascade - InTransaction DumpTables GetTableNames extendBeansForCascade + IsErrNameActivityPubInvalid forgejo.org/models/dbfs file.renameTo @@ -58,7 +58,6 @@ forgejo.org/models/user IsErrUserSettingIsNotExist GetUserAllSettings DeleteUserSetting - GetFederatedUser forgejo.org/modules/activitypub NewContext @@ -88,7 +87,6 @@ forgejo.org/modules/eventsource Event.String forgejo.org/modules/forgefed - NewForgeFollow NewForgeUndoLike ForgeUndoLike.UnmarshalJSON ForgeUndoLike.Validate @@ -134,6 +132,9 @@ forgejo.org/modules/json StdJSON.Indent forgejo.org/modules/log + eventWriterBuffer.Close + eventWriterBuffer.Write + eventWriterBuffer.GetString NewEventWriterBuffer forgejo.org/modules/markup @@ -224,9 +225,6 @@ forgejo.org/routers/web/org forgejo.org/services/context GetPrivateContext -forgejo.org/services/federation - FollowRemoteActor - forgejo.org/services/notify UnregisterNotifier diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fb1a99295f..46b85ad93d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { - "name": "Gitea DevContainer", - "image": "mcr.microsoft.com/devcontainers/go:1.25-trixie", + "name": "forgejo-dev", + "image": "mcr.microsoft.com/devcontainers/go:1.26-trixie", "features": { // installs nodejs into container "ghcr.io/devcontainers/features/node:1": { diff --git a/.editorconfig b/.editorconfig index 5476eb02fb..58daceb4f1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -30,7 +30,6 @@ insert_final_newline = false [options/locale/locale_*.ini] insert_final_newline = false -# Weblate JSON output defaults to four spaces +# Weblate is configured to use one tab for indention [options/locale_next/locale_*.json] -indent_style = space -indent_size = 4 +indent_style = tab diff --git a/.forgejo/issue_template/1-problem.yaml b/.forgejo/issue_template/1-problem.yaml index 39008de1d2..516360a5c4 100644 --- a/.forgejo/issue_template/1-problem.yaml +++ b/.forgejo/issue_template/1-problem.yaml @@ -13,7 +13,18 @@ body: - Please speak English, as this is the language all maintainers can speak and write. - Be civil, and follow the [Forgejo Code of Conduct](https://codeberg.org/forgejo/code-of-conduct). - Take a moment to [check if a similar problem has already been discussed in the past.](https://codeberg.org/forgejo/forgejo/issues?q=&type=all&labels=78137). Feel free to add your own experience there, if applicable. - - We are currently experimenting with a new workflow to understand your problems and requests. Please try to focus on explaining the problem you are facing, which could be a bug in the code, a moment of confusion, or a missing feature. We'll think about solutions to the problem at a later step. If you want to learn more about the background of our workflow, feel invited to read and participate in [the discussion that led to the current approach](https://codeberg.org/forgejo/discussions/issues/415). We are looking forward to your feedback. +- type: markdown + attributes: + value: | + ### New workflow + + We are currently experimenting with a new workflow to manage issues and better understand your problems and needs. This is step 1 of the workflow: Please try to focus on explaining the problem you are facing, which could be a bug in the code, a moment of confusion, or a need that you have. + + We do not expect anything from Forgejo users after creating a problem report, but we appreciate if you stay responsive to further questions. If you want, you can also participate in a discussion for solutions. + + Forgejo contributors will review your report, try to understand how important it is to you and other Forgejo users, and suggest potential solutions. In a next step, solutions can be documented and implemented. + + If you want to learn more about the background of our workflow, feel invited to read and participate in [the discussion that led to the current approach](https://codeberg.org/forgejo/discussions/issues/415). We are looking forward to your feedback. - type: dropdown id: can-reproduce attributes: @@ -30,15 +41,18 @@ body: - type: textarea id: environment attributes: - label: Your usage of Forgejo + label: About your usage of Forgejo description: | - Please provide a description of your usage of Forgejo. To better understand your problem, it will be relevant to know in which environment you use Forgejo and if you have performed specific configuration. + Please provide a brief description of your usage of Forgejo. There is no clear guideline on how much you need to share here. You can be brief, but we value the insights you provide us to better understand your use case. Thank you very much!
Further instructions - * If you report a bug with a certain functionality, it will be relevant to learn about related configuration ("This is how I configured my identity provider", "This is how my Forgejo Actions runner is set up"). - * Please elaborate on your personal relation to Forgejo and user role ("I'm new to Forgejo, but used a comparable product called …", "In my role as a designer, …"). - * If you feel that Forgejo needs a change in functionality, please describe in which environment you want to use it, so that we can understand for which audience the complexity needs to appeal to ("We're a group of friends with no technical / professional background and use a personal Forgejo instance to prepare our first libre game"). - * If you already explained your usage of Forgejo elsewhere (e.g. in another issue or in a user research repository), you can put a link here. + * When reporting problems with certain functionality, you should include related information. Examples: + * When reporting an issue with setting up an identity provider, it is useful to know if you use Forgejo in a 10-users non-profit / start up, or if you are talking about a school / university with several thousands of users. + * When describing confusion, it will be relevant to know your skill level and background ("New, but used a similar product", "In my role as a project manager ..."), so we know for which target audience we need to design the functionality. + * When reporting workflow issues or needs, it will be useful to know how large your project is, how many team members you have, which skill level we are talking about etc. For example, we might choose a different design depending on whether a feature is only for professional developers or for hobby coders. + * If you want, we always appreciate generic information about your Forgejo usage to help us understand your usage and make better decisions. For example: + * Your personal relation to Forgejo and user role ("I'm new to Forgejo, but used a comparable product called …", "In my role as a designer, …"). + * If you already explained your usage of Forgejo elsewhere (e.g. in another issue or in a user research repository), feel free to just drop a link.
validations: @@ -48,7 +62,7 @@ body: attributes: label: Problem description description: | - Please describe your problem as a first-hand experience. Try to focus on the problem. Finding a solution will happen in a next step. + Please describe your problem as a first-hand experience. Try to focus on the problem. You do not have to find a solution, you can leave this to us.
Further instructions * Start by explaining what you want to achieve ("I wanted to find an issue to work on"). @@ -75,7 +89,7 @@ body: id: forgejo-ver attributes: label: Forgejo Version - description: Forgejo version (or commit reference) your instance is running + description: Forgejo version (or commit reference) your instance is running or that you used to reproduce the bug on Forgejo Next. - type: textarea id: versions attributes: @@ -84,3 +98,10 @@ body: Please include details to help us understand your problem: browser engine and version (for UI issues), operating system and version running Forgejo, database engine and version, deployment methods and relevant third-party packages (e.g. renderers, identity providers) validations: required: true +- type: markdown + attributes: + value: | + ### Solutions + + *Accepted solutions to address this problem will go here* + visible: [content] diff --git a/.forgejo/issue_template/2-enhancement.yaml b/.forgejo/issue_template/2-enhancement.yaml index 3014d8901a..68594152b6 100644 --- a/.forgejo/issue_template/2-enhancement.yaml +++ b/.forgejo/issue_template/2-enhancement.yaml @@ -1,5 +1,5 @@ name: "Step 2: Enhancement" -description: Suggest a solution to one or multiple problems that have already been reported (see step 1). +description: "[Advanced users only] Suggest a solution to one or multiple problems that have already been reported (see step 1)." title: "enh: " labels: ["enhancement/feature"] body: @@ -8,19 +8,28 @@ body: value: | - Please speak English, as this is the language all maintainers can speak and write. - Be civil, and follow the [Forgejo Code of Conduct](https://codeberg.org/forgejo/code-of-conduct). - - We are currently experimenting with a new workflow to understand your problems and requests. This is step 2 of a two-step-process. The goal is to discuss potential solutions to already-reported problems. If you want to learn more about the background of our workflow, feel invited to read and participate in [the discussion that led to the current approach](https://codeberg.org/forgejo/discussions/issues/415). We are looking forward to your feedback. +- type: markdown + attributes: + value: | + ### New workflow + + We are currently experimenting with a new workflow to manage issues and better understand your problems and needs. This is step 2 of the workflow, which is intended for Forgejo contributors: If you just want to raise a problem or need you have, please refer to step 1. + + This step allows to document a solution to one or multiple problems. It allows developers to focus on actionable implementation tasks without the clutter of previous discussions or triaging work. + + If you want to learn more about the background of our workflow, feel invited to read and participate in [the discussion that led to the current approach](https://codeberg.org/forgejo/discussions/issues/415). We are looking forward to your feedback. - type: textarea id: problems attributes: label: Existing problems this enhancement addresses - description: List the issue numbers of the reported problems that this enhancement addresses. If you haven't previously described a problem, please [complete step one of the workflow](https://codeberg.org/forgejo/forgejo/issues/new?template=.forgejo%2fissue_template%2fproblem.yaml) and describe the problem you'd like to solve first. + description: Only list the issue numbers of the **existing** problems that your proposal addresses. **Do not add new descriptions.** If you haven't previously described a problem, please [complete step one of the workflow](https://codeberg.org/forgejo/forgejo/issues/new?template=.forgejo%2fissue_template%2fproblem.yaml) and describe the problem you'd like to solve first. validations: required: true - type: textarea id: description attributes: label: Enhancement description - description: As concisely as possible, describe the changes you suggest for Forgejo and explain how they address the problems. + description: Describe the changes you suggest for Forgejo and explain how they address the problems. validations: required: true - type: textarea diff --git a/.forgejo/issue_template/config.yml b/.forgejo/issue_template/config.yml index f2ea8d945a..b4bd06af17 100644 --- a/.forgejo/issue_template/config.yml +++ b/.forgejo/issue_template/config.yml @@ -1,3 +1,4 @@ +blank_issues_enabled: false contact_links: - name: 🔓 Security Reports url: mailto:security@forgejo.org diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md index d30af48446..cfdb8a5336 100644 --- a/.forgejo/pull_request_template.md +++ b/.forgejo/pull_request_template.md @@ -10,13 +10,22 @@ labels: ## Checklist -The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). +The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). -### Tests +### Tests for Go changes + +(can be removed for JavaScript changes) - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. +- I ran... + - [ ] `make pr-go` before pushing + +### Tests for JavaScript changes + +(can be removed for Go changes) + - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). @@ -28,6 +37,9 @@ The [contributor guide](https://forgejo.org/docs/next/contributor/) contains inf ### Release notes -- [ ] I do not want this change to show in the release notes. -- [ ] I want the title to show in the release notes with a link to this pull request. -- [ ] I want the content of the `release-notes/.md` to be be used for the release notes instead of the title. +- [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. +- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. + +*The decision if the pull request will be shown in the release notes is up to the mergers / release team.* + +The content of the `release-notes/.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead. diff --git a/renovate.json b/.forgejo/renovate.json similarity index 82% rename from renovate.json rename to .forgejo/renovate.json index 6d75cc303c..758ae282a8 100644 --- a/renovate.json +++ b/.forgejo/renovate.json @@ -9,11 +9,12 @@ "baseBranchPatterns": [ "$default", "/^v11\\.\\d+/forgejo$/", - "/^v13\\.\\d+/forgejo$/" + "/^v15\\.\\d+/forgejo$/" ], "postUpdateOptions": ["gomodTidy", "gomodUpdateImportPaths", "npmDedupe"], "prConcurrentLimit": 10, - "osvVulnerabilityAlerts": true, + "dependencyDashboardReportAbandonment": false, + "dependencyDashboardHeader": "This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more.\n- [ ] Check this box to trigger a request for Renovate to run again on this repository\n", "automergeStrategy": "squash", "labels": ["dependency-upgrade", "test/not-needed"], "packageRules": [ @@ -137,6 +138,13 @@ ], "automerge": true }, + { + "description": "Run end-to-end tests for some dependencies", + "matchPackageNames": [ + "code.forgejo.org/forgejo/runner/**" + ], + "addLabels": ["run-end-to-end-tests"] + }, { "description": "Disable indirect updates for stable branches", "matchBaseBranches": ["/^v\\d+\\.\\d+\\/forgejo$/"], @@ -145,6 +153,18 @@ "matchDepTypes": ["indirect"], "enabled": false }, + { + "description": "Disable major updates for stable branches", + "matchBaseBranches": ["/^v\\d+\\.\\d+\\/forgejo$/"], + "matchUpdateTypes": ["major"], + "enabled": false + }, + { + "description": "Disable updates for old stable branches but still allow security updates", + "matchBaseBranches": ["v11.0/forgejo", "v14.0/forgejo"], + "matchUpdateTypes": ["minor", "patch", "digest"], + "enabled": false + }, { "description": "Require approval for stable branches (must be last rule to override all others)", "matchBaseBranches": ["/^v\\d+\\.\\d+\\/forgejo$/"], diff --git a/.forgejo/workflows-composite/build-backend/action.yaml b/.forgejo/workflows-composite/build-backend/action.yaml index 68a99ffaf9..8c6fdb024b 100644 --- a/.forgejo/workflows-composite/build-backend/action.yaml +++ b/.forgejo/workflows-composite/build-backend/action.yaml @@ -3,7 +3,7 @@ runs: steps: - run: | su forgejo -c 'make deps-backend' - - uses: https://data.forgejo.org/actions/cache@v4 + - uses: https://data.forgejo.org/actions/cache@v5 id: cache-backend with: path: ${{github.workspace}}/gitea diff --git a/.forgejo/workflows-composite/setup-cache-go/action.yaml b/.forgejo/workflows-composite/setup-cache-go/action.yaml index c44eaf24bd..392723567d 100644 --- a/.forgejo/workflows-composite/setup-cache-go/action.yaml +++ b/.forgejo/workflows-composite/setup-cache-go/action.yaml @@ -50,7 +50,7 @@ runs: - name: "Restore Go dependencies from cache or mark for later caching" id: cache-deps - uses: https://data.forgejo.org/actions/cache@v4 + uses: https://data.forgejo.org/actions/cache@v5 with: key: setup-cache-go-deps-${{ runner.os }}-${{ inputs.username }}-${{ steps.go-version.outputs.go_version }}-${{ hashFiles('go.sum', 'go.mod', 'Makefile') }} restore-keys: | diff --git a/.forgejo/workflows/backport.yml b/.forgejo/workflows/backport.yml index 56956c4641..4854eb225e 100644 --- a/.forgejo/workflows/backport.yml +++ b/.forgejo/workflows/backport.yml @@ -47,7 +47,7 @@ jobs: cat <<'EOF' ${{ toJSON(github) }} EOF - - uses: https://data.forgejo.org/actions/git-backporting@v4.8.7 + - uses: https://data.forgejo.org/actions/git-backporting@v4.9.1 with: target-branch-pattern: "^backport/(?(v.*))$" strategy: ort diff --git a/.forgejo/workflows/build-release-integration.yml b/.forgejo/workflows/build-release-integration.yml index d22126bad4..3d024f5f9e 100644 --- a/.forgejo/workflows/build-release-integration.yml +++ b/.forgejo/workflows/build-release-integration.yml @@ -1,6 +1,9 @@ name: Integration tests for the release process enable-email-notifications: true +env: + FORGEJO_VERSION: 11.0.14 # renovate: datasource=docker depName=data.forgejo.org/forgejo/forgejo + on: push: paths: @@ -26,14 +29,14 @@ jobs: if: vars.ROLE == 'forgejo-coding' runs-on: lxc-bookworm steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - id: forgejo - uses: https://data.forgejo.org/actions/setup-forgejo@v3.1.7 + uses: https://data.forgejo.org/actions/setup-forgejo@v3.1.11 with: user: root password: admin1234 - image-version: 1.21 + image-version: ${{ env.FORGEJO_VERSION }} lxc-ip-prefix: 10.0.9 - name: publish the forgejo release diff --git a/.forgejo/workflows/build-release.yml b/.forgejo/workflows/build-release.yml index eb99efbc38..ca259f7e7b 100644 --- a/.forgejo/workflows/build-release.yml +++ b/.forgejo/workflows/build-release.yml @@ -33,7 +33,7 @@ jobs: # root is used for testing, allow it if: vars.ROLE == 'forgejo-integration' || github.repository_owner == 'root' steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 with: fetch-depth: 0 @@ -93,7 +93,7 @@ jobs: - name: cache node_modules id: node - uses: https://data.forgejo.org/actions/cache@v4 + uses: https://data.forgejo.org/actions/cache@v5 with: path: | node_modules @@ -164,7 +164,7 @@ jobs: - name: build container & release if: ${{ secrets.TOKEN != '' }} - uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.5.1 + uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.6.0 with: forgejo: "${{ env.GITHUB_SERVER_URL }}" owner: "${{ env.GITHUB_REPOSITORY_OWNER }}" @@ -183,7 +183,7 @@ jobs: - name: build rootless container if: ${{ secrets.TOKEN != '' }} - uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.5.1 + uses: https://data.forgejo.org/forgejo/forgejo-build-publish/build@v5.6.0 with: forgejo: "${{ env.GITHUB_SERVER_URL }}" owner: "${{ env.GITHUB_REPOSITORY_OWNER }}" @@ -227,11 +227,14 @@ jobs: curl -sS -X DELETE $url/api/v1/repos/forgejo-experimental/forgejo/releases/tags/$tag > /dev/null curl -sS -X DELETE $url/api/v1/repos/forgejo-experimental/forgejo/tags/$tag > /dev/null fi - # actions/checkout@v3 sets http.https://codeberg.org/.extraheader with the automatic token. - # Get rid of it so it does not prevent using the token that has write permissions - git config --local --unset http.https://codeberg.org/.extraheader + # actions/checkout@v6 sets http.https://codeberg.org/.extraheader with the automatic token. Get rid of it so + # it does not prevent using the token that has write permissions. As of @v6, it is stored in + # $RUNNER_TEMP/git-credentials-(uuid).config and included in the repo via + # includeif.gitdir:...=$RUNNER_TEMP/git-credentials-(uuid).config. Since we don't need these credentials + # anymore we can just remove the generated config file. + rm -f $RUNNER_TEMP/git-credentials-* if test -f .git/shallow ; then - echo "unexptected .git/shallow file is present" + echo "unexpected .git/shallow file is present" echo "it suggests a checkout --depth X was used which may prevent pushing the commit" echo "it happens when actions/checkout is called without depth: 0" fi diff --git a/.forgejo/workflows/cascade-setup-end-to-end.yml b/.forgejo/workflows/cascade-setup-end-to-end.yml index 747e41075f..36b464d9f3 100644 --- a/.forgejo/workflows/cascade-setup-end-to-end.yml +++ b/.forgejo/workflows/cascade-setup-end-to-end.yml @@ -37,7 +37,7 @@ jobs: container: image: data.forgejo.org/oci/node:24-bookworm steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 with: fetch-depth: '0' show-progress: 'false' diff --git a/.forgejo/workflows/coverage.yml b/.forgejo/workflows/coverage.yml index f0a27889a1..3c2a6c245e 100644 --- a/.forgejo/workflows/coverage.yml +++ b/.forgejo/workflows/coverage.yml @@ -61,7 +61,7 @@ jobs: image: registry.redict.io/redict:7.3.6-scratch options: --tmpfs /data:noatime steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 with: repository: ${{ inputs.repository }} ref: ${{ inputs.ref }} @@ -83,7 +83,7 @@ jobs: TEST_MINIO_ENDPOINT: minio:9000 TEST_LDAP: 1 TEST_REDIS_SERVER: cacher:6379 - - uses: https://code.forgejo.org/forgejo/upload-artifact@v4 + - uses: https://data.forgejo.org/forgejo/upload-artifact@v5 with: name: coverage path: ${{ forge.workspace }}/coverage/merged diff --git a/.forgejo/workflows/publish-release.yml b/.forgejo/workflows/publish-release.yml index 208c16d002..4695076581 100644 --- a/.forgejo/workflows/publish-release.yml +++ b/.forgejo/workflows/publish-release.yml @@ -41,10 +41,10 @@ jobs: runs-on: lxc-bookworm if: vars.DOER != '' && vars.FORGEJO != '' && vars.TO_OWNER != '' && vars.FROM_OWNER != '' && secrets.TOKEN != '' steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - name: copy & sign - uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.5.1 + uses: https://data.forgejo.org/forgejo/forgejo-build-publish/publish@v5.6.0 with: from-forgejo: ${{ vars.FORGEJO }} to-forgejo: ${{ vars.FORGEJO }} diff --git a/.forgejo/workflows/release-notes-assistant-milestones.yml b/.forgejo/workflows/release-notes-assistant-milestones.yml index 76799b06dc..f075406335 100644 --- a/.forgejo/workflows/release-notes-assistant-milestones.yml +++ b/.forgejo/workflows/release-notes-assistant-milestones.yml @@ -6,7 +6,7 @@ on: env: RNA_WORKDIR: /srv/rna - RNA_VERSION: v1.4.1 # renovate: datasource=gitea-releases depName=forgejo/release-notes-assistant registryUrl=https://code.forgejo.org + RNA_VERSION: v1.7.0 # renovate: datasource=forgejo-releases depName=forgejo/release-notes-assistant registryUrl=https://code.forgejo.org jobs: release-notes: @@ -15,9 +15,9 @@ jobs: container: image: 'data.forgejo.org/oci/ci:1' steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - - uses: https://data.forgejo.org/actions/cache@v4 + - uses: https://data.forgejo.org/actions/cache@v5 with: key: rna-${{ env.RNA_VERSION }} path: ${{ env.RNA_WORKDIR }} diff --git a/.forgejo/workflows/release-notes-assistant.yml b/.forgejo/workflows/release-notes-assistant.yml index 4640eb37df..e85f11b7f8 100644 --- a/.forgejo/workflows/release-notes-assistant.yml +++ b/.forgejo/workflows/release-notes-assistant.yml @@ -8,16 +8,16 @@ on: - labeled env: - RNA_VERSION: v1.4.1 # renovate: datasource=gitea-releases depName=forgejo/release-notes-assistant registryUrl=https://code.forgejo.org + RNA_VERSION: v1.7.0 # renovate: datasource=forgejo-releases depName=forgejo/release-notes-assistant registryUrl=https://code.forgejo.org jobs: release-notes: if: ( vars.ROLE == 'forgejo-coding' ) && contains(github.event.pull_request.labels.*.name, 'worth a release-note') runs-on: docker container: - image: 'data.forgejo.org/oci/node:24-bookworm' + image: 'data.forgejo.org/oci/ci:1' steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - name: event run: | @@ -28,17 +28,12 @@ jobs: ${{ toJSON(github.event) }} EOF - - uses: https://data.forgejo.org/actions/setup-go@v6 - with: - go-version-file: "go.mod" - cache: false - - - name: apt install jq + - name: install release-notes-assistant run: | - export DEBIAN_FRONTEND=noninteractive - apt-get update -qq - apt-get -q install -y -qq jq + set -x + wget -O /usr/local/bin/rna https://code.forgejo.org/forgejo/release-notes-assistant/releases/download/${{ env.RNA_VERSION}}/release-notes-assistant + chmod +x /usr/local/bin/rna - name: release-notes-assistant preview run: | - go run code.forgejo.org/forgejo/release-notes-assistant@$RNA_VERSION --config .release-notes-assistant.yaml --storage pr --storage-location ${{ github.event.pull_request.number }} --forgejo-url $GITHUB_SERVER_URL --repository $GITHUB_REPOSITORY --token ${{ secrets.RELEASE_NOTES_ASSISTANT_TOKEN }} preview ${{ github.event.pull_request.number }} + rna --config .release-notes-assistant.yaml --storage pr --storage-location ${{ github.event.pull_request.number }} --forgejo-url $GITHUB_SERVER_URL --repository $GITHUB_REPOSITORY --token ${{ secrets.RELEASE_NOTES_ASSISTANT_TOKEN }} preview ${{ github.event.pull_request.number }} diff --git a/.forgejo/workflows/renovate.yml b/.forgejo/workflows/renovate.yml deleted file mode 100644 index cc4801d811..0000000000 --- a/.forgejo/workflows/renovate.yml +++ /dev/null @@ -1,79 +0,0 @@ -# -# Runs every 2 hours, but Renovate is limited to create new PR before 4am. -# See renovate.json for more settings. -# Automerge is enabled for Renovate PR's but need to be approved before. -# -name: renovate - -on: - push: - branches: - - renovate/** # self-test updates - paths: - - .forgejo/workflows/renovate.yml - schedule: - - cron: '0 0/2 * * *' - workflow_dispatch: - -env: - RENOVATE_DRY_RUN: ${{ (github.event_name != 'schedule' && github.ref_name != github.event.repository.default_branch) && 'full' || '' }} - RENOVATE_REPOSITORIES: ${{ github.repository }} - # fix because 10.0.0-58-7e1df53+gitea-1.22.0 < 10.0.0 for semver - # and codeberg api returns such versions from `git describe --tags` - # RENOVATE_X_PLATFORM_VERSION: 10.0.0+gitea-1.22.0 currently not needed - -jobs: - renovate: - if: vars.ROLE == 'forgejo-coding' && secrets.RENOVATE_TOKEN != '' - - runs-on: docker - container: - image: data.forgejo.org/renovate/renovate:42.39.2 - - steps: - - name: Load renovate repo cache - uses: https://data.forgejo.org/actions/cache/restore@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 - with: - path: | - .tmp/cache/renovate/repository - .tmp/cache/renovate/renovate-cache-sqlite - .tmp/osv - key: repo-cache-${{ github.run_id }} - restore-keys: | - repo-cache- - - - name: Run renovate - run: renovate - env: - GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }} - LOG_LEVEL: debug - RENOVATE_BASE_DIR: ${{ github.workspace }}/.tmp - RENOVATE_ENDPOINT: ${{ github.server_url }} - RENOVATE_PLATFORM: forgejo - RENOVATE_REPOSITORY_CACHE: 'enabled' - RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} - RENOVATE_GIT_AUTHOR: 'Renovate Bot ' - RENOVATE_CONFIG_FILE_NAMES: '[".forgejo/renovate.json"]' - - RENOVATE_X_SQLITE_PACKAGE_CACHE: true - - GIT_AUTHOR_NAME: 'Renovate Bot' - GIT_AUTHOR_EMAIL: 'forgejo-renovate-action@forgejo.org' - GIT_COMMITTER_NAME: 'Renovate Bot' - GIT_COMMITTER_EMAIL: 'forgejo-renovate-action@forgejo.org' - - OSV_OFFLINE_ROOT_DIR: ${{ github.workspace }}/.tmp/osv - - # use direct connection for these domains for renovate go datasource instead of the go proxy - # allows faster lookups - GONOPROXY: code.forgejo.org - - - name: Save renovate repo cache - if: always() && env.RENOVATE_DRY_RUN != 'full' - uses: https://data.forgejo.org/actions/cache/save@3624ceb22c1c5a301c8db4169662070a689d9ea8 # v4.1.1 - with: - path: | - .tmp/cache/renovate/repository - .tmp/cache/renovate/renovate-cache-sqlite - .tmp/osv - key: repo-cache-${{ github.run_id }} diff --git a/.forgejo/workflows/testing-integration.yml b/.forgejo/workflows/testing-integration.yml index 6c690c484f..11d20e2ab0 100644 --- a/.forgejo/workflows/testing-integration.yml +++ b/.forgejo/workflows/testing-integration.yml @@ -34,7 +34,7 @@ jobs: image: 'data.forgejo.org/oci/node:24-trixie' options: --tmpfs /tmp:exec,noatime steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - name: install git 2.34.1 and git-lfs 3.0.2 uses: ./.forgejo/workflows-composite/install-minimum-git-version @@ -53,7 +53,7 @@ jobs: image: 'data.forgejo.org/oci/node:24-trixie' options: --tmpfs /tmp:exec,noatime steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - name: install git 2.34.1 and git-lfs 3.0.2 uses: ./.forgejo/workflows-composite/install-minimum-git-version @@ -85,7 +85,7 @@ jobs: MARIADB_DATABASE: testgitea options: --tmpfs /var/lib/mysql:noatime steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - name: install dependencies run: apt-get update -qq && apt-get -q install -qq -y git-lfs diff --git a/.forgejo/workflows/testing.yml b/.forgejo/workflows/testing.yml index ac9d9f0801..06ef5eb131 100644 --- a/.forgejo/workflows/testing.yml +++ b/.forgejo/workflows/testing.yml @@ -22,10 +22,12 @@ jobs: cat <<'EOF' ${{ toJSON(github) }} EOF - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - - run: su forgejo -c 'make deps-backend deps-tools' - - run: su forgejo -c 'make --always-make -j$(nproc) lint-backend tidy-check swagger-check lint-swagger fmt-check swagger-validate' # ensure the "go-licenses" make target runs + # DO NOT add checks here, but rather in the makefile + - run: su forgejo -c './tools/cimake.sh pr-go' + # this will re-run the backend target also contained in pr-go, but + # a re-build is insignificant - uses: ./.forgejo/workflows-composite/build-backend frontend-checks: if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing' @@ -34,14 +36,17 @@ jobs: image: 'data.forgejo.org/oci/node:24-trixie' options: --tmpfs /tmp:exec,noatime steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 + - uses: https://data.forgejo.org/actions/setup-node@v6 with: node-version-file: .node-version + - run: make deps-frontend - run: make lint-frontend - run: make checks-frontend - - run: | + - name: make test-frontend-coverage + run: | # Usage of `dayjs` can be impacted by local system timezone and can be sensitive to DST differences; since # frontend tests are very short they're run twice with varying DST rules to reduce regression risk. TZ=Europe/Berlin make test-frontend-coverage @@ -53,8 +58,8 @@ jobs: run: | apt-get update -qq apt-get -q install -qq -y zstd - - name: "Cache frontend build for playwright testing" - uses: https://data.forgejo.org/actions/cache/save@v4 + - name: 'Cache frontend build for playwright testing' + uses: https://data.forgejo.org/actions/cache/save@v5 with: path: ${{github.workspace}}/public/assets key: frontend-build-${{ github.sha }} @@ -71,7 +76,7 @@ jobs: options: --tmpfs /bitnami/elasticsearch/data env: discovery.type: single-node - ES_JAVA_OPTS: "-Xms512m -Xmx512m" + ES_JAVA_OPTS: '-Xms512m -Xmx512m' minio: image: data.forgejo.org/oci/bitnami/minio:2024.8.17 options: >- @@ -81,7 +86,7 @@ jobs: MINIO_ROOT_USER: 123456 MINIO_ROOT_PASSWORD: 12345678 steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - name: test release-notes-assistant.sh run: | @@ -104,17 +109,17 @@ jobs: image: 'data.forgejo.org/oci/playwright:latest' options: --tmpfs /tmp:exec,noatime steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 with: fetch-depth: 20 - uses: ./.forgejo/workflows-composite/setup-env - - name: "Restore frontend build" - uses: https://data.forgejo.org/actions/cache/restore@v4 + - name: 'Restore frontend build' + uses: https://data.forgejo.org/actions/cache/restore@v5 id: cache-frontend with: path: ${{github.workspace}}/public/assets key: frontend-build-${{ github.sha }} - - name: "Build frontend (if not cached)" + - name: 'Build frontend (if not cached)' if: steps.cache-frontend.outputs.cache-hit != 'true' run: | su forgejo -c 'make deps-frontend frontend' @@ -139,7 +144,7 @@ jobs: RUN_ALL: ${{steps.run-all.all}} - name: Upload test artifacts on failure if: failure() - uses: https://data.forgejo.org/forgejo/upload-artifact@v4 + uses: https://data.forgejo.org/forgejo/upload-artifact@v5 with: name: test-artifacts.zip path: tests/e2e/test-artifacts/ @@ -172,9 +177,9 @@ jobs: image: ${{ matrix.cacher.image }} options: ${{ matrix.cacher.options }} env: - ALLOW_EMPTY_PASSWORD: "yes" # redis & valkey will immediately shutdown with no defined password unless overridden + ALLOW_EMPTY_PASSWORD: 'yes' # redis & valkey will immediately shutdown with no defined password unless overridden steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - uses: ./.forgejo/workflows-composite/build-backend - run: | @@ -203,7 +208,7 @@ jobs: MYSQL_EXTRA_FLAGS: --innodb-adaptive-flushing=OFF --innodb-buffer-pool-size=4G --innodb-log-buffer-size=128M --innodb-flush-log-at-trx-commit=0 --innodb-flush-log-at-timeout=30 --innodb-flush-method=nosync --innodb-fsync-threshold=1000000000 --disable-log-bin options: --tmpfs /bitnami/mysql/data:noatime steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - name: install dependencies run: apt-get update -qq && apt-get -q install -qq -y git-lfs @@ -241,7 +246,7 @@ jobs: POSTGRESQL_EXTRA_FLAGS: -c full_page_writes=off options: --tmpfs /bitnami/postgresql steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - name: install dependencies run: apt-get update -qq && apt-get -q install -qq -y git-lfs @@ -263,7 +268,7 @@ jobs: image: 'data.forgejo.org/oci/node:24-trixie' options: --tmpfs /tmp:exec,noatime steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - name: install dependencies run: apt-get update -qq && apt-get -q install -qq -y git-lfs @@ -291,7 +296,20 @@ jobs: image: 'data.forgejo.org/oci/node:24-trixie' options: --tmpfs /tmp:exec,noatime steps: - - uses: https://data.forgejo.org/actions/checkout@v5 + - uses: https://data.forgejo.org/actions/checkout@v6 - uses: ./.forgejo/workflows-composite/setup-env - run: su forgejo -c 'make deps-backend deps-tools' - run: su forgejo -c 'make security-check' + semgrep: + if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing' + name: semgrep/ci + runs-on: docker + container: + image: 'data.forgejo.org/oci/semgrep:latest' + steps: + - run: apk add nodejs # required for actions/checkout + - uses: https://data.forgejo.org/actions/checkout@v6 + - name: self-check semgrep rules + run: semgrep --test .semgrep/tests/ --config .semgrep/config/ + - name: semgrep ci + run: semgrep ci --config .semgrep/config/ --metrics=off diff --git a/.gitignore b/.gitignore index ffc493a51d..c524583ce8 100644 --- a/.gitignore +++ b/.gitignore @@ -107,6 +107,7 @@ cpu.out /.air /.go-licenses /.cur-deadcode-out +/.deadcode.diff # Files and folders that were previously generated /public/assets/img/webpack diff --git a/.golangci.yml b/.golangci.yml index aed39e3c0e..5fa6c13860 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,6 +15,7 @@ linters: - govet - importas - ineffassign + - modernize - nakedret - nolintlint - revive @@ -25,6 +26,7 @@ linters: - unused - usetesting - wastedassign + - nilnil settings: depguard: rules: @@ -44,6 +46,25 @@ linters: desc: use forgejo.org/modules/git instead, see https://codeberg.org/forgejo/forgejo/pulls/4941 - pkg: gopkg.in/yaml.v3 desc: use go.yaml.in/yaml instead, see https://codeberg.org/forgejo/forgejo/pulls/8956 + migration-isolation: + list-mode: lax + files: + - "**/models/forgejo_migrations/**" + deny: + - pkg: "forgejo.org/models" + desc: > + Migrations must not import application models. Application models will be the most recent schema for + Forgejo, while migrations will be operating against the database schema that existed when they were + authored. + - pkg: "forgejo.org/services" + desc: > + Migrations must not import application services. Application services will reference application + models which will use the most recent schema for Forgejo, while migrations will be operating against the + database schema that existed when they were authored. + allow: + - "forgejo.org/models/db" + - "forgejo.org/models/gitea_migrations/base" + - "forgejo.org/models/gitea_migrations/test" gocritic: disabled-checks: - ifElseChain @@ -113,6 +134,8 @@ linters: disable: - error-is-as - go-require + nilnil: + only-two: false exclusions: generated: lax presets: @@ -163,6 +186,215 @@ linters: - linters: - staticcheck text: "(ST1005|ST1003|QF1001):" + + # TODO: eventually remove this section entirely + - path: cmd/admin_auth_ldap_test.go + linters: + - nilnil + - path: cmd/admin_auth_oauth_test.go + linters: + - nilnil + - path: cmd/admin_auth_pam_test.go + linters: + - nilnil + - path: cmd/cmd.go + linters: + - nilnil + - path: cmd/forgejo/actions.go + linters: + - nilnil + - path: models/actions/run.go + linters: + - nilnil + - path: models/actions/task.go + linters: + - nilnil + - path: models/activities/action_list.go + linters: + - nilnil + - path: models/asymkey/gpg_key_object_verification.go + linters: + - nilnil + - path: models/auth/oauth2.go + linters: + - nilnil + - path: models/db/collation.go + linters: + - nilnil + - path: models/dbfs/dbfile.go + linters: + - nilnil + - path: models/forgejo_migrations_legacy/v32.go + linters: + - nilnil + - path: models/forgejo_migrations_legacy/v32_test.go + linters: + - nilnil + - path: models/db/context.go + linters: + - nilnil + - path: models/git/branch_list.go + linters: + - nilnil + - path: models/git/lfs_lock.go + linters: + - nilnil + - path: models/git/protected_branch.go + linters: + - nilnil + - path: models/git/protected_tag.go + linters: + - nilnil + - path: models/issues/issue.go + linters: + - nilnil + - path: models/issues/issue_xref.go + linters: + - nilnil + - path: models/issues/review.go + linters: + - nilnil + - path: models/organization/org_user.go + linters: + - nilnil + - path: models/quota/rule.go + linters: + - nilnil + - path: models/repo/archiver.go + linters: + - nilnil + - path: models/repo/fork.go + linters: + - nilnil + - path: models/repo/topic.go + linters: + - nilnil + - path: models/user/email_address.go + linters: + - nilnil + - path: models/user/list.go + linters: + - nilnil + - path: models/user/user.go + linters: + - nilnil + - path: models/repo/repo.go + linters: + - nilnil + - path: modules/git/commit.go + linters: + - nilnil + - path: modules/git/foreachref/parser.go + linters: + - nilnil + - path: modules/git/last_commit_cache.go + linters: + - nilnil + - path: modules/git/log_name_status.go + linters: + - nilnil + - path: modules/graceful/net_unix.go + linters: + - nilnil + - path: modules/indexer/internal/bleve/util.go + linters: + - nilnil + - path: modules/indexer/issues/util.go + linters: + - nilnil + - path: modules/optional/serialization.go + linters: + - nilnil + - path: modules/setting/storage.go + linters: + - nilnil + - path: routers/api/packages/chef/auth.go + linters: + - nilnil + - path: routers/api/packages/container/auth.go + linters: + - nilnil + - path: routers/api/packages/nuget/auth.go + linters: + - nilnil + - path: routers/api/packages/swift/swift.go + linters: + - nilnil + - path: routers/web/auth/oauth.go + linters: + - nilnil + - path: routers/web/repo/compare.go + linters: + - nilnil + - path: routers/web/repo/release.go + linters: + - nilnil + - path: routers/web/repo/setting/runners.go + linters: + - nilnil + - path: routers/web/repo/setting/secrets.go + linters: + - nilnil + - path: routers/web/repo/setting/variables.go + linters: + - nilnil + - path: services/actions/context.go + linters: + - nilnil + - path: services/actions/task.go + linters: + - nilnil + - path: services/actions/trust.go + linters: + - nilnil + - path: services/contexttest/context_tests.go + linters: + - nilnil + - path: services/gitdiff/csv.go + linters: + - nilnil + - path: services/issue/assignee.go + linters: + - nilnil + - path: routers/api/packages/conan/auth.go + linters: + - nilnil + - path: services/issue/commit.go + linters: + - nilnil + - path: services/issue/issue.go + linters: + - nilnil + - path: services/migrations/onedev.go + linters: + - nilnil + - path: services/packages/cargo/index.go + linters: + - nilnil + - path: services/pull/check.go + linters: + - nilnil + - path: services/pull/comment.go + linters: + - nilnil + - path: services/pull/merge.go + linters: + - nilnil + - path: services/pull/review.go + linters: + - nilnil + - path: services/remote/promote.go + linters: + - nilnil + - path: services/repository/archiver/archiver.go + linters: + - nilnil + - path: services/repository/generate_repo_commit.go + linters: + - nilnil + - path: services/repository/repository.go + linters: + - nilnil paths: - node_modules - public diff --git a/.mockery.yml b/.mockery.yml new file mode 100644 index 0000000000..2e9427cd77 --- /dev/null +++ b/.mockery.yml @@ -0,0 +1,17 @@ +formatter: gofmt +template: testify +packages: + forgejo.org/modules/nosql: + config: + filename: mocks.go # make mocks public so that external packages can use + forgejo.org/services/auth/method: + forgejo.org/services/authz: + config: + filename: authorization_reducer_mock.go # make mocks public so that external packages can use + code.forgejo.org/go-chi/cache: + interfaces: + Cache: + config: + pkgname: cache + dir: modules/cache + filename: mocks.go # make mocks public, not `_test.go`, so that external packages can mock caching diff --git a/.node-version b/.node-version index 1e4f3920b5..eefb690f4a 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -24.12.0 \ No newline at end of file +24.15.0 \ No newline at end of file diff --git a/.release-notes-assistant.yaml b/.release-notes-assistant.yaml index f8264c0897..f942778fe7 100644 --- a/.release-notes-assistant.yaml +++ b/.release-notes-assistant.yaml @@ -7,7 +7,8 @@ branch-from-version: 'v%[1]d.%[2]d/forgejo' tag-from-version: 'v%[1]d.%[2]d.%[3]d' supported-release-count: 3 branch-known: - - 'v7.0/forgejo' +# replace with v15 when v11 becomes EOL + - 'v11.0/forgejo' cleanup-line: 'sed -Ee "s/^(feat|fix):\s*//g" -e "s/^\[WIP\] //" -e "s/^WIP: //" -e "s;\[(UI|BUG|FEAT|v.*?/forgejo)\]\s*;;g"' render-header: | diff --git a/.semgrep/config/auth.yaml b/.semgrep/config/auth.yaml new file mode 100644 index 0000000000..09dacf28c1 --- /dev/null +++ b/.semgrep/config/auth.yaml @@ -0,0 +1,111 @@ +rules: + - id: forgejo-api-use-resource-SearchRepoOptions + patterns: + - pattern: | + repo_model.SearchRepoOptions{...} + - pattern-not: | + repo_model.SearchRepoOptions{ + ..., + AuthorizationReducer: ctx.Reducer, + ... + } + languages: + - go + message: > + SearchRepoOptions does not take into account fine-grained access token limitations. Include the + AuthorizationReducer field. + severity: ERROR + paths: + include: + - "/routers/api/**/*.go" + + - id: forgejo-api-use-resource-SearchRepoOptions + patterns: + - pattern: | + organization.SearchTeamRepoOptions{...} + - pattern-not: | + organization.SearchTeamRepoOptions{ + ..., + AuthorizationReducer: ctx.Reducer, + ... + } + languages: + - go + message: > + SearchTeamRepoOptions does not take into account fine-grained access token limitations. Include the + AuthorizationReducer field. + severity: ERROR + paths: + include: + - "/routers/api/**/*.go" + + - id: forgejo-api-use-resource-GetUserRepoPermission + patterns: + - pattern: | + $X.GetUserRepoPermission($CTX, $REPO, $DOER) + - metavariable-type: + metavariable: $CTX + types: + - "*context.APIContext" + languages: + - go + message: > + GetUserRepoPermission does not take into account fine-grained access token limitations. Use + GetUserRepoPermissionWithReducer. + fix: | + $X.GetUserRepoPermissionWithReducer($CTX, $REPO, $DOER, $CTX.Reducer) + severity: ERROR + + - id: forgejo-api-suspicious-GetUserRepoPermission + patterns: + - pattern: $X.GetUserRepoPermission($CTX, $REPO, $DOER) + - pattern-not: # don't match if identical to forgejo-api-use-resource-GetUserRepoPermission + patterns: + - pattern: | + $X.GetUserRepoPermission($CTX, $REPO, $DOER) + - metavariable-type: + metavariable: $CTX + types: + - "*context.APIContext" + languages: + - go + message: > + API code is accessing GetUserRepoPermission which does not take into account fine-grained access token + limitations. Should this use GetUserRepoPermissionWithReducer? + severity: ERROR + paths: + include: + - "/routers/api/**/*.go" + + - id: forgejo-api-direct-IsAdmin-check + patterns: + - pattern: | + ctx.Doer.IsAdmin + languages: + - go + message: | + ctx.Doer.IsAdmin does not take into account limited API access tokens. Use ctx.IsUserSiteAdmin() instead. + fix: | + ctx.IsUserSiteAdmin() + severity: ERROR + paths: + include: + - "/routers/api/**/*.go" + + - id: forgejo-api-direct-repo-Admin-check + patterns: + - pattern: | + ctx.Repo.IsAdmin() + - pattern: | + ctx.Repo.IsOwner() + languages: + - go + message: | + ctx.Repo.IsAdmin/IsOwner() does not take into account limited API access tokens. Use ctx.IsUserRepoAdmin() instead. + fix: | + ctx.IsUserRepoAdmin() + severity: ERROR + paths: + include: + - "/routers/api/**/*.go" + diff --git a/.semgrep/config/go.yaml b/.semgrep/config/go.yaml new file mode 100644 index 0000000000..f73e5e43d7 --- /dev/null +++ b/.semgrep/config/go.yaml @@ -0,0 +1,18 @@ +rules: + - id: forgejo-switch-empty-case + pattern-either: + - pattern: |- + switch $_ { + case $_: + } + - patterns: + - pattern: |- + switch { + case $_: + } + languages: + - go + severity: ERROR + message: > + switch has a case block with no content. This is treated as "break" by Go, but developers may confuse it for + "fallthrough". To fix this error, disambiguate by using "break" or "fallthrough". diff --git a/.semgrep/config/logic.yaml b/.semgrep/config/logic.yaml new file mode 100644 index 0000000000..85c43f5531 --- /dev/null +++ b/.semgrep/config/logic.yaml @@ -0,0 +1,11 @@ +rules: + - id: forgejo-logic-suspicious-OwnerID-check + pattern: |- + $X.OwnerID > 0 + languages: + - go + severity: ERROR + message: > + Many resources like comments or runners cannot only be owned by regular users, which have positive IDs, but also + by predefined system users like Ghost or Forgejo Actions that have negative IDs. In those cases, ownership checks + should only exclude 0: `OwnerID != 0`. diff --git a/.semgrep/config/xorm.yaml b/.semgrep/config/xorm.yaml new file mode 100644 index 0000000000..057e1d3aef --- /dev/null +++ b/.semgrep/config/xorm.yaml @@ -0,0 +1,24 @@ +rules: + - id: xorm-sync-missing-ignore-drop-indices + patterns: + - pattern-either: + - pattern: | + $X.Sync(...) + - pattern: | + $X.SyncWithOptions($OPTS, ...) + - pattern-not: | + $X.SyncWithOptions(xorm.SyncOptions{..., IgnoreDropIndices: true, ...}, ...) + - metavariable-type: + metavariable: $X + types: + - "*xorm.Engine" + - "*xorm.Session" + paths: + exclude: + - /models/gitea_migrations/**/*.go + - /models/forgejo_migrations_legacy/**/*.go + languages: + - go + message: | + xorm Sync operation may drop indices if used on an incomplete bean definition for an existing table. Use SyncWithOptions with IgnoreDropIndices: true instead. + severity: ERROR diff --git a/.semgrep/tests/auth.fixed.go b/.semgrep/tests/auth.fixed.go new file mode 100644 index 0000000000..99703b2005 --- /dev/null +++ b/.semgrep/tests/auth.fixed.go @@ -0,0 +1,58 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "forgejo.org/models/db" + access_model "forgejo.org/models/perm/access" + repo_model "forgejo.org/models/repo" + api "forgejo.org/modules/structs" + "forgejo.org/routers/api/v1/utils" + "forgejo.org/services/context" + "forgejo.org/services/convert" +) + +// ListForks list a repository's forks +func ListForks(ctx *context.APIContext) { + forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetForks", err) + return + } + apiForks := make([]*api.Repository, len(forks)) + for i, fork := range forks { + // ruleid:forgejo-api-use-resource-GetUserRepoPermission + permission, err := access_model.GetUserRepoPermissionWithReducer(ctx, fork, ctx.Doer, ctx.Reducer) + // ok:forgejo-api-use-resource-GetUserRepoPermission + permission, err := access_model.GetUserRepoPermissionWithReducer(ctx, fork, ctx.Doer, ctx.Reducer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + return + } + apiForks[i] = convert.ToRepo(ctx, fork, permission) + } +} + +// getStarredRepos returns the repos that the user with the specified userID has +// starred +func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) { + starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions) + if err != nil { + return nil, err + } + + repos := make([]*api.Repository, len(starredRepos)) + for i, starred := range starredRepos { + // ruleid:forgejo-api-suspicious-GetUserRepoPermission + permission, err := access_model.GetUserRepoPermission(ctx, starred, user) + if err != nil { + return nil, err + } + repos[i] = convert.ToRepo(ctx, starred, permission) + } + return repos, nil +} diff --git a/.semgrep/tests/auth.go b/.semgrep/tests/auth.go new file mode 100644 index 0000000000..686dc5f9f7 --- /dev/null +++ b/.semgrep/tests/auth.go @@ -0,0 +1,58 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "net/http" + + "forgejo.org/models/db" + access_model "forgejo.org/models/perm/access" + repo_model "forgejo.org/models/repo" + api "forgejo.org/modules/structs" + "forgejo.org/routers/api/v1/utils" + "forgejo.org/services/context" + "forgejo.org/services/convert" +) + +// ListForks list a repository's forks +func ListForks(ctx *context.APIContext) { + forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetForks", err) + return + } + apiForks := make([]*api.Repository, len(forks)) + for i, fork := range forks { + // ruleid:forgejo-api-use-resource-GetUserRepoPermission + permission, err := access_model.GetUserRepoPermission(ctx, fork, ctx.Doer) + // ok:forgejo-api-use-resource-GetUserRepoPermission + permission, err := access_model.GetUserRepoPermissionWithReducer(ctx, fork, ctx.Doer, ctx.Reducer) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err) + return + } + apiForks[i] = convert.ToRepo(ctx, fork, permission) + } +} + +// getStarredRepos returns the repos that the user with the specified userID has +// starred +func getStarredRepos(ctx std_context.Context, user *user_model.User, private bool, listOptions db.ListOptions) ([]*api.Repository, error) { + starredRepos, err := repo_model.GetStarredRepos(ctx, user.ID, private, listOptions) + if err != nil { + return nil, err + } + + repos := make([]*api.Repository, len(starredRepos)) + for i, starred := range starredRepos { + // ruleid:forgejo-api-suspicious-GetUserRepoPermission + permission, err := access_model.GetUserRepoPermission(ctx, starred, user) + if err != nil { + return nil, err + } + repos[i] = convert.ToRepo(ctx, starred, permission) + } + return repos, nil +} diff --git a/.semgrep/tests/go.go b/.semgrep/tests/go.go new file mode 100644 index 0000000000..14c9a31912 --- /dev/null +++ b/.semgrep/tests/go.go @@ -0,0 +1,44 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "os" + "os/signal" + "strings" + "syscall" + + _ "net/http/pprof" // Used for debugging if enabled and a web server is running + + "forgejo.org/modules/setting" +) + +func setPortEmptyCaseBad(port string) error { + setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, port, 1) + setting.HTTPPort = port + + // ruleid:forgejo-switch-empty-case + switch setting.Protocol { + case setting.HTTPUnix: + case setting.FCGI: + case setting.FCGIUnix: + default: + defaultLocalURL := string(setting.Protocol) + "://" + } + + // ok:forgejo-switch-empty-case + switch setting.Protocol { + case setting.HTTPUnix: + break + case setting.FCGI: + break + case setting.FCGIUnix: + break + default: + defaultLocalURL := string(setting.Protocol) + "://" + } + + return nil +} diff --git a/.semgrep/tests/logic.go b/.semgrep/tests/logic.go new file mode 100644 index 0000000000..32a2777ff6 --- /dev/null +++ b/.semgrep/tests/logic.go @@ -0,0 +1,35 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import "xorm.io/builder" + +type FindRunJobOptions struct { + RepoID int64 + OwnerID int64 +} + +func (opts FindRunJobOptions) Bad() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + // ruleid:forgejo-logic-suspicious-OwnerID-check + if opts.OwnerID > 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + return cond +} + +func (opts FindRunJobOptions) Good() builder.Cond { + cond := builder.NewCond() + if opts.RepoID > 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + // ok:forgejo-logic-suspicious-OwnerID-check + if opts.OwnerID != 0 { + cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) + } + return cond +} diff --git a/.semgrep/tests/xorm.go b/.semgrep/tests/xorm.go new file mode 100644 index 0000000000..00b86acee1 --- /dev/null +++ b/.semgrep/tests/xorm.go @@ -0,0 +1,45 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "io/fs" + + "forgejo.org/modules/timeutil" + + "xorm.io/xorm" +) + +type ActionUser struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(user, id)"` + RepoID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(repository, id)"` + + TrustedWithPullRequests bool + + LastAccess timeutil.TimeStamp `xorm:"INDEX"` +} + +func testSyncBad1(x *xorm.Engine) error { + // ruleid:xorm-sync-missing-ignore-drop-indices + return x.Sync(new(ActionUser)) +} + +func testSyncBad2(x *xorm.Engine) error { + // ruleid:xorm-sync-missing-ignore-drop-indices + _, err = x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: false}, bean) + return err +} + +func testSyncGood1(x *xorm.Engine) error { + // ok:xorm-sync-missing-ignore-drop-indices + _, err = x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, bean) + return err +} + +func testSyncGood2(x *fs.File) error { + // ok:xorm-sync-missing-ignore-drop-indices + _, err = x.Sync() + return err +} diff --git a/CODEOWNERS b/CODEOWNERS index 4934331197..c2459d4ab9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -51,3 +51,12 @@ modules/structs/.* @Cyborus routers/api/v1/.* @Cyborus routers/api/forgejo/.* @Cyborus tests/integration/api_.* @Cyborus + +# Federation code, requires care to be taken with regards to interoperability +# and backwards compatibility due to the way signatures work. +services/federation/.* @famfo @0xllx0 +modules/forgefed/.* @famfo @0xllx0 +models/forgefed/.* @famfo @0xllx0 +routers/api/v1/activitypub/.* @famfo @0xllx0 +tests/integration/api_activitypub_.* @famfo @0xllx0 +tests/integration/activitypub_.* @famfo @0xllx0 diff --git a/Dockerfile b/Dockerfile index 3deba255be..2639d91e44 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx -FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.25-alpine3.23 AS build-env +FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.26-alpine3.23 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct} diff --git a/Dockerfile.rootless b/Dockerfile.rootless index c4079fd918..d6e037c522 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -1,6 +1,6 @@ FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/xx AS xx -FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.25-alpine3.23 AS build-env +FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.26-alpine3.23 AS build-env ARG GOPROXY ENV GOPROXY=${GOPROXY:-https://proxy.golang.org,direct} @@ -86,8 +86,8 @@ RUN addgroup \ -G git \ git -RUN mkdir -p /var/lib/gitea /etc/gitea -RUN chown git:git /var/lib/gitea /etc/gitea +RUN mkdir -p /var/lib/gitea +RUN chown git:git /var/lib/gitea COPY --from=build-env /tmp/local / RUN cd /usr/local/bin ; ln -s gitea forgejo @@ -103,13 +103,9 @@ ENV GITEA_CUSTOM=/var/lib/gitea/custom ENV GITEA_TEMP=/tmp/gitea ENV TMPDIR=/tmp/gitea -# Legacy config file for backwards compatibility -# TODO: remove on next major version release -ENV GITEA_APP_INI_LEGACY=/etc/gitea/app.ini - ENV GITEA_APP_INI=${GITEA_CUSTOM}/conf/app.ini ENV HOME="/var/lib/gitea/git" -VOLUME ["/var/lib/gitea", "/etc/gitea"] +VOLUME ["/var/lib/gitea"] WORKDIR /var/lib/gitea ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile index db363d216b..80779cfecc 100644 --- a/Makefile +++ b/Makefile @@ -37,17 +37,18 @@ endif XGO_VERSION := go-1.21.x AIR_PACKAGE ?= github.com/air-verse/air@v1 # renovate: datasource=go -EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.6.0 # renovate: datasource=go +EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3.6.1 # renovate: datasource=go GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 # renovate: datasource=go -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2 # renovate: datasource=go +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 # renovate: datasource=go GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 # renovate: datasource=go -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 # renovate: datasource=go +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.2 # renovate: datasource=go XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest -GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1.6.0 # renovate: datasource=go +GO_LICENSES_PACKAGE ?= github.com/google/go-licenses/v2@v2.0.1 # renovate: datasource=go GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasource=go -DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.39.0 # renovate: datasource=go -GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.6.0 # renovate: datasource=go -RENOVATE_NPM_PACKAGE ?= renovate@42.39.2 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate +DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.45.0 # renovate: datasource=go +ERRORTYPE_PACKAGE ?= fillmore-labs.com/errortype@v0.0.11 # renovate: datasource=go +RENOVATE_NPM_PACKAGE ?= renovate@43.170.20 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate +MOCKERY_PACKAGE ?= github.com/vektra/mockery/v3@v3.7.0 # renovate: datasource=go # https://github.com/disposable-email-domains/disposable-email-domains/commits/main/ DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ... @@ -244,7 +245,7 @@ help: @echo " - generate-license update license files" @echo " - generate-gitignore update gitignore files" @echo " - generate-manpage generate manpage" - @echo " - generate-gomock generate gomock files" + @echo " - generate-mockery generate mockery files" @echo " - generate-forgejo-api generate the forgejo API from spec" @echo " - forgejo-api-validate check if the forgejo API matches the specs" @echo " - generate-swagger generate the swagger spec from code comments" @@ -322,7 +323,7 @@ git-check: node-check: $(eval MIN_NODE_VERSION_STR := $(shell grep -Eo '"node":.*[0-9.]+"' package.json | sed -n 's/.*[^0-9.]\([0-9.]*\)"/\1/p')) $(eval MIN_NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell echo '$(MIN_NODE_VERSION_STR)' | tr '.' ' '))) - $(eval NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v | cut -c2- | tr '.' ' ');)) + $(eval NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v | cut -c2- | sed 's:-.*::' | tr '.' ' ');)) $(eval NPM_MISSING := $(shell hash npm > /dev/null 2>&1 || echo 1)) @if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" -o "$(NPM_MISSING)" = "1" ]; then \ echo "Forgejo requires Node.js $(MIN_NODE_VERSION_STR) or greater and npm to build. You can get it at https://nodejs.org/en/download/"; \ @@ -465,7 +466,7 @@ lint-swagger: node_modules .PHONY: lint-renovate lint-renovate: node_modules - npx --yes --package $(RENOVATE_NPM_PACKAGE) -- renovate-config-validator > .lint-renovate 2>&1 || true + npx --yes --package $(RENOVATE_NPM_PACKAGE) -- renovate-config-validator --no-global .forgejo/renovate.json > .lint-renovate 2>&1 || true @if grep --quiet --extended-regexp -e '^( ERROR:)' .lint-renovate ; then cat .lint-renovate ; rm .lint-renovate ; exit 1 ; fi @rm .lint-renovate @@ -485,10 +486,23 @@ RUN_DEADCODE = $(GO) run $(DEADCODE_PACKAGE) -generated=false -f='{{println .Pat .PHONY: lint-go lint-go: - $(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS) - $(RUN_DEADCODE) > .cur-deadcode-out - @$(DIFF) .deadcode-out .cur-deadcode-out \ + $(GO) run $(GOLANGCI_LINT_PACKAGE) run $(GOLANGCI_LINT_ARGS) \ || (code=$$?; echo "Please run 'make lint-go-fix' and commit the result"; exit $${code}) + $(RUN_DEADCODE) > .cur-deadcode-out + @$(DIFF) .deadcode-out .cur-deadcode-out >.deadcode.diff || true + @if grep -qE '^[+][^+]' .deadcode.diff ; then \ + cat .deadcode.diff ; \ + echo "Looks like you added dead code, please evaluate and remove or use it."; \ + echo "If you are sure the dead code should stay around, please run 'make lint-go-fix',"; \ + echo "commit the result and explain the reason in the commit message / PR description."; \ + exit 1; \ + fi + @if grep -qE '^[-][^-]' .deadcode.diff ; then \ + cat .deadcode.diff ; \ + echo "Looks like you removed dead code. Thank you!"; \ + echo "Run 'make lint-go-fix' and commit the result to accept."; \ + fi + $(GO) run $(ERRORTYPE_PACKAGE) ./... .PHONY: lint-go-fix lint-go-fix: @@ -520,6 +534,11 @@ security-check: tsc: node_modules npx tsc --noEmit +# target for PRs to be pushed. Mandatory to succeed in CI +.PHONY: pr-go +pr-go: deps-backend deps-tools lint-backend tidy-check swagger-check lint-swagger fmt-check swagger-validate + TAGS=bindata $(MAKE) backend + ### # Development and testing targets ### @@ -543,12 +562,12 @@ test: test-frontend test-backend .PHONY: test-backend test-backend: | compute-go-test-packages @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." - @TZ=UTC $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES) + @TZ=UTC GITEA_ROOT="$(CURDIR)" $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_PACKAGES) .PHONY: test-remote-cacher test-remote-cacher: @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." - @$(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_REMOTE_CACHER_PACKAGES) + GITEA_ROOT="$(CURDIR)" $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' $(GO_TEST_REMOTE_CACHER_PACKAGES) .PHONY: test-frontend test-frontend: node_modules @@ -573,7 +592,7 @@ test-check: .PHONY: test\#% test\#%: | compute-go-test-packages @echo "Running go test with $(GOTESTFLAGS) -tags '$(TEST_TAGS)'..." - @TZ=UTC $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES) + @TZ=UTC GITEA_ROOT="$(CURDIR)" $(GOTEST) $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -run $(subst .,/,$*) $(GO_TEST_PACKAGES) coverage-merge: rm -fr coverage/merged ; mkdir -p coverage/merged @@ -949,16 +968,13 @@ deps-tools: $(GO) install $(XGO_PACKAGE) $(GO) install $(GO_LICENSES_PACKAGE) $(GO) install $(GOVULNCHECK_PACKAGE) - $(GO) install $(GOMOCK_PACKAGE) + $(GO) install $(ERRORTYPE_PACKAGE) + $(GO) install $(MOCKERY_PACKAGE) node_modules: package-lock.json npm install --no-save @touch node_modules -.venv: poetry.lock - poetry install - @touch .venv - .PHONY: fomantic fomantic: rm -rf $(FOMANTIC_WORK_DIR)/build @@ -1008,9 +1024,9 @@ generate-license: generate-gitignore: $(GO) run build/generate-gitignores.go -.PHONY: generate-gomock -generate-gomock: - $(GO) run $(GOMOCK_PACKAGE) -package mock -destination ./modules/queue/mock/redisuniversalclient.go forgejo.org/modules/nosql RedisClient +.PHONY: generate-mockery +generate-mockery: + $(GO) run $(MOCKERY_PACKAGE) .PHONY: generate-images generate-images: | node_modules diff --git a/assets/go-licenses.json b/assets/go-licenses.json index 5843e169e4..7f1498042c 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -389,6 +389,11 @@ "path": "github.com/blevesearch/zapx/v16/LICENSE", "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." }, + { + "name": "github.com/blevesearch/zapx/v17", + "path": "github.com/blevesearch/zapx/v17/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." + }, { "name": "github.com/bmatcuk/doublestar/v4", "path": "github.com/bmatcuk/doublestar/v4/LICENSE", @@ -459,11 +464,6 @@ "path": "github.com/davecgh/go-spew/spew/LICENSE", "licenseText": "ISC License\n\nCopyright (c) 2012-2016 Dave Collins \u003cdave@davec.name\u003e\n\nPermission to use, copy, modify, and/or distribute this software for any\npurpose with or without fee is hereby granted, provided that the above\ncopyright notice and this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\nWITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\nANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\nWHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\nACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\nOR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n" }, - { - "name": "github.com/dgryski/go-rendezvous", - "path": "github.com/dgryski/go-rendezvous/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2017-2020 Damian Gryski \u003cdamian@gryski.com\u003e\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" - }, { "name": "github.com/djherbis/buffer", "path": "github.com/djherbis/buffer/LICENSE.txt", @@ -549,6 +549,11 @@ "path": "github.com/fxamacker/cbor/v2/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2019-present Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE." }, + { + "name": "github.com/gdgvda/cron", + "path": "github.com/gdgvda/cron/LICENSE", + "licenseText": "Copyright (C) 2012 Rob Figueiredo\nAll Rights Reserved.\n\nMIT LICENSE\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" + }, { "name": "github.com/gliderlabs/ssh", "path": "github.com/gliderlabs/ssh/LICENSE", @@ -604,21 +609,6 @@ "path": "github.com/go-fed/httpsig/LICENSE", "licenseText": "BSD 3-Clause License\n\nCopyright (c) 2018, go-fed\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, - { - "name": "github.com/go-git/gcfg", - "path": "github.com/go-git/gcfg/LICENSE", - "licenseText": "Copyright (c) 2012 Péter Surányi. Portions Copyright (c) 2009 The Go\nAuthors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" - }, - { - "name": "github.com/go-git/go-billy/v5", - "path": "github.com/go-git/go-billy/v5/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2017 Sourced Technologies S.L.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, - { - "name": "github.com/go-git/go-git/v5", - "path": "github.com/go-git/go-git/v5/LICENSE", - "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright 2018 Sourced Technologies, S.L.\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" - }, { "name": "github.com/go-ini/ini", "path": "github.com/go-ini/ini/LICENSE", @@ -634,10 +624,20 @@ "path": "github.com/go-sql-driver/mysql/LICENSE", "licenseText": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n means each individual or legal entity that creates, contributes to\n the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n means the combination of the Contributions of others (if any) used\n by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n means Source Code Form to which the initial Contributor has attached\n the notice in Exhibit A, the Executable Form of such Source Code\n Form, and Modifications of such Source Code Form, in each case\n including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n (a) that the initial Contributor has attached the notice described\n in Exhibit B to the Covered Software; or\n\n (b) that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the\n terms of a Secondary License.\n\n1.6. \"Executable Form\"\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n means a work that combines Covered Software with other material, in \n a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n means this document.\n\n1.9. \"Licensable\"\n means having the right to grant, to the maximum extent possible,\n whether at the time of the initial grant or subsequently, any and\n all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n means any of the following:\n\n (a) any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered\n Software; or\n\n (b) any new file in Source Code Form that contains any Covered\n Software.\n\n1.11. \"Patent Claims\" of a Contributor\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the\n License, by the making, using, selling, offering for sale, having\n made, import, or transfer of either its Contributions or its\n Contributor Version.\n\n1.12. \"Secondary License\"\n means either the GNU General Public License, Version 2.0, the GNU\n Lesser General Public License, Version 2.1, the GNU Affero General\n Public License, Version 3.0, or any later versions of those\n licenses.\n\n1.13. \"Source Code Form\"\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that\n controls, is controlled by, or is under common control with You. For\n purposes of this definition, \"control\" means (a) the power, direct\n or indirect, to cause the direction or management of such entity,\n whether by contract or otherwise, or (b) ownership of more than\n fifty percent (50%) of the outstanding shares or beneficial\n ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n for sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n or\n\n(b) for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\nThis License does not grant any rights in the trademarks, service marks,\nor logos of any Contributor (except as may be necessary to comply with\nthe notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\nNo Contributor makes additional grants as a result of Your choice to\ndistribute the Covered Software under a subsequent version of this\nLicense (see Section 10.2) or under the terms of a Secondary License (if\npermitted under the terms of Section 3.3).\n\n2.5. Representation\n\nEach Contributor represents that the Contributor believes its\nContributions are its original creation(s) or it has sufficient rights\nto grant the rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\nThis License is not intended to limit any rights You have under\napplicable copyright doctrines of fair use, fair dealing, or other\nequivalents.\n\n2.7. Conditions\n\nSections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted\nin Section 2.1.\n\n3. Responsibilities\n-------------------\n\n3.1. Distribution of Source Form\n\nAll distribution of Covered Software in Source Code Form, including any\nModifications that You create or to which You contribute, must be under\nthe terms of this License. You must inform recipients that the Source\nCode Form of the Covered Software is governed by the terms of this\nLicense, and how they can obtain a copy of this License. You may not\nattempt to alter or restrict the recipients' rights in the Source Code\nForm.\n\n3.2. Distribution of Executable Form\n\nIf You distribute Covered Software in Executable Form then:\n\n(a) such Covered Software must also be made available in Source Code\n Form, as described in Section 3.1, and You must inform recipients of\n the Executable Form how they can obtain a copy of such Source Code\n Form by reasonable means in a timely manner, at a charge no more\n than the cost of distribution to the recipient; and\n\n(b) You may distribute such Executable Form under the terms of this\n License, or sublicense it under different terms, provided that the\n license for the Executable Form does not attempt to limit or alter\n the recipients' rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\nYou may create and distribute a Larger Work under terms of Your choice,\nprovided that You also comply with the requirements of this License for\nthe Covered Software. If the Larger Work is a combination of Covered\nSoftware with a work governed by one or more Secondary Licenses, and the\nCovered Software is not Incompatible With Secondary Licenses, this\nLicense permits You to additionally distribute such Covered Software\nunder the terms of such Secondary License(s), so that the recipient of\nthe Larger Work may, at their option, further distribute the Covered\nSoftware under the terms of either this License or such Secondary\nLicense(s).\n\n3.4. Notices\n\nYou may not remove or alter the substance of any license notices\n(including copyright notices, patent notices, disclaimers of warranty,\nor limitations of liability) contained within the Source Code Form of\nthe Covered Software, except that You may alter any license notices to\nthe extent required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\nYou may choose to offer, and to charge a fee for, warranty, support,\nindemnity or liability obligations to one or more recipients of Covered\nSoftware. However, You may do so only on Your own behalf, and not on\nbehalf of any Contributor. You must make it absolutely clear that any\nsuch warranty, support, indemnity, or liability obligation is offered by\nYou alone, and You hereby agree to indemnify every Contributor for any\nliability incurred by such Contributor as a result of warranty, support,\nindemnity or liability terms You offer. You may include additional\ndisclaimers of warranty and limitations of liability specific to any\njurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n---------------------------------------------------\n\nIf it is impossible for You to comply with any of the terms of this\nLicense with respect to some or all of the Covered Software due to\nstatute, judicial order, or regulation then You must: (a) comply with\nthe terms of this License to the maximum extent possible; and (b)\ndescribe the limitations and the code they affect. Such description must\nbe placed in a text file included with all distributions of the Covered\nSoftware under this License. Except to the extent prohibited by statute\nor regulation, such description must be sufficiently detailed for a\nrecipient of ordinary skill to be able to understand it.\n\n5. Termination\n--------------\n\n5.1. The rights granted under this License will terminate automatically\nif You fail to comply with any of its terms. However, if You become\ncompliant, then the rights granted under this License from a particular\nContributor are reinstated (a) provisionally, unless and until such\nContributor explicitly and finally terminates Your grants, and (b) on an\nongoing basis, if such Contributor fails to notify You of the\nnon-compliance by some reasonable means prior to 60 days after You have\ncome back into compliance. Moreover, Your grants from a particular\nContributor are reinstated on an ongoing basis if such Contributor\nnotifies You of the non-compliance by some reasonable means, this is the\nfirst time You have received notice of non-compliance with this License\nfrom such Contributor, and You become compliant prior to 30 days after\nYour receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\ninfringement claim (excluding declaratory judgment actions,\ncounter-claims, and cross-claims) alleging that a Contributor Version\ndirectly or indirectly infringes any patent, then the rights granted to\nYou by any and all Contributors for the Covered Software under Section\n2.1 of this License shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all\nend user license agreements (excluding distributors and resellers) which\nhave been validly granted by You or Your distributors under this License\nprior to termination shall survive termination.\n\n************************************************************************\n* *\n* 6. Disclaimer of Warranty *\n* ------------------------- *\n* *\n* Covered Software is provided under this License on an \"as is\" *\n* basis, without warranty of any kind, either expressed, implied, or *\n* statutory, including, without limitation, warranties that the *\n* Covered Software is free of defects, merchantable, fit for a *\n* particular purpose or non-infringing. The entire risk as to the *\n* quality and performance of the Covered Software is with You. *\n* Should any Covered Software prove defective in any respect, You *\n* (not any Contributor) assume the cost of any necessary servicing, *\n* repair, or correction. This disclaimer of warranty constitutes an *\n* essential part of this License. No use of any Covered Software is *\n* authorized under this License except under this disclaimer. *\n* *\n************************************************************************\n\n************************************************************************\n* *\n* 7. Limitation of Liability *\n* -------------------------- *\n* *\n* Under no circumstances and under no legal theory, whether tort *\n* (including negligence), contract, or otherwise, shall any *\n* Contributor, or anyone who distributes Covered Software as *\n* permitted above, be liable to You for any direct, indirect, *\n* special, incidental, or consequential damages of any character *\n* including, without limitation, damages for lost profits, loss of *\n* goodwill, work stoppage, computer failure or malfunction, or any *\n* and all other commercial damages or losses, even if such party *\n* shall have been informed of the possibility of such damages. This *\n* limitation of liability shall not apply to liability for death or *\n* personal injury resulting from such party's negligence to the *\n* extent applicable law prohibits such limitation. Some *\n* jurisdictions do not allow the exclusion or limitation of *\n* incidental or consequential damages, so this exclusion and *\n* limitation may not apply to You. *\n* *\n************************************************************************\n\n8. Litigation\n-------------\n\nAny litigation relating to this License may be brought only in the\ncourts of a jurisdiction where the defendant maintains its principal\nplace of business and such litigation shall be governed by laws of that\njurisdiction, without reference to its conflict-of-law provisions.\nNothing in this Section shall prevent a party's ability to bring\ncross-claims or counter-claims.\n\n9. Miscellaneous\n----------------\n\nThis License represents the complete agreement concerning the subject\nmatter hereof. If any provision of this License is held to be\nunenforceable, such provision shall be reformed only to the extent\nnecessary to make it enforceable. Any law or regulation which provides\nthat the language of a contract shall be construed against the drafter\nshall not be used to construe this License against a Contributor.\n\n10. Versions of the License\n---------------------------\n\n10.1. New Versions\n\nMozilla Foundation is the license steward. Except as provided in Section\n10.3, no one other than the license steward has the right to modify or\npublish new versions of this License. Each version will be given a\ndistinguishing version number.\n\n10.2. Effect of New Versions\n\nYou may distribute the Covered Software under the terms of the version\nof the License under which You originally received the Covered Software,\nor under the terms of any subsequent version published by the license\nsteward.\n\n10.3. Modified Versions\n\nIf you create software not governed by this License, and you want to\ncreate a new license for such software, you may create and use a\nmodified version of this License if you rename the license and remove\nany references to the name of the license steward (except to note that\nsuch modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary\nLicenses\n\nIf You choose to distribute Source Code Form that is Incompatible With\nSecondary Licenses under the terms of this version of the License, the\nnotice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n-------------------------------------------\n\n This Source Code Form is subject to the terms of the Mozilla Public\n License, v. 2.0. If a copy of the MPL was not distributed with this\n file, You can obtain one at http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular\nfile, then You may include the notice in a location (such as a LICENSE\nfile in a relevant directory) where a recipient would be likely to look\nfor such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - \"Incompatible With Secondary Licenses\" Notice\n---------------------------------------------------------\n\n This Source Code Form is \"Incompatible With Secondary Licenses\", as\n defined by the Mozilla Public License, v. 2.0.\n" }, + { + "name": "github.com/go-viper/mapstructure/v2", + "path": "github.com/go-viper/mapstructure/v2/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" + }, { "name": "github.com/go-webauthn/webauthn", "path": "github.com/go-webauthn/webauthn/LICENSE", - "licenseText": "Copyright (c) 2017 Duo Security, Inc. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\nCopyright (c) 2021-2022 github.com/go-webauthn/webauthn authors.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the\nfollowing conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following\n disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\nTHIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + "licenseText": "Copyright (c) 2025 github.com/go-webauthn/webauthn authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in the\n documentation and/or other materials provided with the distribution.\n3. Neither the name of the copyright holder nor the names of its\n contributors may be used to endorse or promote products derived from\n this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + }, + { + "name": "github.com/go-webauthn/x/encoding/asn1", + "path": "github.com/go-webauthn/x/encoding/asn1/LICENSE", + "licenseText": "Copyright (c) 2021-2023 github.com/go-webauthn authors.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the\nfollowing conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following\n disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following\n disclaimer in the documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products\n derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,\nWHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\nTHIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." }, { "name": "github.com/go-webauthn/x/revoke", @@ -654,11 +654,6 @@ "path": "github.com/gobwas/glob/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2016 Sergey Kamardin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE." }, - { - "name": "github.com/goccy/go-json", - "path": "github.com/goccy/go-json/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2020 Masaaki Goshima\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" - }, { "name": "github.com/gogs/chardet", "path": "github.com/gogs/chardet/LICENSE", @@ -669,11 +664,6 @@ "path": "github.com/gogs/go-gogs-client/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Go Git Service\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, - { - "name": "github.com/golang-jwt/jwt/v4", - "path": "github.com/golang-jwt/jwt/v4/LICENSE", - "licenseText": "Copyright (c) 2012 Dave Grijalva\nCopyright (c) 2021 golang-jwt maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n" - }, { "name": "github.com/golang-jwt/jwt/v5", "path": "github.com/golang-jwt/jwt/v5/LICENSE", @@ -804,11 +794,6 @@ "path": "github.com/jackc/puddle/v2/LICENSE", "licenseText": "Copyright (c) 2018 Jack Christensen\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, - { - "name": "github.com/jbenet/go-context/io", - "path": "github.com/jbenet/go-context/io/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Juan Batiz-Benet\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" - }, { "name": "github.com/jhillyerd/enmime/v2", "path": "github.com/jhillyerd/enmime/v2/LICENSE", @@ -859,16 +844,16 @@ "path": "github.com/klauspost/cpuid/v2/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Klaus Post\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, + { + "name": "github.com/klauspost/crc32", + "path": "github.com/klauspost/crc32/LICENSE", + "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "github.com/klauspost/pgzip", "path": "github.com/klauspost/pgzip/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Klaus Post\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, - { - "name": "github.com/lib/pq", - "path": "github.com/lib/pq/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2011-2013, 'pq' Contributors. Portions Copyright (c) 2011 Blake Mizerany\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" - }, { "name": "github.com/libdns/libdns", "path": "github.com/libdns/libdns/LICENSE", @@ -959,11 +944,6 @@ "path": "github.com/minio/minlz/LICENSE", "licenseText": "\r\n Apache License\r\n Version 2.0, January 2004\r\n http://www.apache.org/licenses/\r\n\r\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n 1. Definitions.\r\n\r\n \"License\" shall mean the terms and conditions for use, reproduction,\r\n and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n \"Licensor\" shall mean the copyright owner or entity authorized by\r\n the copyright owner that is granting the License.\r\n\r\n \"Legal Entity\" shall mean the union of the acting entity and all\r\n other entities that control, are controlled by, or are under common\r\n control with that entity. For the purposes of this definition,\r\n \"control\" means (i) the power, direct or indirect, to cause the\r\n direction or management of such entity, whether by contract or\r\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n exercising permissions granted by this License.\r\n\r\n \"Source\" form shall mean the preferred form for making modifications,\r\n including but not limited to software source code, documentation\r\n source, and configuration files.\r\n\r\n \"Object\" form shall mean any form resulting from mechanical\r\n transformation or translation of a Source form, including but\r\n not limited to compiled object code, generated documentation,\r\n and conversions to other media types.\r\n\r\n \"Work\" shall mean the work of authorship, whether in Source or\r\n Object form, made available under the License, as indicated by a\r\n copyright notice that is included in or attached to the work\r\n (an example is provided in the Appendix below).\r\n\r\n \"Derivative Works\" shall mean any work, whether in Source or Object\r\n form, that is based on (or derived from) the Work and for which the\r\n editorial revisions, annotations, elaborations, or other modifications\r\n represent, as a whole, an original work of authorship. For the purposes\r\n of this License, Derivative Works shall not include works that remain\r\n separable from, or merely link (or bind by name) to the interfaces of,\r\n the Work and Derivative Works thereof.\r\n\r\n \"Contribution\" shall mean any work of authorship, including\r\n the original version of the Work and any modifications or additions\r\n to that Work or Derivative Works thereof, that is intentionally\r\n submitted to Licensor for inclusion in the Work by the copyright owner\r\n or by an individual or Legal Entity authorized to submit on behalf of\r\n the copyright owner. For the purposes of this definition, \"submitted\"\r\n means any form of electronic, verbal, or written communication sent\r\n to the Licensor or its representatives, including but not limited to\r\n communication on electronic mailing lists, source code control systems,\r\n and issue tracking systems that are managed by, or on behalf of, the\r\n Licensor for the purpose of discussing and improving the Work, but\r\n excluding communication that is conspicuously marked or otherwise\r\n designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n on behalf of whom a Contribution has been received by Licensor and\r\n subsequently incorporated within the Work.\r\n\r\n 2. Grant of Copyright License. Subject to the terms and conditions of\r\n this License, each Contributor hereby grants to You a perpetual,\r\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n copyright license to reproduce, prepare Derivative Works of,\r\n publicly display, publicly perform, sublicense, and distribute the\r\n Work and such Derivative Works in Source or Object form.\r\n\r\n 3. Grant of Patent License. Subject to the terms and conditions of\r\n this License, each Contributor hereby grants to You a perpetual,\r\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n (except as stated in this section) patent license to make, have made,\r\n use, offer to sell, sell, import, and otherwise transfer the Work,\r\n where such license applies only to those patent claims licensable\r\n by such Contributor that are necessarily infringed by their\r\n Contribution(s) alone or by combination of their Contribution(s)\r\n with the Work to which such Contribution(s) was submitted. If You\r\n institute patent litigation against any entity (including a\r\n cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n or a Contribution incorporated within the Work constitutes direct\r\n or contributory patent infringement, then any patent licenses\r\n granted to You under this License for that Work shall terminate\r\n as of the date such litigation is filed.\r\n\r\n 4. Redistribution. You may reproduce and distribute copies of the\r\n Work or Derivative Works thereof in any medium, with or without\r\n modifications, and in Source or Object form, provided that You\r\n meet the following conditions:\r\n\r\n (a) You must give any other recipients of the Work or\r\n Derivative Works a copy of this License; and\r\n\r\n (b) You must cause any modified files to carry prominent notices\r\n stating that You changed the files; and\r\n\r\n (c) You must retain, in the Source form of any Derivative Works\r\n that You distribute, all copyright, patent, trademark, and\r\n attribution notices from the Source form of the Work,\r\n excluding those notices that do not pertain to any part of\r\n the Derivative Works; and\r\n\r\n (d) If the Work includes a \"NOTICE\" text file as part of its\r\n distribution, then any Derivative Works that You distribute must\r\n include a readable copy of the attribution notices contained\r\n within such NOTICE file, excluding those notices that do not\r\n pertain to any part of the Derivative Works, in at least one\r\n of the following places: within a NOTICE text file distributed\r\n as part of the Derivative Works; within the Source form or\r\n documentation, if provided along with the Derivative Works; or,\r\n within a display generated by the Derivative Works, if and\r\n wherever such third-party notices normally appear. The contents\r\n of the NOTICE file are for informational purposes only and\r\n do not modify the License. You may add Your own attribution\r\n notices within Derivative Works that You distribute, alongside\r\n or as an addendum to the NOTICE text from the Work, provided\r\n that such additional attribution notices cannot be construed\r\n as modifying the License.\r\n\r\n You may add Your own copyright statement to Your modifications and\r\n may provide additional or different license terms and conditions\r\n for use, reproduction, or distribution of Your modifications, or\r\n for any such Derivative Works as a whole, provided Your use,\r\n reproduction, and distribution of the Work otherwise complies with\r\n the conditions stated in this License.\r\n\r\n 5. Submission of Contributions. Unless You explicitly state otherwise,\r\n any Contribution intentionally submitted for inclusion in the Work\r\n by You to the Licensor shall be under the terms and conditions of\r\n this License, without any additional terms or conditions.\r\n Notwithstanding the above, nothing herein shall supersede or modify\r\n the terms of any separate license agreement you may have executed\r\n with Licensor regarding such Contributions.\r\n\r\n 6. Trademarks. This License does not grant permission to use the trade\r\n names, trademarks, service marks, or product names of the Licensor,\r\n except as required for reasonable and customary use in describing the\r\n origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n 7. Disclaimer of Warranty. Unless required by applicable law or\r\n agreed to in writing, Licensor provides the Work (and each\r\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n implied, including, without limitation, any warranties or conditions\r\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n PARTICULAR PURPOSE. You are solely responsible for determining the\r\n appropriateness of using or redistributing the Work and assume any\r\n risks associated with Your exercise of permissions under this License.\r\n\r\n 8. Limitation of Liability. In no event and under no legal theory,\r\n whether in tort (including negligence), contract, or otherwise,\r\n unless required by applicable law (such as deliberate and grossly\r\n negligent acts) or agreed to in writing, shall any Contributor be\r\n liable to You for damages, including any direct, indirect, special,\r\n incidental, or consequential damages of any character arising as a\r\n result of this License or out of the use or inability to use the\r\n Work (including but not limited to damages for loss of goodwill,\r\n work stoppage, computer failure or malfunction, or any and all\r\n other commercial damages or losses), even if such Contributor\r\n has been advised of the possibility of such damages.\r\n\r\n 9. Accepting Warranty or Additional Liability. While redistributing\r\n the Work or Derivative Works thereof, You may choose to offer,\r\n and charge a fee for, acceptance of support, warranty, indemnity,\r\n or other liability obligations and/or rights consistent with this\r\n License. However, in accepting such obligations, You may act only\r\n on Your own behalf and on Your sole responsibility, not on behalf\r\n of any other Contributor, and only if You agree to indemnify,\r\n defend, and hold each Contributor harmless for any liability\r\n incurred by, or claims asserted against, such Contributor by reason\r\n of your accepting any such warranty or additional liability.\r\n\r\nEND OF TERMS AND CONDITIONS" }, - { - "name": "github.com/mitchellh/mapstructure", - "path": "github.com/mitchellh/mapstructure/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2013 Mitchell Hashimoto\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" - }, { "name": "github.com/modern-go/concurrent", "path": "github.com/modern-go/concurrent/LICENSE", @@ -974,6 +954,11 @@ "path": "github.com/modern-go/reflect2/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, + { + "name": "github.com/mschoch/smat", + "path": "github.com/mschoch/smat/LICENSE", + "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License." + }, { "name": "github.com/munnerz/goautoneg", "path": "github.com/munnerz/goautoneg/LICENSE", @@ -1129,6 +1114,11 @@ "path": "github.com/ssor/bom/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2017 Asher\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, + { + "name": "github.com/stretchr/objx", + "path": "github.com/stretchr/objx/LICENSE", + "licenseText": "The MIT License\n\nCopyright (c) 2014 Stretchr, Inc.\nCopyright (c) 2017-2018 objx contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" + }, { "name": "github.com/stretchr/testify", "path": "github.com/stretchr/testify/LICENSE", @@ -1164,11 +1154,6 @@ "path": "github.com/x448/float16/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n" }, - { - "name": "github.com/yohcop/openid-go", - "path": "github.com/yohcop/openid-go/LICENSE", - "licenseText": "Copyright 2015 Yohann Coppel\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n" - }, { "name": "github.com/yuin/goldmark-highlighting/v2", "path": "github.com/yuin/goldmark-highlighting/v2/LICENSE", @@ -1184,6 +1169,11 @@ "path": "github.com/zeebo/blake3/LICENSE", "licenseText": "This work is released into the public domain with CC0 1.0.\n\n-------------------------------------------------------------------------------\n\nCreative Commons Legal Code\n\nCC0 1.0 Universal\n\n CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE\n LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN\n ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS\n INFORMATION ON AN \"AS-IS\" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES\n REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS\n PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM\n THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED\n HEREUNDER.\n\nStatement of Purpose\n\nThe laws of most jurisdictions throughout the world automatically confer\nexclusive Copyright and Related Rights (defined below) upon the creator\nand subsequent owner(s) (each and all, an \"owner\") of an original work of\nauthorship and/or a database (each, a \"Work\").\n\nCertain owners wish to permanently relinquish those rights to a Work for\nthe purpose of contributing to a commons of creative, cultural and\nscientific works (\"Commons\") that the public can reliably and without fear\nof later claims of infringement build upon, modify, incorporate in other\nworks, reuse and redistribute as freely as possible in any form whatsoever\nand for any purposes, including without limitation commercial purposes.\nThese owners may contribute to the Commons to promote the ideal of a free\nculture and the further production of creative, cultural and scientific\nworks, or to gain reputation or greater distribution for their Work in\npart through the use and efforts of others.\n\nFor these and/or other purposes and motivations, and without any\nexpectation of additional consideration or compensation, the person\nassociating CC0 with a Work (the \"Affirmer\"), to the extent that he or she\nis an owner of Copyright and Related Rights in the Work, voluntarily\nelects to apply CC0 to the Work and publicly distribute the Work under its\nterms, with knowledge of his or her Copyright and Related Rights in the\nWork and the meaning and intended legal effect of CC0 on those rights.\n\n1. Copyright and Related Rights. A Work made available under CC0 may be\nprotected by copyright and related or neighboring rights (\"Copyright and\nRelated Rights\"). Copyright and Related Rights include, but are not\nlimited to, the following:\n\n i. the right to reproduce, adapt, distribute, perform, display,\n communicate, and translate a Work;\n ii. moral rights retained by the original author(s) and/or performer(s);\niii. publicity and privacy rights pertaining to a person's image or\n likeness depicted in a Work;\n iv. rights protecting against unfair competition in regards to a Work,\n subject to the limitations in paragraph 4(a), below;\n v. rights protecting the extraction, dissemination, use and reuse of data\n in a Work;\n vi. database rights (such as those arising under Directive 96/9/EC of the\n European Parliament and of the Council of 11 March 1996 on the legal\n protection of databases, and under any national implementation\n thereof, including any amended or successor version of such\n directive); and\nvii. other similar, equivalent or corresponding rights throughout the\n world based on applicable law or treaty, and any national\n implementations thereof.\n\n2. Waiver. To the greatest extent permitted by, but not in contravention\nof, applicable law, Affirmer hereby overtly, fully, permanently,\nirrevocably and unconditionally waives, abandons, and surrenders all of\nAffirmer's Copyright and Related Rights and associated claims and causes\nof action, whether now known or unknown (including existing as well as\nfuture claims and causes of action), in the Work (i) in all territories\nworldwide, (ii) for the maximum duration provided by applicable law or\ntreaty (including future time extensions), (iii) in any current or future\nmedium and for any number of copies, and (iv) for any purpose whatsoever,\nincluding without limitation commercial, advertising or promotional\npurposes (the \"Waiver\"). Affirmer makes the Waiver for the benefit of each\nmember of the public at large and to the detriment of Affirmer's heirs and\nsuccessors, fully intending that such Waiver shall not be subject to\nrevocation, rescission, cancellation, termination, or any other legal or\nequitable action to disrupt the quiet enjoyment of the Work by the public\nas contemplated by Affirmer's express Statement of Purpose.\n\n3. Public License Fallback. Should any part of the Waiver for any reason\nbe judged legally invalid or ineffective under applicable law, then the\nWaiver shall be preserved to the maximum extent permitted taking into\naccount Affirmer's express Statement of Purpose. In addition, to the\nextent the Waiver is so judged Affirmer hereby grants to each affected\nperson a royalty-free, non transferable, non sublicensable, non exclusive,\nirrevocable and unconditional license to exercise Affirmer's Copyright and\nRelated Rights in the Work (i) in all territories worldwide, (ii) for the\nmaximum duration provided by applicable law or treaty (including future\ntime extensions), (iii) in any current or future medium and for any number\nof copies, and (iv) for any purpose whatsoever, including without\nlimitation commercial, advertising or promotional purposes (the\n\"License\"). The License shall be deemed effective as of the date CC0 was\napplied by Affirmer to the Work. Should any part of the License for any\nreason be judged legally invalid or ineffective under applicable law, such\npartial invalidity or ineffectiveness shall not invalidate the remainder\nof the License, and in such case Affirmer hereby affirms that he or she\nwill not (i) exercise any of his or her remaining Copyright and Related\nRights in the Work or (ii) assert any associated claims and causes of\naction with respect to the Work, in either case contrary to Affirmer's\nexpress Statement of Purpose.\n\n4. Limitations and Disclaimers.\n\n a. No trademark or patent rights held by Affirmer are waived, abandoned,\n surrendered, licensed or otherwise affected by this document.\n b. Affirmer offers the Work as-is and makes no representations or\n warranties of any kind concerning the Work, express, implied,\n statutory or otherwise, including without limitation warranties of\n title, merchantability, fitness for a particular purpose, non\n infringement, or the absence of latent or other defects, accuracy, or\n the present or absence of errors, whether or not discoverable, all to\n the greatest extent permissible under applicable law.\n c. Affirmer disclaims responsibility for clearing rights of other persons\n that may apply to the Work or any use thereof, including without\n limitation any person's Copyright and Related Rights in the Work.\n Further, Affirmer disclaims responsibility for obtaining any necessary\n consents, permissions or other rights required for any use of the\n Work.\n d. Affirmer understands and acknowledges that Creative Commons is not a\n party to this document and has no duty or obligation with respect to\n this CC0 or use of the Work.\n" }, + { + "name": "github.com/zeebo/xxh3", + "path": "github.com/zeebo/xxh3/LICENSE", + "licenseText": "BSD 2-Clause License\n\nCopyright (c) 2012-2014, Yann Collet\nCopyright (c) 2019, Jeff Wendling\nAll rights reserved.\n\nxxHash Library\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice, this\n list of conditions and the following disclaimer in the documentation and/or\n other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" + }, { "name": "gitlab.com/gitlab-org/api/client-go", "path": "gitlab.com/gitlab-org/api/client-go/LICENSE", @@ -1207,7 +1197,7 @@ { "name": "go.uber.org/zap", "path": "go.uber.org/zap/LICENSE", - "licenseText": "Copyright (c) 2016-2017 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" + "licenseText": "Copyright (c) 2016-2024 Uber Technologies, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n" }, { "name": "go.uber.org/zap/exp/zapslog", @@ -1289,11 +1279,6 @@ "path": "gopkg.in/ini.v1/LICENSE", "licenseText": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n1. Definitions.\n\n\"License\" shall mean the terms and conditions for use, reproduction, and\ndistribution as defined by Sections 1 through 9 of this document.\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\nowner that is granting the License.\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\nthat control, are controlled by, or are under common control with that entity.\nFor the purposes of this definition, \"control\" means (i) the power, direct or\nindirect, to cause the direction or management of such entity, whether by\ncontract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\noutstanding shares, or (iii) beneficial ownership of such entity.\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\npermissions granted by this License.\n\n\"Source\" form shall mean the preferred form for making modifications, including\nbut not limited to software source code, documentation source, and configuration\nfiles.\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\ntranslation of a Source form, including but not limited to compiled object code,\ngenerated documentation, and conversions to other media types.\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\navailable under the License, as indicated by a copyright notice that is included\nin or attached to the work (an example is provided in the Appendix below).\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\nis based on (or derived from) the Work and for which the editorial revisions,\nannotations, elaborations, or other modifications represent, as a whole, an\noriginal work of authorship. For the purposes of this License, Derivative Works\nshall not include works that remain separable from, or merely link (or bind by\nname) to the interfaces of, the Work and Derivative Works thereof.\n\n\"Contribution\" shall mean any work of authorship, including the original version\nof the Work and any modifications or additions to that Work or Derivative Works\nthereof, that is intentionally submitted to Licensor for inclusion in the Work\nby the copyright owner or by an individual or Legal Entity authorized to submit\non behalf of the copyright owner. For the purposes of this definition,\n\"submitted\" means any form of electronic, verbal, or written communication sent\nto the Licensor or its representatives, including but not limited to\ncommunication on electronic mailing lists, source code control systems, and\nissue tracking systems that are managed by, or on behalf of, the Licensor for\nthe purpose of discussing and improving the Work, but excluding communication\nthat is conspicuously marked or otherwise designated in writing by the copyright\nowner as \"Not a Contribution.\"\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\nof whom a Contribution has been received by Licensor and subsequently\nincorporated within the Work.\n\n2. Grant of Copyright License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable copyright license to reproduce, prepare Derivative Works of,\npublicly display, publicly perform, sublicense, and distribute the Work and such\nDerivative Works in Source or Object form.\n\n3. Grant of Patent License.\n\nSubject to the terms and conditions of this License, each Contributor hereby\ngrants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,\nirrevocable (except as stated in this section) patent license to make, have\nmade, use, offer to sell, sell, import, and otherwise transfer the Work, where\nsuch license applies only to those patent claims licensable by such Contributor\nthat are necessarily infringed by their Contribution(s) alone or by combination\nof their Contribution(s) with the Work to which such Contribution(s) was\nsubmitted. If You institute patent litigation against any entity (including a\ncross-claim or counterclaim in a lawsuit) alleging that the Work or a\nContribution incorporated within the Work constitutes direct or contributory\npatent infringement, then any patent licenses granted to You under this License\nfor that Work shall terminate as of the date such litigation is filed.\n\n4. Redistribution.\n\nYou may reproduce and distribute copies of the Work or Derivative Works thereof\nin any medium, with or without modifications, and in Source or Object form,\nprovided that You meet the following conditions:\n\nYou must give any other recipients of the Work or Derivative Works a copy of\nthis License; and\nYou must cause any modified files to carry prominent notices stating that You\nchanged the files; and\nYou must retain, in the Source form of any Derivative Works that You distribute,\nall copyright, patent, trademark, and attribution notices from the Source form\nof the Work, excluding those notices that do not pertain to any part of the\nDerivative Works; and\nIf the Work includes a \"NOTICE\" text file as part of its distribution, then any\nDerivative Works that You distribute must include a readable copy of the\nattribution notices contained within such NOTICE file, excluding those notices\nthat do not pertain to any part of the Derivative Works, in at least one of the\nfollowing places: within a NOTICE text file distributed as part of the\nDerivative Works; within the Source form or documentation, if provided along\nwith the Derivative Works; or, within a display generated by the Derivative\nWorks, if and wherever such third-party notices normally appear. The contents of\nthe NOTICE file are for informational purposes only and do not modify the\nLicense. You may add Your own attribution notices within Derivative Works that\nYou distribute, alongside or as an addendum to the NOTICE text from the Work,\nprovided that such additional attribution notices cannot be construed as\nmodifying the License.\nYou may add Your own copyright statement to Your modifications and may provide\nadditional or different license terms and conditions for use, reproduction, or\ndistribution of Your modifications, or for any such Derivative Works as a whole,\nprovided Your use, reproduction, and distribution of the Work otherwise complies\nwith the conditions stated in this License.\n\n5. Submission of Contributions.\n\nUnless You explicitly state otherwise, any Contribution intentionally submitted\nfor inclusion in the Work by You to the Licensor shall be under the terms and\nconditions of this License, without any additional terms or conditions.\nNotwithstanding the above, nothing herein shall supersede or modify the terms of\nany separate license agreement you may have executed with Licensor regarding\nsuch Contributions.\n\n6. Trademarks.\n\nThis License does not grant permission to use the trade names, trademarks,\nservice marks, or product names of the Licensor, except as required for\nreasonable and customary use in describing the origin of the Work and\nreproducing the content of the NOTICE file.\n\n7. Disclaimer of Warranty.\n\nUnless required by applicable law or agreed to in writing, Licensor provides the\nWork (and each Contributor provides its Contributions) on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,\nincluding, without limitation, any warranties or conditions of TITLE,\nNON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are\nsolely responsible for determining the appropriateness of using or\nredistributing the Work and assume any risks associated with Your exercise of\npermissions under this License.\n\n8. Limitation of Liability.\n\nIn no event and under no legal theory, whether in tort (including negligence),\ncontract, or otherwise, unless required by applicable law (such as deliberate\nand grossly negligent acts) or agreed to in writing, shall any Contributor be\nliable to You for damages, including any direct, indirect, special, incidental,\nor consequential damages of any character arising as a result of this License or\nout of the use or inability to use the Work (including but not limited to\ndamages for loss of goodwill, work stoppage, computer failure or malfunction, or\nany and all other commercial damages or losses), even if such Contributor has\nbeen advised of the possibility of such damages.\n\n9. Accepting Warranty or Additional Liability.\n\nWhile redistributing the Work or Derivative Works thereof, You may choose to\noffer, and charge a fee for, acceptance of support, warranty, indemnity, or\nother liability obligations and/or rights consistent with this License. However,\nin accepting such obligations, You may act only on Your own behalf and on Your\nsole responsibility, not on behalf of any other Contributor, and only if You\nagree to indemnify, defend, and hold each Contributor harmless for any liability\nincurred by, or claims asserted against, such Contributor by reason of your\naccepting any such warranty or additional liability.\n\nEND OF TERMS AND CONDITIONS\n\nAPPENDIX: How to apply the Apache License to your work\n\nTo apply the Apache License to your work, attach the following boilerplate\nnotice, with the fields enclosed by brackets \"[]\" replaced with your own\nidentifying information. (Don't include the brackets!) The text should be\nenclosed in the appropriate comment syntax for the file format. We also\nrecommend that a file or class name and description of purpose be included on\nthe same \"printed page\" as the copyright notice for easier identification within\nthird-party archives.\n\n Copyright 2014 Unknwon\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, - { - "name": "gopkg.in/warnings.v0", - "path": "gopkg.in/warnings.v0/LICENSE", - "licenseText": "Copyright (c) 2016 Péter Surányi.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" - }, { "name": "gopkg.in/yaml.v2", "path": "gopkg.in/yaml.v2/LICENSE", diff --git a/build/lint-locale-usage/allowed-masked-usage.txt b/build/lint-locale-usage/allowed-masked-usage.txt index cfab25a5fd..205d3c3cca 100644 --- a/build/lint-locale-usage/allowed-masked-usage.txt +++ b/build/lint-locale-usage/allowed-masked-usage.txt @@ -6,39 +6,12 @@ translation_meta.test # this also gets instantiated as a Messenger once repo.migrate.migrating_failed.error -# models/asymkey/gpg_key_object_verification.go: $ObjectVerification.Reason -# unfortunately, it is non-trivial to parse all the occurences -gpg.error.extract_sign -gpg.error.failed_retrieval_gpg_keys -gpg.error.generate_hash -gpg.error.no_committer_account - -# models/system/notice.go: func (n *Notice) TrStr() string -admin.notices.type_1 -admin.notices.type_2 - # modules/setting/ui.go themes.names. # services/context/context.go relativetime. -# templates/repo/issue/view_content.tmpl: indirection via $closeTranslationKey -repo.issues.close -repo.pulls.close - -# templates/repo/issue/view_content/comments.tmpl: indirection via $refTr -repo.issues.ref_closing_from -repo.issues.ref_issue_from -repo.issues.ref_pull_from -repo.issues.ref_reopening_from - -# templates/repo/issue/view_content/comments.tmpl: ctx.Locale.Tr (printf "projects.type-%d.display_name" .OldProject.Type) -projects. -projects.type-1.display_name -projects.type-2.display_name -projects.type-3.display_name - # templates/repo/settings/webhook/link_menu.tmpl, templates/webhook/new.tmpl: repo.settings.web_hook_name_ # tests/integration/repo_archive_text_test.go repo.settings. diff --git a/build/lint-locale-usage/bin/handle-go.go b/build/lint-locale-usage/bin/handle-go.go index b1757fa0fc..5b68543e9d 100644 --- a/build/lint-locale-usage/bin/handle-go.go +++ b/build/lint-locale-usage/bin/handle-go.go @@ -15,6 +15,7 @@ import ( "strings" llu "forgejo.org/build/lint-locale-usage" + lluAsymKey "forgejo.org/models/asymkey/lint-locale-usage" lluUnit "forgejo.org/models/unit/lint-locale-usage" lluMigrate "forgejo.org/services/migrations/lint-locale-usage" ) @@ -36,142 +37,172 @@ func HandleGoFile(handler llu.Handler, fname string, src any) error { } ast.Inspect(node, func(n ast.Node) bool { - // search for function calls of the form `anything.Tr(any-string-lit, ...)` + return HandleGoNode(handler, fset, fname, n) + }) - switch n2 := n.(type) { - case *ast.CallExpr: - if len(n2.Args) == 0 { - return true + return nil +} + +func HandleGoNode(handler llu.Handler, fset *token.FileSet, fname string, n ast.Node) bool { + // search for function calls of the form `anything.Tr(any-string-lit, ...)` + + switch n2 := n.(type) { + case *ast.CallExpr: + if len(n2.Args) == 0 { + return true + } + funSel, ok := n2.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + + ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name] + if !ok { + return true + } + + var gotUnexpectedInvoke *int + + for _, argNum := range ltf { + if len(n2.Args) <= int(argNum) { + argc := len(n2.Args) + gotUnexpectedInvoke = &argc + } else { + handler.HandleGoTrArgument(fset, n2.Args[int(argNum)], "") } - funSel, ok := n2.Fun.(*ast.SelectorExpr) - if !ok { + } + + if gotUnexpectedInvoke != nil { + handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke) + } + + case *ast.CompositeLit: + if strings.HasSuffix(fname, "models/unit/unit.go") { + lluUnit.HandleCompositeUnit(handler, fset, n2) + } else if strings.Contains(fname, "models/asymkey/") { + lluAsymKey.HandleCompositeErrorReason(handler, fset, n2) + } + + case *ast.FuncDecl: + if matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKeyWeak"); matchInsPrefix != nil { + results := n2.Type.Results.List + if len(results) != 1 { + handler.OnWarning(fset, n2.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", n2.Name.Name)) return true } - ltf, ok := handler.LocaleTrFunctions[funSel.Sel.Name] - if !ok { - return true - } - - var gotUnexpectedInvoke *int - - for _, argNum := range ltf { - if len(n2.Args) <= int(argNum) { - argc := len(n2.Args) - gotUnexpectedInvoke = &argc - } else { - handler.HandleGoTrArgument(fset, n2.Args[int(argNum)], "") + ast.Inspect(n2.Body, func(n ast.Node) bool { + // search for return stmts + // TODO: what about nested functions? + if ret, ok := n.(*ast.ReturnStmt); ok { + for _, res := range ret.Results { + ast.Inspect(res, func(n ast.Node) bool { + if expr, ok := n.(ast.Expr); ok { + handler.HandleGoTrArgument(fset, expr, *matchInsPrefix) + } + return true + }) + } + return false } + return true + }) + } + + if matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKey"); matchInsPrefix != nil { + results := n2.Type.Results.List + if len(results) != 1 { + handler.OnWarning(fset, n2.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", n2.Name.Name)) + return true } - if gotUnexpectedInvoke != nil { - handler.OnUnexpectedInvoke(fset, funSel.Sel.NamePos, funSel.Sel.Name, *gotUnexpectedInvoke) - } - - case *ast.CompositeLit: - if strings.HasSuffix(fname, "models/unit/unit.go") { - lluUnit.HandleCompositeUnit(handler, fset, n2) - } - - case *ast.FuncDecl: - matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, "llu:returnsTrKey") - if matchInsPrefix != nil { - results := n2.Type.Results.List - if len(results) != 1 { - handler.OnWarning(fset, n2.Type.Func, fmt.Sprintf("function %s has unexpected return type; expected single return value", n2.Name.Name)) - return true + ast.Inspect(n2.Body, func(n ast.Node) bool { + // search for return stmts + if ret, ok := n.(*ast.ReturnStmt); ok { + for _, res := range ret.Results { + handler.HandleGoTrArgument(fset, res, *matchInsPrefix) + } + return false + } else if _, ok := n.(*ast.FuncDecl); ok { + ast.Inspect(n, func(n2 ast.Node) bool { + return HandleGoNode(handler, fset, fname, n2) + }) + // don't search inside nested functions for return stmts + return false } + return true + }) + } - ast.Inspect(n2.Body, func(n ast.Node) bool { - // search for return stmts - // TODO: what about nested functions? - if ret, ok := n.(*ast.ReturnStmt); ok { - for _, res := range ret.Results { - ast.Inspect(res, func(n ast.Node) bool { - if expr, ok := n.(ast.Expr); ok { - handler.HandleGoTrArgument(fset, expr, *matchInsPrefix) - } - return true - }) - } + if strings.HasSuffix(fname, "services/migrations/migrate.go") { + lluMigrate.HandleMessengerInFunc(handler, fset, n2) + } + return true + case *ast.GenDecl: + switch n2.Tok { + case token.CONST, token.VAR: + matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, " llu:TrKeys") + if matchInsPrefix == nil { + return true + } + for _, spec := range n2.Specs { + // interpret all contained strings as message IDs + ast.Inspect(spec, func(n ast.Node) bool { + if argLit, ok := n.(*ast.BasicLit); ok { + handler.HandleGoTrBasicLit(fset, argLit, *matchInsPrefix) return false } return true }) } - if strings.HasSuffix(fname, "services/migrations/migrate.go") { - lluMigrate.HandleMessengerInFunc(handler, fset, n2) - } - return true - case *ast.GenDecl: - switch n2.Tok { - case token.CONST, token.VAR: - matchInsPrefix := handler.HandleGoCommentGroup(fset, n2.Doc, " llu:TrKeys") - if matchInsPrefix == nil { - return true - } - for _, spec := range n2.Specs { - // interpret all contained strings as message IDs - ast.Inspect(spec, func(n ast.Node) bool { - if argLit, ok := n.(*ast.BasicLit); ok { - handler.HandleGoTrBasicLit(fset, argLit, *matchInsPrefix) - return false - } - return true - }) - } + case token.TYPE: + // modules/web/middleware/binding.go:Validate uses the convention that structs + // entries can have tags. + // In particular, `locale:$msgid` should be handled; any fields with `form:-` shouldn't. + // Problem: we don't know which structs are forms, actually. - case token.TYPE: - // modules/web/middleware/binding.go:Validate uses the convention that structs - // entries can have tags. - // In particular, `locale:$msgid` should be handled; any fields with `form:-` shouldn't. - // Problem: we don't know which structs are forms, actually. - - for _, spec := range n2.Specs { - tspec := spec.(*ast.TypeSpec) - structNode, ok := tspec.Type.(*ast.StructType) - if !ok || !(strings.HasSuffix(tspec.Name.Name, "Form") || - (tspec.Doc != nil && - slices.ContainsFunc(tspec.Doc.List, func(c *ast.Comment) bool { - return c.Text == "// swagger:model" - }))) { + for _, spec := range n2.Specs { + tspec := spec.(*ast.TypeSpec) + structNode, ok := tspec.Type.(*ast.StructType) + if !ok || !(strings.HasSuffix(tspec.Name.Name, "Form") || + (tspec.Doc != nil && + slices.ContainsFunc(tspec.Doc.List, func(c *ast.Comment) bool { + return c.Text == "// swagger:model" + }))) { + continue + } + for _, field := range structNode.Fields.List { + if field.Names == nil { continue } - for _, field := range structNode.Fields.List { - if field.Names == nil { - continue - } - if len(field.Names) != 1 { - handler.OnWarning(fset, field.Type.Pos(), "unsupported multiple field names") - continue - } - msgidPos := field.Names[0].NamePos - msgid := "form." + field.Names[0].Name - if field.Tag != nil && field.Tag.Kind == token.STRING { - rawTag, err := strconv.Unquote(field.Tag.Value) - if err != nil { - handler.OnWarning(fset, field.Tag.ValuePos, "invalid tag value encountered") - continue - } - tag := reflect.StructTag(rawTag) - if tag.Get("form") == "-" { - continue - } - tmp := tag.Get("locale") - if len(tmp) != 0 { - msgidPos = field.Tag.ValuePos - msgid = tmp - } - } - handler.OnMsgid(fset, msgidPos, msgid, true) + if len(field.Names) != 1 { + handler.OnWarning(fset, field.Type.Pos(), "unsupported multiple field names") + continue } + msgidPos := field.Names[0].NamePos + msgid := "form." + field.Names[0].Name + if field.Tag != nil && field.Tag.Kind == token.STRING { + rawTag, err := strconv.Unquote(field.Tag.Value) + if err != nil { + handler.OnWarning(fset, field.Tag.ValuePos, "invalid tag value encountered") + continue + } + tag := reflect.StructTag(rawTag) + if tag.Get("form") == "-" { + continue + } + tmp := tag.Get("locale") + if len(tmp) != 0 { + msgidPos = field.Tag.ValuePos + msgid = tmp + } + } + handler.OnMsgid(fset, msgidPos, msgid, true) } } } + } - return true - }) - - return nil + return true } diff --git a/build/lint-locale-usage/bin/lint-locale-usage.go b/build/lint-locale-usage/bin/lint-locale-usage.go index d9cdf9f321..f04b14f3b2 100644 --- a/build/lint-locale-usage/bin/lint-locale-usage.go +++ b/build/lint-locale-usage/bin/lint-locale-usage.go @@ -13,6 +13,7 @@ import ( "io/fs" "os" "path/filepath" + "regexp" "sort" "strings" @@ -44,12 +45,57 @@ type StringTrie interface { type StringTrieMap map[string]StringTrie +func printfPatternToRegex(key string) (string, bool) { + parts := strings.Split(key, "%") + if len(parts) < 2 { + return key, false + } + var pattern strings.Builder + pattern.WriteString("^") + pattern.WriteString(parts[0]) + skip := false + for _, part := range parts[1:] { + if skip { + skip = false + continue + } + if len(part) == 0 { + // "%%" + pattern.WriteString("%") + continue + } + switch part[0] { + case 'd': + pattern.WriteString("[0-9]+") + default: + pattern.WriteString("[A-Za-z0-9]*") + } + pattern.WriteString(part[1:]) + } + pattern.WriteString("$") + return pattern.String(), true +} + func (m StringTrieMap) Matches(key []string) bool { if len(key) == 0 || m == nil { return true } value, ok := m[key[0]] if !ok { + for altKey, value := range m { + // TODO: cache mapping $printfFormatString -> $regexpCompileOutput + pattern, found := printfPatternToRegex(altKey) + if !found { + continue + } + matched, err := regexp.MatchString(pattern, key[0]) + if err != nil { + panic(fmt.Sprintf("unable to compile regexp '%s': %s", pattern, err.Error())) + } + if matched && (value == nil || value.Matches(key[1:])) { + return true + } + } return false } if value == nil { @@ -101,7 +147,7 @@ func ParseAllowedMaskedUsages(fname string, usedMsgids container.Set[string], al if line == "" || strings.HasPrefix(line, "#") { continue } - if linePrefix, found := strings.CutSuffix(line, "."); found { + if linePrefix, found := strings.CutSuffix(line, "."); found || strings.Contains(line, "%") { allowedMaskedPrefixes.Insert(strings.Split(linePrefix, ".")) } else { if !chkMsgid(line) { @@ -145,9 +191,14 @@ func Usage() { fmt.Fprintf(outp, "\nSpecial Go doc comments:\n") for _, i := range []string{ + "//llu:returnsTrKeyWeak", + "\tcan be used in front of functions to indicate", + "\tthat the function returns message IDs (allows nesting inside complicated function calls)", + "\tWARNING: this currently doesn't support nested functions properly", + "", "//llu:returnsTrKey", "\tcan be used in front of functions to indicate", - "\tthat the function returns message IDs", + "\tthat the function returns message IDs (doesn't allow nesting inside complicated function calls)", "\tWARNING: this currently doesn't support nested functions properly", "", "//llu:returnsTrKeySuffix prefix.", @@ -260,6 +311,10 @@ func main() { } handler := llu.Handler{ + OnMsgidPattern: func(fset *token.FileSet, pos token.Pos, msgidPattern string) { + msgidPatternSplit := strings.Split(msgidPattern, ".") + allowedMaskedPrefixes.Insert(msgidPatternSplit) + }, OnMsgidPrefix: func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool) { msgidPrefixSplit := strings.Split(msgidPrefix, ".") if !truncated { @@ -270,6 +325,10 @@ func main() { } }, OnMsgid: func(fset *token.FileSet, pos token.Pos, msgid string, weak bool) { + if strings.Contains(msgid, "%") { + fmt.Printf("%s:\tunexpected msgid pattern: %s\n", fset.Position(pos).String(), msgid) + return + } if !msgids.Contains(msgid) { if weak && allowWeakMissingMsgids { return @@ -302,7 +361,7 @@ func main() { if name == "docker" || name == ".git" || name == "node_modules" { return fs.SkipDir } - } else if name == "bindata.go" || fpath == "modules/translation/i18n/i18n_test.go" { + } else if name == "bindata.go" || fpath == "modules/translation/i18n/i18n_test.go" || fpath == "modules/translation/i18n/i18n_ini_test.go" { // skip false positives } else if strings.HasSuffix(name, ".go") { onError(HandleGoFile(handler, fpath, nil)) diff --git a/build/lint-locale-usage/handle-go.go b/build/lint-locale-usage/handle-go.go index 44229e52f7..a8e478a6ef 100644 --- a/build/lint-locale-usage/handle-go.go +++ b/build/lint-locale-usage/handle-go.go @@ -34,12 +34,14 @@ func (handler Handler) HandleGoTrBasicLit(fset *token.FileSet, argLit *ast.Basic } func (handler Handler) HandleGoTrArgument(fset *token.FileSet, n ast.Expr, prefix string) { - if argLit, ok := n.(*ast.BasicLit); ok { - handler.HandleGoTrBasicLit(fset, argLit, prefix) - } else if argBinExpr, ok := n.(*ast.BinaryExpr); ok { - if argBinExpr.Op != token.ADD { + switch n := n.(type) { + case *ast.BasicLit: + handler.HandleGoTrBasicLit(fset, n, prefix) + + case *ast.BinaryExpr: + if n.Op != token.ADD { // pass - } else if argLit, ok := argBinExpr.X.(*ast.BasicLit); ok && argLit.Kind == token.STRING { + } else if argLit, ok := n.X.(*ast.BasicLit); ok && argLit.Kind == token.STRING { // extract string content arg, err := strconv.Unquote(argLit.Value) if err != nil { @@ -53,6 +55,39 @@ func (handler Handler) HandleGoTrArgument(fset *token.FileSet, n ast.Expr, prefi } handler.OnMsgidPrefix(fset, argLit.ValuePos, prep, trunc) } + + case *ast.CallExpr: + if selExpr, ok := n.Fun.(*ast.SelectorExpr); ok { + if xIdent, xok := selExpr.X.(*ast.Ident); !xok || xIdent.Name != "fmt" { + return + } + if selExpr.Sel.Name != "Sprintf" { + handler.OnWarning(fset, selExpr.Sel.NamePos, fmt.Sprintf("unexpected formatting function encountered: %s", selExpr.Sel.Name)) + return + } + if len(n.Args) == 0 { + handler.OnWarning(fset, selExpr.Sel.NamePos, fmt.Sprintf("unexpected formatting function invocation (no arguments) of '%s'", selExpr.Sel.Name)) + return + } + + if argLit, ok := n.Args[0].(*ast.BasicLit); ok && argLit.Kind == token.STRING { + // extract string content + arg, err := strconv.Unquote(argLit.Value) + if err != nil { + return + } + if strings.Contains(arg, " ") { + handler.OnWarning(fset, argLit.ValuePos, fmt.Sprintf( + "formatting function invocation of '%s' with weird msgid format string: %s", + selExpr.Sel.Name, + arg, + )) + return + } + // found interesting strings + handler.OnMsgidPattern(fset, argLit.ValuePos, prefix+arg) + } + } } } diff --git a/build/lint-locale-usage/handle-tmpl.go b/build/lint-locale-usage/handle-tmpl.go index 8d03291205..e37c1eb486 100644 --- a/build/lint-locale-usage/handle-tmpl.go +++ b/build/lint-locale-usage/handle-tmpl.go @@ -60,9 +60,13 @@ func (handler Handler) handleTemplateNode(fset *token.FileSet, node tmplParser.N case tmplParser.NodeField: nodeField := nodeCommand.Args[0].(*tmplParser.FieldNode) - if len(nodeField.Ident) != 2 || !(nodeField.Ident[0] == "locale" || nodeField.Ident[0] == "Locale") { + if len(nodeField.Ident) != 2 || nodeField.Ident[0] != "locale" { return } + resolvedPos := fset.PositionFor(token.Pos(nodeCommand.Pos), false) + if !strings.Contains(resolvedPos.Filename, "templates/mail/") { + handler.OnWarning(fset, token.Pos(nodeCommand.Pos), "encountered unexpected .locale usage") + } funcname = nodeField.Ident[1] case tmplParser.NodeVariable: @@ -146,16 +150,12 @@ func (handler Handler) handleTemplateMsgid(fset *token.FileSet, node tmplParser. handler.OnMsgid(fset, stringPos, msgidPrefix, false) } else { if nodeIdent.Ident == "printf" { - parts := strings.SplitN(msgidPrefix, "%", 2) - if len(parts) != 2 { - handler.OnWarning( - fset, - stringPos, - fmt.Sprintf("unsupported invocation of locate function (format string doesn't match \"prefix%%smth\" pattern): %s", nodeString.String()), - ) + // found interesting strings + if !(strings.HasSuffix(msgidPrefix, ".%s") && strings.Count(msgidPrefix, "%") == 1) { + handler.OnMsgidPattern(fset, stringPos, msgidPrefix) return } - msgidPrefix = parts[0] + msgidPrefix = strings.TrimSuffix(msgidPrefix, "%s") } msgidPrefixFin, truncated := PrepareMsgidPrefix(msgidPrefix) diff --git a/build/lint-locale-usage/handler.go b/build/lint-locale-usage/handler.go index 6673ac3a4d..ef517cd040 100644 --- a/build/lint-locale-usage/handler.go +++ b/build/lint-locale-usage/handler.go @@ -47,6 +47,7 @@ func InitLocaleTrFunctions() map[string][]uint { type Handler struct { OnMsgid func(fset *token.FileSet, pos token.Pos, msgid string, weak bool) OnMsgidPrefix func(fset *token.FileSet, pos token.Pos, msgidPrefix string, truncated bool) + OnMsgidPattern func(fset *token.FileSet, pos token.Pos, msgidPattern string) OnUnexpectedInvoke func(fset *token.FileSet, pos token.Pos, funcname string, argc int) OnWarning func(fset *token.FileSet, pos token.Pos, msg string) LocaleTrFunctions map[string][]uint diff --git a/cmd/admin.go b/cmd/admin.go index 60b25eb971..fe212cc388 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -47,7 +47,6 @@ func subcmdRegenerate() *cli.Command { Name: "regenerate", Usage: "Regenerate specific files", Commands: []*cli.Command{ - microcmdRegenHooks, microcmdRegenKeys, }, } diff --git a/cmd/admin_regenerate.go b/cmd/admin_regenerate.go index 4d14df317d..4620a7106d 100644 --- a/cmd/admin_regenerate.go +++ b/cmd/admin_regenerate.go @@ -7,36 +7,15 @@ import ( "context" asymkey_model "forgejo.org/models/asymkey" - "forgejo.org/modules/graceful" - repo_service "forgejo.org/services/repository" "github.com/urfave/cli/v3" ) -var ( - microcmdRegenHooks = &cli.Command{ - Name: "hooks", - Usage: "Regenerate git-hooks", - Before: noDanglingArgs, - Action: runRegenerateHooks, - } - - microcmdRegenKeys = &cli.Command{ - Name: "keys", - Usage: "Regenerate authorized_keys file", - Before: noDanglingArgs, - Action: runRegenerateKeys, - } -) - -func runRegenerateHooks(ctx context.Context, c *cli.Command) error { - ctx, cancel := installSignals(ctx) - defer cancel() - - if err := initDB(ctx); err != nil { - return err - } - return repo_service.SyncRepositoryHooks(graceful.GetManager().ShutdownContext()) +var microcmdRegenKeys = &cli.Command{ + Name: "keys", + Usage: "Regenerate authorized_keys file", + Before: noDanglingArgs, + Action: runRegenerateKeys, } func runRegenerateKeys(ctx context.Context, c *cli.Command) error { diff --git a/cmd/admin_user.go b/cmd/admin_user.go index f4f6fb49af..ea62bd3a45 100644 --- a/cmd/admin_user.go +++ b/cmd/admin_user.go @@ -17,6 +17,7 @@ func subcmdUser() *cli.Command { microcmdUserChangePassword(), microcmdUserDelete(), microcmdUserGenerateAccessToken(), + microcmdUserCreateAuthorizedIntegration(), microcmdUserMustChangePassword(), microcmdUserResetMFA(), }, diff --git a/cmd/admin_user_create.go b/cmd/admin_user_create.go index e3800bdb59..2881091589 100644 --- a/cmd/admin_user_create.go +++ b/cmd/admin_user_create.go @@ -205,7 +205,15 @@ func runCreateUser(ctx context.Context, c *cli.Command) error { // create the access token if accessTokenScope != "" { - t := &auth_model.AccessToken{Name: accessTokenName, UID: u.ID, Scope: accessTokenScope} + t := &auth_model.AccessToken{ + Name: accessTokenName, + UID: u.ID, + Scope: accessTokenScope, + + // maintain legacy behaviour until new CLI options are added -- token has access to all resources, is not + // fine-grained + ResourceAllRepos: true, + } if err := auth_model.NewAccessToken(ctx, t); err != nil { return err } diff --git a/cmd/admin_user_generate_access_token.go b/cmd/admin_user_generate_access_token.go index 0a3a7fa89d..8b1d14f946 100644 --- a/cmd/admin_user_generate_access_token.go +++ b/cmd/admin_user_generate_access_token.go @@ -86,6 +86,10 @@ func runGenerateAccessToken(ctx context.Context, c *cli.Command) error { } t.Scope = accessTokenScope + // maintain legacy behaviour until new CLI options are added -- token has access to all resources, is not + // fine-grained + t.ResourceAllRepos = true + // create the token if err := auth_model.NewAccessToken(ctx, t); err != nil { return err diff --git a/cmd/admin_user_generate_authorized_integration.go b/cmd/admin_user_generate_authorized_integration.go new file mode 100644 index 0000000000..05d7bcc07a --- /dev/null +++ b/cmd/admin_user_generate_authorized_integration.go @@ -0,0 +1,269 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package cmd + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "strings" + + auth_model "forgejo.org/models/auth" + "forgejo.org/models/db" + "forgejo.org/models/repo" + user_model "forgejo.org/models/user" + "forgejo.org/modules/json" + "forgejo.org/services/authz" + + "github.com/urfave/cli/v3" +) + +func microcmdUserCreateAuthorizedIntegration() *cli.Command { + return &cli.Command{ + Name: "create-authorized-integration", + Description: `Creates an authorized integration. Authorized integrations allow Forgejo to +receive JWTs from external sources, validate their claims against +user-defined rules, and grant access to Forgejo's API on behalf of a user. + +The issuer may be set to "urn:forgejo:authorized-integrations:actions" +to support JWTs from the local instance's Forgejo Actions, utilizing the +enable-openid-connect flag in a workflow.`, + + // `--claim-in sub=v1,v2,v3` needs to be parsed as a single parameter so that we can comma-split the value into + // an array. To accomplish this, we disable urfave 's slice flag separator, which would cause this to be + // treated as "sub=v1", "v2=?", and "v3=?", resulting in an error of missing values. + DisableSliceFlagSeparator: true, + + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username", + Required: true, + }, + &cli.StringFlag{ + Name: "name", + Usage: "Name of the authorized integration for later identification", + Required: true, + }, + &cli.StringFlag{ + Name: "description", + Usage: "Optional description for the authorized integration", + }, + + // JWT validation: + &cli.StringFlag{ + Name: "issuer", + Usage: `JWT issuer ('iss' claim), example: https://forgejo.example.org/api/actions`, + Required: true, + }, + &cli.StringMapFlag{ + Name: "claim-eq", + Value: map[string]string{}, + Usage: `Zero-or-more claim equality checks, formatted as claim=value, example: "actor=someuser"`, + }, + &cli.StringMapFlag{ + Name: "claim-in", + Value: map[string]string{}, + Usage: `Zero-or-more claim equality in list checks, formatted as claim=value1,value2,... example: "actor=user1,user2"`, + }, + &cli.StringMapFlag{ + Name: "claim-glob", + Value: map[string]string{}, + Usage: `Zero-or-more claim glob checks, formatted as claim=value, example: "sub=repo:forgejo/*:pull_request"`, + }, + &cli.StringMapFlag{ + Name: "claim-glob-in", + Value: map[string]string{}, + Usage: `Zero-or-more claim glob in list checks, formatted as claim=va*ue1,va*ue2,... example: "sub=repo:*/*:pull_request,repo:*/*:refs:*"`, + }, + // nested claim support omitted for now -- pretty complex for a CLI + + // Permissions available on successful auth: + &cli.StringSliceFlag{ + Name: "scope", + Value: []string{"all"}, + Usage: `One-or-more scopes to apply to access token, examples: "all", "read:issue", "write:repository"`, + }, + &cli.StringSliceFlag{ + Name: "repo", + Value: []string{"all"}, + Usage: `Zero-or-more specific repositories that can be accessed, or "all" to allow access to all repositories, example: "owner1/repo1"`, + }, + }, + Before: noDanglingArgs, + Action: runCreateAuthorizedIntegration, + } +} + +func runCreateAuthorizedIntegration(ctx context.Context, c *cli.Command) error { + if !c.IsSet("username") { + return errors.New("you must provide a username to generate a token for") + } + + ctx, cancel := installSignals(ctx) + defer cancel() + + if err := initDB(ctx); err != nil { + return err + } + + user, err := user_model.GetUserByName(ctx, c.String("username")) + if err != nil { + return err + } + + ai := &auth_model.AuthorizedIntegration{ + UserID: user.ID, + Name: c.String("name"), + Description: c.String("description"), + } + + var rules []auth_model.ClaimRule + ai.Issuer = c.String("issuer") + for claim, value := range c.StringMap("claim-eq") { + rules = append(rules, auth_model.ClaimRule{ + Claim: claim, + Comparison: auth_model.ClaimEqual, + Value: value, + }) + } + for claim, value := range c.StringMap("claim-in") { + values := []string{} + for s := range strings.SplitSeq(value, ",") { + values = append(values, strings.TrimSpace(s)) + } + rules = append(rules, auth_model.ClaimRule{ + Claim: claim, + Comparison: auth_model.ClaimIn, + Values: values, + }) + } + for claim, value := range c.StringMap("claim-glob") { + rules = append(rules, auth_model.ClaimRule{ + Claim: claim, + Comparison: auth_model.ClaimGlob, + Value: value, + }) + } + for claim, value := range c.StringMap("claim-glob-in") { + values := []string{} + for s := range strings.SplitSeq(value, ",") { + values = append(values, strings.TrimSpace(s)) + } + rules = append(rules, auth_model.ClaimRule{ + Claim: claim, + Comparison: auth_model.ClaimGlobIn, + Values: values, + }) + } + ai.ClaimRules = &auth_model.ClaimRules{Rules: rules} + + scopes := strings.Join(c.StringSlice("scope"), ",") + accessTokenScope, err := auth_model.AccessTokenScope(scopes).Normalize() + if err != nil { + return fmt.Errorf("invalid access token scope provided: %w", err) + } + ai.Scope = accessTokenScope + + allRepos := false + repos := []*repo.Repository{} + for _, repoName := range c.StringSlice("repo") { + if repoName == "all" { + allRepos = true + } else { + split := strings.Split(repoName, "/") + if len(split) != 2 { + return fmt.Errorf("invalid repo name: %q", split) + } + owner := split[0] + name := split[1] + repo, err := repo.GetRepositoryByOwnerAndName(ctx, owner, name) + if err != nil { + return err + } + repos = append(repos, repo) + } + } + ai.ResourceAllRepos = allRepos + + rr := make([]*auth_model.AuthorizedIntegResourceRepo, len(repos)) + for i := range repos { + rr[i] = &auth_model.AuthorizedIntegResourceRepo{RepoID: repos[i].ID} + } + if err := authz.ValidateAuthorizedIntegration(ai, rr); err != nil { + return err + } + + err = db.WithTx(ctx, func(ctx context.Context) error { + if err := auth_model.InsertAuthorizedIntegration(ctx, ai); err != nil { + return err + } + if !allRepos { + if err := auth_model.InsertAuthorizedIntegrationResourceRepos(ctx, ai.ID, rr); err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + + type ClaimRuleDescription struct { + Description string `json:"description"` + Claim string `json:"claim"` + Comparison auth_model.ClaimComparison `json:"compare"` + Value string `json:"value,omitempty"` + Values []string `json:"values,omitempty"` + } + output := struct { + Message string `json:"message"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Issuer string `json:"issuer"` + Audience string `json:"audience"` + ClaimRules []ClaimRuleDescription `json:"claim_rules"` + }{ + Message: "Authorized integration was successfully created.", + Name: ai.Name, + Description: ai.Description, + Issuer: ai.Issuer, + Audience: ai.Audience, + } + for _, cr := range ai.ClaimRules.Rules { + var description string + switch cr.Comparison { + case auth_model.ClaimEqual: + description = fmt.Sprintf("%q = %q", cr.Claim, cr.Value) + case auth_model.ClaimIn: + description = fmt.Sprintf("%q in %q", cr.Claim, cr.Values) + case auth_model.ClaimGlob: + description = fmt.Sprintf("%q matches %q", cr.Claim, cr.Value) + case auth_model.ClaimGlobIn: + description = fmt.Sprintf("%q matches in %q", cr.Claim, cr.Values) + } + output.ClaimRules = append(output.ClaimRules, ClaimRuleDescription{ + Description: description, + Claim: cr.Claim, + Comparison: cr.Comparison, + Value: cr.Value, + Values: cr.Values, + }) + } + + raw, err := json.Marshal(output) + if err != nil { + return err + } + var indent bytes.Buffer + if err := json.Indent(&indent, raw, "", " "); err != nil { + return err + } + os.Stdout.Write(indent.Bytes()) + + return nil +} diff --git a/cmd/cert.go b/cmd/cert.go index baadcbda85..516ac4ce84 100644 --- a/cmd/cert.go +++ b/cmd/cert.go @@ -150,8 +150,8 @@ func runCert(ctx context.Context, c *cli.Command) error { BasicConstraintsValid: true, } - hosts := strings.Split(c.String("host"), ",") - for _, h := range hosts { + hosts := strings.SplitSeq(c.String("host"), ",") + for h := range hosts { if ip := net.ParseIP(h); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { diff --git a/cmd/cmd.go b/cmd/cmd.go index 379609d294..a6e5d8fcfe 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -1,4 +1,5 @@ // Copyright 2018 The Gitea Authors. All rights reserved. +// Copyright 2026 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT // Package cmd provides subcommands to the gitea binary - such as "web" or @@ -89,26 +90,9 @@ If this is the intended configuration file complete the [database] section.`, se return nil } +// installSignals returns a context that's cancelled on the SIGINT and SIGTERM signals or if the passed ctx is cancelled. func installSignals(ctx context.Context) (context.Context, context.CancelFunc) { - ctx, cancel := context.WithCancel(ctx) - go func() { - // install notify - signalChannel := make(chan os.Signal, 1) - - signal.Notify( - signalChannel, - syscall.SIGINT, - syscall.SIGTERM, - ) - select { - case <-signalChannel: - case <-ctx.Done(): - } - cancel() - signal.Reset() - }() - - return ctx, cancel + return signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) } func setupConsoleLogger(level log.Level, colorize bool, out io.Writer) { diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go new file mode 100644 index 0000000000..8e66cbdba1 --- /dev/null +++ b/cmd/cmd_test.go @@ -0,0 +1,39 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT +package cmd + +import ( + "context" + "fmt" + "runtime" + "syscall" + "testing" + "time" +) + +func Test_installSignals(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skipf("Windows does not terminate in an awaitable manner") + return + } + + for _, s := range []syscall.Signal{syscall.SIGTERM, syscall.SIGINT} { + t.Run(fmt.Sprintf("Context is terminated on %s", s), func(t *testing.T) { + // Register the signal handler. context.Background() is chosen deliberately, + // because unlike t.Context(), we can be sure that it's not cancelled by a + // different handler. + ctx, cancel := installSignals(context.Background()) + t.Cleanup(cancel) + + // Send the signal in the background. + go syscall.Kill(syscall.Getpid(), s) + + select { + case <-time.Tick(time.Second * 10): + t.Fatalf("Context not cancelled via signal after 10 seconds") + case <-ctx.Done(): + t.Logf("Context was cancelled") + } + }) + } +} diff --git a/cmd/dump.go b/cmd/dump.go index b94277e529..9fe326dfbb 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -12,6 +12,7 @@ import ( "os" "path" "path/filepath" + "slices" "strings" "sync" "time" @@ -83,11 +84,9 @@ func (o outputType) Join() string { } func (o *outputType) Set(value string) error { - for _, enum := range o.Enum { - if enum == value { - o.selected = value - return nil - } + if slices.Contains(o.Enum, value) { + o.selected = value + return nil } return fmt.Errorf("allowed values are %s", o.Join()) @@ -113,7 +112,10 @@ func getArchiverByType(outType string) (archives.ArchiverAsync, error) { var archiver archives.ArchiverAsync switch outType { case "zip": - archiver = archives.Zip{} + archiver = archives.Zip{ + Compression: 8, + SelectiveCompression: false, + } case "tar": archiver = archives.Tar{} case "tar.sz": @@ -250,8 +252,8 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { setupConsoleLogger(log.FATAL, log.CanColorStderr, os.Stderr) } else { for _, suffix := range outputTypeEnum.Enum { - if strings.HasSuffix(fileName, "."+suffix) { - fileName = strings.TrimSuffix(fileName, "."+suffix) + if before, ok := strings.CutSuffix(fileName, "."+suffix); ok { + fileName = before break } } @@ -330,14 +332,12 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { go dumpDatabase(ctx, archiveJobs, &wg, verbose) if len(setting.CustomConf) > 0 { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { log.Info("Adding custom configuration file from %s", setting.CustomConf) if err := addFile(archiveJobs, "app.ini", setting.CustomConf, verbose); err != nil { fatal("Failed to include specified app.ini: %v", err) } - }() + }) } if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") { @@ -361,15 +361,13 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { if ctx.IsSet("skip-attachment-data") && ctx.Bool("skip-attachment-data") { log.Info("Skipping attachment data") } else { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { if err := storage.Attachments.IterateObjects("", func(objPath string, object storage.Object) error { return addObject(archiveJobs, object, path.Join("data", "attachments", objPath), verbose) }); err != nil { fatal("Failed to dump attachments: %v", err) } - }() + }) } if ctx.IsSet("skip-package-data") && ctx.Bool("skip-package-data") { @@ -377,15 +375,13 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { } else if !setting.Packages.Enabled { log.Info("Package registry not enabled - skipping") } else { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { if err := storage.Packages.IterateObjects("", func(objPath string, object storage.Object) error { return addObject(archiveJobs, object, path.Join("data", "packages", objPath), verbose) }); err != nil { fatal("Failed to dump packages: %v", err) } - }() + }) } // Doesn't check if LogRootPath exists before processing --skip-log intentionally, @@ -399,13 +395,11 @@ func runDump(stdCtx context.Context, ctx *cli.Command) error { log.Error("Failed to check if %s exists: %v", setting.Log.RootPath, err) } if isExist { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { if err := addRecursiveExclude(archiveJobs, "log", setting.Log.RootPath, []string{absFileName}, verbose); err != nil { fatal("Failed to include log: %v", err) } - }() + }) } } diff --git a/cmd/dump_repo.go b/cmd/dump_repo.go index 8e0ef0311f..60fffe1226 100644 --- a/cmd/dump_repo.go +++ b/cmd/dump_repo.go @@ -143,8 +143,8 @@ func runDumpRepository(stdCtx context.Context, ctx *cli.Command) error { opts.PullRequests = true opts.ReleaseAssets = true } else { - units := strings.Split(ctx.String("units"), ",") - for _, unit := range units { + units := strings.SplitSeq(ctx.String("units"), ",") + for unit := range units { switch strings.ToLower(strings.TrimSpace(unit)) { case "": continue diff --git a/cmd/forgejo/actions.go b/cmd/forgejo/actions.go index f520924c20..309b9801cb 100644 --- a/cmd/forgejo/actions.go +++ b/cmd/forgejo/actions.go @@ -102,6 +102,11 @@ func SubcmdActionsRegister(ctx context.Context) *cli.Command { Value: "", Usage: "version of the runner (not required since v1.21)", }, + &cli.BoolFlag{ + Name: "ephemeral", + Value: false, + Usage: "instruct Forgejo to permanently unregister this runner after it has run one job", + }, }, } } @@ -172,6 +177,7 @@ func RunRegister(ctx context.Context, cli *cli.Command) error { scope := cli.String("scope") name := cli.String("name") version := cli.String("version") + ephemeral := cli.Bool("ephemeral") labels, err := getLabels(cli) if err != nil { return err @@ -199,7 +205,7 @@ func RunRegister(ctx context.Context, cli *cli.Command) error { return err } - runner, err := actions_model.RegisterRunner(ctx, owner, repo, secret, labels, name, version) + runner, err := actions_model.RegisterRunner(ctx, owner, repo, secret, labels, name, version, ephemeral) if err != nil { return fmt.Errorf("error while registering runner: %v", err) } diff --git a/cmd/forgejo/f3.go b/cmd/forgejo/f3.go index c4aafeac58..2d1b617e46 100644 --- a/cmd/forgejo/f3.go +++ b/cmd/forgejo/f3.go @@ -24,7 +24,6 @@ import ( ) func CmdF3(ctx context.Context) *cli.Command { - ctx = f3_logger.ContextSetLogger(ctx, util.NewF3Logger(nil, log.GetLogger(log.DEFAULT))) return &cli.Command{ Name: "f3", Usage: "F3", @@ -38,7 +37,9 @@ func SubcmdF3Mirror(ctx context.Context) *cli.Command { mirrorCmd := f3_cmd.CreateCmdMirror() mirrorCmd.Before = prepareWorkPathAndCustomConf(ctx) f3Action := mirrorCmd.Action - mirrorCmd.Action = func(ctx context.Context, cli *cli.Command) error { return runMirror(ctx, cli, f3Action) } + mirrorCmd.Action = func(ctx context.Context, cli *cli.Command) error { + return runMirror(ctx, cli, f3Action) + } return mirrorCmd } @@ -67,6 +68,8 @@ func runMirror(ctx context.Context, c *cli.Command, action cli.ActionFunc) error if err := models.Init(ctx); err != nil { return err } + + ctx = f3_logger.ContextSetLogger(ctx, util.NewF3Logger(nil, log.GetLogger(log.DEFAULT))) } err := action(ctx, c) diff --git a/cmd/forgejo/forgejo.go b/cmd/forgejo/forgejo.go index 171ef1a71d..7b59eaab15 100644 --- a/cmd/forgejo/forgejo.go +++ b/cmd/forgejo/forgejo.go @@ -126,7 +126,9 @@ func installSignals(ctx context.Context) (context.Context, context.CancelFunc) { ) select { case <-signalChannel: + break case <-ctx.Done(): + break } cancel() signal.Reset() diff --git a/cmd/hook.go b/cmd/hook.go index 82dcb30866..d3eb4dea89 100644 --- a/cmd/hook.go +++ b/cmd/hook.go @@ -568,7 +568,7 @@ Forgejo or set your environment appropriately.`, "") hookOptions.RefFullNames = make([]git.RefName, 0, hookBatchSize) for { - // note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed + // note: pktLineTypeUnknown means pktLineTypeFlush and pktLineTypeData all allowed rs, err = readPktLine(ctx, reader, pktLineTypeUnknown) if err != nil { return err diff --git a/cmd/mailer.go b/cmd/mailer.go index d05d6c849b..2576e60b58 100644 --- a/cmd/mailer.go +++ b/cmd/mailer.go @@ -24,10 +24,10 @@ func runSendMail(ctx context.Context, c *cli.Command) error { } subject := c.String("title") - confirmSkiped := c.Bool("force") + confirmSkipped := c.Bool("force") body := c.String("content") - if !confirmSkiped { + if !confirmSkipped { if len(body) == 0 { fmt.Print("warning: Content is empty") } diff --git a/cmd/main.go b/cmd/main.go index 65cde47884..005dd763cf 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -80,7 +80,6 @@ func appGlobalFlags() []cli.Flag { func prepareSubcommandWithConfig(command *cli.Command, globalFlags func() []cli.Flag) { command.Flags = append(globalFlags(), command.Flags...) command.Action = prepareWorkPathAndCustomConf(command.Action) - command.HideHelp = true if command.Name != "help" { command.Commands = append(command.Commands, cmdHelp()) } @@ -199,7 +198,6 @@ func innerNewMainApp(version, versionExtra string, subCmdsStandaloneArgs, subCmd } app.Flags = append(app.Flags, cli.VersionFlag) app.Flags = append(app.Flags, globalFlags()...) - app.HideHelp = true // use our own help action to show helps (with more information like default config) app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { prepareSubcommandWithConfig(subCmdWithConfig[i], globalFlags) diff --git a/cmd/main_test.go b/cmd/main_test.go index 737150c62f..ca6a56f4af 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "os" "path/filepath" "strings" "testing" @@ -62,7 +63,12 @@ func runTestApp(app *cli.Command, args ...string) (runResult, error) { } func TestCliCmd(t *testing.T) { - defaultWorkPath := filepath.Dir(setting.AppPath) + path, err := os.Executable() + if err != nil { + panic(err) + } + + defaultWorkPath := filepath.Dir(path) defaultCustomPath := filepath.Join(defaultWorkPath, "custom") defaultCustomConf := filepath.Join(defaultCustomPath, "conf/app.ini") @@ -108,6 +114,11 @@ func TestCliCmd(t *testing.T) { cmd: "./gitea test-cmd --config /tmp/app-other.ini", exp: makePathOutput("/tmp", "/tmp/custom", "/tmp/app-other.ini"), }, + { + env: map[string]string{"GITEA_WORK_DIR": "/tmp"}, + cmd: "./gitea forgejo-cli --help", + exp: "(subcommand help template)", + }, } for _, c := range cases { diff --git a/cmd/serv.go b/cmd/serv.go index 0e0551d297..3a92b0e5fb 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -290,10 +290,9 @@ func runServ(ctx context.Context, c *cli.Command) error { Op: lfsVerb, UserID: results.UserID, } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Sign and get the complete encoded token as a string using the secret - tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes) + tokenString, err := setting.LFS.SigningKey.JWT(claims) if err != nil { return fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err) } diff --git a/cmd/web.go b/cmd/web.go index 12a8cac797..4a2a1de1fe 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -164,8 +164,6 @@ func serveInstall(_ context.Context, ctx *cli.Command) error { } func serveInstalled(_ context.Context, ctx *cli.Command) error { - setting.InitCfgProvider(setting.CustomConf) - setting.LoadCommonSettings() setting.MustInstalled() showWebStartupMessage("Prepare to run web server") @@ -278,8 +276,11 @@ func setPort(port string) error { switch setting.Protocol { case setting.HTTPUnix: + break case setting.FCGI: + break case setting.FCGIUnix: + break default: defaultLocalURL := string(setting.Protocol) + "://" if setting.HTTPAddr == "0.0.0.0" { diff --git a/contrib/systemd/forgejo.service b/contrib/systemd/forgejo.service index ee019e11ea..36dc2a3a5b 100644 --- a/contrib/systemd/forgejo.service +++ b/contrib/systemd/forgejo.service @@ -52,7 +52,7 @@ After=network.target # Uncomment the next line if you have repos with lots of files and get a HTTP 500 error because of that # LimitNOFILE=524288:524288 RestartSec=2s -Type=simple +Type=notify User=git Group=git WorkingDirectory=/var/lib/forgejo/ diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index f45b7731c5..40dd7d56db 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -313,6 +313,9 @@ RUN_USER = ; git ;LFS_START_SERVER = false ;; ;; +;; see JWT_* under [oauth2] +;LFS_JWT_SIGNING_ALGORITHM = HS256 +;LFS_JWT_SIGNING_PRIVATE_KEY_FILE = jwt/lfs_private.pem ;; LFS authentication secret, change this yourself ;LFS_JWT_SECRET = ;; @@ -417,8 +420,8 @@ DB_TYPE = sqlite3 ;; Database connection max idle time, 0 prevents closing due to idle time. ;CONN_MAX_IDLETIME = 0 ;; -;; Database maximum number of open connections, default is 100 which is the lowest default from Postgres (MariaDB + MySQL default to 151). Ensure you only increase the value if you configured your database server accordingly. -;MAX_OPEN_CONNS = 100 +;; Database maximum number of open connections. Ensure you only increase the value if your database server is configured to handle the amount of open connections accordingly. +;MAX_OPEN_CONNS = 30 ;; ;; Whether execute database models migrations automatically ;AUTO_MIGRATION = true @@ -457,7 +460,7 @@ INTERNAL_TOKEN = ;GLOBAL_TWO_FACTOR_REQUIREMENT = none ;; ;; Name of cookie used to store authentication information. -;COOKIE_REMEMBER_NAME = gitea_incredible +;COOKIE_REMEMBER_NAME = persistent ;; ;; Reverse proxy authentication header name of user name, email, and full name ;REVERSE_PROXY_AUTHENTICATION_USER = X-WEBAUTH-USER @@ -544,6 +547,7 @@ ENABLED = true ;; Private key file path used to sign OAuth2 tokens. The path is relative to APP_DATA_PATH. ;; This setting is only needed if JWT_SIGNING_ALGORITHM is set to RS256, RS384, RS512, ES256, ES384 or ES512. ;; The file must contain a RSA or ECDSA private key in the PKCS8 format. If no key exists a 4096 bit key will be created for you. +;; XXX jwt/ is a misnomer, it should rather be oauth2/, because we use many JWTs ;JWT_SIGNING_PRIVATE_KEY_FILE = jwt/private.pem ;; ;; OAuth2 authentication secret for access and refresh tokens, change this yourself to a unique string. CLI generate option is helpful in this case. https://forgejo.org/docs/latest/admin/command-line/#generate-secret @@ -1062,7 +1066,7 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; List of file extensions for which lines should be wrapped in the Monaco editor +;; List of file extensions for which lines should be wrapped in the CodeMirror editor ;; Separate extensions with a comma. To line wrap files without an extension, just put a comma ;LINE_WRAP_EXTENSIONS = .txt,.md,.markdown,.mdown,.mkd,.livemd, @@ -1132,9 +1136,6 @@ LEVEL = Info ;; If an squash commit's comment should be populated with the commit messages of the squashed commits ;POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES = false ;; -;; Add co-authored-by and co-committed-by trailers if committer does not match author -;ADD_CO_COMMITTER_TRAILERS = true -;; ;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo. ;RETARGET_CHILDREN_ON_MERGE = true @@ -1561,15 +1562,17 @@ LEVEL = Info ;DEFAULT_EMAIL_NOTIFICATIONS = enabled ;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false ;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false -;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys" more features can be disabled in future +;; Disabled features for users, could be "deletion", "manage_ssh_keys","manage_gpg_keys", "manage_password" more features can be disabled in future ;; - deletion: a user cannot delete their own account ;; - manage_ssh_keys: a user cannot configure ssh keys ;; - manage_gpg_keys: a user cannot configure gpg keys +;; - manage_password: a user cannot configure their password ;USER_DISABLED_FEATURES = -;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior. +;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be `deletion`, `manage_ssh_keys`, `manage_gpg_keys`, `manage_password`. This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior. ;; - deletion: a user cannot delete their own account ;; - manage_ssh_keys: a user cannot configure ssh keys ;; - manage_gpg_keys: a user cannot configure gpg keys +;; - manage_password: a user cannot configure their password ;;EXTERNAL_USER_DISABLE_FEATURES = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1896,7 +1899,7 @@ LEVEL = Info ;PROVIDER_CONFIG = data/sessions ; Relative paths will be made absolute against _`AppWorkPath`_. ;; ;; Session cookie name -;COOKIE_NAME = i_like_gitea +;COOKIE_NAME = session ;; ;; 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 = @@ -2456,6 +2459,15 @@ LEVEL = Info ;; Enable/Disable RSS/Atom feed ;ENABLE_FEED = true +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;[pwa] +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Enable standalone mode; this allows PWA enabled browsers to "install" the website as an progressive web app, +;; by setting the https://developer.mozilla.org/en-US/docs/Web/Manifest/display property to "standalone". +;STANDALONE = false + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;[markup] @@ -2543,7 +2555,7 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;[F3] +;[f3] ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; @@ -2776,12 +2788,21 @@ LEVEL = Info ;ABANDONED_JOB_TIMEOUT = 24h ;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow ;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip] -;; Limit on inputs for manual / workflow_dispatch triggers, default is 10 -;LIMIT_DISPATCH_INPUTS = 10 +;; Limit on inputs for manual / workflow_dispatch triggers, default is 100 +;LIMIT_DISPATCH_INPUTS = 100 ;; Support queuing workflow jobs, by setting `concurrency.group` & `concurrency.cancel-in-progress: false`, can increase ;; server and database workload due to more complex database queries and more frequent server task querying; this ;; feature can be disabled to reduce performance impact ;CONCURRENCY_GROUP_QUEUE_ENABLED = true +;; Algorithm used to sign ID tokens. Valid values: RS256, RS384, RS512, ES256, ES384, ES512, EdDSA. +;; RS256 will ensure compatibility with all relying parties. +;; If a different algorithm is chosen, verify that relying parties of interest support the signing algorithm. +;ID_TOKEN_SIGNING_ALGORITHM = RS256 +;; Private key file path used to sign ID tokens. The path is relative to APP_DATA_PATH. +;; The file must contain an RSA or ECDSA private key in the PKCS8 format. If no key exists, a key will be created for you. +;ID_TOKEN_SIGNING_PRIVATE_KEY_FILE = actions_id_token/private.pem +;; Lifetime of ID tokens generated by the actions `/idtoken` endpoint in seconds. +;ID_TOKEN_EXPIRATION_TIME = 3600 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -2802,3 +2823,30 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; storage type ;STORAGE_TYPE = local + +;; Authorized integrations are a capability for users to define external systems which can generate JWTs that Forgejo +;; will trust in order to perform API access on behalf of that user. While validating a JWT from an external system, +;; Forgejo makes outgoing HTTP requests to the JWT issuer. +; [authorized_integration] +;; Timeout for HTTP requests to remote servers. Default is 10 seconds. +;REQUEST_TIMEOUT = 10s +; +;; Allowed domains for authorized integrations. Default is blank which means all domains will be allowed (except local +;; networks, see ALLOW_LOCALNETWORKS). +;; Multiple domains can be separated by commas. +;; Wildcards are supported: "github.com, *.github.com" +;ALLOWED_DOMAINS = +; +;; Blocklist for authorized integrations, default is blank. +;; Multiple domains can be separated by commas. +;; Wildcards are supported: "github.com, *.github.com" +;BLOCKED_DOMAINS = +; +;; Allow private addresses defined by RFC 1918, RFC 1122, RFC 4632 and RFC 4291. +;; Default is false. +;; If a domain is allowed by ALLOWED_DOMAINS, this option will be ignored. +;ALLOW_LOCALNETWORKS = false +; +;; Remote requests are cached after being received for the cache time-to-live (TTL). Default is 10 minutes. +;; Caching uses the configured adapter in the [cache] config section. +;CACHE_TTL = 10m diff --git a/docker/rootless/usr/local/bin/docker-entrypoint.sh b/docker/rootless/usr/local/bin/docker-entrypoint.sh index e5fa41cc78..ca509214bf 100755 --- a/docker/rootless/usr/local/bin/docker-entrypoint.sh +++ b/docker/rootless/usr/local/bin/docker-entrypoint.sh @@ -13,10 +13,5 @@ fi if [ $# -gt 0 ]; then exec "$@" else - # TODO: remove on next major version release - # Honour legacy config file if existing - if [ -f ${GITEA_APP_INI_LEGACY} ]; then - GITEA_APP_INI=${GITEA_APP_INI_LEGACY} - fi exec /usr/local/bin/gitea -c ${GITEA_APP_INI} web fi diff --git a/docker/rootless/usr/local/bin/docker-setup.sh b/docker/rootless/usr/local/bin/docker-setup.sh index 09bbeabc63..7bcb78c88c 100755 --- a/docker/rootless/usr/local/bin/docker-setup.sh +++ b/docker/rootless/usr/local/bin/docker-setup.sh @@ -11,22 +11,10 @@ mkdir -p ${GITEA_CUSTOM} && chmod 0700 ${GITEA_CUSTOM} mkdir -p ${GITEA_TEMP} && chmod 0700 ${GITEA_TEMP} if [ ! -w ${GITEA_TEMP} ]; then echo "${GITEA_TEMP} is not writable"; exit 1; fi -# TODO: remove on next major version release -# Honour legacy config file if existing, but inform the user -if [ -f ${GITEA_APP_INI_LEGACY} ] && [ ${GITEA_APP_INI} != ${GITEA_APP_INI_LEGACY} ]; then - GITEA_APP_INI_DEFAULT=/var/lib/gitea/custom/conf/app.ini - echo -e \ - "\033[33mWARNING\033[0m: detected configuration file in deprecated default path ${GITEA_APP_INI_LEGACY}." \ - "The new default is ${GITEA_APP_INI_DEFAULT}. To remove this warning, choose one of the options:\n" \ - "* Move ${GITEA_APP_INI_LEGACY} to ${GITEA_APP_INI_DEFAULT} (or to \$GITEA_APP_INI if you want to override this variable)\n" \ - "* Explicitly override GITEA_APP_INI=${GITEA_APP_INI_LEGACY} in the container environment" - GITEA_APP_INI=${GITEA_APP_INI_LEGACY} -fi - -#Prepare config file +# Prepare config file if [ ! -f ${GITEA_APP_INI} ]; then - #Prepare config file folder + # Prepare config file folder GITEA_APP_INI_DIR=$(dirname ${GITEA_APP_INI}) mkdir -p ${GITEA_APP_INI_DIR} && chmod 0700 ${GITEA_APP_INI_DIR} if [ ! -w ${GITEA_APP_INI_DIR} ]; then echo "${GITEA_APP_INI_DIR} is not writable"; exit 1; fi diff --git a/docker/rootless/usr/local/bin/gitea b/docker/rootless/usr/local/bin/gitea index 9a9a569b12..d87ec84e86 100644 --- a/docker/rootless/usr/local/bin/gitea +++ b/docker/rootless/usr/local/bin/gitea @@ -9,7 +9,7 @@ # And place the original in /usr/lib/gitea with working files in /data/gitea GITEA="/app/gitea/gitea" WORK_DIR="/var/lib/gitea" -APP_INI="/etc/gitea/app.ini" +APP_INI="/var/lib/gitea/custom/conf/app.ini" APP_INI_SET="" for i in "$@"; do diff --git a/eslint.config.mjs b/eslint.config.mjs index df3425d7a0..03ca13176a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1145,6 +1145,7 @@ export default tseslint.config( 'playwright/prefer-comparison-matcher': [2], 'playwright/prefer-equality-matcher': [2], + 'playwright/prefer-locator': [0], 'playwright/prefer-native-locators': [2], 'playwright/prefer-to-contain': [2], 'playwright/prefer-to-have-length': [2], diff --git a/flake.lock b/flake.lock index 4ff76cf2ca..c6670e17b9 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1762977756, - "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", + "lastModified": 1777954456, + "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", + "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", "type": "github" }, "original": { diff --git a/go.mod b/go.mod index 17bf773003..bffbfa66cb 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,37 @@ module forgejo.org -go 1.25.0 +go 1.26.0 -toolchain go1.25.9 +toolchain go1.26.3 require ( - code.forgejo.org/f3/gof3/v3 v3.11.1 - code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251 - code.forgejo.org/forgejo/actions-proto v0.6.0 + code.forgejo.org/f3/gof3/v3 v3.11.15 + code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20260301104140-add494e31dab + code.forgejo.org/forgejo/actions-proto v0.7.0 code.forgejo.org/forgejo/go-rpmutils v1.0.0 code.forgejo.org/forgejo/levelqueue v1.0.0 code.forgejo.org/forgejo/reply v1.0.2 - code.forgejo.org/forgejo/runner/v12 v12.6.4 + code.forgejo.org/forgejo/runner/v12 v12.10.1 code.forgejo.org/go-chi/binding v1.0.1 code.forgejo.org/go-chi/cache v1.0.1 code.forgejo.org/go-chi/captcha v1.0.2 - code.forgejo.org/go-chi/session v1.0.2 + code.forgejo.org/go-chi/session v1.0.4 code.gitea.io/sdk/gitea v0.21.0 - code.superseriousbusiness.org/exif-terminator v0.11.1 + code.superseriousbusiness.org/exif-terminator v0.11.2 code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 - connectrpc.com/connect v1.19.1 + connectrpc.com/connect v1.19.2 github.com/42wim/httpsig v1.2.3 github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 - github.com/ProtonMail/go-crypto v1.3.0 - github.com/PuerkitoBio/goquery v1.10.3 - github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2 - github.com/alecthomas/chroma/v2 v2.20.0 + github.com/ProtonMail/go-crypto v1.4.1 + github.com/PuerkitoBio/goquery v1.12.0 + github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 + github.com/alecthomas/chroma/v2 v2.23.1 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb - github.com/blevesearch/bleve/v2 v2.5.6 + github.com/blevesearch/bleve/v2 v2.6.0 github.com/buildkite/terminal-to-html/v3 v3.16.8 - github.com/caddyserver/certmagic v0.24.0 + github.com/caddyserver/certmagic v0.25.3 github.com/chi-middleware/proxy v1.1.1 github.com/djherbis/buffer v1.2.0 github.com/djherbis/nio/v3 v3.0.1 @@ -41,78 +41,77 @@ require ( github.com/editorconfig/editorconfig-core-go/v2 v2.6.4 github.com/emersion/go-imap v1.2.1 github.com/felixge/fgprof v0.9.5 - github.com/fsnotify/fsnotify v1.9.0 + github.com/fsnotify/fsnotify v1.10.1 + github.com/gdgvda/cron v0.7.0 github.com/gliderlabs/ssh v0.3.8 - github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 + github.com/go-ap/activitypub v0.0.0-20260208110334-902f6cf8c2cc github.com/go-ap/jsonld v0.0.0-20251216162253-e38fa664ea77 - github.com/go-chi/chi/v5 v5.2.4 + github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/cors v1.2.2 github.com/go-co-op/gocron v1.37.0 - github.com/go-enry/go-enry/v2 v2.9.2 + github.com/go-enry/go-enry/v2 v2.9.6 github.com/go-ldap/ldap/v3 v3.4.12 - github.com/go-openapi/spec v0.22.1 - github.com/go-sql-driver/mysql v1.9.3 - github.com/go-webauthn/webauthn v0.14.0 + github.com/go-openapi/spec v0.22.3 + github.com/go-sql-driver/mysql v1.10.0 + github.com/go-webauthn/webauthn v0.16.5 github.com/gobwas/glob v0.2.3 github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 - github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/google/go-github/v81 v81.0.0 - github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 + github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 github.com/gorilla/sessions v1.4.0 - github.com/hashicorp/go-version v1.7.0 + github.com/hashicorp/go-version v1.8.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.5.0 - github.com/inbucket/html2text v0.9.0 - github.com/jackc/pgx/v5 v5.7.6 + github.com/inbucket/html2text v1.0.0 + github.com/jackc/pgx/v5 v5.9.2 github.com/jhillyerd/enmime/v2 v2.2.0 github.com/json-iterator/go v1.1.12 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/klauspost/compress v1.18.3 - github.com/klauspost/cpuid/v2 v2.2.11 - github.com/markbates/goth v1.80.0 - github.com/mattn/go-isatty v0.0.20 - github.com/mattn/go-sqlite3 v1.14.34 - github.com/meilisearch/meilisearch-go v0.34.0 + github.com/klauspost/compress v1.18.6 + github.com/klauspost/cpuid/v2 v2.3.0 + github.com/markbates/goth v1.82.0 + github.com/mattn/go-isatty v0.0.21 + github.com/mattn/go-sqlite3 v1.14.44 + github.com/meilisearch/meilisearch-go v0.36.2 github.com/mholt/archives v0.1.5 github.com/microcosm-cc/bluemonday v1.0.27 - github.com/minio/minio-go/v7 v7.0.95 + github.com/minio/minio-go/v7 v7.1.0 github.com/msteinert/pam/v2 v2.1.0 github.com/niklasfasching/go-org v1.9.1 github.com/olivere/elastic/v7 v7.0.32 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 - github.com/pquerna/otp v1.4.0 + github.com/pquerna/otp v1.5.0 github.com/prometheus/client_golang v1.21.1 - github.com/redis/go-redis/v9 v9.17.2 - github.com/robfig/cron/v3 v3.0.1 + github.com/redis/go-redis/v9 v9.19.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/sergi/go-diff v1.4.0 github.com/stretchr/testify v1.11.1 github.com/syndtr/goleveldb v1.0.0 github.com/ulikunitz/xz v0.5.15 - github.com/urfave/cli/v3 v3.5.0 - github.com/valyala/fastjson v1.6.7 + github.com/urfave/cli/v3 v3.8.0 + github.com/valyala/fastjson v1.6.10 github.com/yohcop/openid-go v1.0.1 - github.com/yuin/goldmark v1.7.13 + github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc gitlab.com/gitlab-org/api/client-go v0.143.2 - go.uber.org/mock v0.6.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.48.0 - golang.org/x/image v0.38.0 - golang.org/x/net v0.51.0 - golang.org/x/oauth2 v0.34.0 + golang.org/x/crypto v0.51.0 + golang.org/x/image v0.40.0 + golang.org/x/net v0.54.0 + golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.20.0 - golang.org/x/sys v0.41.0 - golang.org/x/text v0.35.0 + golang.org/x/sys v0.44.0 + golang.org/x/text v0.37.0 google.golang.org/protobuf v1.36.11 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 - mvdan.cc/xurls/v2 v2.5.0 + mvdan.cc/xurls/v2 v2.6.0 xorm.io/builder v0.3.13 xorm.io/xorm v1.3.9 ) @@ -120,46 +119,46 @@ require ( require ( cloud.google.com/go/compute/metadata v0.6.0 // indirect code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0 // indirect - filippo.io/edwards25519 v1.1.1 // indirect + filippo.io/edwards25519 v1.2.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect - github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect + github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect github.com/STARRY-S/zip v0.2.3 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.22.0 // indirect - github.com/blevesearch/bleve_index_api v1.2.11 // indirect - github.com/blevesearch/geo v0.2.4 // indirect - github.com/blevesearch/go-faiss v1.0.26 // indirect + github.com/bits-and-blooms/bitset v1.24.2 // indirect + github.com/blevesearch/bleve_index_api v1.3.11 // indirect + github.com/blevesearch/geo v0.2.5 // indirect + github.com/blevesearch/go-faiss v1.1.0 // indirect github.com/blevesearch/go-porterstemmer v1.0.3 // indirect github.com/blevesearch/gtreap v0.1.1 // indirect - github.com/blevesearch/mmap-go v1.0.4 // indirect - github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect + github.com/blevesearch/mmap-go v1.2.0 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.4.7 // indirect github.com/blevesearch/segment v0.9.1 // indirect github.com/blevesearch/snowballstem v0.9.0 // indirect github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect - github.com/blevesearch/vellum v1.1.0 // indirect - github.com/blevesearch/zapx/v11 v11.4.2 // indirect - github.com/blevesearch/zapx/v12 v12.4.2 // indirect - github.com/blevesearch/zapx/v13 v13.4.2 // indirect - github.com/blevesearch/zapx/v14 v14.4.2 // indirect - github.com/blevesearch/zapx/v15 v15.4.2 // indirect - github.com/blevesearch/zapx/v16 v16.2.7 // indirect + github.com/blevesearch/vellum v1.2.0 // indirect + github.com/blevesearch/zapx/v11 v11.4.3 // indirect + github.com/blevesearch/zapx/v12 v12.4.3 // indirect + github.com/blevesearch/zapx/v13 v13.4.3 // indirect + github.com/blevesearch/zapx/v14 v14.4.3 // indirect + github.com/blevesearch/zapx/v15 v15.4.3 // indirect + github.com/blevesearch/zapx/v16 v16.3.4 // indirect + github.com/blevesearch/zapx/v17 v17.1.2 // indirect github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/boombuler/barcode v1.0.1 // indirect github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf // indirect - github.com/caddyserver/zerossl v0.1.3 // indirect + github.com/caddyserver/zerossl v0.1.5 // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidmz/go-pageant v1.0.2 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb // indirect github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect @@ -167,35 +166,32 @@ require ( github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect + github.com/fxamacker/cbor/v2 v2.9.1 // indirect + github.com/go-ap/errors v0.0.0-20260208110149-e1b309365966 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-fed/httpsig v1.1.0 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.8.0 // indirect - github.com/go-git/go-git/v5 v5.17.1 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-openapi/jsonpointer v0.22.3 // indirect - github.com/go-openapi/jsonreference v0.21.3 // indirect - github.com/go-openapi/swag/conv v0.25.3 // indirect - github.com/go-openapi/swag/jsonname v0.25.3 // indirect - github.com/go-openapi/swag/jsonutils v0.25.3 // indirect - github.com/go-openapi/swag/loading v0.25.3 // indirect - github.com/go-openapi/swag/stringutils v0.25.3 // indirect - github.com/go-openapi/swag/typeutils v0.25.3 // indirect - github.com/go-openapi/swag/yamlutils v0.25.3 // indirect - github.com/go-webauthn/x v0.1.25 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/go-webauthn/x v0.2.3 // indirect github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.1.2 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/go-tpm v0.9.5 // indirect + github.com/google/go-tpm v0.9.8 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect @@ -204,23 +200,21 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect - github.com/lib/pq v1.11.2 // indirect - github.com/libdns/libdns v1.0.0 // indirect + github.com/libdns/libdns v1.1.1 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/markbates/going v1.0.3 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-runewidth v0.0.17 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect - github.com/mholt/acmez/v3 v3.1.2 // indirect - github.com/miekg/dns v1.1.63 // indirect + github.com/mholt/acmez/v3 v3.1.6 // indirect + github.com/miekg/dns v1.1.72 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect - github.com/minio/crc64nvme v1.0.2 // indirect + github.com/minio/crc64nvme v1.1.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/minlz v1.0.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect @@ -231,6 +225,7 @@ require ( github.com/olekukonko/ll v0.0.9 // indirect github.com/olekukonko/tablewriter v1.0.7 // indirect github.com/onsi/ginkgo v1.16.5 // indirect + github.com/onsi/gomega v1.34.1 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -240,27 +235,28 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/rhysd/actionlint v1.7.10 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rs/xid v1.6.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect - github.com/tinylib/msgp v1.3.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/tinylib/msgp v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/zeebo/assert v1.3.0 // indirect github.com/zeebo/blake3 v0.2.4 // indirect + github.com/zeebo/xxh3 v1.1.0 // indirect go.etcd.io/bbolt v1.4.3 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/zap v1.27.1 // indirect go.uber.org/zap/exp v0.3.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.42.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.44.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -273,4 +269,4 @@ replace github.com/gliderlabs/ssh => code.forgejo.org/forgejo/ssh v0.0.0-2024121 replace git.sr.ht/~mariusor/go-xsd-duration => code.forgejo.org/forgejo/go-xsd-duration v0.0.0-20220703122237-02e73435a078 -replace xorm.io/xorm v1.3.9 => code.forgejo.org/xorm/xorm v1.3.9-forgejo.8 +replace xorm.io/xorm v1.3.9 => code.forgejo.org/xorm/xorm v1.3.9-forgejo.12 diff --git a/go.sum b/go.sum index 0a15ddef1c..8595a4a6b5 100644 --- a/go.sum +++ b/go.sum @@ -16,12 +16,12 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -code.forgejo.org/f3/gof3/v3 v3.11.1 h1:c0vE8XvqpbXuSv8gzttn96k5T2FQi0u9bYnux46qSAs= -code.forgejo.org/f3/gof3/v3 v3.11.1/go.mod h1:1p2UKrqZiwxKneQF2DKrMnc403YIgR/lfcfvadZtmDs= -code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251 h1:HTZl3CBk3ABNYtFI6TPLvJgGKFIhKT5CBk0sbOtkDKU= -code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:PphB88CPbx601QrWPMZATeorACeVmQlyv3u+uUMbSaM= -code.forgejo.org/forgejo/actions-proto v0.6.0 h1:dw1Dogk9A4V/yrLVkhe9dSZPsqNAIkI1kCXPSqG3tZA= -code.forgejo.org/forgejo/actions-proto v0.6.0/go.mod h1:+444hHBs9/qDh5X/AedaTv0Egj3vd/EXP93vg9zFV2E= +code.forgejo.org/f3/gof3/v3 v3.11.15 h1:/EzJWRQUhVVia1EmCWRZHCuW8qdAX1DEXY0M1n0mECc= +code.forgejo.org/f3/gof3/v3 v3.11.15/go.mod h1:k99wl7QiI9LjS3qEd1RXLdcZPE3wZP5gkVhy8/WhzJ4= +code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20260301104140-add494e31dab h1:g/uf/tNOvCdGL9EuYV9EJQglEH8n572P1ZwH4lBBkjI= +code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20260301104140-add494e31dab/go.mod h1:PphB88CPbx601QrWPMZATeorACeVmQlyv3u+uUMbSaM= +code.forgejo.org/forgejo/actions-proto v0.7.0 h1:0UA8AIOIWEiMLQvBbnZ6GQ9reh6BbM15Ep8x+oeTCY8= +code.forgejo.org/forgejo/actions-proto v0.7.0/go.mod h1:+444hHBs9/qDh5X/AedaTv0Egj3vd/EXP93vg9zFV2E= code.forgejo.org/forgejo/go-rpmutils v1.0.0 h1:RZGGeKt70p/WaIEL97pyT6uiiEIoN8/aLmS5Z6WmX0M= code.forgejo.org/forgejo/go-rpmutils v1.0.0/go.mod h1:cg+VbgLXfrDPza9T+kBsMb3TVmmzPN4XseT6gDGLSUk= code.forgejo.org/forgejo/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:RArF5AsF9LH4nEoJxqRxcP5r8hhRfWcId84G82YbqzA= @@ -30,8 +30,8 @@ code.forgejo.org/forgejo/levelqueue v1.0.0 h1:9krYpU6BM+j/1Ntj6m+VCAIu0UNnne1/Uf code.forgejo.org/forgejo/levelqueue v1.0.0/go.mod h1:fmG6zhVuqim2rxSFOoasgXO8V2W/k9U31VVYqLIRLhQ= code.forgejo.org/forgejo/reply v1.0.2 h1:dMhQCHV6/O3L5CLWNTol+dNzDAuyCK88z4J/lCdgFuQ= code.forgejo.org/forgejo/reply v1.0.2/go.mod h1:RyZUfzQLc+fuLIGjTSQWDAJWPiL4WtKXB/FifT5fM7U= -code.forgejo.org/forgejo/runner/v12 v12.6.4 h1:nhYj2wSj5BVKxcF0bRtMt/A9iGkxHPFJiIui+T/4mrc= -code.forgejo.org/forgejo/runner/v12 v12.6.4/go.mod h1:34ATLtcxtOgjAJiINaJvBoNJiKpL1hGn0kt+gk+zdyk= +code.forgejo.org/forgejo/runner/v12 v12.10.1 h1:qRKjWItVDc2lEMl3jGlKyFpqgX4xHeOdEyl/irO/Nk8= +code.forgejo.org/forgejo/runner/v12 v12.10.1/go.mod h1:A51GyZJlril5cIpVMvOn3NqE8upOE6ePjwC6s31kRHk= code.forgejo.org/forgejo/ssh v0.0.0-20241211213324-5fc306ca0616 h1:kEZL84+02jY9RxXM4zHBWZ3Fml0B09cmP1LGkDsCfIA= code.forgejo.org/forgejo/ssh v0.0.0-20241211213324-5fc306ca0616/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= code.forgejo.org/go-chi/binding v1.0.1 h1:coKNI+X1NzRN7X85LlrpvBRqk0TXpJ+ja28vusQWEuY= @@ -40,25 +40,27 @@ code.forgejo.org/go-chi/cache v1.0.1 h1:w6IsDcPbeEnEYZn7M2HJe3/3/Ehtcw/72VjcVK7+ code.forgejo.org/go-chi/cache v1.0.1/go.mod h1:K3aQSyRIN4xiuqV1kanfQ6O4ToDpzDpY3bNOyGjFe3U= code.forgejo.org/go-chi/captcha v1.0.2 h1:vyHDPXkpjDv8bLO9NqtWzZayzstD/WpJ5xwEkAaqZGQ= code.forgejo.org/go-chi/captcha v1.0.2/go.mod h1:lxiPLcJ76UCZHoH31/Wbum4GUi2NgjfFZLrJkKv1lLE= -code.forgejo.org/go-chi/session v1.0.2 h1:pG+AXre9L9VXJmTaADXkmeEPuRalhmBXyv6tG2Rvjcc= -code.forgejo.org/go-chi/session v1.0.2/go.mod h1:HnEGyBny7WPzCiVLP2vzL5ssma+3gCSl/vLpuVNYrqc= -code.forgejo.org/xorm/xorm v1.3.9-forgejo.8 h1:dsSKm2nus0NhHsqYxeuB3Gldk6TtlusD1CBGV6V1SS0= -code.forgejo.org/xorm/xorm v1.3.9-forgejo.8/go.mod h1:A7sFd3BFmRp20h6drSsCXgQRQdF8Vz8HuCSrzFS3m90= +code.forgejo.org/go-chi/session v1.0.4 h1:WQ1NaVxcCpxYwCliEGypKclZnOCjh3p1fk8XciJc62U= +code.forgejo.org/go-chi/session v1.0.4/go.mod h1:+sSTiomM5C8AUPtxZyTENIbcTz22kcVottKO0lnmDRk= +code.forgejo.org/xorm/xorm v1.3.9-forgejo.12 h1:EodlU2MGu/EeAdUpEOe+X4cU/qlnJol1ucJ0sHUCM1o= +code.forgejo.org/xorm/xorm v1.3.9-forgejo.12/go.mod h1:ozQINrM8b7uYFMBB/w19nUTTLcda3RKTQ8HspocVKdg= code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= -code.superseriousbusiness.org/exif-terminator v0.11.1 h1:qnujLH4/Yk/CFtFMmtjozbdV6Ry5G3Q/E/mLlWm/gQI= -code.superseriousbusiness.org/exif-terminator v0.11.1/go.mod h1:/Z+3DHSrefCzzN5ePkGjVYKFErRimoeUf694Gz8Pn/Y= +code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= +code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= +code.superseriousbusiness.org/exif-terminator v0.11.2 h1:nkTdaghZb6I0oGYFhTLSALhA2ShgkPnYAJryE5IE9+0= +code.superseriousbusiness.org/exif-terminator v0.11.2/go.mod h1:/Z+3DHSrefCzzN5ePkGjVYKFErRimoeUf694Gz8Pn/Y= code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0 h1:r9uq8StaSHYKJ8DklR9Xy+E9c40G1Z8yj5TRGi8L6+4= code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0/go.mod h1:IK1OlR6APjVB3E9tuYGvf0qXMrwP+TrzcHS5rf4wffQ= code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0 h1:I512jiIeXDC4//2BeSPrRM2ZS4wpBKUaPeTPxakMNGA= code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0/go.mod h1:SNHomXNW88o1pFfLHpD4KsCZLfcr4z5dm+xcX5SV10A= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM= -connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= -connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= +connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= +connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= -filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= @@ -71,24 +73,24 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= -github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= -github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= -github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= +github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= +github.com/RoaringBitmap/roaring/v2 v2.14.5 h1:ckd0o545JqDPeVJDgeFoaM21eBixUnlWfYgjE5VnyWw= +github.com/RoaringBitmap/roaring/v2 v2.14.5/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4= github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= -github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2 h1:cSXom2MoKJ9KPPw29RoZtHvUETY4F4n/kXl8m9btnQ0= -github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.7.2/go.mod h1:JitQWJ8JuV4Y87l8VsHiiwhb3cgdyn68mX40s7NT6PA= +github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 h1:tgjwQrDH5m6jIYB7kac5IQZmfUzQNseac/e3H4VoCNE= +github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R1QWahSeMJkjSkq6CYAZu1aIbYSpfJ4o= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= -github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= +github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= +github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= -github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= -github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= @@ -101,47 +103,48 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= -github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.2 h1:M7/NzVbsytmtfHbumG+K2bremQPMJuqv1JD3vOaFxp0= +github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= -github.com/blevesearch/bleve/v2 v2.5.6 h1:YdixQmOUuZHojQRe8Te7BY2cRirbzpbcpybAFs0m2DI= -github.com/blevesearch/bleve/v2 v2.5.6/go.mod h1:t5WoESS5TDteTdnjhhvpA1BpLYErOBX2IQViTMLK7wo= -github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s= -github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0= -github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk= -github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8= -github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI= -github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/bleve/v2 v2.6.0 h1:Cyd3dd4q5tCbOV8MnKUVRUDYMHOir9xn12NZzXVSEd4= +github.com/blevesearch/bleve/v2 v2.6.0/go.mod h1:gLmI8lWgHgrIYf7UpUX7JISI1CaqC6VScu46mHThuAY= +github.com/blevesearch/bleve_index_api v1.3.11 h1:x29vbV8OjWfLcrDVd7Lr1q+BkLNS0JWNEig0MCVnKH4= +github.com/blevesearch/bleve_index_api v1.3.11/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko= +github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE= +github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4= +github.com/blevesearch/go-faiss v1.1.0 h1:xM7Jc0ZUCv5lssG9Ohj3Jv0SdTpxcUABU1dDt9XVsc4= +github.com/blevesearch/go-faiss v1.1.0/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo= github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M= github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y= github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk= -github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc= -github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs= -github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY= -github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc= +github.com/blevesearch/mmap-go v1.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI= +github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0= +github.com/blevesearch/scorch_segment_api/v2 v2.4.7 h1:GlMzW08hcsM3DnLUxhyF/1PcDal1qtvvIuytuph5djw= +github.com/blevesearch/scorch_segment_api/v2 v2.4.7/go.mod h1://IJ7tG3QCf0cWW/aVSXqy77tc1AvLu3fcJLYEvOAFs= github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU= github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw= github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s= github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs= github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A= github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= -github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w= -github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y= -github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs= -github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= -github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE= -github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= -github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks= -github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= -github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0= -github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= -github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k= -github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= -github.com/blevesearch/zapx/v16 v16.2.7 h1:xcgFRa7f/tQXOwApVq7JWgPYSlzyUMmkuYa54tMDuR0= -github.com/blevesearch/zapx/v16 v16.2.7/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14= +github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0= +github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k= +github.com/blevesearch/zapx/v11 v11.4.3 h1:PTZOO5loKpHC/x/GzmPZNa9cw7GZIQxd5qRjwij9tHY= +github.com/blevesearch/zapx/v11 v11.4.3/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc= +github.com/blevesearch/zapx/v12 v12.4.3 h1:eElXvAaAX4m04t//CGBQAtHNPA+Q6A1hHZVrN3LSFYo= +github.com/blevesearch/zapx/v12 v12.4.3/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58= +github.com/blevesearch/zapx/v13 v13.4.3 h1:qsdhRhaSpVnqDFlRiH9vG5+KJ+dE7KAW9WyZz/KXAiE= +github.com/blevesearch/zapx/v13 v13.4.3/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk= +github.com/blevesearch/zapx/v14 v14.4.3 h1:GY4Hecx0C6UTmiNC2pKdeA2rOKiLR5/rwpU9WR51dgM= +github.com/blevesearch/zapx/v14 v14.4.3/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8= +github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2lc/0/CU= +github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw= +github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0= +github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg= +github.com/blevesearch/zapx/v17 v17.1.2 h1:avbOk2igaASNoiy0BE/jPgcxAnRI2PGeydeP4hg7Ikk= +github.com/blevesearch/zapx/v17 v17.1.2/go.mod h1:WQObxKrqUX7cd0G1GMvDfc/bmZzQvoy7APOPimx7DiI= github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= @@ -161,10 +164,10 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc= github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM= -github.com/caddyserver/certmagic v0.24.0 h1:EfXTWpxHAUKgDfOj6MHImJN8Jm4AMFfMT6ITuKhrDF0= -github.com/caddyserver/certmagic v0.24.0/go.mod h1:xPT7dC1DuHHnS2yuEQCEyks+b89sUkMENh8dJF+InLE= -github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= -github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= +github.com/caddyserver/certmagic v0.25.3 h1:mGf5ba8F7xA4c5jfDZZbK2buY1VEkbnwpMDixaju94A= +github.com/caddyserver/certmagic v0.25.3/go.mod h1:YVs43D5+H/Dckt4bTga1KSO/xYfFBfVZainGDywYPAA= +github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= +github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI= github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= @@ -191,8 +194,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o= github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ= github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= @@ -249,28 +250,29 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9 h1:j2TrkUG/NATGi/EQS+MvEoF79CxiRUmT16ErFroNcKI= -github.com/go-ap/activitypub v0.0.0-20231114162308-e219254dc5c9/go.mod h1:cJ9Ye0ZNSMN7RzZDBRY3E+8M3Bpf/R1JX22Ir9yX6WI= -github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 h1:I2nuhyVI/48VXoRCCZR2hYBgnSXa+EuDJf/VyX06TC0= -github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI= -github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= +github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= +github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gdgvda/cron v0.7.0 h1:LFPZUTbCb5ZpzYxavbQDDbjd6nwTwkiNUWyulOdlY2I= +github.com/gdgvda/cron v0.7.0/go.mod h1:caBF+mzTZGtQqFE05T1m6u9OmCASY3EK51XAICf3wio= +github.com/go-ap/activitypub v0.0.0-20260208110334-902f6cf8c2cc h1:yLe7YJhK+XNjNV4SqDxAjpWAgft+KU+XwKZS4AKEUV0= +github.com/go-ap/activitypub v0.0.0-20260208110334-902f6cf8c2cc/go.mod h1:jUs8eczo1EAT4ByRpZ4mQmNvjarw9eNf7Nm5udpMRhY= +github.com/go-ap/errors v0.0.0-20260208110149-e1b309365966 h1:tV+3kZgqFMKVUf+JPKBV400ISM8440+6y/SQCS0WZwQ= +github.com/go-ap/errors v0.0.0-20260208110149-e1b309365966/go.mod h1:zkp58Q5yXpCxZbh3d0GDvwqiYclfVuHEHjc9SZKAj6I= github.com/go-ap/jsonld v0.0.0-20251216162253-e38fa664ea77 h1:yHAmoR6avNy84PlLmjHt1z9flAp2Qs2ens5QDE/CNWk= github.com/go-ap/jsonld v0.0.0-20251216162253-e38fa664ea77/go.mod h1:4h93IBxgfnE/DEleMLgJ/XCeu/RtQ+MUh3ucANseeXA= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= -github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= -github.com/go-enry/go-enry/v2 v2.9.2 h1:giOQAtCgBX08kosrX818DCQJTCNtKwoPBGu0qb6nKTY= -github.com/go-enry/go-enry/v2 v2.9.2/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= +github.com/go-enry/go-enry/v2 v2.9.6 h1:np63eOtMV56zfYDHnFVgpEVOk8fr2kmylcMnAZUDbSs= +github.com/go-enry/go-enry/v2 v2.9.6/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -280,53 +282,51 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= -github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= -github.com/go-git/go-git/v5 v5.17.1 h1:WnljyxIzSj9BRRUlnmAU35ohDsjRK0EKmL0evDqi5Jk= -github.com/go-git/go-git/v5 v5.17.1/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= -github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8= -github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= -github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc= -github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4= -github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k= -github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA= -github.com/go-openapi/swag/conv v0.25.3 h1:PcB18wwfba7MN5BVlBIV+VxvUUeC2kEuCEyJ2/t2X7E= -github.com/go-openapi/swag/conv v0.25.3/go.mod h1:n4Ibfwhn8NJnPXNRhBO5Cqb9ez7alBR40JS4rbASUPU= -github.com/go-openapi/swag/jsonname v0.25.3 h1:U20VKDS74HiPaLV7UZkztpyVOw3JNVsit+w+gTXRj0A= -github.com/go-openapi/swag/jsonname v0.25.3/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/swag/jsonutils v0.25.3 h1:kV7wer79KXUM4Ea4tBdAVTU842Rg6tWstX3QbM4fGdw= -github.com/go-openapi/swag/jsonutils v0.25.3/go.mod h1:ILcKqe4HC1VEZmJx51cVuZQ6MF8QvdfXsQfiaCs0z9o= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.3 h1:/i3E9hBujtXfHy91rjtwJ7Fgv5TuDHgnSrYjhFxwxOw= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.3/go.mod h1:8kYfCR2rHyOj25HVvxL5Nm8wkfzggddgjZm6RgjT8Ao= -github.com/go-openapi/swag/loading v0.25.3 h1:Nn65Zlzf4854MY6Ft0JdNrtnHh2bdcS/tXckpSnOb2Y= -github.com/go-openapi/swag/loading v0.25.3/go.mod h1:xajJ5P4Ang+cwM5gKFrHBgkEDWfLcsAKepIuzTmOb/c= -github.com/go-openapi/swag/stringutils v0.25.3 h1:nAmWq1fUTWl/XiaEPwALjp/8BPZJun70iDHRNq/sH6w= -github.com/go-openapi/swag/stringutils v0.25.3/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= -github.com/go-openapi/swag/typeutils v0.25.3 h1:2w4mEEo7DQt3V4veWMZw0yTPQibiL3ri2fdDV4t2TQc= -github.com/go-openapi/swag/typeutils v0.25.3/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= -github.com/go-openapi/swag/yamlutils v0.25.3 h1:LKTJjCn/W1ZfMec0XDL4Vxh8kyAnv1orH5F2OREDUrg= -github.com/go-openapi/swag/yamlutils v0.25.3/go.mod h1:Y7QN6Wc5DOBXK14/xeo1cQlq0EA0wvLoSv13gDQoCao= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= +github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0= -github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k= -github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88= -github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-webauthn/webauthn v0.16.5 h1:x+vADHlaiIjta23kGhtwyCIlB5mayKx6SBlpwQ5NF9A= +github.com/go-webauthn/webauthn v0.16.5/go.mod h1:mQC6L0lZ5Kiu35G70zeB2WnrW4+vbHjR8Koq4HdVaMg= +github.com/go-webauthn/x v0.2.3 h1:8oArS+Rc1SWFLXhE17KZNx258Z4kUSyaDgsSncCO5RA= +github.com/go-webauthn/x v0.2.3/go.mod h1:tM04GF3V6VYq79AZMl7vbj4q6pz9r7L2criWRzbWhPk= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo= github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -340,10 +340,8 @@ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7w github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:UjoPNDAQ5JPCjlxoJd6K8ALZqSDDhk2ymieAZOVaDg0= github.com/gogs/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:fR6z1Ie6rtF7kl/vBYMfgD5/G5B1blui7z426/sj2DU= -github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= @@ -369,12 +367,12 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -388,8 +386,10 @@ github.com/google/go-github/v81 v81.0.0 h1:hTLugQRxSLD1Yei18fk4A5eYjOGLUBKAl/VCq github.com/google/go-github/v81 v81.0.0/go.mod h1:upyjaybucIbBIuxgJS7YLOZGziyvvJ92WX6WEBNE3sM= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/go-tpm v0.9.5 h1:ocUmnDebX54dnW+MQWGQRbdaAcJELsa6PqZhJ48KwVU= -github.com/google/go-tpm v0.9.5/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc= +github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -398,8 +398,8 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE= -github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -439,18 +439,16 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= -github.com/inbucket/html2text v0.9.0 h1:ULJmVcBEMAcmLE+/rN815KG1Fx6+a4HhbUxiDiN+qks= -github.com/inbucket/html2text v0.9.0/go.mod h1:QDaumzl+/OzlSVbNohhmg+yAy5pKjUjzCKW2BMvztKE= +github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs= +github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= -github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -477,12 +475,14 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= -github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= +github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -497,49 +497,49 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= -github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/libdns/libdns v1.0.0 h1:IvYaz07JNz6jUQ4h/fv2R4sVnRnm77J/aOuC9B+TQTA= -github.com/libdns/libdns v1.0.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= +github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU= +github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk= +github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U= +github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ= +github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U= +github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE= github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o= -github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8= -github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY= +github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ= +github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= -github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/meilisearch/meilisearch-go v0.34.0 h1:P+Ohdx4/PCxXaoI5wNi0LMwPkuiNrF/kGIzBrKYS4tw= -github.com/meilisearch/meilisearch-go v0.34.0/go.mod h1:cUVJZ2zMqTvvwIMEEAdsWH+zrHsrLpAw6gm8Lt1MXK0= -github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= -github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= +github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +github.com/meilisearch/meilisearch-go v0.36.2 h1:MYaMPCpdLh2aYPt+zK+19mLoA4dfBY3S1L7T0FADCjU= +github.com/meilisearch/meilisearch-go v0.36.2/go.mod h1:hWcR0MuWLSzHfbz9GGzIr3s9rnXLm1jqkmHkJPbUSvM= +github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk= +github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY= github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= -github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= -github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg= -github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= +github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI= +github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU= -github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo= +github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8= +github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA= github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -595,8 +595,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= -github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -606,8 +606,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= -github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/redis/go-redis/v9 v9.19.0 h1:XPVaaPSnG6RhYf7p+rmSa9zZfeVAnWsH5h3lxthOm/k= +github.com/redis/go-redis/v9 v9.19.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rhysd/actionlint v1.7.10 h1:FL3XIEs72G4/++168vlv5FKOWMSWvWIQw1kBCadyOcM= @@ -655,16 +655,15 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= -github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww= -github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ= +github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw= -github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= -github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= -github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI= +github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= @@ -674,8 +673,8 @@ github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBz github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= -github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -684,6 +683,8 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= gitlab.com/gitlab-org/api/client-go v0.143.2 h1:tfmUW8u+G/DGKOB/FDR0c06f0RVUAEe0ym8WpLoiHXI= gitlab.com/gitlab-org/api/client-go v0.143.2/go.mod h1:gJn5yLx9vYGXr73Yv0ueHWCVl+fL8iUOgJFxC7qV+iM= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= @@ -701,8 +702,8 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -722,8 +723,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -732,12 +733,12 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE= -golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY= +golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8= +golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -759,8 +760,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -791,15 +792,15 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -849,8 +850,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -860,8 +861,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -875,12 +876,12 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -910,8 +911,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -972,8 +973,6 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -983,7 +982,6 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= @@ -993,16 +991,16 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= -modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c= +modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= -modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= -modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= -mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= -mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= +modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM= +modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew= +mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= +mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/models/actions/TestActionTask_GetTasksByRunnerRequestKey/action_task.yml b/models/actions/TestActionTask_GetTasksByRunnerRequestKey/action_task.yml new file mode 100644 index 0000000000..2d6ce501ab --- /dev/null +++ b/models/actions/TestActionTask_GetTasksByRunnerRequestKey/action_task.yml @@ -0,0 +1,36 @@ +- + id: 100 + attempt: 3 + runner_id: 12345678 + status: 5 # StatusWaiting + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: false + token_hash: a1 + token_salt: eeeeeeee + token_last_eight: eeeeeeee + log_filename: artifact-test2/2f/47.log + log_in_storage: true + log_length: 707 + log_size: 90179 + log_expired: false + runner_request_key: 0a7e017d-4201-4b34-8cf4-de0f431893a4 +- + id: 101 + attempt: 3 + runner_id: 12345678 + status: 5 # StatusWaiting + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + is_fork_pull_request: false + token_hash: a2 + token_salt: eeeeeeee + token_last_eight: eeeeeeee + log_filename: artifact-test2/2f/47.log + log_in_storage: true + log_length: 707 + log_size: 90179 + log_expired: false + runner_request_key: 0a7e017d-4201-4b34-8cf4-de0f431893a4 diff --git a/models/actions/TestRunner_FindRunnerOptionsToConds/action_runner.yml b/models/actions/TestRunner_FindRunnerOptionsToConds/action_runner.yml new file mode 100644 index 0000000000..fc5190107e --- /dev/null +++ b/models/actions/TestRunner_FindRunnerOptionsToConds/action_runner.yml @@ -0,0 +1,45 @@ +- id: 719931 + uuid: "9e0eb762-cbdf-4725-9c90-4298568a2a77" + name: "runner-1" + version: "dev" + owner_id: 3 # Owned by org3 + repo_id: 0 + description: "A superb runner" + agent_labels: ["debian", "gpu"] + deleted: 0 +- id: 719932 + uuid: "b0a0a168-1b08-4365-95dd-e65040ca384d" + name: "runner-2" + version: "11.3.1" + owner_id: 2 # Owned by user2 + repo_id: 0 + description: "An exclusive runner" + agent_labels: ["docker"] + deleted: 0 +- id: 719933 + uuid: "974f9caf-ee64-4022-a56b-67b28ea06a0d" + name: "runner-3" + version: "12.2.0" + owner_id: 0 + repo_id: 0 + description: "A runner for everyone" + agent_labels: ["docker"] + deleted: 0 +- id: 719934 + uuid: "022650a8-e999-4a3d-afb0-21f75233798b" + name: "runner-4" + version: "12.1.0" + owner_id: 0 + repo_id: 32 # owned by org3 + description: "" + agent_labels: ["debian"] + deleted: 0 +- id: 719935 + uuid: "f3031695-87ea-41cf-8d48-21948e83ee17" + name: "runner-5" + version: "12.7.0" + owner_id: 0 + repo_id: 36 # owned by user2 + description: "" + agent_labels: ["arch"] + deleted: 0 diff --git a/models/actions/TestRunner_GetVisibleRunnerByID/action_runner.yml b/models/actions/TestRunner_GetVisibleRunnerByID/action_runner.yml new file mode 100644 index 0000000000..1b8198236e --- /dev/null +++ b/models/actions/TestRunner_GetVisibleRunnerByID/action_runner.yml @@ -0,0 +1,36 @@ +- id: 719931 + uuid: "9e0eb762-cbdf-4725-9c90-4298568a2a77" + name: "runner-1" + version: "dev" + owner_id: 3 # Owned by org3 + repo_id: 0 + description: "A superb runner" + agent_labels: ["debian", "gpu"] + deleted: 0 +- id: 719932 + uuid: "b0a0a168-1b08-4365-95dd-e65040ca384d" + name: "runner-2" + version: "11.3.1" + owner_id: 2 # Owned by user2 + repo_id: 0 + description: "An exclusive runner" + agent_labels: ["docker"] + deleted: 0 +- id: 719933 + uuid: "974f9caf-ee64-4022-a56b-67b28ea06a0d" + name: "runner-3" + version: "12.2.0" + owner_id: 0 + repo_id: 0 + description: "A runner for everyone" + agent_labels: ["docker"] + deleted: 0 +- id: 719934 + uuid: "022650a8-e999-4a3d-afb0-21f75233798b" + name: "runner-4" + version: "12.1.0" + owner_id: 0 + repo_id: 32 + description: "" + agent_labels: ["debian"] + deleted: 0 diff --git a/models/actions/artifact.go b/models/actions/artifact.go index 492e3f6cea..20b96f829c 100644 --- a/models/actions/artifact.go +++ b/models/actions/artifact.go @@ -9,6 +9,7 @@ package actions import ( "context" "errors" + "fmt" "time" "forgejo.org/models/db" @@ -88,6 +89,13 @@ func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPa return artifact, nil } +// IsV4 reports whether the artifact was uploaded via the v4 backend. +// The v4 backend stores the whole artifact as a single zip file; +// v1-v3 stores each file as a separate row. +func (a *ActionArtifact) IsV4() bool { + return a.ArtifactName+".zip" == a.ArtifactPath && a.ContentEncoding == "application/zip" +} + func getArtifactByNameAndPath(ctx context.Context, runID int64, name, fpath string) (*ActionArtifact, error) { var art ActionArtifact has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ?", runID, name, fpath).Get(&art) @@ -150,11 +158,32 @@ type ActionArtifactMeta struct { Status ArtifactStatus } +// AggregatedArtifact is the aggregated view of a logical artifact +// (one or more rows sharing the same run_id + artifact_name), used by the +// public API to represent a single artifact to clients. +type AggregatedArtifact struct { + ID int64 `xorm:"id"` + RunID int64 `xorm:"run_id"` + RepoID int64 `xorm:"-"` + ArtifactName string `xorm:"artifact_name"` + FileSize int64 `xorm:"file_size"` + Status ArtifactStatus `xorm:"status"` + CreatedUnix timeutil.TimeStamp `xorm:"created_unix"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated_unix"` + ExpiredUnix timeutil.TimeStamp `xorm:"expired_unix"` +} + +// APIDownloadURL returns the download URL for this artifact under the given +// repository API URL prefix (e.g. "https://host/api/v1/repos/owner/name"). +func (a *AggregatedArtifact) APIDownloadURL(repoAPIURL string) string { + return fmt.Sprintf("%s/actions/artifacts/%d/zip", repoAPIURL, a.ID) +} + // 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=? OR status=?)", runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). + Where(builder.Eq{"run_id": runID}.And(builder.In("status", ArtifactStatusUploadConfirmed, ArtifactStatusExpired))). GroupBy("artifact_name"). Select("artifact_name, sum(file_size) as file_size, max(status) as status"). Find(&arts) @@ -192,3 +221,94 @@ func SetArtifactDeleted(ctx context.Context, artifactID int64) error { _, err := db.GetEngine(ctx).ID(artifactID).Cols("status").Update(&ActionArtifact{Status: int64(ArtifactStatusDeleted)}) return err } + +// SetArtifactsOfRunDeleted marks all artifacts of the given run as deleted. +func SetArtifactsOfRunDeleted(ctx context.Context, runID int64) error { + _, err := db.GetEngine(ctx). + Where("run_id=?", runID). + Cols("status"). + Update(&ActionArtifact{Status: int64(ArtifactStatusPendingDeletion)}) + return err +} + +// aggregatedArtifactConds returns the common WHERE clause used by aggregated +// artifact queries: restrict to visible statuses and apply the caller's filters. +// The Status field on opts is ignored — visibility is fixed to UploadConfirmed/Expired. +func aggregatedArtifactConds(opts FindArtifactsOptions) builder.Cond { + opts.Status = 0 + return opts.ToConds().And(builder.In("status", ArtifactStatusUploadConfirmed, ArtifactStatusExpired)) +} + +const aggregatedArtifactSelect = "min(id) as id, run_id, artifact_name, sum(file_size) as file_size, max(status) as status, min(created_unix) as created_unix, max(updated_unix) as updated_unix, max(expired_unix) as expired_unix" + +// ListAggregatedArtifacts returns paginated aggregated artifacts. +// Each result represents one logical artifact: a (run_id, artifact_name) group, +// with ID = MIN(id), FileSize = SUM(file_size), Status = MAX(status), and +// timestamps aggregated accordingly. Status filter in opts is ignored; results +// are always restricted to UploadConfirmed and Expired statuses. +func ListAggregatedArtifacts(ctx context.Context, opts FindArtifactsOptions) ([]*AggregatedArtifact, int64, error) { + cond := aggregatedArtifactConds(opts) + + var countKeys []struct { + ID int64 `xorm:"id"` + } + if err := db.GetEngine(ctx).Table("action_artifact"). + Where(cond). + GroupBy("run_id, artifact_name"). + Select("min(id) as id"). + Find(&countKeys); err != nil { + return nil, 0, err + } + total := int64(len(countKeys)) + + sess := db.GetEngine(ctx).Table("action_artifact"). + Where(cond). + GroupBy("run_id, artifact_name"). + Select(aggregatedArtifactSelect). + OrderBy("id DESC") + + capacity := 10 + if opts.PageSize > 0 { + sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) + capacity = opts.PageSize + } + + arts := make([]*AggregatedArtifact, 0, capacity) + return arts, total, sess.Find(&arts) +} + +// GetAggregatedArtifactByID returns the aggregated artifact by its canonical ID +// (MIN(id) of the group), scoped to the given repository. Returns util.ErrNotExist +// when the ID does not exist, is not canonical for its group, or does not belong to repoID. +// The repoID scoping is performed in the query so callers don't need a follow-up check. +func GetAggregatedArtifactByID(ctx context.Context, repoID, artifactID int64) (*AggregatedArtifact, error) { + var art ActionArtifact + has, err := db.GetEngine(ctx).Where(builder.Eq{"id": artifactID, "repo_id": repoID}).Get(&art) + if err != nil { + return nil, err + } + if !has { + return nil, util.ErrNotExist + } + + cond := aggregatedArtifactConds(FindArtifactsOptions{ + RunID: art.RunID, + ArtifactName: art.ArtifactName, + }) + + meta := new(AggregatedArtifact) + has, err = db.GetEngine(ctx).Table("action_artifact"). + Where(cond). + GroupBy("run_id, artifact_name"). + Select(aggregatedArtifactSelect). + Get(meta) + if err != nil { + return nil, err + } + if !has || meta.ID != artifactID { + return nil, util.ErrNotExist + } + + meta.RepoID = art.RepoID + return meta, nil +} diff --git a/models/actions/forgejo.go b/models/actions/forgejo.go index ce3f8b0c8b..a55066fe3a 100644 --- a/models/actions/forgejo.go +++ b/models/actions/forgejo.go @@ -14,7 +14,7 @@ import ( gouuid "github.com/google/uuid" ) -func RegisterRunner(ctx context.Context, ownerID, repoID int64, token string, labels *[]string, name, version string) (*ActionRunner, error) { +func RegisterRunner(ctx context.Context, ownerID, repoID int64, token string, labels *[]string, name, version string, ephemeral bool) (*ActionRunner, error) { uuid, err := gouuid.FromBytes([]byte(token[:16])) if err != nil { return nil, fmt.Errorf("gouuid.FromBytes %v", err) @@ -60,11 +60,12 @@ func RegisterRunner(ctx context.Context, ownerID, repoID int64, token string, la // name, _ = util.SplitStringAtByteN(name, 255) - cols := []string{"name", "owner_id", "repo_id", "version"} + cols := []string{"name", "owner_id", "repo_id", "version", "ephemeral"} runner.Name = name runner.OwnerID = ownerID runner.RepoID = repoID runner.Version = version + runner.Ephemeral = ephemeral if labels != nil { runner.AgentLabels = *labels cols = append(cols, "agent_labels") diff --git a/models/actions/forgejo_test.go b/models/actions/forgejo_test.go index 5702068c1b..2170b0927a 100644 --- a/models/actions/forgejo_test.go +++ b/models/actions/forgejo_test.go @@ -22,9 +22,11 @@ func TestActions_RegisterRunner_Token(t *testing.T) { labels := []string{} name := "runner" version := "v1.2.3" - runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, &labels, name, version) + ephemeral := true + runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, &labels, name, version, ephemeral) require.NoError(t, err) assert.Equal(t, name, runner.Name) + assert.True(t, runner.Ephemeral) assert.Equal(t, 1, subtle.ConstantTimeCompare([]byte(runner.TokenHash), []byte(auth_model.HashToken(token, runner.TokenSalt))), "the token cannot be verified with the same method as routers/api/actions/runner/interceptor.go as of 8228751c55d6a4263f0fec2932ca16181c09c97d") } @@ -44,7 +46,7 @@ func TestActions_RegisterRunner_TokenUpdate(t *testing.T) { "the initial token should match the runner's secret", ) - RegisterRunner(db.DefaultContext, before.OwnerID, before.RepoID, newToken, nil, before.Name, before.Version) + RegisterRunner(db.DefaultContext, before.OwnerID, before.RepoID, newToken, nil, before.Name, before.Version, false) after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: recordID}) @@ -66,10 +68,11 @@ func TestActions_RegisterRunner_CreateWithLabels(t *testing.T) { token := "0123456789012345678901234567890123456789" name := "runner" version := "v1.2.3" + ephemeral := true labels := []string{"woop", "doop"} labelsCopy := labels // labels may be affected by the tested function so we copy them - runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, &labels, name, version) + runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, &labels, name, version, ephemeral) require.NoError(t, err) // Check that the returned record has been updated, except for the labels @@ -78,6 +81,7 @@ func TestActions_RegisterRunner_CreateWithLabels(t *testing.T) { assert.Equal(t, name, runner.Name) assert.Equal(t, version, runner.Version) assert.Equal(t, labelsCopy, runner.AgentLabels) + assert.Equal(t, ephemeral, runner.Ephemeral) // Check that whatever is in the DB has been updated, except for the labels after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: runner.ID}) @@ -86,6 +90,7 @@ func TestActions_RegisterRunner_CreateWithLabels(t *testing.T) { assert.Equal(t, name, after.Name) assert.Equal(t, version, after.Version) assert.Equal(t, labelsCopy, after.AgentLabels) + assert.Equal(t, ephemeral, after.Ephemeral) } func TestActions_RegisterRunner_CreateWithoutLabels(t *testing.T) { @@ -95,8 +100,9 @@ func TestActions_RegisterRunner_CreateWithoutLabels(t *testing.T) { token := "0123456789012345678901234567890123456789" name := "runner" version := "v1.2.3" + ephemeral := true - runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, nil, name, version) + runner, err := RegisterRunner(db.DefaultContext, ownerID, repoID, token, nil, name, version, ephemeral) require.NoError(t, err) // Check that the returned record has been updated, except for the labels @@ -105,6 +111,7 @@ func TestActions_RegisterRunner_CreateWithoutLabels(t *testing.T) { assert.Equal(t, name, runner.Name) assert.Equal(t, version, runner.Version) assert.Equal(t, []string{}, runner.AgentLabels) + assert.Equal(t, ephemeral, runner.Ephemeral) // Check that whatever is in the DB has been updated, except for the labels after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: runner.ID}) @@ -113,6 +120,7 @@ func TestActions_RegisterRunner_CreateWithoutLabels(t *testing.T) { assert.Equal(t, name, after.Name) assert.Equal(t, version, after.Version) assert.Equal(t, []string{}, after.AgentLabels) + assert.Equal(t, ephemeral, after.Ephemeral) } func TestActions_RegisterRunner_UpdateWithLabels(t *testing.T) { @@ -125,10 +133,11 @@ func TestActions_RegisterRunner_UpdateWithLabels(t *testing.T) { newRepoID := int64(1) newName := "rennur" newVersion := "v4.5.6" + ephemeral := true newLabels := []string{"warp", "darp"} labelsCopy := newLabels // labels may be affected by the tested function so we copy them - runner, err := RegisterRunner(db.DefaultContext, newOwnerID, newRepoID, token, &newLabels, newName, newVersion) + runner, err := RegisterRunner(db.DefaultContext, newOwnerID, newRepoID, token, &newLabels, newName, newVersion, ephemeral) require.NoError(t, err) // Check that the returned record has been updated @@ -137,6 +146,7 @@ func TestActions_RegisterRunner_UpdateWithLabels(t *testing.T) { assert.Equal(t, newName, runner.Name) assert.Equal(t, newVersion, runner.Version) assert.Equal(t, labelsCopy, runner.AgentLabels) + assert.Equal(t, ephemeral, runner.Ephemeral) // Check that whatever is in the DB has been updated after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: recordID}) @@ -145,6 +155,7 @@ func TestActions_RegisterRunner_UpdateWithLabels(t *testing.T) { assert.Equal(t, newName, after.Name) assert.Equal(t, newVersion, after.Version) assert.Equal(t, labelsCopy, after.AgentLabels) + assert.Equal(t, ephemeral, after.Ephemeral) } func TestActions_RegisterRunner_UpdateWithoutLabels(t *testing.T) { @@ -157,8 +168,9 @@ func TestActions_RegisterRunner_UpdateWithoutLabels(t *testing.T) { newRepoID := int64(1) newName := "rennur" newVersion := "v4.5.6" + ephemeral := true - runner, err := RegisterRunner(db.DefaultContext, newOwnerID, newRepoID, token, nil, newName, newVersion) + runner, err := RegisterRunner(db.DefaultContext, newOwnerID, newRepoID, token, nil, newName, newVersion, ephemeral) require.NoError(t, err) // Check that the returned record has been updated, except for the labels @@ -167,6 +179,7 @@ func TestActions_RegisterRunner_UpdateWithoutLabels(t *testing.T) { assert.Equal(t, newName, runner.Name) assert.Equal(t, newVersion, runner.Version) assert.Equal(t, before.AgentLabels, runner.AgentLabels) + assert.Equal(t, ephemeral, runner.Ephemeral) // Check that whatever is in the DB has been updated, except for the labels after := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: recordID}) @@ -175,4 +188,5 @@ func TestActions_RegisterRunner_UpdateWithoutLabels(t *testing.T) { assert.Equal(t, newName, after.Name) assert.Equal(t, newVersion, after.Version) assert.Equal(t, before.AgentLabels, after.AgentLabels) + assert.Equal(t, ephemeral, after.Ephemeral) } diff --git a/models/actions/pre_execution_errors.go b/models/actions/pre_execution_errors.go index 28cd589ef9..000d59d71d 100644 --- a/models/actions/pre_execution_errors.go +++ b/models/actions/pre_execution_errors.go @@ -26,6 +26,11 @@ const ( ErrorCodeIncompleteRunsOnMissingOutput ErrorCodeIncompleteRunsOnMissingMatrixDimension ErrorCodeIncompleteRunsOnUnknownCause + ErrorCodeIncompleteWithMissingJob + ErrorCodeIncompleteWithMissingOutput + ErrorCodeIncompleteWithMissingMatrixDimension + ErrorCodeIncompleteWithUnknownCause + ErrorCodeUnknownJobInNeeds ) func TranslatePreExecutionError(lang translation.Locale, run *ActionRun) string { @@ -57,6 +62,16 @@ func TranslatePreExecutionError(lang translation.Locale, run *ActionRun) string return lang.TrString("actions.workflow.incomplete_runson_missing_matrix_dimension", run.PreExecutionErrorDetails...) case ErrorCodeIncompleteRunsOnUnknownCause: return lang.TrString("actions.workflow.incomplete_runson_unknown_cause", run.PreExecutionErrorDetails...) + case ErrorCodeIncompleteWithMissingJob: + return lang.TrString("actions.workflow.incomplete_with_missing_job", run.PreExecutionErrorDetails...) + case ErrorCodeIncompleteWithMissingOutput: + return lang.TrString("actions.workflow.incomplete_with_missing_output", run.PreExecutionErrorDetails...) + case ErrorCodeIncompleteWithMissingMatrixDimension: + return lang.TrString("actions.workflow.incomplete_with_missing_matrix_dimension", run.PreExecutionErrorDetails...) + case ErrorCodeIncompleteWithUnknownCause: + return lang.TrString("actions.workflow.incomplete_with_unknown_cause", run.PreExecutionErrorDetails...) + case ErrorCodeUnknownJobInNeeds: + return lang.TrString("actions.workflow.unknown_job_in_needs", run.PreExecutionErrorDetails...) } return fmt.Sprintf(" 0 || run.NeedApproval || v.IncompleteMatrix || v.IncompleteRunsOn { + if len(needs) > 0 || run.NeedApproval || v.IncompleteMatrix || v.IncompleteRunsOn || v.IncompleteWith { status = StatusBlocked } else { status = StatusWaiting @@ -351,7 +427,8 @@ func InsertRunJobs(ctx context.Context, run *ActionRun, jobs []*jobparser.Single name, _ = util.SplitStringAtByteN(job.Name, 255) runsOn = job.RunsOn() } - runJobs = append(runJobs, &ActionRunJob{ + + runJob := &ActionRunJob{ RunID: run.ID, RepoID: run.RepoID, OwnerID: run.OwnerID, @@ -362,8 +439,12 @@ func InsertRunJobs(ctx context.Context, run *ActionRun, jobs []*jobparser.Single JobID: id, Needs: needs, RunsOn: runsOn, - Status: status, - }) + } + if err := runJob.PrepareNextAttempt(status); err != nil { + return err + } + + runJobs = append(runJobs, runJob) } if len(runJobs) > 0 { @@ -531,4 +612,11 @@ func ComputeRunStatus(ctx context.Context, runID int64) (run *ActionRun, columns return run, columns, nil } +// DeleteRun removes the given run. It is the caller's responsibility to handle the run's dependencies like artifacts or +// jobs. Nothing happens if the run does not exist. +func DeleteRun(ctx context.Context, runID int64) error { + _, err := db.GetEngine(ctx).Delete(&ActionRun{ID: runID}) + return err +} + type ActionRunIndex db.ResourceIndex diff --git a/models/actions/run_job.go b/models/actions/run_job.go index ed1cefbaa5..e64b62805c 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -15,6 +15,7 @@ import ( "forgejo.org/modules/util" "code.forgejo.org/forgejo/runner/v12/act/jobparser" + gouuid "github.com/google/uuid" "go.yaml.in/yaml/v3" "xorm.io/builder" ) @@ -30,6 +31,7 @@ type ActionRunJob struct { IsForkPullRequest bool Name string `xorm:"VARCHAR(255)"` Attempt int64 + Handle string `xorm:"unique"` WorkflowPayload []byte JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id Needs []string `xorm:"JSON TEXT"` @@ -108,6 +110,12 @@ func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { return job.Run.LoadAttributes(ctx) } +// IsRequestedByRunner returns true if this attempt of this ActionRunJob was explicitly requested by the runner or if +// the runner expressed no preference. +func (job *ActionRunJob) IsRequestedByRunner(handle *string) bool { + return handle == nil || job.Handle == *handle +} + func (job *ActionRunJob) ItRunsOn(labels []string) bool { if len(labels) == 0 || len(job.RunsOn) == 0 { return false @@ -117,6 +125,34 @@ func (job *ActionRunJob) ItRunsOn(labels []string) bool { return labelSet.IsSubset(job.RunsOn) } +func (job *ActionRunJob) PrepareNextAttempt(initialStatus Status) error { + if job.Status != StatusUnknown && !job.Status.IsDone() { + return fmt.Errorf("cannot prepare next attempt because job %d is active: %s", job.ID, job.Status.String()) + } + + job.Attempt++ + job.Started = 0 + job.Stopped = 0 + job.TaskID = 0 + job.Handle = gouuid.New().String() + job.Status = initialStatus + + return nil +} + +// CanBeRerun answers whether this ActionRunJob can be rerun. Returns true if it is done and the Run it belongs to +// is valid. Returns false in all other cases. +func (job *ActionRunJob) CanBeRerun(ctx context.Context) (bool, error) { + if err := job.LoadRun(ctx); err != nil { + return false, fmt.Errorf("cannot load run %d of job %d: %w", job.RunID, job.ID, err) + } + + if !job.Run.IsValid() { + return false, nil + } + return job.Status.IsDone(), nil +} + func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { var job ActionRunJob has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job) @@ -234,7 +270,9 @@ var AggregateJobStatus = func(jobs []*ActionRunJob) Status { } } -func (job *ActionRunJob) decodeWorkflowPayload() (*jobparser.SingleWorkflow, error) { +// Retrieves the parsed workflow for this specific job. This field is often accessed multiple times in succession, so +// the parsed content is cached in-memory on the `ActionRunJob` instance. +func (job *ActionRunJob) DecodeWorkflowPayload() (*jobparser.SingleWorkflow, error) { if job.workflowPayloadDecoded != nil { return job.workflowPayloadDecoded, nil } @@ -258,8 +296,8 @@ func (job *ActionRunJob) ClearCachedWorkflowPayload() { // Checks whether the target job is an `(incomplete matrix)` job that will be blocked until the matrix is complete, and // then regenerated and deleted. If it is incomplete, and if the information is available, the specific job and/or // output that causes it to be incomplete will be returned as well. -func (job *ActionRunJob) IsIncompleteMatrix() (bool, *jobparser.IncompleteNeeds, error) { - jobWorkflow, err := job.decodeWorkflowPayload() +func (job *ActionRunJob) HasIncompleteMatrix() (bool, *jobparser.IncompleteNeeds, error) { + jobWorkflow, err := job.DecodeWorkflowPayload() if err != nil { return false, nil, fmt.Errorf("failure decoding workflow payload: %w", err) } @@ -268,10 +306,69 @@ func (job *ActionRunJob) IsIncompleteMatrix() (bool, *jobparser.IncompleteNeeds, // Checks whether the target job has a `runs-on` field with an expression that requires an input from another job. The // job will be blocked until the other job is complete, and then regenerated and deleted. -func (job *ActionRunJob) IsIncompleteRunsOn() (bool, *jobparser.IncompleteNeeds, *jobparser.IncompleteMatrix, error) { - jobWorkflow, err := job.decodeWorkflowPayload() +func (job *ActionRunJob) HasIncompleteRunsOn() (bool, *jobparser.IncompleteNeeds, *jobparser.IncompleteMatrix, error) { + jobWorkflow, err := job.DecodeWorkflowPayload() if err != nil { return false, nil, nil, fmt.Errorf("failure decoding workflow payload: %w", err) } return jobWorkflow.IncompleteRunsOn, jobWorkflow.IncompleteRunsOnNeeds, jobWorkflow.IncompleteRunsOnMatrix, nil } + +// Check whether the target job was generated as a result of expanding a reusable workflow. +func (job *ActionRunJob) IsWorkflowCallInnerJob() (bool, error) { + jobWorkflow, err := job.DecodeWorkflowPayload() + if err != nil { + return false, fmt.Errorf("failure decoding workflow payload: %w", err) + } + return jobWorkflow.Metadata.WorkflowCallParent != "", nil +} + +// Check whether this job is a caller of a reusable workflow -- in other words, the real work done in this job is in +// spawned child jobs, not this job. +func (job *ActionRunJob) IsWorkflowCallOuterJob() (bool, error) { + jobWorkflow, err := job.DecodeWorkflowPayload() + if err != nil { + return false, fmt.Errorf("failure decoding workflow payload: %w", err) + } + return jobWorkflow.Metadata.WorkflowCallID != "", nil +} + +// Checks whether the target job has a `with` field with an expression that requires an input from another job. The job +// will be blocked until the other job is complete, and then regenerated and deleted. +func (job *ActionRunJob) HasIncompleteWith() (bool, *jobparser.IncompleteNeeds, *jobparser.IncompleteMatrix, error) { + jobWorkflow, err := job.DecodeWorkflowPayload() + if err != nil { + return false, nil, nil, fmt.Errorf("failure decoding workflow payload: %w", err) + } + return jobWorkflow.IncompleteWith, jobWorkflow.IncompleteWithNeeds, jobWorkflow.IncompleteWithMatrix, nil +} + +// EnableOpenIDConnect checks whether the job allows for ID token generation. +func (job *ActionRunJob) EnableOpenIDConnect() (bool, error) { + jobWorkflow, err := job.DecodeWorkflowPayload() + if err != nil { + return false, fmt.Errorf("failure decoding workflow payload: %w", err) + } + return jobWorkflow.EnableOpenIDConnect, nil +} + +// AllNeedsExist checks whether this ActionRunJob's Needs can theoretically be met by comparing them with the supplied +// list of all job IDs that part of a particular workflow run. Returns the list of unknown job IDs found in Needs +// alongside an indicator whether the check was successful. +func (job *ActionRunJob) AllNeedsExist(allExistingJobIDs container.Set[string]) ([]string, bool) { + unknownJobIDs := []string{} + for _, need := range job.Needs { + if !allExistingJobIDs.Contains(need) { + unknownJobIDs = append(unknownJobIDs, need) + } + } + + return unknownJobIDs, len(unknownJobIDs) == 0 +} + +// DeleteJob removes the given job. Removing all associated tasks is up to the caller. If the given job does not exist, +// nothing happens. +func DeleteJob(ctx context.Context, jobID int64) error { + _, err := db.GetEngine(ctx).Delete(&ActionRunJob{ID: jobID}) + return err +} diff --git a/models/actions/run_job_list.go b/models/actions/run_job_list.go index f4cc4bfb45..40a182082f 100644 --- a/models/actions/run_job_list.go +++ b/models/actions/run_job_list.go @@ -22,6 +22,14 @@ func (jobs ActionJobList) GetRunIDs() []int64 { }) } +func (jobs ActionJobList) GetJobIDs() container.Set[string] { + jobIDs := container.SetOf[string]() + for _, job := range jobs { + jobIDs.Add(job.JobID) + } + return jobIDs +} + func (jobs ActionJobList) LoadRuns(ctx context.Context, withRepo bool) error { runIDs := jobs.GetRunIDs() runs := make(map[int64]*ActionRun, len(runIDs)) @@ -55,8 +63,6 @@ type FindRunJobOptions struct { CommitSHA string Statuses []Status UpdatedBefore timeutil.TimeStamp - Events []string // []webhook_module.HookEventType - RunNumber int64 RunNeedsApproval optional.Option[bool] } @@ -68,7 +74,7 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.OwnerID > 0 { + if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } if opts.CommitSHA != "" { @@ -80,16 +86,10 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.UpdatedBefore > 0 { cond = cond.And(builder.Lt{"updated": opts.UpdatedBefore}) } - if len(opts.Events) > 0 { - cond = cond.And(builder.In("event", opts.Events)) - } - if opts.RunNumber > 0 { - cond = cond.And(builder.Eq{"`index`": opts.RunNumber}) - } - if opts.RunNeedsApproval.Has() { + if has, value := opts.RunNeedsApproval.Get(); has { cond = cond.And(builder.Exists(builder.Select("id").From("action_run", "outer_run"). Where(builder.Eq{ - "outer_run.need_approval": opts.RunNeedsApproval.Value(), + "outer_run.need_approval": value, "outer_run.id": builder.Expr("run_id"), }))) } diff --git a/models/actions/run_job_list_test.go b/models/actions/run_job_list_test.go new file mode 100644 index 0000000000..223dd3b411 --- /dev/null +++ b/models/actions/run_job_list_test.go @@ -0,0 +1,21 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package actions + +import ( + "testing" + + "forgejo.org/modules/container" + + "github.com/stretchr/testify/assert" +) + +func TestActionJobList_GetJobIDs(t *testing.T) { + jobs := ActionJobList{ + &ActionRunJob{JobID: "job 1"}, + &ActionRunJob{JobID: "job 2"}, + } + + assert.Equal(t, container.SetOf("job 2", "job 1"), jobs.GetJobIDs()) +} diff --git a/models/actions/run_job_test.go b/models/actions/run_job_test.go index c122b1ff55..d9275a81ec 100644 --- a/models/actions/run_job_test.go +++ b/models/actions/run_job_test.go @@ -8,6 +8,8 @@ import ( "forgejo.org/models/db" "forgejo.org/models/unittest" + "forgejo.org/modules/container" + "forgejo.org/modules/timeutil" "code.forgejo.org/forgejo/runner/v12/act/jobparser" "github.com/stretchr/testify/assert" @@ -72,7 +74,7 @@ func TestActionRunJob_HTMLURL(t *testing.T) { } } -func TestActionRunJob_IsIncompleteMatrix(t *testing.T) { +func TestActionRunJob_HasIncompleteMatrix(t *testing.T) { tests := []struct { name string job ActionRunJob @@ -100,7 +102,7 @@ func TestActionRunJob_IsIncompleteMatrix(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - isIncomplete, needs, err := tt.job.IsIncompleteMatrix() + isIncomplete, needs, err := tt.job.HasIncompleteMatrix() if tt.errContains != "" { assert.ErrorContains(t, err, tt.errContains) } else { @@ -112,7 +114,7 @@ func TestActionRunJob_IsIncompleteMatrix(t *testing.T) { } } -func TestActionRunJob_IsIncompleteRunsOn(t *testing.T) { +func TestActionRunJob_HasIncompleteRunsOn(t *testing.T) { tests := []struct { name string job ActionRunJob @@ -147,7 +149,129 @@ func TestActionRunJob_IsIncompleteRunsOn(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - isIncomplete, needs, matrix, err := tt.job.IsIncompleteRunsOn() + isIncomplete, needs, matrix, err := tt.job.HasIncompleteRunsOn() + if tt.errContains != "" { + assert.ErrorContains(t, err, tt.errContains) + } else { + require.NoError(t, err) + assert.Equal(t, tt.isIncomplete, isIncomplete) + assert.Equal(t, tt.needs, needs) + assert.Equal(t, tt.matrix, matrix) + } + }) + } +} + +func TestActionRunJob_IsWorkflowCallOuterJob(t *testing.T) { + tests := []struct { + name string + job ActionRunJob + isWorkflowCallOuterJob bool + errContains string + }{ + { + name: "normal workflow", + job: ActionRunJob{WorkflowPayload: []byte("name: workflow")}, + isWorkflowCallOuterJob: false, + }, + { + name: "workflow_call outer job", + job: ActionRunJob{WorkflowPayload: []byte("name: test\njobs:\n job:\n if: false\n__metadata:\n workflow_call_id: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed\n")}, + isWorkflowCallOuterJob: true, + }, + { + name: "unparseable workflow", + job: ActionRunJob{WorkflowPayload: []byte("name: []\nincomplete_runs_on: true")}, + errContains: "failure unmarshaling WorkflowPayload to SingleWorkflow: yaml: unmarshal errors", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isWorkflowCallOuterJob, err := tt.job.IsWorkflowCallOuterJob() + if tt.errContains != "" { + assert.ErrorContains(t, err, tt.errContains) + } else { + require.NoError(t, err) + assert.Equal(t, tt.isWorkflowCallOuterJob, isWorkflowCallOuterJob) + } + }) + } +} + +func TestActionRunJob_IsWorkflowCallInnerJob(t *testing.T) { + tests := []struct { + name string + job ActionRunJob + isWorkflowCallInnerJob bool + errContains string + }{ + { + name: "normal workflow", + job: ActionRunJob{WorkflowPayload: []byte("on: [workflow_dispatch]\nname: workflow")}, + isWorkflowCallInnerJob: false, + }, + { + name: "inner job", + job: ActionRunJob{WorkflowPayload: []byte("on:\n workflow_call:\nname: workflow\n__metadata:\n workflow_call_parent: b5a9f46f1f2513d7777fde50b169d323a6519e349cc175484c947ac315a209ed\n")}, + isWorkflowCallInnerJob: true, + }, + { + name: "unparseable workflow", + job: ActionRunJob{WorkflowPayload: []byte("name: []\nincomplete_runs_on: true")}, + errContains: "failure unmarshaling WorkflowPayload to SingleWorkflow: yaml: unmarshal errors", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isWorkflowCallInnerJob, err := tt.job.IsWorkflowCallInnerJob() + if tt.errContains != "" { + assert.ErrorContains(t, err, tt.errContains) + } else { + require.NoError(t, err) + assert.Equal(t, tt.isWorkflowCallInnerJob, isWorkflowCallInnerJob) + } + }) + } +} + +func TestActionRunJob_HasIncompleteWith(t *testing.T) { + tests := []struct { + name string + job ActionRunJob + isIncomplete bool + needs *jobparser.IncompleteNeeds + matrix *jobparser.IncompleteMatrix + errContains string + }{ + { + name: "normal workflow", + job: ActionRunJob{WorkflowPayload: []byte("name: workflow")}, + isIncomplete: false, + }, + { + name: "incomplete_with workflow", + job: ActionRunJob{WorkflowPayload: []byte("name: workflow\nincomplete_with: true\nincomplete_with_needs: { job: abc }")}, + needs: &jobparser.IncompleteNeeds{Job: "abc"}, + isIncomplete: true, + }, + { + name: "incomplete_with workflow", + job: ActionRunJob{WorkflowPayload: []byte("name: workflow\nincomplete_with: true\nincomplete_with_matrix: { dimension: abc }")}, + matrix: &jobparser.IncompleteMatrix{Dimension: "abc"}, + isIncomplete: true, + }, + { + name: "unparseable workflow", + job: ActionRunJob{WorkflowPayload: []byte("name: []\nincomplete_with: true")}, + errContains: "failure unmarshaling WorkflowPayload to SingleWorkflow: yaml: unmarshal errors", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isIncomplete, needs, matrix, err := tt.job.HasIncompleteWith() if tt.errContains != "" { assert.ErrorContains(t, err, tt.errContains) } else { @@ -179,3 +303,202 @@ func TestRunHasOtherJobs(t *testing.T) { require.NoError(t, err) assert.False(t, has) } + +func TestActionRunJobPrepareNextAttempt(t *testing.T) { + lastHandle := "original-handle" + job := ActionRunJob{ID: 46, Handle: lastHandle} + + err := job.PrepareNextAttempt(StatusWaiting) + require.NoError(t, err) + + assert.NotEqual(t, lastHandle, job.Handle) + assert.NotEmpty(t, job.Handle) + assert.Equal(t, int64(1), job.Attempt) + assert.Zero(t, job.Started) + assert.Zero(t, job.Stopped) + assert.Zero(t, job.TaskID) + assert.Equal(t, StatusWaiting, job.Status) + + lastHandle = job.Handle + job.Started = timeutil.TimeStampNow() + job.Stopped = timeutil.TimeStampNow() + job.TaskID = int64(59) + job.Status = StatusFailure + + err = job.PrepareNextAttempt(StatusBlocked) + require.NoError(t, err) + + assert.NotEqual(t, lastHandle, job.Handle) + assert.NotEmpty(t, job.Handle) + assert.Equal(t, int64(2), job.Attempt) + assert.Zero(t, job.Started) + assert.Zero(t, job.Stopped) + assert.Zero(t, job.TaskID) + assert.Equal(t, StatusBlocked, job.Status) + + lastHandle = job.Handle + + // The job hasn't finished yet. Preparing a next attempt should not be possible. It should be left untouched. + err = job.PrepareNextAttempt(StatusWaiting) + require.ErrorContains(t, err, "cannot prepare next attempt because job 46 is active: blocked") + + assert.Equal(t, lastHandle, job.Handle) + assert.Equal(t, int64(2), job.Attempt) + assert.Zero(t, job.Started) + assert.Zero(t, job.Stopped) + assert.Zero(t, job.TaskID) + assert.Equal(t, StatusBlocked, job.Status) +} + +func TestIsRequestedByRunner(t *testing.T) { + sameHandle := "4a1ca0be-4470-486d-8504-89b4a5ac00cf" + differentHandle := "88423da3-67af-4f2d-9a92-a0db822697e9" + emptyHandle := "" + + job := &ActionRunJob{ID: 422, Attempt: 5, Handle: sameHandle} + + assert.True(t, job.IsRequestedByRunner(nil)) + assert.True(t, job.IsRequestedByRunner(&sameHandle)) + assert.False(t, job.IsRequestedByRunner(&differentHandle)) + assert.False(t, job.IsRequestedByRunner(&emptyHandle)) + + // Old jobs that were created before the introduction of Handle do not have one. + emptyHandleJob := &ActionRunJob{ID: 422, Attempt: 5, Handle: ""} + + assert.True(t, emptyHandleJob.IsRequestedByRunner(nil)) + assert.True(t, emptyHandleJob.IsRequestedByRunner(&emptyHandle)) + + assert.False(t, emptyHandleJob.IsRequestedByRunner(&differentHandle)) +} + +func TestAllNeedsExist(t *testing.T) { + testCases := []struct { + name string + job ActionRunJob + existingJobIDs container.Set[string] + expectedUnknownIDs []string + ok bool + }{ + { + name: "no needs", + job: ActionRunJob{Needs: nil}, + existingJobIDs: container.Set[string]{}, + expectedUnknownIDs: []string{}, + ok: true, + }, + { + name: "empty needs", + job: ActionRunJob{Needs: []string{}}, + existingJobIDs: container.Set[string]{}, + expectedUnknownIDs: []string{}, + ok: true, + }, + { + name: "satisfied needs", + job: ActionRunJob{Needs: []string{"job1", "job2"}}, + existingJobIDs: container.SetOf("job2", "job1"), + expectedUnknownIDs: []string{}, + ok: true, + }, + { + name: "unsatisfied needs", + job: ActionRunJob{Needs: []string{"unknown", "job2"}}, + existingJobIDs: container.SetOf("job2", "job1"), + expectedUnknownIDs: []string{"unknown"}, + ok: false, + }, + { + name: "comparison is case-sensitive", + job: ActionRunJob{Needs: []string{"Job1", "job2"}}, + existingJobIDs: container.SetOf("job2", "job1"), + expectedUnknownIDs: []string{"Job1"}, + ok: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + unknownIDs, ok := testCase.job.AllNeedsExist(testCase.existingJobIDs) + + assert.Equal(t, testCase.ok, ok) + assert.Equal(t, testCase.expectedUnknownIDs, unknownIDs) + }) + } +} + +func TestActionRunJob_CanBeRerun(t *testing.T) { + testCases := []struct { + name string + job ActionRunJob + canBeRerun bool + expectedError string + }{ + { + name: "job with unknown status", + job: ActionRunJob{Run: &ActionRun{Status: StatusSuccess}, Status: StatusUnknown}, + canBeRerun: false, + }, + { + name: "successful job", + job: ActionRunJob{Run: &ActionRun{Status: StatusSuccess}, Status: StatusSuccess}, + canBeRerun: true, + }, + { + name: "failed job", + job: ActionRunJob{Run: &ActionRun{Status: StatusSuccess}, Status: StatusFailure}, + canBeRerun: true, + }, + { + name: "cancelled job", + job: ActionRunJob{Run: &ActionRun{Status: StatusSuccess}, Status: StatusCancelled}, + canBeRerun: true, + }, + { + name: "skipped job", + job: ActionRunJob{Run: &ActionRun{Status: StatusSuccess}, Status: StatusSkipped}, + canBeRerun: true, + }, + { + name: "waiting job", + job: ActionRunJob{Run: &ActionRun{Status: StatusSuccess}, Status: StatusWaiting}, + canBeRerun: false, + }, + { + name: "blocked job", + job: ActionRunJob{Run: &ActionRun{Status: StatusSuccess}, Status: StatusBlocked}, + canBeRerun: false, + }, + { + name: "ActionRun is nil", + job: ActionRunJob{ID: 12, Run: nil, Status: StatusSuccess}, + expectedError: "cannot load run 0 of job 12", + }, + { + name: "with busy run but completed job", + job: ActionRunJob{Run: &ActionRun{Status: StatusRunning}, Status: StatusSuccess}, + canBeRerun: true, + }, + { + name: "with run that cannot be run", + job: ActionRunJob{ + Run: &ActionRun{Status: StatusRunning, PreExecutionErrorCode: ErrorCodeEventDetectionError}, + Status: StatusSuccess, + }, + canBeRerun: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result, err := testCase.job.CanBeRerun(t.Context()) + + if testCase.expectedError == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, testCase.expectedError) + } + + assert.Equal(t, testCase.canBeRerun, result) + }) + } +} diff --git a/models/actions/run_list.go b/models/actions/run_list.go index 92be510569..11fa8441a5 100644 --- a/models/actions/run_list.go +++ b/models/actions/run_list.go @@ -5,6 +5,7 @@ package actions import ( "context" + "slices" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" @@ -13,6 +14,8 @@ import ( "forgejo.org/modules/translation" webhook_module "forgejo.org/modules/webhook" + "golang.org/x/text/collate" + "golang.org/x/text/language" "xorm.io/builder" ) @@ -72,6 +75,9 @@ type FindRunOptions struct { TriggerEvent webhook_module.HookEventType Approved bool // not util.OptionalBool, it works only when it's true Status []Status + Events []string // []webhook_module.HookEventType + RunNumber int64 + CommitSHA string } func (opts FindRunOptions) ToConds() builder.Cond { @@ -79,7 +85,7 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.OwnerID > 0 { + if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } if opts.WorkflowID != "" { @@ -100,6 +106,15 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.TriggerEvent != "" { cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent}) } + if len(opts.Events) > 0 { + cond = cond.And(builder.In("event", opts.Events)) + } + if opts.RunNumber > 0 { + cond = cond.And(builder.Eq{"`index`": opts.RunNumber}) + } + if opts.CommitSHA != "" { + cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA}) + } return cond } @@ -115,14 +130,18 @@ type StatusInfo struct { // GetStatusInfoList returns a slice of StatusInfo func GetStatusInfoList(ctx context.Context, lang translation.Locale) []StatusInfo { // same as those in aggregateJobStatus - allStatus := []Status{StatusSuccess, StatusFailure, StatusWaiting, StatusRunning} - statusInfoList := make([]StatusInfo, 0, 4) + allStatus := []Status{StatusBlocked, StatusCancelled, StatusFailure, StatusRunning, StatusSkipped, StatusSuccess, StatusWaiting} + statusInfoList := make([]StatusInfo, 0, 7) for _, s := range allStatus { statusInfoList = append(statusInfoList, StatusInfo{ Status: int(s), DisplayedStatus: s.LocaleString(lang), }) } + collator := collate.New(language.Und, collate.IgnoreCase) + slices.SortFunc(statusInfoList, func(a, b StatusInfo) int { + return collator.CompareString(a.DisplayedStatus, b.DisplayedStatus) + }) return statusInfoList } diff --git a/models/actions/run_list_test.go b/models/actions/run_list_test.go new file mode 100644 index 0000000000..b3d22b5155 --- /dev/null +++ b/models/actions/run_list_test.go @@ -0,0 +1,37 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package actions + +import ( + "testing" + + "forgejo.org/models/unittest" + "forgejo.org/modules/translation" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestActionStatusList(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + translation.InitLocales(t.Context()) + + statusInfoList := GetStatusInfoList(t.Context(), translation.NewLocale("en-US")) + assert.Len(t, statusInfoList, 7) + statuses := []string{"Blocked", "Canceled", "Failure", "Running", "Skipped", "Success", "Waiting"} + statusInts := []int{7, 3, 2, 6, 4, 1, 5} + for i, statusString := range statuses { + assert.Equal(t, statusInfoList[i].Status, statusInts[i]) + assert.Equal(t, statusInfoList[i].DisplayedStatus, statusString) + } + + statusInfoList = GetStatusInfoList(t.Context(), translation.NewLocale("de-DE")) + assert.Len(t, statusInfoList, 7) + statuses = []string{"Abgebrochen", "Blockiert", "Erfolg", "Fehler", "Laufend", "Übersprungen", "Wartend"} + statusInts = []int{3, 7, 1, 2, 6, 4, 5} + for i, statusString := range statuses { + assert.Equal(t, statusInfoList[i].Status, statusInts[i]) + assert.Equal(t, statusInfoList[i].DisplayedStatus, statusString) + } +} diff --git a/models/actions/run_test.go b/models/actions/run_test.go index 064f3fbcea..c87765ed72 100644 --- a/models/actions/run_test.go +++ b/models/actions/run_test.go @@ -11,15 +11,14 @@ import ( repo_model "forgejo.org/models/repo" "forgejo.org/models/unittest" "forgejo.org/modules/cache" + "forgejo.org/modules/setting" + "forgejo.org/modules/test" "code.forgejo.org/forgejo/runner/v12/act/jobparser" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestGetRunBefore(t *testing.T) { -} - func TestSetConcurrencyGroup(t *testing.T) { run := ActionRun{} run.SetConcurrencyGroup("abc123") @@ -45,6 +44,135 @@ func TestSetDefaultConcurrencyGroup(t *testing.T) { assert.Equal(t, "refs/heads/main_testing_pull_request__auto", run.ConcurrencyGroup) } +func TestGetWorkflowPath(t *testing.T) { + run := ActionRun{ + WorkflowID: "ci.yml", + WorkflowDirectory: ".some/path/to/workflows", + } + assert.Equal(t, ".some/path/to/workflows/ci.yml", run.WorkflowPath()) +} + +func TestGetCommitLink(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + defer test.MockVariableValue(&setting.AppSubURL, "/sub")() + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + run := ActionRun{ + Repo: repo, + CommitSHA: "a356d1f1f82945a039cd16d4ce0137bd55284e77", + } + assert.Equal(t, "/sub/user2/repo1/commit/a356d1f1f82945a039cd16d4ce0137bd55284e77", run.CommitLink()) +} + +func TestIsScheduledRun(t *testing.T) { + scheduledRun := ActionRun{ + CommitSHA: "a356d1f1f82945a039cd16d4ce0137bd55284e77", + TriggerEvent: "schedule", + } + pushRun := ActionRun{ + CommitSHA: "8f9b5c6ab342eb11d7422deecef7195b18058b90", + TriggerEvent: "push", + } + + assert.True(t, scheduledRun.IsScheduledRun()) + assert.False(t, pushRun.IsScheduledRun()) +} + +func TestIsManualRun(t *testing.T) { + manualRunRun := ActionRun{ + CommitSHA: "a356d1f1f82945a039cd16d4ce0137bd55284e77", + TriggerEvent: "workflow_dispatch", + } + pushRun := ActionRun{ + CommitSHA: "8f9b5c6ab342eb11d7422deecef7195b18058b90", + TriggerEvent: "push", + } + + assert.True(t, manualRunRun.IsDispatchedRun()) + assert.False(t, pushRun.IsDispatchedRun()) +} + +func TestActionRun_IsValid(t *testing.T) { + testCases := []struct { + name string + run ActionRun + isValid bool + }{ + { + name: "valid run", + run: ActionRun{}, + isValid: true, + }, + { + name: "with pre-execution error", + run: ActionRun{PreExecutionErrorCode: ErrorCodeIncompleteRunsOnMissingOutput}, + isValid: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + assert.Equal(t, testCase.isValid, testCase.run.IsValid()) + }) + } +} + +func TestActionRun_CanBeRerun(t *testing.T) { + testCases := []struct { + name string + run ActionRun + canBeRerun bool + }{ + { + name: "run with unknown status", + run: ActionRun{Status: StatusUnknown}, + canBeRerun: false, + }, + { + name: "successful run", + run: ActionRun{Status: StatusSuccess}, + canBeRerun: true, + }, + { + name: "failed run", + run: ActionRun{Status: StatusFailure}, + canBeRerun: true, + }, + { + name: "cancelled run", + run: ActionRun{Status: StatusCancelled}, + canBeRerun: true, + }, + { + name: "skipped run", + run: ActionRun{Status: StatusSkipped}, + canBeRerun: true, + }, + { + name: "waiting run", + run: ActionRun{Status: StatusWaiting}, + canBeRerun: false, + }, + { + name: "blocked run", + run: ActionRun{Status: StatusBlocked}, + canBeRerun: false, + }, + { + name: "with pre-execution error", + run: ActionRun{PreExecutionErrorCode: ErrorCodeIncompleteRunsOnMissingOutput, Status: StatusSuccess}, + canBeRerun: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + assert.Equal(t, testCase.canBeRerun, testCase.run.CanBeRerun()) + }) + } +} + func TestRepoNumOpenActions(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) err := cache.Init() @@ -273,6 +401,109 @@ jobs: assert.Equal(t, StatusBlocked, job.Status) } +func TestActionRun_FindOuterWorkflowCall(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + pullRequestPosterID := int64(4) + repoID := int64(10) + pullRequestID := int64(2) + run := &ActionRun{ + RepoID: repoID, + PullRequestID: pullRequestID, + PullRequestPosterID: pullRequestPosterID, + } + + workflowRaw := []byte(` +jobs: + outer-job: + uses: ./.forgejo/workflows/reusable.yml +`) + workflows, err := jobparser.Parse(workflowRaw, false, + jobparser.WithJobOutputs(map[string]map[string]string{}), + jobparser.ExpandLocalReusableWorkflows(func(job *jobparser.Job, path string) ([]byte, error) { + return []byte(` +on: + workflow_call: +jobs: + inner-job-1: + runs-on: debian + steps: [] + inner-job-2: + runs-on: debian + steps: [] +`), nil + })) + require.NoError(t, err) + require.NoError(t, InsertRun(t.Context(), run, workflows)) + + jobs, err := db.Find[ActionRunJob](t.Context(), FindRunJobOptions{RunID: run.ID}) + require.NoError(t, err) + require.Len(t, jobs, 3) + + for _, j := range jobs { + t.Run(j.Name, func(t *testing.T) { + _, err := j.DecodeWorkflowPayload() + require.NoError(t, err) + outer, err := run.FindOuterWorkflowCall(t.Context(), j) + if j.Name == "outer-job" { + require.ErrorContains(t, err, "invalid state for FindOuterWorkflowCall") + } else { + require.NoError(t, err) + require.NotNil(t, outer) + assert.Equal(t, "outer-job", outer.Name) + } + }) + } +} + +func TestActionRun_IncompleteWith(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + pullRequestPosterID := int64(4) + repoID := int64(10) + pullRequestID := int64(2) + runDoesNotNeedApproval := &ActionRun{ + RepoID: repoID, + PullRequestID: pullRequestID, + PullRequestPosterID: pullRequestPosterID, + } + + workflowRaw := []byte(` +jobs: + outer-job: + with: + some_input: ${{ needs.other-job.outputs.some-output }} + uses: ./.forgejo/workflows/reusable.yml +`) + workflows, err := jobparser.Parse(workflowRaw, false, + jobparser.WithJobOutputs(map[string]map[string]string{}), + jobparser.ExpandLocalReusableWorkflows(func(job *jobparser.Job, path string) ([]byte, error) { + return []byte(` +on: + workflow_call: + inputs: + some_input: + type: string +jobs: + inner-job: + runs-on: debian + steps: [] +`), nil + })) + require.NoError(t, err) + require.True(t, workflows[0].IncompleteWith) // must be set for this test scenario to be valid + + require.NoError(t, InsertRun(t.Context(), runDoesNotNeedApproval, workflows)) + + jobs, err := db.Find[ActionRunJob](t.Context(), FindRunJobOptions{RunID: runDoesNotNeedApproval.ID}) + require.NoError(t, err) + require.Len(t, jobs, 1) + job := jobs[0] + + // Expect job with an incomplete with to be StatusBlocked: + assert.Equal(t, StatusBlocked, job.Status) +} + func TestComputeRunStatus(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) @@ -391,3 +622,73 @@ func TestComputeRunStatus(t *testing.T) { assert.Contains(t, columns, "stopped") }) } + +func TestInsertRunJobs(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + pullRequestPosterID := int64(4) + repoID := int64(10) + pullRequestID := int64(2) + actionRun := &ActionRun{ + RepoID: repoID, + PullRequestID: pullRequestID, + PullRequestPosterID: pullRequestPosterID, + CommitSHA: "1421f75bc5474c69fdb1dc176bcb96d381f935dd", + } + + workflowRaw := []byte(` +jobs: + build: + runs-on: fedora + test: + runs-on: debian + steps: [] +`) + jobs, err := jobparser.Parse(workflowRaw, false) + require.NoError(t, err) + + require.NoError(t, InsertRun(t.Context(), actionRun, jobs)) + + insertedJobs, err := db.Find[ActionRunJob](t.Context(), FindRunJobOptions{RunID: actionRun.ID}) + require.NoError(t, err) + require.Len(t, insertedJobs, 2) + + assert.Equal(t, actionRun.ID, insertedJobs[0].RunID) + assert.Equal(t, actionRun.RepoID, insertedJobs[0].RepoID) + assert.Equal(t, actionRun.OwnerID, insertedJobs[0].OwnerID) + assert.Equal(t, actionRun.CommitSHA, insertedJobs[0].CommitSHA) + assert.Equal(t, actionRun.IsForkPullRequest, insertedJobs[0].IsForkPullRequest) + assert.Equal(t, "build", insertedJobs[0].Name) + assert.Equal(t, "build", insertedJobs[0].JobID) + assert.Empty(t, insertedJobs[0].Needs) + assert.Equal(t, []string{"fedora"}, insertedJobs[0].RunsOn) + assert.Equal(t, int64(1), insertedJobs[0].Attempt) + assert.Zero(t, insertedJobs[0].Started) + assert.Zero(t, insertedJobs[0].Stopped) + assert.Zero(t, insertedJobs[0].TaskID) + assert.Equal(t, StatusWaiting, insertedJobs[0].Status) + + assert.Equal(t, actionRun.ID, insertedJobs[1].RunID) + assert.Equal(t, actionRun.RepoID, insertedJobs[1].RepoID) + assert.Equal(t, actionRun.OwnerID, insertedJobs[1].OwnerID) + assert.Equal(t, actionRun.CommitSHA, insertedJobs[1].CommitSHA) + assert.Equal(t, actionRun.IsForkPullRequest, insertedJobs[1].IsForkPullRequest) + assert.Equal(t, "test", insertedJobs[1].Name) + assert.Equal(t, "test", insertedJobs[1].JobID) + assert.Empty(t, insertedJobs[1].Needs) + assert.Equal(t, []string{"debian"}, insertedJobs[1].RunsOn) + assert.Equal(t, int64(1), insertedJobs[1].Attempt) + assert.Zero(t, insertedJobs[1].Started) + assert.Zero(t, insertedJobs[1].Stopped) + assert.Zero(t, insertedJobs[1].TaskID) + assert.Equal(t, StatusWaiting, insertedJobs[1].Status) +} + +func TestActionRunLoadAttributes(t *testing.T) { + run := &ActionRun{ + RepoID: 10, + TriggerUserID: 1000, + } + require.NoError(t, run.LoadAttributes(t.Context())) + assert.Equal(t, "ghost", run.TriggerUser.LowerName) +} diff --git a/models/actions/runner.go b/models/actions/runner.go index 99ae7629a8..f324adec52 100644 --- a/models/actions/runner.go +++ b/models/actions/runner.go @@ -61,6 +61,8 @@ type ActionRunner struct { // Store labels defined in state file (default: .runner file) of `act_runner` AgentLabels []string `xorm:"TEXT"` + // Store if this is a runner that only ever get one single job assigned + Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"` Created timeutil.TimeStamp `xorm:"created"` Updated timeutil.TimeStamp `xorm:"updated"` @@ -125,6 +127,18 @@ func (r *ActionRunner) IsOnline() bool { return false } +func (r *ActionRunner) IsActive() bool { + return r.Status() == runnerv1.RunnerStatus_RUNNER_STATUS_ACTIVE +} + +func (r *ActionRunner) IsIdle() bool { + return r.Status() == runnerv1.RunnerStatus_RUNNER_STATUS_IDLE +} + +func (r *ActionRunner) IsOffline() bool { + return r.Status() == runnerv1.RunnerStatus_RUNNER_STATUS_OFFLINE +} + // Editable checks if the runner is editable by the user func (r *ActionRunner) Editable(ownerID, repoID int64) bool { if ownerID == 0 && repoID == 0 { @@ -138,6 +152,7 @@ func (r *ActionRunner) Editable(ownerID, repoID int64) bool { // LoadAttributes loads the attributes of the runner func (r *ActionRunner) LoadAttributes(ctx context.Context) error { + // nosemgrep: forgejo-logic-suspicious-OwnerID-check (system users are not stored in the database) if r.OwnerID > 0 { var user user_model.User has, err := db.GetEngine(ctx).ID(r.OwnerID).Get(&user) @@ -182,12 +197,12 @@ func init() { type FindRunnerOptions struct { db.ListOptions - RepoID int64 - OwnerID int64 // it will be ignored if RepoID is set - Sort string - Filter string - IsOnline optional.Option[bool] - WithAvailable bool // not only runners belong to, but also runners can be used + RepoID int64 + OwnerID int64 // it will be ignored if RepoID is set + Sort string + Filter string + IsOnline optional.Option[bool] + WithVisible bool // include all runners that are visible to the repository, owner, or instance } func (opts FindRunnerOptions) ToConds() builder.Cond { @@ -195,25 +210,27 @@ func (opts FindRunnerOptions) ToConds() builder.Cond { if opts.RepoID > 0 { c := builder.NewCond().And(builder.Eq{"repo_id": opts.RepoID}) - if opts.WithAvailable { + if opts.WithVisible { c = c.Or(builder.Eq{"owner_id": builder.Select("owner_id").From("repository").Where(builder.Eq{"id": opts.RepoID})}) c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) } cond = cond.And(c) - } else if opts.OwnerID > 0 { // OwnerID is ignored if RepoID is set + } else if opts.OwnerID != 0 { // OwnerID is ignored if RepoID is set c := builder.NewCond().And(builder.Eq{"owner_id": opts.OwnerID}) - if opts.WithAvailable { + if opts.WithVisible { c = c.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) } cond = cond.And(c) + } else if !opts.WithVisible { + cond = cond.And(builder.Eq{"repo_id": 0, "owner_id": 0}) } if opts.Filter != "" { - cond = cond.And(builder.Like{"name", opts.Filter}) + cond = cond.And(builder.Like{"name", opts.Filter}).Or(builder.Like{"uuid", opts.Filter}) } - if opts.IsOnline.Has() { - if opts.IsOnline.Value() { + if has, value := opts.IsOnline.Get(); has { + if value { cond = cond.And(builder.Gt{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) } else { cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) @@ -264,6 +281,31 @@ func GetRunnerByID(ctx context.Context, id int64) (*ActionRunner, error) { return &runner, nil } +// GetVisibleRunnerByID is like GetRunnerByID, but it only finds the runner if it is visible to the given owner or +// repository. If it is not, util.ErrNotExist will be returned even if the runner exists. +func GetVisibleRunnerByID(ctx context.Context, id, ownerID, repoID int64) (*ActionRunner, error) { + query := db.GetEngine(ctx).Where("id=?", id) + + if repoID > 0 { + cond := builder.NewCond().And(builder.Eq{"repo_id": repoID}) + cond = cond.Or(builder.Eq{"owner_id": builder.Select("owner_id").From("repository").Where(builder.Eq{"id": repoID})}) + cond = cond.Or(builder.Eq{"repo_id": 0, "owner_id": 0}) + query = query.And(cond) + } else if ownerID > 0 { // ownerID is ignored if repoID is set + cond := builder.NewCond().And(builder.Eq{"owner_id": ownerID}).Or(builder.Eq{"repo_id": 0, "owner_id": 0}) + query = query.And(cond) + } + + var runner ActionRunner + has, err := query.Get(&runner) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("runner with ID %d: %w", id, util.ErrNotExist) + } + return &runner, nil +} + // UpdateRunner updates runner's information. func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { e := db.GetEngine(ctx) @@ -354,6 +396,13 @@ func FixRunnersWithoutBelongingRepo(ctx context.Context) (int64, error) { return res.RowsAffected() } +// DeleteEphemeralRunner removes the ephemeral runner with the given ID. If the runner with the given ID is not an +// ephemeral runner, nothing happens. +func DeleteEphemeralRunner(ctx context.Context, id int64) error { + _, err := db.GetEngine(ctx).Where(builder.Eq{"id": id, "ephemeral": true}).Delete(&ActionRunner{}) + return err +} + func DeleteOfflineRunners(ctx context.Context, olderThan timeutil.TimeStamp, globalOnly bool) error { log.Info("Doing: DeleteOfflineRunners") diff --git a/models/actions/runner_list.go b/models/actions/runner_list.go index 6a64c46596..4186ee60be 100644 --- a/models/actions/runner_list.go +++ b/models/actions/runner_list.go @@ -28,6 +28,7 @@ func (runners RunnerList) LoadOwners(ctx context.Context) error { return err } for _, runner := range runners { + // nosemgrep: forgejo-logic-suspicious-OwnerID-check (system users are not stored in the database) if runner.OwnerID > 0 && runner.Owner == nil { runner.Owner = users[runner.OwnerID] } diff --git a/models/actions/runner_test.go b/models/actions/runner_test.go index 1916c35a76..31e9706851 100644 --- a/models/actions/runner_test.go +++ b/models/actions/runner_test.go @@ -10,6 +10,7 @@ import ( auth_model "forgejo.org/models/auth" "forgejo.org/models/db" + "forgejo.org/models/repo" "forgejo.org/models/unittest" "forgejo.org/modules/timeutil" @@ -140,3 +141,400 @@ func TestDeleteOfflineRunnersErrorOnInvalidOlderThanValue(t *testing.T) { defer timeutil.MockUnset() require.Error(t, DeleteOfflineRunners(db.DefaultContext, timeutil.TimeStampNow(), false)) } + +func TestRunnerEditable(t *testing.T) { + testCases := []struct { + name string + runner *ActionRunner + ownerID int64 + repoID int64 + editable bool + }{ + { + name: "admin-can-edit-global-runner", + runner: &ActionRunner{Name: "global-runner", OwnerID: 0, RepoID: 0}, + ownerID: 0, + repoID: 0, + editable: true, + }, + { + name: "admin-can-edit-user-runner", + runner: &ActionRunner{Name: "user-runner", OwnerID: 36, RepoID: 0}, + ownerID: 0, + repoID: 0, + editable: true, + }, + { + name: "admin-can-edit-repository-runner", + runner: &ActionRunner{Name: "user-runner", OwnerID: 0, RepoID: 110}, + ownerID: 0, + repoID: 0, + editable: true, + }, + { + name: "user-can-edit-its-runner", + runner: &ActionRunner{Name: "user-runner", OwnerID: 469, RepoID: 0}, + ownerID: 469, + repoID: 0, + editable: true, + }, + { + name: "user-cannot-edit-global-runner", + runner: &ActionRunner{Name: "global-runner", OwnerID: 0, RepoID: 0}, + ownerID: 469, + repoID: 0, + editable: false, + }, + { + name: "user-cannot-edit-other-users-runner", + runner: &ActionRunner{Name: "user-runner", OwnerID: 892, RepoID: 0}, + ownerID: 469, + repoID: 0, + editable: false, + }, + { + name: "user-cannot-edit-repo-runner", + runner: &ActionRunner{Name: "repo-runner", OwnerID: 0, RepoID: 151}, + ownerID: 469, + repoID: 0, + editable: false, + }, + { + name: "repo-can-edit-its-runner", + runner: &ActionRunner{Name: "repo-runner", OwnerID: 0, RepoID: 693}, + ownerID: 0, + repoID: 693, + editable: true, + }, + { + name: "repo-cannot-edit-other-repo-runner", + runner: &ActionRunner{Name: "repo-runner", OwnerID: 0, RepoID: 519}, + ownerID: 0, + repoID: 693, + editable: false, + }, + { + name: "repo-cannot-edit-global-runner", + runner: &ActionRunner{Name: "global-runner", OwnerID: 0, RepoID: 0}, + ownerID: 0, + repoID: 693, + editable: false, + }, + { + name: "repo-cannot-edit-user-runner", + runner: &ActionRunner{Name: "user-runner", OwnerID: 6, RepoID: 0}, + ownerID: 0, + repoID: 693, + editable: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + result := testCase.runner.Editable(testCase.ownerID, testCase.repoID) + assert.Equal(t, testCase.editable, result) + }) + } +} + +func TestRunner_GetVisibleRunnerByID(t *testing.T) { + defer unittest.OverrideFixtures("models/actions/TestRunner_GetVisibleRunnerByID")() + require.NoError(t, unittest.PrepareTestDatabase()) + + repository32 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 32, OwnerID: 3}) + repository1 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1, OwnerID: 2}) + + runner1 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719931, OwnerID: 3, RepoID: 0}) // Owned by org3 + runner2 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719932, OwnerID: 2, RepoID: 0}) // Owned by user2 + runner3 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719933, OwnerID: 0, RepoID: 0}) + runner4 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719934, OwnerID: 0, RepoID: repository32.ID}) + + testCases := []struct { + name string + runner *ActionRunner + ownerID int64 + repoID int64 + expectedError string + }{ + { + name: "Organization runner", + runner: runner1, + ownerID: 3, + repoID: 0, + expectedError: "", + }, + { + name: "Organization runner visible to admins", + runner: runner1, + ownerID: 0, + repoID: 0, + expectedError: "", + }, + { + name: "Organization runner invisible to different owner", + runner: runner1, + ownerID: 2, + repoID: 0, + expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner1.ID), + }, + { + name: "Organization runner visible to its repositories", + runner: runner1, + ownerID: 0, + repoID: repository32.ID, + expectedError: "", + }, + { + name: "Organization runner invisible to repositories owned by somebody else", + runner: runner1, + ownerID: 0, + repoID: repository1.ID, + expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner1.ID), + }, + { + name: "User runner", + runner: runner2, + ownerID: 2, + repoID: 0, + expectedError: "", + }, + { + name: "User runner invisible to different user", + runner: runner2, + ownerID: 1, + repoID: 0, + expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner2.ID), + }, + { + name: "User runner visible to repository owned by user", + runner: runner2, + ownerID: 0, + repoID: repository1.ID, + expectedError: "", + }, + { + name: "User runner invisible to repository owned by different user", + runner: runner2, + ownerID: 0, + repoID: repository32.ID, + expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner2.ID), + }, + { + name: "Global runner", + runner: runner3, + ownerID: 0, + repoID: 0, + expectedError: "", + }, + { + name: "Global runner is visible to any user", + runner: runner3, + ownerID: 2, + repoID: 0, + expectedError: "", + }, + { + name: "Global runner is visible to any repository", + runner: runner3, + ownerID: 0, + repoID: repository32.ID, + expectedError: "", + }, + { + name: "Repository runner", + runner: runner4, + ownerID: 0, + repoID: repository32.ID, + expectedError: "", + }, + { + name: "Repository runner is visible to admins", + runner: runner4, + ownerID: 0, + repoID: 0, + expectedError: "", + }, + { + name: "Repository runner is invisible to repository owner", + runner: runner4, + ownerID: repository32.OwnerID, + repoID: 0, + expectedError: fmt.Sprintf("runner with ID %d: resource does not exist", runner4.ID), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + _, err := GetVisibleRunnerByID(t.Context(), testCase.runner.ID, testCase.ownerID, testCase.repoID) + if testCase.expectedError == "" { + require.NoError(t, err) + } else { + assert.ErrorContains(t, err, testCase.expectedError) + } + }) + } +} + +func TestRunner_FindRunnerOptionsToConds(t *testing.T) { + defer unittest.OverrideFixtures("models/actions/TestRunner_FindRunnerOptionsToConds")() + require.NoError(t, unittest.PrepareTestDatabase()) + + runner1 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719931, OwnerID: 3, RepoID: 0}) // Owned by org3 + runner2 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719932, OwnerID: 2, RepoID: 0}) // Owned by user2 + runner3 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719933, OwnerID: 0, RepoID: 0}) + runner4 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719934, OwnerID: 0, RepoID: 32}) + runner5 := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 719935, OwnerID: 0, RepoID: 36}) + + testCases := []struct { + name string + opts FindRunnerOptions + expectedRunners RunnerList + unexpectedRunners RunnerList + }{ + { + name: "Only runners owned by instance", + opts: FindRunnerOptions{OwnerID: 0, RepoID: 0, WithVisible: false}, + expectedRunners: RunnerList{runner3}, + unexpectedRunners: RunnerList{runner1, runner2, runner4, runner5}, + }, + { + name: "All runners on instance", + opts: FindRunnerOptions{OwnerID: 0, RepoID: 0, WithVisible: true}, + expectedRunners: RunnerList{runner1, runner2, runner3, runner4, runner5}, + unexpectedRunners: RunnerList{}, + }, + { + name: "Only runners owned by organization", + opts: FindRunnerOptions{OwnerID: 3, RepoID: 0, WithVisible: false}, + expectedRunners: RunnerList{runner1}, + unexpectedRunners: RunnerList{runner2, runner3, runner4, runner5}, + }, + { + name: "Runners available to organization", + opts: FindRunnerOptions{OwnerID: 3, RepoID: 0, WithVisible: true}, + expectedRunners: RunnerList{runner1, runner3}, + unexpectedRunners: RunnerList{runner2, runner4, runner5}, + }, + { + name: "Only runners owned by user", + opts: FindRunnerOptions{OwnerID: 2, RepoID: 0, WithVisible: false}, + expectedRunners: RunnerList{runner2}, + unexpectedRunners: RunnerList{runner1, runner3, runner4, runner5}, + }, + { + name: "Runners available to user", + opts: FindRunnerOptions{OwnerID: 2, RepoID: 0, WithVisible: true}, + expectedRunners: RunnerList{runner2, runner3}, + unexpectedRunners: RunnerList{runner1, runner4, runner5}, + }, + { + name: "Only runners owned by organization repository", + opts: FindRunnerOptions{OwnerID: 0, RepoID: 32, WithVisible: false}, + expectedRunners: RunnerList{runner4}, + unexpectedRunners: RunnerList{runner1, runner2, runner3, runner5}, + }, + { + name: "Runners available to organization repository", + opts: FindRunnerOptions{OwnerID: 0, RepoID: 32, WithVisible: true}, + expectedRunners: RunnerList{runner1, runner3, runner4}, + unexpectedRunners: RunnerList{runner2, runner5}, + }, + { + name: "Only runners owned by user repository", + opts: FindRunnerOptions{OwnerID: 0, RepoID: 36, WithVisible: false}, + expectedRunners: RunnerList{runner5}, + unexpectedRunners: RunnerList{runner1, runner2, runner3, runner4}, + }, + { + name: "Runners available to user repository", + opts: FindRunnerOptions{OwnerID: 0, RepoID: 36, WithVisible: true}, + expectedRunners: RunnerList{runner2, runner3, runner5}, + unexpectedRunners: RunnerList{runner1, runner4}, + }, + { + name: "Runners with partially matching name", + opts: FindRunnerOptions{Filter: "er-3"}, + expectedRunners: RunnerList{runner3}, + unexpectedRunners: RunnerList{runner1, runner2, runner4, runner5}, + }, + { + name: "Runners with partially matching UUID", + opts: FindRunnerOptions{Filter: "21f75233798b"}, + expectedRunners: RunnerList{runner4}, + unexpectedRunners: RunnerList{runner1, runner2, runner3, runner5}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + runners, err := db.Find[ActionRunner](t.Context(), testCase.opts) + require.NoError(t, err) + + for _, expectedRunner := range testCase.expectedRunners { + assert.Contains(t, runners, expectedRunner) + } + for _, unexpectedRunner := range testCase.unexpectedRunners { + assert.NotContains(t, runners, unexpectedRunner) + } + }) + } +} + +func TestDeleteEphemeralRunner(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + persistentRunnerOne := &ActionRunner{ + ID: 606526, + UUID: "d53a1222-ae7a-4430-97f8-8fcb6efd04c9", + Name: "persistent-runner-one", + OwnerID: 2, + RepoID: 0, + Ephemeral: false, + TokenHash: "J9YDsQL", + } + persistentRunnerTwo := &ActionRunner{ + ID: 606527, + UUID: "3dc23067-b2fd-4daf-b428-dddad80d7f37", + Name: "persistent-runner-two", + OwnerID: 2, + RepoID: 0, + Ephemeral: false, + TokenHash: "jvIylZtHsS", + } + ephemeralRunnerOne := &ActionRunner{ + ID: 606528, + UUID: "2d9bc0a1-7019-4ed3-ba67-6415415ac2a9", + Name: "ephemeral-runner-one", + OwnerID: 2, + RepoID: 0, + Ephemeral: true, + TokenHash: "t9C8L0kM3W", + } + ephemeralRunnerTwo := &ActionRunner{ + ID: 606529, + UUID: "da7a03f8-ab39-4c54-9ec9-2bd312fe3be1", + Name: "ephemeral-runner-two", + OwnerID: 2, + RepoID: 0, + Ephemeral: true, + TokenHash: "g9oTOFM", + } + + require.NoError(t, CreateRunner(t.Context(), persistentRunnerOne)) + require.NoError(t, CreateRunner(t.Context(), persistentRunnerTwo)) + require.NoError(t, CreateRunner(t.Context(), ephemeralRunnerOne)) + require.NoError(t, CreateRunner(t.Context(), ephemeralRunnerTwo)) + + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: persistentRunnerOne.ID}) + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: persistentRunnerTwo.ID}) + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: ephemeralRunnerOne.ID}) + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: ephemeralRunnerTwo.ID}) + + require.NoError(t, DeleteEphemeralRunner(t.Context(), persistentRunnerOne.ID)) + require.NoError(t, DeleteEphemeralRunner(t.Context(), ephemeralRunnerOne.ID)) + + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: persistentRunnerOne.ID}) + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: persistentRunnerTwo.ID}) + unittest.AssertNotExistsBean(t, &ActionRunner{ID: ephemeralRunnerOne.ID}) + unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: ephemeralRunnerTwo.ID}) +} diff --git a/models/actions/runner_token.go b/models/actions/runner_token.go index ece15b55e8..f789f30ddf 100644 --- a/models/actions/runner_token.go +++ b/models/actions/runner_token.go @@ -10,8 +10,11 @@ import ( "forgejo.org/models/db" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" + "forgejo.org/modules/optional" "forgejo.org/modules/timeutil" "forgejo.org/modules/util" + + "xorm.io/builder" ) // ActionRunnerToken represents runner tokens @@ -29,15 +32,14 @@ import ( type ActionRunnerToken struct { ID int64 Token string `xorm:"UNIQUE"` - OwnerID int64 `xorm:"index"` + OwnerID optional.Option[int64] `xorm:"index REFERENCES(user, id)"` Owner *user_model.User `xorm:"-"` - RepoID int64 `xorm:"index"` + RepoID optional.Option[int64] `xorm:"index REFERENCES(repository, id)"` Repo *repo_model.Repository `xorm:"-"` IsActive bool // true means it can be used Created timeutil.TimeStamp `xorm:"created"` Updated timeutil.TimeStamp `xorm:"updated"` - Deleted timeutil.TimeStamp `xorm:"deleted"` } func init() { @@ -70,11 +72,11 @@ func UpdateRunnerToken(ctx context.Context, r *ActionRunnerToken, cols ...string // NewRunnerToken creates a new active runner token and invalidate all old tokens // ownerID will be ignored and treated as 0 if repoID is non-zero. -func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { - if ownerID != 0 && repoID != 0 { +func NewRunnerToken(ctx context.Context, ownerID, repoID optional.Option[int64]) (*ActionRunnerToken, error) { + if ownerID.Has() && repoID.Has() { // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. // Remove OwnerID to avoid confusion; it's not worth returning an error here. - ownerID = 0 + ownerID = optional.None[int64]() } token := util.CryptoRandomString(util.RandomStringHigh) @@ -86,7 +88,7 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo } return runnerToken, db.WithTx(ctx, func(ctx context.Context) error { - if _, err := db.GetEngine(ctx).Where("owner_id =? AND repo_id = ?", ownerID, repoID).Cols("is_active").Update(&ActionRunnerToken{ + if _, err := db.GetEngine(ctx).Where(runnerTokenCond(ownerID, repoID)).Cols("is_active").Update(&ActionRunnerToken{ IsActive: false, }); err != nil { return err @@ -97,16 +99,32 @@ func NewRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerTo }) } +func runnerTokenCond(ownerID, repoID optional.Option[int64]) builder.Cond { + var condOwnerID builder.Cond + if has, value := ownerID.Get(); !has { + condOwnerID = builder.IsNull{"owner_id"} + } else { + condOwnerID = builder.Eq{"owner_id": value} + } + var condRepoID builder.Cond + if has, value := repoID.Get(); !has { + condRepoID = builder.IsNull{"repo_id"} + } else { + condRepoID = builder.Eq{"repo_id": value} + } + return builder.And(condOwnerID, condRepoID) +} + // GetLatestRunnerToken returns the latest runner token -func GetLatestRunnerToken(ctx context.Context, ownerID, repoID int64) (*ActionRunnerToken, error) { - if ownerID != 0 && repoID != 0 { - // It's trying to get a runner token that belongs to a repository, but OwnerID has been set accidentally. +func GetLatestRunnerToken(ctx context.Context, ownerID, repoID optional.Option[int64]) (*ActionRunnerToken, error) { + if ownerID.Has() && repoID.Has() { + // It's trying to create a runner token that belongs to a repository, but OwnerID has been set accidentally. // Remove OwnerID to avoid confusion; it's not worth returning an error here. - ownerID = 0 + ownerID = optional.None[int64]() } var runnerToken ActionRunnerToken - has, err := db.GetEngine(ctx).Where("owner_id=? AND repo_id=?", ownerID, repoID). + has, err := db.GetEngine(ctx).Where(runnerTokenCond(ownerID, repoID)). OrderBy("id DESC").Get(&runnerToken) if err != nil { return nil, err diff --git a/models/actions/runner_token_test.go b/models/actions/runner_token_test.go index 0de9ca5648..4b69f5488f 100644 --- a/models/actions/runner_token_test.go +++ b/models/actions/runner_token_test.go @@ -8,6 +8,7 @@ import ( "forgejo.org/models/db" "forgejo.org/models/unittest" + "forgejo.org/modules/optional" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,16 +17,16 @@ import ( func TestGetLatestRunnerToken(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3}) - expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) + expectedToken, err := GetLatestRunnerToken(db.DefaultContext, optional.Some[int64](1), optional.None[int64]()) require.NoError(t, err) assert.Equal(t, expectedToken, token) } func TestNewRunnerToken(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) - token, err := NewRunnerToken(db.DefaultContext, 1, 0) + token, err := NewRunnerToken(db.DefaultContext, optional.Some[int64](1), optional.None[int64]()) require.NoError(t, err) - expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) + expectedToken, err := GetLatestRunnerToken(db.DefaultContext, optional.Some[int64](1), optional.None[int64]()) require.NoError(t, err) assert.Equal(t, expectedToken, token) } @@ -34,8 +35,8 @@ func TestUpdateRunnerToken(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) token := unittest.AssertExistsAndLoadBean(t, &ActionRunnerToken{ID: 3}) token.IsActive = true - require.NoError(t, UpdateRunnerToken(db.DefaultContext, token)) - expectedToken, err := GetLatestRunnerToken(db.DefaultContext, 1, 0) + require.NoError(t, UpdateRunnerToken(db.DefaultContext, token, "is_active")) + expectedToken, err := GetLatestRunnerToken(db.DefaultContext, optional.Some[int64](1), optional.None[int64]()) require.NoError(t, err) assert.Equal(t, expectedToken, token) } diff --git a/models/actions/schedule.go b/models/actions/schedule.go index 05c9f15d38..bd6755621b 100644 --- a/models/actions/schedule.go +++ b/models/actions/schedule.go @@ -5,7 +5,6 @@ package actions import ( "context" - "time" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" @@ -21,7 +20,7 @@ import ( type ActionSchedule struct { ID int64 Title string - Specs []string + Specs []*ActionScheduleSpec `xorm:"-"` RepoID int64 `xorm:"index"` Repo *repo_model.Repository `xorm:"-"` OwnerID int64 `xorm:"index"` @@ -73,25 +72,12 @@ func CreateScheduleTask(ctx context.Context, rows []*ActionSchedule) error { return err } - // Loop through each schedule spec and create a new spec row - now := time.Now() - for _, spec := range row.Specs { - specRow := &ActionScheduleSpec{ - RepoID: row.RepoID, - ScheduleID: row.ID, - Spec: spec, - } - // Parse the spec and check for errors - schedule, err := specRow.Parse() - if err != nil { - continue // skip to the next spec if there's an error - } - - specRow.Next = timeutil.TimeStamp(schedule.Next(now).Unix()) + spec.ScheduleID = row.ID + spec.RepoID = row.RepoID // Insert the new schedule spec row - if err = db.Insert(ctx, specRow); err != nil { + if err = db.Insert(ctx, spec); err != nil { return err } } @@ -130,7 +116,7 @@ func (opts FindScheduleOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.OwnerID > 0 { + if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } diff --git a/models/actions/schedule_spec.go b/models/actions/schedule_spec.go index 83bdceb850..864fb5e2db 100644 --- a/models/actions/schedule_spec.go +++ b/models/actions/schedule_spec.go @@ -10,9 +10,10 @@ import ( "forgejo.org/models/db" repo_model "forgejo.org/models/repo" + "forgejo.org/modules/optional" "forgejo.org/modules/timeutil" - "github.com/robfig/cron/v3" + "github.com/gdgvda/cron" ) // ActionScheduleSpec represents a schedule spec of a workflow file @@ -27,36 +28,58 @@ type ActionScheduleSpec struct { // started or this entry's schedule is unsatisfiable Next timeutil.TimeStamp `xorm:"index"` // Prev is the last time this job was run, or the zero time if never. - Prev timeutil.TimeStamp - Spec string + Prev timeutil.TimeStamp + Spec string + TimeZone optional.Option[string] Created timeutil.TimeStamp `xorm:"created"` Updated timeutil.TimeStamp `xorm:"updated"` } +func NewActionScheduleSpec(cron string, tz optional.Option[string], referenceTime time.Time) (*ActionScheduleSpec, error) { + spec := &ActionScheduleSpec{ + Spec: cron, + TimeZone: tz, + } + cronSchedule, err := spec.Parse() + if err != nil { + return nil, err + } + + spec.Next = timeutil.TimeStamp(cronSchedule.Next(referenceTime).Unix()) + return spec, nil +} + // Parse parses the spec and returns a cron.Schedule // Unlike the default cron parser, Parse uses UTC timezone as the default if none is specified. func (s *ActionScheduleSpec) Parse() (cron.Schedule, error) { - parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) + parser, err := cron.NewDefaultParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor) + if err != nil { + return nil, err + } + schedule, err := parser.Parse(s.Spec) if err != nil { return nil, err } - // If the spec has specified a timezone, use it - if strings.HasPrefix(s.Spec, "TZ=") || strings.HasPrefix(s.Spec, "CRON_TZ=") { + // If `timezone` is not defined in the workflow, but the spec includes a timezone, use it. + if !s.TimeZone.Has() && (strings.HasPrefix(s.Spec, "TZ=") || strings.HasPrefix(s.Spec, "CRON_TZ=")) { return schedule, nil } - specSchedule, ok := schedule.(*cron.SpecSchedule) - // If it's not a spec schedule, like "@every 5m", timezone is not relevant - if !ok { - return schedule, nil + var location *time.Location + if present, tz := s.TimeZone.Get(); present { + location, err = time.LoadLocation(tz) + if err != nil { + return nil, err + } + } else { + // UTC is the default time zone. + location = time.UTC } - // Set the timezone to UTC - specSchedule.Location = time.UTC - return specSchedule, nil + return schedule.WithLocation(location), nil } func init() { diff --git a/models/actions/schedule_spec_test.go b/models/actions/schedule_spec_test.go index 0c26fce4b2..6b9db9f39a 100644 --- a/models/actions/schedule_spec_test.go +++ b/models/actions/schedule_spec_test.go @@ -7,10 +7,50 @@ import ( "testing" "time" + "forgejo.org/modules/optional" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestActionScheduleSpec_NewActionScheduleSpec(t *testing.T) { + tests := []struct { + name string + refTime time.Time + cronPattern string + timeZone string + want string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "without timezone", + refTime: time.Date(2026, 4, 6, 11, 56, 0, 0, time.UTC), + cronPattern: "58 14 * * *", + want: "2026-04-06T14:58:00Z", + wantErr: assert.NoError, + }, + { + name: "with separate timezone", + refTime: time.Date(2026, 4, 6, 11, 56, 0, 0, time.UTC), + cronPattern: "58 14 * * *", + timeZone: "Europe/Tallinn", // +03 (EEST) + want: "2026-04-06T11:58:00Z", + wantErr: assert.NoError, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + s, err := NewActionScheduleSpec(test.cronPattern, optional.FromNonDefault(test.timeZone), test.refTime) + test.wantErr(t, err) + + if err == nil { + assert.Equal(t, test.want, s.Next.AsTime().UTC().Format(time.RFC3339)) + } + }) + } +} + func TestActionScheduleSpec_Parse(t *testing.T) { // Mock the local timezone is not UTC local := time.Local @@ -21,50 +61,105 @@ func TestActionScheduleSpec_Parse(t *testing.T) { }() time.Local = tz - now, err := time.Parse(time.RFC3339, "2024-07-31T15:47:55+08:00") - require.NoError(t, err) - tests := []struct { - name string - spec string - want string - wantErr assert.ErrorAssertionFunc + name string + refTime time.Time + spec string + timeZone string + want string + wantErr assert.ErrorAssertionFunc }{ { name: "regular", + refTime: time.Date(2024, 7, 31, 15, 47, 55, 0, time.Local), spec: "0 10 * * *", want: "2024-07-31T10:00:00Z", wantErr: assert.NoError, }, { name: "invalid", + refTime: time.Date(2024, 7, 31, 15, 47, 55, 0, time.Local), spec: "0 10 * *", want: "", wantErr: assert.Error, }, { - name: "with timezone", + name: "with TZ in cron schedule", + refTime: time.Date(2024, 7, 31, 15, 47, 55, 0, time.Local), spec: "TZ=America/New_York 0 10 * * *", want: "2024-07-31T14:00:00Z", wantErr: assert.NoError, }, { - name: "timezone irrelevant", + name: "with CRON_TZ in cron schedule", + refTime: time.Date(2024, 7, 31, 15, 47, 55, 0, time.Local), + spec: "CRON_TZ=America/New_York 0 10 * * *", + want: "2024-07-31T14:00:00Z", + wantErr: assert.NoError, + }, + { + name: "with separate time zone", + refTime: time.Date(2024, 7, 31, 15, 47, 55, 0, time.Local), + spec: "0 10 * * *", + timeZone: "America/New_York", + want: "2024-07-31T14:00:00Z", + wantErr: assert.NoError, + }, + { + name: "separate time zone takes precedence over inlined time zone", + refTime: time.Date(2024, 7, 31, 15, 47, 55, 0, time.Local), + spec: "CRON_TZ=Europe/Berlin 0 10 * * *", + timeZone: "America/New_York", + want: "2024-07-31T14:00:00Z", + wantErr: assert.NoError, + }, + { + name: "time zone irrelevant", + refTime: time.Date(2024, 7, 31, 15, 47, 55, 0, time.Local), spec: "@every 5m", want: "2024-07-31T07:52:55Z", wantErr: assert.NoError, }, + { + // The various cron implementations handle the DST jump forwards differently. The most popular approaches + // are (a) scheduling all jobs at 3 o'clock that were supposed to run between 2 and 3 o'clock, or (b) + // skipping the execution on that day because any time between 2 and 3 o'clock never happened. Forgejo uses + // option B because the code it inherited already did that and was exposed to users. + name: "skips execution during DST jump forwards", + refTime: time.Date(2025, 3, 30, 0, 55, 0, 0, time.UTC), // 01:55 local time + spec: "10 2 * * *", // The clock jumps at 2 o'clock to 3 o'clock. + timeZone: "Europe/Berlin", + want: "2025-03-31T00:10:00Z", + wantErr: assert.NoError, + }, + { + name: "executes a first time before DST jump backwards", + refTime: time.Date(2025, 10, 26, 0, 5, 0, 0, time.UTC), // 02:05 local time + spec: "10 2 * * *", // The clock jumps at 3 o'clock to 2 o'clock. + timeZone: "Europe/Berlin", + want: "2025-10-26T00:10:00Z", + wantErr: assert.NoError, + }, + { + name: "executes a second time after DST jump backwards", + refTime: time.Date(2025, 10, 26, 1, 5, 0, 0, time.UTC), // 02:05 local time + spec: "10 2 * * *", // The clock jumps at 3 o'clock to 2 o'clock. + timeZone: "Europe/Berlin", + want: "2025-10-26T01:10:00Z", + wantErr: assert.NoError, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &ActionScheduleSpec{ - Spec: tt.spec, + Spec: tt.spec, + TimeZone: optional.FromNonDefault(tt.timeZone), } got, err := s.Parse() tt.wantErr(t, err) if err == nil { - assert.Equal(t, tt.want, got.Next(now).UTC().Format(time.RFC3339)) + assert.Equal(t, tt.want, got.Next(tt.refTime).UTC().Format(time.RFC3339)) } }) } diff --git a/models/actions/schedule_test.go b/models/actions/schedule_test.go new file mode 100644 index 0000000000..016185cb42 --- /dev/null +++ b/models/actions/schedule_test.go @@ -0,0 +1,102 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package actions + +import ( + "testing" + "time" + + "forgejo.org/models/db" + "forgejo.org/models/repo" + "forgejo.org/models/unittest" + "forgejo.org/models/user" + "forgejo.org/modules/optional" + "forgejo.org/modules/timeutil" + "forgejo.org/modules/webhook" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScheduleCreateScheduleTask(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + user2 := unittest.AssertExistsAndLoadBean(t, &user.User{ID: 2}) + repo62 := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 62, Name: "test_workflows", OwnerID: user2.ID}) + + content := ` +on: + push: + schedule: + - cron: "2 13 * * *" + - cron: "03 13 * * *" + timezone: Europe/Paris +jobs: + test: + runs-on: debian + steps: + - run: | + echo "OK" +` + + referenceTime := time.Date(2026, 3, 27, 17, 41, 21, 0, time.UTC) + + specWithoutTZ, err := NewActionScheduleSpec("2 13 * * *", optional.None[string](), referenceTime) + require.NoError(t, err) + + specWithTZ, err := NewActionScheduleSpec("3 13 * * *", optional.Some("Europe/Paris"), referenceTime) + require.NoError(t, err) + + schedule := &ActionSchedule{ + Title: ".forgejo/workflows/test.yaml", + Specs: []*ActionScheduleSpec{specWithoutTZ, specWithTZ}, + RepoID: repo62.ID, + OwnerID: user2.ID, + WorkflowID: "test.yaml", + WorkflowDirectory: ".forgejo/workflows", + TriggerUserID: -2, + Ref: "main", + CommitSHA: "6af834a5bc97c1a337eb3a21d26903c5cdceca0c", + Event: webhook.HookEventPush, + EventPayload: "{\"action\":\"schedule\"}", + Content: []byte(content), + } + + err = CreateScheduleTask(t.Context(), []*ActionSchedule{schedule}) + require.NoError(t, err) + + schedules, err := db.Find[ActionSchedule](t.Context(), FindScheduleOptions{OwnerID: user2.ID, RepoID: repo62.ID}) + require.NoError(t, err) + require.Len(t, schedules, 1) + + assert.NotZero(t, schedules[0].ID) + assert.Equal(t, ".forgejo/workflows/test.yaml", schedules[0].Title) + assert.Equal(t, "test.yaml", schedules[0].WorkflowID) + assert.Equal(t, ".forgejo/workflows", schedules[0].WorkflowDirectory) + assert.Equal(t, int64(-2), schedules[0].TriggerUserID) + assert.Equal(t, "main", schedules[0].Ref) + assert.Equal(t, "6af834a5bc97c1a337eb3a21d26903c5cdceca0c", schedules[0].CommitSHA) + assert.Equal(t, webhook.HookEventPush, schedules[0].Event) + assert.JSONEq(t, "{\"action\":\"schedule\"}", schedules[0].EventPayload) + assert.Equal(t, []byte(content), schedules[0].Content) + + specs, total, err := FindSpecs(t.Context(), FindSpecOptions{RepoID: repo62.ID}) + require.NoError(t, err) + + assert.Equal(t, int64(2), total) + + assert.NotZero(t, specs[0].ID) + assert.Equal(t, schedules[0].ID, specs[0].ScheduleID) + assert.Equal(t, timeutil.TimeStamp(1774699380), specs[0].Next) + assert.Equal(t, "3 13 * * *", specs[0].Spec) + assert.Equal(t, optional.Some("Europe/Paris"), specs[0].TimeZone) + assert.Zero(t, specs[0].Prev) + + assert.NotZero(t, specs[1].ID) + assert.Equal(t, schedules[0].ID, specs[1].ScheduleID) + assert.Equal(t, timeutil.TimeStamp(1774702920), specs[1].Next) + assert.Equal(t, "2 13 * * *", specs[1].Spec) + assert.Equal(t, optional.None[string](), specs[1].TimeZone) + assert.Zero(t, specs[1].Prev) +} diff --git a/models/actions/status.go b/models/actions/status.go index 1f6aa5e890..7f06b6231b 100644 --- a/models/actions/status.go +++ b/models/actions/status.go @@ -4,6 +4,8 @@ package actions import ( + "slices" + "forgejo.org/modules/translation" runnerv1 "code.forgejo.org/forgejo/actions-proto/runner/v1" @@ -107,12 +109,7 @@ func (s Status) IsBlocked() bool { // In returns whether s is one of the given statuses func (s Status) In(statuses ...Status) bool { - for _, v := range statuses { - if s == v { - return true - } - } - return false + return slices.Contains(statuses, s) } func (s Status) AsResult() runnerv1.Result { diff --git a/models/actions/task.go b/models/actions/task.go index 1924c816bc..d056c786c4 100644 --- a/models/actions/task.go +++ b/models/actions/task.go @@ -29,7 +29,7 @@ type ActionTask struct { Job *ActionRunJob `xorm:"-"` Steps []*ActionTaskStep `xorm:"-"` Attempt int64 - RunnerID int64 `xorm:"index"` + RunnerID int64 `xorm:"index index(request_key)"` Status Status `xorm:"index"` Started timeutil.TimeStamp `xorm:"index"` Stopped timeutil.TimeStamp `xorm:"index(stopped_log_expired)"` @@ -51,6 +51,15 @@ type ActionTask struct { LogIndexes LogIndexes `xorm:"LONGBLOB"` // line number to offset LogExpired bool `xorm:"index(stopped_log_expired)"` // files that are too old will be deleted + // When the FetchTask() API is invoked to create a task, unpreventable environmental errors may occur; for example, + // network disconnects and timeouts. If that API call has a unique identifier associated with it, it is stored in + // RunnerRequestKey. This allows the API call to be implemented idempotently using this state: if one API call + // assigns a task to a runner and a second API call is received from the same runner with the same request key, the + // existing assigned tasks can be returned. + // + // Indexed for an efficient search on runner_id=? AND runner_request_key=?. + RunnerRequestKey string `xorm:"index(request_key)"` + Created timeutil.TimeStamp `xorm:"created"` Updated timeutil.TimeStamp `xorm:"updated index"` } @@ -147,6 +156,11 @@ func (task *ActionTask) GenerateToken() { task.Token, task.TokenSalt, task.TokenHash, task.TokenLastEight = generateSaltedToken() } +// After using GenerateToken, UpdateToken can be used to update the database record affecting the same columns. +func (task *ActionTask) UpdateToken(ctx context.Context) error { + return UpdateTask(ctx, task, "token_hash", "token_salt", "token_last_eight") +} + // Retrieve all the attempts from the same job as the target `ActionTask`. Limited fields are queried to avoid loading // the LogIndexes blob when not needed. func (task *ActionTask) GetAllAttempts(ctx context.Context) ([]*ActionTask, error) { @@ -174,6 +188,19 @@ func GetTaskByID(ctx context.Context, id int64) (*ActionTask, error) { return &task, nil } +func HasTaskForRunner(ctx context.Context, runnerID int64) (bool, error) { + return db.GetEngine(ctx).Where("runner_id = ?", runnerID).Exist(&ActionTask{}) +} + +func GetTasksOfJob(ctx context.Context, jobID int64) ([]*ActionTask, error) { + var tasks []*ActionTask + err := db.GetEngine(ctx).Where("job_id=?", jobID).Find(&tasks) + if err != nil { + return nil, fmt.Errorf("cannot fetch tasks of job %d: %w", jobID, err) + } + return tasks, nil +} + func GetTaskByJobAttempt(ctx context.Context, jobID, attempt int64) (*ActionTask, error) { var task ActionTask has, err := db.GetEngine(ctx).Where("job_id=?", jobID).Where("attempt=?", attempt).Get(&task) @@ -238,6 +265,15 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro return nil, errNotExist } +func GetTasksByRunnerRequestKey(ctx context.Context, runner *ActionRunner, requestKey string) ([]*ActionTask, error) { + var tasks []*ActionTask + err := db.GetEngine(ctx).Where("runner_id = ? AND runner_request_key = ?", runner.ID, requestKey).Find(&tasks) + if err != nil { + return nil, err + } + return tasks, nil +} + func getConcurrencyCondition() builder.Cond { concurrencyCond := builder.NewCond() @@ -320,12 +356,12 @@ func GetAvailableJobsForRunner(e db.Engine, runner *ActionRunner) ([]*ActionRunJ return jobs, nil } -func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) { - ctx, commiter, err := db.TxContext(ctx) +func CreateTaskForRunner(ctx context.Context, runner *ActionRunner, requestKey, handle *string) (*ActionTask, bool, error) { + ctx, committer, err := db.TxContext(ctx) if err != nil { return nil, false, err } - defer commiter.Close() + defer committer.Close() e := db.GetEngine(ctx) @@ -337,9 +373,9 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask // TODO: a more efficient way to filter labels var job *ActionRunJob log.Trace("runner labels: %v", runner.AgentLabels) - for _, v := range jobs { - if v.ItRunsOn(runner.AgentLabels) { - job = v + for _, j := range jobs { + if j.IsRequestedByRunner(handle) && j.ItRunsOn(runner.AgentLabels) { + job = j break } } @@ -351,7 +387,6 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask } now := timeutil.TimeStampNow() - job.Attempt++ job.Started = now job.Status = StatusRunning @@ -366,6 +401,9 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask CommitSHA: job.CommitSHA, IsForkPullRequest: job.IsForkPullRequest, } + if requestKey != nil { + task.RunnerRequestKey = *requestKey + } task.GenerateToken() var workflowJob *jobparser.Job @@ -414,13 +452,51 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask task.Job = job - if err := commiter.Commit(); err != nil { + if err := committer.Commit(); err != nil { return nil, false, err } return task, true, nil } +// Placeholder tasks are created when the status/content of an [ActionRunJob] is resolved by Forgejo without dispatch to +// a runner, specifically in the case of a workflow call's outer job. +func CreatePlaceholderTask(ctx context.Context, job *ActionRunJob, outputs map[string]string) (*ActionTask, error) { + actionTask := &ActionTask{ + JobID: job.ID, + Attempt: job.Attempt, + Started: timeutil.TimeStampNow(), + Stopped: timeutil.TimeStampNow(), + Status: job.Status, + RepoID: job.RepoID, + OwnerID: job.OwnerID, + CommitSHA: job.CommitSHA, + IsForkPullRequest: job.IsForkPullRequest, + } + // token isn't used on a placeholder task, but generation is needed due to the unique constraint on field TokenHash + actionTask.GenerateToken() + + err := db.WithTx(ctx, func(ctx context.Context) error { + _, err := db.GetEngine(ctx).Insert(actionTask) + if err != nil { + return fmt.Errorf("failure inserting action_task: %w", err) + } + + for key, value := range outputs { + err := InsertTaskOutputIfNotExist(ctx, actionTask.ID, key, value) + if err != nil { + return fmt.Errorf("failure inserting action_task_output %q: %w", key, err) + } + } + + return nil + }) + if err != nil { + return nil, err + } + return actionTask, nil +} + func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { sess := db.GetEngine(ctx).ID(task.ID) if len(cols) > 0 { @@ -430,6 +506,27 @@ func UpdateTask(ctx context.Context, task *ActionTask, cols ...string) error { return err } +// DeleteTask removes the given task including all its steps and outputs. Removing logs and ephemeral runners is the +// caller's responsibility. +func DeleteTask(ctx context.Context, taskID int64) error { + return db.WithTx(ctx, func(ctx context.Context) error { + var err error + _, err = db.GetEngine(ctx).Delete(&ActionTaskStep{TaskID: taskID}) + if err != nil { + return fmt.Errorf("unable to delete steps of task %d: %w", taskID, err) + } + _, err = db.GetEngine(ctx).Delete(&ActionTaskOutput{TaskID: taskID}) + if err != nil { + return fmt.Errorf("unable to delete outputs of task %d: %w", taskID, err) + } + _, err = db.GetEngine(ctx).Delete(&ActionTask{ID: taskID}) + if err != nil { + return fmt.Errorf("unable to delete task %d: %w", taskID, err) + } + return nil + }) +} + func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, limit int) ([]*ActionTask, error) { e := db.GetEngine(ctx) diff --git a/models/actions/task_list.go b/models/actions/task_list.go index 712eada378..4c0e060497 100644 --- a/models/actions/task_list.go +++ b/models/actions/task_list.go @@ -1,4 +1,5 @@ // Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2026 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions @@ -64,13 +65,13 @@ func (opts FindTaskOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.OwnerID > 0 { + if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } if opts.CommitSHA != "" { cond = cond.And(builder.Eq{"commit_sha": opts.CommitSHA}) } - if opts.Status != nil { + if len(opts.Status) > 0 { cond = cond.And(builder.In("status", opts.Status)) } if opts.UpdatedBefore > 0 { @@ -82,11 +83,11 @@ func (opts FindTaskOptions) ToConds() builder.Cond { if opts.RunnerID > 0 { cond = cond.And(builder.Eq{"runner_id": opts.RunnerID}) } - if opts.LogExpired.Has() { - cond = cond.And(builder.Eq{"log_expired": opts.LogExpired.Value()}) + if has, value := opts.LogExpired.Get(); has { + cond = cond.And(builder.Eq{"log_expired": value}) } - if opts.LogInStorage.Has() { - cond = cond.And(builder.Eq{"log_in_storage": opts.LogInStorage.Value()}) + if has, value := opts.LogInStorage.Get(); has { + cond = cond.And(builder.Eq{"log_in_storage": value}) } return cond } diff --git a/models/actions/task_test.go b/models/actions/task_test.go index 73aff17a85..7969f52e73 100644 --- a/models/actions/task_test.go +++ b/models/actions/task_test.go @@ -46,3 +46,56 @@ func TestActionTask_GetTaskByJobAttempt(t *testing.T) { _, err = GetTaskByJobAttempt(t.Context(), 192, 100) assert.ErrorContains(t, err, "task with job_id 192 and attempt 100: resource does not exist") } + +func TestActionTask_CreatePlaceholderTask(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + job := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: 396}) + assert.EqualValues(t, 0, job.TaskID) + + task, err := CreatePlaceholderTask(t.Context(), job, map[string]string{"output1": "value1", "output2": "value2"}) + require.NoError(t, err) + + assert.NotEqualValues(t, 0, task.ID) + assert.Equal(t, job.ID, task.JobID) + assert.EqualValues(t, 0, task.Attempt) + assert.NotEqualValues(t, 0, task.Started) + assert.NotEqualValues(t, 0, task.Stopped) + assert.Equal(t, job.Status, task.Status) + assert.Equal(t, job.RepoID, task.RepoID) + assert.Equal(t, job.OwnerID, task.OwnerID) + assert.Equal(t, job.CommitSHA, task.CommitSHA) + assert.Equal(t, job.IsForkPullRequest, task.IsForkPullRequest) + + taskOutputs, err := FindTaskOutputByTaskID(t.Context(), task.ID) + require.NoError(t, err) + require.Len(t, taskOutputs, 2) + finalOutputs := map[string]string{} + for _, to := range taskOutputs { + finalOutputs[to.OutputKey] = to.OutputValue + } + assert.Equal(t, map[string]string{"output1": "value1", "output2": "value2"}, finalOutputs) +} + +func TestActionTask_GetTasksByRunnerRequestKey(t *testing.T) { + defer unittest.OverrideFixtures("models/actions/TestActionTask_GetTasksByRunnerRequestKey")() + require.NoError(t, unittest.PrepareTestDatabase()) + + runner := unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 12345678}) + + // not matching runner_request_key + tasks, err := GetTasksByRunnerRequestKey(t.Context(), runner, "22288392-2c70-4125-bb01-c7da79fa280c") + require.NoError(t, err) + assert.Empty(t, tasks) + + // matching both runner_id and runner_request_key + tasks, err = GetTasksByRunnerRequestKey(t.Context(), runner, "0a7e017d-4201-4b34-8cf4-de0f431893a4") + require.NoError(t, err) + assert.Len(t, tasks, 2) + + // not matching runner_id + runner = unittest.AssertExistsAndLoadBean(t, &ActionRunner{ID: 10000001}) + tasks, err = GetTasksByRunnerRequestKey(t.Context(), runner, "0a7e017d-4201-4b34-8cf4-de0f431893a4") + require.NoError(t, err) + assert.Empty(t, tasks) +} diff --git a/models/actions/tasks_version.go b/models/actions/tasks_version.go index a5c357888f..8678f052f0 100644 --- a/models/actions/tasks_version.go +++ b/models/actions/tasks_version.go @@ -73,11 +73,11 @@ func increaseTasksVersionByScope(ctx context.Context, ownerID, repoID int64) err } func IncreaseTaskVersion(ctx context.Context, ownerID, repoID int64) error { - ctx, commiter, err := db.TxContext(ctx) + ctx, committer, err := db.TxContext(ctx) if err != nil { return err } - defer commiter.Close() + defer committer.Close() // 1. increase global if err := increaseTasksVersionByScope(ctx, 0, 0); err != nil { @@ -101,5 +101,5 @@ func IncreaseTaskVersion(ctx context.Context, ownerID, repoID int64) error { } } - return commiter.Commit() + return committer.Commit() } diff --git a/models/activities/action.go b/models/activities/action.go index 8fd7709e81..9b67bd67f6 100644 --- a/models/activities/action.go +++ b/models/activities/action.go @@ -132,12 +132,7 @@ func (at ActionType) String() string { } func (at ActionType) InActions(actions ...string) bool { - for _, action := range actions { - if action == at.String() { - return true - } - } - return false + return slices.Contains(actions, at.String()) } // Action represents user operation type and other information to @@ -817,3 +812,36 @@ func FixActionCreatedUnixString(ctx context.Context) (int64, error) { } return 0, nil } + +func (a *Action) IsActionPrivate(ctx context.Context) (bool, error) { + if a.IsPrivate { + return true, nil + } + + a.loadRepo(ctx) + if a.Repo == nil { + return true, repo_model.ErrRepoNotExist{} + } + + repo := a.Repo + err := repo.LoadOwner(ctx) + if err != nil { + return true, err + } + + if repo.IsPrivate || repo.Owner.KeepActivityPrivate || repo.Owner.Visibility != structs.VisibleTypePublic { + return true, nil + } + + a.LoadActUser(ctx) + if a.ActUser == nil { + return true, user_model.ErrUserNotExist{} + } + + user := a.ActUser + if user.KeepActivityPrivate || user.Visibility != structs.VisibleTypePublic { + return true, nil + } + + return false, nil +} diff --git a/models/activities/action_test.go b/models/activities/action_test.go index bfc07f58ae..592fdf71e3 100644 --- a/models/activities/action_test.go +++ b/models/activities/action_test.go @@ -371,3 +371,28 @@ func TestGetIssueInfos(t *testing.T) { assert.Equal(t, test.field3, issueInfos[2]) } } + +func TestIsPrivate(t *testing.T) { + defer unittest.OverrideFixtures("models/activities/fixtures/TestIsPrivate")() + require.NoError(t, unittest.PrepareTestDatabase()) + + tt := []struct { + activityID int64 + private bool + }{ + {1, true}, // private repo + {3, false}, // public activities, public repo + {11, true}, // private activities + } + + for _, test := range tt { + ctx := t.Context() + action, err := activities_model.GetActivityByID(ctx, test.activityID) + require.NoError(t, err) + + private, err := action.IsActionPrivate(ctx) + require.NoError(t, err) + + assert.Equal(t, test.private, private, "action ID: %d", test.activityID) + } +} diff --git a/models/activities/error.go b/models/activities/error.go new file mode 100644 index 0000000000..85896e97d9 --- /dev/null +++ b/models/activities/error.go @@ -0,0 +1,14 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package activities + +import "fmt" + +type ErrActivityPrivate struct { + id int64 +} + +func (err ErrActivityPrivate) Error() string { + return fmt.Sprintf("Activity with id %d is private", err.id) +} diff --git a/models/activities/federated_user_activity.go b/models/activities/federated_user_activity.go index 1ff3a855d0..a9f509e8f7 100644 --- a/models/activities/federated_user_activity.go +++ b/models/activities/federated_user_activity.go @@ -82,7 +82,7 @@ func GetFollowingFeeds(ctx context.Context, actorID int64, opts GetFollowingFeed sess = db.SetSessionPagination(sess, &opts) actions := make([]*FederatedUserActivity, 0, opts.PageSize) - count, err := sess.FindAndCount(&actions) + count, err := sess.Desc("`federated_user_activity`.created").FindAndCount(&actions) if err != nil { return nil, 0, fmt.Errorf("FindAndCount: %w", err) } diff --git a/models/activities/fixtures/TestIsPrivate/action.yml b/models/activities/fixtures/TestIsPrivate/action.yml new file mode 100644 index 0000000000..681cea1939 --- /dev/null +++ b/models/activities/fixtures/TestIsPrivate/action.yml @@ -0,0 +1,7 @@ +- id: 11 + user_id: 44 + op_type: 1 # create repo + act_user_id: 44 # private user activities + repo_id: 60 # public + is_private: false + created_unix: 1680454039 diff --git a/models/activities/fixtures/TestIsPrivate/user.yml b/models/activities/fixtures/TestIsPrivate/user.yml new file mode 100644 index 0000000000..dd7bf1a657 --- /dev/null +++ b/models/activities/fixtures/TestIsPrivate/user.yml @@ -0,0 +1,36 @@ +- id: 44 + lower_name: user44 + name: user44 + full_name: user44 + email: user44@example.com + keep_email_private: false + email_notifications_preference: enabled + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: user44 + type: 0 + salt: ZogKvWdyEx + max_repo_creation: -1 + is_active: true + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: "" + avatar_email: user44@example.com + use_custom_avatar: true + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 0 + num_teams: 0 + num_members: 0 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: true + created_unix: 1672578380 diff --git a/models/activities/notification_list.go b/models/activities/notification_list.go index 02206b1d6b..3f3a48eaa5 100644 --- a/models/activities/notification_list.go +++ b/models/activities/notification_list.go @@ -210,34 +210,9 @@ func (nl NotificationList) LoadRepos(ctx context.Context) (repo_model.Repository } repoIDs := nl.getPendingRepoIDs() - repos := make(map[int64]*repo_model.Repository, len(repoIDs)) - left := len(repoIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", repoIDs[:limit]). - Rows(new(repo_model.Repository)) - if err != nil { - return nil, nil, err - } - - for rows.Next() { - var repo repo_model.Repository - err = rows.Scan(&repo) - if err != nil { - rows.Close() - return nil, nil, err - } - - repos[repo.ID] = &repo - } - _ = rows.Close() - - left -= limit - repoIDs = repoIDs[limit:] + repos, err := db.GetByIDs(ctx, "id", repoIDs, &repo_model.Repository{}) + if err != nil { + return nil, nil, err } failed := []int{} @@ -284,34 +259,9 @@ func (nl NotificationList) LoadIssues(ctx context.Context) ([]int, error) { } issueIDs := nl.getPendingIssueIDs() - issues := make(map[int64]*issues_model.Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", issueIDs[:limit]). - Rows(new(issues_model.Issue)) - if err != nil { - return nil, err - } - - for rows.Next() { - var issue issues_model.Issue - err = rows.Scan(&issue) - if err != nil { - rows.Close() - return nil, err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] + issues, err := db.GetByIDs(ctx, "id", issueIDs, &issues_model.Issue{}) + if err != nil { + return nil, err } failures := []int{} @@ -379,34 +329,9 @@ func (nl NotificationList) LoadUsers(ctx context.Context) ([]int, error) { } userIDs := nl.getUserIDs() - users := make(map[int64]*user_model.User, len(userIDs)) - left := len(userIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", userIDs[:limit]). - Rows(new(user_model.User)) - if err != nil { - return nil, err - } - - for rows.Next() { - var user user_model.User - err = rows.Scan(&user) - if err != nil { - rows.Close() - return nil, err - } - - users[user.ID] = &user - } - _ = rows.Close() - - left -= limit - userIDs = userIDs[limit:] + users, err := db.GetByIDs(ctx, "id", userIDs, &user_model.User{}) + if err != nil { + return nil, err } failures := []int{} @@ -430,34 +355,9 @@ func (nl NotificationList) LoadComments(ctx context.Context) ([]int, error) { } commentIDs := nl.getPendingCommentIDs() - comments := make(map[int64]*issues_model.Comment, len(commentIDs)) - left := len(commentIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", commentIDs[:limit]). - Rows(new(issues_model.Comment)) - if err != nil { - return nil, err - } - - for rows.Next() { - var comment issues_model.Comment - err = rows.Scan(&comment) - if err != nil { - rows.Close() - return nil, err - } - - comments[comment.ID] = &comment - } - _ = rows.Close() - - left -= limit - commentIDs = commentIDs[limit:] + comments, err := db.GetByIDs(ctx, "id", commentIDs, &issues_model.Comment{}) + if err != nil { + return nil, err } failures := []int{} diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go index 3d15c22e19..0b9522be1b 100644 --- a/models/activities/repo_activity.go +++ b/models/activities/repo_activity.go @@ -138,10 +138,7 @@ func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository return v[i].Commits > v[j].Commits }) - cnt := count - if cnt > len(v) { - cnt = len(v) - } + cnt := min(count, len(v)) return v[:cnt], nil } diff --git a/models/asymkey/gpg_key.go b/models/asymkey/gpg_key.go index cae717011e..620e0ef0ae 100644 --- a/models/asymkey/gpg_key.go +++ b/models/asymkey/gpg_key.go @@ -81,7 +81,7 @@ func (opts FindGPGKeyOptions) ToConds() builder.Cond { cond = cond.And(builder.Eq{"primary_key_id": ""}) } - if opts.OwnerID > 0 { + if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } if opts.KeyID != "" { diff --git a/models/asymkey/gpg_key_test.go b/models/asymkey/gpg_key_test.go index d265f438eb..2da3bbe6e0 100644 --- a/models/asymkey/gpg_key_test.go +++ b/models/asymkey/gpg_key_test.go @@ -11,7 +11,6 @@ import ( "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/timeutil" - "forgejo.org/modules/util" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/stretchr/testify/assert" @@ -458,7 +457,7 @@ epiDVQ== func TestTryGetKeyIDFromSignature(t *testing.T) { assert.Empty(t, tryGetKeyIDFromSignature(&packet.Signature{})) assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{ - IssuerKeyId: util.ToPointer(uint64(0x38D1A3EADDBEA9C)), + IssuerKeyId: new(uint64(0x38D1A3EADDBEA9C)), })) assert.Equal(t, "038D1A3EADDBEA9C", tryGetKeyIDFromSignature(&packet.Signature{ IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c}, diff --git a/models/asymkey/lint-locale-usage/llu.go b/models/asymkey/lint-locale-usage/llu.go new file mode 100644 index 0000000000..2d6a02ff44 --- /dev/null +++ b/models/asymkey/lint-locale-usage/llu.go @@ -0,0 +1,51 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package lintLocaleUsage + +import ( + "go/ast" + "go/token" + + llu "forgejo.org/build/lint-locale-usage" +) + +// special case: models/asymkey/*.go, +// +// handle &ObjectVerification{...} +func HandleCompositeErrorReason(handler llu.Handler, fset *token.FileSet, n *ast.CompositeLit) { + ident, ok := n.Type.(*ast.Ident) + if !ok || ident.Name != "ObjectVerification" { + return + } + + // fields are normally named + var reason ast.Expr + verified := false + for _, i := range n.Elts { + if kve, ok := i.(*ast.KeyValueExpr); ok { + ident, ok = kve.Key.(*ast.Ident) + if !ok { + continue + } + switch ident.Name { + case "Reason": + reason = kve.Value + case "Verified": + if valueIdent, ok := kve.Value.(*ast.Ident); ok { + switch valueIdent.Name { + case "true": + verified = true + case "false": + verified = false + } + } + } + } else { + handler.OnWarning(fset, i.Pos(), "unable to parse ObjectVerification field assignment") + } + } + if !verified && reason != nil { + handler.HandleGoTrArgument(fset, reason, "") + } +} diff --git a/models/asymkey/ssh_key.go b/models/asymkey/ssh_key.go index 7f76009e7f..ea05195b5d 100644 --- a/models/asymkey/ssh_key.go +++ b/models/asymkey/ssh_key.go @@ -190,7 +190,7 @@ type FindPublicKeyOptions struct { func (opts FindPublicKeyOptions) ToConds() builder.Cond { cond := builder.NewCond() - if opts.OwnerID > 0 { + if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } if opts.Fingerprint != "" { diff --git a/models/asymkey/ssh_key_object_verification_test.go b/models/asymkey/ssh_key_object_verification_test.go index 4bfd79d56e..27d8d66a3c 100644 --- a/models/asymkey/ssh_key_object_verification_test.go +++ b/models/asymkey/ssh_key_object_verification_test.go @@ -24,14 +24,14 @@ func TestParseCommitWithSSHSignature(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) sshKey := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1000, OwnerID: 2}) - t.Run("No commiter", func(t *testing.T) { + t.Run("No committer", func(t *testing.T) { o := commitToGitObject(&git.Commit{}) commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, &user_model.User{}) assert.False(t, commitVerification.Verified) assert.Equal(t, NoKeyFound, commitVerification.Reason) }) - t.Run("Commiter without keys", func(t *testing.T) { + t.Run("Committer without keys", func(t *testing.T) { user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) o := commitToGitObject(&git.Commit{Committer: &git.Signature{Email: user.Email}}) diff --git a/models/auth/TestGetRepositoriesAccessibleWithIntegration/authorized_integ_resource_repo.yml b/models/auth/TestGetRepositoriesAccessibleWithIntegration/authorized_integ_resource_repo.yml new file mode 100644 index 0000000000..5e0ccf1130 --- /dev/null +++ b/models/auth/TestGetRepositoriesAccessibleWithIntegration/authorized_integ_resource_repo.yml @@ -0,0 +1,9 @@ +- integ_id: 1 + repo_id: 1 + created_unix: 1772158384 +- integ_id: 1 + repo_id: 2 + created_unix: 1772158384 +- integ_id: 1 + repo_id: 3 + created_unix: 1772158384 diff --git a/models/auth/TestGetRepositoriesAccessibleWithIntegration/authorized_integration.yml b/models/auth/TestGetRepositoriesAccessibleWithIntegration/authorized_integration.yml new file mode 100644 index 0000000000..18e5e68054 --- /dev/null +++ b/models/auth/TestGetRepositoriesAccessibleWithIntegration/authorized_integration.yml @@ -0,0 +1,9 @@ +- id: 1 + user_id: 2 + scope: all + resource_all_repos: false + issuer: https://example.org/ + audience: https://forgejo.example.org/-/user/integration/abcdef123 + claim_rules: "{}" + created_unix: 1777153359 + updated_unix: 1777153359 diff --git a/models/auth/TestGetRepositoriesAccessibleWithToken/access_token_resource_repo.yml b/models/auth/TestGetRepositoriesAccessibleWithToken/access_token_resource_repo.yml new file mode 100644 index 0000000000..e3266a8c91 --- /dev/null +++ b/models/auth/TestGetRepositoriesAccessibleWithToken/access_token_resource_repo.yml @@ -0,0 +1,9 @@ +- token_id: 3 + repo_id: 1 + created_unix: 1772158384 +- token_id: 3 + repo_id: 2 + created_unix: 1772158384 +- token_id: 3 + repo_id: 3 + created_unix: 1772158384 diff --git a/models/auth/TestGetRepositoriesAccessibleWithTokens/access_token_resource_repo.yml b/models/auth/TestGetRepositoriesAccessibleWithTokens/access_token_resource_repo.yml new file mode 100644 index 0000000000..1f1a552361 --- /dev/null +++ b/models/auth/TestGetRepositoriesAccessibleWithTokens/access_token_resource_repo.yml @@ -0,0 +1,17 @@ +- token_id: 3 + repo_id: 1 + created_unix: 1772158384 +- token_id: 3 + repo_id: 2 + created_unix: 1772158384 +- token_id: 3 + repo_id: 3 + created_unix: 1772158384 + +- token_id: 2 + repo_id: 1 + created_unix: 1772158384 + # (no repo 2 for token 2) +- token_id: 2 + repo_id: 3 + created_unix: 1772158384 diff --git a/models/auth/access_token.go b/models/auth/access_token.go index ceade7dbad..3311769573 100644 --- a/models/auth/access_token.go +++ b/models/auth/access_token.go @@ -73,6 +73,8 @@ type AccessToken struct { UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` HasRecentActivity bool `xorm:"-"` HasUsed bool `xorm:"-"` + + ResourceAllRepos bool `xorm:"NOT NULL DEFAULT TRUE"` // flag for whether AccessTokenResourceRepo instances will limit the resources this access token can access (false) or won't limit them (true). } // AfterLoad is invoked from XORM after setting the values of all fields of this object. @@ -222,15 +224,23 @@ func (opts ListAccessTokensOptions) ToOrders() string { // DeleteAccessTokenByID deletes access token by given ID. func DeleteAccessTokenByID(ctx context.Context, id, userID int64) error { - cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{ - UID: userID, + return db.WithTx(ctx, func(ctx context.Context) error { + if err := db.DeleteBeans(ctx, + &AccessTokenResourceRepo{TokenID: id}, + ); err != nil { + return fmt.Errorf("DeleteBeans: %w", err) + } + + cnt, err := db.GetEngine(ctx).ID(id).Delete(&AccessToken{ + UID: userID, + }) + if err != nil { + return err + } else if cnt != 1 { + return ErrAccessTokenNotExist{} + } + return nil }) - if err != nil { - return err - } else if cnt != 1 { - return ErrAccessTokenNotExist{} - } - return nil } // RegenerateAccessTokenByID regenerates access token by given ID. diff --git a/models/auth/access_token_resource.go b/models/auth/access_token_resource.go new file mode 100644 index 0000000000..85978dd131 --- /dev/null +++ b/models/auth/access_token_resource.go @@ -0,0 +1,72 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package auth + +import ( + "context" + + "forgejo.org/models/db" + "forgejo.org/modules/timeutil" +) + +// Represents a many-to-many join table which indicates specific repositories (RepoID) that can be accessed by an access +// token (TokenID). An access token's ResourceAllRepos field must be false for records in this table to become active. +type AccessTokenResourceRepo struct { + ID int64 `xorm:"pk autoincr"` + TokenID int64 `xorm:"NOT NULL REFERENCES(access_token, id)"` // needs to be shortened from "AccessTokenID" for the index to fit MySQL table identifier length restrictions + RepoID int64 `xorm:"NOT NULL REFERENCES(repository, id)"` + + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` +} + +func init() { + db.RegisterModel(new(AccessTokenResourceRepo)) +} + +func (atr *AccessTokenResourceRepo) GetTargetRepoID() int64 { + return atr.RepoID +} + +func GetRepositoriesAccessibleWithToken(ctx context.Context, accessTokenID int64) ([]*AccessTokenResourceRepo, error) { + var resources []*AccessTokenResourceRepo + err := db.GetEngine(ctx). + Where("token_id = ?", accessTokenID). + Find(&resources) + if err != nil { + return nil, err + } + return resources, nil +} + +func GetRepositoriesAccessibleWithTokens(ctx context.Context, accessTokens []*AccessToken) (map[int64][]*AccessTokenResourceRepo, error) { + accessTokenIDs := make([]int64, len(accessTokens)) + for i, at := range accessTokens { + accessTokenIDs[i] = at.ID + } + + var resources []*AccessTokenResourceRepo + err := db.GetEngine(ctx). + In("token_id", accessTokenIDs). + Find(&resources) + if err != nil { + return nil, err + } + retval := make(map[int64][]*AccessTokenResourceRepo) + for _, resource := range resources { + retval[resource.TokenID] = append(retval[resource.TokenID], resource) + } + return retval, nil +} + +func InsertAccessTokenResourceRepos(ctx context.Context, accessTokenID int64, resources []*AccessTokenResourceRepo) error { + return db.WithTx(ctx, func(ctx context.Context) error { + for _, resourceRepo := range resources { + resourceRepo.TokenID = accessTokenID + if err := db.Insert(ctx, resourceRepo); err != nil { + return err + } + } + return nil + }) +} diff --git a/models/auth/access_token_resource_test.go b/models/auth/access_token_resource_test.go new file mode 100644 index 0000000000..d8cd4e286a --- /dev/null +++ b/models/auth/access_token_resource_test.go @@ -0,0 +1,123 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package auth_test + +import ( + "testing" + + auth_model "forgejo.org/models/auth" + "forgejo.org/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetRepositoriesAccessibleWithToken(t *testing.T) { + defer unittest.OverrideFixtures("models/auth/TestGetRepositoriesAccessibleWithToken")() + require.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("No Resources", func(t *testing.T) { + resources, err := auth_model.GetRepositoriesAccessibleWithToken(t.Context(), 999) + require.NoError(t, err) + assert.Empty(t, resources) + }) + + t.Run("Has Resources", func(t *testing.T) { + resources, err := auth_model.GetRepositoriesAccessibleWithToken(t.Context(), 3) + require.NoError(t, err) + require.Len(t, resources, 3) + + // Verify all expected repo IDs are present + repoIDs := make([]int64, len(resources)) + for i, res := range resources { + repoIDs[i] = res.RepoID + } + assert.Contains(t, repoIDs, int64(1)) + assert.Contains(t, repoIDs, int64(2)) + assert.Contains(t, repoIDs, int64(3)) + }) +} + +func TestGetRepositoriesAccessibleWithTokens(t *testing.T) { + defer unittest.OverrideFixtures("models/auth/TestGetRepositoriesAccessibleWithTokens")() + require.NoError(t, unittest.PrepareTestDatabase()) + + token1 := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{ID: 1}) + token2 := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{ID: 2}) + token3 := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{ID: 3}) + + t.Run("No Tokens", func(t *testing.T) { + resources, err := auth_model.GetRepositoriesAccessibleWithTokens(t.Context(), []*auth_model.AccessToken{}) + require.NoError(t, err) + assert.Empty(t, resources) + }) + + t.Run("Multiple Access Tokens", func(t *testing.T) { + resources, err := auth_model.GetRepositoriesAccessibleWithTokens(t.Context(), []*auth_model.AccessToken{token1, token2, token3}) + require.NoError(t, err) + + repos1, ok := resources[token1.ID] + assert.False(t, ok) + assert.Empty(t, repos1) + + repos2, ok := resources[token2.ID] + assert.True(t, ok) + assert.Len(t, repos2, 2) + + repos3, ok := resources[token3.ID] + assert.True(t, ok) + assert.Len(t, repos3, 3) + }) +} + +func TestInsertAccessTokenResourceRepos(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + token1 := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{ID: 1}) + token2 := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{ID: 2}) + token3 := unittest.AssertExistsAndLoadBean(t, &auth_model.AccessToken{ID: 3}) + + t.Run("blank insert", func(t *testing.T) { + err := auth_model.InsertAccessTokenResourceRepos(t.Context(), token1.ID, nil) + require.NoError(t, err) + }) + + t.Run("multiple insert", func(t *testing.T) { + resRepo1 := &auth_model.AccessTokenResourceRepo{ + TokenID: token2.ID, + RepoID: 1, + } + resRepo3 := &auth_model.AccessTokenResourceRepo{ + TokenID: token2.ID, + RepoID: 3, + } + err := auth_model.InsertAccessTokenResourceRepos(t.Context(), token2.ID, + []*auth_model.AccessTokenResourceRepo{resRepo1, resRepo3}) + require.NoError(t, err) + + unittest.AssertCount(t, &auth_model.AccessTokenResourceRepo{TokenID: token2.ID}, 2) + }) + + t.Run("in tx", func(t *testing.T) { + // Pre-condition: count is 0. + unittest.AssertCount(t, &auth_model.AccessTokenResourceRepo{TokenID: token3.ID}, 0) + + // Verify that InsertAccessTokenResourceRepos performs inserts in a TX by having a second one with an invalid + // RepoID, causing a foreign key violation + resRepo1 := &auth_model.AccessTokenResourceRepo{ + TokenID: token3.ID, + RepoID: 1, + } + resRepo3 := &auth_model.AccessTokenResourceRepo{ + TokenID: token3.ID, + RepoID: 30000, // invalid + } + err := auth_model.InsertAccessTokenResourceRepos(t.Context(), token3.ID, + []*auth_model.AccessTokenResourceRepo{resRepo1, resRepo3}) + require.ErrorContains(t, err, "foreign key") + + // Count remains 0; the first record was not inserted. + unittest.AssertCount(t, &auth_model.AccessTokenResourceRepo{TokenID: token3.ID}, 0) + }) +} diff --git a/models/auth/access_token_scope.go b/models/auth/access_token_scope.go index d14838cf02..6dc143b122 100644 --- a/models/auth/access_token_scope.go +++ b/models/auth/access_token_scope.go @@ -5,6 +5,7 @@ package auth import ( "fmt" + "slices" "strings" "forgejo.org/models/perm" @@ -204,12 +205,7 @@ func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTok // ContainsCategory checks if a list of categories contains a specific category func ContainsCategory(categories []AccessTokenScopeCategory, category AccessTokenScopeCategory) bool { - for _, c := range categories { - if c == category { - return true - } - } - return false + return slices.Contains(categories, category) } // GetScopeLevelFromAccessMode converts permission access mode to scope level diff --git a/models/auth/authorized_integration.go b/models/auth/authorized_integration.go new file mode 100644 index 0000000000..61f0725efb --- /dev/null +++ b/models/auth/authorized_integration.go @@ -0,0 +1,189 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package auth + +import ( + "context" + "errors" + "fmt" + "time" + + "forgejo.org/models/db" + "forgejo.org/modules/timeutil" + "forgejo.org/modules/util" + + gouuid "github.com/google/uuid" + "xorm.io/builder" +) + +// An Authorized Integration allow users to define external systems which can generate JSON Web Tokens (JWTs) that +// Forgejo will trust in order to perform API access on behalf of a user defined by the UserID field. +// +// When a JWT is received by Forgejo, the issuer (iss) and audience (aud) claims are used to lookup an authorized +// integration with an exact match. Together these fields serve as a unique key for the authorized issuer. Duplicates +// cannot be permitted because we would not know which user to authenticate the JWT as. +type AuthorizedIntegration struct { + ID int64 `xorm:"pk autoincr"` + + UserID int64 `xorm:"NOT NULL REFERENCES(user, id)"` + Scope AccessTokenScope `xorm:"NOT NULL"` + ResourceAllRepos bool `xorm:"NOT NULL"` // flag for whether AuthorizedIntegrationResourceRepo instances will limit the resources this access token can access (false) or won't limit them (true). + + Name string // short name for lists of authorized integrations + Description string `xorm:"LONGTEXT"` // long description, optional to document relevant details of the integration + + // Exact-match `iss` claim of the JWT + Issuer string `xorm:"NOT NULL UNIQUE(s)"` + // Exact-match `aud` claim of the JWT + Audience string `xorm:"NOT NULL UNIQUE(s)"` + ClaimRules *ClaimRules `xorm:"NOT NULL JSON"` + + CreatedUnix timeutil.TimeStamp `xorm:"NOT NULL created"` + UpdatedUnix timeutil.TimeStamp `xorm:"NOT NULL updated"` +} + +func init() { + db.RegisterModel(new(AuthorizedIntegration)) +} + +// An [AuthorizedIntegration] can validate the claims in a JWT against a set of rules defined by this structure. +// +// JWTs can contain any number of claims, which are represented as a JSON object. A small number of common claims are +// described in RFC7519 (sec 4.1) which defines JWTs, but most claims are entirely arbitrarily defined by the JWT +// issuer. +// +// For example, eg. a claim may be {"sub": "repo:coolguy/forgejo-runner-testrepo:pull_request"} indicating that an OIDC +// token was received from an Actions execution in a specific repo on a specific event. +// +// Validating the claims from a JWT issuer is a critical part of creating a secure [AuthorizedIssuer]. For example, +// assume that we receive a JWT from a public hosting platform like Codeberg. We will validate that it is a claim +// created by the correct Issuer, Codeberg -- but anyone can do that through Forgejo Actions. We will validate that it +// has the correct audience -- but that's an *input* to Forgejo Actions, so anyone can create a claim on Codeberg with +// an arbitrary audience. The rest of the claims contain the critical information about who ran a Forgejo Action, on +// which repository, and in response to which events, and those must be validated to ensure that an authorized issuer is +// correctly authorized. +// +// Following that an example, a minimum claim rule that would be required for securely using Forgejo Actions would be +// something like: +// +// { +// "rules": [{ +// "claim": "sub", +// "comparison": "eq", +// "value": "repo:forgejo/website:pull_request" +// }] +// } +// +// This defines a single rule which says that the `sub` claim must be exactly equal to +// "repo:forgejo/website:pull_request". Forgejo Actions would generate this subject when an Action is running on the +// repo forgejo/website in response to the pull_request event. +// +// Some JWT claims are JSON objects. The [ClaimNested] comparison operator can be used to define rules that inspect the +// object within a claim. For example, AWS STS generates a claim "https://sts.amazonaws.com/": {...} with values inside +// an object, like "aws_account". A nested claim can inspect those values: +// +// { +// "rules":[{ +// "claim": "https://sts.amazonaws.com/", +// "compare": "nest", +// "nested": {"rules":[ +// {"claim": "aws_account", "compare": "eq", "value": "1234567890"}, +// {"claim": "lambda_source_function_arn", "compare": "eq", "value": "arn:aws:lambda:ca-central-1:1234567890:function:forgejo-oidc-accepting-test"} +// ]} +// } +// +// ]} +// +// This defines a rule that looks into the "https://sts..." claim and verifies the "aws_account" and +// "lambda_source_function_arn" keys match specific known values. +type ClaimRules struct { + Rules []ClaimRule `json:"rules"` +} + +// Defines a single rule that will check the value of one JWT claim. +type ClaimRule struct { + // The target claim, eg. "sub" + Claim string `json:"claim"` + // Comparison rule to use on this claim + Comparison ClaimComparison `json:"compare"` + + // For Comparison of ClaimEqual or ClaimGlob, the specific value or glob to match against + Value string `json:"value,omitempty"` + + // For Comparison of ClaimIn or ClaimGlobIn, an array of values to match against + Values []string `json:"values,omitempty"` + + // For ClaimNested, the rules to apply to the nested object + Nested *ClaimRules `json:"nested,omitempty"` +} + +type ClaimComparison string + +const ( + ClaimEqual ClaimComparison = "eq" // exactly equal claim + ClaimIn ClaimComparison = "in" // exactly equal any of the options in a list + ClaimGlob ClaimComparison = "glob" // glob match complete claim string + ClaimGlobIn ClaimComparison = "glob-in" // glob match any of the options in a list + ClaimNested ClaimComparison = "nest" // recurse into a claim that is an map[string]any with it's own data fields +) + +func GetAuthorizedIntegration(ctx context.Context, issuer, audience string) (*AuthorizedIntegration, error) { + var ai AuthorizedIntegration + found, err := db.GetEngine(ctx).Where("issuer = ? AND audience = ?", issuer, audience).Get(&ai) + if err != nil { + return nil, err + } else if !found { + return nil, util.ErrNotExist + } + return &ai, nil +} + +func InsertAuthorizedIntegration(ctx context.Context, ai *AuthorizedIntegration) error { + if ai.Audience != "" { + return errors.New("audience cannot be provided, and must be generated by NewAuthorizedIntegration") + } else if err := ai.generateAudience(); err != nil { + return err + } + _, err := db.GetEngine(ctx).Insert(ai) + return err +} + +// Bump the UpdatedUnix field of this authorized integration to now, tracking when it was last used for authentication. +// To reduce database write workload, this is only tracked by one-minute intervals -- the UPDATE statement conditionally +// avoids writes. +func (ai *AuthorizedIntegration) UpdateLastUsed(ctx context.Context) error { + newTime := timeutil.TimeStampNow() + cnt, err := db.GetEngine(ctx). + Table(&AuthorizedIntegration{}). + Where(builder.Eq{"id": ai.ID}). + Where(builder.Lt{"updated_unix": newTime.AddDuration(-1 * time.Minute)}). + NoAutoTime(). + Update(map[string]any{"updated_unix": newTime}) + if cnt == 1 { + ai.UpdatedUnix = newTime + } + return err +} + +// Generates the `aud` claim that the remote JWT generator must use to match this authorized integration. The `aud` +// claim is an arbitrary value in a JWT claim, but Forgejo is faced with a few hard and soft requirements: +// +// - Hard requirement: each authorized integration must have a unique `aud`, as it is used to find the DB record that +// authenticates a request. +// - If authentication is failing, being able to inspect the `aud` claim can be useful to identify the intent. +// - Inspection should have a stable meaning -- eg. if it included the username, and the user was renamed, the `aud` +// value which can't be changed would continue to reference the old username causing confusion when inspecting it. +// - Forgejo & GitHub Actions uses a URL $ACTIONS_ID_TOKEN_REQUEST_URL&audience=... to generate a JWT for the running +// action, so it should only consist of safe characters for URL encoding. +// - It should be relatively short, as it's encoded into the JWT and increases its size. +// +// Meeting these requirements decently well is a combination of the owner's ID, a guid, and a "u:" prefix that makes the +// fact that it's an `aud` claim value a little bit identifiable. +func (ai *AuthorizedIntegration) generateAudience() error { + if ai.UserID == 0 { + return errors.New("UserID must be initialized") + } + ai.Audience = fmt.Sprintf("u:%d:%s", ai.UserID, gouuid.New().String()) + return nil +} diff --git a/models/auth/authorized_integration_resource_repo.go b/models/auth/authorized_integration_resource_repo.go new file mode 100644 index 0000000000..77445b60ae --- /dev/null +++ b/models/auth/authorized_integration_resource_repo.go @@ -0,0 +1,56 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package auth + +import ( + "context" + + "forgejo.org/models/db" + "forgejo.org/modules/timeutil" +) + +// Represents a many-to-many join table which indicates specific repositories (RepoID) that can be accessed by an +// authorized integration (IntegID). An authorized integrations's ResourceAllRepos field must be false for records in +// this table to become active. +// +// Model name is shortend (from AuthorizedIntegrationResourceRepo) to accomodate recreate-tables + MySQL, where the +// "tmp_recreate_" + foreign key index name would exceed the max identifier length. +type AuthorizedIntegResourceRepo struct { + ID int64 `xorm:"pk autoincr"` + IntegID int64 `xorm:"NOT NULL REFERENCES(authorized_integration, id)"` // field name shortened (AuthorizationIntegrationID) for max identifier length + RepoID int64 `xorm:"NOT NULL REFERENCES(repository, id)"` + + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` +} + +func init() { + db.RegisterModel(new(AuthorizedIntegResourceRepo)) +} + +func (air *AuthorizedIntegResourceRepo) GetTargetRepoID() int64 { + return air.RepoID +} + +func GetRepositoriesAccessibleWithIntegration(ctx context.Context, aiID int64) ([]*AuthorizedIntegResourceRepo, error) { + var resources []*AuthorizedIntegResourceRepo + err := db.GetEngine(ctx). + Where("integ_id = ?", aiID). + Find(&resources) + if err != nil { + return nil, err + } + return resources, nil +} + +func InsertAuthorizedIntegrationResourceRepos(ctx context.Context, aiID int64, resources []*AuthorizedIntegResourceRepo) error { + return db.WithTx(ctx, func(ctx context.Context) error { + for _, resourceRepo := range resources { + resourceRepo.IntegID = aiID + if err := db.Insert(ctx, resourceRepo); err != nil { + return err + } + } + return nil + }) +} diff --git a/models/auth/authorized_integration_resource_repo_test.go b/models/auth/authorized_integration_resource_repo_test.go new file mode 100644 index 0000000000..15c5b4854f --- /dev/null +++ b/models/auth/authorized_integration_resource_repo_test.go @@ -0,0 +1,91 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package auth_test + +import ( + "testing" + + auth_model "forgejo.org/models/auth" + "forgejo.org/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetRepositoriesAccessibleWithIntegration(t *testing.T) { + defer unittest.OverrideFixtures("models/auth/TestGetRepositoriesAccessibleWithIntegration")() + require.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("No Resources", func(t *testing.T) { + resources, err := auth_model.GetRepositoriesAccessibleWithIntegration(t.Context(), 999) + require.NoError(t, err) + assert.Empty(t, resources) + }) + + t.Run("Has Resources", func(t *testing.T) { + resources, err := auth_model.GetRepositoriesAccessibleWithIntegration(t.Context(), 1) + require.NoError(t, err) + require.Len(t, resources, 3) + + // Verify all expected repo IDs are present + repoIDs := make([]int64, len(resources)) + for i, res := range resources { + repoIDs[i] = res.RepoID + } + assert.Contains(t, repoIDs, int64(1)) + assert.Contains(t, repoIDs, int64(2)) + assert.Contains(t, repoIDs, int64(3)) + }) +} + +func TestInsertAuthorizedIntegration(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + ai1 := makeAuthorizedIntegration(t) + ai2 := makeAuthorizedIntegration(t) + ai3 := makeAuthorizedIntegration(t) + + t.Run("blank insert", func(t *testing.T) { + err := auth_model.InsertAuthorizedIntegrationResourceRepos(t.Context(), ai1.ID, nil) + require.NoError(t, err) + }) + + t.Run("multiple insert", func(t *testing.T) { + resRepo1 := &auth_model.AuthorizedIntegResourceRepo{ + IntegID: ai2.ID, + RepoID: 1, + } + resRepo3 := &auth_model.AuthorizedIntegResourceRepo{ + IntegID: ai2.ID, + RepoID: 3, + } + err := auth_model.InsertAuthorizedIntegrationResourceRepos(t.Context(), ai2.ID, + []*auth_model.AuthorizedIntegResourceRepo{resRepo1, resRepo3}) + require.NoError(t, err) + + unittest.AssertCount(t, &auth_model.AuthorizedIntegResourceRepo{IntegID: ai2.ID}, 2) + }) + + t.Run("in tx", func(t *testing.T) { + // Pre-condition: count is 0. + unittest.AssertCount(t, &auth_model.AuthorizedIntegResourceRepo{IntegID: ai3.ID}, 0) + + // Verify that InsertAuthorizedIntegrationResourceRepos performs inserts in a TX by having a second one with an invalid + // RepoID, causing a foreign key violation + resRepo1 := &auth_model.AuthorizedIntegResourceRepo{ + IntegID: ai3.ID, + RepoID: 1, + } + resRepo3 := &auth_model.AuthorizedIntegResourceRepo{ + IntegID: ai3.ID, + RepoID: 30000, // invalid + } + err := auth_model.InsertAuthorizedIntegrationResourceRepos(t.Context(), ai3.ID, + []*auth_model.AuthorizedIntegResourceRepo{resRepo1, resRepo3}) + require.ErrorContains(t, err, "foreign key") + + // Count remains 0; the first record was not inserted. + unittest.AssertCount(t, &auth_model.AuthorizedIntegResourceRepo{IntegID: ai3.ID}, 0) + }) +} diff --git a/models/auth/authorized_integration_test.go b/models/auth/authorized_integration_test.go new file mode 100644 index 0000000000..d705f1a144 --- /dev/null +++ b/models/auth/authorized_integration_test.go @@ -0,0 +1,106 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package auth_test + +import ( + "testing" + "time" + + auth_model "forgejo.org/models/auth" + "forgejo.org/models/db" + "forgejo.org/models/unittest" + "forgejo.org/modules/timeutil" + "forgejo.org/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func makeAuthorizedIntegration(t *testing.T) *auth_model.AuthorizedIntegration { + t.Helper() + ai := &auth_model.AuthorizedIntegration{ + UserID: 2, + Scope: auth_model.AccessTokenScopeAll, + ResourceAllRepos: true, + Issuer: "https://example.org/", + ClaimRules: &auth_model.ClaimRules{}, + } + require.NoError(t, auth_model.InsertAuthorizedIntegration(t.Context(), ai)) + return ai +} + +func TestGetAuthorizedIntegration(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ai := makeAuthorizedIntegration(t) + + get, err := auth_model.GetAuthorizedIntegration(t.Context(), "abc", "123") + require.ErrorIs(t, err, util.ErrNotExist) + assert.Nil(t, get) + + get, err = auth_model.GetAuthorizedIntegration(t.Context(), ai.Issuer, ai.Audience) + require.NoError(t, err) + require.NotNil(t, get) + assert.Equal(t, ai.ID, get.ID) +} + +func TestAuthorizedIntegrationUpdateLastUsed(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + ai := makeAuthorizedIntegration(t) + ai.UpdatedUnix = 0 + cnt, err := db.GetEngine(t.Context()).ID(ai.ID).Cols("updated_unix").NoAutoTime().Update(ai) + require.NoError(t, err) + assert.EqualValues(t, 1, cnt) + + timeutil.MockSet(time.Unix(1777130023, 0)) + defer timeutil.MockUnset() + + assert.EqualValues(t, 0, ai.UpdatedUnix) + require.NoError(t, ai.UpdateLastUsed(t.Context())) + assert.EqualValues(t, 1777130023, ai.UpdatedUnix) // object field updated + assert.EqualValues(t, 1777130023, unittest.AssertExistsAndLoadBean(t, &auth_model.AuthorizedIntegration{ID: ai.ID}).UpdatedUnix) + + // nearly immediate redo should have same timestamp due to the 1 minute deduplication: + timeutil.MockSet(time.Unix(1777130025, 0)) + require.NoError(t, ai.UpdateLastUsed(t.Context())) + assert.EqualValues(t, 1777130023, ai.UpdatedUnix) // object field not updated + assert.EqualValues(t, 1777130023, unittest.AssertExistsAndLoadBean(t, &auth_model.AuthorizedIntegration{ID: ai.ID}).UpdatedUnix) // database field not updated + + // but if it's a little while later.. + timeutil.MockSet(time.Unix(1777131139, 0)) + require.NoError(t, ai.UpdateLastUsed(t.Context())) + assert.EqualValues(t, 1777131139, ai.UpdatedUnix) // object field updated + assert.EqualValues(t, 1777131139, unittest.AssertExistsAndLoadBean(t, &auth_model.AuthorizedIntegration{ID: ai.ID}).UpdatedUnix) // database field updated +} + +func TestNewAuthorizedIntegration(t *testing.T) { + ai := &auth_model.AuthorizedIntegration{ + UserID: 2, + Scope: auth_model.AccessTokenScopeAll, + ResourceAllRepos: true, + Issuer: "https://example.org/", + ClaimRules: &auth_model.ClaimRules{}, + } + require.NoError(t, auth_model.InsertAuthorizedIntegration(t.Context(), ai)) + assert.Contains(t, ai.Audience, "u:2:") + + ai = &auth_model.AuthorizedIntegration{ + UserID: 2, + Scope: auth_model.AccessTokenScopeAll, + ResourceAllRepos: true, + Issuer: "https://example.org/", + Audience: "I made my own audience", + ClaimRules: &auth_model.ClaimRules{}, + } + require.ErrorContains(t, auth_model.InsertAuthorizedIntegration(t.Context(), ai), "audience cannot be provided") + + ai = &auth_model.AuthorizedIntegration{ + // Forgot to set UserID + Scope: auth_model.AccessTokenScopeAll, + ResourceAllRepos: true, + Issuer: "https://example.org/", + ClaimRules: &auth_model.ClaimRules{}, + } + require.ErrorContains(t, auth_model.InsertAuthorizedIntegration(t.Context(), ai), "UserID must be initialized") +} diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index fa68197cf0..9ef89cb624 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -6,6 +6,7 @@ package auth import ( "context" "crypto/sha256" + "crypto/subtle" "encoding/base32" "encoding/base64" "errors" @@ -151,9 +152,9 @@ func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool { // https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-12#section-3.1 contains := func(s string) bool { - s = strings.TrimSuffix(strings.ToLower(s), "/") + s = strings.TrimSuffix(util.ToUpperASCII(s), "/") for _, u := range app.RedirectURIs { - if strings.TrimSuffix(strings.ToLower(u), "/") == s { + if strings.TrimSuffix(util.ToUpperASCII(u), "/") == s { return true } } @@ -408,26 +409,41 @@ func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (*url.URL return redirect, err } -// Invalidate deletes the auth code from the database to invalidate this code +// Invalidate deletes the auth code from the database to invalidate this code. +// It returns an error if the code was already invalidated (i.e., no rows were deleted), +// which prevents authorization code replay attacks. func (code *OAuth2AuthorizationCode) Invalidate(ctx context.Context) error { - _, err := db.GetEngine(ctx).ID(code.ID).NoAutoCondition().Delete(code) - return err + affected, err := db.GetEngine(ctx).ID(code.ID).NoAutoCondition().Delete(code) + if err != nil { + return err + } + if affected == 0 { + return fmt.Errorf("authorization code already used or does not exist") + } + return nil } -// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation. +// ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE +// implementation. If a code challenge was set during authorization, a valid verifier MUST be provided. func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool { + // If no PKCE was used during authorization, no verifier is needed. + if code.CodeChallengeMethod == "" && code.CodeChallenge == "" { + return true + } + // A challenge was set but no verifier provided: reject outright, no comparison or hashing is required. + if verifier == "" { + return false + } switch code.CodeChallengeMethod { case "S256": // base64url(SHA256(verifier)) see https://tools.ietf.org/html/rfc7636#section-4.6 h := sha256.Sum256([]byte(verifier)) hashedVerifier := base64.RawURLEncoding.EncodeToString(h[:]) - return hashedVerifier == code.CodeChallenge + return subtle.ConstantTimeCompare([]byte(hashedVerifier), []byte(code.CodeChallenge)) == 1 case "plain": - return verifier == code.CodeChallenge - case "": - return true + return subtle.ConstantTimeCompare([]byte(verifier), []byte(code.CodeChallenge)) == 1 default: - // unsupported method -> return false + // unsupported or empty method with a non-empty challenge -> reject return false } } @@ -490,22 +506,25 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi } // IncreaseCounter increases the counter and updates the grant +// IncreaseCounter atomically increments the counter only if it still matches +// the value loaded into this grant. Returns an error if the counter was already +// changed by a concurrent request (refresh token replay). func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error { - _, err := db.GetEngine(ctx).ID(grant.ID).Incr("counter").Update(new(OAuth2Grant)) + affected, err := db.GetEngine(ctx).Where("id = ? AND counter = ?", grant.ID, grant.Counter). + Incr("counter").Update(new(OAuth2Grant)) if err != nil { return err } - updatedGrant, err := GetOAuth2GrantByID(ctx, grant.ID) - if err != nil { - return err + if affected == 0 { + return fmt.Errorf("grant counter changed unexpectedly (possible replay)") } - grant.Counter = updatedGrant.Counter + grant.Counter++ return nil } // ScopeContains returns true if the grant scope contains the specified scope func (grant *OAuth2Grant) ScopeContains(scope string) bool { - for _, currentScope := range strings.Split(grant.Scope, " ") { + for currentScope := range strings.SplitSeq(grant.Scope, " ") { if scope == currentScope { return true } diff --git a/models/auth/oauth2_test.go b/models/auth/oauth2_test.go index 9c6836ed0d..aa474393c1 100644 --- a/models/auth/oauth2_test.go +++ b/models/auth/oauth2_test.go @@ -75,6 +75,13 @@ func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) { assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other")) } +func TestOAuth2Application_ContainsRedirect_Normalization(t *testing.T) { + app := &auth_model.OAuth2Application{RedirectURIs: []string{"https://website.com"}} + assert.True(t, app.ContainsRedirectURI("https://website.com")) + assert.True(t, app.ContainsRedirectURI("https://webSITE.com")) // ascii uppercase I + assert.False(t, app.ContainsRedirectURI("https://websİte.com")) // U+0130 as I, Latin Capital Letter I with Dot Above +} + func TestOAuth2Application_ValidateClientSecret(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) @@ -147,9 +154,18 @@ func TestGetOAuth2GrantByID(t *testing.T) { func TestOAuth2Grant_IncreaseCounter(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 1}) + + // First increment succeeds require.NoError(t, grant.IncreaseCounter(db.DefaultContext)) assert.Equal(t, int64(2), grant.Counter) unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2}) + + // Simulate a stale grant (counter still 1): must fail (concurrent replay) + grant.Counter = 1 + require.Error(t, grant.IncreaseCounter(db.DefaultContext), "stale counter must be rejected") + + // Counter in DB should be unchanged + unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2}) } func TestOAuth2Grant_ScopeContains(t *testing.T) { @@ -232,12 +248,33 @@ func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) { } assert.False(t, code.ValidateCodeChallenge("foiwgjioriogeiogjerger")) - // test no code challenge + // test no PKCE at all (no challenge set, no verifier needed) + code = &auth_model.OAuth2AuthorizationCode{ + CodeChallengeMethod: "", + CodeChallenge: "", + } + assert.True(t, code.ValidateCodeChallenge("")) + + // test PKCE required: challenge was set but verifier is empty + code = &auth_model.OAuth2AuthorizationCode{ + CodeChallengeMethod: "S256", + CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", + } + assert.False(t, code.ValidateCodeChallenge(""), "PKCE required: S256 challenge set but empty verifier must be rejected") + + code = &auth_model.OAuth2AuthorizationCode{ + CodeChallengeMethod: "plain", + CodeChallenge: "test123", + } + assert.False(t, code.ValidateCodeChallenge(""), "PKCE required: plain challenge set but empty verifier must be rejected") + + // test challenge stored but method empty (malformed: should reject) code = &auth_model.OAuth2AuthorizationCode{ CodeChallengeMethod: "", CodeChallenge: "foierjiogerogerg", } - assert.True(t, code.ValidateCodeChallenge("")) + assert.False(t, code.ValidateCodeChallenge(""), "challenge present with empty method must be rejected") + assert.False(t, code.ValidateCodeChallenge("foierjiogerogerg"), "challenge present with empty method must be rejected even with matching verifier") } func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) { @@ -262,6 +299,20 @@ func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) { unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) } +func TestOAuth2AuthorizationCode_Invalidate_DoubleUse(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + code := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) + + // First invalidation should succeed + require.NoError(t, code.Invalidate(db.DefaultContext)) + unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"}) + + // Second invalidation of the same code must fail (replay prevention) + err := code.Invalidate(db.DefaultContext) + require.Error(t, err) + assert.Contains(t, err.Error(), "authorization code already used") +} + func TestOAuth2AuthorizationCode_TableName(t *testing.T) { assert.Equal(t, "oauth2_authorization_code", new(auth_model.OAuth2AuthorizationCode).TableName()) } diff --git a/models/auth/source.go b/models/auth/source.go index ecd3abc39d..fd016e02da 100644 --- a/models/auth/source.go +++ b/models/auth/source.go @@ -91,7 +91,7 @@ var registeredConfigs = map[Type]func() Config{} // RegisterTypeConfig register a config for a provided type func RegisterTypeConfig(typ Type, exemplar Config) { - if reflect.TypeOf(exemplar).Kind() == reflect.Ptr { + if reflect.TypeOf(exemplar).Kind() == reflect.Pointer { // Pointer: registeredConfigs[typ] = func() Config { return reflect.New(reflect.ValueOf(exemplar).Elem().Type()).Interface().(Config) @@ -250,8 +250,8 @@ type FindSourcesOptions struct { func (opts FindSourcesOptions) ToConds() builder.Cond { conds := builder.NewCond() - if opts.IsActive.Has() { - conds = conds.And(builder.Eq{"is_active": opts.IsActive.Value()}) + if has, value := opts.IsActive.Get(); has { + conds = conds.And(builder.Eq{"is_active": value}) } if opts.LoginType != NoType { conds = conds.And(builder.Eq{"`type`": opts.LoginType}) diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go index ad59bd8769..43d52c562b 100644 --- a/models/avatars/avatar.go +++ b/models/avatars/avatar.go @@ -165,10 +165,7 @@ func GenerateUserAvatarFastLink(userName string, size int) string { } // GenerateUserAvatarImageLink returns a link for `User.Avatar` image file: "/avatars/${User.Avatar}" -func GenerateUserAvatarImageLink(userAvatar string, size int) string { - if size > 0 { - return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar) + "?size=" + strconv.Itoa(size) - } +func GenerateUserAvatarImageLink(userAvatar string) string { return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar) } @@ -210,9 +207,6 @@ func generateEmailAvatarLink(ctx context.Context, email string, size int, final } // for non-final link, we should return fast (use a 302 redirection link) urlStr := setting.AppSubURL + "/avatar/" + url.PathEscape(emailHash) - if size > 0 { - urlStr += "?size=" + strconv.Itoa(size) - } return urlStr } diff --git a/models/db/context.go b/models/db/context.go index 9be158ccca..b51861d896 100644 --- a/models/db/context.go +++ b/models/db/context.go @@ -6,6 +6,9 @@ package db import ( "context" "database/sql" + "errors" + "fmt" + "slices" "xorm.io/builder" "xorm.io/xorm" @@ -268,6 +271,91 @@ func GetByID[T any](ctx context.Context, id int64) (object *T, exist bool, err e return &bean, true, nil } +// Retrieves multiple objects with database queries similar to an xorm `.In(idField, idList)`. idField must be a unique +// field on the database table, as a map[id]obj is returned and the usage of a non-unique field would result in objects +// being overwritten in the map. +// +// The length of the IN list is constrained to DefaultMaxInSize for each database query, resulting in multiple database +// queries if the length of the idList exceeds that setting; this constraint prevents exceeding bind parameter +// limitations or query length limitations in the database engine. +func GetByIDs[Bean any, Id comparable](ctx context.Context, idField string, idList []Id, bean *Bean) (map[Id]*Bean, error) { + retval := make(map[Id]*Bean, len(idList)) + if len(idList) == 0 { + return retval, nil + } + + table, err := TableInfo(bean) + if err != nil { + return nil, fmt.Errorf("unable to fetch table info for bean %v: %w", bean, err) + } + + var structFieldName string + for _, c := range table.Columns() { + if c.Name == idField { + structFieldName = c.FieldName + break + } + } + if structFieldName == "" { + return nil, fmt.Errorf("unable to identify struct field for id field %s", idField) + } + + for idChunk := range slices.Chunk(idList, DefaultMaxInSize) { + beans := make([]*Bean, 0, len(idChunk)) + if err := GetEngine(ctx).In(idField, idChunk).Find(&beans); err != nil { + return nil, err + } + for _, bean := range beans { + retval[extractFieldValue(bean, structFieldName).(Id)] = bean + } + } + + return retval, nil +} + +// Retrieves multiple objects with database queries similar to an xorm `.In(field, valueList)`. Similar to GetByIDs, +// except that a map[Id][]*Bean is returned as the field value is not assumed to be a unique value -- if there are +// multiple rows in the table for each value, all of them are returned. +// +// The length of the IN list is constrained to DefaultMaxInSize for each database query, resulting in multiple database +// queries if the length of the idList exceeds that setting; this constraint prevents exceeding bind parameter +// limitations or query length limitations in the database engine. +func GetByFieldIn[Bean any, Id comparable](ctx context.Context, field string, valueList []Id, bean *Bean) (map[Id][]*Bean, error) { + retval := make(map[Id][]*Bean, len(valueList)) + if len(valueList) == 0 { + return retval, nil + } + + table, err := TableInfo(bean) + if err != nil { + return nil, fmt.Errorf("unable to fetch table info for bean %v: %w", bean, err) + } + + var structFieldName string + for _, c := range table.Columns() { + if c.Name == field { + structFieldName = c.FieldName + break + } + } + if structFieldName == "" { + return nil, fmt.Errorf("unable to identify struct field for field %s", field) + } + + for idChunk := range slices.Chunk(valueList, DefaultMaxInSize) { + beans := make([]*Bean, 0, len(idChunk)) + if err := GetEngine(ctx).In(field, idChunk).Find(&beans); err != nil { + return nil, err + } + for _, bean := range beans { + fieldValue := extractFieldValue(bean, structFieldName).(Id) + retval[fieldValue] = append(retval[fieldValue], bean) + } + } + + return retval, nil +} + func Exist[T any](ctx context.Context, cond builder.Cond) (bool, error) { if !cond.IsValid() { panic("cond is invalid in db.Exist(ctx, cond). This should not be possible.") @@ -416,3 +504,68 @@ func inTransaction(ctx context.Context) (*xorm.Session, bool) { return nil, false } } + +type RetryConfig struct { + ErrorIs []error + AttemptCount int +} + +var ErrNestedRetryTxFailure = errors.New("(nested)") + +type nestedRetryTxState int + +var nestedRetryTx nestedRetryTxState + +// Execute the given function in a transaction. RetryConfig will retry the function on an error, if it matches the +// ErrorIs parameter, up to the total of AttemptCount number of tries. RetryTx cannot be invoked when already within a +// transaction and will return an error immediately. +// +// ErrNestedRetryTxFailure is an error type that will occur when RetryTx is nested within each other, and indicates that +// an inner RetryTx encountered an error that matched its error list. +func RetryTx(ctx context.Context, config RetryConfig, f func(ctx context.Context) error) error { + matchError := func(err error) bool { + for _, possibleError := range config.ErrorIs { + if errors.Is(err, possibleError) { + return true + } + } + return false + } + + // Accept `ErrNestedRetryTxFailure` as error to retry on, means that a nested + // RetryTx indicated to retry the whole transaction. + config.ErrorIs = append(config.ErrorIs, ErrNestedRetryTxFailure) + + withinRetryTx, present := ctx.Value(nestedRetryTx).(bool) + if present && withinRetryTx { + // If a caller already started `RetryTx`, then we assume we don't have to actually perform retries here -- we + // can attempt the requested function once, and if an error is returned that matches the configured error list, + // we'll return that error + ErrNestedRetryTxFailure wrapping. + err := f(ctx) + if err == nil { + return nil + } else if matchError(err) { + return fmt.Errorf("nested RetryTx; internal Tx failed with error that won't be retried: %w %w", err, ErrNestedRetryTxFailure) + } + return err + } else if InTransaction(ctx) { + return errors.New("unsupported operation: attempted to use RetryTx while already within a transaction") + } else if config.AttemptCount == 0 { + return errors.New("unsupported operation: attempted to use RetryTx with 0 attempts") + } + + innerCtx := context.WithValue(ctx, nestedRetryTx, true) + var lastError error + for range config.AttemptCount { + err := WithTx(innerCtx, f) + if err == nil { + return nil + } else if !matchError(err) { + return err + } + + lastError = err + } + + return fmt.Errorf("retry tx failed after %d attempts; last error: %w", config.AttemptCount, lastError) +} diff --git a/models/db/context_test.go b/models/db/context_test.go index 525ab54d99..59ef1e7754 100644 --- a/models/db/context_test.go +++ b/models/db/context_test.go @@ -220,3 +220,103 @@ func TestAfterTx(t *testing.T) { }) } } + +func TestRetryTx(t *testing.T) { + t.Run("success", func(t *testing.T) { + err := db.RetryTx(t.Context(), db.RetryConfig{AttemptCount: 1}, func(ctx context.Context) error { + assert.True(t, db.InTransaction(ctx)) + return nil + }) + require.NoError(t, err) + }) + + t.Run("fail constantly", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{testError}, + }, func(ctx context.Context) error { + attemptCount++ + return testError + }) + require.ErrorIs(t, err, testError) + require.ErrorContains(t, err, "2 attempts") + assert.Equal(t, 2, attemptCount) + }) + + t.Run("fail w/ non retriable error", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{}, + }, func(ctx context.Context) error { + attemptCount++ + return testError + }) + require.ErrorIs(t, err, testError) + assert.Equal(t, 1, attemptCount) + }) + + t.Run("succeed on retry", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{testError}, + }, func(ctx context.Context) error { + attemptCount++ + if attemptCount == 1 { + return testError + } + return nil + }) + require.NoError(t, err) + assert.Equal(t, 2, attemptCount) + }) + + t.Run("nested", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + }, func(ctx context.Context) error { + attemptCount++ + return db.RetryTx(ctx, db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{testError}, + }, func(ctx context.Context) error { + if attemptCount == 2 { + return nil + } + return testError + }) + }) + + require.NoError(t, err) + assert.Equal(t, 2, attemptCount) + }) + + t.Run("inner RetryTx decides on error", func(t *testing.T) { + attemptCount := 0 + testError := errors.New("hello") + err := db.RetryTx(t.Context(), db.RetryConfig{ + AttemptCount: 2, + ErrorIs: []error{}, + }, func(ctx context.Context) error { + attemptCount++ + return db.RetryTx(ctx, db.RetryConfig{ + AttemptCount: 2, + }, func(ctx context.Context) error { + if attemptCount == 2 { + return nil + } + return testError + }) + }) + + require.ErrorIs(t, err, testError) + assert.Equal(t, 1, attemptCount) + }) +} diff --git a/models/db/foreign_keys.go b/models/db/foreign_keys.go index 7af0b92f73..1bf022596d 100644 --- a/models/db/foreign_keys.go +++ b/models/db/foreign_keys.go @@ -256,6 +256,13 @@ func extendBeansForCascade(beans []any) ([]any, error) { if deduplicateTables.Contains(schema.Name) { continue } + + for _, column := range schema.Columns() { + if column.IsDeleted { + return nil, fmt.Errorf("unable to use table %q in a cascade operation, as it has a soft-delete column %q", schema.Name, column.FieldName) + } + } + deduplicateTables.Add(schema.Name) for _, referencingTable := range referencedTables[schema.Name] { table := tableMap[referencingTable] diff --git a/models/db/index.go b/models/db/index.go index c069f0febd..923576edb3 100644 --- a/models/db/index.go +++ b/models/db/index.go @@ -20,8 +20,8 @@ type ResourceIndex struct { } var ( - // ErrResouceOutdated represents an error when request resource outdated - ErrResouceOutdated = errors.New("resource outdated") + // ErrResourceOutdated represents an error when request resource outdated + ErrResourceOutdated = errors.New("resource outdated") // ErrGetResourceIndexFailed represents an error when resource index retries 3 times ErrGetResourceIndexFailed = errors.New("get resource index failed") ) diff --git a/models/db/iterate.go b/models/db/iterate.go index d2315cb12c..c56871d503 100644 --- a/models/db/iterate.go +++ b/models/db/iterate.go @@ -80,7 +80,7 @@ func Iterate[Bean any](ctx context.Context, cond builder.Cond, f func(ctx contex func extractFieldValue(bean any, fieldName string) any { v := reflect.ValueOf(bean) - if v.Kind() == reflect.Ptr { + if v.Kind() == reflect.Pointer { v = v.Elem() } field := v.FieldByName(fieldName) diff --git a/models/db/list.go b/models/db/list.go index 057221936c..71e9a0b1d2 100644 --- a/models/db/list.go +++ b/models/db/list.go @@ -14,7 +14,7 @@ import ( const ( // DefaultMaxInSize represents default variables number on IN () in SQL - DefaultMaxInSize = 50 + DefaultMaxInSize = 500 defaultFindSliceSize = 10 ) diff --git a/models/db/name.go b/models/db/name.go index 29b60b2373..efd1c2b5f3 100644 --- a/models/db/name.go +++ b/models/db/name.go @@ -6,6 +6,7 @@ package db import ( "fmt" "regexp" + "slices" "strings" "unicode/utf8" @@ -80,6 +81,31 @@ func (err ErrNameCharsNotAllowed) Unwrap() error { return util.ErrInvalidArgument } +// ErrNameActivityPubInvalid represents an error for usernames which cannot +// belong to ActivityPub accounts. +type ErrNameActivityPubInvalid struct { + Name string +} + +// Similarly to IsErrNameCharsNotAllowed, IsErrNameActivityPubInvalid checks if +// an error is an ErrNameActivityPubInvalid. +func IsErrNameActivityPubInvalid(err error) bool { + _, ok := err.(ErrNameActivityPubInvalid) + return ok +} + +func (err ErrNameActivityPubInvalid) Error() string { + return fmt.Sprintf( + "name is invalid [%s]: not acceptable for users from federated activitypub instances (e.g. @username@domain.example)", + err.Name, + ) +} + +// Unwrap unwraps this as a ErrInvalidArgument err +func (err ErrNameActivityPubInvalid) Unwrap() error { + return util.ErrInvalidArgument +} + // IsUsableName checks if name is reserved or pattern of name is not allowed // based on given reserved names and patterns. // Names are exact match, patterns can be prefix or suffix match with placeholder '*'. @@ -89,10 +115,8 @@ func IsUsableName(names, patterns []string, name string) error { return ErrNameEmpty } - for i := range names { - if name == names[i] { - return ErrNameReserved{name} - } + if slices.Contains(names, name) { + return ErrNameReserved{name} } for _, pat := range patterns { diff --git a/models/db/optional_test.go b/models/db/optional_test.go new file mode 100644 index 0000000000..fedf20161f --- /dev/null +++ b/models/db/optional_test.go @@ -0,0 +1,202 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package db_test + +import ( + "testing" + + "forgejo.org/models/db" + "forgejo.org/models/unittest" + "forgejo.org/modules/optional" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/xorm/schemas" +) + +func TestOptionFieldInt(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + type OptionInt struct { + ID int64 `xorm:"pk autoincr"` + Number optional.Option[int64] + } + db.GetEngine(t.Context()).Sync(&OptionInt{}) + + t.Run("insert null, read back", func(t *testing.T) { + null := OptionInt{ + Number: optional.None[int64](), + } + cnt, err := db.GetEngine(t.Context()).Insert(&null) + require.NoError(t, err) + assert.EqualValues(t, 1, cnt) + + { + var read OptionInt + has, err := db.GetEngine(t.Context()).ID(null.ID).Get(&read) + require.NoError(t, err) + require.True(t, has) + hasNumber, _ := read.Number.Get() + assert.False(t, hasNumber) + } + }) + + t.Run("insert not null, read back", func(t *testing.T) { + notNull := OptionInt{ + Number: optional.Some[int64](123), + } + + cnt, err := db.GetEngine(t.Context()).Insert(¬Null) + require.NoError(t, err) + assert.EqualValues(t, 1, cnt) + { + var read OptionInt + has, err := db.GetEngine(t.Context()).ID(notNull.ID).Get(&read) + require.NoError(t, err) + require.True(t, has) + hasNumber, number := read.Number.Get() + assert.True(t, hasNumber) + assert.EqualValues(t, 123, number) + } + }) + + t.Run("read multiple records without filters", func(t *testing.T) { + var arr []OptionInt + err := db.GetEngine(t.Context()).Find(&arr) + require.NoError(t, err) + assert.Len(t, arr, 2) + }) + + t.Run("read multiple records with bean filters", func(t *testing.T) { + var arr []OptionInt + cond := &OptionInt{ + Number: optional.Some[int64](123), + } + err := db.GetEngine(t.Context()).Find(&arr, cond) + require.NoError(t, err) + require.Len(t, arr, 1) + v := arr[0] + has, value := v.Number.Get() + assert.True(t, has) + assert.EqualValues(t, 123, value) + }) +} + +func TestOptionFieldString(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + type OptionString struct { + ID int64 `xorm:"pk autoincr"` + String optional.Option[string] + } + assert.NoError(t, db.GetEngine(t.Context()).Sync(new(OptionString))) + + t.Run("insert null, read back", func(t *testing.T) { + null := OptionString{ + String: optional.None[string](), + } + cnt, err := db.GetEngine(t.Context()).Insert(&null) + require.NoError(t, err) + assert.EqualValues(t, 1, cnt) + + { + var read OptionString + has, err := db.GetEngine(t.Context()).ID(null.ID).Get(&read) + require.NoError(t, err) + require.True(t, has) + hasString, _ := read.String.Get() + assert.False(t, hasString) + } + }) + + t.Run("insert not null, read back", func(t *testing.T) { + notNull := OptionString{ + String: optional.Some("hello"), + } + + cnt, err := db.GetEngine(t.Context()).Insert(¬Null) + require.NoError(t, err) + assert.EqualValues(t, 1, cnt) + { + var read OptionString + has, err := db.GetEngine(t.Context()).ID(notNull.ID).Get(&read) + require.NoError(t, err) + require.True(t, has) + hasString, str := read.String.Get() + assert.True(t, hasString) + assert.Equal(t, "hello", str) + } + }) + + t.Run("read multiple records without filters", func(t *testing.T) { + var arr []OptionString + err := db.GetEngine(t.Context()).Find(&arr) + require.NoError(t, err) + assert.Len(t, arr, 2) + }) + + t.Run("read multiple records with bean filters", func(t *testing.T) { + var arr []OptionString + cond := &OptionString{ + String: optional.Some("hello"), + } + err := db.GetEngine(t.Context()).Find(&arr, cond) + require.NoError(t, err) + require.Len(t, arr, 1) + v := arr[0] + has, value := v.String.Get() + assert.True(t, has) + assert.Equal(t, "hello", value) + }) +} + +func TestOptionFieldIntrospection(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + type OptionIntrospectInt struct { + ID int64 `xorm:"pk autoincr"` + OptionNumber optional.Option[int64] + NormalNumber int64 + } + assert.NoError(t, db.GetEngine(t.Context()).Sync(new(OptionIntrospectInt))) + + schema, err := db.GetEngine(t.Context()).NoAutoTime().Engine().TableInfo(&OptionIntrospectInt{}) + require.NoError(t, err) + + var optionColumn, normalColumn *schemas.Column + for _, c := range schema.Columns() { + switch c.Name { + case "option_number": + optionColumn = c + case "normal_number": + normalColumn = c + } + } + require.NotNil(t, optionColumn) + require.NotNil(t, normalColumn) + + assert.Equal(t, normalColumn.TableName, optionColumn.TableName, "field TableName") + assert.Equal(t, normalColumn.SQLType, optionColumn.SQLType, "field SQLType") + assert.Equal(t, normalColumn.IsJSON, optionColumn.IsJSON, "field IsJSON") + assert.Equal(t, normalColumn.Length, optionColumn.Length, "field Length") + assert.Equal(t, normalColumn.Length2, optionColumn.Length2, "field Length2") + assert.Equal(t, normalColumn.Nullable, optionColumn.Nullable, "field Nullable") + assert.Equal(t, normalColumn.Default, optionColumn.Default, "field Default") + assert.Equal(t, normalColumn.Indexes, optionColumn.Indexes, "field Indexes") + assert.Equal(t, normalColumn.IsPrimaryKey, optionColumn.IsPrimaryKey, "field IsPrimaryKey") + assert.Equal(t, normalColumn.IsAutoIncrement, optionColumn.IsAutoIncrement, "field IsAutoIncrement") + assert.Equal(t, normalColumn.MapType, optionColumn.MapType, "field MapType") + assert.Equal(t, normalColumn.IsCreated, optionColumn.IsCreated, "field IsCreated") + assert.Equal(t, normalColumn.IsUpdated, optionColumn.IsUpdated, "field IsUpdated") + assert.Equal(t, normalColumn.IsDeleted, optionColumn.IsDeleted, "field IsDeleted") + assert.Equal(t, normalColumn.IsCascade, optionColumn.IsCascade, "field IsCascade") + assert.Equal(t, normalColumn.IsVersion, optionColumn.IsVersion, "field IsVersion") + assert.Equal(t, normalColumn.DefaultIsEmpty, optionColumn.DefaultIsEmpty, "field DefaultIsEmpty") + assert.Equal(t, normalColumn.EnumOptions, optionColumn.EnumOptions, "field EnumOptions") + assert.Equal(t, normalColumn.SetOptions, optionColumn.SetOptions, "field SetOptions") + assert.Equal(t, normalColumn.DisableTimeZone, optionColumn.DisableTimeZone, "field DisableTimeZone") + assert.Equal(t, normalColumn.TimeZone, optionColumn.TimeZone, "field TimeZone") + assert.Equal(t, normalColumn.Comment, optionColumn.Comment, "field Comment") + assert.Equal(t, normalColumn.Collation, optionColumn.Collation, "field Collation") +} diff --git a/models/dbfs/dbfile.go b/models/dbfs/dbfile.go index 8cd64177dd..7e7c58cc6c 100644 --- a/models/dbfs/dbfile.go +++ b/models/dbfs/dbfile.go @@ -46,10 +46,7 @@ func (f *file) readAt(fileMeta *DbfsMeta, offset int64, p []byte) (n int, err er blobPos := int(offset % f.blockSize) blobOffset := offset - int64(blobPos) blobRemaining := int(f.blockSize) - blobPos - needRead := len(p) - if needRead > blobRemaining { - needRead = blobRemaining - } + needRead := min(len(p), blobRemaining) if blobOffset+int64(blobPos)+int64(needRead) > fileMeta.FileSize { needRead = int(fileMeta.FileSize - blobOffset - int64(blobPos)) } @@ -66,14 +63,8 @@ func (f *file) readAt(fileMeta *DbfsMeta, offset int64, p []byte) (n int, err er blobData = nil } - canCopy := len(blobData) - blobPos - if canCopy <= 0 { - canCopy = 0 - } - realRead := needRead - if realRead > canCopy { - realRead = canCopy - } + canCopy := max(len(blobData)-blobPos, 0) + realRead := min(needRead, canCopy) if realRead > 0 { copy(p[:realRead], fileData.BlobData[blobPos:blobPos+realRead]) } @@ -113,10 +104,7 @@ func (f *file) Write(p []byte) (n int, err error) { blobPos := int(f.offset % f.blockSize) blobOffset := f.offset - int64(blobPos) blobRemaining := int(f.blockSize) - blobPos - needWrite := len(p) - if needWrite > blobRemaining { - needWrite = blobRemaining - } + needWrite := min(len(p), blobRemaining) buf := make([]byte, f.blockSize) readBytes, err := f.readAt(fileMeta, blobOffset, buf) if err != nil && !errors.Is(err, io.EOF) { diff --git a/models/fixtures/PrivateIssueProjects/project_board.yml b/models/fixtures/PrivateIssueProjects/project_board.yml index 3f1fe1e705..a1c257dacc 100644 --- a/models/fixtures/PrivateIssueProjects/project_board.yml +++ b/models/fixtures/PrivateIssueProjects/project_board.yml @@ -4,6 +4,7 @@ title: Triage creator_id: 2 default: true + sorting: 0 created_unix: 1738000000 updated_unix: 1738000000 @@ -13,5 +14,6 @@ title: Triage creator_id: 2 default: true + sorting: 0 created_unix: 1738000000 updated_unix: 1738000000 diff --git a/models/fixtures/PrivateIssueProjects/project_issue.yml b/models/fixtures/PrivateIssueProjects/project_issue.yml index 0245fb47f3..edbaa9640b 100644 --- a/models/fixtures/PrivateIssueProjects/project_issue.yml +++ b/models/fixtures/PrivateIssueProjects/project_issue.yml @@ -3,21 +3,25 @@ issue_id: 6 project_id: 1001 project_board_id: 1001 + sorting: 1 - id: 1002 issue_id: 7 project_id: 1002 project_board_id: 1002 + sorting: 0 - id: 1003 issue_id: 16 project_id: 1001 project_board_id: 1001 + sorting: 0 - id: 1004 issue_id: 1 project_id: 1002 project_board_id: 1002 + sorting: 1 diff --git a/models/fixtures/TestPrivateRepoProjects/project_board.yml b/models/fixtures/TestPrivateRepoProjects/project_board.yml index 9829cf7e27..68121387b9 100644 --- a/models/fixtures/TestPrivateRepoProjects/project_board.yml +++ b/models/fixtures/TestPrivateRepoProjects/project_board.yml @@ -4,5 +4,6 @@ title: Triage creator_id: 2 default: true + sorting: 0 created_unix: 1738000000 updated_unix: 1738000000 diff --git a/models/fixtures/TestPrivateRepoProjects/project_issue.yml b/models/fixtures/TestPrivateRepoProjects/project_issue.yml index 3e8c1dca9e..aa3082af27 100644 --- a/models/fixtures/TestPrivateRepoProjects/project_issue.yml +++ b/models/fixtures/TestPrivateRepoProjects/project_issue.yml @@ -3,9 +3,11 @@ issue_id: 6 project_id: 1001 project_board_id: 1001 + sorting: 1 - id: 1002 issue_id: 15 project_id: 1001 project_board_id: 1001 + sorting: 0 diff --git a/models/fixtures/action.yml b/models/fixtures/action.yml index f1592d4569..c776218be0 100644 --- a/models/fixtures/action.yml +++ b/models/fixtures/action.yml @@ -81,4 +81,4 @@ act_user_id: 40 repo_id: 60 # public is_private: false - created_unix: 1577404800 # end of heatmap \ No newline at end of file + created_unix: 1577404800 # end of heatmap diff --git a/models/fixtures/action_run.yml b/models/fixtures/action_run.yml index 41559ef788..e37fb2f955 100644 --- a/models/fixtures/action_run.yml +++ b/models/fixtures/action_run.yml @@ -2,7 +2,7 @@ id: 791 title: "update actions" repo_id: 4 - owner_id: 1 + owner_id: 5 workflow_id: "artifact.yaml" index: 187 trigger_user_id: 1 @@ -210,7 +210,7 @@ id: 792 title: "update actions" repo_id: 4 - owner_id: 1 + owner_id: 5 workflow_id: "artifact.yaml" index: 188 trigger_user_id: 1 @@ -417,7 +417,7 @@ id: 793 title: "job output" repo_id: 4 - owner_id: 1 + owner_id: 5 workflow_id: "test.yaml" index: 189 trigger_user_id: 1 @@ -436,7 +436,7 @@ id: 794 title: "job output" repo_id: 4 - owner_id: 1 + owner_id: 5 workflow_id: "test.yaml" index: 190 trigger_user_id: 1 @@ -455,7 +455,7 @@ id: 891 title: "update actions" repo_id: 1 - owner_id: 1 + owner_id: 5 workflow_id: "artifact.yaml" index: 187 trigger_user_id: 1 @@ -538,7 +538,7 @@ id: 895 title: "job output" repo_id: 4 - owner_id: 1 + owner_id: 5 workflow_id: "test.yaml" index: 191 trigger_user_id: 1 @@ -558,7 +558,7 @@ id: 896 title: "job output" repo_id: 4 - owner_id: 1 + owner_id: 5 workflow_id: "test.yaml" index: 192 trigger_user_id: 1 diff --git a/models/fixtures/action_run_job.yml b/models/fixtures/action_run_job.yml index 2d8ea9ff6f..a3afaa8461 100644 --- a/models/fixtures/action_run_job.yml +++ b/models/fixtures/action_run_job.yml @@ -152,6 +152,7 @@ is_fork_pull_request: false name: job_2 attempt: 1 + handle: 18e9cf40-c2f6-409f-b832-b945ea7dc79b job_id: job_2 task_id: 47 status: 5 @@ -167,6 +168,7 @@ is_fork_pull_request: false name: job_2 attempt: 2 + handle: a723d3e3-49a1-4e6b-947f-e987e60bfbd6 job_id: job_2 task_id: 47 status: 5 @@ -182,6 +184,7 @@ is_fork_pull_request: false name: job_2 attempt: 1 + handle: 40317a2f-2f00-4a82-8cc4-57347989a493 job_id: job_2 task_id: 47 status: 5 diff --git a/models/fixtures/action_runner_token.yml b/models/fixtures/action_runner_token.yml index 8318349c9d..dffada23e2 100644 --- a/models/fixtures/action_runner_token.yml +++ b/models/fixtures/action_runner_token.yml @@ -1,8 +1,8 @@ - id: 1 # instance scope token: xeiWBL5kuTYxGPynHCqQdoeYmJAeG3IzGXCYTrDX - owner_id: 0 - repo_id: 0 + owner_id: null + repo_id: null is_active: true created: 1695617748 updated: 1695617748 @@ -11,7 +11,7 @@ id: 2 # user scope and can't be used token: vohJB9QcZuSv1gAXESTk2uqpSjHhsKT9j4zYF84x owner_id: 1 - repo_id: 0 + repo_id: null is_active: false created: 1695617749 updated: 1695617749 @@ -20,7 +20,7 @@ id: 3 # user scope and can be used token: gjItAeJ3CA74hNPmPPo0Zco8I1eMaNcP1jVifjOE owner_id: 1 - repo_id: 0 + repo_id: null is_active: true created: 1695617750 updated: 1695617750 @@ -28,7 +28,7 @@ - id: 4 # repo scope token: NOjLubxzFxPGhPXflZknys0gjVvQNhomFbAYuhbH - owner_id: 0 + owner_id: null repo_id: 1 is_active: true created: 1695617751 diff --git a/models/fixtures/project_board.yml b/models/fixtures/project_board.yml index 3293dea6ed..e205c01a21 100644 --- a/models/fixtures/project_board.yml +++ b/models/fixtures/project_board.yml @@ -4,6 +4,7 @@ title: To Do creator_id: 2 default: true + sorting: 0 created_unix: 1588117528 updated_unix: 1588117528 @@ -12,6 +13,7 @@ project_id: 1 title: In Progress creator_id: 2 + sorting: 1 created_unix: 1588117528 updated_unix: 1588117528 @@ -20,6 +22,7 @@ project_id: 1 title: Done creator_id: 2 + sorting: 2 created_unix: 1588117528 updated_unix: 1588117528 @@ -28,6 +31,7 @@ project_id: 4 title: Done creator_id: 2 + sorting: 0 created_unix: 1588117528 updated_unix: 1588117528 @@ -37,6 +41,7 @@ title: Backlog creator_id: 2 default: true + sorting: 0 created_unix: 1588117528 updated_unix: 1588117528 @@ -46,6 +51,7 @@ title: Backlog creator_id: 2 default: true + sorting: 1 created_unix: 1588117528 updated_unix: 1588117528 @@ -55,6 +61,7 @@ title: Done creator_id: 2 default: false + sorting: 0 created_unix: 1588117528 updated_unix: 1588117528 @@ -64,6 +71,7 @@ title: Backlog creator_id: 2 default: true + sorting: 0 created_unix: 1588117528 updated_unix: 1588117528 @@ -73,5 +81,6 @@ title: Uncategorized creator_id: 2 default: true + sorting: 1 created_unix: 1588117528 updated_unix: 1588117528 diff --git a/models/fixtures/project_issue.yml b/models/fixtures/project_issue.yml index b1af05908a..61ad79cd3e 100644 --- a/models/fixtures/project_issue.yml +++ b/models/fixtures/project_issue.yml @@ -3,21 +3,25 @@ issue_id: 1 project_id: 1 project_board_id: 1 + sorting: 0 - id: 2 issue_id: 2 project_id: 1 project_board_id: 0 # no board assigned + sorting: 0 - id: 3 issue_id: 3 project_id: 1 project_board_id: 2 + sorting: 0 - id: 4 issue_id: 5 project_id: 1 project_board_id: 3 + sorting: 0 diff --git a/models/fixtures/public_key.yml b/models/fixtures/public_key.yml index ae620ee2d1..a1c7f4feb6 100644 --- a/models/fixtures/public_key.yml +++ b/models/fixtures/public_key.yml @@ -9,3 +9,36 @@ created_unix: 1559593109 updated_unix: 1565224552 login_source_id: 0 +- + id: 2 + owner_id: 5 + name: user5 + fingerprint: "" + content: "user5" + mode: 2 + type: 3 + created_unix: 1775805112 + updated_unix: 1775805112 + login_source_id: 0 +- + id: 3 + owner_id: 9 + name: user9@localhost + fingerprint: "SHA256:K3RfDvtQ/aYVzh6RfXGFxlffLLTRgksf9UQwTlwSM8M" + content: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDN7KuFUnlztx/UM6PUTyiBAq5SeIqr+qSVFC6JzLQAh user9@localhost" + mode: 2 + type: 1 + created_unix: 1775805112 + updated_unix: 1775805112 + login_source_id: 0 +- + id: 4 + owner_id: 9 + name: user9 + fingerprint: "" + content: "user9" + mode: 2 + type: 3 + created_unix: 1775805112 + updated_unix: 1775805112 + login_source_id: 0 diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 4e957f30f3..17592ef330 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -1566,9 +1566,9 @@ - id: 42 - lower_name: federated-example.net - name: federated-example.net - full_name: federated + lower_name: "@federated@example.net" + name: "@federated@example.net" + full_name: "@federated@example.net" email: f73240e82-c061-41ef-b7d6-4376cb6f2e1c@example.com keep_email_private: false email_notifications_preference: enabled @@ -1576,7 +1576,7 @@ passwd_hash_algo: "" must_change_password: false login_source: 0 - login_name: federated-example.net + login_name: "@federated@example.net" type: 6 salt: "" max_repo_creation: -1 diff --git a/models/forgefed/error.go b/models/forgefed/error.go new file mode 100644 index 0000000000..f8b5ef852e --- /dev/null +++ b/models/forgefed/error.go @@ -0,0 +1,22 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgefed + +import ( + "fmt" +) + +type ErrFederationHostNotFound struct { + SearchKey string + SearchValue string +} + +func (err ErrFederationHostNotFound) Error() string { + return fmt.Sprintf("ErrFederationHostNotFound: search key: %s, search value: %s", err.SearchKey, err.SearchValue) +} + +func IsErrFederationHostNotFound(err error) bool { + _, ok := err.(ErrFederationHostNotFound) + return ok +} diff --git a/models/forgefed/federationhost_repository.go b/models/forgefed/federationhost_repository.go index a44b502ba1..0b3329313e 100644 --- a/models/forgefed/federationhost_repository.go +++ b/models/forgefed/federationhost_repository.go @@ -16,6 +16,31 @@ func init() { db.RegisterModel(new(FederationHost)) } +func CountFederationHosts(ctx context.Context) (int64, error) { + return db.GetEngine(ctx).Count(FederationHost{}) +} + +func FindFederationHosts(ctx context.Context, opts db.ListOptions) (hosts []*FederationHost, err error) { + sess := db.GetEngine(ctx) + + if opts.PageSize > 0 { + sess = db.SetSessionPagination(sess, &opts) + } + + err = sess.Find(&hosts) + if err != nil { + return nil, err + } + + for _, host := range hosts { + if res, err := validation.IsValid(host); !res { + return nil, err + } + } + + return hosts, nil +} + func GetFederationHost(ctx context.Context, ID int64) (*FederationHost, error) { log.Trace("GetFederationHost: %v", ID) host := new(FederationHost) @@ -32,13 +57,13 @@ func GetFederationHost(ctx context.Context, ID int64) (*FederationHost, error) { return host, nil } -func findFederationHostFromDB(ctx context.Context, searchKey, searchValue string) (*FederationHost, error) { +func findFederationHostFromDB(ctx context.Context, searchKey string, searchValue ...any) (*FederationHost, error) { host := new(FederationHost) - has, err := db.GetEngine(ctx).Where(searchKey, searchValue).Get(host) + has, err := db.GetEngine(ctx).Where(searchKey, searchValue...).Get(host) if err != nil { return nil, err } else if !has { - return nil, nil + return nil, ErrFederationHostNotFound{SearchKey: searchKey, SearchValue: fmt.Sprintf("%v", searchValue)} } if res, err := validation.IsValid(host); !res { return nil, err @@ -47,17 +72,7 @@ func findFederationHostFromDB(ctx context.Context, searchKey, searchValue string } func FindFederationHostByFqdnAndPort(ctx context.Context, fqdn string, port uint16) (*FederationHost, error) { - host := new(FederationHost) - has, err := db.GetEngine(ctx).Where("host_fqdn=? AND host_port=?", fqdn, port).Get(host) - if err != nil { - return nil, err - } else if !has { - return nil, nil - } - if res, err := validation.IsValid(host); !res { - return nil, err - } - return host, nil + return findFederationHostFromDB(ctx, "host_fqdn=? AND host_port=?", fqdn, port) } func FindFederationHostByKeyID(ctx context.Context, keyID string) (*FederationHost, error) { diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go index 53ab95d216..d64009db51 100644 --- a/models/forgejo_migrations/migrate.go +++ b/models/forgejo_migrations/migrate.go @@ -153,7 +153,7 @@ func Migrate(x *xorm.Engine, freshDB bool) error { // Set a new clean the default mapper to GonicMapper as that is the default for . x.SetMapper(names.GonicMapper{}) - if err := x.Sync(new(ForgejoMigration)); err != nil { + if _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ForgejoMigration)); err != nil { return fmt.Errorf("sync: %w", err) } diff --git a/models/forgejo_migrations/migrate_test.go b/models/forgejo_migrations/migrate_test.go index 360f6a16d5..87dea780d8 100644 --- a/models/forgejo_migrations/migrate_test.go +++ b/models/forgejo_migrations/migrate_test.go @@ -44,7 +44,7 @@ func TestRegisterMigration(t *testing.T) { "v99b_neat_migration.go", // no leading path "vb_neat_migration.go", // no version number "v12_neat_migration.go", // no migration group letter - "v12a-neat-migration.go", // no undescore + "v12a-neat-migration.go", // no underscore "v12a.go", // no descriptive identifier } { t.Run(fmt.Sprintf("bad name - %s", fn), func(t *testing.T) { diff --git a/models/forgejo_migrations/v14a_actions-approval-and-trust.go b/models/forgejo_migrations/v14a_actions-approval-and-trust.go index 657d512548..f8be109dda 100644 --- a/models/forgejo_migrations/v14a_actions-approval-and-trust.go +++ b/models/forgejo_migrations/v14a_actions-approval-and-trust.go @@ -6,7 +6,6 @@ package forgejo_migrations import ( "context" - actions_model "forgejo.org/models/actions" "forgejo.org/models/db" "forgejo.org/modules/log" "forgejo.org/modules/timeutil" @@ -41,7 +40,7 @@ func v14ActionsApprovalAndTrustCreateTableActionUser(x *xorm.Engine) error { LastAccess timeutil.TimeStamp `xorm:"INDEX"` } - return x.Sync(new(ActionUser)) + return x.Sync(new(ActionUser)) // nosemgrep:xorm-sync-missing-ignore-drop-indices } func v14ActionsApprovalAndTrustAddActionsRunFields(x *xorm.Engine) error { @@ -59,6 +58,18 @@ type v14ActionsApprovalAndTrustTrusted struct { } func v14ActionsApprovalAndTrustPopulateTableActionUser(x *xorm.Engine) error { + type ActionUser struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(user, id)"` + RepoID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(repository, id)"` + TrustedWithPullRequests bool + LastAccess timeutil.TimeStamp `xorm:"INDEX"` + } + insertActionUser := func(ctx context.Context, user *ActionUser) error { + user.LastAccess = timeutil.TimeStampNow() + return db.Insert(ctx, user) + } + // // Users approved once were trusted before and are trusted now. // @@ -87,7 +98,7 @@ func v14ActionsApprovalAndTrustPopulateTableActionUser(x *xorm.Engine) error { if err := db.WithTx(db.DefaultContext, func(ctx context.Context) error { for _, trusted := range trustedList { log.Debug("v14a_actions-approval-and-trust: repository %d trusts user %d", trusted.RepoID, trusted.UserID) - if err := actions_model.InsertActionUser(ctx, &actions_model.ActionUser{ + if err := insertActionUser(ctx, &ActionUser{ RepoID: trusted.RepoID, UserID: trusted.UserID, TrustedWithPullRequests: true, diff --git a/models/forgejo_migrations/v14a_actions-approval-and-trust_test.go b/models/forgejo_migrations/v14a_actions-approval-and-trust_test.go index c639a0d2e9..8ff1b1c066 100644 --- a/models/forgejo_migrations/v14a_actions-approval-and-trust_test.go +++ b/models/forgejo_migrations/v14a_actions-approval-and-trust_test.go @@ -7,11 +7,8 @@ import ( "testing" "time" - actions_model "forgejo.org/models/actions" "forgejo.org/models/db" migration_tests "forgejo.org/models/gitea_migrations/test" - repo_model "forgejo.org/models/repo" - user_model "forgejo.org/models/user" "forgejo.org/modules/timeutil" webhook_module "forgejo.org/modules/webhook" @@ -20,6 +17,9 @@ import ( ) func Test_v14ActionsApprovalAndTrustPopulateTableActionUser(t *testing.T) { + type ConcurrencyMode int + type Status int + type ActionUser struct { ID int64 `xorm:"pk autoincr"` UserID int64 `xorm:"INDEX UNIQUE(action_user_index) REFERENCES(user, id)"` @@ -32,21 +32,18 @@ func Test_v14ActionsApprovalAndTrustPopulateTableActionUser(t *testing.T) { type ActionRun struct { ID int64 Title string - RepoID int64 `xorm:"index unique(repo_index) index(concurrency)"` - Repo *repo_model.Repository `xorm:"-"` - OwnerID int64 `xorm:"index"` - WorkflowID string `xorm:"index"` // the name of workflow file - 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:"-"` + RepoID int64 `xorm:"index unique(repo_index) index(concurrency)"` + OwnerID int64 `xorm:"index"` + WorkflowID string `xorm:"index"` // the name of workflow file + Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository + TriggerUserID int64 `xorm:"index"` ScheduleID int64 Ref string `xorm:"index"` // the commit/tag/… that caused the run - IsRefDeleted bool `xorm:"-"` CommitSHA string Event webhook_module.HookEventType // the webhook event that causes the workflow to run EventPayload string `xorm:"LONGTEXT"` TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow - Status actions_model.Status `xorm:"index"` + Status Status `xorm:"index"` Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed // Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 Started timeutil.TimeStamp @@ -65,7 +62,7 @@ func Test_v14ActionsApprovalAndTrustPopulateTableActionUser(t *testing.T) { ApprovedBy int64 `xorm:"index"` ConcurrencyGroup string `xorm:"'concurrency_group' index(concurrency)"` - ConcurrencyType actions_model.ConcurrencyMode + ConcurrencyType ConcurrencyMode PreExecutionError string `xorm:"LONGTEXT"` // used to report errors that blocked execution of a workflow } @@ -83,10 +80,10 @@ func Test_v14ActionsApprovalAndTrustPopulateTableActionUser(t *testing.T) { require.NoError(t, v14ActionsApprovalAndTrustPopulateTableActionUser(x)) - var users []*actions_model.ActionUser + var users []*ActionUser require.NoError(t, db.GetEngine(t.Context()).Select("`repo_id`, `user_id`").OrderBy("`id`").Find(&users)) // See models/gitea_migrations/fixtures/Test_v14ActionsApprovalAndTrustPopulateTableActionUser/action_run.yml - assert.Equal(t, []*actions_model.ActionUser{ + assert.Equal(t, []*ActionUser{ { UserID: 3, RepoID: 15, diff --git a/models/forgejo_migrations/v14a_add-forgejo-migrations-table.go b/models/forgejo_migrations/v14a_add-forgejo-migrations-table.go index f78eec8789..8f22983e80 100644 --- a/models/forgejo_migrations/v14a_add-forgejo-migrations-table.go +++ b/models/forgejo_migrations/v14a_add-forgejo-migrations-table.go @@ -21,5 +21,5 @@ func addForgejoMigration(x *xorm.Engine) error { ID string `xorm:"pk"` CreatedUnix timeutil.TimeStamp `xorm:"created"` } - return x.Sync(new(ForgejoMigration)) + return x.Sync(new(ForgejoMigration)) // nosemgrep:xorm-sync-missing-ignore-drop-indices } diff --git a/models/forgejo_migrations/v14a_ap-change-fedi-handle-structure.go b/models/forgejo_migrations/v14a_ap-change-fedi-handle-structure.go new file mode 100644 index 0000000000..a412ceb737 --- /dev/null +++ b/models/forgejo_migrations/v14a_ap-change-fedi-handle-structure.go @@ -0,0 +1,179 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Due to a mistake during code review, this code was merged with the prefix 14a +// but this code was merged for the v15 cycle, the correct prefix would be 15a. +// As it would lead to breakage for instance who already ran with the old prefix +// the incorrect prefix is kept. + +package forgejo_migrations + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "forgejo.org/models/db" + "forgejo.org/modules/log" + "forgejo.org/modules/timeutil" + "forgejo.org/modules/validation" + + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "use structure @PreferredUsername@host.tld:port for actors", + Upgrade: changeActivityPubUsernameFormat, + }) +} + +func changeActivityPubUsernameFormat(x *xorm.Engine) error { + type FederationHost struct { + ID int64 `xorm:"pk autoincr"` + HostFqdn string `xorm:"host_fqdn UNIQUE(federation_host) INDEX VARCHAR(255) NOT NULL"` + HostPort uint16 `xorm:" UNIQUE(federation_host) INDEX NOT NULL DEFAULT 443"` + HostSchema string `xorm:"NOT NULL DEFAULT 'https'"` + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + } + type FederatedUser struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL INDEX user_id"` + ExternalID string `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` + FederationHostID int64 `xorm:"UNIQUE(federation_user_mapping) NOT NULL"` + KeyID sql.NullString `xorm:"key_id UNIQUE"` + PublicKey sql.Null[sql.RawBytes] `xorm:"BLOB"` + InboxPath string + NormalizedOriginalURL string // This field is just to keep original information. Pls. do not use for search or as ID! + } + type User struct { + ID int64 `xorm:"pk autoincr"` + LowerName string `xorm:"UNIQUE NOT NULL"` + Name string `xorm:"UNIQUE NOT NULL"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + deleteFederatedUser := func(ctx context.Context, userID int64) error { + _, err := db.GetEngine(ctx).Delete(&FederatedUser{UserID: userID}) + return err + } + userLogString := func(u *User) string { + if u == nil { + return "" + } + return fmt.Sprintf("", u.ID, u.Name) + } + + // Normally, the db.WithTx statement ensures that the database transaction (aka. all changes made + // by this migration) will only be committed if the SQL operations inside of the iteration + // (db.Iterate) don't return an error. + // + // This migration was originally authored with those cases in mind, but it was later agreed that + // migrations concerning Forgejo's federation-related components should not return any errors at + // this point in time, as federation is not considered to be stable at the moment. For more + // information, check the relevant discussion here: + // https://codeberg.org/forgejo-contrib/federation/issues/67 + // + // Nevertheless, this structure involves some useful boilerplate that can be used for future + // migrations at a later point and has been kept as-is. + return db.WithTx(db.DefaultContext, func(ctx context.Context) error { + // The transaction is committed only if modifying all federated users is possible. + return db.Iterate(ctx, nil, func(ctx context.Context, federatedUser *FederatedUser) error { + // localUser represents the "local" representation of an ActivityPub (federated) user + localUser := &User{} + has, err := db.GetEngine(ctx).ID(federatedUser.UserID).Get(localUser) + if err != nil { + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while getting local user (ID: %d), ignoring...: %e", federatedUser.UserID, err) + return nil + } + + if !has { + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: User missing for federated user: %v", federatedUser) + err := deleteFederatedUser(ctx, federatedUser.UserID) + if err != nil { + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while deleting federated user (%s), ignoring...: %e", federatedUser, err) + return nil + } + } + + if validation.IsValidActivityPubUsername(localUser.Name) { + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: FederatedUser was already migrated: %v", federatedUser) + } else { + // Copied from models/forgefed/federationhost_repository.go (forgefed.GetFederationHost), + // minus some validation code for FederationHost which we do not otherwise manipulate here. + federationHost := new(FederationHost) + has, err := db.GetEngine(ctx).ID(federatedUser.FederationHostID).Get(federationHost) + if err != nil { + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while looking up federation host info (for %v), ignoring...: %e", federatedUser, err) + return nil + } else if !has { + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Federation host for federated user %s is missing", federatedUser) + return nil + } + + // Take part of the username before the first dash, reconstruct the rest + // of it using whatever we have in FederationHost. Before this migration, + // usernames of ActivityPub accounts have an expected format of + // "username-subdomain-domain-tld-port". We don't know how many subdomains + // there are, but that doesn't matter. We can always get the username unless + // if the username of an ActivityPub account was manually changed by an admin, + // in which case they should either delete the account or change it back. + s := strings.Split(localUser.Name, "-") + if len(s) == 0 { + log.Warn( + "Migration[v14a_ap-change-fedi-handle-structure]: Username %s belonging to federatedUser %v does not contain any dashes, can't construct new username", + localUser.Name, + federatedUser, + ) + return nil + } + + // Were a running Forgejo instance to create a new federated account, would the port + // have been marked as "supplemented" (thus leading to its omission)? + var newUsername string + if (federationHost.HostPort == 443 && federationHost.HostSchema == "https") || (federationHost.HostPort == 80 && federationHost.HostSchema == "http") { + newUsername = fmt.Sprintf("@%s@%s", s[0], federationHost.HostFqdn) + } else { + newUsername = fmt.Sprintf("@%s@%s:%d", s[0], federationHost.HostFqdn, federationHost.HostPort) + } + + // Implicitly assumes that there won't be a lower name unique constraint violation. + // Potentially a bit paranoid, but why not? + userThatShouldntExist := &User{} + lowernameTaken, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(newUsername)).Table("user").Get(userThatShouldntExist) + if err != nil { + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred, skipping migration of %s: %e", userLogString(localUser), err) + return nil + } + + if lowernameTaken { + log.Warn( + "Migration[v14a_ap-change-fedi-handle-structure]: New username %s for %s already taken by %s, deleting the former...", + newUsername, + userLogString(localUser), + userLogString(userThatShouldntExist), + ) + err := deleteFederatedUser(ctx, localUser.ID) + if err != nil { + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred while deleting federated user (%s), ignoring...: %e", userLogString(localUser), err) + } + return nil + } + + // Safe to assume that the following operations should just work now. + log.Info("Migration[v14a_ap-change-fedi-handle-structure]: Updating username of %s to %s", userLogString(localUser), newUsername) + if _, err := db.GetEngine(ctx).ID(localUser.ID).Cols("lower_name", "name").Update(&User{ + LowerName: strings.ToLower(newUsername), + Name: newUsername, + }); err != nil { + log.Warn("Migration[v14a_ap-change-fedi-handle-structure]: Database error occurred when updating federated user's username (%s), ignoring...: %e", userLogString(localUser), err) + return nil + } + } + + return nil + }) + }) +} diff --git a/models/forgejo_migrations/v14a_migrate_task_secrets.go b/models/forgejo_migrations/v14a_migrate_task_secrets.go index a177dff92a..3484a024b2 100644 --- a/models/forgejo_migrations/v14a_migrate_task_secrets.go +++ b/models/forgejo_migrations/v14a_migrate_task_secrets.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "fmt" - admin_model "forgejo.org/models/admin" "forgejo.org/models/db" "forgejo.org/modules/json" "forgejo.org/modules/keying" @@ -17,6 +16,7 @@ import ( "forgejo.org/modules/secret" "forgejo.org/modules/setting" "forgejo.org/modules/structs" + "forgejo.org/modules/timeutil" "xorm.io/builder" "xorm.io/xorm" @@ -30,6 +30,19 @@ func init() { } func migrateTaskSecrets(x *xorm.Engine) error { + type Task struct { + ID int64 + DoerID int64 `xorm:"index"` + OwnerID int64 `xorm:"index"` + RepoID int64 `xorm:"index"` + PayloadContent string `xorm:"TEXT"` + Created timeutil.TimeStamp `xorm:"created"` + } + taskUpdateCols := func(ctx context.Context, task *Task, cols ...string) error { + _, err := db.GetEngine(ctx).ID(task.ID).Cols(cols...).Update(task) + return err + } + return db.WithTx(db.DefaultContext, func(ctx context.Context) error { sess := db.GetEngine(ctx) @@ -39,7 +52,7 @@ func migrateTaskSecrets(x *xorm.Engine) error { messages := make([]string, 0, 100) ids := make([]int64, 0, 100) - err := db.Iterate(ctx, builder.Eq{"type": structs.TaskTypeMigrateRepo}, func(ctx context.Context, bean *admin_model.Task) error { + err := db.Iterate(ctx, builder.Eq{"type": structs.TaskTypeMigrateRepo}, func(ctx context.Context, bean *Task) error { var opts migration.MigrateOptions err := json.Unmarshal([]byte(bean.PayloadContent), &opts) if err != nil { @@ -96,7 +109,7 @@ func migrateTaskSecrets(x *xorm.Engine) error { } bean.PayloadContent = string(bs) - return bean.UpdateCols(ctx, "payload_content") + return taskUpdateCols(ctx, bean, "payload_content") }) if err == nil { @@ -106,7 +119,7 @@ func migrateTaskSecrets(x *xorm.Engine) error { log.Error("v14a_migrate_task_secrets: %s", message) } - _, err = sess.In("id", ids).NoAutoCondition().NoAutoTime().Delete(&admin_model.Task{}) + _, err = sess.In("id", ids).NoAutoCondition().NoAutoTime().Delete(&Task{}) } } return err diff --git a/models/forgejo_migrations/v14a_migrate_webhook_authorization.go b/models/forgejo_migrations/v14a_migrate_webhook_authorization.go new file mode 100644 index 0000000000..5921329b3e --- /dev/null +++ b/models/forgejo_migrations/v14a_migrate_webhook_authorization.go @@ -0,0 +1,101 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "context" + "fmt" + + "forgejo.org/models/db" + "forgejo.org/modules/keying" + "forgejo.org/modules/log" + "forgejo.org/modules/secret" + "forgejo.org/modules/setting" + "forgejo.org/modules/timeutil" + + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +func init() { + registerMigration(&Migration{ + Description: "migrate `header_authorization_encrypted` of `webhook` table to store keying material", + Upgrade: migrateWebhookSecrets, + }) +} + +func migrateWebhookSecrets(x *xorm.Engine) error { + type Webhook struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook + OwnerID int64 `xorm:"INDEX"` + HeaderAuthorizationEncrypted []byte `xorm:"BLOB"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + + return db.WithTx(db.DefaultContext, func(ctx context.Context) error { + sess := db.GetEngine(ctx) + + switch x.Dialect().URI().DBType { + case schemas.MYSQL: + if _, err := sess.Exec("ALTER TABLE `webhook` MODIFY `header_authorization_encrypted` BLOB"); err != nil { + return err + } + case schemas.SQLITE: + if _, err := sess.Exec("ALTER TABLE `webhook` RENAME COLUMN `header_authorization_encrypted` TO `header_authorization_encrypted_backup`"); err != nil { + return err + } + if _, err := sess.Exec("ALTER TABLE `webhook` ADD COLUMN `header_authorization_encrypted` BLOB"); err != nil { + return err + } + if _, err := sess.Exec("UPDATE `webhook` SET `header_authorization_encrypted` = `header_authorization_encrypted_backup`"); err != nil { + return err + } + if _, err := sess.Exec("ALTER TABLE `webhook` DROP COLUMN `header_authorization_encrypted_backup`"); err != nil { + return err + } + case schemas.POSTGRES: + if _, err := sess.Exec("ALTER TABLE `webhook` ALTER COLUMN `header_authorization_encrypted` SET DATA TYPE bytea USING header_authorization_encrypted::text::bytea"); err != nil { + return err + } + } + + key := keying.Webhook + + oldEncryptionKey := setting.SecretKey + messages := make([]string, 0, 100) + ids := make([]int64, 0, 100) + + err := db.Iterate(ctx, nil, func(ctx context.Context, bean *Webhook) error { + if len(bean.HeaderAuthorizationEncrypted) == 0 { + return nil + } + + secretBytes, err := secret.DecryptSecret(oldEncryptionKey, string(bean.HeaderAuthorizationEncrypted)) + if err != nil { + messages = append(messages, fmt.Sprintf("webhook.id=%d, webhook.repo_id=%d, webhook.owner_id=%d: secret.DecryptSecret(): %v", bean.ID, bean.RepoID, bean.OwnerID, err)) + ids = append(ids, bean.ID) + return nil + } + + bean.HeaderAuthorizationEncrypted = key.Encrypt([]byte(secretBytes), keying.ColumnAndID("header_authorization_encrypted", bean.ID)) + _, err = sess.Cols("header_authorization_encrypted").ID(bean.ID).Update(bean) + return err + }) + + if err == nil { + if len(ids) > 0 { + log.Error("migration[v14a_migrate_webhook_authorization]: The following webhook were found to be corrupted and removed from the database.") + for _, message := range messages { + log.Error("migration[v14a_migrate_webhook_authorization]: %s", message) + } + + _, err = sess.In("id", ids).NoAutoCondition().NoAutoTime().Delete(&Webhook{}) + } + } + return err + }) +} diff --git a/models/forgejo_migrations/v14a_migrate_webhook_authorization_test.go b/models/forgejo_migrations/v14a_migrate_webhook_authorization_test.go new file mode 100644 index 0000000000..9da06f4baf --- /dev/null +++ b/models/forgejo_migrations/v14a_migrate_webhook_authorization_test.go @@ -0,0 +1,97 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "testing" + + migration_tests "forgejo.org/models/gitea_migrations/test" + "forgejo.org/modules/keying" + "forgejo.org/modules/timeutil" + webhook_module "forgejo.org/modules/webhook" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_MigrateWebhookSecrets(t *testing.T) { + type HookContentType int + type Webhook struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OwnerID int64 `xorm:"INDEX"` + IsSystemWebhook bool + URL string `xorm:"url TEXT"` + HTTPMethod string `xorm:"http_method"` + ContentType HookContentType + Secret string `xorm:"TEXT"` + Events string `xorm:"TEXT"` + IsActive bool `xorm:"INDEX"` + Type webhook_module.HookType `xorm:"VARCHAR(16) 'type'"` + Meta string `xorm:"TEXT"` + LastStatus webhook_module.HookStatus + + HeaderAuthorizationEncrypted string `xorm:"TEXT"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + + type NewWebhook struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + OwnerID int64 `xorm:"INDEX"` + IsSystemWebhook bool + URL string `xorm:"url TEXT"` + HTTPMethod string `xorm:"http_method"` + ContentType HookContentType + Secret string `xorm:"TEXT"` + Events string `xorm:"TEXT"` + IsActive bool `xorm:"INDEX"` + Type webhook_module.HookType `xorm:"VARCHAR(16) 'type'"` + Meta string `xorm:"TEXT"` + LastStatus webhook_module.HookStatus + + HeaderAuthorizationEncrypted []byte `xorm:"BLOB"` + + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + } + + // Prepare and load the testing database + x, deferable := migration_tests.PrepareTestEnv(t, 0, new(Webhook)) + defer deferable() + if x == nil || t.Failed() { + return + } + + cnt, err := x.Table("webhook").Count() + require.NoError(t, err) + assert.EqualValues(t, 3, cnt) + + require.NoError(t, migrateWebhookSecrets(x)) + + cnt, err = x.Table("webhook").Count() + require.NoError(t, err) + assert.EqualValues(t, 2, cnt) + + key := keying.Webhook + + t.Run("webhook 1", func(t *testing.T) { + var webhook NewWebhook + _, err = x.Table("webhook").ID(1).Get(&webhook) + require.NoError(t, err) + + secret, err := key.Decrypt(webhook.HeaderAuthorizationEncrypted, keying.ColumnAndID("header_authorization_encrypted", webhook.ID)) + require.NoError(t, err) + assert.EqualValues(t, "Bearer s3cr3t", secret) + }) + + t.Run("webhook 3", func(t *testing.T) { + var webhook NewWebhook + _, err = x.Table("webhook").ID(3).Get(&webhook) + require.NoError(t, err) + assert.Empty(t, webhook.HeaderAuthorizationEncrypted) + }) +} diff --git a/models/forgejo_migrations/v14a_rework-notification.go b/models/forgejo_migrations/v14a_rework-notification.go index 77ae79d86f..04303559e8 100644 --- a/models/forgejo_migrations/v14a_rework-notification.go +++ b/models/forgejo_migrations/v14a_rework-notification.go @@ -4,7 +4,6 @@ package forgejo_migrations import ( - activities_model "forgejo.org/models/activities" "forgejo.org/modules/setting" "xorm.io/xorm" @@ -18,9 +17,10 @@ func init() { } func reworkNotification(x *xorm.Engine) error { + type NotificationStatus uint8 type Notification struct { - UserID int64 `xorm:"NOT NULL INDEX(s)"` - Status activities_model.NotificationStatus `xorm:"SMALLINT NOT NULL INDEX(s)"` + UserID int64 `xorm:"NOT NULL INDEX(s)"` + Status NotificationStatus `xorm:"SMALLINT NOT NULL INDEX(s)"` } if err := dropIndexIfExists(x, "notification", "IDX_notification_user_id"); err != nil { diff --git a/models/forgejo_migrations/v14a_set_remote_user_prohibit_login.go b/models/forgejo_migrations/v14a_set_remote_user_prohibit_login.go index 3575dad832..9f453e05f3 100644 --- a/models/forgejo_migrations/v14a_set_remote_user_prohibit_login.go +++ b/models/forgejo_migrations/v14a_set_remote_user_prohibit_login.go @@ -5,10 +5,11 @@ package forgejo_migrations import ( "context" + "fmt" "forgejo.org/models/db" - user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/modules/timeutil" "xorm.io/builder" "xorm.io/xorm" @@ -22,13 +23,45 @@ func init() { } func setProhibitLoginActivityPubUser(x *xorm.Engine) error { + type UserType int + const ( + UserTypeIndividual UserType = iota // Historic reason to make it starts at 0. + UserTypeOrganization // 1 + UserTypeUserReserved // 2 + UserTypeOrganizationReserved // 3 + UserTypeBot // 4 + UserTypeRemoteUser // 5 + UserTypeActivityPubUser // 6 + ) + type User struct { + ID int64 `xorm:"pk autoincr"` + Name string `xorm:"UNIQUE NOT NULL"` + Passwd string `xorm:"NOT NULL"` + PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'argon2'"` + Type UserType + Salt string `xorm:"VARCHAR(32)"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` + UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` + ProhibitLogin bool `xorm:"NOT NULL DEFAULT false"` + } + type FederatedUser struct { + UserID int64 `xorm:"NOT NULL INDEX user_id"` + } + + userLogString := func(u *User) string { + if u == nil { + return "" + } + return fmt.Sprintf("", u.ID, u.Name) + } + return db.WithTx(db.DefaultContext, func(ctx context.Context) error { - return db.Iterate(ctx, builder.Eq{"type": 5}, func(ctx context.Context, user *user_model.User) error { - log.Info("Checking if user %s is created from ActivityPub", user.LogString()) + return db.Iterate(ctx, builder.Eq{"type": 5}, func(ctx context.Context, user *User) error { + log.Info("Checking if user %s is created from ActivityPub", userLogString(user)) // Users created from f3 also have the RemoteUser user type. All // FederatedUser should reference exactly one User. - has, err := db.GetEngine(ctx).Table("federated_user").Get(&user_model.FederatedUser{UserID: user.ID}) + has, err := db.GetEngine(ctx).Table("federated_user").Get(&FederatedUser{UserID: user.ID}) if err != nil { return err } @@ -37,9 +70,9 @@ func setProhibitLoginActivityPubUser(x *xorm.Engine) error { return nil } - log.Info("Updating user %s", user.LogString()) - _, err = db.GetEngine(ctx).Table("user").ID(user.ID).Cols("type", "prohibit_login", "passwd", "salt", "passwd_hash_algo").Update(&user_model.User{ - Type: user_model.UserTypeActivityPubUser, + log.Info("Updating user %s", userLogString(user)) + _, err = db.GetEngine(ctx).Table("user").ID(user.ID).Cols("type", "prohibit_login", "passwd", "salt", "passwd_hash_algo").Update(&User{ + Type: UserTypeActivityPubUser, ProhibitLogin: true, Passwd: "", Salt: "", diff --git a/models/forgejo_migrations/v14b_action-reindexing.go b/models/forgejo_migrations/v14b_action-reindexing.go index 6b5608a5d5..83dc452191 100644 --- a/models/forgejo_migrations/v14b_action-reindexing.go +++ b/models/forgejo_migrations/v14b_action-reindexing.go @@ -69,5 +69,5 @@ func reworkActionIndexes(x *xorm.Engine) error { return err } - return x.Sync(new(v14bAction)) + return x.Sync(new(v14bAction)) // nosemgrep:xorm-sync-missing-ignore-drop-indices } diff --git a/models/forgejo_migrations/v15a_remove-softdelete-action_runner_token.go b/models/forgejo_migrations/v15a_remove-softdelete-action_runner_token.go new file mode 100644 index 0000000000..c0f2e964a9 --- /dev/null +++ b/models/forgejo_migrations/v15a_remove-softdelete-action_runner_token.go @@ -0,0 +1,35 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "xorm.io/builder" + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "remove soft-delete capability from action_runner_token", + Upgrade: removeSoftDeleteActionRunnerToken, + }) +} + +func removeSoftDeleteActionRunnerToken(x *xorm.Engine) error { + // ActionRunnerToken was implemented with a column: "Deleted timeutil.TimeStamp `xorm:"deleted"``", which invokes + // xorm's soft-delete capability -- that is, if a record is deleted from the table, then it is just marked with a + // delete timestamp which causes it to be automatically excluded from future queries. This functionality is not + // used on `ActionRunnerToken` and it stands in the way of foreign key implementation -- if you can't actually + // delete the record in the table, then you can't remove foreign key references and therefore can't delete contents + // of the target tables, repository and user. + // + // This migration removes that column and deletes any records that were soft-deleted. + + // Before dropping the 'deleted' column, hard-delete any soft-deleted records. + if _, err := x.Table("action_runner_token").Where(builder.NotNull{"deleted"}).Delete(); err != nil { + return err + } + + _, err := x.Exec("ALTER TABLE action_runner_token DROP COLUMN `deleted`") + return err +} diff --git a/models/forgejo_migrations/v15a_remove-softdelete-action_runner_token_test.go b/models/forgejo_migrations/v15a_remove-softdelete-action_runner_token_test.go new file mode 100644 index 0000000000..7a2c80e547 --- /dev/null +++ b/models/forgejo_migrations/v15a_remove-softdelete-action_runner_token_test.go @@ -0,0 +1,51 @@ +// Copyright 2026 The Forgejo Authors. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "testing" + + "forgejo.org/models/db" + migration_tests "forgejo.org/models/gitea_migrations/test" + "forgejo.org/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_removeSoftDeleteActionRunnerToken(t *testing.T) { + type ActionRunnerToken struct { + ID int64 + Token string `xorm:"UNIQUE"` + OwnerID int64 `xorm:"index"` + RepoID int64 `xorm:"index"` + IsActive bool + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + Deleted timeutil.TimeStamp `xorm:"deleted"` + } + x, deferable := migration_tests.PrepareTestEnv(t, 0, new(ActionRunnerToken)) + defer deferable() + if x == nil || t.Failed() { + return + } + + require.NoError(t, removeSoftDeleteActionRunnerToken(x)) + + var remainingRecords []*ActionRunnerToken + require.NoError(t, + db.GetEngine(t.Context()). + Table("action_runner_token"). + Select("`id`, `owner_id`, `repo_id`"). + OrderBy("`id`"). + Unscoped(). // `Deleted` column doesn't exist anymore, so don't include in query + Find(&remainingRecords)) + assert.Equal(t, + []*ActionRunnerToken{ + {ID: 4}, + {ID: 5, OwnerID: 1}, + {ID: 6, RepoID: 1}, + }, + remainingRecords) +} diff --git a/models/forgejo_migrations/v15b_add-access_token-owned-repos.go b/models/forgejo_migrations/v15b_add-access_token-owned-repos.go new file mode 100644 index 0000000000..dd96c55c05 --- /dev/null +++ b/models/forgejo_migrations/v15b_add-access_token-owned-repos.go @@ -0,0 +1,23 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add resource_all_owned_repositories to table access_token", + Upgrade: addAllOwnedRepositoriesToAccessToken, + }) +} + +func addAllOwnedRepositoriesToAccessToken(x *xorm.Engine) error { + type AccessToken struct { + ResourceAllRepos bool `xorm:"NOT NULL DEFAULT TRUE"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(AccessToken)) + return err +} diff --git a/models/forgejo_migrations/v15b_add-access_token_resource.go b/models/forgejo_migrations/v15b_add-access_token_resource.go new file mode 100644 index 0000000000..2e4afbcd43 --- /dev/null +++ b/models/forgejo_migrations/v15b_add-access_token_resource.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "forgejo.org/modules/timeutil" + + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add access_token_resource table", + Upgrade: addAccessTokenResource, + }) +} + +func addAccessTokenResource(x *xorm.Engine) error { + type AccessTokenResourceRepo struct { + ID int64 `xorm:"pk autoincr"` + TokenID int64 `xorm:"NOT NULL REFERENCES(access_token, id)"` // needs to be shortened from "AccessTokenID" for the index to fit MySQL table identifier length restrictions + RepoID int64 `xorm:"NOT NULL REFERENCES(repository, id)"` + + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(AccessTokenResourceRepo)) + return err +} diff --git a/models/forgejo_migrations/v15b_add-ephemeral_runner.go b/models/forgejo_migrations/v15b_add-ephemeral_runner.go new file mode 100644 index 0000000000..746763aa66 --- /dev/null +++ b/models/forgejo_migrations/v15b_add-ephemeral_runner.go @@ -0,0 +1,24 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add ephemeral to action_runner", + Upgrade: addRunnerEphemeralField, + }) +} + +func addRunnerEphemeralField(x *xorm.Engine) error { + type ActionRunner struct { + Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"` + } + + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRunner)) + return err +} diff --git a/models/forgejo_migrations/v15b_add-foreign-keys-action_runner_token.go b/models/forgejo_migrations/v15b_add-foreign-keys-action_runner_token.go new file mode 100644 index 0000000000..584addf77b --- /dev/null +++ b/models/forgejo_migrations/v15b_add-foreign-keys-action_runner_token.go @@ -0,0 +1,49 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "xorm.io/builder" + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add foreign keys to action_runner_token", + Upgrade: addForeignKeysActionRunnerToken, + }) +} + +func addForeignKeysActionRunnerToken(x *xorm.Engine) error { + type ActionRunnerToken struct { + OwnerID int64 `xorm:"index REFERENCES(user, id)"` + RepoID int64 `xorm:"index REFERENCES(repository, id)"` + } + + // With the introduction of a foreign key, owner_id & repo_id cannot be set to "0". Runners can be registered as + // global (owner_id = NULL, repo_id = NULL), user/org (repo_id = NULL), or repo (owner_id = NULL) and NULL values + // now replace the '0' values. + _, err := x.Table(&ActionRunnerToken{}).Where("owner_id = 0").Update(map[string]any{"owner_id": nil}) + if err != nil { + return err + } + _, err = x.Table(&ActionRunnerToken{}).Where("repo_id = 0").Update(map[string]any{"repo_id": nil}) + if err != nil { + return err + } + + return syncForeignKeyWithDelete(x, + new(ActionRunnerToken), + builder.Or( + builder.And( + builder.Expr("owner_id IS NOT NULL"), + builder.Expr("NOT EXISTS (SELECT id FROM `user` WHERE `user`.id = action_runner_token.owner_id)"), + ), + builder.And( + builder.Expr("repo_id IS NOT NULL"), + builder.Expr("NOT EXISTS (SELECT id FROM repository WHERE repository.id = action_runner_token.repo_id)"), + ), + ), + ) +} diff --git a/models/forgejo_migrations/v15b_add-foreign-keys-action_runner_token_test.go b/models/forgejo_migrations/v15b_add-foreign-keys-action_runner_token_test.go new file mode 100644 index 0000000000..04589d8eaf --- /dev/null +++ b/models/forgejo_migrations/v15b_add-foreign-keys-action_runner_token_test.go @@ -0,0 +1,55 @@ +// Copyright 2026 The Forgejo Authors. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "testing" + + "forgejo.org/models/db" + migration_tests "forgejo.org/models/gitea_migrations/test" + "forgejo.org/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_addForeignKeysActionRunnerToken(t *testing.T) { + type ActionRunnerToken struct { + ID int64 + Token string `xorm:"UNIQUE"` + OwnerID int64 `xorm:"index"` + RepoID int64 `xorm:"index"` + IsActive bool + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated"` + } + type User struct { + ID int64 `xorm:"pk autoincr"` + } + type Repository struct { + ID int64 `xorm:"pk autoincr"` + } + x, deferable := migration_tests.PrepareTestEnv(t, 0, new(User), new(Repository), new(ActionRunnerToken)) + defer deferable() + if x == nil || t.Failed() { + return + } + + require.NoError(t, addForeignKeysActionRunnerToken(x)) + + var remainingRecords []*ActionRunnerToken + require.NoError(t, + db.GetEngine(t.Context()). + Table("action_runner_token"). + Select("`id`, `owner_id`, `repo_id`"). + OrderBy("`id`"). + Find(&remainingRecords)) + assert.Equal(t, + []*ActionRunnerToken{ + {ID: 1}, + {ID: 2, OwnerID: 1}, + {ID: 3, RepoID: 1}, + }, + remainingRecords) +} diff --git a/models/forgejo_migrations/v15b_add-runner_request_key.go b/models/forgejo_migrations/v15b_add-runner_request_key.go new file mode 100644 index 0000000000..fb8150469b --- /dev/null +++ b/models/forgejo_migrations/v15b_add-runner_request_key.go @@ -0,0 +1,24 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add runner_request_key to action_task", + Upgrade: addActionTaskRunnerRequestKey, + }) +} + +func addActionTaskRunnerRequestKey(x *xorm.Engine) error { + type ActionTask struct { + RunnerID int64 `xorm:"index index(request_key)"` + RunnerRequestKey string `xorm:"index(request_key)"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionTask)) + return err +} diff --git a/models/forgejo_migrations/v15c_add_job_handle.go b/models/forgejo_migrations/v15c_add_job_handle.go new file mode 100644 index 0000000000..59dca1b991 --- /dev/null +++ b/models/forgejo_migrations/v15c_add_job_handle.go @@ -0,0 +1,23 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add handle to action_run_job", + Upgrade: addActionRunJobHandle, + }) +} + +func addActionRunJobHandle(x *xorm.Engine) error { + type ActionRunJob struct { + Handle string `xorm:"unique"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRunJob)) + return err +} diff --git a/models/forgejo_migrations/v15c_add_mirror_remoteaddressauth.go b/models/forgejo_migrations/v15c_add_mirror_remoteaddressauth.go new file mode 100644 index 0000000000..b2f30235ad --- /dev/null +++ b/models/forgejo_migrations/v15c_add_mirror_remoteaddressauth.go @@ -0,0 +1,30 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "replace remote_address with encrypted_remote_address in table mirror", + Upgrade: addMirrorRemoteAddressAuth, + }) +} + +func addMirrorRemoteAddressAuth(x *xorm.Engine) error { + type Mirror struct { + EncryptedRemoteAddress []byte `xorm:"BLOB NULL"` + } + if _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(Mirror)); err != nil { + return err + } + // No data migration is necessary or desired. `remote_address` contains sanitized URLs which don't have + // credentials, so they can't be migrated to `encrypted_remote_address`. Instead, as this data is accessed, + // `DecryptOrRecoverRemoteAddress` will recover the fully credentialed contents of the remote address from the git + // repo's `origin` remote address. + _, err := x.Exec("ALTER TABLE `mirror` DROP COLUMN `remote_address`") + return err +} diff --git a/models/forgejo_migrations/v15c_add_schedule_spec_time_zones.go b/models/forgejo_migrations/v15c_add_schedule_spec_time_zones.go new file mode 100644 index 0000000000..d72d585725 --- /dev/null +++ b/models/forgejo_migrations/v15c_add_schedule_spec_time_zones.go @@ -0,0 +1,31 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "forgejo.org/modules/optional" + + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add time zone support to action_schedule_spec", + Upgrade: addActionScheduleSpecTimeZone, + }) +} + +func addActionScheduleSpecTimeZone(x *xorm.Engine) error { + type ActionScheduleSpec struct { + TimeZone optional.Option[string] + } + + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionScheduleSpec)) + if err != nil { + return err + } + + _, err = x.Exec("ALTER TABLE action_schedule DROP COLUMN `specs`") + return err +} diff --git a/models/forgejo_migrations/v15c_fix-project-sorting-unique-constraints.go b/models/forgejo_migrations/v15c_fix-project-sorting-unique-constraints.go new file mode 100644 index 0000000000..9fc28d523c --- /dev/null +++ b/models/forgejo_migrations/v15c_fix-project-sorting-unique-constraints.go @@ -0,0 +1,218 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "fmt" + + "forgejo.org/models/gitea_migrations/base" + "forgejo.org/modules/setting" + + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "Fix duplicate project sorting values and add unique constraints", + Upgrade: fixProjectSortingUniqueConstraints, + }) +} + +func fixProjectSortingUniqueConstraints(x *xorm.Engine) error { + // Step 1: Fix existing duplicates in project_issue (cards) + // Reassign sequential sorting values within each column + if err := fixProjectIssueDuplicates(x); err != nil { + return err + } + + // Step 2: Fix existing duplicates in project_board (columns) + // Reassign sequential sorting values within each project + if err := fixProjectBoardDuplicates(x); err != nil { + return err + } + + // Step 3: Remove duplicate (project_id, issue_id) rows keeping the lowest id + if err := fixProjectIssueDuplicateAssignments(x); err != nil { + return err + } + + // Step 4: Add unique constraints (idempotent — skip if already exists) + if err := createUniqueIndexIfNotExists(x, "project_issue", "UQE_project_issue_column_sorting", "project_board_id, sorting"); err != nil { + return err + } + if err := createUniqueIndexIfNotExists(x, "project_issue", "UQE_project_issue_project_issue", "project_id, issue_id"); err != nil { + return err + } + if err := createUniqueIndexIfNotExists(x, "project_board", "UQE_project_board_project_sorting", "project_id, sorting"); err != nil { + return err + } + + // Step 5: Enforce NOT NULL on project_issue columns. + // The struct tags declare NOT NULL but the DB may not enforce it. + // On SQLite, RecreateTables rebuilds the table with NOT NULL and unique constraints. + return enforceProjectIssueNotNull(x) +} + +func createUniqueIndexIfNotExists(x *xorm.Engine, tableName, indexName, columns string) error { + exists, err := indexExists(x, tableName, indexName) + if err != nil { + return err + } + if exists { + return nil + } + _, err = x.Exec(fmt.Sprintf("CREATE UNIQUE INDEX %s ON %s (%s)", indexName, tableName, columns)) + return err +} + +func fixProjectIssueDuplicates(x *xorm.Engine) error { + switch { + case setting.Database.Type.IsSQLite3(): + // SQLite: Use UPDATE with subquery + _, err := x.Exec(` + UPDATE project_issue SET sorting = ( + SELECT new_sort FROM ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY project_board_id ORDER BY sorting, id) - 1 as new_sort + FROM project_issue + ) ranked WHERE ranked.id = project_issue.id + ) + `) + return err + + case setting.Database.Type.IsPostgreSQL(): + // PostgreSQL: Use UPDATE FROM with subquery + _, err := x.Exec(` + UPDATE project_issue pi SET sorting = ranked.new_sort + FROM ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY project_board_id ORDER BY sorting, id) - 1 as new_sort + FROM project_issue + ) ranked + WHERE pi.id = ranked.id + `) + return err + + case setting.Database.Type.IsMySQL(): + // MySQL: Use UPDATE with JOIN + _, err := x.Exec(` + UPDATE project_issue pi + INNER JOIN ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY project_board_id ORDER BY sorting, id) - 1 as new_sort + FROM project_issue + ) ranked ON pi.id = ranked.id + SET pi.sorting = ranked.new_sort + `) + return err + + default: + return fmt.Errorf("unsupported db dialect type %v", x.Dialect().URI().DBType) + } +} + +func fixProjectBoardDuplicates(x *xorm.Engine) error { + switch { + case setting.Database.Type.IsSQLite3(): + // SQLite: Use UPDATE with subquery + _, err := x.Exec(` + UPDATE project_board SET sorting = ( + SELECT new_sort FROM ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY sorting, id) - 1 as new_sort + FROM project_board + ) ranked WHERE ranked.id = project_board.id + ) + `) + return err + + case setting.Database.Type.IsPostgreSQL(): + // PostgreSQL: Use UPDATE FROM with subquery + _, err := x.Exec(` + UPDATE project_board pb SET sorting = ranked.new_sort + FROM ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY sorting, id) - 1 as new_sort + FROM project_board + ) ranked + WHERE pb.id = ranked.id + `) + return err + + case setting.Database.Type.IsMySQL(): + // MySQL: Use UPDATE with JOIN + _, err := x.Exec(` + UPDATE project_board pb + INNER JOIN ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY project_id ORDER BY sorting, id) - 1 as new_sort + FROM project_board + ) ranked ON pb.id = ranked.id + SET pb.sorting = ranked.new_sort + `) + return err + + default: + return fmt.Errorf("unsupported db dialect type %v", x.Dialect().URI().DBType) + } +} + +func enforceProjectIssueNotNull(x *xorm.Engine) error { + switch { + case setting.Database.Type.IsSQLite3(): + type ProjectIssue struct { + ID int64 `xorm:"pk autoincr"` + IssueID int64 `xorm:"INDEX NOT NULL unique(project_issue)"` + ProjectID int64 `xorm:"INDEX NOT NULL unique(project_issue)"` + ProjectColumnID int64 `xorm:"'project_board_id' INDEX NOT NULL unique(column_sorting)"` + Sorting int64 `xorm:"NOT NULL DEFAULT 0 unique(column_sorting)"` + } + return base.RecreateTables(new(ProjectIssue))(x) + + case setting.Database.Type.IsPostgreSQL(): + for _, col := range []string{"issue_id", "project_id", "project_board_id"} { + if _, err := x.Exec(fmt.Sprintf("ALTER TABLE project_issue ALTER COLUMN %s SET NOT NULL", col)); err != nil { + return err + } + } + return nil + + case setting.Database.Type.IsMySQL(): + for _, col := range []string{"issue_id", "project_id", "project_board_id"} { + if _, err := x.Exec(fmt.Sprintf("ALTER TABLE project_issue MODIFY COLUMN %s BIGINT NOT NULL", col)); err != nil { + return err + } + } + return nil + + default: + return fmt.Errorf("unsupported db dialect type %v", x.Dialect().URI().DBType) + } +} + +// fixProjectIssueDuplicateAssignments removes duplicate (project_id, issue_id) rows, +// keeping only the row with the lowest id for each pair. +func fixProjectIssueDuplicateAssignments(x *xorm.Engine) error { + switch { + case setting.Database.Type.IsSQLite3(): + _, err := x.Exec(` + DELETE FROM project_issue WHERE id NOT IN ( + SELECT MIN(id) FROM project_issue GROUP BY project_id, issue_id + ) + `) + return err + + case setting.Database.Type.IsPostgreSQL(): + _, err := x.Exec(` + DELETE FROM project_issue pi USING project_issue pi2 + WHERE pi.project_id = pi2.project_id AND pi.issue_id = pi2.issue_id AND pi.id > pi2.id + `) + return err + + case setting.Database.Type.IsMySQL(): + _, err := x.Exec(` + DELETE pi FROM project_issue pi + INNER JOIN project_issue pi2 + ON pi.project_id = pi2.project_id AND pi.issue_id = pi2.issue_id AND pi.id > pi2.id + `) + return err + + default: + return fmt.Errorf("unsupported db dialect type %v", x.Dialect().URI().DBType) + } +} diff --git a/models/forgejo_migrations/v16a_add_authorized_integration.go b/models/forgejo_migrations/v16a_add_authorized_integration.go new file mode 100644 index 0000000000..4cfd5219f4 --- /dev/null +++ b/models/forgejo_migrations/v16a_add_authorized_integration.go @@ -0,0 +1,45 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "forgejo.org/modules/timeutil" + + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add authorized_integration tables", + Upgrade: addAuthorizedIntegrationTables, + }) +} + +func addAuthorizedIntegrationTables(x *xorm.Engine) error { + type ClaimRules struct{} + type AuthorizedIntegration struct { + ID int64 `xorm:"pk autoincr"` + UserID int64 `xorm:"NOT NULL REFERENCES(user, id)"` + Scope string `xorm:"NOT NULL"` + ResourceAllRepos bool `xorm:"NOT NULL"` + Issuer string `xorm:"NOT NULL UNIQUE(s)"` + Audience string `xorm:"NOT NULL UNIQUE(s)"` + ClaimRules *ClaimRules `xorm:"NOT NULL JSON"` + CreatedUnix timeutil.TimeStamp `xorm:"NOT NULL created"` + UpdatedUnix timeutil.TimeStamp `xorm:"NOT NULL updated"` + } + type AuthorizedIntegResourceRepo struct { + ID int64 `xorm:"pk autoincr"` + IntegID int64 `xorm:"NOT NULL REFERENCES(authorized_integration, id)"` + RepoID int64 `xorm:"NOT NULL REFERENCES(repository, id)"` + CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"` + } + + _, err := x.SyncWithOptions( + xorm.SyncOptions{IgnoreDropIndices: true}, + new(AuthorizedIntegration), + new(AuthorizedIntegResourceRepo), + ) + return err +} diff --git a/models/forgejo_migrations/v16a_add_oidcsubjectformat_actions_unit_config.go b/models/forgejo_migrations/v16a_add_oidcsubjectformat_actions_unit_config.go new file mode 100644 index 0000000000..efe737ad5a --- /dev/null +++ b/models/forgejo_migrations/v16a_add_oidcsubjectformat_actions_unit_config.go @@ -0,0 +1,68 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "context" + "fmt" + + "forgejo.org/models/db" + "forgejo.org/modules/json" + "forgejo.org/modules/optional" + + "xorm.io/builder" + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add OIDCSubjectFormat=legacy-forgejo-v15 to all existing repositories with actions enabled", + Upgrade: setOIDCSubjectFormatLegacy15, + }) +} + +func setOIDCSubjectFormatLegacy15(x *xorm.Engine) error { + type Type int + const TypeActions Type = 10 // 10 Actions + type ActionsConfig struct { + DisabledWorkflows []string `json:",omitempty"` + OIDCSubjectFormat string + } + type RepoUnit struct { //revive:disable-line:exported + ID int64 `xorm:"pk"` + Type Type `xorm:"INDEX(s)"` + Config optional.Option[string] `xorm:"TEXT"` + } + + return db.Iterate( + db.DefaultContext, + builder.Eq{"type": TypeActions}, + func(ctx context.Context, unit *RepoUnit) error { + has, config := unit.Config.Get() + if !has { + config = "{}" + } + var actionsConfig ActionsConfig + if err := json.Unmarshal([]byte(config), &actionsConfig); err != nil { + return fmt.Errorf("failed to parse Actions config %q: %w", config, err) + } + + actionsConfig.OIDCSubjectFormat = "legacy-forgejo-v15" + + configBytes, err := json.Marshal(actionsConfig) + if err != nil { + return fmt.Errorf("failed to convert Actions config to JSON: %w", err) + } + r, err := db.GetEngine(ctx). + ID(unit.ID). + Cols("config"). + Update(&RepoUnit{Config: optional.Some(string(configBytes))}) + if err != nil { + return err + } else if r != 1 { + return fmt.Errorf("UPDATE expected to affect 1 row, but was %d", r) + } + return nil + }) +} diff --git a/models/forgejo_migrations/v16a_add_oidcsubjectformat_actions_unit_config_test.go b/models/forgejo_migrations/v16a_add_oidcsubjectformat_actions_unit_config_test.go new file mode 100644 index 0000000000..865c0e394b --- /dev/null +++ b/models/forgejo_migrations/v16a_add_oidcsubjectformat_actions_unit_config_test.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Forgejo Authors. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "testing" + + "forgejo.org/models/db" + migration_tests "forgejo.org/models/gitea_migrations/test" + "forgejo.org/modules/timeutil" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/xorm/convert" +) + +func Test_setOIDCSubjectFormatLegacy15(t *testing.T) { + type Type int + type UnitAccessMode int + type RepoUnit struct { //revive:disable-line:exported + ID int64 + RepoID int64 `xorm:"INDEX(s)"` + Type Type `xorm:"INDEX(s)"` + Config convert.Conversion `xorm:"TEXT"` + CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"` + DefaultPermissions UnitAccessMode `xorm:"NOT NULL DEFAULT 0"` + } + x, deferable := migration_tests.PrepareTestEnv(t, 0, new(RepoUnit)) + defer deferable() + if x == nil || t.Failed() { + return + } + + require.NoError(t, setOIDCSubjectFormatLegacy15(x)) + + var records []map[string]string + require.NoError(t, + db.GetEngine(t.Context()). + Table("repo_unit"). + Select("`id`, `repo_id`, `config`"). + OrderBy("`id`"). + Find(&records)) + assert.Equal(t, []map[string]string{ + { + "config": "{\"OIDCSubjectFormat\":\"legacy-forgejo-v15\"}", + "id": "1", + "repo_id": "4", + }, + { + "config": "{\"DisabledWorkflows\":[\"renovate.yml\"],\"OIDCSubjectFormat\":\"legacy-forgejo-v15\"}", + "id": "2", + "repo_id": "5", + }, + { + "config": "", + "id": "3", + "repo_id": "6", + }, + }, records) +} diff --git a/models/forgejo_migrations/v16b_authorized_integration_name_description.go b/models/forgejo_migrations/v16b_authorized_integration_name_description.go new file mode 100644 index 0000000000..3f5450310f --- /dev/null +++ b/models/forgejo_migrations/v16b_authorized_integration_name_description.go @@ -0,0 +1,60 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package forgejo_migrations + +import ( + "context" + "fmt" + + "forgejo.org/models/db" + "forgejo.org/modules/timeutil" + + "xorm.io/xorm" +) + +func init() { + registerMigration(&Migration{ + Description: "add name & description to authorized_integration", + Upgrade: addAuthorizedIntegrationNameDescription, + }) +} + +func addAuthorizedIntegrationNameDescription(x *xorm.Engine) error { + type AuthorizedIntegration struct { + // New fields: + Name string + Description string `xorm:"LONGTEXT"` + + // Existing fields, used for UPDATE in migration: + ID int64 `xorm:"pk autoincr"` + Issuer string `xorm:"NOT NULL UNIQUE(s)"` + Audience string `xorm:"NOT NULL UNIQUE(s)"` + CreatedUnix timeutil.TimeStamp `xorm:"NOT NULL created"` + // don't include `UpdatedUnix`, so the updated timestamp isn't bumped when Name is set in migration + } + + _, err := x.SyncWithOptions( + xorm.SyncOptions{IgnoreDropIndices: true}, + new(AuthorizedIntegration), + ) + if err != nil { + return err + } + + // As v16a has creating this table, v16b will likely have no records for any users. But for developers working on + // v16, populate "Name" with a quick computed value: + return db.Iterate(db.DefaultContext, nil, func(ctx context.Context, ai *AuthorizedIntegration) error { + ai.Name = fmt.Sprintf("%s created %s", ai.Issuer, ai.CreatedUnix.FormatDate()) + r, err := db.GetEngine(ctx). + ID(ai.ID). + Cols("name"). + Update(ai) + if err != nil { + return err + } else if r != 1 { + return fmt.Errorf("UPDATE expected to affect 1 row, but was %d", r) + } + return nil + }) +} diff --git a/models/forgejo_migrations_legacy/v33.go b/models/forgejo_migrations_legacy/v33.go index ce220d8179..b29c5feeb3 100644 --- a/models/forgejo_migrations_legacy/v33.go +++ b/models/forgejo_migrations_legacy/v33.go @@ -52,7 +52,7 @@ func addFederatedUserActivityTables(x *xorm.Engine) { FollowingUserID int64 `xorm:"NOT NULL unique(fuf_rel)"` } - // Add InboxPath to FederatedUser & add index fo UserID + // Add InboxPath to FederatedUser & add index to UserID type FederatedUser struct { ID int64 `xorm:"pk autoincr"` UserID int64 `xorm:"NOT NULL INDEX user_id"` diff --git a/models/forgejo_migrations_legacy/v39.go b/models/forgejo_migrations_legacy/v39.go index 3f0d94b6aa..616404f7eb 100644 --- a/models/forgejo_migrations_legacy/v39.go +++ b/models/forgejo_migrations_legacy/v39.go @@ -58,7 +58,7 @@ func MigrateActionSecretsToKeying(x *xorm.Engine) error { return nil } - bean.SetSecret(secretBytes) + bean.SetData(secretBytes) _, err = sess.Cols("data").ID(bean.ID).Update(bean) return err }) diff --git a/models/git/branch_list.go b/models/git/branch_list.go index 4b678f15c0..07bcff8db9 100644 --- a/models/git/branch_list.go +++ b/models/git/branch_list.go @@ -77,8 +77,8 @@ func (opts FindBranchOptions) ToConds() builder.Cond { if len(opts.ExcludeBranchNames) > 0 { cond = cond.And(builder.NotIn("name", opts.ExcludeBranchNames)) } - if opts.IsDeletedBranch.Has() { - cond = cond.And(builder.Eq{"is_deleted": opts.IsDeletedBranch.Value()}) + if has, value := opts.IsDeletedBranch.Get(); has { + cond = cond.And(builder.Eq{"is_deleted": value}) } if opts.Keyword != "" { cond = cond.And(builder.Like{"name", opts.Keyword}) diff --git a/models/git/branch_test.go b/models/git/branch_test.go index 4340e8f729..5eca70324d 100644 --- a/models/git/branch_test.go +++ b/models/git/branch_test.go @@ -115,7 +115,7 @@ func TestFindRenamedBranch(t *testing.T) { assert.True(t, exist) assert.Equal(t, "master", branch.To) - _, exist, err = git_model.FindRenamedBranch(db.DefaultContext, 1, "unknow") + _, exist, err = git_model.FindRenamedBranch(db.DefaultContext, 1, "unknown") require.NoError(t, err) assert.False(t, exist) } diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go index c1eb750230..3eaada2fdd 100644 --- a/models/git/protected_branch.go +++ b/models/git/protected_branch.go @@ -213,7 +213,7 @@ func (protectBranch *ProtectedBranch) GetUnprotectedFilePatterns() []glob.Glob { func getFilePatterns(filePatterns string) []glob.Glob { extarr := make([]glob.Glob, 0, 10) - for _, expr := range strings.Split(strings.ToLower(filePatterns), ";") { + for expr := range strings.SplitSeq(strings.ToLower(filePatterns), ";") { expr = strings.TrimSpace(expr) if expr != "" { if g, err := glob.Compile(expr, '.', '/'); err != nil { diff --git a/models/git/protected_branch_list.go b/models/git/protected_branch_list.go index c7a3154884..54930b7eac 100644 --- a/models/git/protected_branch_list.go +++ b/models/git/protected_branch_list.go @@ -50,7 +50,7 @@ func FindRepoProtectedBranchRules(ctx context.Context, repoID int64) (ProtectedB func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) ([]string, error) { results := make([]string, 0, 10) for page := 1; ; page++ { - brancheNames, err := FindBranchNames(ctx, FindBranchOptions{ + branchNames, err := FindBranchNames(ctx, FindBranchOptions{ ListOptions: db.ListOptions{ PageSize: 100, Page: page, @@ -63,12 +63,12 @@ func FindAllMatchedBranches(ctx context.Context, repoID int64, ruleName string) } rule := glob.MustCompile(ruleName) - for _, branch := range brancheNames { + for _, branch := range branchNames { if rule.Match(branch) { results = append(results, branch) } } - if len(brancheNames) < 100 { + if len(branchNames) < 100 { break } } diff --git a/models/gitea_migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml b/models/gitea_migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml index 2e1b1c7eee..d7eab38185 100644 --- a/models/gitea_migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml +++ b/models/gitea_migrations/fixtures/Test_CheckProjectColumnsConsistency/project_board.yml @@ -4,6 +4,7 @@ title: Done creator_id: 2 default: false + sorting: 1 created_unix: 1588117528 updated_unix: 1588117528 @@ -13,6 +14,7 @@ title: Backlog creator_id: 2 default: true + sorting: 0 created_unix: 1588117528 updated_unix: 1588117528 @@ -22,5 +24,6 @@ title: Uncategorized creator_id: 2 default: true + sorting: 1 created_unix: 1588117528 updated_unix: 1588117528 diff --git a/models/gitea_migrations/fixtures/Test_MigrateWebhookSecrets/webhook.yml b/models/gitea_migrations/fixtures/Test_MigrateWebhookSecrets/webhook.yml new file mode 100644 index 0000000000..338bf4468e --- /dev/null +++ b/models/gitea_migrations/fixtures/Test_MigrateWebhookSecrets/webhook.yml @@ -0,0 +1,17 @@ +- + id: 1 + owner_id: 3 + repo_id: 3 + header_authorization_encrypted: '54586e944822336738b940c2560b7ef38bea3a91dcfe43c9c32e55b2a57050f75c63456b' + +- + id: 2 + owner_id: 1 + repo_id: 2 + header_authorization_encrypted: 'badbadbad' + +- + id: 3 + owner_id: 2 + repo_id: 1 + header_authorization_encrypted: '' diff --git a/models/gitea_migrations/fixtures/Test_addForeignKeysActionRunnerToken/action_runner_token.yml b/models/gitea_migrations/fixtures/Test_addForeignKeysActionRunnerToken/action_runner_token.yml new file mode 100644 index 0000000000..9905c440cc --- /dev/null +++ b/models/gitea_migrations/fixtures/Test_addForeignKeysActionRunnerToken/action_runner_token.yml @@ -0,0 +1,24 @@ +- + id: 1 + owner_id: null + repo_id: null + +- + id: 2 + owner_id: 1 + repo_id: null + +- + id: 3 + owner_id: null + repo_id: 1 + +# Expected to be deleted due to invalid owner_id foreign key +- + id: 4 + owner_id: 100 + +# Expected to be deleted due to invalid repo_id foreign key +- + id: 5 + repo_id: 100 diff --git a/models/gitea_migrations/fixtures/Test_addForeignKeysActionRunnerToken/repository.yml b/models/gitea_migrations/fixtures/Test_addForeignKeysActionRunnerToken/repository.yml new file mode 100644 index 0000000000..a88c2ef89f --- /dev/null +++ b/models/gitea_migrations/fixtures/Test_addForeignKeysActionRunnerToken/repository.yml @@ -0,0 +1,2 @@ +- + id: 1 diff --git a/models/gitea_migrations/fixtures/Test_addForeignKeysActionRunnerToken/user.yml b/models/gitea_migrations/fixtures/Test_addForeignKeysActionRunnerToken/user.yml new file mode 100644 index 0000000000..a88c2ef89f --- /dev/null +++ b/models/gitea_migrations/fixtures/Test_addForeignKeysActionRunnerToken/user.yml @@ -0,0 +1,2 @@ +- + id: 1 diff --git a/models/gitea_migrations/fixtures/Test_removeSoftDeleteActionRunnerToken/action_runner_token.yml b/models/gitea_migrations/fixtures/Test_removeSoftDeleteActionRunnerToken/action_runner_token.yml new file mode 100644 index 0000000000..3e79a7cb8a --- /dev/null +++ b/models/gitea_migrations/fixtures/Test_removeSoftDeleteActionRunnerToken/action_runner_token.yml @@ -0,0 +1,33 @@ +- + id: 1 + owner_id: null + repo_id: null + deleted: 1767994875 + +- + id: 2 + owner_id: 1 + repo_id: null + deleted: 1767994875 + +- + id: 3 + owner_id: null + repo_id: 1 + deleted: 1767994875 + +- + id: 4 + owner_id: null + repo_id: null + +- + id: 5 + owner_id: 1 + repo_id: null + +- + id: 6 + owner_id: null + repo_id: 1 + diff --git a/models/gitea_migrations/fixtures/Test_setOIDCSubjectFormatLegacy15/repo_unit.yml b/models/gitea_migrations/fixtures/Test_setOIDCSubjectFormatLegacy15/repo_unit.yml new file mode 100644 index 0000000000..bcd732bc1e --- /dev/null +++ b/models/gitea_migrations/fixtures/Test_setOIDCSubjectFormatLegacy15/repo_unit.yml @@ -0,0 +1,14 @@ +- id: 1 + repo_id: 4 + type: 10 # actions + # config - null + created_unix: 1773518296 +- id: 2 + repo_id: 5 + type: 10 # actions + config: '{"DisabledWorkflows":["renovate.yml"]}' + created_unix: 1773518296 +- id: 3 + repo_id: 6 + type: 1 # code + created_unix: 1773518296 diff --git a/models/gitea_migrations/migrations.go b/models/gitea_migrations/migrations.go index 5d6bf0aab2..9b30f9940d 100644 --- a/models/gitea_migrations/migrations.go +++ b/models/gitea_migrations/migrations.go @@ -126,7 +126,7 @@ func prepareMigrationTasks() []*migration { newMigration(102, "update migration repositories' service type", v1_11.DropColumnHeadUserNameOnPullRequest), newMigration(103, "Add WhitelistDeployKeys to protected branch", v1_11.AddWhitelistDeployKeysToBranches), - newMigration(104, "remove unnecessary columns from label", v1_11.RemoveLabelUneededCols), + newMigration(104, "remove unnecessary columns from label", v1_11.RemoveLabelUnneededCols), newMigration(105, "add includes_all_repositories to teams", v1_11.AddTeamIncludesAllRepositories), newMigration(106, "add column `mode` to table watch", v1_11.AddModeColumnToWatch), newMigration(107, "Add template options to repository", v1_11.AddTemplateToRepo), @@ -323,7 +323,7 @@ func prepareMigrationTasks() []*migration { newMigration(268, "Update Action Ref", v1_21.UpdateActionsRefIndex), newMigration(269, "Drop deleted branch table", v1_21.DropDeletedBranchTable), newMigration(270, "Fix PackageProperty typo", v1_21.FixPackagePropertyTypo), - newMigration(271, "Allow archiving labels", v1_21.AddArchivedUnixColumInLabelTable), + newMigration(271, "Allow archiving labels", v1_21.AddArchivedUnixColumnInLabelTable), newMigration(272, "Add Version to ActionRun table", v1_21.AddVersionToActionRunTable), newMigration(273, "Add Action Schedule Table", v1_21.AddActionScheduleTable), newMigration(274, "Add Actions artifacts expiration date", v1_21.AddExpiredUnixColumnInActionArtifactTable), diff --git a/models/gitea_migrations/test/tests.go b/models/gitea_migrations/test/tests.go index fc54b65626..086127a2e8 100644 --- a/models/gitea_migrations/test/tests.go +++ b/models/gitea_migrations/test/tests.go @@ -265,7 +265,7 @@ func deleteDB() error { func removeAllWithRetry(dir string) error { var err error - for i := 0; i < 20; i++ { + for range 20 { err = os.RemoveAll(dir) if err == nil { break diff --git a/models/gitea_migrations/v1_11/v104.go b/models/gitea_migrations/v1_11/v104.go index 47cf320359..606690009d 100644 --- a/models/gitea_migrations/v1_11/v104.go +++ b/models/gitea_migrations/v1_11/v104.go @@ -9,7 +9,7 @@ import ( "xorm.io/xorm" ) -func RemoveLabelUneededCols(x *xorm.Engine) error { +func RemoveLabelUnneededCols(x *xorm.Engine) error { // Make sure the columns exist before dropping them type Label struct { QueryString string diff --git a/models/gitea_migrations/v1_11/v111.go b/models/gitea_migrations/v1_11/v111.go index 6f531e4858..59ca416af0 100644 --- a/models/gitea_migrations/v1_11/v111.go +++ b/models/gitea_migrations/v1_11/v111.go @@ -5,6 +5,7 @@ package v1_11 import ( "fmt" + "slices" "xorm.io/xorm" ) @@ -345,10 +346,8 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { } return AccessModeWrite <= perm.UnitsMode[UnitTypeCode], nil } - for _, id := range protectedBranch.ApprovalsWhitelistUserIDs { - if id == reviewer.ID { - return true, nil - } + if slices.Contains(protectedBranch.ApprovalsWhitelistUserIDs, reviewer.ID) { + return true, nil } // isUserInTeams @@ -410,7 +409,7 @@ func AddBranchProtectionCanPushAndEnableWhitelist(x *xorm.Engine) error { official, err := isOfficialReviewer(sess, review.IssueID, reviewer) if err != nil { - // Branch might not be proteced or other error, ignore it. + // Branch might not be protected or other error, ignore it. continue } review.Official = official diff --git a/models/gitea_migrations/v1_11/v115.go b/models/gitea_migrations/v1_11/v115.go index 65094df93d..84364e310b 100644 --- a/models/gitea_migrations/v1_11/v115.go +++ b/models/gitea_migrations/v1_11/v115.go @@ -146,7 +146,7 @@ func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) return "", fmt.Errorf("io.ReadAll: %w", err) } - newAvatar := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", userID, md5.Sum(data))))) + newAvatar := fmt.Sprintf("%x", md5.Sum(fmt.Appendf(nil, "%d-%x", userID, md5.Sum(data)))) if newAvatar == oldAvatar { return newAvatar, nil } diff --git a/models/gitea_migrations/v1_13/v151.go b/models/gitea_migrations/v1_13/v151.go index a464e6e7a7..09abfd4b9e 100644 --- a/models/gitea_migrations/v1_13/v151.go +++ b/models/gitea_migrations/v1_13/v151.go @@ -25,6 +25,7 @@ func SetDefaultPasswordToArgon2(x *xorm.Engine) error { return err case setting.Database.Type.IsSQLite3(): // drop through + break default: log.Fatal("Unrecognized DB") } diff --git a/models/gitea_migrations/v1_15/v182.go b/models/gitea_migrations/v1_15/v182.go index f53ff11df9..069f9890cd 100644 --- a/models/gitea_migrations/v1_15/v182.go +++ b/models/gitea_migrations/v1_15/v182.go @@ -24,7 +24,7 @@ func AddIssueResourceIndexTable(x *xorm.Engine) error { return err } - // Remove data we're goint to rebuild + // Remove data we're going to rebuild if _, err := sess.Table("issue_index").Where("1=1").Delete(&ResourceIndex{}); err != nil { return err } diff --git a/models/gitea_migrations/v1_20/v259.go b/models/gitea_migrations/v1_20/v259.go index 9b2b68263e..1ae8b2e30f 100644 --- a/models/gitea_migrations/v1_20/v259.go +++ b/models/gitea_migrations/v1_20/v259.go @@ -329,7 +329,7 @@ func ConvertScopedAccessTokens(x *xorm.Engine) error { for _, token := range tokens { var scopes []string allNewScopesMap := make(map[AccessTokenScope]bool) - for _, oldScope := range strings.Split(token.Scope, ",") { + for oldScope := range strings.SplitSeq(token.Scope, ",") { if newScopes, exists := accessTokenScopeMap[OldAccessTokenScope(oldScope)]; exists { for _, newScope := range newScopes { allNewScopesMap[newScope] = true diff --git a/models/gitea_migrations/v1_21/v271.go b/models/gitea_migrations/v1_21/v271.go index e3ce2d4b74..38b171c323 100644 --- a/models/gitea_migrations/v1_21/v271.go +++ b/models/gitea_migrations/v1_21/v271.go @@ -9,7 +9,7 @@ import ( "xorm.io/xorm" ) -func AddArchivedUnixColumInLabelTable(x *xorm.Engine) error { +func AddArchivedUnixColumnInLabelTable(x *xorm.Engine) error { type Label struct { ArchivedUnix timeutil.TimeStamp `xorm:"DEFAULT NULL"` } diff --git a/models/issues/TestGetParticipantIDsByIssue/comment.yml b/models/issues/TestGetParticipantIDsByIssue/comment.yml new file mode 100644 index 0000000000..7ab1cf88a6 --- /dev/null +++ b/models/issues/TestGetParticipantIDsByIssue/comment.yml @@ -0,0 +1,10 @@ +- id: 1001 + type: 21 # code comment + poster_id: 10 + issue_id: 1 + review_id: 1001 + content: "Some code comment that is pending to be published" + line: -4 + tree_path: "README.md" + created_unix: 946684812 + invalidated: false diff --git a/models/issues/TestGetParticipantIDsByIssue/review.yml b/models/issues/TestGetParticipantIDsByIssue/review.yml new file mode 100644 index 0000000000..2b6fe5706e --- /dev/null +++ b/models/issues/TestGetParticipantIDsByIssue/review.yml @@ -0,0 +1,7 @@ +- id: 1001 + type: 0 # Pending review + reviewer_id: 10 + issue_id: 1 + content: "Pending review for issue 1" + updated_unix: 946684810 + created_unix: 946684810 diff --git a/models/issues/comment.go b/models/issues/comment.go index 769feba54a..e0ea1e111d 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -6,10 +6,14 @@ package issues import ( + "bytes" "context" + "errors" "fmt" "html/template" + "slices" "strconv" + "strings" "unicode/utf8" "forgejo.org/models/db" @@ -18,7 +22,9 @@ import ( project_model "forgejo.org/models/project" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" + "forgejo.org/modules/cache" "forgejo.org/modules/container" + "forgejo.org/modules/git" "forgejo.org/modules/gitrepo" "forgejo.org/modules/json" "forgejo.org/modules/log" @@ -198,12 +204,7 @@ func (t CommentType) HasMailReplySupport() bool { } func (t CommentType) CountedAsConversation() bool { - for _, ct := range ConversationCountedCommentType() { - if t == ct { - return true - } - } - return false + return slices.Contains(ConversationCountedCommentType(), t) } // ConversationCountedCommentType returns the comment types that are counted as a conversation @@ -325,6 +326,8 @@ type Comment struct { CommitsNum int64 `xorm:"-"` IsForcePush bool `xorm:"-"` + reverseLineBlame *git.ReverseLineBlame `xorm:"-"` + // If you add new fields that might be used to store abusive content (mainly string fields), // please also add them in the CommentData struct and the corresponding constructor. } @@ -619,7 +622,7 @@ func (c *Comment) UpdateAttachments(ctx context.Context, uuids []string) error { if err != nil { return fmt.Errorf("FindRepoAttachmentsByUUID[uuids=%q,repoID=%d]: %w", uuids, c.Issue.RepoID, err) } - for i := 0; i < len(attachments); i++ { + for i := range attachments { attachments[i].IssueID = c.IssueID attachments[i].CommentID = c.ID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { @@ -667,21 +670,6 @@ func (c *Comment) LoadAssigneeUserAndTeam(ctx context.Context) error { return nil } -// LoadResolveDoer if comment.Type is CommentTypeCode and ResolveDoerID not zero, then load resolveDoer -func (c *Comment) LoadResolveDoer(ctx context.Context) (err error) { - if c.ResolveDoerID == 0 || c.Type != CommentTypeCode { - return nil - } - c.ResolveDoer, err = user_model.GetUserByID(ctx, c.ResolveDoerID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - c.ResolveDoer = user_model.NewGhostUser() - err = nil - } - } - return err -} - // IsResolved check if an code comment is resolved func (c *Comment) IsResolved() bool { return c.ResolveDoerID != 0 && c.Type == CommentTypeCode @@ -763,6 +751,89 @@ func (c *Comment) UnsignedLine() uint64 { return uint64(c.Line) } +func (c *Comment) ResolveCurrentLine(ctx context.Context, repo *repo_model.Repository, currentHead string) (*git.ReverseLineBlame, error) { + if c.reverseLineBlame != nil { + return c.reverseLineBlame, nil + } + + // When a PR is viewed, the requirement to perform `git blame --reverse...` on every comment is a bit of a + // performance risk. To minimize this risk, cache the results relative to the requested head, so it only needs to be + // recalculated when head changes (or on cache eviction). + // + // Some performance testing was done which showed that a hot cache is much faster than the blame reverse + // operation -- 500-1000x runtime difference: + // + // - cache miss (Forgejo repo) took 7,690,574 ns + // - cache miss (~1000 commit repo) took 1,671,223 ns + // - cache hit (in-memory adapter) took 3,710 ns + // - cache hit (redis adapter) took 77,311 ns + resolveJSON, err := cache.GetString(fmt.Sprintf("comment.Resolve;ID=%d;HEAD=%s", c.ID, currentHead), func() (string, error) { + gitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, repo) + if err != nil { + return "", fmt.Errorf("failed to open repo: %w", err) + } + defer closer.Close() + + var reverseBlame *git.ReverseLineBlame + if c.Line > 0 { + var err error + reverseBlame, err = gitRepo.ReverseLineBlame(c.CommitSHA, c.TreePath, c.UnsignedLine(), currentHead) + if err != nil { + return "", fmt.Errorf("failed to perform `git blame --reverse` to resolve current line for comment (id=%d): %w", c.ID, err) + } + } else { + // For comments on removed lines, perform a `git diff` between the last commit that the line of code was + // known to exist (which is recorded as CommitSHA) and the requested head. Then inspect the diff to verify + // that the removed line of code is present in the diff. + buffer := bytes.Buffer{} + err := git.GetRepoRawDiffForFile(gitRepo, c.CommitSHA, currentHead, git.RawDiffNormal, c.TreePath, &buffer) + if err != nil { + return "", fmt.Errorf("failed to get diff: %w", err) + } + + diff := buffer.String() + adjustedLine, err := git.FindAdjustedLineNumber(c.Patch, int64(c.UnsignedLine()), strings.NewReader(diff)) + if err != nil && errors.Is(err, git.ErrLineNotFound) { + // Line not found in the diff. Don't treat this as an error, because that would break the caching -- + // instead, return a blame where CommitID != headCommitID, which will be an indicator to callers (for + // both resolution methods) that the line of code is outdated in the diff. + reverseBlame = &git.ReverseLineBlame{ + CommitID: "", // not currentHead + LineNumber: c.UnsignedLine(), + FilePath: c.TreePath, + } + } else if err != nil { + return "", fmt.Errorf("failed in finding adjusted line number: %w", err) + } else { + reverseBlame = &git.ReverseLineBlame{ + CommitID: currentHead, + LineNumber: uint64(adjustedLine.Left), + FilePath: c.TreePath, + } + } + } + + data, err := json.Marshal(reverseBlame) + if err != nil { + return "", err + } + + return string(data), nil + }) + if err != nil { + return nil, err + } + + var reverseBlame *git.ReverseLineBlame + err = json.Unmarshal([]byte(resolveJSON), &reverseBlame) + if err != nil { + return nil, err + } + + c.reverseLineBlame = reverseBlame + return c.reverseLineBlame, nil +} + // CodeCommentLink returns the url to a comment in code func (c *Comment) CodeCommentLink(ctx context.Context) string { err := c.LoadIssue(ctx) @@ -1089,11 +1160,11 @@ func (opts FindCommentsOptions) ToConds() builder.Cond { if len(opts.TreePath) > 0 { cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath}) } - if opts.Invalidated.Has() { - cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.Value()}) + if has, value := opts.Invalidated.Get(); has { + cond = cond.And(builder.Eq{"comment.invalidated": value}) } - if opts.IsPull.Has() { - cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.Value()}) + if has, value := opts.IsPull.Get(); has { + cond = cond.And(builder.Eq{"issue.is_pull": value}) } return cond } diff --git a/models/issues/comment_code.go b/models/issues/comment_code.go index 3c87a1b41a..cc46541743 100644 --- a/models/issues/comment_code.go +++ b/models/issues/comment_code.go @@ -7,7 +7,10 @@ import ( "context" "forgejo.org/models/db" + repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" + "forgejo.org/modules/git" + "forgejo.org/modules/log" "forgejo.org/modules/markup" "forgejo.org/modules/markup/markdown" @@ -23,33 +26,62 @@ type CodeConversationsAtLine map[int64][]CodeConversation // CodeConversationsAtLineAndTreePath contains the conversations for a given TreePath and line type CodeConversationsAtLineAndTreePath map[string]CodeConversationsAtLine -func newCodeConversationsAtLineAndTreePath(comments []*Comment) CodeConversationsAtLineAndTreePath { +func newCodeConversationsAtLineAndTreePath(ctx context.Context, comments []*Comment, repo *repo_model.Repository, headCommitID string) (CodeConversationsAtLineAndTreePath, error) { tree := make(CodeConversationsAtLineAndTreePath) for _, comment := range comments { - tree.insertComment(comment) + blame, err := comment.ResolveCurrentLine(ctx, repo, headCommitID) + if err != nil { + // ResolveCurrentLine can fail in at least one known situation -- where a comment is left on a line in a + // file that is being deleted. The blame would be for the commit that deleted the file, and a reverse git + // blame won't work because the file is missing in the target sha. + log.Warn("ResolveCurrentLine failed: %s", err.Error()) + // handle gracefully -- insertComment will use the original values which may be usable + blame = nil + } else if blame.CommitID != headCommitID { + // Commit was made on a line that can't be reverse-blamed to the currently viewing head. This can happen + // because: + // - line of code was removed between the commit it was tagged on, and the head commit + // - force push on the repo caused there to be no git relationship between blame.CommitID->headCommitID + // We won't insert this comment into the comment tree because we don't know where to place it; it may appear + // when the user views a different commit in the PR, and it will always appear on the "Conversations" tab. + continue + } + tree.insertComment(comment, blame) } - return tree + return tree, nil } -func (tree CodeConversationsAtLineAndTreePath) insertComment(comment *Comment) { +func (tree CodeConversationsAtLineAndTreePath) insertComment(comment *Comment, blame *git.ReverseLineBlame) { + treePath := comment.TreePath + line := comment.Line + if blame != nil { + treePath = blame.FilePath + line = int64(blame.LineNumber) + if comment.Line < 0 { + line *= -1 + } + } + // attempt to append comment to existing conversations (i.e. list of comments belonging to the same review) - for i, conversation := range tree[comment.TreePath][comment.Line] { + for i, conversation := range tree[treePath][line] { if conversation[0].ReviewID == comment.ReviewID { - tree[comment.TreePath][comment.Line][i] = append(conversation, comment) + tree[treePath][line][i] = append(conversation, comment) return } } // no previous conversation was found at this line, create it - if tree[comment.TreePath] == nil { - tree[comment.TreePath] = make(map[int64][]CodeConversation) + if tree[treePath] == nil { + tree[treePath] = make(map[int64][]CodeConversation) } - tree[comment.TreePath][comment.Line] = append(tree[comment.TreePath][comment.Line], CodeConversation{comment}) + tree[treePath][line] = append(tree[treePath][line], CodeConversation{comment}) } -// FetchCodeConversations will return a 2d-map: ["Path"]["Line"] = List of CodeConversation (one per review) for this line -func FetchCodeConversations(ctx context.Context, issue *Issue, doer *user_model.User, showOutdatedComments bool) (CodeConversationsAtLineAndTreePath, error) { +// FetchCodeConversations will return a 2d-map: ["Path"]["Line"] = List of CodeConversation (one per review) for this +// line. headCommitID will be used to reverse-blame the comment into the correct path & line for the current context +// that is being viewed. +func FetchCodeConversations(ctx context.Context, issue *Issue, doer *user_model.User, showOutdatedComments bool, headCommitID string) (CodeConversationsAtLineAndTreePath, error) { opts := FindCommentsOptions{ Type: CommentTypeCode, IssueID: issue.ID, @@ -59,7 +91,7 @@ func FetchCodeConversations(ctx context.Context, issue *Issue, doer *user_model. return nil, err } - return newCodeConversationsAtLineAndTreePath(comments), nil + return newCodeConversationsAtLineAndTreePath(ctx, comments, issue.Repo, headCommitID) } // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS @@ -133,7 +165,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu return nil, err } - n := 0 + readyComments := make(CommentList, 0, len(comments)) for _, comment := range comments { if re, ok := reviews[comment.ReviewID]; ok && re != nil { // If the review is pending only the author can see the comments (except if the review is set) @@ -143,17 +175,18 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu } comment.Review = re } - comments[n] = comment - n++ + readyComments = append(readyComments, comment) + } - if err := comment.LoadResolveDoer(ctx); err != nil { - return nil, err - } + if err := readyComments.LoadResolveDoers(ctx); err != nil { + return nil, err + } - if err := comment.LoadReactions(ctx, issue.Repo); err != nil { - return nil, err - } + if err := readyComments.LoadReactions(ctx, issue.Repo); err != nil { + return nil, err + } + for _, comment := range readyComments { var err error if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{ Ctx: ctx, @@ -165,7 +198,8 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu return nil, err } } - return comments[:n], nil + + return readyComments, nil } // FetchCodeConversation fetches the code conversation of a given comment (same review, treePath and line number) diff --git a/models/issues/comment_list.go b/models/issues/comment_list.go index 9b502d1c91..b218f11dfa 100644 --- a/models/issues/comment_list.go +++ b/models/issues/comment_list.go @@ -5,6 +5,7 @@ package issues import ( "context" + "errors" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" @@ -51,32 +52,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error { } labelIDs := comments.getLabelIDs() - commentLabels := make(map[int64]*Label, len(labelIDs)) - left := len(labelIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", labelIDs[:limit]). - Rows(new(Label)) - if err != nil { - return err - } - - for rows.Next() { - var label Label - err = rows.Scan(&label) - if err != nil { - _ = rows.Close() - return err - } - commentLabels[label.ID] = &label - } - _ = rows.Close() - left -= limit - labelIDs = labelIDs[limit:] + commentLabels, err := db.GetByIDs(ctx, "id", labelIDs, &Label{}) + if err != nil { + return err } for _, comment := range comments { @@ -101,21 +79,9 @@ func (comments CommentList) loadMilestones(ctx context.Context) error { return nil } - milestones := make(map[int64]*Milestone, len(milestoneIDs)) - left := len(milestoneIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", milestoneIDs[:limit]). - Find(&milestones) - if err != nil { - return err - } - left -= limit - milestoneIDs = milestoneIDs[limit:] + milestones, err := db.GetByIDs(ctx, "id", milestoneIDs, &Milestone{}) + if err != nil { + return err } for _, comment := range comments { @@ -140,21 +106,9 @@ func (comments CommentList) loadOldMilestones(ctx context.Context) error { return nil } - milestones := make(map[int64]*Milestone, len(milestoneIDs)) - left := len(milestoneIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", milestoneIDs[:limit]). - Find(&milestones) - if err != nil { - return err - } - left -= limit - milestoneIDs = milestoneIDs[limit:] + milestones, err := db.GetByIDs(ctx, "id", milestoneIDs, &Milestone{}) + if err != nil { + return err } for _, comment := range comments { @@ -175,34 +129,9 @@ func (comments CommentList) loadAssignees(ctx context.Context) error { } assigneeIDs := comments.getAssigneeIDs() - assignees := make(map[int64]*user_model.User, len(assigneeIDs)) - left := len(assigneeIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", assigneeIDs[:limit]). - Rows(new(user_model.User)) - if err != nil { - return err - } - - for rows.Next() { - var user user_model.User - err = rows.Scan(&user) - if err != nil { - rows.Close() - return err - } - - assignees[user.ID] = &user - } - _ = rows.Close() - - left -= limit - assigneeIDs = assigneeIDs[limit:] + assignees, err := db.GetByIDs(ctx, "id", assigneeIDs, &user_model.User{}) + if err != nil { + return err } for _, comment := range comments { @@ -243,34 +172,9 @@ func (comments CommentList) LoadIssues(ctx context.Context) error { } issueIDs := comments.getIssueIDs() - issues := make(map[int64]*Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("id", issueIDs[:limit]). - Rows(new(Issue)) - if err != nil { - return err - } - - for rows.Next() { - var issue Issue - err = rows.Scan(&issue) - if err != nil { - rows.Close() - return err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] + issues, err := db.GetByIDs(ctx, "id", issueIDs, &Issue{}) + if err != nil { + return err } for _, comment := range comments { @@ -295,36 +199,10 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error { return nil } - e := db.GetEngine(ctx) issueIDs := comments.getDependentIssueIDs() - issues := make(map[int64]*Issue, len(issueIDs)) - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := e. - In("id", issueIDs[:limit]). - Rows(new(Issue)) - if err != nil { - return err - } - - for rows.Next() { - var issue Issue - err = rows.Scan(&issue) - if err != nil { - _ = rows.Close() - return err - } - - issues[issue.ID] = &issue - } - _ = rows.Close() - - left -= limit - issueIDs = issueIDs[limit:] + issues, err := db.GetByIDs(ctx, "id", issueIDs, &Issue{}) + if err != nil { + return err } for _, comment := range comments { @@ -375,34 +253,10 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { return nil } - attachments := make(map[int64][]*repo_model.Attachment, len(comments)) commentsIDs := comments.getAttachmentCommentIDs() - left := len(commentsIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("comment_id", commentsIDs[:limit]). - Rows(new(repo_model.Attachment)) - if err != nil { - return err - } - - for rows.Next() { - var attachment repo_model.Attachment - err = rows.Scan(&attachment) - if err != nil { - _ = rows.Close() - return err - } - attachments[attachment.CommentID] = append(attachments[attachment.CommentID], &attachment) - } - - _ = rows.Close() - left -= limit - commentsIDs = commentsIDs[limit:] + attachments, err := db.GetByFieldIn(ctx, "comment_id", commentsIDs, &repo_model.Attachment{}) + if err != nil { + return err } for _, comment := range comments { @@ -411,13 +265,91 @@ func (comments CommentList) LoadAttachments(ctx context.Context) (err error) { return nil } +func (comments CommentList) LoadResolveDoers(ctx context.Context) (err error) { + relevant := func(c *Comment) bool { + return c.ResolveDoerID != 0 && c.Type == CommentTypeCode + } + userIDs := make(container.Set[int64]) + for _, comment := range comments { + if relevant(comment) { + userIDs.Add(comment.ResolveDoerID) + } + } + + if len(userIDs) == 0 { + return nil + } + + userMap := make(map[int64]*user_model.User) + users, err := user_model.GetUsersByIDs(ctx, userIDs.Slice()) + if err != nil { + return err + } + for _, user := range users { + userMap[user.ID] = user + } + + for _, comment := range comments { + if !relevant(comment) { + continue + } + resolveDoer, ok := userMap[comment.ResolveDoerID] + if !ok { + comment.ResolveDoer = user_model.NewGhostUser() + } else { + comment.ResolveDoer = resolveDoer + } + } + + return nil +} + +func (comments CommentList) LoadReactions(ctx context.Context, repo *repo_model.Repository) (err error) { + loadIssueID := int64(0) + loadCommentIDs := make([]int64, 0, len(comments)) + + for _, comment := range comments { + if loadIssueID == 0 { + loadIssueID = comment.IssueID + } else if loadIssueID != comment.IssueID { + return errors.New("unable to load reactions from comments on different issues than each other") + } + if comment.Reactions == nil { + loadCommentIDs = append(loadCommentIDs, comment.ID) + } + } + + if loadIssueID == 0 { + return nil + } + + reactions, err := getReactionsForComments(ctx, loadIssueID, loadCommentIDs) + if err != nil { + return err + } + + allReactions := make(ReactionList, 0, len(reactions)) + for _, comment := range comments { + if comment.Reactions == nil { + comment.Reactions = reactions[comment.ID] + allReactions = append(allReactions, comment.Reactions...) + } + } + + if _, err := allReactions.LoadUsers(ctx, repo); err != nil { + return err + } + + return nil +} + func (comments CommentList) getReviewIDs() []int64 { return container.FilterSlice(comments, func(comment *Comment) (int64, bool) { return comment.ReviewID, comment.ReviewID > 0 }) } -func (comments CommentList) loadReviews(ctx context.Context) error { +func (comments CommentList) LoadReviews(ctx context.Context) error { if len(comments) == 0 { return nil } @@ -476,7 +408,7 @@ func (comments CommentList) LoadAttributes(ctx context.Context) (err error) { return err } - if err = comments.loadReviews(ctx); err != nil { + if err = comments.LoadReviews(ctx); err != nil { return err } diff --git a/models/issues/comment_list_test.go b/models/issues/comment_list_test.go index 062a710b84..12a9144722 100644 --- a/models/issues/comment_list_test.go +++ b/models/issues/comment_list_test.go @@ -84,3 +84,111 @@ func TestCommentListLoadUser(t *testing.T) { }) } } + +func TestCommentListLoadResolveDoers(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &Issue{}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + empty := CommentList{} + require.NoError(t, empty.LoadResolveDoers(t.Context())) + + comment1, err := CreateComment(db.DefaultContext, &CreateCommentOptions{ + Type: CommentTypeCode, + Doer: doer, + Repo: repo, + Issue: issue, + Content: "Hello", + }) + require.NoError(t, err) + require.NoError(t, MarkConversation(t.Context(), comment1, doer, true)) + comment1 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment1.ID}) // reload after change + comment1List := CommentList{comment1} + require.NoError(t, comment1List.LoadResolveDoers(t.Context())) + require.NotNil(t, comment1.ResolveDoer) + assert.Equal(t, doer.ID, comment1.ResolveDoer.ID) + + comment2, err := CreateComment(db.DefaultContext, &CreateCommentOptions{ + Type: CommentTypeCode, + Doer: doer, + Repo: repo, + Issue: issue, + Content: "Hello again", + }) + require.NoError(t, err) + require.NoError(t, MarkConversation(t.Context(), comment2, user_model.NewGhostUser(), true)) + + // Reload for fresh objects + comment1 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment1.ID}) + comment2 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment2.ID}) + + comment2List := CommentList{comment1, comment2} + require.NoError(t, comment2List.LoadResolveDoers(t.Context())) + require.NotNil(t, comment1.ResolveDoer) + assert.Equal(t, doer.ID, comment1.ResolveDoer.ID) + require.NotNil(t, comment2.ResolveDoer) + assert.EqualValues(t, -1, comment2.ResolveDoer.ID) +} + +func TestCommentListLoadReactions(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + issue := unittest.AssertExistsAndLoadBean(t, &Issue{}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + empty := CommentList{} + require.NoError(t, empty.LoadReactions(t.Context(), repo)) + + comment1, err := CreateComment(db.DefaultContext, &CreateCommentOptions{ + Type: CommentTypeCode, + Doer: doer, + Repo: repo, + Issue: issue, + Content: "Hello", + }) + require.NoError(t, err) + _, err = CreateReaction(t.Context(), &ReactionOptions{ + Type: "eyes", + DoerID: doer.ID, + IssueID: issue.ID, + CommentID: comment1.ID, + }) + require.NoError(t, err) + + comment1 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment1.ID}) // reload after change + comment1List := CommentList{comment1} + require.NoError(t, comment1List.LoadReactions(t.Context(), repo)) + require.Len(t, comment1.Reactions, 1) + assert.Equal(t, "eyes", comment1.Reactions[0].Type) + assert.NotNil(t, comment1.Reactions[0].User) + + comment2, err := CreateComment(db.DefaultContext, &CreateCommentOptions{ + Type: CommentTypeCode, + Doer: doer, + Repo: repo, + Issue: issue, + Content: "Hello again", + }) + require.NoError(t, err) + _, err = CreateReaction(t.Context(), &ReactionOptions{ + Type: "rocket", + DoerID: doer.ID, + IssueID: issue.ID, + CommentID: comment2.ID, + }) + require.NoError(t, err) + + // Reload for fresh objects + comment1 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment1.ID}) + comment2 = unittest.AssertExistsAndLoadBean(t, &Comment{ID: comment2.ID}) + + comment2List := CommentList{comment1, comment2} + require.NoError(t, comment2List.LoadReactions(t.Context(), repo)) + require.Len(t, comment1.Reactions, 1) + require.Len(t, comment2.Reactions, 1) + assert.Equal(t, "rocket", comment2.Reactions[0].Type) + assert.NotNil(t, comment2.Reactions[0].User) +} diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index c7adf6f62e..ea59d4f215 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -52,15 +52,32 @@ func TestFetchCodeConversations(t *testing.T) { issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - res, err := issues_model.FetchCodeConversations(db.DefaultContext, issue, user, false) + _, err := issues_model.CreateReaction(t.Context(), &issues_model.ReactionOptions{ + Type: "eyes", + DoerID: 2, + IssueID: issue.ID, + CommentID: 4, + }) require.NoError(t, err) - assert.Contains(t, res, "README.md") - assert.Contains(t, res["README.md"], int64(4)) - assert.Len(t, res["README.md"][4], 1) - assert.Equal(t, int64(4), res["README.md"][4][0][0].ID) + require.NoError(t, issues_model.MarkConversation(t.Context(), + unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4}), + user, true)) + + res, err := issues_model.FetchCodeConversations(db.DefaultContext, issue, user, false, "") + require.NoError(t, err) + require.Contains(t, res, "README.md") + require.Contains(t, res["README.md"], int64(4)) + require.Len(t, res["README.md"][4], 1) + require.Len(t, res["README.md"][4][0], 1) + comment := res["README.md"][4][0][0] + assert.Equal(t, int64(4), comment.ID) + assert.NotNil(t, comment.ResolveDoer) + require.Len(t, comment.Reactions, 1) + r := comment.Reactions[0] + assert.NotNil(t, r.User) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - res, err = issues_model.FetchCodeConversations(db.DefaultContext, issue, user2, false) + res, err = issues_model.FetchCodeConversations(db.DefaultContext, issue, user2, false, "") require.NoError(t, err) assert.Len(t, res, 1) } diff --git a/models/issues/content_history.go b/models/issues/content_history.go index 476c6e0f90..0849686607 100644 --- a/models/issues/content_history.go +++ b/models/issues/content_history.go @@ -222,7 +222,7 @@ func GetIssueContentHistoryAndPrev(dbCtx context.Context, issueID, id int64) (hi return nil, nil, err } else if !has { log.Error("issue content history does not exist. id=%v. err=%v", id, err) - return nil, nil, &ErrIssueContentHistoryNotExist{id} + return nil, nil, ErrIssueContentHistoryNotExist{id} } prevHistory = &ContentHistory{} diff --git a/models/issues/issue.go b/models/issues/issue.go index fc8794ad50..a90686eb50 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -591,10 +591,12 @@ func GetParticipantsIDsByIssueID(ctx context.Context, issueID int64) ([]int64, e userIDs := make([]int64, 0, 5) return userIDs, db.GetEngine(ctx). Table("comment"). - Cols("poster_id"). - Where("issue_id = ?", issueID). - And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview). - Distinct("poster_id"). + Cols("`comment`.poster_id"). + Where("`comment`.issue_id = ?", issueID). + And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview). + And("`review`.type is null or `review`.type != ?", ReviewTypePending). + Join("LEFT", "`review`", "`review`.id = `comment`.review_id"). + Distinct("`comment`.poster_id"). Find(&userIDs) } @@ -623,9 +625,11 @@ func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, erro if err := db.GetEngine(ctx).Table("comment").Cols("poster_id"). Where("`comment`.issue_id = ?", issue.ID). And("`comment`.type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview). + And("`review`.type != ?", ReviewTypePending). And("`user`.is_active = ?", true). And("`user`.prohibit_login = ?", false). Join("INNER", "`user`", "`user`.id = `comment`.poster_id"). + Join("INNER", "`review`", "`review`.reviewer_id = `user`.id"). Distinct("poster_id"). Find(&userIDs); err != nil { return nil, fmt.Errorf("get poster IDs: %w", err) diff --git a/models/issues/issue_list.go b/models/issues/issue_list.go index 5a02baa428..e4fd9eef2b 100644 --- a/models/issues/issue_list.go +++ b/models/issues/issue_list.go @@ -6,6 +6,7 @@ package issues import ( "context" "fmt" + "slices" "forgejo.org/models/db" project_model "forgejo.org/models/project" @@ -40,21 +41,9 @@ func (issues IssueList) LoadRepositories(ctx context.Context) (repo_model.Reposi } repoIDs := issues.getRepoIDs() - repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs)) - left := len(repoIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", repoIDs[:limit]). - Find(&repoMaps) - if err != nil { - return nil, fmt.Errorf("find repository: %w", err) - } - left -= limit - repoIDs = repoIDs[limit:] + repoMaps, err := db.GetByIDs(ctx, "id", repoIDs, &repo_model.Repository{}) + if err != nil { + return nil, fmt.Errorf("find repository: %w", err) } for _, issue := range issues { @@ -96,21 +85,9 @@ func (issues IssueList) LoadPosters(ctx context.Context) error { } func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) { - posterMaps := make(map[int64]*user_model.User, len(posterIDs)) - left := len(posterIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", posterIDs[:limit]). - Find(&posterMaps) - if err != nil { - return nil, err - } - left -= limit - posterIDs = posterIDs[limit:] + posterMaps, err := db.GetByIDs(ctx, "id", posterIDs, &user_model.User{}) + if err != nil { + return nil, err } return posterMaps, nil } @@ -135,21 +112,15 @@ func (issues IssueList) LoadLabels(ctx context.Context) error { issueLabels := make(map[int64][]*Label, len(issues)*3) issueIDs := issues.getIssueIDs() - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + for issueIDChunk := range slices.Chunk(issueIDs, db.DefaultMaxInSize) { rows, err := db.GetEngine(ctx).Table("label"). Join("LEFT", "issue_label", "issue_label.label_id = label.id"). - In("issue_label.issue_id", issueIDs[:limit]). + In("issue_label.issue_id", issueIDChunk). Asc("label.name"). Rows(new(LabelIssue)) if err != nil { return err } - for rows.Next() { var labelIssue LabelIssue err = rows.Scan(&labelIssue) @@ -166,8 +137,6 @@ func (issues IssueList) LoadLabels(ctx context.Context) error { if err1 := rows.Close(); err1 != nil { return fmt.Errorf("IssueList.LoadLabels: Close: %w", err1) } - left -= limit - issueIDs = issueIDs[limit:] } for _, issue := range issues { @@ -189,21 +158,9 @@ func (issues IssueList) LoadMilestones(ctx context.Context) error { return nil } - milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs)) - left := len(milestoneIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - err := db.GetEngine(ctx). - In("id", milestoneIDs[:limit]). - Find(&milestoneMaps) - if err != nil { - return err - } - left -= limit - milestoneIDs = milestoneIDs[limit:] + milestoneMaps, err := db.GetByIDs(ctx, "id", milestoneIDs, &Milestone{}) + if err != nil { + return err } for _, issue := range issues { @@ -216,25 +173,19 @@ func (issues IssueList) LoadMilestones(ctx context.Context) error { func (issues IssueList) LoadProjects(ctx context.Context) error { issueIDs := issues.getIssueIDs() projectMaps := make(map[int64]*project_model.Project, len(issues)) - left := len(issueIDs) type projectWithIssueID struct { *project_model.Project `xorm:"extends"` IssueID int64 } - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - - projects := make([]*projectWithIssueID, 0, limit) + for issueIDChunk := range slices.Chunk(issueIDs, db.DefaultMaxInSize) { + projects := make([]*projectWithIssueID, 0, len(issueIDChunk)) err := db.GetEngine(ctx). Table("project"). Select("project.*, project_issue.issue_id"). Join("INNER", "project_issue", "project.id = project_issue.project_id"). - In("project_issue.issue_id", issueIDs[:limit]). + In("project_issue.issue_id", issueIDChunk). Find(&projects) if err != nil { return err @@ -242,8 +193,6 @@ func (issues IssueList) LoadProjects(ctx context.Context) error { for _, project := range projects { projectMaps[project.IssueID] = project.Project } - left -= limit - issueIDs = issueIDs[limit:] } for _, issue := range issues { @@ -264,15 +213,10 @@ func (issues IssueList) LoadAssignees(ctx context.Context) error { assignees := make(map[int64][]*user_model.User, len(issues)) issueIDs := issues.getIssueIDs() - left := len(issueIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + for issueIDChunk := range slices.Chunk(issueIDs, db.DefaultMaxInSize) { rows, err := db.GetEngine(ctx).Table("issue_assignees"). Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id"). - In("`issue_assignees`.issue_id", issueIDs[:limit]).OrderBy(user_model.GetOrderByName()). + In("`issue_assignees`.issue_id", issueIDChunk).OrderBy(user_model.GetOrderByName()). Rows(new(AssigneeIssue)) if err != nil { return err @@ -293,8 +237,6 @@ func (issues IssueList) LoadAssignees(ctx context.Context) error { if err1 := rows.Close(); err1 != nil { return fmt.Errorf("IssueList.loadAssignees: Close: %w", err1) } - left -= limit - issueIDs = issueIDs[limit:] } for _, issue := range issues { @@ -324,36 +266,9 @@ func (issues IssueList) LoadPullRequests(ctx context.Context) error { return nil } - pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs)) - left := len(issuesIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("issue_id", issuesIDs[:limit]). - Rows(new(PullRequest)) - if err != nil { - return err - } - - for rows.Next() { - var pr PullRequest - err = rows.Scan(&pr) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1) - } - return err - } - pullRequestMaps[pr.IssueID] = &pr - } - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadPullRequests: Close: %w", err1) - } - left -= limit - issuesIDs = issuesIDs[limit:] + pullRequestMaps, err := db.GetByIDs(ctx, "issue_id", issuesIDs, &PullRequest{}) + if err != nil { + return err } for _, issue := range issues { @@ -371,37 +286,10 @@ func (issues IssueList) LoadAttachments(ctx context.Context) (err error) { return nil } - attachments := make(map[int64][]*repo_model.Attachment, len(issues)) issuesIDs := issues.getIssueIDs() - left := len(issuesIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - rows, err := db.GetEngine(ctx). - In("issue_id", issuesIDs[:limit]). - Rows(new(repo_model.Attachment)) - if err != nil { - return err - } - - for rows.Next() { - var attachment repo_model.Attachment - err = rows.Scan(&attachment) - if err != nil { - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1) - } - return err - } - attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment) - } - if err1 := rows.Close(); err1 != nil { - return fmt.Errorf("IssueList.loadAttachments: Close: %w", err1) - } - left -= limit - issuesIDs = issuesIDs[limit:] + attachments, err := db.GetByFieldIn(ctx, "issue_id", issuesIDs, &repo_model.Attachment{}) + if err != nil { + return err } for _, issue := range issues { @@ -418,15 +306,10 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er comments := make(map[int64][]*Comment, len(issues)) issuesIDs := issues.getIssueIDs() - left := len(issuesIDs) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } + for issueIDChunk := range slices.Chunk(issuesIDs, db.DefaultMaxInSize) { rows, err := db.GetEngine(ctx).Table("comment"). Join("INNER", "issue", "issue.id = comment.issue_id"). - In("issue.id", issuesIDs[:limit]). + In("issue.id", issueIDChunk). Where(cond). Rows(new(Comment)) if err != nil { @@ -447,8 +330,6 @@ func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (er if err1 := rows.Close(); err1 != nil { return fmt.Errorf("IssueList.loadComments: Close: %w", err1) } - left -= limit - issuesIDs = issuesIDs[limit:] } for _, issue := range issues { @@ -484,18 +365,12 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { } } - left := len(ids) - for left > 0 { - limit := db.DefaultMaxInSize - if left < limit { - limit = left - } - + for idChunk := range slices.Chunk(ids, db.DefaultMaxInSize) { // select issue_id, sum(time) from tracked_time where issue_id in () group by issue_id rows, err := db.GetEngine(ctx).Table("tracked_time"). Where("deleted = ?", false). Select("issue_id, sum(time) as time"). - In("issue_id", ids[:limit]). + In("issue_id", idChunk). GroupBy("issue_id"). Rows(new(totalTimesByIssue)) if err != nil { @@ -516,8 +391,6 @@ func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) { if err1 := rows.Close(); err1 != nil { return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %w", err1) } - left -= limit - ids = ids[limit:] } for _, issue := range issues { diff --git a/models/issues/issue_search.go b/models/issues/issue_search.go index fbfcd3529a..a8644758ce 100644 --- a/models/issues/issue_search.go +++ b/models/issues/issue_search.go @@ -226,8 +226,8 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) { applyRepoConditions(sess, opts) - if opts.IsClosed.Has() { - sess.And("issue.is_closed=?", opts.IsClosed.Value()) + if has, value := opts.IsClosed.Get(); has { + sess.And("issue.is_closed=?", value) } if opts.AssigneeID > 0 { @@ -269,18 +269,18 @@ func applyConditions(sess *xorm.Session, opts *IssuesOptions) { applyProjectColumnCondition(sess, opts) - if opts.IsPull.Has() { - sess.And("issue.is_pull=?", opts.IsPull.Value()) + if has, value := opts.IsPull.Get(); has { + sess.And("issue.is_pull=?", value) } - if opts.IsArchived.Has() { - sess.And(builder.Eq{"repository.is_archived": opts.IsArchived.Value()}) + if has, value := opts.IsArchived.Get(); has { + sess.And(builder.Eq{"repository.is_archived": value}) } applyLabelsCondition(sess, opts) if opts.User != nil { - cond := issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.Value()) + cond := issuePullAccessibleRepoCond("issue.repo_id", opts.User.ID, opts.Org, opts.Team, opts.IsPull.ValueOrZeroValue()) // If AllPublic was set, then also consider all issues in public // repositories in addition to the private repositories the user has access // to. diff --git a/models/issues/issue_stats.go b/models/issues/issue_stats.go index eee8760b9f..03660803a4 100644 --- a/models/issues/issue_stats.go +++ b/models/issues/issue_stats.go @@ -94,10 +94,7 @@ func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error // ids in a temporary table and join from them. accum := &IssueStats{} for i := 0; i < len(opts.IssueIDs); { - chunk := i + MaxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } + chunk := min(i+MaxQueryParameters, len(opts.IssueIDs)) stats, err := getIssueStatsChunk(ctx, opts, opts.IssueIDs[i:chunk]) if err != nil { return nil, err @@ -180,8 +177,8 @@ func applyIssuesOptions(sess *xorm.Session, opts *IssuesOptions, issueIDs []int6 applyReviewedCondition(sess, opts.ReviewedID) } - if opts.IsPull.Has() { - sess.And("issue.is_pull=?", opts.IsPull.Value()) + if has, value := opts.IsPull.Get(); has { + sess.And("issue.is_pull=?", value) } return sess diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 2059c5013a..0c5da6a2aa 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -5,6 +5,7 @@ package issues_test import ( "fmt" + "slices" "sort" "sync" "testing" @@ -85,6 +86,7 @@ func TestGetIssuesByIDs(t *testing.T) { } func TestGetParticipantIDsByIssue(t *testing.T) { + defer unittest.OverrideFixtures("models/issues/TestGetParticipantIDsByIssue")() require.NoError(t, unittest.PrepareTestDatabase()) checkParticipants := func(issueID int64, userIDs []int) { @@ -107,6 +109,7 @@ func TestGetParticipantIDsByIssue(t *testing.T) { // User 2 only labeled issue1 (see fixtures/comment.yml) // Users 3 and 5 made actual comments (see fixtures/comment.yml) // User 3 is inactive, thus not active participant + // User 10 has a pending review, thus not an active participant, yet (see TestGetParticipantIDsByIssue/comment.yml) checkParticipants(1, []int{1, 5}) } @@ -309,7 +312,7 @@ func TestIssue_ResolveMentions(t *testing.T) { for i, user := range resolved { ids[i] = user.ID } - sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + slices.Sort(ids) assert.Equal(t, expected, ids) } @@ -336,7 +339,7 @@ func TestResourceIndex(t *testing.T) { require.NoError(t, err) var wg sync.WaitGroup - for i := 0; i < 100; i++ { + for i := range 100 { wg.Add(1) t.Run(fmt.Sprintf("issue %d", i+1), func(t *testing.T) { t.Parallel() @@ -367,7 +370,7 @@ func TestCorrectIssueStats(t *testing.T) { issueAmount := issues_model.MaxQueryParameters + 10 var wg sync.WaitGroup - for i := 0; i < issueAmount; i++ { + for i := range issueAmount { wg.Add(1) go func(i int) { testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0) diff --git a/models/issues/issue_update.go b/models/issues/issue_update.go index 22e6fcb8d4..35f69e3a0b 100644 --- a/models/issues/issue_update.go +++ b/models/issues/issue_update.go @@ -244,7 +244,7 @@ func UpdateIssueAttachments(ctx context.Context, issue *Issue, uuids []string) ( if err != nil { return fmt.Errorf("FindRepoAttachmentsByUUID[uuids=%q,repoID=%d]: %w", uuids, issue.RepoID, err) } - for i := 0; i < len(attachments); i++ { + for i := range attachments { attachments[i].IssueID = issue.ID if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil { return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err) diff --git a/models/issues/label.go b/models/issues/label.go index 4a5c3b4bfe..29ac82cfa3 100644 --- a/models/issues/label.go +++ b/models/issues/label.go @@ -150,6 +150,7 @@ func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, curr for i, curSel := range currentSelectedLabels { if curSel == l.ID { labelSelected = true + l.IsExcluded = false } else if -curSel == l.ID { labelSelected = true l.IsExcluded = true @@ -161,9 +162,10 @@ func (l *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, curr } } - if !labelSelected { + if !labelSelected || l.IsExcluded { labelQuerySlice = append(labelQuerySlice, l.ID) } + l.IsSelected = labelSelected // Sort and deduplicate the ids to avoid the crawlers asking for the diff --git a/models/issues/label_test.go b/models/issues/label_test.go index d3e6146fc9..f52c20e4a2 100644 --- a/models/issues/label_test.go +++ b/models/issues/label_test.go @@ -32,18 +32,27 @@ func TestLabel_LoadSelectedLabelsAfterClick(t *testing.T) { // First test : with negative and scope label.LoadSelectedLabelsAfterClick([]int64{1, -8}, []string{"", "scope"}) + // Flips to positive to include after click + assert.Equal(t, "1,8", label.QueryString) + assert.True(t, label.IsSelected) + assert.True(t, label.IsExcluded) + + // Second test : with positive and scope + label.LoadSelectedLabelsAfterClick([]int64{1, 8}, []string{"", "scope"}) assert.Equal(t, "1", label.QueryString) assert.True(t, label.IsSelected) - // Second test : with duplicates + // Third test : with duplicates label.LoadSelectedLabelsAfterClick([]int64{1, 7, 1, 7, 7}, []string{"", "scope", "", "scope", "scope"}) assert.Equal(t, "1,8", label.QueryString) assert.False(t, label.IsSelected) + assert.False(t, label.IsExcluded) - // Third test : empty set + // Fourth test : empty set label.LoadSelectedLabelsAfterClick([]int64{}, []string{}) assert.False(t, label.IsSelected) assert.Equal(t, "8", label.QueryString) + assert.False(t, label.IsExcluded) } func TestLabel_ExclusiveScope(t *testing.T) { diff --git a/models/issues/milestone.go b/models/issues/milestone.go index d718decb18..d5b3c2c293 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -378,8 +378,8 @@ func doRecalcMilestone(ctx context.Context, cond builder.Cond, updateTimestamp o }), ). Where(cond) - if updateTimestamp.Has() { - sess.SetExpr("updated_unix", updateTimestamp.Value()).NoAutoTime() + if has, value := updateTimestamp.Get(); has { + sess.SetExpr("updated_unix", value).NoAutoTime() } _, err := sess.Update(&Milestone{}) if err != nil { diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go index e2079fb324..6ef3385f7c 100644 --- a/models/issues/milestone_list.go +++ b/models/issues/milestone_list.go @@ -40,8 +40,8 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond { if opts.RepoID != 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.IsClosed.Has() { - cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()}) + if has, value := opts.IsClosed.Get(); has { + cond = cond.And(builder.Eq{"is_closed": value}) } if opts.RepoCond != nil && opts.RepoCond.IsValid() { cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond))) diff --git a/models/issues/pull_list.go b/models/issues/pull_list.go index ddb813cf44..10e39c6a49 100644 --- a/models/issues/pull_list.go +++ b/models/issues/pull_list.go @@ -27,6 +27,8 @@ type PullRequestsOptions struct { Labels []int64 MilestoneID int64 PosterID int64 + BaseBranch string + HeadBranch string } func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullRequestsOptions) *xorm.Session { @@ -51,6 +53,14 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR sess.And("issue.poster_id=?", opts.PosterID) } + if opts.BaseBranch != "" { + sess.And("pull_request.base_branch=?", opts.BaseBranch) + } + + if opts.HeadBranch != "" { + sess.And("pull_request.head_branch=?", opts.HeadBranch) + } + return sess } @@ -63,7 +73,7 @@ func GetUnmergedPullRequestsByHeadInfoMax(ctx context.Context, repoID, olderThan } // GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged -func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) { +func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) (PullRequestList, error) { prs := make([]*PullRequest, 0, 2) sess := db.GetEngine(ctx). Join("INNER", "issue", "issue.id = pull_request.issue_id"). @@ -82,18 +92,44 @@ func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, } prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, p.Units[0].RepoID, branch) + // All these error cases return `false` to defer to the safer choice of not allowing write access on an error. if err != nil { + log.Error("GetUnmergedPullRequestsByHeadInfo failed: %s", err) + return false + } else if issues, err := prs.LoadIssues(ctx); err != nil { + log.Error("LoadIssues failed: %s", err) + return false + } else if err := issues.LoadPosters(ctx); err != nil { + log.Error("LoadPosters failed: %s", err) + return false + } else if err := prs.LoadHeadRepos(ctx); err != nil { + log.Error("LoadHeadRepos failed: %s", err) return false } for _, pr := range prs { if pr.AllowMaintainerEdit { + // PR Poster must have write access to the head, so that when they turned on "AllowMaintainerEdit" they + // delegated that write access to the maintainers of the PR base. If they don't currently have write + // access, they can't delegate that access. + poster := pr.Issue.Poster + posterHeadPerm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, poster) + if err != nil { + log.Error("GetUserRepoPermission failed: %s", err) + continue + } + if !posterHeadPerm.CanWrite(unit.TypeCode) { + continue + } + err = pr.LoadBaseRepo(ctx) if err != nil { + log.Error("LoadBaseRepo failed: %s", err) continue } prPerm, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, user) if err != nil { + log.Error("GetUserRepoPermission failed: %s", err) continue } if prPerm.CanWrite(unit.TypeCode) { @@ -240,6 +276,25 @@ func (prs PullRequestList) LoadIssues(ctx context.Context) (IssueList, error) { return issueList, nil } +func (prs PullRequestList) LoadHeadRepos(ctx context.Context) error { + repoIDs := []int64{} + for _, pr := range prs { + repoIDs = append(repoIDs, pr.HeadRepoID) + } + repos, err := db.GetByIDs(ctx, "id", repoIDs, &repo_model.Repository{}) + if err != nil { + return err + } + for _, pr := range prs { + repo, ok := repos[pr.HeadRepoID] + if !ok { + return fmt.Errorf("unable to find repo %d", pr.HeadRepoID) + } + pr.HeadRepo = repo + } + return nil +} + // GetIssueIDs returns all issue ids func (prs PullRequestList) GetIssueIDs() []int64 { return container.FilterSlice(prs, func(pr *PullRequest) (int64, bool) { diff --git a/models/issues/reaction.go b/models/issues/reaction.go index 522040c022..21975c6b00 100644 --- a/models/issues/reaction.go +++ b/models/issues/reaction.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "slices" "forgejo.org/models/db" repo_model "forgejo.org/models/repo" @@ -176,6 +177,34 @@ func FindReactions(ctx context.Context, opts FindReactionsOptions) (ReactionList return reactions, count, err } +func getReactionsForComments(ctx context.Context, issueID int64, commentIDs []int64) (map[int64]ReactionList, error) { + reactions := make(map[int64]ReactionList, len(commentIDs)) + + for commentIDChunk := range slices.Chunk(commentIDs, db.DefaultMaxInSize) { + rows, err := db.GetEngine(ctx). + Where(builder.Eq{"issue_id": issueID}). + In("reaction.`type`", setting.UI.Reactions). + In("comment_id", commentIDChunk). + Rows(&Reaction{}) + if err != nil { + return nil, err + } + + for rows.Next() { + var reaction Reaction + err = rows.Scan(&reaction) + if err != nil { + _ = rows.Close() + return nil, err + } + reactions[reaction.CommentID] = append(reactions[reaction.CommentID], &reaction) + } + + _ = rows.Close() + } + return reactions, nil +} + func createReaction(ctx context.Context, opts *ReactionOptions) (*Reaction, error) { reaction := &Reaction{ Type: opts.Type, diff --git a/models/issues/review.go b/models/issues/review.go index 5370117a81..6ff36615db 100644 --- a/models/issues/review.go +++ b/models/issues/review.go @@ -457,6 +457,11 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi if official, err = IsOfficialReviewer(ctx, issue, doer); err != nil { return nil, nil, err } + // delete previous review requests from the same user + reviewCond := builder.Eq{"reviewer_id": doer.ID, "issue_id": issue.ID} + if _, err := sess.Where(reviewCond.And(builder.Eq{"type": ReviewTypeRequest})).Delete(new(Review)); err != nil { + return nil, nil, err + } } review.Official = official @@ -511,10 +516,14 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi // GetReviewByIssueIDAndUserID get the latest review of reviewer for a pull request func GetReviewByIssueIDAndUserID(ctx context.Context, issueID, userID int64) (*Review, error) { + return GetReviewByIssueIDUserIDAndTypes(ctx, issueID, userID, []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest}) +} + +func GetReviewByIssueIDUserIDAndTypes(ctx context.Context, issueID, userID int64, types []ReviewType) (*Review, error) { review := new(Review) has, err := db.GetEngine(ctx).Where( - builder.In("type", ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest). + builder.In("type", types). And(builder.Eq{"issue_id": issueID, "reviewer_id": userID, "original_author_id": 0})). Desc("id"). Get(review) @@ -707,12 +716,12 @@ func RemoveReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user } defer committer.Close() - review, err := GetReviewByIssueIDAndUserID(ctx, issue.ID, reviewer.ID) + review, err := GetReviewByIssueIDUserIDAndTypes(ctx, issue.ID, reviewer.ID, []ReviewType{ReviewTypeRequest}) if err != nil && !IsErrReviewNotExist(err) { return nil, err } - if review == nil || review.Type != ReviewTypeRequest { + if review == nil { return nil, nil } diff --git a/models/issues/review_list.go b/models/issues/review_list.go index 9eca4a9b4b..878ceac9ce 100644 --- a/models/issues/review_list.go +++ b/models/issues/review_list.go @@ -20,7 +20,7 @@ type ReviewList []*Review // LoadReviewers loads reviewers func (reviews ReviewList) LoadReviewers(ctx context.Context) error { reviewerIDs := make([]int64, len(reviews)) - for i := 0; i < len(reviews); i++ { + for i := range reviews { reviewerIDs[i] = reviews[i].ReviewerID } reviewers, err := user_model.GetPossibleUserByIDs(ctx, reviewerIDs) @@ -113,8 +113,8 @@ func (opts *FindReviewOptions) toCond() builder.Cond { if opts.OfficialOnly { cond = cond.And(builder.Eq{"official": true}) } - if opts.Dismissed.Has() { - cond = cond.And(builder.Eq{"dismissed": opts.Dismissed.Value()}) + if has, value := opts.Dismissed.Get(); has { + cond = cond.And(builder.Eq{"dismissed": value}) } return cond } diff --git a/models/issues/review_test.go b/models/issues/review_test.go index bdeaae5ea3..e035be617b 100644 --- a/models/issues/review_test.go +++ b/models/issues/review_test.go @@ -321,6 +321,82 @@ func TestAddReviewRequest(t *testing.T) { assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) } +func TestSubmitPendingReviewDeletesReviewRequest(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + require.NoError(t, pull.LoadIssue(db.DefaultContext)) + issue := pull.Issue + require.NoError(t, issue.LoadRepo(db.DefaultContext)) + reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + reviewRequest, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Issue: issue, + Reviewer: reviewer, + Type: issues_model.ReviewTypeRequest, + }) + require.NoError(t, err) + + // creating a pending review should NOT remove review requests + reviewPending, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Issue: issue, + Reviewer: reviewer, + Type: issues_model.ReviewTypePending, + }) + require.NoError(t, err) + unittest.AssertExistsIf(t, true, &issues_model.Review{ID: reviewRequest.ID}) + // submitting a pending review to finish it SHOULD remove review requests + _, _, err = issues_model.SubmitReview( + db.DefaultContext, + reviewer, + issue, + issues_model.ReviewTypeReject, + "test content", + reviewPending.CommitID, + false, + []string{}, + ) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &issues_model.Review{ID: reviewRequest.ID}) +} + +// this test is for handling a state correctly that should never exist, but is representable and was +// achievable thanks to #12243 +func TestReviewRequestDeletesReviewRequestsBeforeRejectedReviews(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + sess := db.GetEngine(db.DefaultContext) + + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + require.NoError(t, pull.LoadIssue(db.DefaultContext)) + issue := pull.Issue + require.NoError(t, issue.LoadRepo(db.DefaultContext)) + reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + + // this one will end up being a ReviewTypeRequest. We are initially creating it as + // ReviewTypeReject to avoid it being deleted on making the actual rejected review + reviewRequest, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Issue: issue, + Reviewer: reviewer, + Type: issues_model.ReviewTypeReject, + }) + require.NoError(t, err) + // this review is an actual rejected review that somehow managed to be saved without deleting + // reviewRequest. This is a state that is representable and is/was achievable thanks to #12243 + _, err = issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{ + Issue: issue, + Reviewer: reviewer, + Type: issues_model.ReviewTypeReject, + }) + require.NoError(t, err) + reviewRequest.Type = issues_model.ReviewTypeRequest + _, err = sess.ID(reviewRequest.ID).Cols("type").Update(reviewRequest) + require.NoError(t, err) + + _, err = issues_model.RemoveReviewRequest(db.DefaultContext, issue, reviewer, doer) + require.NoError(t, err) + unittest.AssertNotExistsBean(t, &issues_model.Review{ID: reviewRequest.ID}) +} + func TestAddTeamReviewRequest(t *testing.T) { defer unittest.OverrideFixtures("models/fixtures/TestAddTeamReviewRequest")() require.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/issues/tracked_time.go b/models/issues/tracked_time.go index e083f6e1e8..54173681bd 100644 --- a/models/issues/tracked_time.go +++ b/models/issues/tracked_time.go @@ -350,10 +350,7 @@ func GetIssueTotalTrackedTime(ctx context.Context, opts *IssuesOptions, isClosed // we get the statistics in smaller chunks and get accumulates var accum int64 for i := 0; i < len(opts.IssueIDs); { - chunk := i + MaxQueryParameters - if chunk > len(opts.IssueIDs) { - chunk = len(opts.IssueIDs) - } + chunk := min(i+MaxQueryParameters, len(opts.IssueIDs)) time, err := getIssueTotalTrackedTimeChunk(ctx, opts, isClosed, opts.IssueIDs[i:chunk]) if err != nil { return 0, err @@ -379,8 +376,8 @@ func getIssueTotalTrackedTimeChunk(ctx context.Context, opts *IssuesOptions, isC } session := sumSession(opts, issueIDs) - if isClosed.Has() { - session = session.And("issue.is_closed = ?", isClosed.Value()) + if has, value := isClosed.Get(); has { + session = session.And("issue.is_closed = ?", value) } return session.SumInt(new(trackedTime), "tracked_time.time") } diff --git a/models/org_team.go b/models/org_team.go index ecda43f0a9..187ea1560d 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -23,34 +23,44 @@ import ( "xorm.io/builder" ) -func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository) (err error) { - if err = organization.AddTeamRepo(ctx, t.OrgID, t.ID, repo.ID); err != nil { - return err +func AddRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository) error { + _, err := InsertTeamRepository(ctx, t, repo) + return err +} + +func InsertTeamRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository) (teamRepo *organization.TeamRepo, err error) { + teamRepo = &organization.TeamRepo{ + OrgID: t.OrgID, + TeamID: t.ID, + RepoID: repo.ID, + } + if _, err = db.GetEngine(ctx).Insert(teamRepo); err != nil { + return nil, err } if err = organization.IncrTeamRepoNum(ctx, t.ID); err != nil { - return fmt.Errorf("update team: %w", err) + return nil, fmt.Errorf("update team: %w", err) } t.NumRepos++ if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil { - return fmt.Errorf("recalculateAccesses: %w", err) + return nil, fmt.Errorf("recalculateAccesses: %w", err) } // Make all team members watch this repo if enabled in global settings if setting.Service.AutoWatchNewRepos { if err = t.LoadMembers(ctx); err != nil { - return fmt.Errorf("getMembers: %w", err) + return nil, fmt.Errorf("getMembers: %w", err) } for _, u := range t.Members { if err = repo_model.WatchRepo(ctx, u.ID, repo.ID, true); err != nil { - return fmt.Errorf("watchRepo: %w", err) + return nil, fmt.Errorf("watchRepo: %w", err) } } } - return nil + return teamRepo, nil } // addAllRepositories adds all repositories to the team. @@ -116,7 +126,7 @@ func removeAllRepositories(ctx context.Context, t *organization.Team) (err error return err } - // Remove watches from all users and now unaccessible repos + // Remove watches from all users and now inaccessible repos for _, user := range t.Members { has, err := access_model.HasAccess(ctx, user.ID, repo) if err != nil { @@ -354,16 +364,27 @@ func DeleteTeam(ctx context.Context, t *organization.Team) error { return committer.Commit() } +func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error { + _, err := InsertTeamMember(ctx, team, userID) + return err +} + // AddTeamMember adds new membership of given team to given organization, // the user will have membership to given organization automatically when needed. -func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) error { +func InsertTeamMember(ctx context.Context, team *organization.Team, userID int64) (*organization.TeamUser, error) { isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, userID) if err != nil || isAlreadyMember { - return err + return nil, err } if err := organization.AddOrgUser(ctx, team.OrgID, userID); err != nil { - return err + return nil, err + } + + teamUser := &organization.TeamUser{ + UID: userID, + OrgID: team.OrgID, + TeamID: team.ID, } err = db.WithTx(ctx, func(ctx context.Context) error { @@ -375,11 +396,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e sess := db.GetEngine(ctx) - if err := db.Insert(ctx, &organization.TeamUser{ - UID: userID, - OrgID: team.OrgID, - TeamID: team.ID, - }); err != nil { + if err := db.Insert(ctx, teamUser); err != nil { return err } else if _, err := sess.Incr("num_members").ID(team.ID).Update(new(organization.Team)); err != nil { return err @@ -420,7 +437,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e return nil }) if err != nil { - return err + return nil, err } // this behaviour may spend much time so run it in a goroutine @@ -440,7 +457,7 @@ func AddTeamMember(ctx context.Context, team *organization.Team, userID int64) e }(team.Repos) } - return nil + return teamUser, nil } func removeTeamMember(ctx context.Context, team *organization.Team, userID int64) error { @@ -480,12 +497,12 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64 return err } - // Remove watches from now unaccessible + // Remove watches from now inaccessible if err := ReconsiderWatches(ctx, repo, userID); err != nil { return err } - // Remove issue assignments from now unaccessible + // Remove issue assignments from now inaccessible if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { return err } diff --git a/models/org_team_test.go b/models/org_team_test.go index 730bc65f7d..5f7544e9ea 100644 --- a/models/org_team_test.go +++ b/models/org_team_test.go @@ -19,20 +19,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestTeam_AddMember(t *testing.T) { - require.NoError(t, unittest.PrepareTestDatabase()) - - test := func(teamID, userID int64) { - team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID}) - require.NoError(t, AddTeamMember(db.DefaultContext, team, userID)) - unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: userID, TeamID: teamID}) - unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &user_model.User{ID: team.OrgID}) - } - test(1, 2) - test(1, 4) - test(3, 2) -} - func TestTeam_RemoveMember(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) @@ -132,6 +118,96 @@ func TestAddTeamMember(t *testing.T) { test(3, 2) } +func TestTeam_AddAndReturnTeamMember(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + for _, testCase := range []struct { + name string + alreadyMember bool + teamID int64 + userID int64 + }{ + { + name: "Already member of a team with repositories", + alreadyMember: true, + teamID: 1, + userID: 2, + }, + { + name: "New member of a team with repositories", + teamID: 1, + userID: 4, + }, + { + name: "New member of a team with no repositories", + teamID: 3, + userID: 2, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: testCase.teamID}) + teamUser, err := InsertTeamMember(db.DefaultContext, team, testCase.userID) + require.NoError(t, err) + if testCase.alreadyMember { + assert.Nil(t, teamUser) + } else { + require.NotNil(t, teamUser) + assert.Equal(t, testCase.teamID, teamUser.TeamID) + assert.Equal(t, testCase.userID, teamUser.UID) + } + unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: testCase.userID, TeamID: testCase.teamID}) + unittest.CheckConsistencyFor(t, &organization.Team{ID: testCase.teamID}, &user_model.User{ID: team.OrgID}) + }) + } +} + +func TestTeam_AddTeamRepository(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + for _, testCase := range []struct { + name string + teamID int64 + repoID int64 + }{ + { + name: "AddAndReturnTeamRepository", + teamID: 8, + repoID: 23, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: testCase.teamID}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: testCase.repoID}) + teamRepo, err := InsertTeamRepository(t.Context(), team, repo) + require.NoError(t, err) + require.NotNil(t, teamRepo) + assert.Equal(t, testCase.teamID, teamRepo.TeamID) + assert.Equal(t, testCase.repoID, teamRepo.RepoID) + unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{RepoID: testCase.repoID, TeamID: testCase.teamID}) + }) + } + + for _, testCase := range []struct { + name string + teamID int64 + repoID int64 + }{ + { + name: "AddTeamRepository", + teamID: 9, + repoID: 23, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: testCase.teamID}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: testCase.repoID}) + err := AddRepository(t.Context(), team, repo) + require.NoError(t, err) + unittest.AssertExistsAndLoadBean(t, &organization.TeamRepo{RepoID: testCase.repoID, TeamID: testCase.teamID}) + }) + } +} + func TestRemoveTeamMember(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) diff --git a/models/organization/org.go b/models/organization/org.go index 6da8886c2f..02794ba2cb 100644 --- a/models/organization/org.go +++ b/models/organization/org.go @@ -17,6 +17,7 @@ import ( "forgejo.org/models/unit" user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/modules/optional" "forgejo.org/modules/setting" "forgejo.org/modules/structs" "forgejo.org/modules/util" @@ -191,7 +192,7 @@ func (org *Organization) IsGhost() bool { return org.AsUser().IsGhost() } -// FindOrgMembersOpts represensts find org members conditions +// FindOrgMembersOpts represents find org members conditions type FindOrgMembersOpts struct { db.ListOptions Doer *user_model.User @@ -396,6 +397,14 @@ func DeleteOrganization(ctx context.Context, org *Organization) error { return fmt.Errorf("%s is a user not an organization", org.Name) } + // Decrease following count of users that follow the organisation. + followerIDs, err := db.FindIDs(ctx, "follow", "follow.user_id", builder.Eq{"follow.follow_id": org.ID}) + if err != nil { + return fmt.Errorf("get all followers: %w", err) + } else if err = db.DecrByIDs(ctx, followerIDs, "num_following", new(user_model.User)); err != nil { + return fmt.Errorf("decrease user num_following: %w", err) + } + if err := db.DeleteBeans(ctx, &Team{OrgID: org.ID}, &OrgUser{OrgID: org.ID}, @@ -404,7 +413,9 @@ func DeleteOrganization(ctx context.Context, org *Organization) error { &TeamInvite{OrgID: org.ID}, &secret_model.Secret{OwnerID: org.ID}, &actions_model.ActionRunner{OwnerID: org.ID}, - &actions_model.ActionRunnerToken{OwnerID: org.ID}, + &actions_model.ActionRunnerToken{OwnerID: optional.Some(org.ID)}, + &user_model.BlockedUser{UserID: org.ID}, + &user_model.Follow{FollowID: org.ID}, ); err != nil { return fmt.Errorf("DeleteBeans: %w", err) } @@ -510,10 +521,10 @@ func ChangeOrgUserStatus(ctx context.Context, orgID, uid int64, public bool) err // AddOrgUser adds new user to given organization. func AddOrgUser(ctx context.Context, orgID, uid int64) error { - isUser, err := user_model.IsUserByID(ctx, uid) + eligible, err := IsAnEligibleTeamMemberByID(ctx, uid) if err != nil { return err - } else if !isUser { + } else if !eligible { return user_model.ErrUserWrongType{UID: uid} } diff --git a/models/organization/org_user.go b/models/organization/org_user.go index 81671c5cf5..4c84fccbf3 100644 --- a/models/organization/org_user.go +++ b/models/organization/org_user.go @@ -112,6 +112,15 @@ func IsUserOrgOwner(ctx context.Context, users user_model.UserList, orgID int64) return results } +// Returns true if the given user ID is allowed to be a team member +func IsAnEligibleTeamMemberByID(ctx context.Context, uid int64) (bool, error) { + return db.GetEngine(ctx). + Where("id=?", uid). + In("type", user_model.UserTypeIndividual, user_model.UserTypeBot, user_model.UserTypeRemoteUser). + Table("user"). + Exist() +} + func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) { if len(users) == 0 { return nil, nil diff --git a/models/organization/org_user_test.go b/models/organization/org_user_test.go index 4011e5df88..670a641214 100644 --- a/models/organization/org_user_test.go +++ b/models/organization/org_user_test.go @@ -162,3 +162,41 @@ func TestAddOrgUser(t *testing.T) { unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{}) } + +func TestIsAnEligibleTeamMemberByID(t *testing.T) { + defer unittest.OverrideFixtures("models/user/fixtures/")() + require.NoError(t, unittest.PrepareTestDatabase()) + + for _, testCase := range []struct { + name string + id int64 + eligible bool + }{ + { + name: "Regular user", + id: 1, + eligible: true, + }, + { + name: "Bot user", + id: 1042, + eligible: true, + }, + { + name: "Organization", + id: 3, + eligible: false, + }, + { + name: "F3 Remote user", + id: 1041, + eligible: true, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + eligible, err := organization.IsAnEligibleTeamMemberByID(t.Context(), testCase.id) + require.NoError(t, err) + assert.Equal(t, testCase.eligible, eligible) + }) + } +} diff --git a/models/organization/team.go b/models/organization/team.go index 209471e013..b7b93821ad 100644 --- a/models/organization/team.go +++ b/models/organization/team.go @@ -161,10 +161,16 @@ func (t *Team) LoadRepositories(ctx context.Context) (err error) { return err } -// LoadMembers returns paginated members in team of organization. +// LoadMembers loads the members of the team in t.Members. func (t *Team) LoadMembers(ctx context.Context) (err error) { + return t.LoadPaginatedMembers(ctx, db.ListOptionsAll) +} + +// LoadPaginatedMembers loads paginated members of the team in t.Members. +func (t *Team) LoadPaginatedMembers(ctx context.Context, listOptions db.ListOptions) (err error) { t.Members, err = GetTeamMembers(ctx, &SearchMembersOptions{ - TeamID: t.ID, + ListOptions: listOptions, + TeamID: t.ID, }) return err } diff --git a/models/organization/team_repo.go b/models/organization/team_repo.go index 334b139808..895d5a56d0 100644 --- a/models/organization/team_repo.go +++ b/models/organization/team_repo.go @@ -34,6 +34,8 @@ func HasTeamRepo(ctx context.Context, orgID, teamID, repoID int64) bool { type SearchTeamRepoOptions struct { db.ListOptions TeamID int64 + // Filters repositories based upon optional authorization restrictions. + AuthorizationReducer repo_model.RepositoryAuthorizationReducer } // GetRepositories returns paginated repositories in team of organization. @@ -46,6 +48,9 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (repo Where(builder.Eq{"team_id": opts.TeamID}), ) } + if opts.AuthorizationReducer != nil { + sess = sess.Where(opts.AuthorizationReducer.RepoReadAccessFilter()) + } if opts.PageSize > 0 { sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) } @@ -54,16 +59,6 @@ func GetTeamRepositories(ctx context.Context, opts *SearchTeamRepoOptions) (repo Find(&repos) } -// AddTeamRepo adds a repo for an organization's team -func AddTeamRepo(ctx context.Context, orgID, teamID, repoID int64) error { - _, err := db.GetEngine(ctx).Insert(&TeamRepo{ - OrgID: orgID, - TeamID: teamID, - RepoID: repoID, - }) - return err -} - // RemoveTeamRepo remove repository from team func RemoveTeamRepo(ctx context.Context, teamID, repoID int64) error { _, err := db.DeleteByBean(ctx, &TeamRepo{ diff --git a/models/packages/descriptor.go b/models/packages/descriptor.go index 19e0e8f5d5..947144ec54 100644 --- a/models/packages/descriptor.go +++ b/models/packages/descriptor.go @@ -174,8 +174,10 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc metadata = &debian.Metadata{} case TypeGeneric: // generic packages have no metadata + break case TypeGo: // go packages have no metadata + break case TypeHelm: metadata = &helm.Metadata{} case TypeNuGet: diff --git a/models/packages/nuget/search.go b/models/packages/nuget/search.go index af83c27c66..d609e7e894 100644 --- a/models/packages/nuget/search.go +++ b/models/packages/nuget/search.go @@ -55,7 +55,7 @@ func CountPackages(ctx context.Context, opts *packages_model.PackageSearchOption func toConds(opts *packages_model.PackageSearchOptions) builder.Cond { var cond builder.Cond = builder.Eq{ - "package.is_internal": opts.IsInternal.Value(), + "package.is_internal": opts.IsInternal.ValueOrZeroValue(), "package.owner_id": opts.OwnerID, "package.type": packages_model.TypeNuGet, } diff --git a/models/packages/package_version.go b/models/packages/package_version.go index 87a97143f3..545ad63eb4 100644 --- a/models/packages/package_version.go +++ b/models/packages/package_version.go @@ -5,11 +5,13 @@ package packages import ( "context" + "errors" "strconv" "strings" "forgejo.org/models/db" "forgejo.org/modules/optional" + "forgejo.org/modules/setting" "forgejo.org/modules/timeutil" "forgejo.org/modules/util" @@ -155,6 +157,25 @@ func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error }) } +func (pv *PackageVersion) LockForUpdate(ctx context.Context) error { + if !db.InTransaction(ctx) { + return errors.New("invalid state for PackageVersion.LockForUpdate: database is not in a transaction") + } else if setting.Database.Type.IsSQLite3() { + // SQLite both doesn't support "SELECT ... FOR UPDATE", and it's irrelevant for SQLite as the entire database is + // locked for write when a write transaction is open. + return nil + } + + pvfu := PackageVersion{} + has, err := db.GetEngine(ctx).ID(pv.ID).ForUpdate().Get(&pvfu) + if err != nil { + return err + } else if !has { + return ErrPackageNotExist + } + return nil +} + // SearchValue describes a value to search // If ExactMatch is true, the field must match the value otherwise a LIKE search is performed. type SearchValue struct { @@ -192,9 +213,9 @@ type PackageSearchOptions struct { func (opts *PackageSearchOptions) ToConds() builder.Cond { cond := builder.NewCond() - if opts.IsInternal.Has() { + if has, value := opts.IsInternal.Get(); has { cond = builder.Eq{ - "package_version.is_internal": opts.IsInternal.Value(), + "package_version.is_internal": value, } } @@ -254,10 +275,10 @@ func (opts *PackageSearchOptions) ToConds() builder.Cond { cond = cond.And(builder.Exists(builder.Select("package_file.id").From("package_file").Where(fileCond))) } - if opts.HasFiles.Has() { + if has, value := opts.HasFiles.Get(); has { filesCond := builder.Exists(builder.Select("package_file.id").From("package_file").Where(builder.Expr("package_file.version_id = package_version.id"))) - if !opts.HasFiles.Value() { + if !value { filesCond = builder.Not{filesCond} } diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go index f7daf38e5c..fd1b93c867 100644 --- a/models/perm/access/repo_permission.go +++ b/models/perm/access/repo_permission.go @@ -6,6 +6,7 @@ package access import ( "context" "fmt" + "strings" actions_model "forgejo.org/models/actions" "forgejo.org/models/db" @@ -15,6 +16,7 @@ import ( "forgejo.org/models/unit" user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/services/authz" ) // Permission contains all the permissions related variables to a repository for a user @@ -114,7 +116,8 @@ func (p *Permission) CanWriteIssuesOrPulls(isPull bool) bool { } func (p *Permission) LogString() string { - format := "") + return fmt.Sprintf(format.String(), args...) } func GetActionRepoPermission(ctx context.Context, repo *repo_model.Repository, task *actions_model.ActionTask) (Permission, error) { @@ -164,7 +167,28 @@ func GetActionRepoPermission(ctx context.Context, repo *repo_model.Repository, t return GetUserRepoPermission(ctx, repo, user_model.NewActionsUser()) } -// GetUserRepoPermission returns the user permissions to the repository +// GetUserRepoPermission returns the user permissions to the repository, where the user's permissions may be +// artificially restricted by a an authorization reducer. +func GetUserRepoPermissionWithReducer(ctx context.Context, repo *repo_model.Repository, user *user_model.User, reducer authz.AuthorizationReducer) (Permission, error) { + perm, err := GetUserRepoPermission(ctx, repo, user) + if err != nil { + return perm, err + } + perm.AccessMode, err = reducer.ReduceRepoAccess(ctx, repo, perm.AccessMode) + if err != nil { + return perm, fmt.Errorf("failure in ReduceRepoAccess: %w", err) + } + for unit, currentAccessMode := range perm.UnitsMode { + reduced, err := reducer.ReduceRepoAccess(ctx, repo, currentAccessMode) + if err != nil { + return perm, fmt.Errorf("failure in ReduceRepoAccess: %w", err) + } + perm.UnitsMode[unit] = reduced + } + return perm, nil +} + +// GetUserRepoPermission returns the user permissions to the repository. func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) { var perm Permission if log.IsTrace() { diff --git a/models/perm/access/repo_permission_test.go b/models/perm/access/repo_permission_test.go index 55bc975421..303605047f 100644 --- a/models/perm/access/repo_permission_test.go +++ b/models/perm/access/repo_permission_test.go @@ -8,9 +8,13 @@ import ( perm_model "forgejo.org/models/perm" "forgejo.org/models/perm/access" repo_model "forgejo.org/models/repo" + "forgejo.org/models/unit" "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + "forgejo.org/services/authz" "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -76,3 +80,73 @@ func TestActionTaskNoAccessPrivateRepo(t *testing.T) { require.NoError(t, err) assertAccess(t, perm_model.AccessModeNone, &perm) } + +func TestGetUserRepoPermissionWithReducer(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("no unit-level overrides", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + // Baseline check that without a reducer, we get AccessModeOwner... + permWithoutReducer, err := access.GetUserRepoPermission(t.Context(), repo, user) + require.NoError(t, err) + require.NotNil(t, permWithoutReducer) + assert.True(t, permWithoutReducer.IsOwner()) + assert.True(t, permWithoutReducer.IsAdmin()) + assert.True(t, permWithoutReducer.HasAccess()) + assert.True(t, permWithoutReducer.CanWrite(unit.TypeIssues)) + + reducer := authz.NewMockAuthorizationReducer(t) + reducer.On( + "ReduceRepoAccess", + mock.Anything, // context + mock.MatchedBy(func(repo *repo_model.Repository) bool { // repo + return repo.ID == 1 + }), + perm_model.AccessModeOwner, // incoming access mode + ).Return(perm_model.AccessModeNone, nil) + + permWithReducer, err := access.GetUserRepoPermissionWithReducer(t.Context(), repo, user, reducer) + require.NoError(t, err) + require.NotNil(t, permWithReducer) + assert.False(t, permWithReducer.IsOwner()) + assert.False(t, permWithReducer.IsAdmin()) + assert.False(t, permWithReducer.HasAccess()) + assert.False(t, permWithReducer.CanWrite(unit.TypeIssues)) + }) + + t.Run("team unit-level overrides", func(t *testing.T) { + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) + + // Baseline check that without a reducer, we get mixed access for different units... + permWithoutReducer, err := access.GetUserRepoPermission(t.Context(), repo, user) + require.NoError(t, err) + require.NotNil(t, permWithoutReducer) + require.NotEmpty(t, permWithoutReducer.UnitsMode) // unit-specific access modes loaded + assert.True(t, permWithoutReducer.CanRead(unit.TypeCode)) + assert.False(t, permWithoutReducer.CanWrite(unit.TypeCode)) + assert.True(t, permWithoutReducer.CanRead(unit.TypeIssues)) + assert.True(t, permWithoutReducer.CanWrite(unit.TypeIssues)) + + reducer := authz.NewMockAuthorizationReducer(t) + reducer.On( + "ReduceRepoAccess", + mock.Anything, // context + mock.MatchedBy(func(repo *repo_model.Repository) bool { // repo + return repo.ID == 32 + }), + mock.Anything, // incoming access mode - will vary for each unit + ).Return(perm_model.AccessModeRead, nil) + + permWithReducer, err := access.GetUserRepoPermissionWithReducer(t.Context(), repo, user, reducer) + require.NoError(t, err) + require.NotNil(t, permWithReducer) + require.NotEmpty(t, permWithReducer.UnitsMode) // unit-specific access modes loaded + assert.True(t, permWithReducer.CanRead(unit.TypeCode)) + assert.False(t, permWithReducer.CanWrite(unit.TypeCode)) + assert.True(t, permWithReducer.CanRead(unit.TypeIssues)) + assert.False(t, permWithReducer.CanWrite(unit.TypeIssues)) + }) +} diff --git a/models/project/column.go b/models/project/column.go index 20de39357b..858a531bfc 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -10,6 +10,7 @@ import ( "regexp" "forgejo.org/models/db" + "forgejo.org/modules/container" "forgejo.org/modules/setting" "forgejo.org/modules/timeutil" "forgejo.org/modules/util" @@ -42,10 +43,10 @@ type Column struct { ID int64 `xorm:"pk autoincr"` Title string Default bool `xorm:"NOT NULL DEFAULT false"` // issues not assigned to a specific column will be assigned to this column - Sorting int8 `xorm:"NOT NULL DEFAULT 0"` + Sorting int8 `xorm:"NOT NULL DEFAULT 0 unique(project_sorting)"` Color string `xorm:"VARCHAR(7)"` - ProjectID int64 `xorm:"INDEX NOT NULL"` + ProjectID int64 `xorm:"INDEX NOT NULL unique(project_sorting)"` CreatorID int64 `xorm:"NOT NULL"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` @@ -103,8 +104,9 @@ func createDefaultColumnsForProject(ctx context.Context, project *Project) error Title: "Backlog", ProjectID: project.ID, Default: true, + Sorting: 0, } - if err := db.Insert(ctx, column); err != nil { + if err := db.Insert(ctx, &column); err != nil { return err } @@ -113,12 +115,13 @@ func createDefaultColumnsForProject(ctx context.Context, project *Project) error } columns := make([]Column, 0, len(items)) - for _, v := range items { + for i, v := range items { columns = append(columns, Column{ CreatedUnix: timeutil.TimeStampNow(), CreatorID: project.CreatorID, Title: v, ProjectID: project.ID, + Sorting: int8(i + 1), }) } @@ -215,9 +218,7 @@ func GetColumn(ctx context.Context, columnID int64) (*Column, error) { func UpdateColumn(ctx context.Context, column *Column) error { var fieldToUpdate []string - if column.Sorting != 0 { - fieldToUpdate = append(fieldToUpdate, "sorting") - } + fieldToUpdate = append(fieldToUpdate, "sorting") if column.Title != "" { fieldToUpdate = append(fieldToUpdate, "title") @@ -257,12 +258,22 @@ func (p *Project) GetDefaultColumn(ctx context.Context) (*Column, error) { return &column, nil } - // create a default column if none is found + // create a default column if none is found, using the next available sorting value + res := struct { + MaxSorting int64 + ColumnCount int64 + }{} + if _, err := db.GetEngine(ctx).Select("max(sorting) as max_sorting, count(*) as column_count"). + Table("project_board").Where("project_id=?", p.ID).Get(&res); err != nil { + return nil, err + } + column = Column{ ProjectID: p.ID, Default: true, Title: "Uncategorized", CreatorID: p.CreatorID, + Sorting: int8(util.Iif(res.ColumnCount > 0, res.MaxSorting+1, 0)), } if _, err := db.GetEngine(ctx).Insert(&column); err != nil { return nil, err @@ -305,27 +316,59 @@ func GetColumnsByIDs(ctx context.Context, projectID int64, columnsIDs []int64) ( return columns, nil } -// MoveColumnsOnProject sorts columns in a project +// MoveColumnsOnProject sorts columns in a project using a two-phase approach +// to avoid unique constraint collisions during swap operations. +// All columns in the project must be included in the sortedColumnIDs map. func MoveColumnsOnProject(ctx context.Context, project *Project, sortedColumnIDs map[int64]int64) error { return db.WithTx(ctx, func(ctx context.Context) error { sess := db.GetEngine(ctx) + + // Validate no duplicate column IDs in map values + columnIDSet := make(container.Set[int64], len(sortedColumnIDs)) + for _, columnID := range sortedColumnIDs { + if !columnIDSet.Add(columnID) { + return errors.New("duplicate column ID in reorder request") + } + } + + // Validate all columns exist and belong to this project + allColumns, err := project.GetColumns(ctx) + if err != nil { + return err + } + if len(allColumns) != len(sortedColumnIDs) { + return errors.New("all columns in the project must be included in the reorder request") + } + columnIDs := util.ValuesOfMap(sortedColumnIDs) movedColumns, err := GetColumnsByIDs(ctx, project.ID, columnIDs) if err != nil { return err } if len(movedColumns) != len(sortedColumnIDs) { - return errors.New("some columns do not exist") + return errors.New("some columns do not exist in this project") } + // Build reverse map: columnID → target sorting + targetSortingByColumn := make(map[int64]int64, len(sortedColumnIDs)) + for sorting, columnID := range sortedColumnIDs { + targetSortingByColumn[columnID] = sorting + } + + // Phase 1: negate using target sorting values (guaranteed unique since + // they are map keys) to avoid unique constraint collisions during swap for _, column := range movedColumns { - if column.ProjectID != project.ID { - return fmt.Errorf("column[%d]'s projectID is not equal to project's ID [%d]", column.ProjectID, project.ID) + targetSorting := targetSortingByColumn[column.ID] + if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", + -(targetSorting + 1), column.ID); err != nil { + return err } } + // Phase 2: set final values for sorting, columnID := range sortedColumnIDs { - if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", sorting, columnID); err != nil { + if _, err := sess.Exec("UPDATE `project_board` SET sorting=? WHERE id=?", + sorting, columnID); err != nil { return err } } diff --git a/models/project/column_test.go b/models/project/column_test.go index 4fc452df33..2f4cc79367 100644 --- a/models/project/column_test.go +++ b/models/project/column_test.go @@ -84,9 +84,9 @@ func Test_MoveColumnsOnProject(t *testing.T) { columns, err := project1.GetColumns(db.DefaultContext) require.NoError(t, err) assert.Len(t, columns, 3) - assert.EqualValues(t, 0, columns[0].Sorting) // even if there is no default sorting, the code should also work - assert.EqualValues(t, 0, columns[1].Sorting) - assert.EqualValues(t, 0, columns[2].Sorting) + assert.EqualValues(t, 0, columns[0].Sorting) + assert.EqualValues(t, 1, columns[1].Sorting) + assert.EqualValues(t, 2, columns[2].Sorting) err = MoveColumnsOnProject(db.DefaultContext, project1, map[int64]int64{ 0: columns[1].ID, @@ -103,6 +103,59 @@ func Test_MoveColumnsOnProject(t *testing.T) { assert.Equal(t, columns[0].ID, columnsAfter[2].ID) } +func TestMoveColumnsOnProjectSwap(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + project1 := unittest.AssertExistsAndLoadBean(t, &Project{ID: 1}) + columns, err := project1.GetColumns(db.DefaultContext) + require.NoError(t, err) + require.Len(t, columns, 3) + + // First give them distinct positions + err = MoveColumnsOnProject(db.DefaultContext, project1, map[int64]int64{ + 0: columns[0].ID, + 1: columns[1].ID, + 2: columns[2].ID, + }) + require.NoError(t, err) + + // Now swap columns 0 and 1 (would collide under single-phase update) + err = MoveColumnsOnProject(db.DefaultContext, project1, map[int64]int64{ + 0: columns[1].ID, + 1: columns[0].ID, + 2: columns[2].ID, + }) + require.NoError(t, err) + + columnsAfter, err := project1.GetColumns(db.DefaultContext) + require.NoError(t, err) + assert.Len(t, columnsAfter, 3) + assert.Equal(t, columns[1].ID, columnsAfter[0].ID) + assert.Equal(t, columns[0].ID, columnsAfter[1].ID) + assert.Equal(t, columns[2].ID, columnsAfter[2].ID) +} + +func TestUpdateColumnSortingZero(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + column := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1}) + column.Sorting = 5 + require.NoError(t, UpdateColumn(db.DefaultContext, column)) + + // Verify it was set to 5 + updated, err := GetColumn(db.DefaultContext, column.ID) + require.NoError(t, err) + assert.Equal(t, int8(5), updated.Sorting) + + // Now set it back to 0 + column.Sorting = 0 + require.NoError(t, UpdateColumn(db.DefaultContext, column)) + + updated, err = GetColumn(db.DefaultContext, column.ID) + require.NoError(t, err) + assert.Equal(t, int8(0), updated.Sorting) +} + func Test_NewColumn(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) @@ -111,7 +164,7 @@ func Test_NewColumn(t *testing.T) { require.NoError(t, err) assert.Len(t, columns, 3) - for i := 0; i < maxProjectColumns-3; i++ { + for i := range maxProjectColumns - 3 { err := NewColumn(db.DefaultContext, &Column{ Title: fmt.Sprintf("column-%d", i+4), ProjectID: project1.ID, diff --git a/models/project/issue.go b/models/project/issue.go index d404033446..41caac991a 100644 --- a/models/project/issue.go +++ b/models/project/issue.go @@ -6,6 +6,7 @@ package project import ( "context" "errors" + "slices" "forgejo.org/models/db" "forgejo.org/modules/log" @@ -15,14 +16,14 @@ import ( // ProjectIssue saves relation from issue to a project type ProjectIssue struct { //revive:disable-line:exported ID int64 `xorm:"pk autoincr"` - IssueID int64 `xorm:"INDEX"` - ProjectID int64 `xorm:"INDEX"` + IssueID int64 `xorm:"INDEX NOT NULL unique(project_issue)"` + ProjectID int64 `xorm:"INDEX NOT NULL unique(project_issue)"` // ProjectColumnID should not be zero since 1.22. If it's zero, the issue will not be displayed on UI and it might result in errors. - ProjectColumnID int64 `xorm:"'project_board_id' INDEX"` + ProjectColumnID int64 `xorm:"'project_board_id' INDEX NOT NULL unique(column_sorting)"` // the sorting order on the column - Sorting int64 `xorm:"NOT NULL DEFAULT 0"` + Sorting int64 `xorm:"NOT NULL DEFAULT 0 unique(column_sorting)"` } func init() { @@ -62,26 +63,86 @@ func (p *Project) NumOpenIssues(ctx context.Context) int { return int(c) } -// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column +// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column. +// The sortedIssueIDs map keys are sorting positions and values are issue IDs. +// Cards not in the map that already exist in the target column are shifted to +// positions after the highest requested sorting value. func MoveIssuesOnProjectColumn(ctx context.Context, column *Column, sortedIssueIDs map[int64]int64) error { + if len(sortedIssueIDs) == 0 { + return nil + } return db.WithTx(ctx, func(ctx context.Context) error { sess := db.GetEngine(ctx) issueIDs := util.ValuesOfMap(sortedIssueIDs) - count, err := sess.Table(new(ProjectIssue)).Where("project_id=?", column.ProjectID).In("issue_id", issueIDs).Count() + // Build reverse map: issueID → sorting and validate no duplicate issue IDs + sortingByIssue := make(map[int64]int64, len(sortedIssueIDs)) + for sorting, issueID := range sortedIssueIDs { + sortingByIssue[issueID] = sorting + } + if len(sortingByIssue) != len(sortedIssueIDs) { + return errors.New("duplicate issue IDs in reorder request") + } + + // Validate all issues exist and belong to this project + count, err := sess.Table(new(ProjectIssue)). + Where("project_id=?", column.ProjectID). + In("issue_id", issueIDs).Count() if err != nil { return err } if int(count) != len(sortedIssueIDs) { - return errors.New("all issues have to be added to a project first") + return errors.New("all issues must belong to the specified project") } - for sorting, issueID := range sortedIssueIDs { - _, err = sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE issue_id=?", column.ID, sorting, issueID) + // Sort issue IDs to ensure consistent lock ordering across concurrent transactions. + // This prevents deadlocks when multiple transactions update overlapping rows. + slices.Sort(issueIDs) + + // Phase 1: Negate sorting for ALL cards currently in the target column + // to free up all positive sorting positions. This prevents collisions + // when moved cards are assigned their final positions. + if _, err := sess.Exec("UPDATE `project_issue` SET sorting = -(sorting + 1) WHERE project_board_id=? AND sorting >= 0", + column.ID); err != nil { + return err + } + + // Phase 2: Move the specified cards to the target column with their + // final sorting values. Since all existing cards in the column now have + // negative sorting, there are no collisions. + for _, issueID := range issueIDs { + _, err := sess.Exec("UPDATE `project_issue` SET project_board_id=?, sorting=? WHERE project_id=? AND issue_id=?", + column.ID, sortingByIssue[issueID], column.ProjectID, issueID) if err != nil { return err } } + + // Phase 3: Re-pack any remaining cards in the column that still have + // negative sorting (these are pre-existing cards NOT in the move set). + // Assign them positions after the highest requested sorting value. + var maxSorting int64 + for sorting := range sortedIssueIDs { + if sorting > maxSorting { + maxSorting = sorting + } + } + + var remainingCards []ProjectIssue + if err := sess.Where("project_board_id=? AND sorting < 0", column.ID). + OrderBy("sorting DESC"). // original order was -(original+1), so DESC gives ascending original order + Find(&remainingCards); err != nil { + return err + } + nextSorting := maxSorting + 1 + for _, card := range remainingCards { + if _, err := sess.Exec("UPDATE `project_issue` SET sorting=? WHERE id=?", + nextSorting, card.ID); err != nil { + return err + } + nextSorting++ + } + return nil }) } diff --git a/models/project/issue_test.go b/models/project/issue_test.go new file mode 100644 index 0000000000..95a10182d3 --- /dev/null +++ b/models/project/issue_test.go @@ -0,0 +1,149 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package project + +import ( + "testing" + + "forgejo.org/models/db" + "forgejo.org/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMoveIssuesOnProjectColumn(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + // Get column 1 which belongs to project 1 and has issue 1 + column := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1}) + require.Equal(t, int64(1), column.ProjectID) + + t.Run("Success", func(t *testing.T) { + // Issue 1 is in column 1 (from fixtures) + sortedIssueIDs := map[int64]int64{ + 0: 1, // sorting position 0 -> issue_id 1 + } + err := MoveIssuesOnProjectColumn(db.DefaultContext, column, sortedIssueIDs) + require.NoError(t, err) + + // Verify the sorting was updated using direct DB query + var card ProjectIssue + has, err := db.GetEngine(db.DefaultContext).Where("project_id=? AND issue_id=?", column.ProjectID, 1).Get(&card) + require.NoError(t, err) + require.True(t, has) + assert.Equal(t, int64(0), card.Sorting) + }) + + t.Run("MoveIssueFromDifferentColumn", func(t *testing.T) { + // Issue 3 is in column 2, not column 1 — but same project, so cross-column move should succeed + sortedIssueIDs := map[int64]int64{ + 0: 3, + } + err := MoveIssuesOnProjectColumn(db.DefaultContext, column, sortedIssueIDs) + require.NoError(t, err) + + // Verify the card was moved to column 1 and sorting updated + var card ProjectIssue + has, err := db.GetEngine(db.DefaultContext).Where("project_id=? AND issue_id=?", column.ProjectID, 3).Get(&card) + require.NoError(t, err) + require.True(t, has) + assert.Equal(t, column.ID, card.ProjectColumnID) + assert.Equal(t, int64(0), card.Sorting) + }) + + t.Run("ErrorIssueNotInProject", func(t *testing.T) { + // Issue 999 doesn't exist + sortedIssueIDs := map[int64]int64{ + 0: 999, + } + err := MoveIssuesOnProjectColumn(db.DefaultContext, column, sortedIssueIDs) + require.Error(t, err) + assert.Contains(t, err.Error(), "all issues must belong to the specified project") + }) +} + +func TestMoveIssuesOnProjectColumnSwap(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + column := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1}) + + // Setup: insert two cards at distinct positions using direct DB inserts + card1 := &ProjectIssue{ + IssueID: 14, + ProjectID: 1, + ProjectColumnID: column.ID, + Sorting: 10, + } + card2 := &ProjectIssue{ + IssueID: 15, + ProjectID: 1, + ProjectColumnID: column.ID, + Sorting: 11, + } + _, err := db.GetEngine(db.DefaultContext).Insert(card1) + require.NoError(t, err) + _, err = db.GetEngine(db.DefaultContext).Insert(card2) + require.NoError(t, err) + + // Swap them: card at 10→11, card at 11→10 + sortedIssueIDs := map[int64]int64{ + 11: 14, // issue 14 goes to position 11 + 10: 15, // issue 15 goes to position 10 + } + err = MoveIssuesOnProjectColumn(db.DefaultContext, column, sortedIssueIDs) + require.NoError(t, err) + + var resultCard14 ProjectIssue + has, err := db.GetEngine(db.DefaultContext).Where("project_id=? AND issue_id=?", 1, 14).Get(&resultCard14) + require.NoError(t, err) + require.True(t, has) + assert.Equal(t, int64(11), resultCard14.Sorting) + + var resultCard15 ProjectIssue + has, err = db.GetEngine(db.DefaultContext).Where("project_id=? AND issue_id=?", 1, 15).Get(&resultCard15) + require.NoError(t, err) + require.True(t, has) + assert.Equal(t, int64(10), resultCard15.Sorting) +} + +func TestMoveIssuesOnProjectColumnEmptyMap(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + column := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1}) + err := MoveIssuesOnProjectColumn(db.DefaultContext, column, map[int64]int64{}) + require.NoError(t, err) // empty map should be a no-op +} + +func TestMoveIssuesOnProjectColumnDuplicateIssueIDs(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + column := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1}) + err := MoveIssuesOnProjectColumn(db.DefaultContext, column, map[int64]int64{ + 0: 1, + 1: 1, // duplicate issue ID + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "duplicate issue IDs") +} + +func TestMoveIssuesToAnotherColumnErrorPaths(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("DifferentProject", func(t *testing.T) { + col1 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1, ProjectID: 1}) + col5 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 5, ProjectID: 2}) + + err := col1.moveIssuesToAnotherColumn(db.DefaultContext, col5) + require.Error(t, err) + assert.Contains(t, err.Error(), "columns have to be in the same project") + }) + + t.Run("SameColumnIsNoOp", func(t *testing.T) { + col1 := unittest.AssertExistsAndLoadBean(t, &Column{ID: 1, ProjectID: 1}) + + err := col1.moveIssuesToAnotherColumn(db.DefaultContext, col1) + require.NoError(t, err) + }) +} diff --git a/models/project/project.go b/models/project/project.go index 7d507b358d..be1b9d59c6 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -136,6 +136,7 @@ func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { / // Link returns the project's relative URL. func (p *Project) Link(ctx context.Context) string { + // nosemgrep: forgejo-logic-suspicious-OwnerID-check (system users are not stored in the database) if p.OwnerID > 0 { err := p.LoadOwner(ctx) if err != nil { @@ -183,7 +184,7 @@ func init() { // GetCardConfig retrieves the types of configurations project column cards could have // -//llu:returnsTrKey +//llu:returnsTrKeyWeak func GetCardConfig() []CardConfig { return []CardConfig{ {CardTypeTextOnly, "repo.projects.card_type.text_only"}, @@ -217,14 +218,14 @@ func (opts SearchOptions) ToConds() builder.Cond { if opts.RepoID > 0 { cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) } - if opts.IsClosed.Has() { - cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.Value()}) + if has, value := opts.IsClosed.Get(); has { + cond = cond.And(builder.Eq{"is_closed": value}) } if opts.Type > 0 { cond = cond.And(builder.Eq{"type": opts.Type}) } - if opts.OwnerID > 0 { + if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"owner_id": opts.OwnerID}) } diff --git a/models/project/template.go b/models/project/template.go index 278cf5b781..3d55cd27f2 100644 --- a/models/project/template.go +++ b/models/project/template.go @@ -27,7 +27,7 @@ const ( // GetTemplateConfigs retrieves the template configs of configurations project columns could have // -//llu:returnsTrKey +//llu:returnsTrKeyWeak func GetTemplateConfigs() []TemplateConfig { return []TemplateConfig{ {TemplateTypeNone, "repo.projects.type.none"}, diff --git a/models/pull/review_state.go b/models/pull/review_state.go index 2702d5d5a1..3fc3ab65c2 100644 --- a/models/pull/review_state.go +++ b/models/pull/review_state.go @@ -6,6 +6,7 @@ package pull import ( "context" "fmt" + "maps" "forgejo.org/models/db" "forgejo.org/modules/log" @@ -100,9 +101,7 @@ func mergeFiles(oldFiles, newFiles map[string]ViewedState) map[string]ViewedStat return oldFiles } - for file, viewed := range newFiles { - oldFiles[file] = viewed - } + maps.Copy(oldFiles, newFiles) return oldFiles } diff --git a/models/repo.go b/models/repo.go index 1b9cc8fa60..6a4da96b95 100644 --- a/models/repo.go +++ b/models/repo.go @@ -14,10 +14,8 @@ import ( asymkey_model "forgejo.org/models/asymkey" "forgejo.org/models/db" issues_model "forgejo.org/models/issues" - access_model "forgejo.org/models/perm/access" repo_model "forgejo.org/models/repo" "forgejo.org/models/unit" - user_model "forgejo.org/models/user" "forgejo.org/modules/log" "forgejo.org/services/stats" @@ -277,7 +275,7 @@ func DoctorUserStarNum(ctx context.Context) (err error) { } // DeleteDeployKey delete deploy keys -func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error { +func DeleteDeployKey(ctx context.Context, id, repoID int64) error { key, err := asymkey_model.GetDeployKeyByID(ctx, id) if err != nil { if asymkey_model.IsErrDeployKeyNotExist(err) { @@ -286,21 +284,10 @@ func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error return fmt.Errorf("GetDeployKeyByID: %w", err) } - // Check if user has access to delete this key. - if !doer.IsAdmin { - repo, err := repo_model.GetRepositoryByID(ctx, key.RepoID) - if err != nil { - return fmt.Errorf("GetRepositoryByID: %w", err) - } - has, err := access_model.IsUserRepoAdmin(ctx, repo, doer) - if err != nil { - return fmt.Errorf("GetUserRepoPermission: %w", err) - } else if !has { - return asymkey_model.ErrKeyAccessDenied{ - UserID: doer.ID, - KeyID: key.ID, - Note: "deploy", - } + if key.RepoID != repoID { + return asymkey_model.ErrKeyAccessDenied{ + KeyID: key.ID, + Note: "deploy", } } diff --git a/models/repo/authz.go b/models/repo/authz.go new file mode 100644 index 0000000000..9196370583 --- /dev/null +++ b/models/repo/authz.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package repo + +import ( + "context" + + "forgejo.org/models/perm" + + "xorm.io/builder" +) + +// Defines an API for reducing available permissions to specific repositories. +type RepositoryAuthorizationReducer interface { + // Given a repository and an accessMode, ReduceRepoAccess will return a new, possibly reduced, AccessMode that + // reflects the actual access that is currently permitted. For example, when using a fine-grained access token that + // only grants write access to one target repository, `ReduceRepoAccess(target, AccessModeWrite)` would return + // `AccessModeWrite`, and `ReduceRepoAccess(other-repo, AccessModeWrite)` would return a lesser access mode, + // restricting access to other repositories. + ReduceRepoAccess(ctx context.Context, repo *Repository, accessMode perm.AccessMode) (perm.AccessMode, error) + + // If querying the repository table, apply the provided condition to query only repositories that the restriction + // will allow AccessModeRead (or higher). For example, when using a fine-grained access token that only grants + // write access to one target repository, `RepoReadAccessFilter()` will return a query condition that provides + // visibility for all the public repos (which have read access) and all the target private repos (which have write + // access, which is greater-than read access). + RepoReadAccessFilter() builder.Cond +} diff --git a/models/repo/mirror.go b/models/repo/mirror.go index 1fe9afd8e9..c64f9dc734 100644 --- a/models/repo/mirror.go +++ b/models/repo/mirror.go @@ -6,10 +6,14 @@ package repo import ( "context" + "errors" + "net/url" "time" "forgejo.org/models/db" + "forgejo.org/modules/keying" "forgejo.org/modules/log" + "forgejo.org/modules/optional" "forgejo.org/modules/timeutil" "forgejo.org/modules/util" ) @@ -31,7 +35,9 @@ type Mirror struct { LFS bool `xorm:"lfs_enabled NOT NULL DEFAULT false"` LFSEndpoint string `xorm:"lfs_endpoint TEXT"` - RemoteAddress string `xorm:"VARCHAR(2048)"` + // Encrypted remote address w/ credentials; can be NULL if a mirror has not performed a sync since this field was + // introduced, in which case the remote address exists only in the repo's configured git remote on disk. + EncryptedRemoteAddress []byte `xorm:"BLOB NULL"` } func init() { @@ -73,6 +79,71 @@ func (m *Mirror) ScheduleNextUpdate() { } } +// InsertMirror inserts a mirror to database. RemoteAddress must be provided so that it can be encrypted and stored +// during the insert process. +func (m *Mirror) InsertWithAddress(ctx context.Context, addr string) error { + return db.WithTx(ctx, func(ctx context.Context) error { + if _, err := db.GetEngine(ctx).Insert(m); err != nil { + return err + } + return m.UpdateRemoteAddress(ctx, addr) + }) +} + +// Stores a credential-free version of the address in `RemoteAddress`, encrypts the original into `RemoteAddressAuth`, +// and stores both in the database. The ID of the mirror must be known, so this must be done after the mirror is +// inserted. +func (m *Mirror) UpdateRemoteAddress(ctx context.Context, addr string) error { + if m.ID == 0 { + return errors.New("must persist mirror to database before using UpdateRemoteAddress") + } + + m.EncryptedRemoteAddress = keying.PullMirror.Encrypt( + []byte(addr), + keying.ColumnAndID("remote_address_auth", m.ID), + ) + _, err := db.GetEngine(ctx).ID(m.ID).Cols("encrypted_remote_address").Update(m) + return err +} + +// Retrieves the encrypted remote address and decrypts it. Note that this field is expected to be absent for mirrors +// created before the introduction of EncryptedRemoteAddress, in which case credentials are not known to Forgejo +// directly (but may be on-disk in the repository's config file) and None will be returned. +func (m *Mirror) DecryptRemoteAddress() (optional.Option[string], error) { + if m.EncryptedRemoteAddress == nil { + return optional.None[string](), nil + } + + contents, err := keying.PullMirror.Decrypt(m.EncryptedRemoteAddress, keying.ColumnAndID("remote_address_auth", m.ID)) + if err != nil { + return optional.None[string](), err + } + return optional.Some(string(contents)), nil +} + +// Retrieves the remote address but sanitizes it of sensitive credentials. May be absent for mirrors created before the +// introduction of EncryptedRemoteAddress. +func (m *Mirror) SanitizedRemoteAddress() (optional.Option[string], error) { + maybeAddr, err := m.DecryptRemoteAddress() + if err != nil { + return optional.None[string](), err + } else if has, addr := maybeAddr.Get(); has { + parsedURL, err := url.Parse(addr) + if err != nil { + return optional.None[string](), err + } + + // Remove the password if present. Retain the username for consistency with `AddAuthCredentialHelperForRemote` + // which retains the username for the `git clone` command line, which ends up as the remote URL in the mirror's + // git config. + if parsedURL.User != nil { + parsedURL.User = url.User(parsedURL.User.Username()) + } + return optional.Some(parsedURL.String()), nil + } + return optional.None[string](), nil +} + // GetMirrorByRepoID returns mirror information of a repository. func GetMirrorByRepoID(ctx context.Context, repoID int64) (*Mirror, error) { m := &Mirror{RepoID: repoID} @@ -115,9 +186,3 @@ func MirrorsIterate(ctx context.Context, limit int, f func(idx int, bean any) er } return sess.Iterate(new(Mirror), f) } - -// InsertMirror inserts a mirror to database -func InsertMirror(ctx context.Context, mirror *Mirror) error { - _, err := db.GetEngine(ctx).Insert(mirror) - return err -} diff --git a/models/repo/release.go b/models/repo/release.go index a421930d61..8aa447bda8 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -308,14 +308,14 @@ func (opts FindReleasesOptions) ToConds() builder.Cond { if len(opts.TagNames) > 0 { cond = cond.And(builder.In("tag_name", opts.TagNames)) } - if opts.IsPreRelease.Has() { - cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.Value()}) + if has, value := opts.IsPreRelease.Get(); has { + cond = cond.And(builder.Eq{"is_prerelease": value}) } - if opts.IsDraft.Has() { - cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.Value()}) + if has, value := opts.IsDraft.Get(); has { + cond = cond.And(builder.Eq{"is_draft": value}) } - if opts.HasSha1.Has() { - if opts.HasSha1.Value() { + if has, value := opts.HasSha1.Get(); has { + if value { cond = cond.And(builder.Neq{"sha1": ""}) } else { cond = cond.And(builder.Eq{"sha1": ""}) @@ -608,6 +608,7 @@ func InsertReleases(ctx context.Context, rels ...*Release) error { if len(rel.Attachments) > 0 { for i := range rel.Attachments { rel.Attachments[i].ReleaseID = rel.ID + rel.Attachments[i].RepoID = rel.RepoID } if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil { diff --git a/models/repo/release_test.go b/models/repo/release_test.go index 69f9333589..940de757c7 100644 --- a/models/repo/release_test.go +++ b/models/repo/release_test.go @@ -20,11 +20,14 @@ func TestMigrate_InsertReleases(t *testing.T) { UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12", } r := &Release{ + RepoID: 1001, Attachments: []*Attachment{a}, } err := InsertReleases(db.DefaultContext, r) require.NoError(t, err) + + assert.EqualValues(t, 1001, unittest.AssertExistsAndLoadBean(t, &Attachment{UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12"}).RepoID) } func TestReleaseLoadRepo(t *testing.T) { diff --git a/models/repo/repo.go b/models/repo/repo.go index a8432c0f90..1bd779b5a0 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -9,16 +9,19 @@ import ( "errors" "fmt" "html/template" + "maps" "net" "net/url" "path/filepath" "strconv" "strings" + auth_model "forgejo.org/models/auth" "forgejo.org/models/db" "forgejo.org/models/unit" user_model "forgejo.org/models/user" "forgejo.org/modules/cache" + "forgejo.org/modules/container" "forgejo.org/modules/git" "forgejo.org/modules/log" "forgejo.org/modules/markup" @@ -541,9 +544,7 @@ func (repo *Repository) ComposeMetas(ctx context.Context) map[string]string { func (repo *Repository) ComposeDocumentMetas(ctx context.Context) map[string]string { if len(repo.DocumentRenderingMetas) == 0 { metas := map[string]string{} - for k, v := range repo.ComposeMetas(ctx) { - metas[k] = v - } + maps.Copy(metas, repo.ComposeMetas(ctx)) metas["mode"] = "document" repo.DocumentRenderingMetas = metas } @@ -784,8 +785,8 @@ func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repo // getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url func getRepositoryURLPathSegments(repoURL string) []string { - if strings.HasPrefix(repoURL, setting.AppURL) { - return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/") + if after, ok := strings.CutPrefix(repoURL, setting.AppURL); ok { + return strings.Split(after, "/") } sshURLVariants := [4]string{ @@ -796,8 +797,8 @@ func getRepositoryURLPathSegments(repoURL string) []string { } for _, sshURL := range sshURLVariants { - if strings.HasPrefix(repoURL, sshURL) { - return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/") + if after, ok := strings.CutPrefix(repoURL, sshURL); ok { + return strings.Split(after, "/") } } @@ -892,11 +893,12 @@ type CountRepositoryOptions struct { func CountRepositories(ctx context.Context, opts CountRepositoryOptions) (int64, error) { sess := db.GetEngine(ctx).Where("id > 0") + // nosemgrep: forgejo-logic-suspicious-OwnerID-check (repositories cannot be owned by system users) if opts.OwnerID > 0 { sess.And("owner_id = ?", opts.OwnerID) } - if opts.Private.Has() { - sess.And("is_private=?", opts.Private.Value()) + if has, value := opts.Private.Get(); has { + sess.And("is_private=?", value) } count, err := sess.Count(new(Repository)) @@ -1001,3 +1003,55 @@ func UpdateRepoIssueNumbers(ctx context.Context, repoID int64, isPull, isClosed }) return nil } + +// Bulk load of all the repo_model.Repository objects for the repository resources that can be accessed by the given +// access tokens. Any access tokens which are not repository-specific tokens will not be present in the map. An +// optional filter function can be used to remove repositories (based upon a user visibility check, for example) before +// the map is constructed -- return `true` for repos to include. +func BulkGetRepositoriesForAccessTokens(ctx context.Context, tokens []*auth_model.AccessToken, filter func(*Repository) (bool, error)) (map[int64][]*Repository, error) { + // Load all the AccessTokenResourceRepo for the tokens that we're returning: + allRepoIDs := container.Set[int64]{} + repoResourcesByTokenID, err := auth_model.GetRepositoriesAccessibleWithTokens(ctx, tokens) + if err != nil { + return nil, fmt.Errorf("failed to fetch repositories for tokens: %w", err) + } + + // Load all the Repository models that are referenced by the AccessTokenResourceRepo's: + for _, repoResources := range repoResourcesByTokenID { + for _, repoResource := range repoResources { + allRepoIDs.Add(repoResource.RepoID) + } + } + reposByID, err := GetRepositoriesMapByIDs(ctx, allRepoIDs.Slice()) + if err != nil { + return nil, fmt.Errorf("failed to fetch repositories: %w", err) + } + + if filter != nil { + // Rebuild reposByID, filtering it with the provided filter function. It's more efficient to do this here, + // rather than returning the data and allowing the caller to filter it, because this guarantees one invocation + // per repository. `reposByTokenID` could have the same repository referenced by multiple access tokens. + tmp := reposByID + reposByID = make(map[int64]*Repository, len(tmp)) + for id, repo := range tmp { + if ok, err := filter(repo); err != nil { + return nil, fmt.Errorf("error filtering repo %d: %w", repo.ID, err) + } else if ok { + reposByID[id] = repo + } + } + } + + // Prepare a lookup map to access the repositories by token ID: + reposByTokenID := make(map[int64][]*Repository) + for tokenID, repoResources := range repoResourcesByTokenID { + for _, repoResource := range repoResources { + repo, ok := reposByID[repoResource.RepoID] + if ok { + reposByTokenID[tokenID] = append(reposByTokenID[tokenID], repo) + } + } + } + + return reposByTokenID, nil +} diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index ac7d2b69e3..a0eb0ce7c2 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -186,6 +186,11 @@ type SearchRepoOptions struct { // - Don't show forks, when opts.Fork is OptionalBoolNone. // - Do not display repositories that don't have a description, an icon and topics. OnlyShowRelevant bool + // Filters repositories based upon optional authorization restrictions. + AuthorizationReducer RepositoryAuthorizationReducer + // Retrieve multiple repositories by their owner name & repository name, similar to [GetRepositoryByOwnerAndName] + // but in bulk. + OwnerAndName [][2]string } // UserOwnedRepoCond returns user ownered repositories @@ -311,23 +316,6 @@ func userOrgPublicRepoCond(userID int64) builder.Cond { ) } -// userOrgPublicRepoCondPrivate returns the condition that one user could access all public repositories in private organizations -func userOrgPublicRepoCondPrivate(userID int64) builder.Cond { - return builder.And( - builder.Eq{"`repository`.is_private": false}, - builder.In("`repository`.owner_id", - builder.Select("`org_user`.org_id"). - From("org_user"). - Join("INNER", "`user`", "`user`.id = `org_user`.org_id"). - Where(builder.Eq{ - "`org_user`.uid": userID, - "`user`.`type`": user_model.UserTypeOrganization, - "`user`.visibility": structs.VisibleTypePrivate, - }), - ), - ) -} - // UserOrgPublicUnitRepoCond returns the condition that one user could access all public repositories in the special organization func UserOrgPublicUnitRepoCond(userID, orgID int64) builder.Cond { return userOrgPublicRepoCond(userID). @@ -354,12 +342,12 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { ))) } - if opts.IsPrivate.Has() { - cond = cond.And(builder.Eq{"is_private": opts.IsPrivate.Value()}) + if has, value := opts.IsPrivate.Get(); has { + cond = cond.And(builder.Eq{"is_private": value}) } - if opts.Template.Has() { - cond = cond.And(builder.Eq{"is_template": opts.Template.Value()}) + if has, value := opts.Template.Get(); has { + cond = cond.And(builder.Eq{"is_template": value}) } // Restrict to starred repositories @@ -373,33 +361,21 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { } // Restrict repositories to those the OwnerID owns or contributes to as per opts.Collaborate - if opts.OwnerID > 0 { + if opts.OwnerID != 0 { accessCond := builder.NewCond() - if !opts.Collaborate.Value() { + if !opts.Collaborate.ValueOrZeroValue() { accessCond = builder.Eq{"owner_id": opts.OwnerID} } if opts.Collaborate.ValueOrDefault(true) { - // A Collaboration is: - collaborateCond := builder.NewCond() + + // A Collaboration is: // 1. Repository we don't own collaborateCond = collaborateCond.And(builder.Neq{"owner_id": opts.OwnerID}) - // 2. But we can see because of: - { - userAccessCond := builder.NewCond() - // A. We have unit independent access - userAccessCond = userAccessCond.Or(UserAccessRepoCond("`repository`.id", opts.OwnerID)) - // B. We are in a team for - if opts.UnitType == unit.TypeInvalid { - userAccessCond = userAccessCond.Or(UserOrgTeamRepoCond("`repository`.id", opts.OwnerID)) - } else { - userAccessCond = userAccessCond.Or(userOrgTeamUnitRepoCond("`repository`.id", opts.OwnerID, opts.UnitType)) - } - // C. Public repositories in organizations that we are member of - userAccessCond = userAccessCond.Or(userOrgPublicRepoCondPrivate(opts.OwnerID)) - collaborateCond = collaborateCond.And(userAccessCond) - } + // 2. But we can have access to unit on the repo > AccessModeNone + collaborateCond = collaborateCond.And(UserAccessRepoCond("`repository`.id", opts.OwnerID)) + if !opts.Private { collaborateCond = collaborateCond.And(builder.Expr("owner_id NOT IN (SELECT org_id FROM org_user WHERE org_user.uid = ? AND org_user.is_public = ?)", opts.OwnerID, false)) } @@ -425,7 +401,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { if opts.Keyword != "" { // separate keyword subQueryCond := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { + for v := range strings.SplitSeq(opts.Keyword, ",") { if opts.TopicOnly { subQueryCond = subQueryCond.Or(builder.Eq{"topic.name": strings.ToLower(v)}) } else { @@ -440,7 +416,7 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { keywordCond := builder.In("id", subQuery) if !opts.TopicOnly { likes := builder.NewCond() - for _, v := range strings.Split(opts.Keyword, ",") { + for v := range strings.SplitSeq(opts.Keyword, ",") { likes = likes.Or(builder.Like{"lower_name", strings.ToLower(v)}) // If the string looks like "org/repo", match against that pattern too @@ -467,28 +443,28 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { Where(builder.Eq{"language": opts.Language}).And(builder.Eq{"is_primary": true}))) } - if opts.Fork.Has() || opts.OnlyShowRelevant { - if opts.OnlyShowRelevant && !opts.Fork.Has() { + if has, value := opts.Fork.Get(); has || opts.OnlyShowRelevant { + if opts.OnlyShowRelevant && !has { cond = cond.And(builder.Eq{"is_fork": false}) } else { - cond = cond.And(builder.Eq{"is_fork": opts.Fork.Value()}) + cond = cond.And(builder.Eq{"is_fork": value}) } } - if opts.Mirror.Has() { - cond = cond.And(builder.Eq{"is_mirror": opts.Mirror.Value()}) + if has, value := opts.Mirror.Get(); has { + cond = cond.And(builder.Eq{"is_mirror": value}) } if opts.Actor != nil && opts.Actor.IsRestricted { cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid)) } - if opts.Archived.Has() { - cond = cond.And(builder.Eq{"is_archived": opts.Archived.Value()}) + if has, value := opts.Archived.Get(); has { + cond = cond.And(builder.Eq{"is_archived": value}) } - if opts.HasMilestones.Has() { - if opts.HasMilestones.Value() { + if has, value := opts.HasMilestones.Get(); has { + if value { cond = cond.And(builder.Gt{"num_milestones": 0}) } else { cond = cond.And(builder.Eq{"num_milestones": 0}.Or(builder.IsNull{"num_milestones"})) @@ -518,6 +494,30 @@ func SearchRepositoryCondition(opts *SearchRepoOptions) builder.Cond { cond = cond.And(subQueryCond) } + if opts.AuthorizationReducer != nil { + cond = cond.And(opts.AuthorizationReducer.RepoReadAccessFilter()) + } + + if opts.OwnerAndName != nil { + if len(opts.OwnerAndName) > 0 { + // repository is indexed on `(owner_id, lower_name)`, but not on the `owner_name` field. Plus the `owner_name` + // field isn't ToLower'd. So this becomes a subquery: + subQuery := builder.Select("inner_repo.id").From("repository", "inner_repo"). + Join("INNER", "`user`", "`user`.id = inner_repo.owner_id") + for _, ownerAndName := range opts.OwnerAndName { + subQuery.Or(builder.Eq{ + "`user`.lower_name": strings.ToLower(ownerAndName[0]), + "inner_repo.lower_name": strings.ToLower(ownerAndName[1]), + }) + } + cond = cond.And(builder.In("id", subQuery)) + } else { + // If opts.OwnerAndName is a non-nil, empty array, then we want to return zero repositories. The loop to + // build the `Eq` conditions wouldn't occur, so we would have no filtering if this wasn't special-case'd. + cond = cond.And(builder.Eq{"1": "2"}) + } + } + return cond } diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index 085f660f15..fc32226975 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -179,6 +179,26 @@ func getTestCases() []struct { opts: &repo_model.SearchRepoOptions{Keyword: "user20/", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, Private: true, OwnerID: 0}, count: 4, }, + { + name: "OwnerAndName Single", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerAndName: [][2]string{{"user15", "big_test_public_1"}}}, + count: 1, + }, + { + name: "OwnerAndName Multiple", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerAndName: [][2]string{{"user15", "big_test_public_1"}, {"user15", "big_test_public_2"}}}, + count: 2, + }, + { + name: "OwnerAndName Miss", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerAndName: [][2]string{{"user15", "big_test_public_1"}, {"user15", "blah blah"}}}, + count: 1, + }, + { + name: "OwnerAndName Empty", + opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerAndName: [][2]string{}}, + count: 0, + }, } return testCases @@ -331,21 +351,21 @@ func TestSearchRepository(t *testing.T) { assert.False(t, repo.IsPrivate) } - if testCase.opts.Fork.Value() && testCase.opts.Mirror.Value() { + if testCase.opts.Fork.ValueOrZeroValue() && testCase.opts.Mirror.ValueOrZeroValue() { assert.True(t, repo.IsFork && repo.IsMirror) } else { - if testCase.opts.Fork.Has() { - assert.Equal(t, testCase.opts.Fork.Value(), repo.IsFork) + if has, value := testCase.opts.Fork.Get(); has { + assert.Equal(t, value, repo.IsFork) } - if testCase.opts.Mirror.Has() { - assert.Equal(t, testCase.opts.Mirror.Value(), repo.IsMirror) + if has, value := testCase.opts.Mirror.Get(); has { + assert.Equal(t, value, repo.IsMirror) } } if testCase.opts.OwnerID > 0 && !testCase.opts.AllPublic { - if testCase.opts.Collaborate.Has() { - if testCase.opts.Collaborate.Value() { + if has, value := testCase.opts.Collaborate.Get(); has { + if value { assert.NotEqual(t, testCase.opts.OwnerID, repo.Owner.ID) } else { assert.Equal(t, testCase.opts.OwnerID, repo.Owner.ID) diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go index aa6f2fa0ae..2cde375775 100644 --- a/models/repo/repo_unit.go +++ b/models/repo/repo_unit.go @@ -220,8 +220,42 @@ func (cfg *PullRequestsConfig) GetDefaultUpdateStyle() UpdateStyle { return UpdateStyleMerge } +// Represents the current format for `sub` claim generation when using `enable-openid-connect: true` on an Action. +// +// We try to follow GitHub's format for the `sub` claim because it should allow third-party integrations, which may have +// existing knowledge and code that works with GitHub's OIDC tokens, to reuse that knowledge and code in the future to +// implement Forgejo support. As GitHub's format changes, we may implement those format changes to maintain that +// familarity. Forgejo isn't required to do so, though -- if future changes from GitHub don't make sense for Forgejo, +// or vice-versa, this format matching effort may be discarded. +type OIDCSubjectFormat string + +var ( + // Default is the current, most preferred method of generating an `sub` JWT claim for an Actions JWT token. It is + // an empty string which allows [ActionsConfig] to always default to the current preferred default value for new + // repositories. At present, it is a `sub` claim that contains information about the repository owner and + // repository where the action occurred, their immutable identifiers (ID numbers), and the event that triggered the + // Action. + // + // Immutable identifiers have been added since OIDCSubjectFormatLegacyForgejo15. The intent of adding them is to + // protect resource servers which may be requiring a specific subject claim from having that claim be impersonated + // when a user or repository are renamed or deleted. For example, if a JWT from my-org/my-repo is trusted, but then + // my-org is deleted and a new user takes ownership of the name my-org, they should not be granted the same trust. + // + // Example: repo:my-org-123456/my-repo-456789:ref:refs/heads/main + OIDCSubjectFormatDefault OIDCSubjectFormat // defaults to "" + + // The `sub` JWT claim generation that was shipped in Forgejo 15. Contains information about the repository owner + // and repository where the action occurred and the event that triggered the Action. + // + // Example: repo:my-org/my-repo:ref:refs/heads/main + OIDCSubjectFormatLegacyForgejo15 OIDCSubjectFormat = "legacy-forgejo-v15" +) + type ActionsConfig struct { DisabledWorkflows []string + + // Format of the OIDC 'sub' claim that will be used when `enable-openid-connect` is true in an Action. + OIDCSubjectFormat OIDCSubjectFormat `json:",omitempty"` } func (cfg *ActionsConfig) EnableWorkflow(file string) { @@ -237,10 +271,8 @@ func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { } func (cfg *ActionsConfig) DisableWorkflow(file string) { - for _, workflow := range cfg.DisabledWorkflows { - if file == workflow { - return - } + if slices.Contains(cfg.DisabledWorkflows, file) { + return } cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) diff --git a/models/repo/upload.go b/models/repo/upload.go index a213cb1986..67b5409650 100644 --- a/models/repo/upload.go +++ b/models/repo/upload.go @@ -117,7 +117,7 @@ func DeleteUploads(ctx context.Context, uploads ...*Upload) (err error) { defer committer.Close() ids := make([]int64, len(uploads)) - for i := 0; i < len(uploads); i++ { + for i := range uploads { ids[i] = uploads[i].ID } if err = db.DeleteByIDs[Upload](ctx, ids...); err != nil { diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go index ca02c1e3f0..e46e5e1d5a 100644 --- a/models/repo/user_repo.go +++ b/models/repo/user_repo.go @@ -17,13 +17,14 @@ import ( ) // GetStarredRepos returns the repos starred by a particular user -func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, error) { +func GetStarredRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions, reducer RepositoryAuthorizationReducer) ([]*Repository, error) { sess := db.GetEngine(ctx). Where("star.uid=?", userID). Join("LEFT", "star", "`repository`.id=`star`.repo_id") if !private { sess = sess.And("is_private=?", false) } + sess = sess.And(reducer.RepoReadAccessFilter()) if listOptions.Page != 0 { sess = db.SetSessionPagination(sess, &listOptions) @@ -37,7 +38,7 @@ func GetStarredRepos(ctx context.Context, userID int64, private bool, listOption } // GetWatchedRepos returns the repos watched by a particular user -func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions) ([]*Repository, int64, error) { +func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOptions db.ListOptions, reducer RepositoryAuthorizationReducer) ([]*Repository, int64, error) { sess := db.GetEngine(ctx). Where("watch.user_id=?", userID). And("`watch`.mode<>?", WatchModeDont). @@ -45,6 +46,7 @@ func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOption if !private { sess = sess.And("is_private=?", false) } + sess = sess.And(reducer.RepoReadAccessFilter()) if listOptions.Page != 0 { sess = db.SetSessionPagination(sess, &listOptions) diff --git a/models/repo/watch.go b/models/repo/watch.go index e5d4b2f90e..3a20880a5c 100644 --- a/models/repo/watch.go +++ b/models/repo/watch.go @@ -17,12 +17,20 @@ type WatchMode int8 const ( // WatchModeNone don't watch + // This means there is no Watch record in the db. + // We never store this mode in the db and instead remove the record from the db. + // Furthermore, this means there is a WatchMode for all combinations of user and repo. WatchModeNone WatchMode = iota // 0 // WatchModeNormal watch repository (from other sources) + // This means the user explicitly chose to watch the repo. WatchModeNormal // 1 // WatchModeDont explicit don't auto-watch + // This means the user explicitly removed themselves as a watcher. + // Then the AutoWatchOnChanges feature doesn't make the user a watcher when they push to the repo. WatchModeDont // 2 // WatchModeAuto watch repository (from AutoWatchOnChanges) + // This is used when the user pushed to the repo and setting.Service.AutoWatchOnChanges is true. + // That way we can differentiate people explicitly watching the repo and people only watching it because of the AutoWatchOnChanges feature. WatchModeAuto // 3 ) @@ -74,6 +82,7 @@ func watchRepoMode(ctx context.Context, watch Watch, mode WatchMode) (err error) } hadrec := watch.Mode != WatchModeNone + // WatchModeNone means there is no record in the db. needsrec := mode != WatchModeNone repodiff := 0 @@ -169,8 +178,8 @@ func GetRepoWatchers(ctx context.Context, repoID int64, opts db.ListOptions) ([] } // WatchIfAuto subscribes to repo if AutoWatchOnChanges is set -func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error { - if !isWrite || !setting.Service.AutoWatchOnChanges { +func WatchIfAuto(ctx context.Context, userID, repoID int64) error { + if !setting.Service.AutoWatchOnChanges { return nil } watch, err := GetWatch(ctx, userID, repoID) diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go index 698f6a5f49..ccc56ad168 100644 --- a/models/repo/watch_test.go +++ b/models/repo/watch_test.go @@ -74,13 +74,13 @@ func TestWatchIfAuto(t *testing.T) { prevCount := repo.NumWatches // Must not add watch - require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 8, 1, true)) + require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 8, 1)) watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) require.NoError(t, err) assert.Len(t, watchers, prevCount) // Should not add watch - require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 10, 1, true)) + require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 10, 1)) watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) require.NoError(t, err) assert.Len(t, watchers, prevCount) @@ -88,19 +88,19 @@ func TestWatchIfAuto(t *testing.T) { setting.Service.AutoWatchOnChanges = true // Must not add watch - require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 8, 1, true)) + require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 8, 1)) watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) require.NoError(t, err) assert.Len(t, watchers, prevCount) // Should not add watch - require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1, false)) + // We simply don't WatchIfAuto watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) require.NoError(t, err) assert.Len(t, watchers, prevCount) // Should add watch - require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1, true)) + require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1)) watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) require.NoError(t, err) assert.Len(t, watchers, prevCount+1) @@ -112,7 +112,7 @@ func TestWatchIfAuto(t *testing.T) { assert.Len(t, watchers, prevCount) // Must not add watch - require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1, true)) + require.NoError(t, repo_model.WatchIfAuto(db.DefaultContext, 12, 1)) watchers, err = repo_model.GetRepoWatchers(db.DefaultContext, repo.ID, db.ListOptions{Page: 1}) require.NoError(t, err) assert.Len(t, watchers, prevCount) diff --git a/models/repo_transfer.go b/models/repo_transfer.go index f515f1bcf0..8031a20964 100644 --- a/models/repo_transfer.go +++ b/models/repo_transfer.go @@ -175,11 +175,11 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m } // GetPendingTransfers returns the pending transfers of recipient which were sent by by doer. -func GetPendingTransferIDs(ctx context.Context, reciepientID, doerID int64) ([]int64, error) { +func GetPendingTransferIDs(ctx context.Context, recipientID, doerID int64) ([]int64, error) { pendingTransferIDs := make([]int64, 0, 8) return pendingTransferIDs, db.GetEngine(ctx).Table("repo_transfer"). Where("doer_id = ?", doerID). - And("recipient_id = ?", reciepientID). + And("recipient_id = ?", recipientID). Cols("id"). Find(&pendingTransferIDs) } diff --git a/models/repo_transfer_test.go b/models/repo_transfer_test.go index 7f01ac2b97..58fc63a263 100644 --- a/models/repo_transfer_test.go +++ b/models/repo_transfer_test.go @@ -17,10 +17,10 @@ import ( func TestGetPendingTransferIDs(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) - reciepient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - pendingTransfer := unittest.AssertExistsAndLoadBean(t, &RepoTransfer{RecipientID: reciepient.ID, DoerID: doer.ID}) + recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + pendingTransfer := unittest.AssertExistsAndLoadBean(t, &RepoTransfer{RecipientID: recipient.ID, DoerID: doer.ID}) - pendingTransferIDs, err := GetPendingTransferIDs(db.DefaultContext, reciepient.ID, doer.ID) + pendingTransferIDs, err := GetPendingTransferIDs(db.DefaultContext, recipient.ID, doer.ID) require.NoError(t, err) if assert.Len(t, pendingTransferIDs, 1) { assert.Equal(t, pendingTransfer.ID, pendingTransferIDs[0]) diff --git a/models/secret/TestSecretGetSecretByID/secret.yml b/models/secret/TestSecretGetSecretByID/secret.yml new file mode 100644 index 0000000000..08e3e2c561 --- /dev/null +++ b/models/secret/TestSecretGetSecretByID/secret.yml @@ -0,0 +1,23 @@ +- id: 637340 + name: TEST_SECRET + owner_id: 3 + repo_id: 0 + # very secret + data: 0x0e17d357077de0c1f72e9d87e1899c8026a207fcbbc0f1a2a79d0cb305da39fa3e9fe201b085e963fa1f9eeb59b4ff9ee6d748 + created_unix: 1773692671 + +- id: 637341 + name: ANOTHER_SECRET + owner_id: 0 + repo_id: 62 # user2/test_workflows + # also very secret + data: 0xb697cd4a66b02bc36d0afddcb6f158d7602f6cf0b0d31a0c96e178469e8b66babccb30b95e01c84824add04c5bfe91b9b773a21a78088a3e + created_unix: 1773692672 + +- id: 637342 + name: TEST_SECRET + owner_id: 1 + repo_id: 0 + # super secret + data: 0xd97d8cb662c07ca94953a388bc93209f713281a8b0d25499359cbd03a1fbe565a2a0ba183b8290fc110ee6b1c6437c569451e0c3 + created_unix: 1773692673 diff --git a/models/secret/secret.go b/models/secret/secret.go index 0a5db17690..758f346f22 100644 --- a/models/secret/secret.go +++ b/models/secret/secret.go @@ -6,11 +6,10 @@ package secret import ( "context" "fmt" + "regexp" "strings" - actions_model "forgejo.org/models/actions" "forgejo.org/models/db" - actions_module "forgejo.org/modules/actions" "forgejo.org/modules/keying" "forgejo.org/modules/log" "forgejo.org/modules/timeutil" @@ -19,6 +18,13 @@ import ( "xorm.io/builder" ) +var ( + namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$") + forbiddenPrefixPattern = regexp.MustCompile("(?i)^(FORGEJO_|GITEA_|GITHUB_|[0-9])") + + ErrInvalidName = util.NewInvalidArgumentErrorf("invalid secret name") +) + // Secret represents a secret // // It can be: @@ -65,6 +71,9 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat if ownerID == 0 && repoID == 0 { return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument) } + if err := ValidateName(name); err != nil { + return nil, err + } secret := &Secret{ OwnerID: ownerID, @@ -77,7 +86,7 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat return err } - secret.SetSecret(data) + secret.SetData(data) _, err := db.GetEngine(ctx).ID(secret.ID).Cols("data").Update(secret) return err }) @@ -116,44 +125,90 @@ func (opts FindSecretsOptions) ToConds() builder.Cond { return cond } -func (s *Secret) SetSecret(data string) { - s.Data = keying.ActionSecret.Encrypt([]byte(data), keying.ColumnAndID("data", s.ID)) +func (s *Secret) SetData(data string) { + normalizedData := util.ReserveLineBreakForTextarea(data) + s.Data = keying.ActionSecret.Encrypt([]byte(normalizedData), keying.ColumnAndID("data", s.ID)) } -func GetSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) (map[string]string, error) { +func (s *Secret) GetDecryptedData() (string, error) { + key := keying.ActionSecret + v, err := key.Decrypt(s.Data, keying.ColumnAndID("data", s.ID)) + if err != nil { + return "", fmt.Errorf("unable to decrypt secret[id=%d,name=%q]: %w", s.ID, s.Name, err) + } + + return string(v), nil +} + +func GetSecretByID(ctx context.Context, ownerID, repoID, id int64) (*Secret, error) { + query := db.GetEngine(ctx).Where("id=?", id) + + if repoID > 0 { + query = query.And(builder.Eq{"repo_id": repoID}) + } else if ownerID > 0 { + query = query.And(builder.Eq{"owner_id": ownerID}) + } else { + return nil, fmt.Errorf("ownerID and repoID cannot be simultaneously 0") + } + + var secret Secret + has, err := query.Get(&secret) + + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("secret with ID %d: %w", id, util.ErrNotExist) + } + return &secret, nil +} + +func UpdateSecret(ctx context.Context, secret *Secret, columns ...string) error { + e := db.GetEngine(ctx) + + if err := ValidateName(secret.Name); err != nil { + return err + } + secret.Name = strings.ToUpper(secret.Name) + + var err error + if len(columns) == 0 { + _, err = e.ID(secret.ID).AllCols().Update(secret) + } else { + _, err = e.ID(secret.ID).Cols(columns...).Update(secret) + } + + return err +} + +func FetchActionSecrets(ctx context.Context, ownerID, repoID int64) (map[string]string, error) { secrets := map[string]string{} - secrets["GITHUB_TOKEN"] = task.Token - secrets["GITEA_TOKEN"] = task.Token - secrets["FORGEJO_TOKEN"] = task.Token - - if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget { - // ignore secrets for fork pull request, except GITHUB_TOKEN, GITEA_TOKEN and FORGEJO_TOKEN which are automatically generated. - // for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch - // see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target - return secrets, nil - } - - ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID}) + ownerSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{OwnerID: ownerID}) if err != nil { - log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err) + log.Error("find secrets of owner %v: %v", ownerID, err) return nil, err } - repoSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{RepoID: task.Job.Run.RepoID}) + repoSecrets, err := db.Find[Secret](ctx, FindSecretsOptions{RepoID: repoID}) if err != nil { - log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err) + log.Error("find secrets of repo %v: %v", repoID, err) return nil, err } - key := keying.ActionSecret for _, secret := range append(ownerSecrets, repoSecrets...) { - v, err := key.Decrypt(secret.Data, keying.ColumnAndID("data", secret.ID)) + decryptedData, err := secret.GetDecryptedData() if err != nil { - log.Error("unable to decrypt secret[id=%d,name=%q]: %v", secret.ID, secret.Name, err) + log.Error("%v", err) return nil, err } - secrets[secret.Name] = string(v) + secrets[secret.Name] = decryptedData } return secrets, nil } + +func ValidateName(name string) error { + if !namePattern.MatchString(name) || forbiddenPrefixPattern.MatchString(name) { + return ErrInvalidName + } + return nil +} diff --git a/models/secret/secret_test.go b/models/secret/secret_test.go index 76b673f2fe..e323986084 100644 --- a/models/secret/secret_test.go +++ b/models/secret/secret_test.go @@ -4,10 +4,9 @@ package secret import ( + "strings" "testing" - "forgejo.org/models/actions" - "forgejo.org/models/repo" "forgejo.org/models/unittest" "forgejo.org/modules/keying" "forgejo.org/modules/util" @@ -85,19 +84,220 @@ func TestInsertEncryptedSecret(t *testing.T) { }) }) - t.Run("Get secrets", func(t *testing.T) { - secrets, err := GetSecretsOfTask(t.Context(), &actions.ActionTask{ - Job: &actions.ActionRunJob{ - Run: &actions.ActionRun{ - RepoID: 1, - Repo: &repo.Repository{ - OwnerID: 2, - }, - }, - }, - }) + t.Run("Rejects invalid name", func(t *testing.T) { + _, err := InsertEncryptedSecret(t.Context(), 2, 0, "invalid name", "some secret") + require.ErrorContains(t, err, "invalid secret name") + }) + + t.Run("FetchActionSecrets", func(t *testing.T) { + secrets, err := FetchActionSecrets(t.Context(), 2, 1) require.NoError(t, err) assert.Equal(t, "some owner secret", secrets["OWNER_SECRET"]) assert.Equal(t, "some repository secret", secrets["REPO_SECRET"]) }) } + +func TestSecretDataIsNormalized(t *testing.T) { + secret := Secret{ID: 494, OwnerID: 829, RepoID: 0, Name: "A_SECRET"} + + secret.SetData(" \r\ndatà\t ") + + decryptedData, err := secret.GetDecryptedData() + require.NoError(t, err) + assert.Equal(t, " \ndatà\t ", decryptedData) +} + +func TestSecretGetDecryptedData(t *testing.T) { + t.Run("Recovers original data", func(t *testing.T) { + secret := Secret{ID: 494, OwnerID: 829, RepoID: 0, Name: "A_SECRET"} + secret.SetData("data") + + decryptedData, err := secret.GetDecryptedData() + require.NoError(t, err) + assert.Equal(t, "data", decryptedData) + }) + + t.Run("Returns error if data cannot be decrypted", func(t *testing.T) { + secret := Secret{ID: 494, OwnerID: 829, RepoID: 0, Name: "A_SECRET"} + secret.SetData("data") + + // Changing the ID without updating the secret makes the secret irrecoverable. + secret.ID++ + + decryptedData, err := secret.GetDecryptedData() + assert.Empty(t, decryptedData) + assert.ErrorContains(t, err, "unable to decrypt secret[id=495,name=\"A_SECRET\"]") + }) +} + +func TestSecretGetSecretByID(t *testing.T) { + defer unittest.OverrideFixtures("models/secret/TestSecretGetSecretByID")() + require.NoError(t, unittest.PrepareTestDatabase()) + + testCases := []struct { + name string + ownerID int64 + repoID int64 + id int64 + expectedName string + expectedData string + expectedError string + }{ + { + name: "Organization secret", + ownerID: 3, + repoID: 0, + id: 637340, + expectedName: "TEST_SECRET", + expectedData: "very secret", + }, + { + name: "Owner mismatch", + ownerID: 4, + repoID: 0, + id: 637340, + expectedError: "secret with ID 637340: resource does not exist", + }, + { + name: "Repository mismatch", + ownerID: 0, + repoID: 1, + id: 637340, + expectedError: "secret with ID 637340: resource does not exist", + }, + { + name: "Repository secret", + ownerID: 0, + repoID: 62, + id: 637341, + expectedName: "ANOTHER_SECRET", + expectedData: "also very secret", + }, + { + name: "Unsupported instance secret", + ownerID: 0, + repoID: 0, + id: 637341, + expectedError: "ownerID and repoID cannot be simultaneously 0", + }, + { + name: "User secret", + ownerID: 1, + repoID: 0, + id: 637342, + expectedName: "TEST_SECRET", + expectedData: "super secret", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + secret, err := GetSecretByID(t.Context(), testCase.ownerID, testCase.repoID, testCase.id) + + if testCase.expectedError != "" { + assert.ErrorContains(t, err, testCase.expectedError) + } else { + require.NoError(t, err) + assert.Equal(t, testCase.id, secret.ID) + assert.Equal(t, testCase.ownerID, secret.OwnerID) + assert.Equal(t, testCase.repoID, secret.RepoID) + assert.Equal(t, testCase.expectedName, secret.Name) + + data, err := secret.GetDecryptedData() + require.NoError(t, err) + assert.Equal(t, testCase.expectedData, data) + } + }) + } +} + +func TestSecretUpdateSecret(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + secret, err := InsertEncryptedSecret(t.Context(), 2, 0, "a_secret", "very secret") + require.NoError(t, err) + + secret.Name = "new_name" + secret.SetData("also very secret") + + err = UpdateSecret(t.Context(), secret) + require.NoError(t, err) + + updatedSecret := unittest.AssertExistsAndLoadBean(t, &Secret{ID: secret.ID}) + decryptedData, err := updatedSecret.GetDecryptedData() + require.NoError(t, err) + + assert.Equal(t, "NEW_NAME", updatedSecret.Name) + assert.Equal(t, "also very secret", decryptedData) +} + +func TestSecretUpdateSecret_RejectsInvalidName(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + secret, err := InsertEncryptedSecret(t.Context(), 2, 0, "a_secret", "very secret") + require.NoError(t, err) + + secret.Name = "GITHUB_IS_REJECTED" // Because it starts with `GITHUB_`. + secret.SetData("also very secret") + + err = UpdateSecret(t.Context(), secret) + require.ErrorContains(t, err, "invalid secret name") + + updatedSecret := unittest.AssertExistsAndLoadBean(t, &Secret{ID: secret.ID}) + decryptedData, err := updatedSecret.GetDecryptedData() + require.NoError(t, err) + + assert.Equal(t, "A_SECRET", updatedSecret.Name) + assert.Equal(t, "very secret", decryptedData) +} + +func TestSecretValidateName(t *testing.T) { + testCases := []struct { + name string + valid bool + }{ + {"FORGEJO_", false}, + {"PRE_FORGEJO_", true}, + {"PRE_FORGEJO_SUF", true}, + {"FORGEJO_123", false}, + {"FORGEJO_ABC", false}, + {"GITEA_", false}, + {"PRE_GITEA_", true}, + {"PRE_GITEA_SUF", true}, + {"GITEA_123", false}, + {"GITEA_ABC", false}, + {"GITHUB_", false}, + {"PRE_GITHUB_", true}, + {"PRE_GITHUB_SUF", true}, + {"GITHUB_123", false}, + {"GITHUB_ABC", false}, + {"123_TEST", false}, + {"CI", true}, + {"_CI", true}, + {"CI_", true}, + {"CI123", true}, + {"CIABC", true}, + {"FORGEJO", true}, + {"FORGEJO123", true}, + {"FORGEJOABC", true}, + {"GITEA", true}, + {"GITEA123", true}, + {"GITEAABC", true}, + {"GITHUB", true}, + {"GITHUB123", true}, + {"GITHUBABC", true}, + {"_123_TEST", true}, + } + for _, tC := range testCases { + t.Run(tC.name, func(t *testing.T) { + t.Helper() + if tC.valid { + assert.NoError(t, ValidateName(tC.name)) + assert.NoError(t, ValidateName(strings.ToLower(tC.name))) + } else { + require.ErrorIs(t, ValidateName(tC.name), ErrInvalidName) + require.ErrorIs(t, ValidateName(strings.ToLower(tC.name)), ErrInvalidName) + } + }) + } +} diff --git a/models/system/notice.go b/models/system/notice.go index b1fdd2e4f2..7801686889 100644 --- a/models/system/notice.go +++ b/models/system/notice.go @@ -38,6 +38,8 @@ func init() { } // TrStr returns a translation format string. +// +//llu:returnsTrKey func (n *Notice) TrStr() string { return fmt.Sprintf("admin.notices.type_%d", n.Type) } diff --git a/models/unit/tests/units.go b/models/unit/tests/units.go new file mode 100644 index 0000000000..7507b80e2a --- /dev/null +++ b/models/unit/tests/units.go @@ -0,0 +1,35 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package tests + +import ( + unit_model "forgejo.org/models/unit" + "forgejo.org/modules/setting" + "forgejo.org/modules/test" +) + +func SaveUnits() func() { + disabledGlobal := unit_model.DisabledRepoUnitsGet() + restoreDisabledGlobal := func() { + unit_model.DisabledRepoUnitsSet(disabledGlobal) + } + restoreDisabledRepo := test.MockProtect(&setting.Repository.DisabledRepoUnits) + + restoreDefaultGlobal := test.MockProtect(&unit_model.DefaultRepoUnits) + restoreDefaultRepo := test.MockProtect(&setting.Repository.DefaultRepoUnits) + + restoreForkGlobal := test.MockProtect(&unit_model.DefaultForkRepoUnits) + restoreForkRepo := test.MockProtect(&setting.Repository.DefaultForkRepoUnits) + + return func() { + restoreDisabledGlobal() + restoreDisabledRepo() + + restoreDefaultGlobal() + restoreDefaultRepo() + + restoreForkGlobal() + restoreForkRepo() + } +} diff --git a/models/unit/tests/units_test.go b/models/unit/tests/units_test.go new file mode 100644 index 0000000000..acf543285f --- /dev/null +++ b/models/unit/tests/units_test.go @@ -0,0 +1,37 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package tests + +import ( + "testing" + + unit_model "forgejo.org/models/unit" + "forgejo.org/modules/setting" + + "github.com/stretchr/testify/assert" +) + +func TestSaveUnits(t *testing.T) { + restoreUnits := SaveUnits() + + unit_model.DisabledRepoUnitsSet([]unit_model.Type{unit_model.TypeInvalid}) + setting.Repository.DisabledRepoUnits = []string{"invalid"} + + unit_model.DefaultRepoUnits = []unit_model.Type{unit_model.TypeInvalid} + setting.Repository.DefaultRepoUnits = []string{"invalid"} + + unit_model.DefaultForkRepoUnits = []unit_model.Type{unit_model.TypeInvalid} + setting.Repository.DefaultForkRepoUnits = []string{"invalid"} + + restoreUnits() + + assert.NotEqual(t, []unit_model.Type{unit_model.TypeInvalid}, unit_model.DisabledRepoUnitsGet()) + assert.NotEqual(t, []string{"invalid"}, setting.Repository.DisabledRepoUnits) + + assert.NotEqual(t, []unit_model.Type{unit_model.TypeInvalid}, unit_model.DefaultRepoUnits) + assert.NotEqual(t, []string{"invalid"}, setting.Repository.DefaultRepoUnits) + + assert.NotEqual(t, []unit_model.Type{unit_model.TypeInvalid}, unit_model.DefaultForkRepoUnits) + assert.NotEqual(t, []string{"invalid"}, setting.Repository.DefaultForkRepoUnits) +} diff --git a/models/unit/unit.go b/models/unit/unit.go index 6b4f2765ee..2a31c804aa 100644 --- a/models/unit/unit.go +++ b/models/unit/unit.go @@ -7,6 +7,7 @@ package unit import ( "errors" "fmt" + "slices" "strings" "sync/atomic" @@ -141,7 +142,7 @@ func DisabledRepoUnitsSet(v []Type) { // Get valid set of default repository units from settings func validateDefaultRepoUnits(defaultUnits, settingDefaultUnits []Type) []Type { - units := defaultUnits + units := slices.Clone(defaultUnits) // Use setting if not empty if len(settingDefaultUnits) > 0 { @@ -247,22 +248,12 @@ func LoadUnitConfig() error { // UnitGlobalDisabled checks if unit type is global disabled func (u Type) UnitGlobalDisabled() bool { - for _, ud := range DisabledRepoUnitsGet() { - if u == ud { - return true - } - } - return false + return slices.Contains(DisabledRepoUnitsGet(), u) } // CanBeDefault checks if the unit type can be a default repo unit func (u *Type) CanBeDefault() bool { - for _, nadU := range NotAllowedDefaultRepoUnits { - if *u == nadU { - return false - } - } - return true + return !slices.Contains(NotAllowedDefaultRepoUnits, *u) } // Unit is a section of one repository @@ -431,7 +422,7 @@ func AllUnitKeyNames() []string { return res } -// MinUnitAccessMode returns the minial permission of the permission map +// MinUnitAccessMode returns the minimal permission of the permission map func MinUnitAccessMode(unitsMap map[Type]perm.AccessMode) perm.AccessMode { res := perm.AccessModeNone for t, mode := range unitsMap { @@ -440,7 +431,7 @@ func MinUnitAccessMode(unitsMap map[Type]perm.AccessMode) perm.AccessMode { continue } - // get the minial permission great than AccessModeNone except all are AccessModeNone + // get the minimal permission greater than AccessModeNone except all are AccessModeNone if mode > perm.AccessModeNone && (res == perm.AccessModeNone || mode < res) { res = mode } diff --git a/models/unit/unit_test.go b/models/unit/unit_test.go index efcad4a405..1da15c4d85 100644 --- a/models/unit/unit_test.go +++ b/models/unit/unit_test.go @@ -1,11 +1,13 @@ // Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package unit +package unit_test import ( "testing" + unit_model "forgejo.org/models/unit" + "forgejo.org/models/unit/tests" "forgejo.org/modules/setting" "github.com/stretchr/testify/assert" @@ -14,83 +16,47 @@ import ( func TestLoadUnitConfig(t *testing.T) { t.Run("regular", func(t *testing.T) { - defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []Type) { - DisabledRepoUnitsSet(disabledRepoUnits) - DefaultRepoUnits = defaultRepoUnits - DefaultForkRepoUnits = defaultForkRepoUnits - }(DisabledRepoUnitsGet(), DefaultRepoUnits, DefaultForkRepoUnits) - defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []string) { - setting.Repository.DisabledRepoUnits = disabledRepoUnits - setting.Repository.DefaultRepoUnits = defaultRepoUnits - setting.Repository.DefaultForkRepoUnits = defaultForkRepoUnits - }(setting.Repository.DisabledRepoUnits, setting.Repository.DefaultRepoUnits, setting.Repository.DefaultForkRepoUnits) + defer tests.SaveUnits()() setting.Repository.DisabledRepoUnits = []string{"repo.issues"} setting.Repository.DefaultRepoUnits = []string{"repo.code", "repo.releases", "repo.issues", "repo.pulls"} setting.Repository.DefaultForkRepoUnits = []string{"repo.releases"} - require.NoError(t, LoadUnitConfig()) - assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnitsGet()) - assert.Equal(t, []Type{TypeCode, TypeReleases, TypePullRequests}, DefaultRepoUnits) - assert.Equal(t, []Type{TypeReleases}, DefaultForkRepoUnits) + require.NoError(t, unit_model.LoadUnitConfig()) + assert.Equal(t, []unit_model.Type{unit_model.TypeIssues}, unit_model.DisabledRepoUnitsGet()) + assert.Equal(t, []unit_model.Type{unit_model.TypeCode, unit_model.TypeReleases, unit_model.TypePullRequests}, unit_model.DefaultRepoUnits) + assert.Equal(t, []unit_model.Type{unit_model.TypeReleases}, unit_model.DefaultForkRepoUnits) }) t.Run("invalid", func(t *testing.T) { - defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []Type) { - DisabledRepoUnitsSet(disabledRepoUnits) - DefaultRepoUnits = defaultRepoUnits - DefaultForkRepoUnits = defaultForkRepoUnits - }(DisabledRepoUnitsGet(), DefaultRepoUnits, DefaultForkRepoUnits) - defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []string) { - setting.Repository.DisabledRepoUnits = disabledRepoUnits - setting.Repository.DefaultRepoUnits = defaultRepoUnits - setting.Repository.DefaultForkRepoUnits = defaultForkRepoUnits - }(setting.Repository.DisabledRepoUnits, setting.Repository.DefaultRepoUnits, setting.Repository.DefaultForkRepoUnits) + defer tests.SaveUnits()() setting.Repository.DisabledRepoUnits = []string{"repo.issues", "invalid.1"} setting.Repository.DefaultRepoUnits = []string{"repo.code", "invalid.2", "repo.releases", "repo.issues", "repo.pulls"} setting.Repository.DefaultForkRepoUnits = []string{"invalid.3", "repo.releases"} - require.NoError(t, LoadUnitConfig()) - assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnitsGet()) - assert.Equal(t, []Type{TypeCode, TypeReleases, TypePullRequests}, DefaultRepoUnits) - assert.Equal(t, []Type{TypeReleases}, DefaultForkRepoUnits) + require.NoError(t, unit_model.LoadUnitConfig()) + assert.Equal(t, []unit_model.Type{unit_model.TypeIssues}, unit_model.DisabledRepoUnitsGet()) + assert.Equal(t, []unit_model.Type{unit_model.TypeCode, unit_model.TypeReleases, unit_model.TypePullRequests}, unit_model.DefaultRepoUnits) + assert.Equal(t, []unit_model.Type{unit_model.TypeReleases}, unit_model.DefaultForkRepoUnits) }) t.Run("duplicate", func(t *testing.T) { - defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []Type) { - DisabledRepoUnitsSet(disabledRepoUnits) - DefaultRepoUnits = defaultRepoUnits - DefaultForkRepoUnits = defaultForkRepoUnits - }(DisabledRepoUnitsGet(), DefaultRepoUnits, DefaultForkRepoUnits) - defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []string) { - setting.Repository.DisabledRepoUnits = disabledRepoUnits - setting.Repository.DefaultRepoUnits = defaultRepoUnits - setting.Repository.DefaultForkRepoUnits = defaultForkRepoUnits - }(setting.Repository.DisabledRepoUnits, setting.Repository.DefaultRepoUnits, setting.Repository.DefaultForkRepoUnits) + defer tests.SaveUnits()() setting.Repository.DisabledRepoUnits = []string{"repo.issues", "repo.issues"} setting.Repository.DefaultRepoUnits = []string{"repo.code", "repo.releases", "repo.issues", "repo.pulls", "repo.code"} setting.Repository.DefaultForkRepoUnits = []string{"repo.releases", "repo.releases"} - require.NoError(t, LoadUnitConfig()) - assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnitsGet()) - assert.Equal(t, []Type{TypeCode, TypeReleases, TypePullRequests}, DefaultRepoUnits) - assert.Equal(t, []Type{TypeReleases}, DefaultForkRepoUnits) + require.NoError(t, unit_model.LoadUnitConfig()) + assert.Equal(t, []unit_model.Type{unit_model.TypeIssues}, unit_model.DisabledRepoUnitsGet()) + assert.Equal(t, []unit_model.Type{unit_model.TypeCode, unit_model.TypeReleases, unit_model.TypePullRequests}, unit_model.DefaultRepoUnits) + assert.Equal(t, []unit_model.Type{unit_model.TypeReleases}, unit_model.DefaultForkRepoUnits) }) t.Run("empty_default", func(t *testing.T) { - defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []Type) { - DisabledRepoUnitsSet(disabledRepoUnits) - DefaultRepoUnits = defaultRepoUnits - DefaultForkRepoUnits = defaultForkRepoUnits - }(DisabledRepoUnitsGet(), DefaultRepoUnits, DefaultForkRepoUnits) - defer func(disabledRepoUnits, defaultRepoUnits, defaultForkRepoUnits []string) { - setting.Repository.DisabledRepoUnits = disabledRepoUnits - setting.Repository.DefaultRepoUnits = defaultRepoUnits - setting.Repository.DefaultForkRepoUnits = defaultForkRepoUnits - }(setting.Repository.DisabledRepoUnits, setting.Repository.DefaultRepoUnits, setting.Repository.DefaultForkRepoUnits) + defer tests.SaveUnits()() setting.Repository.DisabledRepoUnits = []string{"repo.issues", "repo.issues"} setting.Repository.DefaultRepoUnits = []string{} setting.Repository.DefaultForkRepoUnits = []string{"repo.releases", "repo.releases"} - require.NoError(t, LoadUnitConfig()) - assert.Equal(t, []Type{TypeIssues}, DisabledRepoUnitsGet()) - assert.ElementsMatch(t, []Type{TypeCode, TypePullRequests, TypeReleases, TypeWiki, TypePackages, TypeProjects, TypeActions}, DefaultRepoUnits) - assert.Equal(t, []Type{TypeReleases}, DefaultForkRepoUnits) + require.NoError(t, unit_model.LoadUnitConfig()) + assert.Equal(t, []unit_model.Type{unit_model.TypeIssues}, unit_model.DisabledRepoUnitsGet()) + assert.ElementsMatch(t, []unit_model.Type{unit_model.TypeCode, unit_model.TypePullRequests, unit_model.TypeReleases, unit_model.TypeWiki, unit_model.TypePackages, unit_model.TypeProjects, unit_model.TypeActions}, unit_model.DefaultRepoUnits) + assert.Equal(t, []unit_model.Type{unit_model.TypeReleases}, unit_model.DefaultForkRepoUnits) }) } diff --git a/models/unittest/fixture_loader.go b/models/unittest/fixture_loader.go index 5aea06550c..3cf2efdced 100644 --- a/models/unittest/fixture_loader.go +++ b/models/unittest/fixture_loader.go @@ -151,8 +151,8 @@ func (l *loader) buildFixtureFile(fixturePath string) (*fixtureFile, error) { switch v := value.(type) { case string: // Try to decode hex. - if strings.HasPrefix(v, "0x") { - value, err = hex.DecodeString(strings.TrimPrefix(v, "0x")) + if after, ok := strings.CutPrefix(v, "0x"); ok { + value, err = hex.DecodeString(after) if err != nil { return nil, err } diff --git a/models/unittest/mock_http.go b/models/unittest/mock_http.go index b8413104b3..5e420533d8 100644 --- a/models/unittest/mock_http.go +++ b/models/unittest/mock_http.go @@ -102,13 +102,13 @@ func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveM // parse back the fixture file into a series of HTTP headers followed by response body lines := strings.Split(stringFixture, "\n") for idx, line := range lines { - colonIndex := strings.Index(line, ": ") - if colonIndex != -1 { + before, after, ok := strings.Cut(line, ": ") + if ok { // Because we modified the body with ReplaceAll() above, we need to // remove Content-Length. w.Write() should add it back. - header := line[0:colonIndex] + header := before if !strings.EqualFold(header, "Content-Length") { - w.Header().Set(line[0:colonIndex], line[colonIndex+2:]) + w.Header().Set(before, after) } } else { // we reached the end of the headers (empty line), so what follows is the body diff --git a/models/unittest/reflection.go b/models/unittest/reflection.go index 141fc66b99..939891283d 100644 --- a/models/unittest/reflection.go +++ b/models/unittest/reflection.go @@ -9,7 +9,7 @@ import ( ) func fieldByName(v reflect.Value, field string) reflect.Value { - if v.Kind() == reflect.Ptr { + if v.Kind() == reflect.Pointer { v = v.Elem() } f := v.FieldByName(field) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 8e212bc0a3..c926a85df6 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -47,10 +47,29 @@ func fatalTestError(fmtStr string, args ...any) { // InitSettings initializes config provider and load common settings for tests func InitSettings() { - if setting.CustomConf == "" { - setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") - _ = os.Remove(setting.CustomConf) + InitCustomSettings("unittest.ini") +} + +func InitCustomSettings(confFileName string) { + root := base.SetupGiteaRoot() + if root == "" { + fatalTestError("Environment variable $GITEA_ROOT not set") } + setting.AppPath = filepath.Join(root, "gitea") + if setting.CustomConf == "" { + templateFile := confFileName + ".tmpl" + content, err := os.ReadFile(filepath.Join(root, "tests", templateFile)) + if err != nil { + log.Fatalf("couldn't read config template: %s", templateFile) + } + err = os.WriteFile(filepath.Join(root, "tests", confFileName), content, 0o644) + if err != nil { + log.Fatalf("couldn't write config: %s", confFileName) + } + setting.CustomConf = filepath.Join(root, "tests", confFileName) + } + os.Setenv("GITEA_CONF", setting.CustomConf) + setting.InitCfgProvider(setting.CustomConf) setting.LoadCommonSettings() @@ -72,9 +91,10 @@ func InitSettings() { // TestOptions represents test options type TestOptions struct { - FixtureFiles []string - SetUp func() error // SetUp will be executed before all tests in this package - TearDown func() error // TearDown will be executed after all tests in this package + FixtureFiles []string + SetUp func() error // SetUp will be executed before all tests in this package + TearDown func() error // TearDown will be executed after all tests in this package + IniFileOverride string } // MainTest a reusable TestMain(..) function for unit tests that need to use a @@ -97,7 +117,11 @@ func MainTest(m *testing.M, testOpts ...*TestOptions) { giteaRoot = searchDir setting.CustomPath = filepath.Join(giteaRoot, "custom") - InitSettings() + if len(testOpts) == 0 || testOpts[0].IniFileOverride == "" { + InitSettings() + } else { + InitCustomSettings(testOpts[0].IniFileOverride) + } fixturesDir = filepath.Join(giteaRoot, "models", "fixtures") var opts FixturesOptions diff --git a/models/unittest/unit_tests.go b/models/unittest/unit_tests.go index 2fa49e443a..411777cb17 100644 --- a/models/unittest/unit_tests.go +++ b/models/unittest/unit_tests.go @@ -67,7 +67,7 @@ func BeanExists(t testing.TB, bean any, conditions ...any) bool { } // AssertExistsAndLoadBean assert that a bean exists and load it from the test database -func AssertExistsAndLoadBean[T any](t testing.TB, bean T, conditions ...any) T { +func AssertExistsAndLoadBean[T any](t require.TestingT, bean T, conditions ...any) T { exists, err := LoadBeanIfExists(bean, conditions...) require.NoError(t, err) assert.True(t, exists, diff --git a/models/user/avatar.go b/models/user/avatar.go index d534bd7bea..726d67f5e0 100644 --- a/models/user/avatar.go +++ b/models/user/avatar.go @@ -60,7 +60,8 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error { return nil } -// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size +// AvatarLinkWithSize returns a link to the user's avatar. Size is only used for +// GenerateEmailAvatarFastLink, for external email-based avatar services func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { if u.IsGhost() || u.ID <= 0 { return avatars.DefaultAvatarLink() @@ -88,7 +89,7 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { if u.Avatar == "" { return avatars.DefaultAvatarLink() } - return avatars.GenerateUserAvatarImageLink(u.Avatar, size) + return avatars.GenerateUserAvatarImageLink(u.Avatar) } return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size) } @@ -107,7 +108,7 @@ func (u *User) IsUploadAvatarChanged(data []byte) bool { if !u.UseCustomAvatar || len(u.Avatar) == 0 { return true } - avatarID := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%d-%x", u.ID, md5.Sum(data))))) + avatarID := fmt.Sprintf("%x", md5.Sum(fmt.Appendf(nil, "%d-%x", u.ID, md5.Sum(data)))) return u.Avatar != avatarID } diff --git a/models/user/email_address.go b/models/user/email_address.go index a3c33ffc00..54e3c1dc19 100644 --- a/models/user/email_address.go +++ b/models/user/email_address.go @@ -323,12 +323,12 @@ func SearchEmails(ctx context.Context, opts *SearchEmailOptions) ([]*SearchEmail )) } - if opts.IsPrimary.Has() { - cond = cond.And(builder.Eq{"email_address.is_primary": opts.IsPrimary.Value()}) + if has, value := opts.IsPrimary.Get(); has { + cond = cond.And(builder.Eq{"email_address.is_primary": value}) } - if opts.IsActivated.Has() { - cond = cond.And(builder.Eq{"email_address.is_activated": opts.IsActivated.Value()}) + if has, value := opts.IsActivated.Get(); has { + cond = cond.And(builder.Eq{"email_address.is_activated": value}) } count, err := db.GetEngine(ctx).Join("INNER", "`user`", "`user`.id = email_address.uid"). diff --git a/models/user/email_address_test.go b/models/user/email_address_test.go index 85f5b16c65..35b33933c2 100644 --- a/models/user/email_address_test.go +++ b/models/user/email_address_test.go @@ -5,6 +5,7 @@ package user_test import ( "fmt" + "slices" "testing" "forgejo.org/models/db" @@ -77,12 +78,7 @@ func TestListEmails(t *testing.T) { assert.Greater(t, count, int64(5)) contains := func(match func(s *user_model.SearchEmailResult) bool) bool { - for _, v := range emails { - if match(v) { - return true - } - } - return false + return slices.ContainsFunc(emails, match) } assert.True(t, contains(func(s *user_model.SearchEmailResult) bool { return s.UID == 18 })) diff --git a/models/user/error.go b/models/user/error.go index 264b3fdcd2..7f23758768 100644 --- a/models/user/error.go +++ b/models/user/error.go @@ -105,3 +105,16 @@ func IsErrUserIsNotLocal(err error) bool { _, ok := err.(ErrUserIsNotLocal) return ok } + +type ErrFederatedUserNotExists struct { + Identifier string +} + +func (err ErrFederatedUserNotExists) Error() string { + return fmt.Sprintf("No cached federated user found for identifier %s", err.Identifier) +} + +func IsErrFederatedUserNotExists(err error) bool { + _, ok := err.(ErrFederatedUserNotExists) + return ok +} diff --git a/models/user/federated_user.go b/models/user/federated_user.go index d2a9c34c9e..8199a9cd22 100644 --- a/models/user/federated_user.go +++ b/models/user/federated_user.go @@ -5,6 +5,7 @@ package user import ( "database/sql" + "fmt" "forgejo.org/modules/validation" ) @@ -42,3 +43,18 @@ func (federatedUser FederatedUser) Validate() []string { result = append(result, validation.ValidateNotEmpty(federatedUser.InboxPath, "InboxPath")...) return result } + +func (federatedUser *FederatedUser) LogString() string { + if federatedUser == nil { + return "" + } + + return fmt.Sprintf( + "", + federatedUser.ID, + federatedUser.UserID, + federatedUser.ExternalID, + federatedUser.NormalizedOriginalURL, + federatedUser.InboxPath, + ) +} diff --git a/models/user/moderation.go b/models/user/moderation.go index 7bc857489a..fe8eef1806 100644 --- a/models/user/moderation.go +++ b/models/user/moderation.go @@ -87,10 +87,10 @@ func newUserData(user *User) UserData { // (e.g. FieldName -> field_name) corresponding to UserData struct fields. var userDataColumnNames = sync.OnceValue(func() []string { mapper := new(names.GonicMapper) - udType := reflect.TypeOf(UserData{}) + udType := reflect.TypeFor[UserData]() columnNames := make([]string, 0, udType.NumField()) - for i := 0; i < udType.NumField(); i++ { - columnNames = append(columnNames, mapper.Obj2Table(udType.Field(i).Name)) + for field := range udType.Fields() { + columnNames = append(columnNames, mapper.Obj2Table(field.Name)) } return columnNames }) diff --git a/models/user/search.go b/models/user/search.go index 08cf6a14a3..25c23a3740 100644 --- a/models/user/search.go +++ b/models/user/search.go @@ -101,40 +101,41 @@ func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Sess cond = cond.And(builder.Eq{"id": opts.UID}) } - if opts.SourceID.Has() { - cond = cond.And(builder.Eq{"login_source": opts.SourceID.Value()}) + if has, value := opts.SourceID.Get(); has { + cond = cond.And(builder.Eq{"login_source": value}) } if opts.LoginName != "" { cond = cond.And(builder.Eq{"login_name": opts.LoginName}) } - if opts.IsActive.Has() { - cond = cond.And(builder.Eq{"is_active": opts.IsActive.Value()}) + if has, value := opts.IsActive.Get(); has { + cond = cond.And(builder.Eq{"is_active": value}) } - if opts.IsAdmin.Has() { - cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()}) + if has, value := opts.IsAdmin.Get(); has { + cond = cond.And(builder.Eq{"is_admin": value}) } - if opts.IsRestricted.Has() { - cond = cond.And(builder.Eq{"is_restricted": opts.IsRestricted.Value()}) + if has, value := opts.IsRestricted.Get(); has { + cond = cond.And(builder.Eq{"is_restricted": value}) } - if opts.IsProhibitLogin.Has() { - cond = cond.And(builder.Eq{"prohibit_login": opts.IsProhibitLogin.Value()}) + if has, value := opts.IsProhibitLogin.Get(); has { + cond = cond.And(builder.Eq{"prohibit_login": value}) } - if opts.AccountType.Has() { - cond = cond.And(builder.Eq{"type": opts.AccountType.Value()}) + if has, value := opts.AccountType.Get(); has { + cond = cond.And(builder.Eq{"type": value}) } e := db.GetEngine(ctx) - if !opts.IsTwoFactorEnabled.Has() { + hasTwoFactor, isTwoFactorEnabled := opts.IsTwoFactorEnabled.Get() + if !hasTwoFactor { return e.Where(cond) } // Check if the user has two factor enabled, which is TOTP or Webauthn. - if opts.IsTwoFactorEnabled.Value() { + if isTwoFactorEnabled { cond = cond.And(builder.Expr("two_factor.uid IS NOT NULL OR webauthn_credential.user_id IS NOT NULL")) } else { cond = cond.And(builder.Expr("two_factor.uid IS NULL AND webauthn_credential.user_id IS NULL")) diff --git a/models/user/user.go b/models/user/user.go index f871284890..2c20cd977d 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -464,15 +464,6 @@ func (u *User) IsUser() bool { return u.Type == UserTypeIndividual || u.Type == UserTypeBot } -// Returns true if the given user ID belongs to an actual user, not an organization -func IsUserByID(ctx context.Context, uid int64) (bool, error) { - return db.GetEngine(ctx). - Where("id=?", uid). - In("type", UserTypeIndividual, UserTypeBot). - Table("user"). - Exist() -} - // IsBot returns whether or not the user is of type bot func (u *User) IsBot() bool { return u.Type == UserTypeBot @@ -664,7 +655,6 @@ var ( "user", // user login/activate/settings, etc "admin", - "devtest", "explore", "issues", "pulls", @@ -699,6 +689,14 @@ func IsUsableUsername(name string) error { return db.IsUsableName(reservedUsernames, reservedUserPatterns, name) } +// IsActivityPubUsername returns an error if a fediverse handle (referred to as a username) cannot exist +func IsActivityPubUsername(name string) error { + if !validation.IsValidActivityPubUsername(name) { + return db.ErrNameActivityPubInvalid{Name: name} + } + return db.IsUsableName(reservedUsernames, reservedUserPatterns, name) +} + // CreateUserOverwriteOptions are an optional options who overwrite system defaults on user creation type CreateUserOverwriteOptions struct { KeepEmailPrivate optional.Option[bool] @@ -709,6 +707,7 @@ type CreateUserOverwriteOptions struct { Theme *string IsRestricted optional.Option[bool] IsActive optional.Option[bool] + IsActivityPub optional.Option[bool] } // CreateUser creates record of a new user. @@ -723,12 +722,26 @@ func AdminCreateUser(ctx context.Context, u *User, overwriteDefault ...*CreateUs // createUser creates record of a new user. func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefault ...*CreateUserOverwriteOptions) (err error) { - if err = IsUsableUsername(u.Name); err != nil { + overwriteDefaultPresent := len(overwriteDefault) != 0 && overwriteDefault[0] != nil + + // If a username is invalid as-is, check whether the username is meant + // for an ActivityPub account. Username constraints that belong to "foreign" + // ActivityPub servers, whose implementations we cannot control, are expected + // to be much less restrictive than those of Forgejo itself. + if overwriteDefaultPresent && overwriteDefault[0].IsActivityPub.Has() { + if err = IsActivityPubUsername(u.Name); err != nil { + return err + } + } else if err := IsUsableUsername(u.Name); err != nil { return err } // Check if the new username can be claimed. // Skip this check if done by an admin. + // + // Note: This skip should not currently cover usernames that could belong to + // fediverse accounts. This "defensive programming" is in place to prevent future + // breakage until the ActivityPub component matures more. if !createdByAdmin { if ok, expireTime, err := CanClaimUsername(ctx, u.Name, -1); err != nil { return err @@ -755,16 +768,16 @@ func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefa } // overwrite defaults if set - if len(overwriteDefault) != 0 && overwriteDefault[0] != nil { + if overwriteDefaultPresent { overwrite := overwriteDefault[0] - if overwrite.KeepEmailPrivate.Has() { - u.KeepEmailPrivate = overwrite.KeepEmailPrivate.Value() + if has, value := overwrite.KeepEmailPrivate.Get(); has { + u.KeepEmailPrivate = value } if overwrite.Visibility != nil { u.Visibility = *overwrite.Visibility } - if overwrite.AllowCreateOrganization.Has() { - u.AllowCreateOrganization = overwrite.AllowCreateOrganization.Value() + if has, value := overwrite.AllowCreateOrganization.Get(); has { + u.AllowCreateOrganization = value } if overwrite.EmailNotificationsPreference != nil { u.EmailNotificationsPreference = *overwrite.EmailNotificationsPreference @@ -775,11 +788,11 @@ func createUser(ctx context.Context, u *User, createdByAdmin bool, overwriteDefa if overwrite.Theme != nil { u.Theme = *overwrite.Theme } - if overwrite.IsRestricted.Has() { - u.IsRestricted = overwrite.IsRestricted.Value() + if has, value := overwrite.IsRestricted.Get(); has { + u.IsRestricted = value } - if overwrite.IsActive.Has() { - u.IsActive = overwrite.IsActive.Value() + if has, value := overwrite.IsActive.Get(); has { + u.IsActive = value } } @@ -888,15 +901,15 @@ func CountUsers(ctx context.Context, opts *CountUserFilter) int64 { func countUsers(ctx context.Context, opts *CountUserFilter) int64 { sess := db.GetEngine(ctx) cond := builder.NewCond() - cond = cond.And(builder.Eq{"type": UserTypeIndividual}) + cond = cond.And(builder.In("type", UserTypeIndividual, UserTypeRemoteUser)) if opts != nil { if opts.LastLoginSince != nil { cond = cond.And(builder.Gte{"last_login_unix": *opts.LastLoginSince}) } - if opts.IsAdmin.Has() { - cond = cond.And(builder.Eq{"is_admin": opts.IsAdmin.Value()}) + if has, value := opts.IsAdmin.Get(); has { + cond = cond.And(builder.Eq{"is_admin": value}) } } @@ -1230,8 +1243,8 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { } // Finally, if email address is the protected email address: - if strings.HasSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress)) { - username := strings.TrimSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress)) + if before, ok := strings.CutSuffix(email, fmt.Sprintf("@%s", setting.Service.NoReplyAddress)); ok { + username := before user := &User{} has, err := db.GetEngine(ctx).Where("lower_name=?", username).Get(user) if err != nil { diff --git a/models/user/user_repository.go b/models/user/user_repository.go index f1d06abe17..506d1cfb15 100644 --- a/models/user/user_repository.go +++ b/models/user/user_repository.go @@ -23,8 +23,9 @@ func CreateFederatedUser(ctx context.Context, user *User, federatedUser *Federat return err } overwrite := CreateUserOverwriteOptions{ - IsActive: optional.Some(false), - IsRestricted: optional.Some(false), + IsActive: optional.Some(false), + IsRestricted: optional.Some(false), + IsActivityPub: optional.Some(true), } // Begin transaction @@ -64,7 +65,7 @@ func FindFederatedUser(ctx context.Context, externalID string, federationHostID if err != nil { return nil, nil, err } else if !has { - return nil, nil, nil + return nil, nil, ErrFederatedUserNotExists{Identifier: externalID} } has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user) if err != nil { @@ -82,14 +83,55 @@ func FindFederatedUser(ctx context.Context, externalID string, federationHostID return user, federatedUser, nil } -func GetFederatedUser(ctx context.Context, externalID string, federationHostID int64) (*User, *FederatedUser, error) { - user, federatedUser, err := FindFederatedUser(ctx, externalID, federationHostID) - if err != nil { - return nil, nil, err - } else if federatedUser == nil { - return nil, nil, fmt.Errorf("FederatedUser not found (given externalId: %v, federationHostId: %v)", externalID, federationHostID) +func CountFederatedUsers(ctx context.Context) (int64, error) { + return db.GetEngine(ctx).Count(FederatedUser{}) +} + +func FindFederatedUsers(ctx context.Context, opts db.ListOptions) (users []*FederatedUser, err error) { + sess := db.GetEngine(ctx) + + if opts.PageSize > 0 { + sess = db.SetSessionPagination(sess, &opts) } - return user, federatedUser, nil + + err = sess.Find(&users) + if err != nil { + return nil, err + } + + for _, user := range users { + if res, err := validation.IsValid(user); !res { + return nil, err + } + } + + return users, err +} + +func CountFederatedUsersByHostID(ctx context.Context, federationHostID int64) (int64, error) { + return db.GetEngine(ctx).Where("federation_host_id = ?", federationHostID).Count(FederatedUser{}) +} + +func FindFederatedUsersByHostID(ctx context.Context, federationHostID int64, opts db.ListOptions) ([]*FederatedUser, error) { + var users []*FederatedUser + sess := db.GetEngine(ctx).Where("federation_host_id = ?", federationHostID) + + if opts.PageSize > 0 { + sess = db.SetSessionPagination(sess, &opts) + } + + err := sess.Find(&users) + if err != nil { + return nil, err + } + + for _, user := range users { + if res, err := validation.IsValid(user); !res { + return nil, err + } + } + + return users, nil } func GetFederatedUserByUserID(ctx context.Context, userID int64) (*User, *FederatedUser, error) { @@ -125,7 +167,7 @@ func FindFederatedUserByKeyID(ctx context.Context, keyID string) (*User, *Federa if err != nil { return nil, nil, err } else if !has { - return nil, nil, nil + return nil, nil, ErrFederatedUserNotExists{Identifier: keyID} } has, err = db.GetEngine(ctx).ID(federatedUser.UserID).Get(user) if err != nil { diff --git a/models/user/user_test.go b/models/user/user_test.go index 330a3fd563..6da645d672 100644 --- a/models/user/user_test.go +++ b/models/user/user_test.go @@ -273,9 +273,9 @@ func TestHashPasswordDeterministic(t *testing.T) { b := make([]byte, 16) u := &user_model.User{} algos := hash.RecommendedHashAlgorithms - for j := 0; j < len(algos); j++ { + for j := range algos { u.PasswdHashAlgo = algos[j] - for i := 0; i < 50; i++ { + for range 50 { // generate a random password rand.Read(b) pass := string(b) @@ -440,6 +440,63 @@ func TestCreateUserClaimingUsername(t *testing.T) { }) } +// Attempts to create a username with a fediverse-format handle, which should +// fail (without the override IsActivityPub, which is set by CreateFederatedUser) +func TestCreateUserPlainWithFediverseHandle(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + _, err := db.GetEngine(db.DefaultContext).NoAutoTime().Insert(&user_model.Redirect{RedirectUserID: 1, LowerName: "redirecting", CreatedUnix: timeutil.TimeStampNow()}) + require.NoError(t, err) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + user.Name = "@example@example.tld" + user.LowerName = strings.ToLower(user.Name) + user.ID = 0 + user.Email = "unique@example.com" + + t.Run("Normal creation (without ActivityPub override)", func(t *testing.T) { + err = user_model.CreateUser(db.DefaultContext, user) + require.Error(t, err) + assert.True(t, db.IsErrNameCharsNotAllowed(err)) + }) + + t.Run("Creation as admin (without ActivityPub override)", func(t *testing.T) { + err = user_model.AdminCreateUser(db.DefaultContext, user) + require.Error(t, err) + assert.True(t, db.IsErrNameCharsNotAllowed(err)) + }) + + // Logic borrowed from CreateFederatedUser (which invokes CreateUser), but + // we "lend" this here to verify CreateUser's paths. + overwrite := user_model.CreateUserOverwriteOptions{ + IsActive: optional.Some(false), + IsRestricted: optional.Some(false), + IsActivityPub: optional.Some(true), + } + + t.Run("Normal creation (with ActivityPub override, invalid format)", func(t *testing.T) { + user.Name = "invalid-format-for-an-activitypub-account" + user.LowerName = strings.ToLower(user.Name) + + err = user_model.CreateUser(db.DefaultContext, user, &overwrite) + require.Error(t, err) + assert.True(t, db.IsErrNameActivityPubInvalid(err)) + }) + + t.Run("Normal creation (with ActivityPub override)", func(t *testing.T) { + user.Name = "@valid@example.tld" + user.LowerName = strings.ToLower(user.Name) + + err = user_model.CreateUser(db.DefaultContext, user, &overwrite) + require.NoError(t, err) + }) + + // Note: We don't expect that admins are able to access any front-facing + // function that sets the overwrite (i.e. CreateFederatedUser), hence it + // has been omitted for now. +} + func TestGetUserIDsByNames(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) @@ -1056,20 +1113,3 @@ func TestGetUserByEmailSimple(t *testing.T) { assert.Nil(t, u) }) } - -func TestIsUserConsistency(t *testing.T) { - defer unittest.OverrideFixtures("models/user/fixtures/")() - require.NoError(t, unittest.PrepareTestDatabase()) - - test := func(userID int64) { - user, err := user_model.GetUserByID(t.Context(), userID) - require.NoError(t, err) - isUser, err := user_model.IsUserByID(t.Context(), userID) - require.NoError(t, err) - assert.Equal(t, user.IsUser(), isUser) - } - - test(1) - test(1041) - test(1042) -} diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index 1f81caf424..196a5313bc 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -11,10 +11,9 @@ import ( "forgejo.org/models/db" "forgejo.org/modules/json" + "forgejo.org/modules/keying" "forgejo.org/modules/log" "forgejo.org/modules/optional" - "forgejo.org/modules/secret" - "forgejo.org/modules/setting" "forgejo.org/modules/timeutil" "forgejo.org/modules/util" webhook_module "forgejo.org/modules/webhook" @@ -137,7 +136,7 @@ type Webhook struct { LastStatus webhook_module.HookStatus // Last delivery status // HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization() - HeaderAuthorizationEncrypted string `xorm:"TEXT"` + HeaderAuthorizationEncrypted []byte `xorm:"BLOB"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` @@ -376,10 +375,15 @@ func (w *Webhook) EventsArray() []string { // HeaderAuthorization returns the decrypted Authorization header. // Not on the reference (*w), to be accessible on WebhooksNew. func (w Webhook) HeaderAuthorization() (string, error) { - if w.HeaderAuthorizationEncrypted == "" { + if len(w.HeaderAuthorizationEncrypted) == 0 { return "", nil } - return secret.DecryptSecret(setting.SecretKey, w.HeaderAuthorizationEncrypted) + + headerAuthorization, err := keying.Webhook.Decrypt(w.HeaderAuthorizationEncrypted, keying.ColumnAndID("header_authorization_encrypted", w.ID)) + if err != nil { + return "", err + } + return string(headerAuthorization), nil } // HeaderAuthorizationTrimPrefix returns the decrypted Authorization with a specified prefix trimmed. @@ -392,23 +396,31 @@ func (w Webhook) HeaderAuthorizationTrimPrefix(prefix string) (string, error) { } // SetHeaderAuthorization encrypts and sets the Authorization header. -func (w *Webhook) SetHeaderAuthorization(cleartext string) error { +func (w *Webhook) SetHeaderAuthorization(cleartext string) { if cleartext == "" { - w.HeaderAuthorizationEncrypted = "" - return nil + w.HeaderAuthorizationEncrypted = nil + return } - ciphertext, err := secret.EncryptSecret(setting.SecretKey, cleartext) - if err != nil { - return err - } - w.HeaderAuthorizationEncrypted = ciphertext - return nil + + w.HeaderAuthorizationEncrypted = keying.Webhook.Encrypt([]byte(cleartext), keying.ColumnAndID("header_authorization_encrypted", w.ID)) } // CreateWebhook creates a new web hook. -func CreateWebhook(ctx context.Context, w *Webhook) error { +func CreateWebhook(ctx context.Context, w *Webhook, authorizationHeader string) error { w.Type = strings.TrimSpace(w.Type) - return db.Insert(ctx, w) + + if len(authorizationHeader) == 0 { + return db.Insert(ctx, w) + } + return db.WithTx(ctx, func(ctx context.Context) error { + if err := db.Insert(ctx, w); err != nil { + return err + } + + w.SetHeaderAuthorization(authorizationHeader) + _, err := db.GetEngine(ctx).Cols("header_authorization_encrypted").ID(w.ID).Update(w) + return err + }) } // CreateWebhooks creates multiple web hooks @@ -417,7 +429,7 @@ func CreateWebhooks(ctx context.Context, ws []*Webhook) error { if len(ws) == 0 { return nil } - for i := 0; i < len(ws); i++ { + for i := range ws { ws[i].Type = strings.TrimSpace(ws[i].Type) } return db.Insert(ctx, ws) @@ -475,8 +487,8 @@ func (opts ListWebhookOptions) ToConds() builder.Cond { if opts.OwnerID != 0 { cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID}) } - if opts.IsActive.Has() { - cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()}) + if has, value := opts.IsActive.Get(); has { + cond = cond.And(builder.Eq{"webhook.is_active": value}) } return cond } diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go index 2e53f639dc..9cdd323235 100644 --- a/models/webhook/webhook_system.go +++ b/models/webhook/webhook_system.go @@ -77,7 +77,7 @@ func CopyDefaultWebhooksToRepo(ctx context.Context, repoID int64) error { for _, w := range ws { w.ID = 0 w.RepoID = repoID - if err := CreateWebhook(ctx, w); err != nil { + if err := CreateWebhook(ctx, w, ""); err != nil { return fmt.Errorf("CreateWebhook: %v", err) } } diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go index 60cd2b333b..1dabc0ff38 100644 --- a/models/webhook/webhook_test.go +++ b/models/webhook/webhook_test.go @@ -98,7 +98,7 @@ func TestCreateWebhook(t *testing.T) { Events: `{"push_only":false,"send_everything":false,"choose_events":true,"events":{"create":false,"push":true,"pull_request":true}}`, } unittest.AssertNotExistsBean(t, hook) - require.NoError(t, CreateWebhook(db.DefaultContext, hook)) + require.NoError(t, CreateWebhook(db.DefaultContext, hook, "")) hookFromDb := unittest.AssertExistsAndLoadBean(t, hook) assert.Equal(t, []string{ string(webhook_module.HookEventPush), @@ -114,7 +114,7 @@ func TestCreateWebhook(t *testing.T) { Events: `{"push_only":false,"send_everything":false,"choose_events":true,"events":{"action_run_recover":false,"action_run_success":true}}`, } unittest.AssertNotExistsBean(t, hook) - require.NoError(t, CreateWebhook(db.DefaultContext, hook)) + require.NoError(t, CreateWebhook(db.DefaultContext, hook, "")) hookFromDb := unittest.AssertExistsAndLoadBean(t, hook) assert.Equal(t, []string{string(webhook_module.HookEventActionRunSuccess)}, hookFromDb.EventsArray()) }) @@ -127,7 +127,7 @@ func TestCreateWebhook(t *testing.T) { Events: `{"push_only":false,"send_everything":false,"choose_events":true,"events":{"create":true,"delete":true,"fork":true,"issues":true,"issue_assign":true,"issue_label":true,"issue_milestone":true,"issue_comment":true,"push":true,"pull_request":true,"pull_request_assign":true,"pull_request_label":true,"pull_request_milestone":true,"pull_request_comment":true,"pull_request_review":true,"pull_request_sync":true,"pull_request_review_request":true,"wiki":true,"repository":true,"release":true,"package":true,"action_run_failure":true,"action_run_recover":true,"action_run_success":true}}`, } unittest.AssertNotExistsBean(t, hook) - require.NoError(t, CreateWebhook(db.DefaultContext, hook)) + require.NoError(t, CreateWebhook(db.DefaultContext, hook, "")) hookFromDb := unittest.AssertExistsAndLoadBean(t, hook) assert.Equal(t, []string{ string(webhook_module.HookEventCreate), diff --git a/modules/actions/task_state.go b/modules/actions/task_state.go index 77bfc747ee..889a0b0364 100644 --- a/modules/actions/task_state.go +++ b/modules/actions/task_state.go @@ -111,6 +111,8 @@ func fullStepsOfEmptySteps(task *actions_model.ActionTask) []*actions_model.Acti preStep.Status = task.Status if preStep.Status.IsSuccess() { postStep.Status = actions_model.StatusSuccess + } else if preStep.Status.IsSkipped() { + postStep.Status = actions_model.StatusSkipped } else { postStep.Status = actions_model.StatusCancelled } diff --git a/modules/actions/task_state_test.go b/modules/actions/task_state_test.go index e18de4573f..084de1c709 100644 --- a/modules/actions/task_state_test.go +++ b/modules/actions/task_state_test.go @@ -156,6 +156,21 @@ func TestFullSteps(t *testing.T) { {Name: postStepName, Status: actions_model.StatusSuccess, LogIndex: 90, LogLength: 10, Started: 10090, Stopped: 10100}, }, }, + { + // situation occurs with a reusable workflow's outer job which has no steps + name: "skipped task w/ zero steps", + task: &actions_model.ActionTask{ + Steps: []*actions_model.ActionTaskStep{}, + Status: actions_model.StatusSkipped, + Started: 0, + Stopped: 0, + LogLength: 0, + }, + want: []*actions_model.ActionTaskStep{ + {Name: preStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + {Name: postStepName, Status: actions_model.StatusSkipped, LogIndex: 0, LogLength: 0, Started: 0, Stopped: 0}, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 6a7630548a..ed54ccc98b 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -5,7 +5,9 @@ package actions import ( "bytes" + "fmt" "io" + "slices" "strings" actions_model "forgejo.org/models/actions" @@ -30,6 +32,12 @@ type DetectedWorkflow struct { NeedApproval actions_model.ApprovalType } +// GetWorkflowPath returns the full path to the workflow from the repository root, for example, +// .forgejo/workflows/test.yaml. +func (wf *DetectedWorkflow) GetWorkflowPath() string { + return fmt.Sprintf("%s/%s", wf.EntryDirectory, wf.EntryName) +} + func init() { model.OnDecodeNodeError = func(node yaml.Node, out any, err error) { // Log the error instead of panic or fatal. @@ -602,11 +610,8 @@ func matchPullRequestReviewEvent(prPayload *api.PullRequestPayload, evt *jobpars matched := false for _, val := range vals { - for _, action := range actions { - if glob.MustCompile(val, '/').Match(action) { - matched = true - break - } + if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) { + matched = true } if matched { break @@ -651,11 +656,8 @@ func matchPullRequestReviewCommentEvent(prPayload *api.PullRequestPayload, evt * matched := false for _, val := range vals { - for _, action := range actions { - if glob.MustCompile(val, '/').Match(action) { - matched = true - break - } + if slices.ContainsFunc(actions, glob.MustCompile(val, '/').Match) { + matched = true } if matched { break diff --git a/modules/actions/workflows_test.go b/modules/actions/workflows_test.go index b431989def..911d72a09a 100644 --- a/modules/actions/workflows_test.go +++ b/modules/actions/workflows_test.go @@ -19,6 +19,14 @@ import ( "github.com/stretchr/testify/require" ) +func TestDetectedWorkflowGetWorkflowPath(t *testing.T) { + buildWorkflow := DetectedWorkflow{EntryDirectory: ".github/workflows", EntryName: "build.yaml"} + testWorkflow := DetectedWorkflow{EntryDirectory: ".forgejo/workflows", EntryName: "test.yaml"} + + assert.Equal(t, ".github/workflows/build.yaml", buildWorkflow.GetWorkflowPath()) + assert.Equal(t, ".forgejo/workflows/test.yaml", testWorkflow.GetWorkflowPath()) +} + func TestActionsWorkflowsDetectMatched(t *testing.T) { testCases := []struct { desc string diff --git a/modules/activitypub/main_test.go b/modules/activitypub/main_test.go index a3f173f408..3f1a1b3562 100644 --- a/modules/activitypub/main_test.go +++ b/modules/activitypub/main_test.go @@ -7,6 +7,8 @@ import ( "testing" "forgejo.org/models/unittest" + + _ "forgejo.org/modules/testimport" ) func TestMain(m *testing.M) { diff --git a/modules/assetfs/layered.go b/modules/assetfs/layered.go index 2041f28bb1..a9b99556e2 100644 --- a/modules/assetfs/layered.go +++ b/modules/assetfs/layered.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "io" "io/fs" "os" "path/filepath" @@ -97,14 +96,12 @@ func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) { func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) { name := util.PathJoinRel(elems...) for _, layer := range l.layers { - f, err := layer.Open(name) - if os.IsNotExist(err) { + bs, err := fs.ReadFile(layer, name) + if errors.Is(err, fs.ErrNotExist) { continue } else if err != nil { return nil, layer.name, err } - bs, err := io.ReadAll(f) - _ = f.Close() return bs, layer.name, err } return nil, "", fs.ErrNotExist diff --git a/modules/auth/password/hash/setting.go b/modules/auth/password/hash/setting.go index 05cd36fe3c..24d0f726c7 100644 --- a/modules/auth/password/hash/setting.go +++ b/modules/auth/password/hash/setting.go @@ -14,7 +14,7 @@ const DefaultHashAlgorithmName = "pbkdf2_hi" var DefaultHashAlgorithm *PasswordHashAlgorithm -// aliasAlgorithNames provides a mapping between the value of PASSWORD_HASH_ALGO +// aliasAlgorithmNames provides a mapping between the value of PASSWORD_HASH_ALGO // configured in the app.ini and the parameters used within the hashers internally. // // If it is necessary to change the default parameters for any hasher in future you diff --git a/modules/auth/password/password.go b/modules/auth/password/password.go index fdbc4ff291..744a431ea8 100644 --- a/modules/auth/password/password.go +++ b/modules/auth/password/password.go @@ -101,7 +101,7 @@ func Generate(n int) (string, error) { buffer := make([]byte, n) max := big.NewInt(int64(len(validChars))) for { - for j := 0; j < n; j++ { + for j := range n { rnd, err := rand.Int(rand.Reader, max) if err != nil { return "", err diff --git a/modules/auth/password/password_test.go b/modules/auth/password/password_test.go index 1fe3fb5ce1..8f5d64514c 100644 --- a/modules/auth/password/password_test.go +++ b/modules/auth/password/password_test.go @@ -51,7 +51,7 @@ func TestComplexity_Generate(t *testing.T) { test := func(t *testing.T, modes []string) { testComplextity(modes) - for i := 0; i < maxCount; i++ { + for range maxCount { pwd, err := Generate(pwdLen) require.NoError(t, err) assert.Len(t, pwd, pwdLen) diff --git a/modules/auth/password/pwn/pwn.go b/modules/auth/password/pwn/pwn.go index 10693ec663..f3277ff616 100644 --- a/modules/auth/password/pwn/pwn.go +++ b/modules/auth/password/pwn/pwn.go @@ -101,7 +101,7 @@ func (c *Client) CheckPassword(pw string, padding bool) (int, error) { } defer resp.Body.Close() - for _, pair := range strings.Split(string(body), "\n") { + for pair := range strings.SplitSeq(string(body), "\n") { parts := strings.Split(pair, ":") if len(parts) != 2 { continue diff --git a/modules/avatar/identicon/block.go b/modules/avatar/identicon/block.go index cb1803a231..fc8ce90212 100644 --- a/modules/avatar/identicon/block.go +++ b/modules/avatar/identicon/block.go @@ -24,8 +24,8 @@ func drawBlock(img *image.Paletted, x, y, size, angle int, points []int) { rotate(points, m, m, angle) } - for i := 0; i < size; i++ { - for j := 0; j < size; j++ { + for i := range size { + for j := range size { if pointInPolygon(i, j, points) { img.SetColorIndex(x+i, y+j, 1) } diff --git a/modules/avatar/identicon/identicon.go b/modules/avatar/identicon/identicon.go index 13e8ec88e6..19f87da85a 100644 --- a/modules/avatar/identicon/identicon.go +++ b/modules/avatar/identicon/identicon.go @@ -134,7 +134,7 @@ func drawBlocks(p *image.Paletted, size int, c, b1, b2 blockFunc, b1Angle, b2Ang // then we make it left-right mirror, so we didn't draw 3/6/9 before for x := 0; x < size/2; x++ { - for y := 0; y < size; y++ { + for y := range size { p.SetColorIndex(size-x, y, p.ColorIndexAt(x, y)) } } diff --git a/modules/base/tool.go b/modules/base/tool.go index e3a3ff4a23..1cc053ba58 100644 --- a/modules/base/tool.go +++ b/modules/base/tool.go @@ -103,7 +103,7 @@ func Int64sToStrings(ints []int64) []string { func EntryIcon(entry *git.TreeEntry) string { switch { case entry.IsLink(): - te, _, err := entry.FollowLink() + te, err := entry.FollowLink() if err != nil { log.Debug(err.Error()) return "file-symlink-file" @@ -118,9 +118,17 @@ func EntryIcon(entry *git.TreeEntry) string { return "file-submodule" } + if IsCitationFile(entry) { + return "cross-reference" + } + return "file" } +func IsCitationFile(entry *git.TreeEntry) bool { + return entry.Name() == "CITATION.cff" || entry.Name() == "CITATION.bib" +} + // SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value func SetupGiteaRoot() string { giteaRoot := os.Getenv("GITEA_ROOT") diff --git a/modules/cache/mocks.go b/modules/cache/mocks.go new file mode 100644 index 0000000000..8bfec19d6b --- /dev/null +++ b/modules/cache/mocks.go @@ -0,0 +1,497 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package cache + +import ( + "code.forgejo.org/go-chi/cache" + mock "github.com/stretchr/testify/mock" +) + +// NewMockCache creates a new instance of MockCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockCache(t interface { + mock.TestingT + Cleanup(func()) +}, +) *MockCache { + mock := &MockCache{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockCache is an autogenerated mock type for the Cache type +type MockCache struct { + mock.Mock +} + +type MockCache_Expecter struct { + mock *mock.Mock +} + +func (_m *MockCache) EXPECT() *MockCache_Expecter { + return &MockCache_Expecter{mock: &_m.Mock} +} + +// Decr provides a mock function for the type MockCache +func (_mock *MockCache) Decr(key string) error { + ret := _mock.Called(key) + + if len(ret) == 0 { + panic("no return value specified for Decr") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(key) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCache_Decr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decr' +type MockCache_Decr_Call struct { + *mock.Call +} + +// Decr is a helper method to define mock.On call +// - key string +func (_e *MockCache_Expecter) Decr(key any) *MockCache_Decr_Call { + return &MockCache_Decr_Call{Call: _e.mock.On("Decr", key)} +} + +func (_c *MockCache_Decr_Call) Run(run func(key string)) *MockCache_Decr_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockCache_Decr_Call) Return(err error) *MockCache_Decr_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCache_Decr_Call) RunAndReturn(run func(key string) error) *MockCache_Decr_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function for the type MockCache +func (_mock *MockCache) Delete(key string) error { + ret := _mock.Called(key) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(key) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCache_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockCache_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - key string +func (_e *MockCache_Expecter) Delete(key any) *MockCache_Delete_Call { + return &MockCache_Delete_Call{Call: _e.mock.On("Delete", key)} +} + +func (_c *MockCache_Delete_Call) Run(run func(key string)) *MockCache_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockCache_Delete_Call) Return(err error) *MockCache_Delete_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCache_Delete_Call) RunAndReturn(run func(key string) error) *MockCache_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Flush provides a mock function for the type MockCache +func (_mock *MockCache) Flush() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Flush") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCache_Flush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Flush' +type MockCache_Flush_Call struct { + *mock.Call +} + +// Flush is a helper method to define mock.On call +func (_e *MockCache_Expecter) Flush() *MockCache_Flush_Call { + return &MockCache_Flush_Call{Call: _e.mock.On("Flush")} +} + +func (_c *MockCache_Flush_Call) Run(run func()) *MockCache_Flush_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockCache_Flush_Call) Return(err error) *MockCache_Flush_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCache_Flush_Call) RunAndReturn(run func() error) *MockCache_Flush_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type MockCache +func (_mock *MockCache) Get(key string) any { + ret := _mock.Called(key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 any + if returnFunc, ok := ret.Get(0).(func(string) any); ok { + r0 = returnFunc(key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(any) + } + } + return r0 +} + +// MockCache_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockCache_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - key string +func (_e *MockCache_Expecter) Get(key any) *MockCache_Get_Call { + return &MockCache_Get_Call{Call: _e.mock.On("Get", key)} +} + +func (_c *MockCache_Get_Call) Run(run func(key string)) *MockCache_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockCache_Get_Call) Return(v any) *MockCache_Get_Call { + _c.Call.Return(v) + return _c +} + +func (_c *MockCache_Get_Call) RunAndReturn(run func(key string) any) *MockCache_Get_Call { + _c.Call.Return(run) + return _c +} + +// Incr provides a mock function for the type MockCache +func (_mock *MockCache) Incr(key string) error { + ret := _mock.Called(key) + + if len(ret) == 0 { + panic("no return value specified for Incr") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string) error); ok { + r0 = returnFunc(key) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCache_Incr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Incr' +type MockCache_Incr_Call struct { + *mock.Call +} + +// Incr is a helper method to define mock.On call +// - key string +func (_e *MockCache_Expecter) Incr(key any) *MockCache_Incr_Call { + return &MockCache_Incr_Call{Call: _e.mock.On("Incr", key)} +} + +func (_c *MockCache_Incr_Call) Run(run func(key string)) *MockCache_Incr_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockCache_Incr_Call) Return(err error) *MockCache_Incr_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCache_Incr_Call) RunAndReturn(run func(key string) error) *MockCache_Incr_Call { + _c.Call.Return(run) + return _c +} + +// IsExist provides a mock function for the type MockCache +func (_mock *MockCache) IsExist(key string) bool { + ret := _mock.Called(key) + + if len(ret) == 0 { + panic("no return value specified for IsExist") + } + + var r0 bool + if returnFunc, ok := ret.Get(0).(func(string) bool); ok { + r0 = returnFunc(key) + } else { + r0 = ret.Get(0).(bool) + } + return r0 +} + +// MockCache_IsExist_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsExist' +type MockCache_IsExist_Call struct { + *mock.Call +} + +// IsExist is a helper method to define mock.On call +// - key string +func (_e *MockCache_Expecter) IsExist(key any) *MockCache_IsExist_Call { + return &MockCache_IsExist_Call{Call: _e.mock.On("IsExist", key)} +} + +func (_c *MockCache_IsExist_Call) Run(run func(key string)) *MockCache_IsExist_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockCache_IsExist_Call) Return(b bool) *MockCache_IsExist_Call { + _c.Call.Return(b) + return _c +} + +func (_c *MockCache_IsExist_Call) RunAndReturn(run func(key string) bool) *MockCache_IsExist_Call { + _c.Call.Return(run) + return _c +} + +// Ping provides a mock function for the type MockCache +func (_mock *MockCache) Ping() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Ping") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCache_Ping_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ping' +type MockCache_Ping_Call struct { + *mock.Call +} + +// Ping is a helper method to define mock.On call +func (_e *MockCache_Expecter) Ping() *MockCache_Ping_Call { + return &MockCache_Ping_Call{Call: _e.mock.On("Ping")} +} + +func (_c *MockCache_Ping_Call) Run(run func()) *MockCache_Ping_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockCache_Ping_Call) Return(err error) *MockCache_Ping_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCache_Ping_Call) RunAndReturn(run func() error) *MockCache_Ping_Call { + _c.Call.Return(run) + return _c +} + +// Put provides a mock function for the type MockCache +func (_mock *MockCache) Put(key string, val any, timeout int64) error { + ret := _mock.Called(key, val, timeout) + + if len(ret) == 0 { + panic("no return value specified for Put") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(string, any, int64) error); ok { + r0 = returnFunc(key, val, timeout) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCache_Put_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Put' +type MockCache_Put_Call struct { + *mock.Call +} + +// Put is a helper method to define mock.On call +// - key string +// - val any +// - timeout int64 +func (_e *MockCache_Expecter) Put(key, val, timeout any) *MockCache_Put_Call { + return &MockCache_Put_Call{Call: _e.mock.On("Put", key, val, timeout)} +} + +func (_c *MockCache_Put_Call) Run(run func(key string, val any, timeout int64)) *MockCache_Put_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 string + if args[0] != nil { + arg0 = args[0].(string) + } + var arg1 any + if args[1] != nil { + arg1 = args[1].(any) + } + var arg2 int64 + if args[2] != nil { + arg2 = args[2].(int64) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockCache_Put_Call) Return(err error) *MockCache_Put_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCache_Put_Call) RunAndReturn(run func(key string, val any, timeout int64) error) *MockCache_Put_Call { + _c.Call.Return(run) + return _c +} + +// StartAndGC provides a mock function for the type MockCache +func (_mock *MockCache) StartAndGC(opt cache.Options) error { + ret := _mock.Called(opt) + + if len(ret) == 0 { + panic("no return value specified for StartAndGC") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(cache.Options) error); ok { + r0 = returnFunc(opt) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockCache_StartAndGC_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartAndGC' +type MockCache_StartAndGC_Call struct { + *mock.Call +} + +// StartAndGC is a helper method to define mock.On call +// - opt cache.Options +func (_e *MockCache_Expecter) StartAndGC(opt any) *MockCache_StartAndGC_Call { + return &MockCache_StartAndGC_Call{Call: _e.mock.On("StartAndGC", opt)} +} + +func (_c *MockCache_StartAndGC_Call) Run(run func(opt cache.Options)) *MockCache_StartAndGC_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 cache.Options + if args[0] != nil { + arg0 = args[0].(cache.Options) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockCache_StartAndGC_Call) Return(err error) *MockCache_StartAndGC_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockCache_StartAndGC_Call) RunAndReturn(run func(opt cache.Options) error) *MockCache_StartAndGC_Call { + _c.Call.Return(run) + return _c +} diff --git a/modules/cache/mutex_map_test.go b/modules/cache/mutex_map_test.go index 324b3228d8..7dd21c2f56 100644 --- a/modules/cache/mutex_map_test.go +++ b/modules/cache/mutex_map_test.go @@ -57,7 +57,7 @@ func TestMutexMap_DifferentKeys(t *testing.T) { done := make(chan bool, 1) go func() { - // If these somehow refered to the same underlying `sync.Mutex`, because `sync.Mutex` is not re-entrant this would + // If these somehow referred to the same underlying `sync.Mutex`, because `sync.Mutex` is not re-entrant this would // never complete. unlock1 := mm.Lock("test-key-1") unlock2 := mm.Lock("test-key-2") diff --git a/modules/charset/charset.go b/modules/charset/charset.go index cb03deb966..d4121fb27f 100644 --- a/modules/charset/charset.go +++ b/modules/charset/charset.go @@ -164,7 +164,7 @@ func DetectEncoding(content []byte) (string, error) { } times := 1024 / len(content) detectContent = make([]byte, 0, times*len(content)) - for i := 0; i < times; i++ { + for range times { detectContent = append(detectContent, content...) } } else { diff --git a/modules/charset/charset_test.go b/modules/charset/charset_test.go index 358220494b..c29987beb6 100644 --- a/modules/charset/charset_test.go +++ b/modules/charset/charset_test.go @@ -243,7 +243,7 @@ func stringMustEndWith(t *testing.T, expected, value string) { func TestToUTF8WithFallbackReader(t *testing.T) { resetDefaultCharsetsOrder() - for testLen := 0; testLen < 2048; testLen++ { + for testLen := range 2048 { pattern := " test { () }\n" input := "" for len(input) < testLen { diff --git a/modules/charset/escape.go b/modules/charset/escape.go index 57b13c1f18..5b185b1608 100644 --- a/modules/charset/escape.go +++ b/modules/charset/escape.go @@ -30,7 +30,7 @@ const ( WikiContext escapeContext = "wiki" // Rendered content (except markup), source code and blames. FileviewContext escapeContext = "file-view" - // Commits or pull requet's diff. + // Commits or pull request's diff. DiffContext escapeContext = "diff" ) diff --git a/modules/eventsource/manager.go b/modules/eventsource/manager.go index 730cacd940..e0d7ab78c4 100644 --- a/modules/eventsource/manager.go +++ b/modules/eventsource/manager.go @@ -39,7 +39,9 @@ func (m *Manager) Register(uid int64) <-chan *Event { } select { case m.connection <- struct{}{}: + break default: + break } m.mutex.Unlock() return messenger.Register() diff --git a/modules/eventsource/manager_run.go b/modules/eventsource/manager_run.go index 0eaee5dc3c..48500feafc 100644 --- a/modules/eventsource/manager_run.go +++ b/modules/eventsource/manager_run.go @@ -47,7 +47,9 @@ loop: // empty the connection channel select { case <-m.connection: + break default: + break } } m.mutex.Unlock() @@ -63,7 +65,9 @@ loop: // We won't change the "then" time because there could be concurrency issues select { case <-timer.C: + break default: + break } continue } diff --git a/modules/eventsource/messenger.go b/modules/eventsource/messenger.go index 378e717126..380cb13f20 100644 --- a/modules/eventsource/messenger.go +++ b/modules/eventsource/messenger.go @@ -62,7 +62,9 @@ func (m *Messenger) SendMessage(message *Event) { channel := m.channels[i] select { case channel <- message: + break default: + break } } } diff --git a/modules/forgefed/activity_follow.go b/modules/forgefed/activity_follow.go index 5cb45ca885..928321a00a 100644 --- a/modules/forgefed/activity_follow.go +++ b/modules/forgefed/activity_follow.go @@ -48,8 +48,8 @@ func (follow *ForgeFollow) UnmarshalJSON(data []byte) error { func (follow ForgeFollow) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(string(follow.Type), "type")...) - result = append(result, validation.ValidateOneOf(string(follow.Type), []any{"Follow"}, "type")...) + result = append(result, validation.ValidateNotEmpty(follow.Type, "type")...) + result = append(result, validation.ValidateOneOf(follow.Type, []any{ap.FollowType}, "type")...) result = append(result, validation.ValidateIDExists(follow.Actor, "actor")...) result = append(result, validation.ValidateIDExists(follow.Object, "object")...) diff --git a/modules/forgefed/activity_follow_test.go b/modules/forgefed/activity_follow_test.go index 18fbef33aa..e0142a39f2 100644 --- a/modules/forgefed/activity_follow_test.go +++ b/modules/forgefed/activity_follow_test.go @@ -15,7 +15,7 @@ import ( func Test_NewForgeFollowValidation(t *testing.T) { sut := forgefed.ForgeFollow{} - sut.Type = "Follow" + sut.Type = ap.FollowType sut.Actor = ap.IRI("example.org/alice") sut.Object = ap.IRI("example.org/bob") diff --git a/modules/forgefed/activity_like.go b/modules/forgefed/activity_like.go index e52d0a9af6..7849aec568 100644 --- a/modules/forgefed/activity_like.go +++ b/modules/forgefed/activity_like.go @@ -44,8 +44,8 @@ func (like ForgeLike) IsNewer(compareTo time.Time) bool { func (like ForgeLike) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(string(like.Type), "type")...) - result = append(result, validation.ValidateOneOf(string(like.Type), []any{"Like"}, "type")...) + result = append(result, validation.ValidateNotEmpty(like.Type, "type")...) + result = append(result, validation.ValidateOneOf(like.Type, []any{ap.LikeType}, "type")...) if like.Actor == nil { result = append(result, "Actor should not be nil.") diff --git a/modules/forgefed/activity_like_test.go b/modules/forgefed/activity_like_test.go index c0b565f4db..dc2d8efdc7 100644 --- a/modules/forgefed/activity_like_test.go +++ b/modules/forgefed/activity_like_test.go @@ -51,7 +51,7 @@ func Test_LikeMarshalJSON(t *testing.T) { item: forgefed.ForgeLike{ Activity: ap.Activity{ Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"), - Type: "Like", + Type: ap.LikeType, Object: ap.IRI("https://codeberg.org/api/v1/activitypub/repository-id/1"), }, }, @@ -80,7 +80,7 @@ func Test_LikeUnmarshalJSON(t *testing.T) { item: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"}`), want: &forgefed.ForgeLike{ Activity: ap.Activity{ - Type: "Like", + Type: ap.LikeType, Actor: ap.IRI("https://repo.prod.meissa.de/api/activitypub/user-id/1"), Object: ap.IRI("https://codeberg.org/api/activitypub/repository-id/1"), }, @@ -124,7 +124,7 @@ func Test_ForgeLikeValidation(t *testing.T) { validate := sut.Validate() assert.Len(t, validate, 2) assert.Equal(t, - "Field type contains the value , which is not in allowed subset [Like]", + "Field type contains the value , which is not in allowed subset [Like]", validate[1]) sut.UnmarshalJSON([]byte(`{"type":"bad-type", diff --git a/modules/forgefed/activity_undo_like.go b/modules/forgefed/activity_undo_like.go index 8b7df582ad..eaf32ab09f 100644 --- a/modules/forgefed/activity_undo_like.go +++ b/modules/forgefed/activity_undo_like.go @@ -42,8 +42,8 @@ func (undo *ForgeUndoLike) UnmarshalJSON(data []byte) error { func (undo ForgeUndoLike) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(string(undo.Type), "type")...) - result = append(result, validation.ValidateOneOf(string(undo.Type), []any{"Undo"}, "type")...) + result = append(result, validation.ValidateNotEmpty(undo.Type, "type")...) + result = append(result, validation.ValidateOneOf(undo.Type, []any{ap.UndoType}, "type")...) if undo.Actor == nil { result = append(result, "Actor should not be nil.") @@ -61,8 +61,8 @@ func (undo ForgeUndoLike) Validate() []string { } else if activity, ok := undo.Object.(*ap.Activity); !ok { result = append(result, "object is not of type Activity") } else { - result = append(result, validation.ValidateNotEmpty(string(activity.Type), "type")...) - result = append(result, validation.ValidateOneOf(string(activity.Type), []any{"Like"}, "type")...) + result = append(result, validation.ValidateNotEmpty(activity.Type, "type")...) + result = append(result, validation.ValidateOneOf(activity.Type, []any{ap.LikeType}, "type")...) if activity.Actor == nil { result = append(result, "Object.Actor should not be nil.") diff --git a/modules/forgefed/activity_undo_like_test.go b/modules/forgefed/activity_undo_like_test.go index 18db688c48..cbb309afc5 100644 --- a/modules/forgefed/activity_undo_like_test.go +++ b/modules/forgefed/activity_undo_like_test.go @@ -64,7 +64,7 @@ func Test_UndoLikeMarshalJSON(t *testing.T) { Activity: ap.Activity{ StartTime: startTime, Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"), - Type: "Undo", + Type: ap.UndoType, Object: like, }, }, @@ -117,7 +117,7 @@ func Test_UndoLikeUnmarshalJSON(t *testing.T) { Activity: ap.Activity{ StartTime: startTime, Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"), - Type: "Undo", + Type: ap.UndoType, Object: like, }, }, diff --git a/modules/forgefed/activity_user_activity.go b/modules/forgefed/activity_user_activity.go index 82353245c9..69e7bc3ead 100644 --- a/modules/forgefed/activity_user_activity.go +++ b/modules/forgefed/activity_user_activity.go @@ -60,8 +60,8 @@ func NewForgeUserActivity(doer *user_model.User, actionID int64, content string) func (userActivity ForgeUserActivity) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(string(userActivity.Type), "type")...) - result = append(result, validation.ValidateOneOf(string(userActivity.Type), []any{"Create"}, "type")...) + result = append(result, validation.ValidateNotEmpty(userActivity.Type, "type")...) + result = append(result, validation.ValidateOneOf(userActivity.Type, []any{ap.CreateType}, "type")...) result = append(result, validation.ValidateIDExists(userActivity.Actor, "actor")...) if len(userActivity.To) == 0 { diff --git a/modules/forgefed/activity_user_activity_test.go b/modules/forgefed/activity_user_activity_test.go index 49137c7ab4..106f0ac73c 100644 --- a/modules/forgefed/activity_user_activity_test.go +++ b/modules/forgefed/activity_user_activity_test.go @@ -15,17 +15,14 @@ import ( func Test_ForgeUserActivityValidation(t *testing.T) { note := forgefed.ForgeUserActivityNote{} - note.Type = "Note" + note.Type = ap.NoteType note.Content = ap.NaturalLanguageValues{ - { - Ref: ap.NilLangRef, - Value: ap.Content("Any Content!"), - }, + ap.NilLangRef: ap.Content("Any Content!"), } note.URL = ap.IRI("example.org/user-id/57") sut := forgefed.ForgeUserActivity{} - sut.Type = "Create" + sut.Type = ap.CreateType sut.Actor = ap.IRI("example.org/user-id/23") sut.CC = ap.ItemCollection{ ap.IRI("example.org/registration/public#2nd"), diff --git a/modules/forgefed/actor.go b/modules/forgefed/actor.go index 5383d5adaf..1f6e1f1fdf 100644 --- a/modules/forgefed/actor.go +++ b/modules/forgefed/actor.go @@ -6,6 +6,7 @@ package forgefed import ( "fmt" "net/url" + "slices" "strconv" "strings" @@ -107,12 +108,7 @@ func newActorID(uri string) (ActorID, error) { } func containsEmptyString(ar []string) bool { - for _, elem := range ar { - if elem == "" { - return true - } - } - return false + return slices.Contains(ar, "") } func removeEmptyStrings(ls []string) []string { diff --git a/modules/forgefed/actor_person.go b/modules/forgefed/actor_person.go index 7c43b0d7ce..aee7d3c37f 100644 --- a/modules/forgefed/actor_person.go +++ b/modules/forgefed/actor_person.go @@ -20,6 +20,8 @@ type PersonID struct { const ( personIDapiPathV1 = "api/v1/activitypub/user-id" personIDapiPathV1Latest = "api/activitypub/user-id" + actorIDapiPathV1 = "api/v1/activitypub" + actorIDapiPathLatest = "api/activitypub" ) // Factory function for PersonID. Created struct is asserted to be valid @@ -71,12 +73,13 @@ func (id PersonID) AsLoginName() string { return result } +// HostSuffix returns the host part of a handle, i.e. @host.tld (if port is supplemented) or @host.tld:1234 func (id PersonID) HostSuffix() string { var result string if !id.IsPortSupplemented { - result = fmt.Sprintf("-%s-%d", strings.ToLower(id.Host), id.HostPort) + result = fmt.Sprintf("@%s:%d", strings.ToLower(id.Host), id.HostPort) } else { - result = fmt.Sprintf("-%s", strings.ToLower(id.Host)) + result = fmt.Sprintf("@%s", strings.ToLower(id.Host)) } return result } @@ -87,8 +90,11 @@ func (id PersonID) Validate() []string { result = append(result, validation.ValidateOneOf(id.Source, []any{"forgejo", "gitea", "mastodon", "gotosocial"}, "Source")...) if id.Source == "forgejo" { result = append(result, validation.ValidateNotEmpty(id.Path, "path")...) - if strings.ToLower(id.Path) != personIDapiPathV1 && strings.ToLower(id.Path) != personIDapiPathV1Latest { - result = append(result, fmt.Sprintf("path: %q has to be a person specific api path", id.Path)) + lowerPath := strings.ToLower(id.Path) + if lowerPath != personIDapiPathV1 && lowerPath != personIDapiPathV1Latest { + if lowerPath != actorIDapiPathV1 && lowerPath != actorIDapiPathLatest || id.ID != "actor" { + result = append(result, fmt.Sprintf("path: %q has to be a person specific api path", id.Path)) + } } } @@ -114,8 +120,8 @@ func (s *ForgePerson) UnmarshalJSON(data []byte) error { func (s ForgePerson) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(string(s.Type), "Type")...) - result = append(result, validation.ValidateOneOf(string(s.Type), []any{string(ap.PersonType)}, "Type")...) + result = append(result, validation.ValidateNotEmpty(s.Type, "Type")...) + result = append(result, validation.ValidateOneOf(s.Type, []any{ap.PersonType}, "Type")...) result = append(result, validation.ValidateNotEmpty(s.PreferredUsername.String(), "PreferredUsername")...) return result diff --git a/modules/forgefed/actor_person_test.go b/modules/forgefed/actor_person_test.go index a5f3ee47b1..72ff976688 100644 --- a/modules/forgefed/actor_person_test.go +++ b/modules/forgefed/actor_person_test.go @@ -159,6 +159,48 @@ func TestPersonIdValidation(t *testing.T) { result, err = validation.IsValid(sut) assert.False(t, result) require.EqualError(t, err, "Validation Error: forgefed.PersonID: Field Source contains the value forgejox, which is not in allowed subset [forgejo gitea mastodon gotosocial]") + + sut = forgefed.PersonID{} + sut.ID = "actor" + sut.Source = "forgejo" + sut.HostSchema = "https" + sut.Path = "api/v1/activitypub" + sut.Host = "example.com" + sut.HostPort = 443 + sut.IsPortSupplemented = true + sut.UnvalidatedInput = "https://example.com/api/v1/activitypub/actor" + + result, err = validation.IsValid(sut) + assert.True(t, result) + require.NoError(t, err) + + sut = forgefed.PersonID{} + sut.ID = "actor" + sut.Source = "forgejo" + sut.HostSchema = "https" + sut.Path = "api/activitypub" + sut.Host = "example.com" + sut.HostPort = 443 + sut.IsPortSupplemented = true + sut.UnvalidatedInput = "https://example.com/api/activitypub/actor" + + result, err = validation.IsValid(sut) + assert.True(t, result) + require.NoError(t, err) + + sut = forgefed.PersonID{} + sut.ID = "1" + sut.Source = "forgejo" + sut.HostSchema = "https" + sut.Path = "api/v1/activitypub" + sut.Host = "example.com" + sut.HostPort = 443 + sut.IsPortSupplemented = true + sut.UnvalidatedInput = "https://example.com/api/v1/activitypub/1" + + result, err = validation.IsValid(sut) + assert.False(t, result) + require.EqualError(t, err, "Validation Error: forgefed.PersonID: path: \"api/v1/activitypub\" has to be a person specific api path") } func TestWebfingerId(t *testing.T) { @@ -194,9 +236,9 @@ func TestShouldThrowErrorOnInvalidInput(t *testing.T) { func Test_PersonMarshalJSON(t *testing.T) { sut := forgefed.ForgePerson{} - sut.Type = "Person" + sut.Type = ap.PersonType sut.PreferredUsername = ap.NaturalLanguageValuesNew() - sut.PreferredUsername.Set("en", ap.Content("MaxMuster")) + sut.PreferredUsername.Set(ap.English, ap.Content("MaxMuster")) result, _ := sut.MarshalJSON() assert.JSONEq(t, `{"type":"Person","preferredUsername":"MaxMuster"}`, string(result), "Expected string is not equal") } @@ -204,9 +246,9 @@ func Test_PersonMarshalJSON(t *testing.T) { func Test_PersonUnmarshalJSON(t *testing.T) { expected := &forgefed.ForgePerson{ Actor: ap.Actor{ - Type: "Person", + Type: ap.PersonType, PreferredUsername: ap.NaturalLanguageValues{ - ap.LangRefValue{Ref: "en", Value: []byte("MaxMuster")}, + ap.English: []byte("MaxMuster"), }, }, } @@ -246,8 +288,19 @@ func TestForgePersonValidation(t *testing.T) { func TestAsloginName(t *testing.T) { sut, _ := forgefed.NewPersonID("https://codeberg.org/api/v1/activitypub/user-id/12345", "forgejo") - assert.Equal(t, "12345-codeberg.org", sut.AsLoginName()) + assert.Equal(t, "12345@codeberg.org", sut.AsLoginName()) sut, _ = forgefed.NewPersonID("https://codeberg.org:443/api/v1/activitypub/user-id/12345", "forgejo") - assert.Equal(t, "12345-codeberg.org-443", sut.AsLoginName()) + assert.Equal(t, "12345@codeberg.org:443", sut.AsLoginName()) +} + +func TestHostSuffix(t *testing.T) { + sut, _ := forgefed.NewPersonID("https://codeberg.org/api/v1/activitypub/user-id/12345", "forgejo") + sut.Host = "forgejo.example.tld" + sut.HostPort = 80 + + // sut.IsPortSupplemented is true by default at time of writing. + assert.Equal(t, "@forgejo.example.tld", sut.HostSuffix()) + sut.IsPortSupplemented = false + assert.Equal(t, "@forgejo.example.tld:80", sut.HostSuffix()) } diff --git a/modules/forgefed/inbox.go b/modules/forgefed/inbox.go new file mode 100644 index 0000000000..c02aedd38c --- /dev/null +++ b/modules/forgefed/inbox.go @@ -0,0 +1,15 @@ +// Copyright 2024, 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgefed + +import ( + ap "github.com/go-ap/activitypub" +) + +// ForgeFollow activity data type +// swagger:model +type ForgeInbox struct { + // swagger:ignore + ap.InboxStream +} diff --git a/modules/forgefed/object_user_activity_note.go b/modules/forgefed/object_user_activity_note.go index 758c25aef8..b8e09ecf94 100644 --- a/modules/forgefed/object_user_activity_note.go +++ b/modules/forgefed/object_user_activity_note.go @@ -35,10 +35,7 @@ func newNote(doer *user_model.User, content, id string, published time.Time) (Fo note.Type = ap.NoteType note.AttributedTo = ap.IRI(doer.APActorID()) note.Content = ap.NaturalLanguageValues{ - { - Ref: ap.NilLangRef, - Value: ap.Content(content), - }, + ap.NilLangRef: ap.Content(content), } note.ID = ap.IRI(id) note.Published = published @@ -59,8 +56,8 @@ func newNote(doer *user_model.User, content, id string, published time.Time) (Fo func (note ForgeUserActivityNote) Validate() []string { var result []string - result = append(result, validation.ValidateNotEmpty(string(note.Type), "type")...) - result = append(result, validation.ValidateOneOf(string(note.Type), []any{"Note"}, "type")...) + result = append(result, validation.ValidateNotEmpty(note.Type, "type")...) + result = append(result, validation.ValidateOneOf(note.Type, []any{ap.NoteType}, "type")...) result = append(result, validation.ValidateNotEmpty(note.Content.String(), "content")...) result = append(result, validation.ValidateIDExists(note.URL, "url")...) diff --git a/modules/forgefed/object_user_activity_note_test.go b/modules/forgefed/object_user_activity_note_test.go index 4f790033bc..e559ce84f7 100644 --- a/modules/forgefed/object_user_activity_note_test.go +++ b/modules/forgefed/object_user_activity_note_test.go @@ -15,12 +15,9 @@ import ( func Test_UserActivityNoteValidation(t *testing.T) { sut := forgefed.ForgeUserActivityNote{} - sut.Type = "Note" + sut.Type = ap.NoteType sut.Content = ap.NaturalLanguageValues{ - { - Ref: ap.NilLangRef, - Value: ap.Content("Any Content!"), - }, + ap.NilLangRef: ap.Content("Any Content!"), } sut.URL = ap.IRI("example.org/user-id/57") diff --git a/modules/forgefed/repository.go b/modules/forgefed/repository.go index 63680ccd35..1e85d1e64c 100644 --- a/modules/forgefed/repository.go +++ b/modules/forgefed/repository.go @@ -88,7 +88,7 @@ func ToRepository(it ap.Item) (*Repository, error) { return (*Repository)(unsafe.Pointer(&i)), nil default: // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes - typ := reflect.TypeOf(new(Repository)) + typ := reflect.TypeFor[*Repository]() if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Repository); ok { return i, nil } diff --git a/modules/generate/generate.go b/modules/generate/generate.go index 2df808fe9e..0dd5d071de 100644 --- a/modules/generate/generate.go +++ b/modules/generate/generate.go @@ -37,7 +37,7 @@ func DecodeJwtSecret(src string) ([]byte, error) { encoding := base64.RawURLEncoding decoded := make([]byte, encoding.DecodedLen(len(src))+3) if n, err := encoding.Decode(decoded, []byte(src)); err != nil { - return nil, err + return nil, fmt.Errorf("JwtSecret decode failed: %v", err) } else if n != defaultJwtSecretLen { return nil, fmt.Errorf("invalid base64 decoded length: %d, expects: %d", n, defaultJwtSecretLen) } diff --git a/modules/git/batch_reader.go b/modules/git/batch_reader.go index 1297c7247f..a8cd626d81 100644 --- a/modules/git/batch_reader.go +++ b/modules/git/batch_reader.go @@ -273,7 +273,7 @@ func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBu idx := bytes.IndexByte(readBytes, ' ') if idx < 0 { log.Debug("missing space in readBytes ParseTreeLine: %s", readBytes) - return mode, fname, sha, n, &ErrNotExist{} + return mode, fname, sha, n, ErrNotExist{} } n += idx + 1 diff --git a/modules/git/blame.go b/modules/git/blame.go index 868edab2b8..881ef04302 100644 --- a/modules/git/blame.go +++ b/modules/git/blame.go @@ -207,5 +207,5 @@ func tryCreateBlameIgnoreRevsFile(commit *Commit) *string { return nil } - return util.ToPointer(f.Name()) + return new(f.Name()) } diff --git a/modules/git/command.go b/modules/git/command.go index bf1d624dbf..4ab081418b 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "net/url" "os" "os/exec" "runtime/trace" @@ -446,6 +447,54 @@ func (c *Command) RunStdBytes(opts *RunOpts) (stdout, stderr []byte, runErr RunS return stdoutBuf.Bytes(), stderr, nil } +// If `remoteURL` is a URL with a password in it, add parameters to the git command that will read that password from a +// credential store file, and return the URL that should be used in the command instead of the original, and a cleanup +// function to call to remove the credential file. If `remoteURL` doesn't have a password, then it is returned as-is. +// This function must be invoked on the the git command before the git sub-command -- eg. before the `clone` or `fetch` +// parameter is added to the command's args. +func (c *Command) AddAuthCredentialHelperForRemote(remoteURL string) (commandURL string, cleanup func(), err error) { + parsedFromURL, _ := url.Parse(remoteURL) + + // If the clone URL has credentials, build a credential file for usage by git-credential-store + // to prevent credential leak in the process list. + // https://git-scm.com/docs/git-credential-store#_storage_format + // credential.helper adjustment must be set before the git subcommand + if strings.Contains(remoteURL, "://") && strings.Contains(remoteURL, "@") && parsedFromURL != nil { + credentialsFile, err := os.CreateTemp("", "forgejo-clone-credentials-") + if err != nil { + return "", nil, err + } + credentialsPath := credentialsFile.Name() + + cleanup := func() { + _ = credentialsFile.Close() + if err := util.Remove(credentialsPath); err != nil { + log.Warn("Unable to remove temporary file %q: %v", credentialsPath, err) + } + } + _, err = credentialsFile.Write([]byte(parsedFromURL.String())) + if err != nil { + cleanup() + return "", nil, err + } + err = credentialsFile.Close() + if err != nil { + cleanup() + return "", nil, err + } + + c.AddArguments("-c").AddDynamicArguments("credential.helper=store --file=" + credentialsPath) + + // remove the password from the URL argument + parsedFromURL.User = url.User(parsedFromURL.User.Username()) + commandURL = parsedFromURL.String() + + return commandURL, cleanup, nil + } + + return remoteURL, func() {}, nil +} + // AllowLFSFiltersArgs return globalCommandArgs with lfs filter, it should only be used for tests func AllowLFSFiltersArgs() TrustedCmdArgs { // Now here we should explicitly allow lfs filters to run diff --git a/modules/git/commit.go b/modules/git/commit.go index 4fb13ecd4f..36ba8ef8ca 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -269,8 +269,8 @@ func NewSearchCommitsOptions(searchString string, forAllRefs bool) SearchCommits var keywords, authors, committers []string var after, before string - fields := strings.Fields(searchString) - for _, k := range fields { + fields := strings.FieldsSeq(searchString) + for k := range fields { switch { case strings.HasPrefix(k, "author:"): authors = append(authors, strings.TrimPrefix(k, "author:")) diff --git a/modules/git/commit_info.go b/modules/git/commit_info.go index 6511a1689a..62f58f8767 100644 --- a/modules/git/commit_info.go +++ b/modules/git/commit_info.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "io" + "maps" "path" "sort" @@ -45,9 +46,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath return nil, nil, err } - for pth, found := range commits { - revs[pth] = found - } + maps.Copy(revs, commits) } } else { sort.Strings(entryPaths) diff --git a/modules/git/diff.go b/modules/git/diff.go index 0ba9c60912..c954a933ba 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -7,6 +7,7 @@ import ( "bufio" "bytes" "context" + "errors" "fmt" "io" "os" @@ -100,7 +101,7 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } // ParseDiffHunkString parse the diffhunk content and return -func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHunk int) { +func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, rightHunk int) { ss := strings.Split(diffhunk, "@@") ranges := strings.Split(ss[1][1:], " ") leftRange := strings.Split(ranges[0], ",") @@ -112,14 +113,14 @@ func ParseDiffHunkString(diffhunk string) (leftLine, leftHunk, rightLine, righHu rightRange := strings.Split(ranges[1], ",") rightLine, _ = strconv.Atoi(rightRange[0]) if len(rightRange) > 1 { - righHunk, _ = strconv.Atoi(rightRange[1]) + rightHunk, _ = strconv.Atoi(rightRange[1]) } } else { log.Debug("Parse line number failed: %v", diffhunk) rightLine = leftLine - righHunk = leftHunk + rightHunk = leftHunk } - return leftLine, leftHunk, rightLine, righHunk + return leftLine, leftHunk, rightLine, rightHunk } // Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9] @@ -221,6 +222,7 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi } case '\\': // FIXME: handle `\ No newline at end of file` + break default: currentLine++ otherLine++ @@ -276,6 +278,76 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi return strings.Join(newHunk, "\n"), nil } +var ErrLineNotFound = errors.New("line not found in diff") + +type LinePlacement struct { + Left int64 + Right int64 +} + +// Find the line of code where an old line of code from an old patch is, if present, in a new patch. Given a cutDiff +// (from CutDiffAroundLine) and the line of code that it was cut from, and, given a single-file diff from the commit +// where that patch came into a new head, this routine will read through the diff and identify the new line number. It +// will only return successful if the line is exactly the same as the original line, but just placed in a new location +// due to added or removed lines in the diff before the target line of code. +func FindAdjustedLineNumber(cutDiff string, originalLine int64, fullDiff io.Reader) (LinePlacement, error) { + cutDiffSplit := strings.Split(cutDiff, "\n") + if len(cutDiffSplit) == 0 { + return LinePlacement{}, errors.New("cutDiff has no contents") + } + endOfCutDiff := cutDiffSplit[len(cutDiffSplit)-1] + + scanner := bufio.NewScanner(fullDiff) + inHunk := false // used to skip header lines before the first hunk + leftLine := int64(-1) + rightLine := int64(-1) + + for scanner.Scan() { + lineText := scanner.Text() + if strings.HasPrefix(lineText, "@@") { + // A map with named groups of our regex to recognize them later more easily + submatches := hunkRegex.FindStringSubmatch(lineText) + groups := make(map[string]string) + for i, name := range hunkRegex.SubexpNames() { + if i != 0 && name != "" { + groups[name] = submatches[i] + } + } + beginLeft, _ := strconv.ParseInt(groups["beginOld"], 10, 64) + beginRight, _ := strconv.ParseInt(groups["beginNew"], 10, 64) + leftLine = beginLeft + rightLine = beginRight + inHunk = true + } else if inHunk { + if leftLine == originalLine { + if lineText != endOfCutDiff { + return LinePlacement{}, fmt.Errorf( + "line was adjusted from index %d to %d, but contents changed from %q to %q: %w", + originalLine, leftLine, endOfCutDiff, lineText, ErrLineNotFound) + } + return LinePlacement{Left: leftLine, Right: rightLine}, nil + } + switch lineText[0] { + case '+': + rightLine++ + case '-': + leftLine++ + case '\\': + // Should be the end-of-file with "\ No newline at end of file" -- nothing to do here. + break + default: + rightLine++ + leftLine++ + } + } + } + if err := scanner.Err(); err != nil { + return LinePlacement{}, err + } + + return LinePlacement{}, fmt.Errorf("line is no longer in diff: %w", ErrLineNotFound) +} + // GetAffectedFiles returns the affected files between two commits func GetAffectedFiles(repo *Repository, oldCommitID, newCommitID string, env []string) ([]string, error) { objectFormat, err := repo.GetObjectFormat() diff --git a/modules/git/diff_compare_test.go b/modules/git/diff_compare_test.go index 433497b5c4..34c2017b8a 100644 --- a/modules/git/diff_compare_test.go +++ b/modules/git/diff_compare_test.go @@ -247,7 +247,7 @@ func TestCheckIfDiffDiffers(t *testing.T) { require.NoError(t, NewCommand(t.Context(), "switch", "-c", "e-2").Run(&RunOpts{Dir: tmpDir})) require.NoError(t, NewCommand(t.Context(), "rebase", "main-D-2").Run(&RunOpts{Dir: tmpDir})) - // The diff changed, because it no longers shows the change made to `README`. + // The diff changed, because it no longer shows the change made to `README`. changed, err := gitRepo.CheckIfDiffDiffers("main-D-2", "e-1", "e-2", nil) require.NoError(t, err) assert.False(t, changed) // This should be true. diff --git a/modules/git/diff_test.go b/modules/git/diff_test.go index 9130767c66..e4b7ce6ace 100644 --- a/modules/git/diff_test.go +++ b/modules/git/diff_test.go @@ -30,7 +30,7 @@ index d8e4c92..19dc8ad 100644 @@ -1,9 +1,10 @@ --some comment --- some comment 5 -+--some coment 2 ++--some comment 2 +-- some comment 3 create or replace procedure test(p1 varchar2) is @@ -135,7 +135,7 @@ func TestCutDiffAroundLine(t *testing.T) { @@ -1,9 +1,10 @@ --some comment --- some comment 5 -+--some coment 2` ++--some comment 2` assert.Equal(t, expected, minusDiff) // Handle minus diffs properly @@ -148,7 +148,7 @@ func TestCutDiffAroundLine(t *testing.T) { @@ -1,9 +1,10 @@ --some comment --- some comment 5 -+--some coment 2 ++--some comment 2 +-- some comment 3` assert.Equal(t, expected, minusDiff) @@ -167,3 +167,215 @@ func TestParseDiffHunkString(t *testing.T) { assert.Equal(t, 19, rightLine) assert.Equal(t, 5, rightHunk) } + +func TestFindAdjustedLineNumber(t *testing.T) { + commentCutDiff := `diff --git a/file1.md b/file1.md +--- a/file1.md ++++ b/file1.md +@@ -47,7 +47,6 @@ Line 46 + Line 47 + Line 48 + Line 49 +-Line 50` + + t.Run("no additional changes", func(t *testing.T) { + diff := `diff --git a/file1.md b/file1.md +index 2d203fb..b21df3f 100644 +--- a/file1.md ++++ b/file1.md +@@ -47,7 +47,6 @@ Line 46 + Line 47 + Line 48 + Line 49 +-Line 50 + Line 51 + Line 52 + Line 53` + lineNumber, err := FindAdjustedLineNumber(commentCutDiff, 50, strings.NewReader(diff)) + require.NoError(t, err) + assert.Equal(t, LinePlacement{Left: 50, Right: 50}, lineNumber) + }) + + t.Run("removed lines before location", func(t *testing.T) { + diff := `diff --git a/file1.md b/file1.md +index 2d203fb..c85b903 100644 +--- a/file1.md ++++ b/file1.md +@@ -1,13 +1,3 @@ +-Line 1 +-Line 2 +-Line 3 +-Line 4 +-Line 5 +-Line 6 +-Line 7 +-Line 8 +-Line 9 +-Line 10 + Line 11 + Line 12 + Line 13 +@@ -47,7 +37,6 @@ Line 46 + Line 47 + Line 48 + Line 49 +-Line 50 + Line 51 + Line 52 + Line 53` + lineNumber, err := FindAdjustedLineNumber(commentCutDiff, 50, strings.NewReader(diff)) + require.NoError(t, err) + assert.Equal(t, LinePlacement{Left: 50, Right: 40}, lineNumber) + }) + + t.Run("added lines before location", func(t *testing.T) { + diff := `diff --git a/file1.md b/file1.md +index 2d203fb..24b1aa6 100644 +--- a/file1.md ++++ b/file1.md +@@ -8,6 +8,11 @@ Line 7 + Line 8 + Line 9 + Line 10 ++Line 10.1 ++Line 10.2 ++Line 10.3 ++Line 10.4 ++Line 10.5 + Line 11 + Line 12 + Line 13 +@@ -47,7 +52,6 @@ Line 46 + Line 47 + Line 48 + Line 49 +-Line 50 + Line 51 + Line 52 + Line 53` + lineNumber, err := FindAdjustedLineNumber(commentCutDiff, 50, strings.NewReader(diff)) + require.NoError(t, err) + assert.Equal(t, LinePlacement{Left: 50, Right: 55}, lineNumber) + }) + + t.Run("added and removed in lines before location", func(t *testing.T) { + diff := `diff --git a/file1.md b/file1.md +index 2d203fb..d0cb63f 100644 +--- a/file1.md ++++ b/file1.md +@@ -5,9 +5,11 @@ Line 4 + Line 5 + Line 6 + Line 7 +-Line 8 +-Line 9 +-Line 10 ++Line 10.1 ++Line 10.2 ++Line 10.3 ++Line 10.4 ++Line 10.5 + Line 11 + Line 12 + Line 13 +@@ -47,7 +49,6 @@ Line 46 + Line 47 + Line 48 + Line 49 +-Line 50 + Line 51 + Line 52 + Line 53` + lineNumber, err := FindAdjustedLineNumber(commentCutDiff, 50, strings.NewReader(diff)) + require.NoError(t, err) + assert.Equal(t, LinePlacement{Left: 50, Right: 52}, lineNumber) + }) + + t.Run("changes above in the same hunk", func(t *testing.T) { + diff := `diff --git a/file1.md b/file1.md +index 2d203fb..f35a466 100644 +--- a/file1.md ++++ b/file1.md +@@ -42,12 +42,6 @@ Line 41 + Line 42 + Line 43 + Line 44 +-Line 45 +-Line 46 +-Line 47 +-Line 48 +-Line 49 +-Line 50 + Line 51 + Line 52 + Line 53` + lineNumber, err := FindAdjustedLineNumber(commentCutDiff, 50, strings.NewReader(diff)) + require.NoError(t, err) + assert.Equal(t, LinePlacement{Left: 50, Right: 45}, lineNumber) + }) + + t.Run("first line in diff", func(t *testing.T) { + commentCutDiff := `diff --git a/file1.md b/file1.md +--- a/file1.md ++++ b/file1.md +@@ -1,4 +1,3 @@ +-Line 1` + diff := `diff --git a/file1.md b/file1.md +index 2d203fb..a490028 100644 +--- a/file1.md ++++ b/file1.md +@@ -1,4 +1,3 @@ +-Line 1 + Line 2 + Line 3 + Line 4` + lineNumber, err := FindAdjustedLineNumber(commentCutDiff, 1, strings.NewReader(diff)) + require.NoError(t, err) + assert.Equal(t, LinePlacement{Left: 1, Right: 1}, lineNumber) + }) + + t.Run("adjusted line not found", func(t *testing.T) { + // "Line 50" is present here but it's no longer "-Line 50", so it should not be identified as present + diff := `diff --git a/file1.md b/file1.md +index 2d203fb..09dd95a 100644 +--- a/file1.md ++++ b/file1.md +@@ -42,10 +42,6 @@ Line 41 + Line 42 + Line 43 + Line 44 +-Line 45 +-Line 46 +-Line 47 +-Line 48 + Line 49 + Line 50 + Line 51` + _, err := FindAdjustedLineNumber(commentCutDiff, 50, strings.NewReader(diff)) + require.ErrorIs(t, err, ErrLineNotFound) + }) + + t.Run("adjusted line hunk not present - not changed anymore", func(t *testing.T) { + diff := `diff --git a/file1.md b/file1.md +index 2d203fb..d0cb63f 100644 +--- a/file1.md ++++ b/file1.md +@@ -5,9 +5,11 @@ Line 4 + Line 5 + Line 6 + Line 7 +-Line 8 +-Line 9 +-Line 10 ++Line 10.1 ++Line 10.2 ++Line 10.3 ++Line 10.4 ++Line 10.5 + Line 11 + Line 12 + Line 13` + _, err := FindAdjustedLineNumber(commentCutDiff, 50, strings.NewReader(diff)) + require.ErrorIs(t, err, ErrLineNotFound) + }) +} diff --git a/modules/git/fetch_test.go b/modules/git/fetch_test.go index 95a1fa387d..b7ead10d4a 100644 --- a/modules/git/fetch_test.go +++ b/modules/git/fetch_test.go @@ -26,7 +26,7 @@ func TestFetch(t *testing.T) { fetchedCommitID, err := repo.Fetch(otherRepoPath, "refs/heads/master") require.NoError(t, err) - assert.Equal(t, "95d3505f2db273e40be79f84416051ae85e9ea0d", fetchedCommitID) + assert.Equal(t, "5684d0c8cfdfb17fcd59101826efc9ff54b80df4", fetchedCommitID) c, err := repo.getCommit(MustIDFromString(fetchedCommitID)) require.NoError(t, err) diff --git a/modules/git/foreachref/format.go b/modules/git/foreachref/format.go index 97e8ee4724..87c1c9a4ff 100644 --- a/modules/git/foreachref/format.go +++ b/modules/git/foreachref/format.go @@ -53,7 +53,7 @@ func (f Format) Flag() string { var formatFlag strings.Builder for i, field := range f.fieldNames { // field key and field value - formatFlag.WriteString(fmt.Sprintf("%s %%(%s)", field, field)) + fmt.Fprintf(&formatFlag, "%s %%(%s)", field, field) if i < len(f.fieldNames)-1 { // note: escape delimiters to allow control characters as @@ -72,12 +72,12 @@ func (f Format) Parser(r io.Reader) *Parser { return NewParser(r, f) } -// hexEscaped produces hex-escpaed characters from a string. For example, "\n\0" +// hexEscaped produces hex-escaped characters from a string. For example, "\n\0" // would turn into "%0a%00". func (f Format) hexEscaped(delim []byte) string { - escaped := "" - for i := 0; i < len(delim); i++ { - escaped += "%" + hex.EncodeToString([]byte{delim[i]}) + var escaped strings.Builder + for i := range delim { + escaped.WriteString("%" + hex.EncodeToString([]byte{delim[i]})) } - return escaped + return escaped.String() } diff --git a/modules/git/git.go b/modules/git/git.go index 650fd3b5af..ea3e2a1320 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -10,6 +10,7 @@ import ( "fmt" "os" "os/exec" + "path" "path/filepath" "regexp" "runtime" @@ -200,6 +201,11 @@ func InitFull(ctx context.Context) (err error) { _, err = exec.LookPath("ssh") HasSSHExecutable = err == nil + err = InitDelegateHooks(HomeDir()) + if err != nil { + return err + } + return syncGitConfig() } @@ -229,6 +235,10 @@ func syncGitConfig() (err error) { } } + if err := configSet("core.hooksPath", path.Join(HomeDir(), "hooks")); err != nil { + return err + } + // Set git some configurations - these must be set to these values for forgejo to work correctly if err := configSet("core.quotePath", "false"); err != nil { return err diff --git a/modules/git/grep_test.go b/modules/git/grep_test.go index 83ddb766af..5666a425f4 100644 --- a/modules/git/grep_test.go +++ b/modules/git/grep_test.go @@ -65,7 +65,7 @@ func TestGrepSearch(t *testing.T) { return } - res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{MatchesPerFile: 1}) + res, err = GrepSearch(t.Context(), repo, "world", GrepOptions{RefName: "95d3505f2db273e40be79f84416051ae85e9ea0d", MatchesPerFile: 1}) require.NoError(t, err) assert.Equal(t, []*GrepResult{ { diff --git a/modules/git/hook.go b/modules/git/hook.go index bef4d024c8..3b650fe9db 100644 --- a/modules/git/hook.go +++ b/modules/git/hook.go @@ -9,6 +9,7 @@ import ( "os" "path" "path/filepath" + "slices" "strings" "forgejo.org/modules/log" @@ -27,12 +28,7 @@ var ErrNotValidHook = errors.New("not a valid Git hook") // IsValidHookName returns true if given name is a valid Git hook. func IsValidHookName(name string) bool { - for _, hn := range hookNames { - if hn == name { - return true - } - } - return false + return slices.Contains(hookNames, name) } // Hook represents a Git hook. diff --git a/modules/repository/hooks.go b/modules/git/hook_generate.go similarity index 60% rename from modules/repository/hooks.go rename to modules/git/hook_generate.go index 0f5e3afc34..2af2487ab3 100644 --- a/modules/repository/hooks.go +++ b/modules/git/hook_generate.go @@ -1,7 +1,7 @@ // Copyright 2020 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package repository +package git import ( "fmt" @@ -21,14 +21,25 @@ func getHookTemplates() (hookNames, hookTpls, giteaHookTpls []string) { data=$(cat) exitcodes="" hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)/..} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do +for hook in $(dirname $0)/${hookname}.d/*; do test -x "${hook}" && test -f "${hook}" || continue echo "${data}" | "${hook}" exitcodes="${exitcodes} $?" done +# Custom hooks +custom_hooks_dir="./hooks/${hookname}.d" +if [ -d "${custom_hooks_dir}" ]; then + for hook in ${custom_hooks_dir}/*; do + if [ $(basename "${hook}") != "gitea" ]; then + test -x "${hook}" && test -f "${hook}" || continue + echo "${data}" | "${hook}" + exitcodes="${exitcodes} $?" + fi + done +fi + for i in ${exitcodes}; do [ ${i} -eq 0 ] || exit ${i} done @@ -39,14 +50,25 @@ done # AUTO GENERATED BY GITEA, DO NOT MODIFY exitcodes="" hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0/..)} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do +for hook in $(dirname $0)/${hookname}.d/*; do test -x "${hook}" && test -f "${hook}" || continue "${hook}" $1 $2 $3 exitcodes="${exitcodes} $?" done +# Custom hooks +custom_hooks_dir="./hooks/${hookname}.d" +if [ -d "${custom_hooks_dir}" ]; then + for hook in ${custom_hooks_dir}/*; do + if [ $(basename "${hook}") != "gitea" ]; then + test -x "${hook}" && test -f "${hook}" || continue + "${hook}" $1 $2 $3 + exitcodes="${exitcodes} $?" + fi + done +fi + for i in ${exitcodes}; do [ ${i} -eq 0 ] || exit ${i} done @@ -58,14 +80,24 @@ done data=$(cat) exitcodes="" hookname=$(basename $0) -GIT_DIR=${GIT_DIR:-$(dirname $0)/..} -for hook in ${GIT_DIR}/hooks/${hookname}.d/*; do +for hook in $(dirname $0)/${hookname}.d/*; do test -x "${hook}" && test -f "${hook}" || continue echo "${data}" | "${hook}" exitcodes="${exitcodes} $?" done +# Custom hooks +custom_hooks_dir="./hooks/${hookname}.d" +if [ -d "${custom_hooks_dir}" ]; then + for hook in ${custom_hooks_dir}/*; do + if [ $(basename "${hook}") != "gitea" ]; then + test -x "${hook}" && test -f "${hook}" || continue + echo "${data}" | "${hook}" + exitcodes="${exitcodes} $?" + fi + done +fi for i in ${exitcodes}; do [ ${i} -eq 0 ] || exit ${i} done @@ -104,10 +136,9 @@ done return hookNames, hookTpls, giteaHookTpls } -// CreateDelegateHooks creates all the hooks scripts for the repo -func CreateDelegateHooks(repoPath string) (err error) { +func InitDelegateHooks(path string) (err error) { hookNames, hookTpls, giteaHookTpls := getHookTemplates() - hookDir := filepath.Join(repoPath, "hooks") + hookDir := filepath.Join(path, "hooks") for i, hookName := range hookNames { oldHookPath := filepath.Join(hookDir, hookName) @@ -144,14 +175,6 @@ func CreateDelegateHooks(repoPath string) (err error) { return nil } -func checkExecutable(filename string) bool { - fileInfo, err := os.Stat(filename) - if err != nil { - return false - } - return (fileInfo.Mode() & 0o100) > 0 -} - func ensureExecutable(filename string) error { fileInfo, err := os.Stat(filename) if err != nil { @@ -163,66 +186,3 @@ func ensureExecutable(filename string) error { mode := fileInfo.Mode() | 0o100 return os.Chmod(filename, mode) } - -// CheckDelegateHooks checks the hooks scripts for the repo -func CheckDelegateHooks(repoPath string) ([]string, error) { - hookNames, hookTpls, giteaHookTpls := getHookTemplates() - - hookDir := filepath.Join(repoPath, "hooks") - results := make([]string, 0, 10) - - for i, hookName := range hookNames { - oldHookPath := filepath.Join(hookDir, hookName) - newHookPath := filepath.Join(hookDir, hookName+".d", "gitea") - - cont := false - isExist, err := util.IsExist(oldHookPath) - if err != nil { - results = append(results, fmt.Sprintf("unable to check if %s exists. Error: %v", oldHookPath, err)) - } - if err == nil && !isExist { - results = append(results, fmt.Sprintf("old hook file %s does not exist", oldHookPath)) - cont = true - } - isExist, err = util.IsExist(oldHookPath + ".d") - if err != nil { - results = append(results, fmt.Sprintf("unable to check if %s exists. Error: %v", oldHookPath+".d", err)) - } - if err == nil && !isExist { - results = append(results, fmt.Sprintf("hooks directory %s does not exist", oldHookPath+".d")) - cont = true - } - isExist, err = util.IsExist(newHookPath) - if err != nil { - results = append(results, fmt.Sprintf("unable to check if %s exists. Error: %v", newHookPath, err)) - } - if err == nil && !isExist { - results = append(results, fmt.Sprintf("new hook file %s does not exist", newHookPath)) - cont = true - } - if cont { - continue - } - contents, err := os.ReadFile(oldHookPath) - if err != nil { - return results, err - } - if string(contents) != hookTpls[i] { - results = append(results, fmt.Sprintf("old hook file %s is out of date", oldHookPath)) - } - if !checkExecutable(oldHookPath) { - results = append(results, fmt.Sprintf("old hook file %s is not executable", oldHookPath)) - } - contents, err = os.ReadFile(newHookPath) - if err != nil { - return results, err - } - if string(contents) != giteaHookTpls[i] { - results = append(results, fmt.Sprintf("new hook file %s is out of date", newHookPath)) - } - if !checkExecutable(newHookPath) { - results = append(results, fmt.Sprintf("new hook file %s is not executable", newHookPath)) - } - } - return results, nil -} diff --git a/modules/git/last_commit_cache.go b/modules/git/last_commit_cache.go index 1d7e74a0d7..9b49a18aaa 100644 --- a/modules/git/last_commit_cache.go +++ b/modules/git/last_commit_cache.go @@ -21,7 +21,7 @@ type Cache interface { } func getCacheKey(repoPath, commitID, entryPath string) string { - hashBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%s", repoPath, commitID, entryPath))) + hashBytes := sha256.Sum256(fmt.Appendf(nil, "%s:%s:%s", repoPath, commitID, entryPath)) return fmt.Sprintf("last_commit:%x", hashBytes) } diff --git a/modules/git/log_name_status.go b/modules/git/log_name_status.go index 50786e7a42..800e83c4a4 100644 --- a/modules/git/log_name_status.go +++ b/modules/git/log_name_status.go @@ -346,10 +346,7 @@ func WalkGitLog(ctx context.Context, repo *Repository, head *Commit, treepath st results := make([]string, len(paths)) remaining := len(paths) - nextRestart := (len(paths) * 3) / 4 - if nextRestart > 70 { - nextRestart = 70 - } + nextRestart := min((len(paths)*3)/4, 70) lastEmptyParent := head.ID.String() commitSinceLastEmptyParent := uint64(0) commitSinceNextRestart := uint64(0) diff --git a/modules/git/notes.go b/modules/git/notes.go index a52314bdd7..1bc68b6366 100644 --- a/modules/git/notes.go +++ b/modules/git/notes.go @@ -8,6 +8,7 @@ import ( "context" "io" "os" + "strings" "forgejo.org/modules/log" ) @@ -33,7 +34,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string) (*Note, err return nil, err } - path := "" + var path strings.Builder tree := ¬es.Tree log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", tree.ID, commitID) @@ -43,12 +44,12 @@ func GetNote(ctx context.Context, repo *Repository, commitID string) (*Note, err for len(commitID) > 2 { entry, err = tree.GetTreeEntryByPath(commitID) if err == nil { - path += commitID + path.WriteString(commitID) break } if IsErrNotExist(err) { tree, err = tree.SubTree(commitID[0:2]) - path += commitID[0:2] + "/" + path.WriteString(commitID[0:2] + "/") commitID = commitID[2:] } if err != nil { @@ -80,9 +81,9 @@ func GetNote(ctx context.Context, repo *Repository, commitID string) (*Note, err _ = dataRc.Close() closed = true - lastCommit, err := repo.getCommitByPathWithID(notes.ID, path) + lastCommit, err := repo.getCommitByPathWithID(notes.ID, path.String()) if err != nil { - log.Error("Unable to get the commit for the path %q. Error: %v", path, err) + log.Error("Unable to get the commit for the path %q. Error: %v", path.String(), err) return nil, err } diff --git a/modules/git/parse.go b/modules/git/parse.go index c7b84d7198..d2d70d4cfa 100644 --- a/modules/git/parse.go +++ b/modules/git/parse.go @@ -33,16 +33,16 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { posEnd += pos } line := data[pos:posEnd] - posTab := bytes.IndexByte(line, '\t') - if posTab == -1 { + before, after, ok := bytes.Cut(line, []byte{'\t'}) + if !ok { return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line) } entry := new(TreeEntry) entry.ptree = ptree - entryAttrs := line[:posTab] - entryName := line[posTab+1:] + entryAttrs := before + entryName := after entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace) _ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type diff --git a/modules/git/pushoptions/pushoptions.go b/modules/git/pushoptions/pushoptions.go index e96ba0a339..14e2c5d283 100644 --- a/modules/git/pushoptions/pushoptions.go +++ b/modules/git/pushoptions/pushoptions.go @@ -52,7 +52,7 @@ func NewFromMap(o *map[string]string) Interface { func (o *gitPushOptions) ReadEnv() Interface { if pushCount, err := strconv.Atoi(os.Getenv(EnvCount)); err == nil { - for idx := 0; idx < pushCount; idx++ { + for idx := range pushCount { _ = o.Parse(os.Getenv(fmt.Sprintf(EnvFormat, idx))) } } @@ -65,12 +65,8 @@ func (o *gitPushOptions) Parse(data string) bool { value = "true" } switch Key(key) { - case RepoPrivate: - case RepoTemplate: - case AgitTopic: - case AgitForcePush: - case AgitTitle: - case AgitDescription: + case RepoPrivate, RepoTemplate, AgitTopic, AgitForcePush, AgitTitle, AgitDescription: + break default: return false } diff --git a/modules/git/ref.go b/modules/git/ref.go index 1475d4dc5a..fdccd2b2e2 100644 --- a/modules/git/ref.go +++ b/modules/git/ref.go @@ -105,8 +105,8 @@ func (ref RefName) IsFor() bool { } func (ref RefName) nameWithoutPrefix(prefix string) string { - if strings.HasPrefix(string(ref), prefix) { - return strings.TrimPrefix(string(ref), prefix) + if after, ok := strings.CutPrefix(string(ref), prefix); ok { + return after } return "" } diff --git a/modules/git/repo.go b/modules/git/repo.go index 21845d9b55..8f9b95f1f2 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -18,7 +18,6 @@ import ( "strings" "time" - "forgejo.org/modules/log" "forgejo.org/modules/proxy" "forgejo.org/modules/setting" "forgejo.org/modules/util" @@ -46,9 +45,9 @@ func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, erro return commits, nil } - parts := bytes.Split(logs, []byte{'\n'}) + parts := bytes.SplitSeq(logs, []byte{'\n'}) - for _, commitID := range parts { + for commitID := range parts { commit, err := repo.GetCommit(string(commitID)) if err != nil { return nil, err @@ -141,44 +140,13 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op envs = proxy.EnvWithProxy(parsedFromURL) } - fromURL := from - sanitizedFrom := from + sanitizedFrom := util.SanitizeCredentialURLs(from) - // If the clone URL has credentials, build a credential file for usage by git-credential-store - // to prevent credential leak in the process list. - // https://git-scm.com/docs/git-credential-store#_storage_format - // credential.helper adjustment must be set before the git subcommand - if strings.Contains(from, "://") && strings.Contains(from, "@") { - sanitizedFrom = util.SanitizeCredentialURLs(from) - if parsedFromURL != nil { - credentialsFile, err := os.CreateTemp("", "forgejo-clone-credentials-") - if err != nil { - return err - } - credentialsPath := credentialsFile.Name() - - defer func() { - _ = credentialsFile.Close() - if err := util.Remove(credentialsPath); err != nil { - log.Warn("Unable to remove temporary file %q: %v", credentialsPath, err) - } - }() - _, err = credentialsFile.Write([]byte(parsedFromURL.String())) - if err != nil { - return err - } - err = credentialsFile.Close() - if err != nil { - return err - } - - cmd.AddArguments("-c").AddDynamicArguments("credential.helper=store --file=" + credentialsPath) - - // remove the password from the URL argument - parsedFromURL.User = url.User(parsedFromURL.User.Username()) - fromURL = parsedFromURL.String() - } + fromURL, cleanup, err := cmd.AddAuthCredentialHelperForRemote(from) + if err != nil { + return err } + defer cleanup() cmd.AddArguments("clone") diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index 2b07513162..56a86bde14 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -96,8 +96,8 @@ func (ca GitAttribute) String() string { // sometimes used within gitlab-language: https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type func (ca GitAttribute) Prefix() string { s := ca.String() - if i := strings.IndexByte(s, '?'); i >= 0 { - return s[:i] + if before, _, ok := strings.Cut(s, "?"); ok { + return before } return s } diff --git a/modules/git/repo_blame.go b/modules/git/repo_blame.go index 50f4c572d5..d760898ac6 100644 --- a/modules/git/repo_blame.go +++ b/modules/git/repo_blame.go @@ -65,3 +65,97 @@ func (repo *Repository) LineBlame(revision, file string, line uint64) (*Commit, return commit, originalLineNo, nil } + +type ReverseLineBlame struct { + CommitID string + LineNumber uint64 + FilePath string +} + +// Reverses the effect of [LineBlame]. If a file was modified at originalLine number in originalRevision, +// ReverseLineBlame will identify the last commit up-to-and-including currentRevision where that line exists, including +// its new path and line number. If the returned commit is not the same as currentRevision, then it indicates that +// content can no longer be located in currentRevision, and the returned commit is the last commit that had it. +func (repo *Repository) ReverseLineBlame(originalRevision, file string, originalLine uint64, currentRevision string) (*ReverseLineBlame, error) { + if originalRevision == currentRevision { + // Would cause an error to run the reverse, "fatal: More than one commit to dig up from, (N) and (N)" + return &ReverseLineBlame{ + CommitID: originalRevision, + LineNumber: originalLine, + FilePath: file, + }, nil + } + + res, _, gitErr := NewCommand(repo.Ctx, "blame"). + AddOptionValues("--reverse"). + AddDynamicArguments(fmt.Sprintf("%s..%s", originalRevision, currentRevision)). + AddOptionFormat("-L %d,%d", originalLine, originalLine). + AddOptionValues("-p"). + AddDashesAndList(file).RunStdString(&RunOpts{Dir: repo.Path}) + if gitErr != nil { + return nil, gitErr + } + + // Example output: + // + // 74be0e8aa338d1374ab7ca0a25a4f594955a69c2 16 9 1 + // author FirstName LastName + // author-mail + // author-time 1775492007 + // author-tz -0600 + // committer FirstName LastName + // committer-mail + // committer-time 1775492007 + // committer-tz -0600 + // summary restore file-in-base to orig, now not present in diff + // filename README.md + // + // Header (https://git-scm.com/docs/git-blame#_the_porcelain_format): + // - 40-byte SHA-1 of the commit the line is attributed to; + // - the line number of the line in the original file; [note: opposite in reverse] + // - the line number of the line in the final file; [note: opposite in reverse] + // - on a line that starts a group of lines from a different commit than the previous one, the number of lines in + // this group. On subsequent lines this field is absent. + + lines := strings.Split(res, "\n") + + header := lines[0] + headerValues := strings.Split(header, " ") + if len(headerValues) < 2 { + return nil, fmt.Errorf("failed to parse blame --reverse header: %q", header) + } + + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return nil, err + } + objectIDLen := objectFormat.FullLength() + objectID := headerValues[0] + if len(objectID) != objectIDLen { + return nil, fmt.Errorf("output of blame is invalid, cannot contain commit ID: %s", objectID) + } + commit, err := repo.GetCommit(objectID) + if err != nil { + return nil, fmt.Errorf("GetCommit: %w", err) + } + + currentLineStr := headerValues[1] + currentLineNo, err := strconv.ParseUint(currentLineStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("strconv.ParseUint: %w", err) + } + + var filename string + for _, otherLine := range lines { + if strings.HasPrefix(otherLine, "filename ") { + filename = otherLine[len("filename "):] + break + } + } + + return &ReverseLineBlame{ + CommitID: commit.ID.String(), + LineNumber: currentLineNo, + FilePath: filename, + }, nil +} diff --git a/modules/git/repo_blame_test.go b/modules/git/repo_blame_test.go index 4ddd5d9c3f..5803d082f0 100644 --- a/modules/git/repo_blame_test.go +++ b/modules/git/repo_blame_test.go @@ -89,11 +89,23 @@ func TestLineBlame(t *testing.T) { assert.Equal(t, firstCommit, commit.ID.String()) assert.EqualValues(t, 1, lineno) + rev, err := gitRepo.ReverseLineBlame(commit.ID.String(), "ANSWER", lineno, secondCommit) + require.NoError(t, err) + assert.Equal(t, secondCommit, rev.CommitID) + assert.Equal(t, "ANSWER", rev.FilePath) + assert.EqualValues(t, 10, rev.LineNumber) + for i := range uint64(9) { commit, lineno, err = gitRepo.LineBlame("HEAD", "ANSWER", i+1) require.NoError(t, err) assert.Equal(t, secondCommit, commit.ID.String()) assert.Equal(t, i+1, lineno) + + rev, err := gitRepo.ReverseLineBlame(commit.ID.String(), "ANSWER", lineno, secondCommit) + require.NoError(t, err) + assert.Equal(t, secondCommit, rev.CommitID) + assert.Equal(t, "ANSWER", rev.FilePath) + assert.Equal(t, i+1, rev.LineNumber) } } @@ -108,3 +120,66 @@ func TestLineBlame(t *testing.T) { }) }) } + +func TestReverseLineBlame(t *testing.T) { + t.Run("single commit", func(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, InitRepository(t.Context(), tmpDir, false, Sha1ObjectFormat.Name())) + + gitRepo, err := OpenRepository(t.Context(), tmpDir) + require.NoError(t, err) + defer gitRepo.Close() + + require.NoError(t, os.WriteFile(path.Join(tmpDir, "file1.md"), []byte("abba\n"), 0o666)) + require.NoError(t, AddChanges(tmpDir, true)) + require.NoError(t, CommitChanges(tmpDir, CommitChangesOptions{Message: "abba spelt backwards"})) + + commit, err := gitRepo.GetRefCommitID("HEAD") + require.NoError(t, err) + + blameCommit, lineno, err := gitRepo.LineBlame("HEAD", "file1.md", 1) + require.NoError(t, err) + assert.Equal(t, commit, blameCommit.ID.String()) + assert.EqualValues(t, 1, lineno) + + rev, err := gitRepo.ReverseLineBlame(commit, "file1.md", lineno, commit) + require.NoError(t, err) + assert.Equal(t, commit, rev.CommitID) + assert.Equal(t, "file1.md", rev.FilePath) + assert.EqualValues(t, 1, rev.LineNumber) + }) + + t.Run("move file", func(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, InitRepository(t.Context(), tmpDir, false, Sha1ObjectFormat.Name())) + + gitRepo, err := OpenRepository(t.Context(), tmpDir) + require.NoError(t, err) + defer gitRepo.Close() + + require.NoError(t, os.WriteFile(path.Join(tmpDir, "file1.md"), []byte("abba\n"), 0o666)) + require.NoError(t, AddChanges(tmpDir, true)) + require.NoError(t, CommitChanges(tmpDir, CommitChangesOptions{Message: "abba spelt backwards"})) + + firstCommit, err := gitRepo.GetRefCommitID("HEAD") + require.NoError(t, err) + + require.NoError(t, os.Rename(path.Join(tmpDir, "file1.md"), path.Join(tmpDir, "file2.md"))) + require.NoError(t, AddChanges(tmpDir, true)) + require.NoError(t, CommitChanges(tmpDir, CommitChangesOptions{Message: "move file"})) + + secondCommit, err := gitRepo.GetRefCommitID("HEAD") + require.NoError(t, err) + + blameCommit, lineno, err := gitRepo.LineBlame("HEAD", "file2.md", 1) + require.NoError(t, err) + assert.Equal(t, firstCommit, blameCommit.ID.String()) + assert.EqualValues(t, 1, lineno) + + rev, err := gitRepo.ReverseLineBlame(firstCommit, "file1.md", lineno, secondCommit) + require.NoError(t, err) + assert.Equal(t, secondCommit, rev.CommitID) + assert.Equal(t, "file2.md", rev.FilePath) + assert.EqualValues(t, 1, rev.LineNumber) + }) +} diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index bd7d5e2014..99ffac7afb 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -217,10 +217,15 @@ type CommitsByFileAndRangeOptions struct { File string Not string Page int + PageSize int } // CommitsByFileAndRange return the commits according revision file and the page func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { + if opts.PageSize <= 0 { + opts.PageSize = setting.Git.CommitsRangeSize + } + skip := (opts.Page - 1) * setting.Git.CommitsRangeSize stdoutReader, stdoutWriter := io.Pipe() @@ -231,7 +236,7 @@ func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) go func() { stderr := strings.Builder{} gitCmd := NewCommand(repo.Ctx, "rev-list"). - AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). + AddOptionFormat("--max-count=%d", opts.PageSize). AddOptionFormat("--skip=%d", skip) gitCmd.AddDynamicArguments(opts.Revision) @@ -473,7 +478,7 @@ func (repo *Repository) GetCommitsFromIDs(commitIDs []string, ignoreExistence bo // It's entirely possible the commit no longer exists, we only care // about the status and verification. Verification is no longer possible, // but getting the status is still possible with just the ID. We do have - // to assumme the commitID is not shortened, we cannot recover the full + // to assume the commitID is not shortened, we cannot recover the full // commitID. id, err := NewIDFromString(commitID) if err == nil { diff --git a/modules/git/repo_commit_test.go b/modules/git/repo_commit_test.go index 99cde92300..401848a975 100644 --- a/modules/git/repo_commit_test.go +++ b/modules/git/repo_commit_test.go @@ -200,6 +200,44 @@ func TestCommitsByFileAndRange(t *testing.T) { } } +func TestCommitsByFileAndRangeWithPageSize(t *testing.T) { + bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") + bareRepo1, err := openRepositoryWithDefaultContext(bareRepo1Path) + require.NoError(t, err) + defer bareRepo1.Close() + defer test.MockVariableValue(&setting.Git.CommitsRangeSize, 2)() + + testCases := []struct { + File string + Page int + PageSize int + ExpectedCommitCount int + }{ + {"file1.txt", 1, 1, 1}, + {"file2.txt", 1, 1, 1}, + {"file*.txt", 1, 2, 2}, + {"file*.txt", 1, 1, 1}, + {"foo", 1, 2, 2}, + {"foo", 1, 1, 1}, + {"foo", 2, 1, 1}, + {"foo", 3, 0, 0}, + {"foo", 3, 2, 0}, + {"f*", 1, 2, 2}, + {"f*", 2, 2, 2}, + {"f*", 3, 1, 1}, + } + for _, testCase := range testCases { + commits, err := bareRepo1.CommitsByFileAndRange(CommitsByFileAndRangeOptions{ + Revision: "master", + File: testCase.File, + Page: testCase.Page, + PageSize: testCase.PageSize, + }) + require.NoError(t, err) + assert.Len(t, commits, testCase.ExpectedCommitCount, "file: '%s', page: %d", testCase.File, testCase.Page) + } +} + func TestGetCommitsFromIDs(t *testing.T) { bareRepo1, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare")) require.NoError(t, err) diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index f58757a9a2..7fc5c573dd 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -95,7 +95,7 @@ func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { return nil, err } filelist := make([]string, 0, len(filenames)) - for _, line := range bytes.Split(res, []byte{'\000'}) { + for line := range bytes.SplitSeq(res, []byte{'\000'}) { filelist = append(filelist, string(line)) } diff --git a/modules/git/repo_language_stats.go b/modules/git/repo_language_stats.go index ee4beb2f87..2832c4f572 100644 --- a/modules/git/repo_language_stats.go +++ b/modules/git/repo_language_stats.go @@ -168,11 +168,21 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err } } - if isFalse(isDetectable) || isTrue(isVendored) || isTrue(isDocumentation) || - (!isFalse(isVendored) && analyze.IsVendor(f.Name())) || - enry.IsDotFile(f.Name()) || - enry.IsConfiguration(f.Name()) || - (!isFalse(isDocumentation) && enry.IsDocumentation(f.Name())) { + // Don't skip this file if it is explicitly set to be detectable. + // Skip this file if one of the following conditions holds: + // 1. Explicitly set to not be detectable. + // 2. Explicitly set that it is vendored. + // 3. Explicitly set that it is documentation. + // 4. Is not explicitly set to not be vendored and is by heuristic considered to be vendored. + // 5. It is considered to be a dot file. + // 6. It is considered to be a configuration file. + // 7. Is not explicitly set to not be documentation and is by heuristic considered to be documentation. + if !isTrue(isDetectable) && + (isFalse(isDetectable) || isTrue(isVendored) || isTrue(isDocumentation) || + (!isFalse(isVendored) && analyze.IsVendor(f.Name())) || + enry.IsDotFile(f.Name()) || + enry.IsConfiguration(f.Name()) || + (!isFalse(isDocumentation) && enry.IsDocumentation(f.Name()))) { continue } diff --git a/modules/git/repo_language_stats_test.go b/modules/git/repo_language_stats_test.go index e3d8ba1f69..2bc57b56c5 100644 --- a/modules/git/repo_language_stats_test.go +++ b/modules/git/repo_language_stats_test.go @@ -34,6 +34,16 @@ func TestRepository_GetLanguageStats(t *testing.T) { "Python": 67, "Java": 112, }, stats) + + stats, err = gitRepo.GetLanguageStats("5684d0c8cfdfb17fcd59101826efc9ff54b80df4") + require.NoError(t, err) + + assert.Equal(t, map[string]int64{ + "Cobra": 67, + "Python": 67, + "Markdown": 15, + "Java": 112, + }, stats) } func TestMergeLanguageStats(t *testing.T) { diff --git a/modules/git/repo_stats.go b/modules/git/repo_stats.go index ef0865e3d3..881acdafcf 100644 --- a/modules/git/repo_stats.go +++ b/modules/git/repo_stats.go @@ -98,6 +98,7 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) } switch p { case 1: // Separator + break case 2: // Commit sha-1 stats.CommitCount++ case 3: // Author diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go index f7f04e1f10..bd851a3be3 100644 --- a/modules/git/repo_tag.go +++ b/modules/git/repo_tag.go @@ -42,8 +42,8 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { return "", err } - tagRefs := strings.Split(stdout, "\n") - for _, tagRef := range tagRefs { + tagRefs := strings.SplitSeq(stdout, "\n") + for tagRef := range tagRefs { if len(strings.TrimSpace(tagRef)) > 0 { fields := strings.Fields(tagRef) if strings.HasPrefix(fields[0], sha) && strings.HasPrefix(fields[1], TagPrefix) { @@ -65,7 +65,7 @@ func (repo *Repository) GetTagID(name string) (string, error) { return "", err } // Make sure exact match is used: "v1" != "release/v1" - for _, line := range strings.Split(stdout, "\n") { + for line := range strings.SplitSeq(stdout, "\n") { fields := strings.Fields(line) if len(fields) == 2 && fields[1] == "refs/tags/"+name { return fields[0], nil diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go index d88851551f..d825dbd973 100644 --- a/modules/git/repo_tag_test.go +++ b/modules/git/repo_tag_test.go @@ -4,6 +4,7 @@ package git import ( + "fmt" "path/filepath" "testing" "time" @@ -150,7 +151,8 @@ func TestRepository_GetAnnotatedTag(t *testing.T) { // Annotated tag's name should fail tag3, err := bareRepo1.GetAnnotatedTag(aTagName) require.Error(t, err) - require.Errorf(t, err, "Length must be 40: %d", len(aTagName)) + require.ErrorContains(t, err, + fmt.Sprintf("length %d has no matched object format: %s", len(aTagName), aTagName)) assert.Nil(t, tag3) // Lightweight Tag should fail diff --git a/modules/git/tag.go b/modules/git/tag.go index edbe54b853..3af062a51a 100644 --- a/modules/git/tag.go +++ b/modules/git/tag.go @@ -50,20 +50,20 @@ l: switch { case eol > 0: line := data[nextline : nextline+eol] - spacepos := bytes.IndexByte(line, ' ') - reftype := line[:spacepos] + before, after, _ := bytes.Cut(line, []byte{' '}) + reftype := before switch string(reftype) { case "object": - id, err := NewIDFromString(string(line[spacepos+1:])) + id, err := NewIDFromString(string(after)) if err != nil { return nil, err } tag.Object = id case "type": // A commit can have one or more parents - tag.Type = string(line[spacepos+1:]) + tag.Type = string(after) case "tagger": - tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(line[spacepos+1:])) + tag.Tagger = parseSignatureFromCommitLine(util.UnsafeBytesToString(after)) } nextline += eol + 1 case eol == 0: diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.idx b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.idx deleted file mode 100644 index 186136cb12..0000000000 Binary files a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.idx and /dev/null differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.pack b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.pack deleted file mode 100644 index 046061c688..0000000000 Binary files a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.pack and /dev/null differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.rev b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.rev deleted file mode 100644 index 7d8c6f3562..0000000000 Binary files a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-371b1f6c24df14da4898b22c00ff8fb55303ac76.rev and /dev/null differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.idx b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.idx new file mode 100644 index 0000000000..80e1ee36b5 Binary files /dev/null and b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.idx differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.pack b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.pack new file mode 100644 index 0000000000..94b6852051 Binary files /dev/null and b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.pack differ diff --git a/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.rev b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.rev new file mode 100644 index 0000000000..c2e2e3aeae Binary files /dev/null and b/modules/git/tests/repos/language_stats_repo/objects/pack/pack-7cd0100ad5c382a7e8e1bacada4b66b927f29b72.rev differ diff --git a/modules/git/tests/repos/language_stats_repo/packed-refs b/modules/git/tests/repos/language_stats_repo/packed-refs index 63e01583a4..b046027fce 100644 --- a/modules/git/tests/repos/language_stats_repo/packed-refs +++ b/modules/git/tests/repos/language_stats_repo/packed-refs @@ -1,2 +1,2 @@ # pack-refs with: peeled fully-peeled sorted -95d3505f2db273e40be79f84416051ae85e9ea0d refs/heads/master +5684d0c8cfdfb17fcd59101826efc9ff54b80df4 refs/heads/master diff --git a/modules/git/tree.go b/modules/git/tree.go index f6201f6cc9..9a91787c9e 100644 --- a/modules/git/tree.go +++ b/modules/git/tree.go @@ -170,7 +170,7 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error return nil, err } filelist := make([]string, 0, len(filenames)) - for _, line := range bytes.Split(res, []byte{'\000'}) { + for line := range bytes.SplitSeq(res, []byte{'\000'}) { filelist = append(filelist, string(line)) } diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go index 8b6c4c467c..0b3564178b 100644 --- a/modules/git/tree_entry.go +++ b/modules/git/tree_entry.go @@ -5,6 +5,7 @@ package git import ( + "fmt" "io" "sort" "strings" @@ -136,11 +137,11 @@ func (te *TreeEntry) LinkTarget() (string, error) { } // FollowLink returns the entry pointed to by a symlink -func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) { +func (te *TreeEntry) FollowLink() (*TreeEntry, error) { // read the link lnk, err := te.LinkTarget() if err != nil { - return nil, "", err + return nil, err } t := te.ptree @@ -151,35 +152,33 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) { } if t == nil { - return nil, "", ErrBadLink{te.Name(), "points outside of repo"} + return nil, ErrBadLink{te.Name(), "points outside of repo"} } target, err := t.GetTreeEntryByPath(lnk) if err != nil { if IsErrNotExist(err) { - return nil, "", ErrBadLink{te.Name(), "broken link"} + return nil, ErrBadLink{te.Name(), "broken link"} } - return nil, "", err + return nil, err } - return target, lnk, nil + return target, nil } // FollowLinks returns the entry ultimately pointed to by a symlink -func (te *TreeEntry) FollowLinks() (*TreeEntry, string, error) { +func (te *TreeEntry) FollowLinks() (*TreeEntry, error) { if !te.IsLink() { - return nil, "", ErrBadLink{te.Name(), "not a symlink"} + return nil, ErrBadLink{te.Name(), "not a symlink"} } entry := te - entryLink := "" - for i := 0; i < 999; i++ { + for range 999 { if entry.IsLink() { - next, link, err := entry.FollowLink() - entryLink = link + next, err := entry.FollowLink() if err != nil { - return nil, "", err + return nil, err } if next.ID == entry.ID { - return nil, "", ErrBadLink{ + return nil, ErrBadLink{ entry.Name(), "recursive link", } @@ -190,12 +189,12 @@ func (te *TreeEntry) FollowLinks() (*TreeEntry, string, error) { } } if entry.IsLink() { - return nil, "", ErrBadLink{ + return nil, ErrBadLink{ te.Name(), "too many levels of symbolic links", } } - return entry, entryLink, nil + return entry, nil } // returns the Tree pointed to by this TreeEntry, or nil if this is not a tree @@ -208,6 +207,42 @@ func (te *TreeEntry) Tree() *Tree { return t } +// returns the calulcated path within the tree of this TreeEntry, or an error if it can be determined +func (te *TreeEntry) Path() (string, error) { + targetPath := te.Name() + parentTree := te.ptree + if parentTree == nil { + return "", fmt.Errorf("couldn't find the parent tree of the entry") + } + + prevID := parentTree.ID + parentTree = parentTree.ptree + for parentTree != nil { + entries, err := parentTree.ListEntries() + if err != nil { + return "", fmt.Errorf("couldn't list entries: %v", err) + } + + var matchingEntry *TreeEntry + for _, entry := range entries { + if entry.ID == prevID { + matchingEntry = entry + break + } + } + + if matchingEntry == nil { + return "", fmt.Errorf("this shouldn't happen: couldn't find entry (ID: %s) in tree (ID: %s)", prevID, parentTree.ID) + } + + targetPath = matchingEntry.name + "/" + targetPath + prevID = parentTree.ID + parentTree = parentTree.ptree + } + + return targetPath, nil +} + // GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory ) func (te *TreeEntry) GetSubJumpablePathName() string { if te.IsSubmodule() || !te.IsDir() { diff --git a/modules/git/tree_entry_test.go b/modules/git/tree_entry_test.go new file mode 100644 index 0000000000..325d3686f0 --- /dev/null +++ b/modules/git/tree_entry_test.go @@ -0,0 +1,46 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git_test + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTreeEntry_Path(t *testing.T) { + repo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "templates_repo")) + require.NoError(t, err) + defer repo.Close() + + tests := []struct { + name string // description of this test case + path string + }{ + { + name: "Top level dir", + path: ".forgejo", + }, + { + name: "File in subdir", + path: ".forgejo/default_merge_message/MERGE_TEMPLATE.md", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tree, err := repo.GetTree("HEAD^{tree}") + require.NoError(t, err) + + te, err := tree.GetTreeEntryByPath(tt.path) + require.NoError(t, err) + + got, gotErr := te.Path() + require.NoError(t, gotErr, "Path() failed: %v", gotErr) + assert.Equal(t, tt.path, got, "Path() = %v, want %v", got, tt.path) + }) + } +} diff --git a/modules/git/tree_test.go b/modules/git/tree_test.go index aa092cc56b..6277154acd 100644 --- a/modules/git/tree_test.go +++ b/modules/git/tree_test.go @@ -20,7 +20,7 @@ func TestSubTree_Issue29101(t *testing.T) { require.NoError(t, err) // old code could produce a different error if called multiple times - for i := 0; i < 10; i++ { + for range 10 { _, err = commit.SubTree("file1.txt") require.Error(t, err) assert.True(t, IsErrNotExist(err)) diff --git a/modules/graceful/manager.go b/modules/graceful/manager.go index db5738c94c..3b0115c51c 100644 --- a/modules/graceful/manager.go +++ b/modules/graceful/manager.go @@ -164,6 +164,7 @@ func (g *Manager) doHammerTime(d time.Duration) { g.lock.Lock() select { case <-g.hammerCtx.Done(): + break default: log.Warn("Setting Hammer condition") g.hammerCtxCancel() @@ -180,6 +181,7 @@ func (g *Manager) doTerminate() { g.lock.Lock() select { case <-g.terminateCtx.Done(): + break default: log.Warn("Terminating") g.terminateCtxCancel() diff --git a/modules/graceful/manager_common.go b/modules/graceful/manager_common.go index 892957e93f..87f18e382f 100644 --- a/modules/graceful/manager_common.go +++ b/modules/graceful/manager_common.go @@ -86,6 +86,7 @@ func (g *Manager) DoGracefulShutdown() { g.lock.Lock() select { case <-g.shutdownRequested: + break default: close(g.shutdownRequested) } diff --git a/modules/graceful/server.go b/modules/graceful/server.go index e812117dbd..0f547a6c30 100644 --- a/modules/graceful/server.go +++ b/modules/graceful/server.go @@ -85,7 +85,6 @@ func (srv *Server) ListenAndServe(serve ServeFunction, useProxyProtocol bool) er listener = &proxyprotocol.Listener{ Listener: listener, ProxyHeaderTimeout: setting.ProxyProtocolHeaderTimeout, - AcceptUnknown: setting.ProxyProtocolAcceptUnknown, } } srv.listener = listener @@ -118,7 +117,6 @@ func (srv *Server) ListenAndServeTLSConfig(tlsConfig *tls.Config, serve ServeFun listener = &proxyprotocol.Listener{ Listener: listener, ProxyHeaderTimeout: setting.ProxyProtocolHeaderTimeout, - AcceptUnknown: setting.ProxyProtocolAcceptUnknown, } } @@ -130,7 +128,6 @@ func (srv *Server) ListenAndServeTLSConfig(tlsConfig *tls.Config, serve ServeFun listener = &proxyprotocol.Listener{ Listener: listener, ProxyHeaderTimeout: setting.ProxyProtocolHeaderTimeout, - AcceptUnknown: setting.ProxyProtocolAcceptUnknown, } } diff --git a/modules/hostmatcher/hostmatcher.go b/modules/hostmatcher/hostmatcher.go index 1069310316..15c6371422 100644 --- a/modules/hostmatcher/hostmatcher.go +++ b/modules/hostmatcher/hostmatcher.go @@ -6,6 +6,7 @@ package hostmatcher import ( "net" "path/filepath" + "slices" "strings" ) @@ -38,7 +39,7 @@ func isBuiltin(s string) bool { // ParseHostMatchList parses the host list HostMatchList func ParseHostMatchList(settingKeyHint, hostList string) *HostMatchList { hl := &HostMatchList{SettingKeyHint: settingKeyHint, SettingValue: hostList} - for _, s := range strings.Split(hostList, ",") { + for s := range strings.SplitSeq(hostList, ",") { s = strings.ToLower(strings.TrimSpace(s)) if s == "" { continue @@ -61,7 +62,7 @@ func ParseSimpleMatchList(settingKeyHint, matchList string) *HostMatchList { SettingKeyHint: settingKeyHint, SettingValue: matchList, } - for _, s := range strings.Split(matchList, ",") { + for s := range strings.SplitSeq(matchList, ",") { s = strings.ToLower(strings.TrimSpace(s)) if s == "" { continue @@ -98,10 +99,8 @@ func (hl *HostMatchList) checkPattern(host string) bool { } func (hl *HostMatchList) checkIP(ip net.IP) bool { - for _, pattern := range hl.patterns { - if pattern == "*" { - return true - } + if slices.Contains(hl.patterns, "*") { + return true } for _, builtin := range hl.builtins { switch builtin { diff --git a/modules/httpcache/httpcache.go b/modules/httpcache/httpcache.go index 7978fc38a1..311f7215b2 100644 --- a/modules/httpcache/httpcache.go +++ b/modules/httpcache/httpcache.go @@ -59,7 +59,7 @@ func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag strin func checkIfNoneMatchIsValid(req *http.Request, etag string) bool { ifNoneMatch := req.Header.Get("If-None-Match") if len(ifNoneMatch) > 0 { - for _, item := range strings.Split(ifNoneMatch, ",") { + for item := range strings.SplitSeq(ifNoneMatch, ",") { item = strings.TrimPrefix(strings.TrimSpace(item), "W/") // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives if item == etag { return true diff --git a/modules/httplib/serve.go b/modules/httplib/serve.go index d385ac21c9..4c71437fc5 100644 --- a/modules/httplib/serve.go +++ b/modules/httplib/serve.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "maps" "net/http" "net/url" "path" @@ -86,9 +87,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { } if opts.AdditionalHeaders != nil { - for k, v := range opts.AdditionalHeaders { - header[k] = v - } + maps.Copy(header, opts.AdditionalHeaders) } } diff --git a/modules/indexer/code/git.go b/modules/indexer/code/git.go index 14a43cf3be..8ec3c1181f 100644 --- a/modules/indexer/code/git.go +++ b/modules/indexer/code/git.go @@ -129,8 +129,8 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio changes.Updates = append(changes.Updates, updates...) return nil } - lines := strings.Split(stdout, "\n") - for _, line := range lines { + lines := strings.SplitSeq(stdout, "\n") + for line := range lines { line = strings.TrimSpace(line) if len(line) == 0 { continue diff --git a/modules/indexer/code/indexer.go b/modules/indexer/code/indexer.go index a3e20e1d5a..a414c40d0e 100644 --- a/modules/indexer/code/indexer.go +++ b/modules/indexer/code/indexer.go @@ -215,7 +215,9 @@ func Init() { } select { case waitChannel <- time.Since(start): + break case <-graceful.GetManager().IsShutdown(): + break } close(waitChannel) diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index 2085251f1c..a17e7192ac 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "html/template" + "slices" "strings" "forgejo.org/modules/highlight" @@ -46,6 +47,19 @@ const ( SearchModeFuzzy = internal.CodeSearchModeFuzzy ) +type Results []*Result + +// Get the set of repo IDs from a list of search results +func (res Results) RepoIDs() []int64 { + ids := make([]int64, len(res)) + for _, r := range res { + if !slices.Contains(ids, r.RepoID) { + ids = append(ids, r.RepoID) + } + } + return ids +} + func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) { startIndex := selectionStartIndex numLinesBefore := 0 @@ -218,7 +232,7 @@ func searchResult(result *internal.SearchResult, startIndex, endIndex int) (*Res } // PerformSearch perform a search on a repository -func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, []*SearchResultLanguages, error) { +func PerformSearch(ctx context.Context, opts *SearchOptions) (int, Results, []*SearchResultLanguages, error) { if opts == nil || len(opts.Keyword) == 0 { return 0, nil, nil, nil } diff --git a/modules/indexer/code/search_test.go b/modules/indexer/code/search_test.go index 2413ddec0b..b8fe852bb8 100644 --- a/modules/indexer/code/search_test.go +++ b/modules/indexer/code/search_test.go @@ -87,8 +87,8 @@ func TestHighlightSearchResultCode(t *testing.T) { Code: "func main() {\n\tfmt.Println(\"mark this\")\n}", Result: []template.HTML{ "func main() {", - "\tfmt.Println("mark this")", - "}", + "\tfmt.Println("mark this")", + "}", }, }, { @@ -98,8 +98,8 @@ func TestHighlightSearchResultCode(t *testing.T) { Code: "func main() {\n\tfmt.Println(\"mark this 😊\")\n}", Result: []template.HTML{ "func main() {", - "\tfmt.Println("mark this 😊")", - "}", + "\tfmt.Println("mark this 😊")", + "}", }, }, } diff --git a/modules/indexer/internal/bleve/query.go b/modules/indexer/internal/bleve/query.go index e043023671..ea56fca0f8 100644 --- a/modules/indexer/internal/bleve/query.go +++ b/modules/indexer/internal/bleve/query.go @@ -48,15 +48,15 @@ func BoolFieldQuery(value bool, field string) *query.BoolFieldQuery { func NumericRangeInclusiveQuery(min, max optional.Option[int64], field string) *query.NumericRangeQuery { var minF, maxF *float64 var minI, maxI *bool - if min.Has() { + if has, value := min.Get(); has { minF = new(float64) - *minF = float64(min.Value()) + *minF = float64(value) minI = new(bool) *minI = true } - if max.Has() { + if has, value := max.Get(); has { maxF = new(float64) - *maxF = float64(max.Value()) + *maxF = float64(value) maxI = new(bool) *maxI = true } diff --git a/modules/indexer/issues/bleve/bleve.go b/modules/indexer/issues/bleve/bleve.go index a7c87c8f47..512788a207 100644 --- a/modules/indexer/issues/bleve/bleve.go +++ b/modules/indexer/issues/bleve/bleve.go @@ -13,7 +13,6 @@ import ( "github.com/blevesearch/bleve/v2" "github.com/blevesearch/bleve/v2/analysis/analyzer/custom" - "github.com/blevesearch/bleve/v2/analysis/token/camelcase" "github.com/blevesearch/bleve/v2/analysis/token/lowercase" "github.com/blevesearch/bleve/v2/analysis/token/unicodenorm" "github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode" @@ -24,7 +23,7 @@ import ( const ( issueIndexerAnalyzer = "issueIndexer" issueIndexerDocType = "issueIndexerDocType" - issueIndexerLatestVersion = 5 + issueIndexerLatestVersion = 7 ) const unicodeNormalizeName = "unicodeNormalize" @@ -83,7 +82,7 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) { docMapping.AddFieldMappingsAt("project_id", numberFieldMapping) docMapping.AddFieldMappingsAt("project_board_id", numberFieldMapping) docMapping.AddFieldMappingsAt("poster_id", numberFieldMapping) - docMapping.AddFieldMappingsAt("assignee_id", numberFieldMapping) + docMapping.AddFieldMappingsAt("assignee_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("mention_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("reviewed_ids", numberFieldMapping) docMapping.AddFieldMappingsAt("review_requested_ids", numberFieldMapping) @@ -100,7 +99,7 @@ func generateIssueIndexMapping() (mapping.IndexMapping, error) { "type": custom.Name, "char_filters": []string{}, "tokenizer": unicode.Name, - "token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name}, + "token_filters": []string{unicodeNormalizeName, lowercase.Name}, }); err != nil { return nil, err } @@ -196,19 +195,19 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( filters = append(filters, bleve.NewDisjunctionQuery(repoQueries...)) } - if options.PriorityRepoID.Has() { - eq := inner_bleve.NumericEqualityQuery(options.PriorityRepoID.Value(), "repo_id") + if has, value := options.PriorityRepoID.Get(); has { + eq := inner_bleve.NumericEqualityQuery(value, "repo_id") eq.SetBoost(10.0) meh := bleve.NewMatchAllQuery() meh.SetBoost(0) q.AddShould(bleve.NewDisjunctionQuery(eq, meh)) } - if options.IsPull.Has() { - filters = append(filters, inner_bleve.BoolFieldQuery(options.IsPull.Value(), "is_pull")) + if has, value := options.IsPull.Get(); has { + filters = append(filters, inner_bleve.BoolFieldQuery(value, "is_pull")) } - if options.IsClosed.Has() { - filters = append(filters, inner_bleve.BoolFieldQuery(options.IsClosed.Value(), "is_closed")) + if has, value := options.IsClosed.Get(); has { + filters = append(filters, inner_bleve.BoolFieldQuery(value, "is_closed")) } if options.NoLabelOnly { @@ -246,14 +245,14 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( "project_id": options.ProjectID, "project_board_id": options.ProjectColumnID, "poster_id": options.PosterID, - "assignee_id": options.AssigneeID, + "assignee_ids": options.AssigneeID, "mention_ids": options.MentionID, "reviewed_ids": options.ReviewedID, "review_requested_ids": options.ReviewRequestedID, "subscriber_ids": options.SubscriberID, } { - if val.Has() { - filters = append(filters, inner_bleve.NumericEqualityQuery(val.Value(), key)) + if has, value := val.Get(); has { + filters = append(filters, inner_bleve.NumericEqualityQuery(value, key)) } } diff --git a/modules/indexer/issues/db/options.go b/modules/indexer/issues/db/options.go index 5025d824a5..c4c05257eb 100644 --- a/modules/indexer/issues/db/options.go +++ b/modules/indexer/issues/db/options.go @@ -39,10 +39,10 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issues_ // See the comment of issues_model.SearchOptions for the reason why we need to convert convertID := func(id optional.Option[int64]) int64 { - if !id.Has() { + has, value := id.Get() + if !has { return 0 } - value := id.Value() if value == 0 { return db.NoConditionID } @@ -69,8 +69,8 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issues_ IncludeMilestones: nil, SortType: sortType, IssueIDs: nil, - UpdatedAfterUnix: options.UpdatedAfterUnix.Value(), - UpdatedBeforeUnix: options.UpdatedBeforeUnix.Value(), + UpdatedAfterUnix: options.UpdatedAfterUnix.ValueOrZeroValue(), + UpdatedBeforeUnix: options.UpdatedBeforeUnix.ValueOrZeroValue(), PriorityRepoID: 0, IsArchived: optional.None[bool](), Org: nil, @@ -78,9 +78,9 @@ func ToDBOptions(ctx context.Context, options *internal.SearchOptions) (*issues_ User: nil, } - if options.PriorityRepoID.Has() { + if has, value := options.PriorityRepoID.Get(); has { opts.SortType = "priorityrepo" - opts.PriorityRepoID = options.PriorityRepoID.Value() + opts.PriorityRepoID = value } if len(options.MilestoneIDs) == 1 && options.MilestoneIDs[0] == 0 { diff --git a/modules/indexer/issues/elasticsearch/elasticsearch.go b/modules/indexer/issues/elasticsearch/elasticsearch.go index 4a5e667c14..123f2a78e4 100644 --- a/modules/indexer/issues/elasticsearch/elasticsearch.go +++ b/modules/indexer/issues/elasticsearch/elasticsearch.go @@ -18,7 +18,7 @@ import ( ) const ( - issueIndexerLatestVersion = 2 + issueIndexerLatestVersion = 3 // multi-match-types, currently only 2 types are used // Reference: https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-dsl-multi-match-query.html#multi-match-types esMultiMatchTypeBestFields = "best_fields" @@ -69,7 +69,7 @@ const ( "project_id": { "type": "long", "index": true }, "project_board_id": { "type": "long", "index": true }, "poster_id": { "type": "long", "index": true }, - "assignee_id": { "type": "long", "index": true }, + "assignee_ids": { "type": "long", "index": true }, "mention_ids": { "type": "long", "index": true }, "reviewed_ids": { "type": "long", "index": true }, "review_requested_ids": { "type": "long", "index": true }, @@ -184,16 +184,16 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( } query.Must(q) } - if options.PriorityRepoID.Has() { - q := elastic.NewTermQuery("repo_id", options.PriorityRepoID.Value()).Boost(10) + if has, value := options.PriorityRepoID.Get(); has { + q := elastic.NewTermQuery("repo_id", value).Boost(10) query.Should(q) } - if options.IsPull.Has() { - query.Must(elastic.NewTermQuery("is_pull", options.IsPull.Value())) + if has, value := options.IsPull.Get(); has { + query.Must(elastic.NewTermQuery("is_pull", value)) } - if options.IsClosed.Has() { - query.Must(elastic.NewTermQuery("is_closed", options.IsClosed.Value())) + if has, value := options.IsClosed.Get(); has { + query.Must(elastic.NewTermQuery("is_closed", value)) } if options.NoLabelOnly { @@ -221,43 +221,43 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.Must(elastic.NewTermsQuery("milestone_id", toAnySlice(options.MilestoneIDs)...)) } - if options.ProjectID.Has() { - query.Must(elastic.NewTermQuery("project_id", options.ProjectID.Value())) + if has, value := options.ProjectID.Get(); has { + query.Must(elastic.NewTermQuery("project_id", value)) } - if options.ProjectColumnID.Has() { - query.Must(elastic.NewTermQuery("project_board_id", options.ProjectColumnID.Value())) + if has, value := options.ProjectColumnID.Get(); has { + query.Must(elastic.NewTermQuery("project_board_id", value)) } - if options.PosterID.Has() { - query.Must(elastic.NewTermQuery("poster_id", options.PosterID.Value())) + if has, value := options.PosterID.Get(); has { + query.Must(elastic.NewTermQuery("poster_id", value)) } - if options.AssigneeID.Has() { - query.Must(elastic.NewTermQuery("assignee_id", options.AssigneeID.Value())) + if has, value := options.AssigneeID.Get(); has { + query.Must(elastic.NewTermQuery("assignee_ids", value)) } - if options.MentionID.Has() { - query.Must(elastic.NewTermQuery("mention_ids", options.MentionID.Value())) + if has, value := options.MentionID.Get(); has { + query.Must(elastic.NewTermQuery("mention_ids", value)) } - if options.ReviewedID.Has() { - query.Must(elastic.NewTermQuery("reviewed_ids", options.ReviewedID.Value())) + if has, value := options.ReviewedID.Get(); has { + query.Must(elastic.NewTermQuery("reviewed_ids", value)) } - if options.ReviewRequestedID.Has() { - query.Must(elastic.NewTermQuery("review_requested_ids", options.ReviewRequestedID.Value())) + if has, value := options.ReviewRequestedID.Get(); has { + query.Must(elastic.NewTermQuery("review_requested_ids", value)) } - if options.SubscriberID.Has() { - query.Must(elastic.NewTermQuery("subscriber_ids", options.SubscriberID.Value())) + if has, value := options.SubscriberID.Get(); has { + query.Must(elastic.NewTermQuery("subscriber_ids", value)) } if options.UpdatedAfterUnix.Has() || options.UpdatedBeforeUnix.Has() { q := elastic.NewRangeQuery("updated_unix") - if options.UpdatedAfterUnix.Has() { - q.Gte(options.UpdatedAfterUnix.Value()) + if has, value := options.UpdatedAfterUnix.Get(); has { + q.Gte(value) } - if options.UpdatedBeforeUnix.Has() { - q.Lte(options.UpdatedBeforeUnix.Value()) + if has, value := options.UpdatedBeforeUnix.Get(); has { + q.Lte(value) } query.Must(q) } diff --git a/modules/indexer/issues/indexer.go b/modules/indexer/issues/indexer.go index dd7102713c..2f70cf7a69 100644 --- a/modules/indexer/issues/indexer.go +++ b/modules/indexer/issues/indexer.go @@ -61,10 +61,12 @@ func init() { // InitIssueIndexer initialize issue indexer, syncReindex is true then reindex until // all issue index done. -func InitIssueIndexer(syncReindex bool) { +// The return value is a done channel that signals that the indexer can be safely used. +func InitIssueIndexer(syncReindex bool) <-chan struct{} { ctx, _, finished := process.GetManager().AddTypedContext(context.Background(), "Service: IssueIndexer", process.SystemProcessType, false) indexerInitWaitChannel := make(chan time.Duration, 1) + done := make(chan struct{}, 1) // Create the Queue issueIndexerQueue = queue.CreateUniqueQueue(ctx, "issue_indexer", getIssueIndexerQueueHandler(ctx)) @@ -136,12 +138,15 @@ func InitIssueIndexer(syncReindex bool) { indexerInitWaitChannel <- time.Since(start) close(indexerInitWaitChannel) + close(done) }() if syncReindex { select { case <-indexerInitWaitChannel: + break case <-graceful.GetManager().IsShutdown(): + break } } else if setting.Indexer.StartupTimeout > 0 { go func() { @@ -161,6 +166,8 @@ func InitIssueIndexer(syncReindex bool) { } }() } + + return done } func getIssueIndexerQueueHandler(ctx context.Context) func(items ...*IndexerMetadata) []*IndexerMetadata { diff --git a/modules/indexer/issues/indexer_test.go b/modules/indexer/issues/indexer_test.go index 7d4ffa7f02..60c46d9a2c 100644 --- a/modules/indexer/issues/indexer_test.go +++ b/modules/indexer/issues/indexer_test.go @@ -30,7 +30,7 @@ func TestDBSearchIssues(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) defer test.MockVariableValue(&setting.Indexer.IssueType, "db")() - InitIssueIndexer(true) + <-InitIssueIndexer(true) t.Run("search issues with keyword", searchIssueWithKeyword) t.Run("search issues in repo", searchIssueInRepo) @@ -58,6 +58,13 @@ func searchIssueWithKeyword(t *testing.T) { }, []int64{2}, }, + { + "ISSUe2", // case-insensitive search + &SearchOptions{ + RepoIDs: []int64{1}, + }, + []int64{2}, + }, { "first", &SearchOptions{ @@ -419,7 +426,7 @@ func TestBleveDeleteIssue(t *testing.T) { tmp := t.TempDir() defer test.MockVariableValue(&setting.Indexer.IssuePath, filepath.Join(tmp, "indexers/issues.bleve"))() defer test.MockVariableValue(&setting.Indexer.IssueType, "bleve")() - InitIssueIndexer(false) + <-InitIssueIndexer(false) ctx := t.Context() issue := unittest.AssertExistsAndLoadBean(t, &issues.Issue{ID: 1}) diff --git a/modules/indexer/issues/internal/model.go b/modules/indexer/issues/internal/model.go index 89134dcac7..4034f6d0f9 100644 --- a/modules/indexer/issues/internal/model.go +++ b/modules/indexer/issues/internal/model.go @@ -30,7 +30,7 @@ type IndexerData struct { ProjectID int64 `json:"project_id"` ProjectColumnID int64 `json:"project_board_id"` // the key should be kept as project_board_id to keep compatible PosterID int64 `json:"poster_id"` - AssigneeID int64 `json:"assignee_id"` + AssigneeIDs []int64 `json:"assignee_ids"` MentionIDs []int64 `json:"mention_ids"` ReviewedIDs []int64 `json:"reviewed_ids"` ReviewRequestedIDs []int64 `json:"review_requested_ids"` diff --git a/modules/indexer/issues/internal/qstring_test.go b/modules/indexer/issues/internal/qstring_test.go index 2ad924076f..66c8a66ca3 100644 --- a/modules/indexer/issues/internal/qstring_test.go +++ b/modules/indexer/issues/internal/qstring_test.go @@ -11,6 +11,8 @@ import ( "forgejo.org/models/user" "forgejo.org/modules/optional" + _ "forgejo.org/modules/testimport" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/modules/indexer/issues/internal/tests/tests.go b/modules/indexer/issues/internal/tests/tests.go index 4466bf25a7..c4a453c9aa 100644 --- a/modules/indexer/issues/internal/tests/tests.go +++ b/modules/indexer/issues/internal/tests/tests.go @@ -461,10 +461,10 @@ var cases = []*testIndexerCase{ Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) for _, v := range result.Hits { - assert.Equal(t, int64(1), data[v.ID].AssigneeID) + assert.Contains(t, data[v.ID].AssigneeIDs, int64(1)) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.AssigneeID == 1 + return slices.Contains(v.AssigneeIDs, 1) }), result.Total) }, }, @@ -479,10 +479,10 @@ var cases = []*testIndexerCase{ Expected: func(t *testing.T, data map[int64]*internal.IndexerData, result *internal.SearchResult) { assert.Len(t, result.Hits, 5) for _, v := range result.Hits { - assert.Equal(t, int64(0), data[v.ID].AssigneeID) + assert.Equal(t, []int64{0}, data[v.ID].AssigneeIDs) } assert.Equal(t, countIndexerData(data, func(v *internal.IndexerData) bool { - return v.AssigneeID == 0 + return slices.Contains(v.AssigneeIDs, 0) }), result.Total) }, }, @@ -840,6 +840,14 @@ func generateDefaultIndexerData() []*internal.IndexerData { subscriberIDs[i] = int64(i) + 1 // SubscriberID should not be 0 } + assigneeIDs := make([]int64, 0, 2) + { + if issueIndex%7 == 0 { // If divisible by 7 we insert 1 too to test multiple assignees + assigneeIDs = append(assigneeIDs, 1) + } + assigneeIDs = append(assigneeIDs, issueIndex%10) + } + data = append(data, &internal.IndexerData{ ID: id, Index: issueIndex, @@ -856,7 +864,7 @@ func generateDefaultIndexerData() []*internal.IndexerData { ProjectID: issueIndex % 5, ProjectColumnID: issueIndex % 6, PosterID: id%10 + 1, // PosterID should not be 0 - AssigneeID: issueIndex % 10, + AssigneeIDs: assigneeIDs, MentionIDs: mentionIDs, ReviewedIDs: reviewedIDs, ReviewRequestedIDs: reviewRequestedIDs, diff --git a/modules/indexer/issues/meilisearch/meilisearch.go b/modules/indexer/issues/meilisearch/meilisearch.go index 82b889f875..98bcf45b45 100644 --- a/modules/indexer/issues/meilisearch/meilisearch.go +++ b/modules/indexer/issues/meilisearch/meilisearch.go @@ -18,7 +18,7 @@ import ( ) const ( - issueIndexerLatestVersion = 3 + issueIndexerLatestVersion = 4 // TODO: make this configurable if necessary maxTotalHits = 10000 @@ -67,7 +67,7 @@ func NewIndexer(url, apiKey, indexerName string) *Indexer { "project_id", "project_board_id", "poster_id", - "assignee_id", + "assignee_ids", "mention_ids", "reviewed_ids", "review_requested_ids", @@ -116,7 +116,7 @@ func (b *Indexer) Delete(_ context.Context, ids ...int64) error { } for _, id := range ids { - _, err := b.inner.Client.Index(b.inner.VersionedIndexName()).DeleteDocument(strconv.FormatInt(id, 10)) + _, err := b.inner.Client.Index(b.inner.VersionedIndexName()).DeleteDocument(strconv.FormatInt(id, 10), nil) if err != nil { return err } @@ -139,11 +139,11 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.And(q) } - if options.IsPull.Has() { - query.And(inner_meilisearch.NewFilterEq("is_pull", options.IsPull.Value())) + if has, value := options.IsPull.Get(); has { + query.And(inner_meilisearch.NewFilterEq("is_pull", value)) } - if options.IsClosed.Has() { - query.And(inner_meilisearch.NewFilterEq("is_closed", options.IsClosed.Value())) + if has, value := options.IsClosed.Get(); has { + query.And(inner_meilisearch.NewFilterEq("is_closed", value)) } if options.NoLabelOnly { @@ -171,41 +171,41 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( query.And(inner_meilisearch.NewFilterIn("milestone_id", options.MilestoneIDs...)) } - if options.ProjectID.Has() { - query.And(inner_meilisearch.NewFilterEq("project_id", options.ProjectID.Value())) + if has, value := options.ProjectID.Get(); has { + query.And(inner_meilisearch.NewFilterEq("project_id", value)) } - if options.ProjectColumnID.Has() { - query.And(inner_meilisearch.NewFilterEq("project_board_id", options.ProjectColumnID.Value())) + if has, value := options.ProjectColumnID.Get(); has { + query.And(inner_meilisearch.NewFilterEq("project_board_id", value)) } - if options.PosterID.Has() { - query.And(inner_meilisearch.NewFilterEq("poster_id", options.PosterID.Value())) + if has, value := options.PosterID.Get(); has { + query.And(inner_meilisearch.NewFilterEq("poster_id", value)) } - if options.AssigneeID.Has() { - query.And(inner_meilisearch.NewFilterEq("assignee_id", options.AssigneeID.Value())) + if has, value := options.AssigneeID.Get(); has { + query.And(inner_meilisearch.NewFilterEq("assignee_ids", value)) } - if options.MentionID.Has() { - query.And(inner_meilisearch.NewFilterEq("mention_ids", options.MentionID.Value())) + if has, value := options.MentionID.Get(); has { + query.And(inner_meilisearch.NewFilterEq("mention_ids", value)) } - if options.ReviewedID.Has() { - query.And(inner_meilisearch.NewFilterEq("reviewed_ids", options.ReviewedID.Value())) + if has, value := options.ReviewedID.Get(); has { + query.And(inner_meilisearch.NewFilterEq("reviewed_ids", value)) } - if options.ReviewRequestedID.Has() { - query.And(inner_meilisearch.NewFilterEq("review_requested_ids", options.ReviewRequestedID.Value())) + if has, value := options.ReviewRequestedID.Get(); has { + query.And(inner_meilisearch.NewFilterEq("review_requested_ids", value)) } - if options.SubscriberID.Has() { - query.And(inner_meilisearch.NewFilterEq("subscriber_ids", options.SubscriberID.Value())) + if has, value := options.SubscriberID.Get(); has { + query.And(inner_meilisearch.NewFilterEq("subscriber_ids", value)) } - if options.UpdatedAfterUnix.Has() { - query.And(inner_meilisearch.NewFilterGte("updated_unix", options.UpdatedAfterUnix.Value())) + if has, value := options.UpdatedAfterUnix.Get(); has { + query.And(inner_meilisearch.NewFilterGte("updated_unix", value)) } - if options.UpdatedBeforeUnix.Has() { - query.And(inner_meilisearch.NewFilterLte("updated_unix", options.UpdatedBeforeUnix.Value())) + if has, value := options.UpdatedBeforeUnix.Get(); has { + query.And(inner_meilisearch.NewFilterLte("updated_unix", value)) } var sortBy []string @@ -233,21 +233,19 @@ func (b *Indexer) Search(ctx context.Context, options *internal.SearchOptions) ( } var keywords []string - if len(options.Tokens) != 0 { - for _, token := range options.Tokens { - if !token.Fuzzy { - // to make it a phrase search, we have to quote the keyword(s) - // https://www.meilisearch.com/docs/reference/api/search#phrase-search - token.Term = doubleQuoteKeyword(token.Term) - } - - // internal.BoolOptShould (Default, requires no modifications) - // internal.BoolOptMust (Not supported by meilisearch) - if token.Kind == internal.BoolOptNot { - token.Term = "-" + token.Term - } - keywords = append(keywords, token.Term) + for _, token := range options.Tokens { + if !token.Fuzzy { + // to make it a phrase search, we have to quote the keyword(s) + // https://www.meilisearch.com/docs/reference/api/search#phrase-search + token.Term = doubleQuoteKeyword(token.Term) } + + // internal.BoolOptShould (Default, requires no modifications) + // internal.BoolOptMust (Not supported by meilisearch) + if token.Kind == internal.BoolOptNot { + token.Term = "-" + token.Term + } + keywords = append(keywords, token.Term) } searchRes, err := b.inner.Client.Index(b.inner.VersionedIndexName()). diff --git a/modules/indexer/issues/util.go b/modules/indexer/issues/util.go index 31b7e888c0..71ee46e235 100644 --- a/modules/indexer/issues/util.go +++ b/modules/indexer/issues/util.go @@ -50,6 +50,15 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD labels = append(labels, label.ID) } + assigneeIDs := make([]int64, 0, len(issue.Assignees)) + if len(issue.Assignees) != 0 { + for _, assignee := range issue.Assignees { + assigneeIDs = append(assigneeIDs, assignee.ID) + } + } else { + assigneeIDs = append(assigneeIDs, 0) + } + mentionIDs, err := issues_model.GetIssueMentionIDs(ctx, issueID) if err != nil { return nil, false, err @@ -108,7 +117,7 @@ func getIssueIndexerData(ctx context.Context, issueID int64) (*internal.IndexerD ProjectID: projectID, ProjectColumnID: issue.ProjectColumnID(ctx), PosterID: issue.PosterID, - AssigneeID: issue.AssigneeID, + AssigneeIDs: assigneeIDs, MentionIDs: mentionIDs, ReviewedIDs: reviewedIDs, ReviewRequestedIDs: reviewRequestedIDs, diff --git a/modules/issue/template/template.go b/modules/issue/template/template.go index 08c1b21c26..09dfe10e08 100644 --- a/modules/issue/template/template.go +++ b/modules/issue/template/template.go @@ -8,6 +8,7 @@ import ( "fmt" "net/url" "regexp" + "slices" "strconv" "strings" @@ -447,12 +448,7 @@ func (o *valuedOption) IsChecked() bool { case api.IssueFormFieldTypeDropdown: checks := strings.Split(o.field.Get(fmt.Sprintf("form-field-%s", o.field.ID)), ",") idx := strconv.Itoa(o.index) - for _, v := range checks { - if v == idx { - return true - } - } - return false + return slices.Contains(checks, idx) case api.IssueFormFieldTypeCheckboxes: return o.field.Get(fmt.Sprintf("form-field-%s-%d", o.field.ID, o.index)) == "on" } diff --git a/modules/jwtx/signingkey.go b/modules/jwtx/signingkey.go new file mode 100644 index 0000000000..f82f694d19 --- /dev/null +++ b/modules/jwtx/signingkey.go @@ -0,0 +1,565 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jwtx + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "math/big" + "os" + "path/filepath" + "slices" + "strings" + + "forgejo.org/modules/log" + "forgejo.org/modules/util" + + "github.com/golang-jwt/jwt/v5" +) + +// The ...KeyCfg types are only used for handover from setting to signingkey +// see comment in setting/security.go + +type SigningKeyCfg struct { + Algorithm string + SecretBytes *[]byte + PrivateKeyPath *string +} + +type KeyCfg struct { + Signing *SigningKeyCfg + // more later +} + +// ErrInvalidAlgorithmType represents an invalid algorithm error. +type ErrInvalidAlgorithmType struct { + Algorithm string +} + +func (err ErrInvalidAlgorithmType) Error() string { + return fmt.Sprintf("JWT signing algorithm is not supported: %s", err.Algorithm) +} + +func jwtHelper(key SigningKey, claims jwt.Claims, opts ...jwt.TokenOption) (string, error) { + jwt := jwt.NewWithClaims(key.SigningMethod(), claims, opts...) + key.PreProcessToken(jwt) + return jwt.SignedString(key.SignKey()) +} + +// SigningKey represents a algorithm/key pair to sign JWTs +type SigningKey interface { + IsSymmetric() bool + SigningMethod() jwt.SigningMethod + SignKey() any + VerifyKey() any + ToJWK() (map[string]string, error) + PreProcessToken(*jwt.Token) + // convenience: jwt.NewWithClaims + PreProcessToken + SignedString + JWT(jwt.Claims, ...jwt.TokenOption) (string, error) +} + +type hmacSigningKey struct { + signingMethod jwt.SigningMethod + secret []byte +} + +func (key hmacSigningKey) IsSymmetric() bool { + return true +} + +func (key hmacSigningKey) SigningMethod() jwt.SigningMethod { + return key.signingMethod +} + +func (key hmacSigningKey) SignKey() any { + return key.secret +} + +func (key hmacSigningKey) VerifyKey() any { + return key.secret +} + +func (key hmacSigningKey) ToJWK() (map[string]string, error) { + return map[string]string{ + "kty": "oct", + "alg": key.SigningMethod().Alg(), + }, nil +} + +func (key hmacSigningKey) PreProcessToken(*jwt.Token) {} + +func (key hmacSigningKey) JWT(claims jwt.Claims, opts ...jwt.TokenOption) (string, error) { + return jwtHelper(key, claims, opts...) +} + +type rsaSigningKey struct { + signingMethod jwt.SigningMethod + key *rsa.PrivateKey + id string +} + +func newRSASigningKey(signingMethod jwt.SigningMethod, key *rsa.PrivateKey) (rsaSigningKey, error) { + kid, err := util.CreatePublicKeyFingerprint(key.Public().(*rsa.PublicKey)) + if err != nil { + return rsaSigningKey{}, err + } + + return rsaSigningKey{ + signingMethod, + key, + base64.RawURLEncoding.EncodeToString(kid), + }, nil +} + +func (key rsaSigningKey) IsSymmetric() bool { + return false +} + +func (key rsaSigningKey) SigningMethod() jwt.SigningMethod { + return key.signingMethod +} + +func (key rsaSigningKey) SignKey() any { + return key.key +} + +func (key rsaSigningKey) VerifyKey() any { + return key.key.Public() +} + +func (key rsaSigningKey) ToJWK() (map[string]string, error) { + pubKey := key.key.Public().(*rsa.PublicKey) + + return map[string]string{ + "kty": "RSA", + "alg": key.SigningMethod().Alg(), + "kid": key.id, + "e": base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pubKey.E)).Bytes()), + "n": base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes()), + }, nil +} + +func (key rsaSigningKey) PreProcessToken(token *jwt.Token) { + token.Header["kid"] = key.id +} + +func (key rsaSigningKey) JWT(claims jwt.Claims, opts ...jwt.TokenOption) (string, error) { + return jwtHelper(key, claims, opts...) +} + +type eddsaSigningKey struct { + signingMethod jwt.SigningMethod + key ed25519.PrivateKey + id string +} + +func newEdDSASigningKey(signingMethod jwt.SigningMethod, key ed25519.PrivateKey) (eddsaSigningKey, error) { + kid, err := util.CreatePublicKeyFingerprint(key.Public().(ed25519.PublicKey)) + if err != nil { + return eddsaSigningKey{}, err + } + + return eddsaSigningKey{ + signingMethod, + key, + base64.RawURLEncoding.EncodeToString(kid), + }, nil +} + +func (key eddsaSigningKey) IsSymmetric() bool { + return false +} + +func (key eddsaSigningKey) SigningMethod() jwt.SigningMethod { + return key.signingMethod +} + +func (key eddsaSigningKey) SignKey() any { + return key.key +} + +func (key eddsaSigningKey) VerifyKey() any { + return key.key.Public() +} + +func (key eddsaSigningKey) ToJWK() (map[string]string, error) { + pubKey := key.key.Public().(ed25519.PublicKey) + + return map[string]string{ + "alg": key.SigningMethod().Alg(), + "kid": key.id, + "kty": "OKP", + "crv": "Ed25519", + "x": base64.RawURLEncoding.EncodeToString(pubKey), + }, nil +} + +func (key eddsaSigningKey) PreProcessToken(token *jwt.Token) { + token.Header["kid"] = key.id +} + +func (key eddsaSigningKey) JWT(claims jwt.Claims, opts ...jwt.TokenOption) (string, error) { + return jwtHelper(key, claims, opts...) +} + +type ecdsaSigningKey struct { + signingMethod jwt.SigningMethod + key *ecdsa.PrivateKey + id string +} + +func newECDSASigningKey(signingMethod jwt.SigningMethod, key *ecdsa.PrivateKey) (ecdsaSigningKey, error) { + kid, err := util.CreatePublicKeyFingerprint(key.Public().(*ecdsa.PublicKey)) + if err != nil { + return ecdsaSigningKey{}, err + } + + return ecdsaSigningKey{ + signingMethod, + key, + base64.RawURLEncoding.EncodeToString(kid), + }, nil +} + +func (key ecdsaSigningKey) IsSymmetric() bool { + return false +} + +func (key ecdsaSigningKey) SigningMethod() jwt.SigningMethod { + return key.signingMethod +} + +func (key ecdsaSigningKey) SignKey() any { + return key.key +} + +func (key ecdsaSigningKey) VerifyKey() any { + return key.key.Public() +} + +func (key ecdsaSigningKey) ToJWK() (map[string]string, error) { + pubKey := key.key.Public().(*ecdsa.PublicKey) + + return map[string]string{ + "kty": "EC", + "alg": key.SigningMethod().Alg(), + "kid": key.id, + "crv": pubKey.Params().Name, + "x": base64.RawURLEncoding.EncodeToString(pubKey.X.Bytes()), //nolint:staticcheck // no easy replacement. JWTX specification mandates marshalling to x, even if unsafe. + "y": base64.RawURLEncoding.EncodeToString(pubKey.Y.Bytes()), //nolint:staticcheck // no easy replacement. JWTX specification mandates marshalling to y, even if unsafe. + }, nil +} + +func (key ecdsaSigningKey) PreProcessToken(token *jwt.Token) { + token.Header["kid"] = key.id +} + +func (key ecdsaSigningKey) JWT(claims jwt.Claims, opts ...jwt.TokenOption) (string, error) { + return jwtHelper(key, claims, opts...) +} + +var allowedAlgorithms = map[string]bool{ + "HS256": true, + "HS384": true, + "HS512": true, + + "RS256": true, + "RS384": true, + "RS512": true, + + "ES256": true, + "ES384": true, + "ES512": true, + "EdDSA": true, +} + +func GetSigningMethod(algorithm string) jwt.SigningMethod { + if !allowedAlgorithms[algorithm] { + return nil + } + return jwt.GetSigningMethod(algorithm) +} + +// CreateSigningKey creates a signing key from an algorithm / key pair. +func CreateSigningKey(algorithm string, key any) (SigningKey, error) { + signingMethod := GetSigningMethod(algorithm) + if signingMethod == nil { + return nil, ErrInvalidAlgorithmType{algorithm} + } + + switch signingMethod.(type) { + case *jwt.SigningMethodEd25519: + privateKey, ok := key.(ed25519.PrivateKey) + if !ok { + return nil, jwt.ErrInvalidKeyType + } + return newEdDSASigningKey(signingMethod, privateKey) + case *jwt.SigningMethodECDSA: + privateKey, ok := key.(*ecdsa.PrivateKey) + if !ok { + return nil, jwt.ErrInvalidKeyType + } + return newECDSASigningKey(signingMethod, privateKey) + case *jwt.SigningMethodRSA: + privateKey, ok := key.(*rsa.PrivateKey) + if !ok { + return nil, jwt.ErrInvalidKeyType + } + return newRSASigningKey(signingMethod, privateKey) + default: + secret, ok := key.([]byte) + if !ok { + return nil, jwt.ErrInvalidKeyType + } + return hmacSigningKey{signingMethod, secret}, nil + } +} + +func createAsymmetricKey(keyPath, algorithm string) error { + key, err := func() (any, error) { + switch { + case strings.HasPrefix(algorithm, "RS"): + var bits int + switch algorithm { + case "RS256": + bits = 2048 + case "RS384": + bits = 3072 + case "RS512": + bits = 4096 + } + return rsa.GenerateKey(rand.Reader, bits) + case algorithm == "EdDSA": + _, pk, err := ed25519.GenerateKey(rand.Reader) + return pk, err + default: + var curve elliptic.Curve + switch algorithm { + case "ES256": + curve = elliptic.P256() + case "ES384": + curve = elliptic.P384() + case "ES512": + curve = elliptic.P521() + } + return ecdsa.GenerateKey(curve, rand.Reader) + } + }() + if err != nil { + return err + } + + bytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return err + } + + privateKeyPEM := &pem.Block{Type: "PRIVATE KEY", Bytes: bytes} + + if err := os.MkdirAll(filepath.Dir(keyPath), os.ModePerm); err != nil { + return err + } + + f, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) + if err != nil { + return err + } + defer func() { + if err = f.Close(); err != nil { + log.Error("Close: %v", err) + } + }() + + return pem.Encode(f, privateKeyPEM) +} + +func loadAsymmetricKey(keyPath string) (any, error) { + bytes, err := os.ReadFile(keyPath) + if err != nil { + return nil, err + } + + block, _ := pem.Decode(bytes) + if block == nil { + return nil, fmt.Errorf("no valid PEM data found in %s", keyPath) + } else if block.Type != "PRIVATE KEY" { + return nil, fmt.Errorf("expected PRIVATE KEY, got %s in %s", block.Type, keyPath) + } + + return x509.ParsePKCS8PrivateKey(block.Bytes) +} + +// loadOrCreateAsymmetricKey checks if the configured private key exists. +// If it does not exist a new random key gets generated and saved on the configured path. +func loadOrCreateAsymmetricKey(keyPath, algorithm string) (any, error) { + isExist, err := util.IsExist(keyPath) + if err != nil { + return nil, fmt.Errorf("Unable to check if %s exists. Error: %v", keyPath, err) + } + if !isExist { + err := createAsymmetricKey(keyPath, algorithm) + if err != nil { + return nil, fmt.Errorf("Error generating private key %s: %v", keyPath, err) + } + } + return loadAsymmetricKey(keyPath) +} + +// InitSigningKey creates a signing key from SigningKeyCfg +// cfgP is set to nil to mark that is has been processed +func InitSigningKey(cfgP **SigningKeyCfg) (SigningKey, error) { + cfg := *cfgP + *cfgP = nil + var err error + var key SigningKey + + if IsValidSymmetricAlgorithm(cfg.Algorithm) { + key, err = CreateSigningKey(cfg.Algorithm, *cfg.SecretBytes) + } else if IsValidAsymmetricAlgorithm(cfg.Algorithm) { + key, err = InitAsymmetricSigningKey(*cfg.PrivateKeyPath, cfg.Algorithm) + } else { + // should never happen, setting.loadSigningKeyCfg() ensures + err = ErrInvalidAlgorithmType{Algorithm: cfg.Algorithm} + } + + return key, err +} + +var ( + ValidSymmetricAlgorighms = []string{"HS256", "HS384", "HS512"} + ValidAsymmetricAlgorithms = []string{"RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "EdDSA"} +) + +// IsValidSymmetricAlgorithm checks if the passed in algorithm is a supported symettric algorithm. +func IsValidSymmetricAlgorithm(algorithm string) bool { + return slices.Contains(ValidSymmetricAlgorighms, algorithm) +} + +// IsValidAsymmetricAlgorithm checks if the passed in algorithm is a supported asymmetric algorithm. +func IsValidAsymmetricAlgorithm(algorithm string) bool { + return slices.Contains(ValidAsymmetricAlgorithms, algorithm) +} + +// InitAsymmetricSigningKey creates an asymmetric signing key from settings or creates a random key. +func InitAsymmetricSigningKey(keyPath, algorithm string) (SigningKey, error) { + var err error + var key any + + if !IsValidAsymmetricAlgorithm(algorithm) { + return nil, ErrInvalidAlgorithmType{Algorithm: algorithm} + } + + key, err = loadOrCreateAsymmetricKey(keyPath, algorithm) + if err != nil { + return nil, fmt.Errorf("Error while loading or creating JWT key: %w", err) + } + + signingKey, err := CreateSigningKey(algorithm, key) + if err != nil { + return nil, err + } + + return signingKey, nil +} + +func requiredJWKStr(jwk map[string]any, key string) (string, error) { + vAny, ok := jwk[key] + if !ok { + return "", fmt.Errorf("JWK missing required field %q", key) + } + vStr, ok := vAny.(string) + if !ok { + return "", fmt.Errorf("JWK field %q must be string, but was %T", key, vAny) + } + return vStr, nil +} + +// Reconstructs public key from a JWKS entry (such as those produced by [SigningKey.ToJWK]), parsing the JWK output and +// returning a key object. The key object produced must be usable for [jwt.SigningMethod] interface's [Verify] method, +// for the related signing method -- an [rsa.PublicKey] object, an [ed25519.PublicKey] object, or [ecdsa.PublicKey] +// object, with the currently supported asymmetric algorithms. +func ParseJWKToPublicKey(jwk map[string]any) (any, error) { + kty := jwk["kty"] + + switch kty { + case "RSA": + eStr, err := requiredJWKStr(jwk, "e") + if err != nil { + return nil, err + } + nStr, err := requiredJWKStr(jwk, "n") + if err != nil { + return nil, err + } + eBytes, err := base64.RawURLEncoding.DecodeString(eStr) + if err != nil { + return nil, fmt.Errorf("invalid RSA JWK 'e' field: %w", err) + } + nBytes, err := base64.RawURLEncoding.DecodeString(nStr) + if err != nil { + return nil, fmt.Errorf("invalid RSA JWK 'n' field: %w", err) + } + pubKey := &rsa.PublicKey{ + E: int(new(big.Int).SetBytes(eBytes).Int64()), + N: new(big.Int).SetBytes(nBytes), + } + return pubKey, nil + case "OKP": + if jwk["crv"] != "Ed25519" { + return nil, fmt.Errorf("OKP curve %d is not supported; only Ed25519", jwk["crv"]) + } + xStr, err := requiredJWKStr(jwk, "x") + if err != nil { + return nil, err + } + xBytes, err := base64.RawURLEncoding.DecodeString(xStr) + if err != nil { + return nil, fmt.Errorf("invalid EdDSA JWK 'x' field: %w", err) + } + return ed25519.PublicKey(xBytes), nil + case "EC": + xStr, err := requiredJWKStr(jwk, "x") + if err != nil { + return nil, err + } + yStr, err := requiredJWKStr(jwk, "y") + if err != nil { + return nil, err + } + var curve elliptic.Curve + switch jwk["crv"] { + case "P-256": + curve = elliptic.P256() + case "P-384": + curve = elliptic.P384() + case "P-521": + curve = elliptic.P521() + default: + return nil, fmt.Errorf("unsupported ECDSA curve in JWK: %s", jwk["crv"]) + } + xBytes, err := base64.RawURLEncoding.DecodeString(xStr) + if err != nil { + return nil, fmt.Errorf("invalid ECDSA JWK 'x' field: %w", err) + } + yBytes, err := base64.RawURLEncoding.DecodeString(yStr) + if err != nil { + return nil, fmt.Errorf("invalid ECDSA JWK 'y' field: %w", err) + } + pubKey := &ecdsa.PublicKey{ + Curve: curve, + X: new(big.Int).SetBytes(xBytes), + Y: new(big.Int).SetBytes(yBytes), + } + return pubKey, nil + default: + return nil, fmt.Errorf("unsupported key type in JWK: %s", kty) + } +} diff --git a/modules/jwtx/signingkey_test.go b/modules/jwtx/signingkey_test.go new file mode 100644 index 0000000000..96f6613b84 --- /dev/null +++ b/modules/jwtx/signingkey_test.go @@ -0,0 +1,186 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package jwtx + +import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" + + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func testSignVerify(t *testing.T, signKey, verifyKey SigningKey) { + t.Helper() + // test sign and verify + claimsIn := jwt.RegisteredClaims{ + Issuer: "abc", + ID: "0815", + } + token, err := signKey.JWT(claimsIn) + require.NoError(t, err) + require.NotEmpty(t, token) + + var claimsOut jwt.RegisteredClaims + parsed, err := jwt.ParseWithClaims(token, &claimsOut, func(valToken *jwt.Token) (any, error) { + assert.NotNil(t, valToken.Method) + assert.Equal(t, signKey.SigningMethod().Alg(), valToken.Method.Alg()) + assert.Equal(t, verifyKey.SigningMethod().Alg(), valToken.Method.Alg()) + kid, ok := valToken.Header["kid"] + assert.True(t, ok) + assert.NotNil(t, kid) + + return verifyKey.VerifyKey(), nil + }) + require.NoError(t, err) + assert.NotNil(t, parsed) + assert.Equal(t, claimsIn, claimsOut) + assert.Equal(t, &claimsIn, parsed.Claims) +} + +// creates private key +// loads it back from the file +func TestLoadOrCreateAsymmetricKey(t *testing.T) { + loadKey := func(t *testing.T, keyPath, algorithm string) any { + t.Helper() + loadOrCreateAsymmetricKey(keyPath, algorithm) + + fileContent, err := os.ReadFile(keyPath) + require.NoError(t, err) + + block, _ := pem.Decode(fileContent) + assert.NotNil(t, block) + assert.Equal(t, "PRIVATE KEY", block.Type) + + parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + require.NoError(t, err) + + return parsedKey + } + useKey := func(t *testing.T, keyPath, algorithm string) { + t.Helper() + // duplicates loadKey() to some extent, but uses SigningKey + cfg := &SigningKeyCfg{ + Algorithm: algorithm, + PrivateKeyPath: &keyPath, + } + + key, err := InitSigningKey(&cfg) + require.NoError(t, err) + assert.NotNil(t, key) + assert.Nil(t, cfg) + + testSignVerify(t, key, key) + } + t.Run("RSA-2048", func(t *testing.T) { + keyPath := filepath.Join(t.TempDir(), "jwt-rsa-2048.priv") + algorithm := "RS256" + + parsedKey := loadKey(t, keyPath, algorithm) + + rsaPrivateKey := parsedKey.(*rsa.PrivateKey) + assert.Equal(t, 2048, rsaPrivateKey.N.BitLen()) + t.Run("Use", func(t *testing.T) { + useKey(t, keyPath, algorithm) + }) + + t.Run("Load key with differ specified algorithm", func(t *testing.T) { + algorithm = "EdDSA" + + parsedKey := loadKey(t, keyPath, algorithm) + rsaPrivateKey := parsedKey.(*rsa.PrivateKey) + assert.Equal(t, 2048, rsaPrivateKey.N.BitLen()) + }) + }) + + t.Run("RSA-3072", func(t *testing.T) { + keyPath := filepath.Join(t.TempDir(), "jwt-rsa-3072.priv") + algorithm := "RS384" + + parsedKey := loadKey(t, keyPath, algorithm) + + rsaPrivateKey := parsedKey.(*rsa.PrivateKey) + assert.Equal(t, 3072, rsaPrivateKey.N.BitLen()) + t.Run("Use", func(t *testing.T) { + useKey(t, keyPath, algorithm) + }) + }) + + t.Run("RSA-4096", func(t *testing.T) { + keyPath := filepath.Join(t.TempDir(), "jwt-rsa-4096.priv") + algorithm := "RS512" + + parsedKey := loadKey(t, keyPath, algorithm) + + rsaPrivateKey := parsedKey.(*rsa.PrivateKey) + assert.Equal(t, 4096, rsaPrivateKey.N.BitLen()) + t.Run("Use", func(t *testing.T) { + useKey(t, keyPath, algorithm) + }) + }) + + t.Run("ECDSA-256", func(t *testing.T) { + keyPath := filepath.Join(t.TempDir(), "jwt-ecdsa-256.priv") + algorithm := "ES256" + + parsedKey := loadKey(t, keyPath, algorithm) + + ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey) + assert.Equal(t, 256, ecdsaPrivateKey.Params().BitSize) + t.Run("Use", func(t *testing.T) { + useKey(t, keyPath, algorithm) + }) + }) + + t.Run("ECDSA-384", func(t *testing.T) { + keyPath := filepath.Join(t.TempDir(), "jwt-ecdsa-384.priv") + algorithm := "ES384" + + parsedKey := loadKey(t, keyPath, algorithm) + + ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey) + assert.Equal(t, 384, ecdsaPrivateKey.Params().BitSize) + t.Run("Use", func(t *testing.T) { + useKey(t, keyPath, algorithm) + }) + }) + + t.Run("ECDSA-512", func(t *testing.T) { + keyPath := filepath.Join(t.TempDir(), "jwt-ecdsa-512.priv") + algorithm := "ES512" + + parsedKey := loadKey(t, keyPath, algorithm) + + ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey) + assert.Equal(t, 521, ecdsaPrivateKey.Params().BitSize) + t.Run("Use", func(t *testing.T) { + useKey(t, keyPath, algorithm) + }) + }) + + t.Run("EdDSA", func(t *testing.T) { + keyPath := filepath.Join(t.TempDir(), "jwt-eddsa.priv") + algorithm := "EdDSA" + + parsedKey := loadKey(t, keyPath, algorithm) + + assert.NotNil(t, parsedKey.(ed25519.PrivateKey)) + t.Run("Use", func(t *testing.T) { + useKey(t, keyPath, algorithm) + }) + }) +} + +func TestCannotCreatePrivateKey(t *testing.T) { + _, err := InitAsymmetricSigningKey("/dev/directory-does-not-exist-and-you-should-not-have-permission-to-create/privatekey.pem", "RS256") + require.Error(t, err) + require.ErrorContains(t, err, "Error generating private key") +} diff --git a/modules/keying/keying.go b/modules/keying/keying.go index 0d7eecb904..14fbaaca98 100644 --- a/modules/keying/keying.go +++ b/modules/keying/keying.go @@ -39,6 +39,10 @@ var ( ActionSecret = deriveKey("action_secret") // Used for the `task` table where type == TaskTypeMigrateRepo. MigrateTask = deriveKey("migrate_repo_task") + // Used for the `webhook` table. + Webhook = deriveKey("webhook") + // Used for the `mirror` table. + PullMirror = deriveKey("pullmirror") ) var ( @@ -149,7 +153,7 @@ func ColumnAndID(column string, id int64) []byte { // it's not bound to a particular table. The table should be part of the context // that the key was derived for, in which case it binds through that. Use this // over `ColumnAndID` if you're encrypting data that's stored inside JSON. -// jsonSelector must be a unambigous selector to the JSON field that stores the +// jsonSelector must be a unambiguous selector to the JSON field that stores the // encrypted data. func ColumnAndJSONSelectorAndID(column, jsonSelector string, id int64) []byte { return binary.BigEndian.AppendUint64(append(append([]byte(column), ':'), append([]byte(jsonSelector), ':')...), uint64(id)) diff --git a/modules/label/parser.go b/modules/label/parser.go index 12fc176967..b27b2c9ee6 100644 --- a/modules/label/parser.go +++ b/modules/label/parser.go @@ -72,7 +72,7 @@ func parseYamlFormat(fileName string, data []byte) ([]*Label, error) { func parseLegacyFormat(fileName string, data []byte) ([]*Label, error) { lines := strings.Split(string(data), "\n") list := make([]*Label, 0, len(lines)) - for i := 0; i < len(lines); i++ { + for i := range lines { line := strings.TrimSpace(lines[i]) if len(line) == 0 { continue @@ -108,7 +108,7 @@ func LoadTemplateDescription(fileName string) (string, error) { return "", err } - for i := 0; i < len(list); i++ { + for i := range list { if i > 0 { buf.WriteString(", ") } diff --git a/modules/log/color_console_other.go b/modules/log/color_console_other.go index 6573d093a5..7505deef61 100644 --- a/modules/log/color_console_other.go +++ b/modules/log/color_console_other.go @@ -39,7 +39,7 @@ func fileStatDevIno(file *os.File) (uint64, uint64, bool) { // Do a type conversion to uint64, because Dev isn't always uint64 // on every operating system + architecture combination. - return uint64(stat.Dev), stat.Ino, true //nolint:unconvert + return uint64(stat.Dev), stat.Ino, true //nolint:unconvert,nolintlint } func fileIsDevIno(file *os.File, dev, ino uint64) bool { diff --git a/modules/log/event_format.go b/modules/log/event_format.go index 6835a4ca5b..70df2cbce2 100644 --- a/modules/log/event_format.go +++ b/modules/log/event_format.go @@ -208,7 +208,7 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms } } if hasColorValue { - msg = []byte(fmt.Sprintf(msgFormat, msgArgs...)) + msg = fmt.Appendf(nil, msgFormat, msgArgs...) } } // try to reuse the pre-formatted simple text message @@ -227,8 +227,8 @@ func EventFormatTextMessage(mode *WriterMode, event *Event, msgFormat string, ms buf = append(buf, msg...) if event.Stacktrace != "" && mode.StacktraceLevel <= event.Level { - lines := bytes.Split([]byte(event.Stacktrace), []byte("\n")) - for _, line := range lines { + lines := bytes.SplitSeq([]byte(event.Stacktrace), []byte("\n")) + for line := range lines { buf = append(buf, "\n\t"...) buf = append(buf, line...) } diff --git a/modules/log/event_writer_base.go b/modules/log/event_writer_base.go index 4de2b953c7..561166b2ad 100644 --- a/modules/log/event_writer_base.go +++ b/modules/log/event_writer_base.go @@ -80,7 +80,9 @@ func (b *EventWriterBaseImpl) Run(ctx context.Context) { if pause := b.GetPauseChan(); pause != nil { select { case <-pause: + break case <-ctx.Done(): + break } } } @@ -178,6 +180,7 @@ func eventWriterStopWait(w EventWriter) { close(w.Base().Queue) select { case <-w.Base().stopped: + break case <-time.After(2 * time.Second): FallbackErrorf("unable to stop log writer %q in time, skip", w.GetWriterName()) } diff --git a/modules/log/event_writer_buffer.go b/modules/log/event_writer_buffer.go index 28857c2189..a7557618a8 100644 --- a/modules/log/event_writer_buffer.go +++ b/modules/log/event_writer_buffer.go @@ -5,18 +5,44 @@ package log import ( "bytes" + "sync" ) -type EventWriterBuffer struct { - *EventWriterBaseImpl - Buffer *bytes.Buffer +type EventWriterBuffer interface { + EventWriter + GetString() string } -var _ EventWriter = (*EventWriterBuffer)(nil) +type eventWriterBuffer struct { + *EventWriterBaseImpl + buffer *bytes.Buffer + mu sync.RWMutex +} -func NewEventWriterBuffer(name string, mode WriterMode) *EventWriterBuffer { - w := &EventWriterBuffer{EventWriterBaseImpl: NewEventWriterBase(name, "buffer", mode)} - w.Buffer = new(bytes.Buffer) - w.OutputWriteCloser = nopCloser{w.Buffer} +var _ EventWriterBuffer = (*eventWriterBuffer)(nil) + +func (*eventWriterBuffer) Close() error { + return nil +} + +func (o *eventWriterBuffer) Write(p []byte) (n int, err error) { + o.mu.Lock() + defer o.mu.Unlock() + return o.buffer.Write(p) +} + +func (o *eventWriterBuffer) GetString() string { + o.mu.Lock() + defer o.mu.Unlock() + b := o.buffer.Bytes() + s := make([]byte, len(b)) + copy(s, b) + return string(s) +} + +func NewEventWriterBuffer(name string, mode WriterMode) EventWriter { + w := &eventWriterBuffer{EventWriterBaseImpl: NewEventWriterBase(name, "buffer", mode)} + w.buffer = new(bytes.Buffer) + w.OutputWriteCloser = w return w } diff --git a/modules/log/event_writer_buffer_test.go b/modules/log/event_writer_buffer_test.go index d1e37c3673..ac0759ad0b 100644 --- a/modules/log/event_writer_buffer_test.go +++ b/modules/log/event_writer_buffer_test.go @@ -29,7 +29,7 @@ func TestBufferLogger(t *testing.T) { MsgSimpleText: expected, }) logger.Close() - assert.Contains(t, bufferWriter.Buffer.String(), expected) + assert.Contains(t, bufferWriter.(log.EventWriterBuffer).GetString(), expected) } func TestBufferLoggerWithExclusion(t *testing.T) { @@ -41,7 +41,7 @@ func TestBufferLoggerWithExclusion(t *testing.T) { Level: level, Prefix: prefix, Exclusion: message, - }) + }).(log.EventWriterBuffer) logger := log.NewLoggerWithWriters(t.Context(), "test", bufferWriter) @@ -50,7 +50,7 @@ func TestBufferLoggerWithExclusion(t *testing.T) { MsgSimpleText: message, }) logger.Close() - assert.NotContains(t, bufferWriter.Buffer.String(), message) + assert.NotContains(t, bufferWriter.GetString(), message) } func TestBufferLoggerWithExpressionAndExclusion(t *testing.T) { @@ -64,7 +64,7 @@ func TestBufferLoggerWithExpressionAndExclusion(t *testing.T) { Prefix: prefix, Expression: expression, Exclusion: exclusion, - }) + }).(log.EventWriterBuffer) logger := log.NewLoggerWithWriters(t.Context(), "test", bufferWriter) @@ -74,6 +74,6 @@ func TestBufferLoggerWithExpressionAndExclusion(t *testing.T) { logger.SendLogEvent(&log.Event{Level: log.INFO, MsgSimpleText: "none"}) logger.Close() - assert.Contains(t, bufferWriter.Buffer.String(), "foo expression") - assert.NotContains(t, bufferWriter.Buffer.String(), "bar") + assert.Contains(t, bufferWriter.GetString(), "foo expression") + assert.NotContains(t, bufferWriter.GetString(), "bar") } diff --git a/modules/log/event_writer_conn_test.go b/modules/log/event_writer_conn_test.go index 0cf447149a..6d528a68d1 100644 --- a/modules/log/event_writer_conn_test.go +++ b/modules/log/event_writer_conn_test.go @@ -63,11 +63,9 @@ func TestConnLogger(t *testing.T) { } expected := fmt.Sprintf("%s%s %s:%d:%s [%c] %s\n", prefix, dateString, event.Filename, event.Line, event.Caller, strings.ToUpper(event.Level.String())[0], event.MsgSimpleText) var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { listenReadAndClose(t, l, expected) - }() + }) logger.SendLogEvent(&event) wg.Wait() diff --git a/modules/log/flags.go b/modules/log/flags.go index 1e4fe830c1..c428d58a1d 100644 --- a/modules/log/flags.go +++ b/modules/log/flags.go @@ -124,7 +124,7 @@ func FlagsFromString(from string, def ...uint32) Flags { return Flags{defined: true, flags: def[0]} } flags := uint32(0) - for _, flag := range strings.Split(strings.ToLower(from), ",") { + for flag := range strings.SplitSeq(strings.ToLower(from), ",") { flags |= flagFromString[strings.TrimSpace(flag)] } return Flags{defined: true, flags: flags} diff --git a/modules/log/level_test.go b/modules/log/level_test.go index e6cacc723b..73e2355960 100644 --- a/modules/log/level_test.go +++ b/modules/log/level_test.go @@ -33,11 +33,11 @@ func TestLevelMarshalUnmarshalJSON(t *testing.T) { require.NoError(t, err) assert.Equal(t, INFO, testLevel.Level) - err = json.Unmarshal([]byte(fmt.Sprintf(`{"level":%d}`, 2)), &testLevel) + err = json.Unmarshal(fmt.Appendf(nil, `{"level":%d}`, 2), &testLevel) require.NoError(t, err) assert.Equal(t, INFO, testLevel.Level) - err = json.Unmarshal([]byte(fmt.Sprintf(`{"level":%d}`, 10012)), &testLevel) + err = json.Unmarshal(fmt.Appendf(nil, `{"level":%d}`, 10012), &testLevel) require.NoError(t, err) assert.Equal(t, INFO, testLevel.Level) @@ -52,5 +52,5 @@ func TestLevelMarshalUnmarshalJSON(t *testing.T) { } func makeTestLevelBytes(level string) []byte { - return []byte(fmt.Sprintf(`{"level":"%s"}`, level)) + return fmt.Appendf(nil, `{"level":"%s"}`, level) } diff --git a/modules/log/logger_impl.go b/modules/log/logger_impl.go index 76d7e6a821..9f25f9f7fb 100644 --- a/modules/log/logger_impl.go +++ b/modules/log/logger_impl.go @@ -58,6 +58,7 @@ func (l *LoggerImpl) SendLogEvent(event *Event) { } select { case w.Base().Queue <- formatted: + break default: bs, _ := json.Marshal(event) FallbackErrorf("log writer %q queue is full, event: %v", w.GetWriterName(), string(bs)) diff --git a/modules/log/logger_impl_test.go b/modules/log/logger_impl_test.go index 59276a83f4..1d77ae0c25 100644 --- a/modules/log/logger_impl_test.go +++ b/modules/log/logger_impl_test.go @@ -23,5 +23,5 @@ func TestLog(t *testing.T) { testGeneric(logger, "I'm the generic value!") logger.Close() - assert.Contains(t, bufferWriter.Buffer.String(), ".../logger_impl_test.go:13:testGeneric() [I] Just testing the logging of a generic function I'm the generic value!") + assert.Contains(t, bufferWriter.(EventWriterBuffer).GetString(), ".../logger_impl_test.go:13:testGeneric() [I] Just testing the logging of a generic function I'm the generic value!") } diff --git a/modules/markup/file_preview.go b/modules/markup/file_preview.go index dab6057cf4..22dcf93d75 100644 --- a/modules/markup/file_preview.go +++ b/modules/markup/file_preview.go @@ -80,8 +80,8 @@ func newFilePreview(ctx *RenderContext, node *html.Node, locale translation.Loca filePath := node.Data[m[6]:m[7]] hash := node.Data[m[8]:m[9]] urlFullSource := urlFull - if strings.HasSuffix(filePath, "?display=source") { - filePath = strings.TrimSuffix(filePath, "?display=source") + if before, ok := strings.CutSuffix(filePath, "?display=source"); ok { + filePath = before } else if Type(filePath) != "" { urlFullSource = node.Data[m[0]:m[6]] + filePath + "?display=source#" + hash } diff --git a/modules/markup/html.go b/modules/markup/html.go index 2d569725d7..5bca4c4596 100644 --- a/modules/markup/html.go +++ b/modules/markup/html.go @@ -11,6 +11,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "strings" "sync" @@ -52,7 +53,7 @@ var ( // hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae // Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length // so that abbreviated hash links can be used as well. This matches git and GitHub usability. - hashCurrentPattern = regexp.MustCompile(`(?:^|\s)[^\w\d]{0,2}([0-9a-f]{7,64})[^\w\d]{0,2}(?:\s|$)`) + hashCurrentPattern = regexp.MustCompile(`(?:^|\s)[^\w\d]*([0-9a-f]{7,64})[^\w\d]*(?:\s|$)`) // shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) @@ -124,13 +125,7 @@ func CustomLinkURLSchemes(schemes []string) { if !validScheme.MatchString(s) { continue } - without := false - for _, sna := range xurls.SchemesNoAuthority { - if s == sna { - without = true - break - } - } + without := slices.Contains(xurls.SchemesNoAuthority, s) if without { s += ":" } else { @@ -300,7 +295,7 @@ func RenderDescriptionHTML( descriptionLinkProcessor, emojiShortCodeProcessor, emojiProcessor, - }, content) + }, escapeInlineCodeBlocks(content)) } // RenderEmoji for when we want to just process emoji and shortcodes @@ -675,9 +670,9 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { // It makes page handling terrible, but we prefer GitHub syntax // And fall back to MediaWiki only when it is obvious from the look // Of text and link contents - sl := strings.Split(content, "|") - for _, v := range sl { - if equalPos := strings.IndexByte(v, '='); equalPos == -1 { + sl := strings.SplitSeq(content, "|") + for v := range sl { + if found := strings.Contains(v, "="); !found { // There is no equal in this argument; this is a mandatory arg if props["name"] == "" { if IsLinkStr(v) { @@ -699,8 +694,8 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { } else { // There is an equal; optional argument. - sep := strings.IndexByte(v, '=') - key, val := v[:sep], html.UnescapeString(v[sep+1:]) + before, after, _ := strings.Cut(v, "=") + key, val := before, html.UnescapeString(after) // When parsing HTML, x/net/html will change all quotes which are // not used for syntax into UTF-8 quotes. So checking val[0] won't @@ -742,6 +737,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { // fast path: empty string, ignore case "": // leave image as false + break case ".jpg", ".jpeg", ".png", ".tif", ".tiff", ".webp", ".gif", ".bmp", ".ico", ".svg": image = true } @@ -1147,7 +1143,7 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { } // Ensure that every group (m[0]...m[9]) has a match - for i := 0; i < 10; i++ { + for i := range 10 { if m[i] == -1 { return } @@ -1543,3 +1539,9 @@ func optionalRepoSlugAndInstancePath(ctx *RenderContext, text *string, fullURL, } } } + +// escapeInlineCodeBlocks escapes HTML symbols in contents of Markdown inline code blocks +// to prevent clashing with HTML parsing +func escapeInlineCodeBlocks(input string) string { + return InlineCodeBlockRegex.ReplaceAllStringFunc(input, html.EscapeString) +} diff --git a/modules/markup/html_internal_test.go b/modules/markup/html_internal_test.go index 752ffeba6f..570e8c5e68 100644 --- a/modules/markup/html_internal_test.go +++ b/modules/markup/html_internal_test.go @@ -486,6 +486,9 @@ func TestRegExp_hashCurrentPattern(t *testing.T) { ":abcd3ef", ".abcd3ef", " (abcd3ef). ", + "abcd3ef...", + "...abcd3ef", + "(!...abcd3ef", } falseTestCases := []string{ "test", @@ -495,6 +498,7 @@ func TestRegExp_hashCurrentPattern(t *testing.T) { "abcdefghijklmnopqrstuvwxyzabcdefghijklmO", "commit/abcdefd", "abcd3ef...defabcd", + "f..defabcd", } for _, testCase := range trueTestCases { @@ -611,3 +615,30 @@ func TestRegExp_shortLinkPattern(t *testing.T) { assert.False(t, shortLinkPattern.MatchString(testCase)) } } + +func TestRender_escapeInlineCodeBlocks(t *testing.T) { + test := func(input, expected string) { + result := escapeInlineCodeBlocks(input) + assert.Equal(t, expected, result) + } + test("``", + "`<test>`") + test("", + "") + test("`` ``", + "`<foo>` `<baz>`") + test(" `` ", + " `<bar>` ") + test(" ` ", + " ` ") + test(" `` `", + " `<bar>` `") + test(" `` `", + " `<bar>` `") + test(" `` ``", + " `<bar>` ``") + test("```", + "```") + test("``<`", + "``<`") +} diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index 8305ada25e..58e230b059 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -57,8 +57,10 @@ func TestRender_Commits(t *testing.T) { } sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" + shaWithExtra := "65f1bf27bc3bf70f64657658635e66094edbcb4d..." repo := markup.TestRepoURL commit := util.URLJoin(repo, "commit", sha) + commitWithExtra := util.URLJoin(repo, "commit", shaWithExtra) tree := util.URLJoin(repo, "tree", sha, "src") file := util.URLJoin(repo, "commit", sha, "example.txt") @@ -69,9 +71,11 @@ func TestRender_Commits(t *testing.T) { commitCompareWithHash := commitCompare + "#L2" test(sha, `

65f1bf27bc

`) + test(shaWithExtra, `

65f1bf27bc...

`) test(sha[:7], `

65f1bf2

`) test(sha[:39], `

65f1bf27bc

`) test(commit, `

65f1bf27bc

`) + test(commitWithExtra, `

65f1bf27bc...

`) test(tree, `

65f1bf27bc/src

`) test(file, `

65f1bf27bc/example.txt

`) @@ -805,7 +809,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -839,7 +843,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -918,7 +922,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -949,7 +953,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -982,7 +986,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -1007,7 +1011,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -1038,7 +1042,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -1063,7 +1067,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -1088,7 +1092,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -1123,7 +1127,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `B`+"\n"+``+ + `B`+"\n"+``+ ``+ ``+ ``+ @@ -1156,7 +1160,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `B`+"\n"+``+ + `B`+"\n"+``+ ``+ ``+ ``+ @@ -1189,7 +1193,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `B`+"\n"+``+ + `B`+"\n"+``+ ``+ ``+ ``+ @@ -1224,7 +1228,7 @@ func TestRender_FilePreview(t *testing.T) { ``+ ``+ ``+ - `C`+"\n"+``+ + `C`+"\n"+``+ ``+ ``+ ``+ @@ -1325,3 +1329,42 @@ func TestRender_FilePreview(t *testing.T) { ) }) } + +func TestRenderDescriptionHTML(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, markup.TestAppURL)() + + test := func(input, expected string) { + buffer, err := markup.RenderDescriptionHTML(&markup.RenderContext{ + Ctx: git.DefaultContext, + }, input) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + + markup.InitializeSanitizer() + + test( + "https://www.example.com", + `https://www.example.com`) + + test( + "Example repository with `Arc`", + "Example repository with `Arc`") + + test( + "Example repository with `Arc` and tools.", + "Example repository with `Arc` and tools.") + + test( + "`Arc` implements", + "`Arc<Test>` implements") + + test( + "Arc is broken", + "Arc is broken") + + // issue #10770 + test( + "A weird alternative to `Arc>`", + "A weird alternative to `Arc<RwLock<T>>`") +} diff --git a/modules/markup/markdown/goldmark.go b/modules/markup/markdown/goldmark.go index 1ea3375ab5..aec710fd46 100644 --- a/modules/markup/markdown/goldmark.go +++ b/modules/markup/markdown/goldmark.go @@ -90,6 +90,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } case *ast.RawHTML: g.transformRawHTML(ctx, v, reader) + case *ast.FencedCodeBlock: + g.transformCodeblockLanguage(v, reader) } return ast.WalkContinue, nil }) diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 2b19e0f1c9..9a112109dd 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -182,10 +182,7 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer) } buf, _ = ExtractMetadataBytes(buf, rc) - metaLength := bufWithMetadataLength - len(buf) - if metaLength < 0 { - metaLength = 0 - } + metaLength := max(bufWithMetadataLength-len(buf), 0) rc.metaLength = metaLength pc.Set(markdownutil.RenderConfigKey, rc) diff --git a/modules/markup/markdown/markdown_test.go b/modules/markup/markdown/markdown_test.go index 82c2c7fe8c..3aee7a372d 100644 --- a/modules/markup/markdown/markdown_test.go +++ b/modules/markup/markdown/markdown_test.go @@ -319,7 +319,7 @@ func TestTotal_RenderWiki(t *testing.T) { answers := testAnswers(util.URLJoin(FullURL, "wiki"), util.URLJoin(FullURL, "wiki", "raw")) - for i := 0; i < len(sameCases); i++ { + for i := range sameCases { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ @@ -363,7 +363,7 @@ func TestTotal_RenderString(t *testing.T) { answers := testAnswers(util.URLJoin(FullURL, "src", "master"), util.URLJoin(FullURL, "media", "master")) - for i := 0; i < len(sameCases); i++ { + for i := range sameCases { line, err := markdown.RenderString(&markup.RenderContext{ Ctx: git.DefaultContext, Links: markup.Links{ @@ -1488,3 +1488,61 @@ func TestCallout(t *testing.T) {

Bad stuff is brewing here

`) } + +func TestCodeblockLanguageTransformation(t *testing.T) { + test := func(input, expected string) { + buffer, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, input) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) + } + + // No transformation + test( + "```rust\n"+ + "fn main() {}\n"+ + "```", + `
fn main() {}
+
`) + + // Comma stripped + test( + "```rust,ignore\n"+ + "fn main() {}\n"+ + "```", + `
fn main() {}
+
`) + + // Pandoc stripping + // https://pandoc.org/MANUAL.html#extension-fenced_code_attributes + test( + "```haskell {.numberLines}\n"+ + "qsort [] = []\n"+ + "qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++\n"+ + " qsort (filter (>= x) xs)\n"+ + "```", + `
qsort []     = []
+qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
+               qsort (filter (>= x) xs)
+
`) + + // Pandoc language extracting + // https://pandoc.org/MANUAL.html#extension-fenced_code_attributes + test( + "``` { #mycode .numberLines .haskell startFrom=\"100\" } \n"+ + "qsort [] = []\n"+ + "qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++\n"+ + " qsort (filter (>= x) xs)\n"+ + "```", + `
qsort []     = []
+qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
+               qsort (filter (>= x) xs)
+
`) + + // No language identifier + test( + "```\n"+ + "fn main() {}\n"+ + "```", + `
fn main() {}
+
`) +} diff --git a/modules/markup/markdown/math/block_renderer.go b/modules/markup/markdown/math/block_renderer.go index 84817ef1e4..d27318c623 100644 --- a/modules/markup/markdown/math/block_renderer.go +++ b/modules/markup/markdown/math/block_renderer.go @@ -24,7 +24,7 @@ func (r *BlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { func (r *BlockRenderer) writeLines(w util.BufWriter, source []byte, n gast.Node) { l := n.Lines().Len() - for i := 0; i < l; i++ { + for i := range l { line := n.Lines().At(i) _, _ = w.Write(util.EscapeHTML(line.Value(source))) } diff --git a/modules/markup/markdown/meta_test.go b/modules/markup/markdown/meta_test.go index aaf116ff20..9345dd528a 100644 --- a/modules/markup/markdown/meta_test.go +++ b/modules/markup/markdown/meta_test.go @@ -63,7 +63,7 @@ func TestExtractMetadata(t *testing.T) { func TestExtractMetadataBytes(t *testing.T) { t.Run("ValidFrontAndBody", func(t *testing.T) { var meta IssueTemplate - body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest)), &meta) + body, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s\n%s", sepTest, frontTest, sepTest, bodyTest), &meta) require.NoError(t, err) assert.Equal(t, bodyTest, string(body)) assert.Equal(t, metaTest, meta) @@ -72,19 +72,19 @@ func TestExtractMetadataBytes(t *testing.T) { t.Run("NoFirstSeparator", func(t *testing.T) { var meta IssueTemplate - _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", frontTest, sepTest, bodyTest)), &meta) + _, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", frontTest, sepTest, bodyTest), &meta) require.Error(t, err) }) t.Run("NoLastSeparator", func(t *testing.T) { var meta IssueTemplate - _, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, bodyTest)), &meta) + _, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", sepTest, frontTest, bodyTest), &meta) require.Error(t, err) }) t.Run("NoBody", func(t *testing.T) { var meta IssueTemplate - body, err := ExtractMetadataBytes([]byte(fmt.Sprintf("%s\n%s\n%s", sepTest, frontTest, sepTest)), &meta) + body, err := ExtractMetadataBytes(fmt.Appendf(nil, "%s\n%s\n%s", sepTest, frontTest, sepTest), &meta) require.NoError(t, err) assert.Empty(t, string(body)) assert.Equal(t, metaTest, meta) diff --git a/modules/markup/markdown/toc.go b/modules/markup/markdown/toc.go index dbfab3e9dc..53add219f5 100644 --- a/modules/markup/markdown/toc.go +++ b/modules/markup/markdown/toc.go @@ -44,7 +44,7 @@ func createTOCNode(toc []markup.Header, lang string, detailsAttrs map[string]str } li := ast.NewListItem(currentLevel * 2) a := ast.NewLink() - a.Destination = []byte(fmt.Sprintf("#%s", url.QueryEscape(header.ID))) + a.Destination = fmt.Appendf(nil, "#%s", url.QueryEscape(header.ID)) a.AppendChild(a, ast.NewString([]byte(header.Text))) li.AppendChild(li, a) ul.AppendChild(ul, li) diff --git a/modules/markup/markdown/transform_codeblock_lang.go b/modules/markup/markdown/transform_codeblock_lang.go new file mode 100644 index 0000000000..f730265b15 --- /dev/null +++ b/modules/markup/markdown/transform_codeblock_lang.go @@ -0,0 +1,57 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markdown + +import ( + "bytes" + + "github.com/alecthomas/chroma/v2/lexers" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/text" +) + +func (g *ASTTransformer) transformCodeblockLanguage(v *ast.FencedCodeBlock, reader text.Reader) { + if v.Info == nil { + return + } + src := reader.Source() + info := v.Info.Segment.Value(src) + + // Parse Pandoc style attributes + // https://pandoc.org/MANUAL.html#extension-fenced_code_attributes + // + // For example, + // ```{.haskell .numberLines} + // ... + // ``` + // Should have a language of "haskell", not "{.haskell .numberLines}" + if trimmed := bytes.TrimSpace(info); bytes.HasPrefix(trimmed, []byte{'{'}) && bytes.HasSuffix(trimmed, []byte{'}'}) { + attributes := trimmed[1 : len(trimmed)-1] + for attribute := range bytes.SplitSeq(attributes, []byte{' '}) { + if class, found := bytes.CutPrefix(attribute, []byte{'.'}); found { + if lexer := lexers.Get(string(class)); lexer != nil { + lang := class + langInx := bytes.Index(info, lang) + start := v.Info.Segment.Start + langInx + end := start + len(lang) + v.Info = ast.NewTextSegment(text.NewSegment(start, end)) + return + } + } + } + return + } + + // Strip language after commas + // + // For example, + // ```rust,ignore + // ... + // ``` + // Should have a language of "rust", not "rust,ignore" + if i := bytes.IndexByte(info, ','); i != -1 { + start := v.Info.Segment.Start + v.Info = ast.NewTextSegment(text.NewSegment(start, start+i)) + } +} diff --git a/modules/markup/markdown/transform_heading.go b/modules/markup/markdown/transform_heading.go index eedaf58556..16779d5099 100644 --- a/modules/markup/markdown/transform_heading.go +++ b/modules/markup/markdown/transform_heading.go @@ -17,7 +17,7 @@ import ( func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]markup.Header) { for _, attr := range v.Attributes() { if _, ok := attr.Value.([]byte); !ok { - v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value))) + v.SetAttribute(attr.Name, fmt.Appendf(nil, "%v", attr.Value)) } } txt := mdutil.Text(v, reader.Source()) diff --git a/modules/markup/orgmode/orgmode.go b/modules/markup/orgmode/orgmode.go index b9d7b21db0..dd92ca90d0 100644 --- a/modules/markup/orgmode/orgmode.go +++ b/modules/markup/orgmode/orgmode.go @@ -7,6 +7,7 @@ import ( "fmt" "html" "io" + "strconv" "strings" "forgejo.org/modules/highlight" @@ -159,6 +160,16 @@ func (r *Writer) resolveLink(node org.Node) string { switch l.Kind() { case "image", "video": base = r.Ctx.Links.ResolveMediaLink(r.Ctx.IsWiki) + case "regular": + // Convert line search syntax to line links + target, search, found := strings.Cut(link, "::") + if found { + if _, err := strconv.Atoi(search); err == nil { + link = target + "#L" + search + } else { + link = target + } + } } link = util.URLJoin(base, link) diff --git a/modules/markup/orgmode/orgmode_test.go b/modules/markup/orgmode/orgmode_test.go index 71157dc7c7..2cd4eada22 100644 --- a/modules/markup/orgmode/orgmode_test.go +++ b/modules/markup/orgmode/orgmode_test.go @@ -89,6 +89,43 @@ func TestRender_BaseLinks(t *testing.T) { `

./src/

`) } +func TestRender_SearchSuffix(t *testing.T) { + setting.AppURL = AppURL + setting.AppSubURL = AppSubURL + + test := func(input, expected string) { + buffer, err := RenderString(&markup.RenderContext{ + Ctx: git.DefaultContext, + Links: markup.Links{ + Base: setting.AppSubURL, + BranchPath: "branch/main", + }, + }, input) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + + // `::N` line search becomes an `#L` anchor. + test("[[./file.el::35][line 35]]", + `

line 35

`) + test("[[file:./file.el::35][line 35]]", + `

line 35

`) + + // Other search types are ignored. + test("[[./file.org::*Heading][heading]]", + `

heading

`) + test("[[./file.org::#custom-id][heading]]", + `

heading

`) + test("[[./file.el::/regex/][regex]]", + `

regex

`) + test("[[file:./file.el::][no search]]", + `

no search

`) + + // Absolute URLs that happen to contain `::` are unchanged. + test("[[https://example.com/foo::35][ext]]", + `

ext

`) +} + func TestRender_Media(t *testing.T) { setting.AppURL = AppURL setting.AppSubURL = AppSubURL @@ -153,8 +190,8 @@ func HelloWorld() { #+end_src `, `
// HelloWorld prints "Hello World"
-func HelloWorld() {
+func HelloWorld() {
 	fmt.Println("Hello World")
-}
+}
`) } diff --git a/modules/markup/renderer.go b/modules/markup/renderer.go index 05dd512815..0a66caf1d5 100644 --- a/modules/markup/renderer.go +++ b/modules/markup/renderer.go @@ -198,10 +198,28 @@ func RegisterRenderer(renderer Renderer) { } } -// GetRendererByFileName get renderer by filename -func GetRendererByFileName(filename string) Renderer { - extension := strings.ToLower(filepath.Ext(filename)) - return extRenderers[extension] +// FullExtension returns the full extension of path, i.e. everything after and including +// the first period in the basename of path. +func FullExtension(path string) string { + _, extension, found := strings.Cut(strings.ToLower(filepath.Base(path)), ".") + if !found { + return "" + } + return "." + extension +} + +// GetRendererByExtension returns the most specific registered renderer for extension. +func GetRendererByExtension(extension string) Renderer { + _, extension, found := strings.Cut(extension, ".") + checkedExtensions := 0 + for found && checkedExtensions < 10 { + if renderer, ok := extRenderers["."+extension]; ok { + return renderer + } + checkedExtensions++ + _, extension, found = strings.Cut(extension, ".") + } + return nil } // GetRendererByType returns a renderer according type @@ -301,23 +319,19 @@ func render(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Wr _ = pw2.Close() }() - wg.Add(1) - go func() { + wg.Go(func() { err = donotpanic.SafeFuncWithError(func() error { return SanitizeReader(pr2, renderer.Name(), output) }) _ = pr2.Close() - wg.Done() - }() + }) } else { pw2 = nopCloser{output} } - wg.Add(1) - go func() { + wg.Go(func() { err = donotpanic.SafeFuncWithError(func() error { return postProcessOrCopy(ctx, renderer, pr, pw2) }) _ = pr.Close() _ = pw2.Close() - wg.Done() - }() + }) if err1 := renderer.Render(ctx, input, pw); err1 != nil { return err1 @@ -350,6 +364,20 @@ func renderByType(ctx *RenderContext, input io.Reader, output io.Writer) error { return ErrUnsupportedRenderType{ctx.Type} } +// ErrMissingExtension represents the error when a path does not have any extension. +type ErrMissingExtension struct { + Path string +} + +func IsErrMissingExtension(err error) bool { + _, ok := err.(ErrMissingExtension) + return ok +} + +func (err ErrMissingExtension) Error() string { + return fmt.Sprintf("path '%s' does not have an extension", err.Path) +} + // ErrUnsupportedRenderExtension represents the error when extension doesn't supported to render type ErrUnsupportedRenderExtension struct { Extension string @@ -365,8 +393,11 @@ func (err ErrUnsupportedRenderExtension) Error() string { } func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error { - extension := strings.ToLower(filepath.Ext(ctx.RelativePath)) - if renderer, ok := extRenderers[extension]; ok { + extension := FullExtension(ctx.RelativePath) + if extension == "" { + return ErrMissingExtension{ctx.RelativePath} + } + if renderer := GetRendererByExtension(extension); renderer != nil { if r, ok := renderer.(ExternalRenderer); ok && r.DisplayInIFrame() { if !ctx.InStandalonePage { // for an external render, it could only output its content in a standalone page @@ -381,7 +412,7 @@ func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error { // Type returns if markup format via the filename func Type(filename string) string { - if parser := GetRendererByFileName(filename); parser != nil { + if parser := GetRendererByExtension(FullExtension(filename)); parser != nil { return parser.Name() } return "" @@ -389,7 +420,7 @@ func Type(filename string) string { // IsMarkupFile reports whether file is a markup type file func IsMarkupFile(name, markup string) bool { - if parser := GetRendererByFileName(name); parser != nil { + if parser := GetRendererByExtension(FullExtension(name)); parser != nil { return parser.Name() == markup } return false diff --git a/modules/migration/retry_downloader.go b/modules/migration/retry_downloader.go index 1cacf5f375..ef3d78a5d9 100644 --- a/modules/migration/retry_downloader.go +++ b/modules/migration/retry_downloader.go @@ -44,6 +44,7 @@ func (d *RetryDownloader) retry(work func() error) error { case <-d.ctx.Done(): return d.ctx.Err() case <-time.After(time.Second * time.Duration(d.RetryDelay)): + break } } return err diff --git a/modules/nosql/manager.go b/modules/nosql/manager.go index 7eea069e09..748afe7587 100644 --- a/modules/nosql/manager.go +++ b/modules/nosql/manager.go @@ -30,6 +30,8 @@ type Manager struct { // RedisClient is a subset of redis.UniversalClient, it exposes less methods // to avoid generating machine code for unused methods. New method definitions // should be copied from the definitions in the Redis library github.com/redis/go-redis. +// +//mockery:generate: true type RedisClient interface { // redis.GenericCmdable Del(ctx context.Context, keys ...string) *redis.IntCmd diff --git a/modules/nosql/mocks.go b/modules/nosql/mocks.go new file mode 100644 index 0000000000..39d0d1fea3 --- /dev/null +++ b/modules/nosql/mocks.go @@ -0,0 +1,1240 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package nosql + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + mock "github.com/stretchr/testify/mock" +) + +// NewMockRedisClient creates a new instance of MockRedisClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRedisClient(t interface { + mock.TestingT + Cleanup(func()) +}, +) *MockRedisClient { + mock := &MockRedisClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockRedisClient is an autogenerated mock type for the RedisClient type +type MockRedisClient struct { + mock.Mock +} + +type MockRedisClient_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRedisClient) EXPECT() *MockRedisClient_Expecter { + return &MockRedisClient_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) Close() error { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() + } else { + r0 = ret.Error(0) + } + return r0 +} + +// MockRedisClient_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type MockRedisClient_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *MockRedisClient_Expecter) Close() *MockRedisClient_Close_Call { + return &MockRedisClient_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *MockRedisClient_Close_Call) Run(run func()) *MockRedisClient_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRedisClient_Close_Call) Return(err error) *MockRedisClient_Close_Call { + _c.Call.Return(err) + return _c +} + +func (_c *MockRedisClient_Close_Call) RunAndReturn(run func() error) *MockRedisClient_Close_Call { + _c.Call.Return(run) + return _c +} + +// DBSize provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) DBSize(ctx context.Context) *redis.IntCmd { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for DBSize") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context) *redis.IntCmd); ok { + r0 = returnFunc(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_DBSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DBSize' +type MockRedisClient_DBSize_Call struct { + *mock.Call +} + +// DBSize is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockRedisClient_Expecter) DBSize(ctx any) *MockRedisClient_DBSize_Call { + return &MockRedisClient_DBSize_Call{Call: _e.mock.On("DBSize", ctx)} +} + +func (_c *MockRedisClient_DBSize_Call) Run(run func(ctx context.Context)) *MockRedisClient_DBSize_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockRedisClient_DBSize_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_DBSize_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_DBSize_Call) RunAndReturn(run func(ctx context.Context) *redis.IntCmd) *MockRedisClient_DBSize_Call { + _c.Call.Return(run) + return _c +} + +// Decr provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) Decr(ctx context.Context, key string) *redis.IntCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Decr") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_Decr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Decr' +type MockRedisClient_Decr_Call struct { + *mock.Call +} + +// Decr is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *MockRedisClient_Expecter) Decr(ctx, key any) *MockRedisClient_Decr_Call { + return &MockRedisClient_Decr_Call{Call: _e.mock.On("Decr", ctx, key)} +} + +func (_c *MockRedisClient_Decr_Call) Run(run func(ctx context.Context, key string)) *MockRedisClient_Decr_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRedisClient_Decr_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_Decr_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_Decr_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.IntCmd) *MockRedisClient_Decr_Call { + _c.Call.Return(run) + return _c +} + +// Del provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd { + var tmpRet mock.Arguments + if len(keys) > 0 { + tmpRet = _mock.Called(ctx, keys) + } else { + tmpRet = _mock.Called(ctx) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Del") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_Del_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Del' +type MockRedisClient_Del_Call struct { + *mock.Call +} + +// Del is a helper method to define mock.On call +// - ctx context.Context +// - keys ...string +func (_e *MockRedisClient_Expecter) Del(ctx any, keys ...any) *MockRedisClient_Del_Call { + return &MockRedisClient_Del_Call{Call: _e.mock.On("Del", + append([]any{ctx}, keys...)...)} +} + +func (_c *MockRedisClient_Del_Call) Run(run func(ctx context.Context, keys ...string)) *MockRedisClient_Del_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + var variadicArgs []string + if len(args) > 1 { + variadicArgs = args[1].([]string) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *MockRedisClient_Del_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_Del_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_Del_Call) RunAndReturn(run func(ctx context.Context, keys ...string) *redis.IntCmd) *MockRedisClient_Del_Call { + _c.Call.Return(run) + return _c +} + +// Exists provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) Exists(ctx context.Context, keys ...string) *redis.IntCmd { + var tmpRet mock.Arguments + if len(keys) > 0 { + tmpRet = _mock.Called(ctx, keys) + } else { + tmpRet = _mock.Called(ctx) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Exists") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, keys...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_Exists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Exists' +type MockRedisClient_Exists_Call struct { + *mock.Call +} + +// Exists is a helper method to define mock.On call +// - ctx context.Context +// - keys ...string +func (_e *MockRedisClient_Expecter) Exists(ctx any, keys ...any) *MockRedisClient_Exists_Call { + return &MockRedisClient_Exists_Call{Call: _e.mock.On("Exists", + append([]any{ctx}, keys...)...)} +} + +func (_c *MockRedisClient_Exists_Call) Run(run func(ctx context.Context, keys ...string)) *MockRedisClient_Exists_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + var variadicArgs []string + if len(args) > 1 { + variadicArgs = args[1].([]string) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *MockRedisClient_Exists_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_Exists_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_Exists_Call) RunAndReturn(run func(ctx context.Context, keys ...string) *redis.IntCmd) *MockRedisClient_Exists_Call { + _c.Call.Return(run) + return _c +} + +// FlushDB provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) FlushDB(ctx context.Context) *redis.StatusCmd { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for FlushDB") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context) *redis.StatusCmd); ok { + r0 = returnFunc(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// MockRedisClient_FlushDB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FlushDB' +type MockRedisClient_FlushDB_Call struct { + *mock.Call +} + +// FlushDB is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockRedisClient_Expecter) FlushDB(ctx any) *MockRedisClient_FlushDB_Call { + return &MockRedisClient_FlushDB_Call{Call: _e.mock.On("FlushDB", ctx)} +} + +func (_c *MockRedisClient_FlushDB_Call) Run(run func(ctx context.Context)) *MockRedisClient_FlushDB_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockRedisClient_FlushDB_Call) Return(statusCmd *redis.StatusCmd) *MockRedisClient_FlushDB_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *MockRedisClient_FlushDB_Call) RunAndReturn(run func(ctx context.Context) *redis.StatusCmd) *MockRedisClient_FlushDB_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// MockRedisClient_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockRedisClient_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *MockRedisClient_Expecter) Get(ctx, key any) *MockRedisClient_Get_Call { + return &MockRedisClient_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *MockRedisClient_Get_Call) Run(run func(ctx context.Context, key string)) *MockRedisClient_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRedisClient_Get_Call) Return(stringCmd *redis.StringCmd) *MockRedisClient_Get_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *MockRedisClient_Get_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringCmd) *MockRedisClient_Get_Call { + _c.Call.Return(run) + return _c +} + +// HDel provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) HDel(ctx context.Context, key string, fields ...string) *redis.IntCmd { + var tmpRet mock.Arguments + if len(fields) > 0 { + tmpRet = _mock.Called(ctx, key, fields) + } else { + tmpRet = _mock.Called(ctx, key) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for HDel") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, key, fields...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_HDel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HDel' +type MockRedisClient_HDel_Call struct { + *mock.Call +} + +// HDel is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - fields ...string +func (_e *MockRedisClient_Expecter) HDel(ctx, key any, fields ...any) *MockRedisClient_HDel_Call { + return &MockRedisClient_HDel_Call{Call: _e.mock.On("HDel", + append([]any{ctx, key}, fields...)...)} +} + +func (_c *MockRedisClient_HDel_Call) Run(run func(ctx context.Context, key string, fields ...string)) *MockRedisClient_HDel_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + var variadicArgs []string + if len(args) > 2 { + variadicArgs = args[2].([]string) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockRedisClient_HDel_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_HDel_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_HDel_Call) RunAndReturn(run func(ctx context.Context, key string, fields ...string) *redis.IntCmd) *MockRedisClient_HDel_Call { + _c.Call.Return(run) + return _c +} + +// HKeys provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) HKeys(ctx context.Context, key string) *redis.StringSliceCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for HKeys") + } + + var r0 *redis.StringSliceCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringSliceCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringSliceCmd) + } + } + return r0 +} + +// MockRedisClient_HKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HKeys' +type MockRedisClient_HKeys_Call struct { + *mock.Call +} + +// HKeys is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *MockRedisClient_Expecter) HKeys(ctx, key any) *MockRedisClient_HKeys_Call { + return &MockRedisClient_HKeys_Call{Call: _e.mock.On("HKeys", ctx, key)} +} + +func (_c *MockRedisClient_HKeys_Call) Run(run func(ctx context.Context, key string)) *MockRedisClient_HKeys_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRedisClient_HKeys_Call) Return(stringSliceCmd *redis.StringSliceCmd) *MockRedisClient_HKeys_Call { + _c.Call.Return(stringSliceCmd) + return _c +} + +func (_c *MockRedisClient_HKeys_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringSliceCmd) *MockRedisClient_HKeys_Call { + _c.Call.Return(run) + return _c +} + +// HSet provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) HSet(ctx context.Context, key string, values ...any) *redis.IntCmd { + var tmpRet mock.Arguments + if len(values) > 0 { + tmpRet = _mock.Called(ctx, key, values) + } else { + tmpRet = _mock.Called(ctx, key) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for HSet") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...any) *redis.IntCmd); ok { + r0 = returnFunc(ctx, key, values...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_HSet_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HSet' +type MockRedisClient_HSet_Call struct { + *mock.Call +} + +// HSet is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - values ...any +func (_e *MockRedisClient_Expecter) HSet(ctx, key any, values ...any) *MockRedisClient_HSet_Call { + return &MockRedisClient_HSet_Call{Call: _e.mock.On("HSet", + append([]any{ctx, key}, values...)...)} +} + +func (_c *MockRedisClient_HSet_Call) Run(run func(ctx context.Context, key string, values ...any)) *MockRedisClient_HSet_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []any + var variadicArgs []any + if len(args) > 2 { + variadicArgs = args[2].([]any) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockRedisClient_HSet_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_HSet_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_HSet_Call) RunAndReturn(run func(ctx context.Context, key string, values ...any) *redis.IntCmd) *MockRedisClient_HSet_Call { + _c.Call.Return(run) + return _c +} + +// Incr provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) Incr(ctx context.Context, key string) *redis.IntCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Incr") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_Incr_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Incr' +type MockRedisClient_Incr_Call struct { + *mock.Call +} + +// Incr is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *MockRedisClient_Expecter) Incr(ctx, key any) *MockRedisClient_Incr_Call { + return &MockRedisClient_Incr_Call{Call: _e.mock.On("Incr", ctx, key)} +} + +func (_c *MockRedisClient_Incr_Call) Run(run func(ctx context.Context, key string)) *MockRedisClient_Incr_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRedisClient_Incr_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_Incr_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_Incr_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.IntCmd) *MockRedisClient_Incr_Call { + _c.Call.Return(run) + return _c +} + +// LLen provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) LLen(ctx context.Context, key string) *redis.IntCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for LLen") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.IntCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_LLen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LLen' +type MockRedisClient_LLen_Call struct { + *mock.Call +} + +// LLen is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *MockRedisClient_Expecter) LLen(ctx, key any) *MockRedisClient_LLen_Call { + return &MockRedisClient_LLen_Call{Call: _e.mock.On("LLen", ctx, key)} +} + +func (_c *MockRedisClient_LLen_Call) Run(run func(ctx context.Context, key string)) *MockRedisClient_LLen_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRedisClient_LLen_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_LLen_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_LLen_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.IntCmd) *MockRedisClient_LLen_Call { + _c.Call.Return(run) + return _c +} + +// LPop provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) LPop(ctx context.Context, key string) *redis.StringCmd { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for LPop") + } + + var r0 *redis.StringCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *redis.StringCmd); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StringCmd) + } + } + return r0 +} + +// MockRedisClient_LPop_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LPop' +type MockRedisClient_LPop_Call struct { + *mock.Call +} + +// LPop is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *MockRedisClient_Expecter) LPop(ctx, key any) *MockRedisClient_LPop_Call { + return &MockRedisClient_LPop_Call{Call: _e.mock.On("LPop", ctx, key)} +} + +func (_c *MockRedisClient_LPop_Call) Run(run func(ctx context.Context, key string)) *MockRedisClient_LPop_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockRedisClient_LPop_Call) Return(stringCmd *redis.StringCmd) *MockRedisClient_LPop_Call { + _c.Call.Return(stringCmd) + return _c +} + +func (_c *MockRedisClient_LPop_Call) RunAndReturn(run func(ctx context.Context, key string) *redis.StringCmd) *MockRedisClient_LPop_Call { + _c.Call.Return(run) + return _c +} + +// Ping provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) Ping(ctx context.Context) *redis.StatusCmd { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Ping") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context) *redis.StatusCmd); ok { + r0 = returnFunc(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// MockRedisClient_Ping_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ping' +type MockRedisClient_Ping_Call struct { + *mock.Call +} + +// Ping is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockRedisClient_Expecter) Ping(ctx any) *MockRedisClient_Ping_Call { + return &MockRedisClient_Ping_Call{Call: _e.mock.On("Ping", ctx)} +} + +func (_c *MockRedisClient_Ping_Call) Run(run func(ctx context.Context)) *MockRedisClient_Ping_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *MockRedisClient_Ping_Call) Return(statusCmd *redis.StatusCmd) *MockRedisClient_Ping_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *MockRedisClient_Ping_Call) RunAndReturn(run func(ctx context.Context) *redis.StatusCmd) *MockRedisClient_Ping_Call { + _c.Call.Return(run) + return _c +} + +// RPush provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) RPush(ctx context.Context, key string, values ...any) *redis.IntCmd { + var tmpRet mock.Arguments + if len(values) > 0 { + tmpRet = _mock.Called(ctx, key, values) + } else { + tmpRet = _mock.Called(ctx, key) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for RPush") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...any) *redis.IntCmd); ok { + r0 = returnFunc(ctx, key, values...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_RPush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RPush' +type MockRedisClient_RPush_Call struct { + *mock.Call +} + +// RPush is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - values ...any +func (_e *MockRedisClient_Expecter) RPush(ctx, key any, values ...any) *MockRedisClient_RPush_Call { + return &MockRedisClient_RPush_Call{Call: _e.mock.On("RPush", + append([]any{ctx, key}, values...)...)} +} + +func (_c *MockRedisClient_RPush_Call) Run(run func(ctx context.Context, key string, values ...any)) *MockRedisClient_RPush_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []any + var variadicArgs []any + if len(args) > 2 { + variadicArgs = args[2].([]any) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockRedisClient_RPush_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_RPush_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_RPush_Call) RunAndReturn(run func(ctx context.Context, key string, values ...any) *redis.IntCmd) *MockRedisClient_RPush_Call { + _c.Call.Return(run) + return _c +} + +// SAdd provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) SAdd(ctx context.Context, key string, members ...any) *redis.IntCmd { + var tmpRet mock.Arguments + if len(members) > 0 { + tmpRet = _mock.Called(ctx, key, members) + } else { + tmpRet = _mock.Called(ctx, key) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for SAdd") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...any) *redis.IntCmd); ok { + r0 = returnFunc(ctx, key, members...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_SAdd_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SAdd' +type MockRedisClient_SAdd_Call struct { + *mock.Call +} + +// SAdd is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - members ...any +func (_e *MockRedisClient_Expecter) SAdd(ctx, key any, members ...any) *MockRedisClient_SAdd_Call { + return &MockRedisClient_SAdd_Call{Call: _e.mock.On("SAdd", + append([]any{ctx, key}, members...)...)} +} + +func (_c *MockRedisClient_SAdd_Call) Run(run func(ctx context.Context, key string, members ...any)) *MockRedisClient_SAdd_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []any + var variadicArgs []any + if len(args) > 2 { + variadicArgs = args[2].([]any) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockRedisClient_SAdd_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_SAdd_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_SAdd_Call) RunAndReturn(run func(ctx context.Context, key string, members ...any) *redis.IntCmd) *MockRedisClient_SAdd_Call { + _c.Call.Return(run) + return _c +} + +// SIsMember provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) SIsMember(ctx context.Context, key string, member any) *redis.BoolCmd { + ret := _mock.Called(ctx, key, member) + + if len(ret) == 0 { + panic("no return value specified for SIsMember") + } + + var r0 *redis.BoolCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, any) *redis.BoolCmd); ok { + r0 = returnFunc(ctx, key, member) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.BoolCmd) + } + } + return r0 +} + +// MockRedisClient_SIsMember_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SIsMember' +type MockRedisClient_SIsMember_Call struct { + *mock.Call +} + +// SIsMember is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - member any +func (_e *MockRedisClient_Expecter) SIsMember(ctx, key, member any) *MockRedisClient_SIsMember_Call { + return &MockRedisClient_SIsMember_Call{Call: _e.mock.On("SIsMember", ctx, key, member)} +} + +func (_c *MockRedisClient_SIsMember_Call) Run(run func(ctx context.Context, key string, member any)) *MockRedisClient_SIsMember_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 any + if args[2] != nil { + arg2 = args[2].(any) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *MockRedisClient_SIsMember_Call) Return(boolCmd *redis.BoolCmd) *MockRedisClient_SIsMember_Call { + _c.Call.Return(boolCmd) + return _c +} + +func (_c *MockRedisClient_SIsMember_Call) RunAndReturn(run func(ctx context.Context, key string, member any) *redis.BoolCmd) *MockRedisClient_SIsMember_Call { + _c.Call.Return(run) + return _c +} + +// SRem provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) SRem(ctx context.Context, key string, members ...any) *redis.IntCmd { + var tmpRet mock.Arguments + if len(members) > 0 { + tmpRet = _mock.Called(ctx, key, members) + } else { + tmpRet = _mock.Called(ctx, key) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for SRem") + } + + var r0 *redis.IntCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...any) *redis.IntCmd); ok { + r0 = returnFunc(ctx, key, members...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.IntCmd) + } + } + return r0 +} + +// MockRedisClient_SRem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SRem' +type MockRedisClient_SRem_Call struct { + *mock.Call +} + +// SRem is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - members ...any +func (_e *MockRedisClient_Expecter) SRem(ctx, key any, members ...any) *MockRedisClient_SRem_Call { + return &MockRedisClient_SRem_Call{Call: _e.mock.On("SRem", + append([]any{ctx, key}, members...)...)} +} + +func (_c *MockRedisClient_SRem_Call) Run(run func(ctx context.Context, key string, members ...any)) *MockRedisClient_SRem_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []any + var variadicArgs []any + if len(args) > 2 { + variadicArgs = args[2].([]any) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *MockRedisClient_SRem_Call) Return(intCmd *redis.IntCmd) *MockRedisClient_SRem_Call { + _c.Call.Return(intCmd) + return _c +} + +func (_c *MockRedisClient_SRem_Call) RunAndReturn(run func(ctx context.Context, key string, members ...any) *redis.IntCmd) *MockRedisClient_SRem_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type MockRedisClient +func (_mock *MockRedisClient) Set(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd { + ret := _mock.Called(ctx, key, value, expiration) + + if len(ret) == 0 { + panic("no return value specified for Set") + } + + var r0 *redis.StatusCmd + if returnFunc, ok := ret.Get(0).(func(context.Context, string, any, time.Duration) *redis.StatusCmd); ok { + r0 = returnFunc(ctx, key, value, expiration) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*redis.StatusCmd) + } + } + return r0 +} + +// MockRedisClient_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type MockRedisClient_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value any +// - expiration time.Duration +func (_e *MockRedisClient_Expecter) Set(ctx, key, value, expiration any) *MockRedisClient_Set_Call { + return &MockRedisClient_Set_Call{Call: _e.mock.On("Set", ctx, key, value, expiration)} +} + +func (_c *MockRedisClient_Set_Call) Run(run func(ctx context.Context, key string, value any, expiration time.Duration)) *MockRedisClient_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 any + if args[2] != nil { + arg2 = args[2].(any) + } + var arg3 time.Duration + if args[3] != nil { + arg3 = args[3].(time.Duration) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *MockRedisClient_Set_Call) Return(statusCmd *redis.StatusCmd) *MockRedisClient_Set_Call { + _c.Call.Return(statusCmd) + return _c +} + +func (_c *MockRedisClient_Set_Call) RunAndReturn(run func(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd) *MockRedisClient_Set_Call { + _c.Call.Return(run) + return _c +} diff --git a/modules/optional/option.go b/modules/optional/option.go index ccbad259c2..2793f985e8 100644 --- a/modules/optional/option.go +++ b/modules/optional/option.go @@ -3,7 +3,14 @@ package optional -import "strconv" +import ( + "database/sql" + "database/sql/driver" + "reflect" + "strconv" + + "xorm.io/xorm/schemas" +) type Option[T any] []T @@ -34,9 +41,17 @@ func (o Option[T]) Has() bool { return o != nil } -func (o Option[T]) Value() T { - var zero T - return o.ValueOrDefault(zero) +func (o Option[T]) Get() (has bool, value T) { + if o != nil { + has = true + value = o[0] + } + return has, value +} + +func (o Option[T]) ValueOrZeroValue() T { + var zeroValue T + return o.ValueOrDefault(zeroValue) } func (o Option[T]) ValueOrDefault(v T) T { @@ -54,3 +69,45 @@ func ParseBool(s string) Option[bool] { } return Some(v) } + +// Option[T] can be used in an xorm bean as a field type for a nullable column. Multiple interfaces must be implemented +// for this to work correctly and won't be checked at compile-time of the bean struct, so they're asserted here in case +// the interface definitions change: +var ( + _ sql.Scanner = (*Option[bool])(nil) // read data from DB + _ driver.Valuer = None[bool]() // write data to DB + _ schemas.SQLTypeDelegator = None[bool]() // represent column field type correctly +) + +// Convert database data into an Option[T]. sql.Null[T] has all the necessary logic to perform Value(), so it is used as +// an implementation. +func (o *Option[T]) Scan(value any) error { + var n sql.Null[T] + if err := n.Scan(value); err != nil { + return err + } + if n.Valid { + *o = Some(n.V) + } else { + *o = None[T]() + } + return nil +} + +// Convert Option[T] into the necessary database data to represent it. sql.Null[T] has all the necessary logic to +// perform Value(), so it is used as an implementation. +func (o Option[T]) Value() (driver.Value, error) { + var n sql.Null[T] + if o.Has() { + n.V = o[0] + n.Valid = true + } else { + n.Valid = false + } + return n.Value() +} + +// Make xorm use whatever SQLType is appropriate for T to represent Option[T] in the database table +func (o Option[T]) DelegateSQLType() reflect.Type { + return reflect.TypeFor[T]() +} diff --git a/modules/optional/option_test.go b/modules/optional/option_test.go index a674caf633..9d9d839746 100644 --- a/modules/optional/option_test.go +++ b/modules/optional/option_test.go @@ -9,32 +9,33 @@ import ( "forgejo.org/modules/optional" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestOption(t *testing.T) { var uninitialized optional.Option[int] assert.False(t, uninitialized.Has()) - assert.Equal(t, int(0), uninitialized.Value()) + assert.Equal(t, int(0), uninitialized.ValueOrZeroValue()) assert.Equal(t, int(1), uninitialized.ValueOrDefault(1)) none := optional.None[int]() assert.False(t, none.Has()) - assert.Equal(t, int(0), none.Value()) + assert.Equal(t, int(0), none.ValueOrZeroValue()) assert.Equal(t, int(1), none.ValueOrDefault(1)) some := optional.Some(1) assert.True(t, some.Has()) - assert.Equal(t, int(1), some.Value()) + assert.Equal(t, int(1), some.ValueOrZeroValue()) assert.Equal(t, int(1), some.ValueOrDefault(2)) noneBool := optional.None[bool]() assert.False(t, noneBool.Has()) - assert.False(t, noneBool.Value()) + assert.False(t, noneBool.ValueOrZeroValue()) assert.True(t, noneBool.ValueOrDefault(true)) someBool := optional.Some(true) assert.True(t, someBool.Has()) - assert.True(t, someBool.Value()) + assert.True(t, someBool.ValueOrZeroValue()) assert.True(t, someBool.ValueOrDefault(false)) var ptr *int @@ -43,19 +44,22 @@ func TestOption(t *testing.T) { int1 := 1 opt1 := optional.FromPtr(&int1) assert.True(t, opt1.Has()) - assert.Equal(t, int(1), opt1.Value()) + _, v := opt1.Get() + assert.Equal(t, int(1), v) assert.False(t, optional.FromNonDefault("").Has()) opt2 := optional.FromNonDefault("test") assert.True(t, opt2.Has()) - assert.Equal(t, "test", opt2.Value()) + _, vStr := opt2.Get() + assert.Equal(t, "test", vStr) assert.False(t, optional.FromNonDefault(0).Has()) opt3 := optional.FromNonDefault(1) assert.True(t, opt3.Has()) - assert.Equal(t, int(1), opt3.Value()) + _, v = opt3.Get() + assert.Equal(t, int(1), v) } func Test_ParseBool(t *testing.T) { @@ -70,3 +74,52 @@ func Test_ParseBool(t *testing.T) { assert.Equal(t, optional.Some(true), optional.ParseBool("t")) assert.Equal(t, optional.Some(true), optional.ParseBool("True")) } + +func roundtrip[T any](t *testing.T, orig optional.Option[T]) { + // invoke (driver.Valuer).Value to get a DB value + dbValue, err := orig.Value() + require.NoError(t, err) + + // invoke (sql.Scanner).Scan to read the DB value + var scanned optional.Option[T] + err = scanned.Scan(dbValue) + require.NoError(t, err) + + hasOrig, origValue := orig.Get() + hasScanned, scannedValue := scanned.Get() + + if hasOrig { + require.True(t, hasScanned, "must hasScanned") + assert.Equal(t, origValue, scannedValue) + } else { + assert.False(t, hasScanned, "must not hasScanned") + } +} + +func TestOptionValueScan(t *testing.T) { + t.Run("string roundtrip", func(t *testing.T) { + roundtrip(t, optional.Some("hello world")) + }) + t.Run("string null", func(t *testing.T) { + roundtrip(t, optional.None[string]()) + }) + t.Run("int64 roundtrip", func(t *testing.T) { + roundtrip(t, optional.Some(int64(1234))) + }) + t.Run("int64 null", func(t *testing.T) { + roundtrip(t, optional.None[int64]()) + }) + t.Run("bool roundtrip", func(t *testing.T) { + roundtrip(t, optional.Some(false)) + }) + t.Run("bool null", func(t *testing.T) { + roundtrip(t, optional.None[bool]()) + }) +} + +func TestDelegateSQLType(t *testing.T) { + assert.Equal(t, "string", optional.Some("hello world").DelegateSQLType().Name()) + assert.Equal(t, "string", optional.None[string]().DelegateSQLType().Name()) + assert.Equal(t, "int64", optional.Some(int64(123)).DelegateSQLType().Name()) + assert.Equal(t, "int64", optional.None[int64]().DelegateSQLType().Name()) +} diff --git a/modules/optional/serialization.go b/modules/optional/serialization.go index 2e2ccd6786..46e20d95e8 100644 --- a/modules/optional/serialization.go +++ b/modules/optional/serialization.go @@ -19,11 +19,11 @@ func (o *Option[T]) UnmarshalJSON(data []byte) error { } func (o Option[T]) MarshalJSON() ([]byte, error) { - if !o.Has() { + has, v := o.Get() + if !has { return []byte("null"), nil } - - return json.Marshal(o.Value()) + return json.Marshal(v) } func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error { @@ -36,11 +36,12 @@ func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error { } func (o Option[T]) MarshalYAML() (any, error) { - if !o.Has() { + has, v := o.Get() + if !has { return nil, nil } value := new(yaml.Node) - err := value.Encode(o.Value()) + err := value.Encode(v) return value, err } diff --git a/modules/packages/container/metadata.go b/modules/packages/container/metadata.go index 6cac77b7ff..4f332783ba 100644 --- a/modules/packages/container/metadata.go +++ b/modules/packages/container/metadata.go @@ -16,11 +16,12 @@ import ( ) const ( - PropertyRepository = "container.repository" - PropertyDigest = "container.digest" - PropertyMediaType = "container.mediatype" - PropertyManifestTagged = "container.manifest.tagged" - PropertyManifestReference = "container.manifest.reference" + PropertyRepository = "container.repository" + PropertyRepositoryAutolinkingPending = "container.repository.autolinking-pending" + PropertyDigest = "container.digest" + PropertyMediaType = "container.mediatype" + PropertyManifestTagged = "container.manifest.tagged" + PropertyManifestReference = "container.manifest.reference" DefaultPlatform = "linux/amd64" @@ -63,6 +64,7 @@ type Metadata struct { Labels map[string]string `json:"labels,omitempty"` ImageLayers []string `json:"layer_creation,omitempty"` Manifests []*Manifest `json:"manifests,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` } type Manifest struct { diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index ed163d30ac..2f83d2ee7b 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -58,7 +58,7 @@ type PackageMetadata struct { Time map[string]time.Time `json:"time,omitempty"` Homepage string `json:"homepage,omitempty"` Keywords []string `json:"keywords,omitempty"` - Repository Repository `json:"repository,omitempty"` + Repository Repository `json:"repository"` Author User `json:"author"` ReadmeFilename string `json:"readmeFilename,omitempty"` Users map[string]bool `json:"users,omitempty"` @@ -75,7 +75,7 @@ type PackageMetadataVersion struct { Author User `json:"author"` Homepage string `json:"homepage,omitempty"` License string `json:"license,omitempty"` - Repository Repository `json:"repository,omitempty"` + Repository Repository `json:"repository"` Keywords []string `json:"keywords,omitempty"` Dependencies map[string]string `json:"dependencies,omitempty"` BundleDependencies []string `json:"bundleDependencies,omitempty"` diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index 6bb77f302b..0e5bf19ce7 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -22,5 +22,5 @@ type Metadata struct { OptionalDependencies map[string]string `json:"optional_dependencies,omitempty"` Bin map[string]string `json:"bin,omitempty"` Readme string `json:"readme,omitempty"` - Repository Repository `json:"repository,omitempty"` + Repository Repository `json:"repository"` } diff --git a/modules/packages/nuget/symbol_extractor.go b/modules/packages/nuget/symbol_extractor.go index 992ade7e8f..dd9fac96c6 100644 --- a/modules/packages/nuget/symbol_extractor.go +++ b/modules/packages/nuget/symbol_extractor.go @@ -142,8 +142,8 @@ func ParseDebugHeaderID(r io.ReadSeeker) (string, error) { if _, err := r.Read(b); err != nil { return "", err } - if i := bytes.IndexByte(b, 0); i != -1 { - buf.Write(b[:i]) + if before, _, ok := bytes.Cut(b, []byte{0}); ok { + buf.Write(before) return buf.String(), nil } buf.Write(b) diff --git a/modules/packages/pypi/metadata.go b/modules/packages/pypi/metadata.go index 125728c4f1..0aa90aeca5 100644 --- a/modules/packages/pypi/metadata.go +++ b/modules/packages/pypi/metadata.go @@ -13,3 +13,26 @@ type Metadata struct { License string `json:"license,omitempty"` RequiresPython string `json:"requires_python,omitempty"` } + +type FileHashesJSON struct { + SHA256 string `json:"sha256"` +} + +type FileJSON struct { + Filename string `json:"filename"` + URL string `json:"url"` + Hashes FileHashesJSON `json:"hashes"` + RequiresPython string `json:"requires-python"` + Size int64 `json:"size"` +} + +type PackageMetaJSON struct { + APIVersion string `json:"api-version"` +} + +type PackageJSON struct { + Name string `json:"name"` + Meta PackageMetaJSON `json:"meta"` + Versions []string `json:"versions"` + Files []FileJSON `json:"files"` +} diff --git a/modules/packages/rubygems/marshal.go b/modules/packages/rubygems/marshal.go index 191efc7c0e..7d498c66b8 100644 --- a/modules/packages/rubygems/marshal.go +++ b/modules/packages/rubygems/marshal.go @@ -91,7 +91,7 @@ func (e *MarshalEncoder) marshal(v any) error { val := reflect.ValueOf(v) typ := reflect.TypeOf(v) - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { val = val.Elem() typ = typ.Elem() } @@ -250,7 +250,7 @@ func (e *MarshalEncoder) marshalArray(arr reflect.Value) error { return err } - for i := 0; i < length; i++ { + for i := range length { if err := e.marshal(arr.Index(i).Interface()); err != nil { return err } diff --git a/modules/packages/rubygems/metadata.go b/modules/packages/rubygems/metadata.go index 9d3fd18f12..0091569fce 100644 --- a/modules/packages/rubygems/metadata.go +++ b/modules/packages/rubygems/metadata.go @@ -128,7 +128,11 @@ func (r requirement) AsVersionRequirement() []VersionRequirement { continue } version, ok := versionInt.(string) - if !ok || version == "0" { + if !ok { + continue + } + + if restriction == ">=" && version == "0" { continue } diff --git a/modules/packages/rubygems/metadata_test.go b/modules/packages/rubygems/metadata_test.go index cd3a5bbd10..683f8d1df8 100644 --- a/modules/packages/rubygems/metadata_test.go +++ b/modules/packages/rubygems/metadata_test.go @@ -87,3 +87,30 @@ yjAbmt9LsOMp8xMamFkSQ38fP5EFjdz8LA4do2C69VvqWXAJgrPbKZb58/xZXrKoW6ttW13Bhvzi assert.Equal(t, "~>", rp.Metadata.DevelopmentDependencies[0].Version[0].Restriction) assert.Equal(t, "5.2", rp.Metadata.DevelopmentDependencies[0].Version[0].Version) } + +func TestPessimisticVersioning(t *testing.T) { + content, _ := base64.StdEncoding.DecodeString(`H4sIABmkhGkCA+1WTY/TMBC9+1eYvfSU1G1ZkCxRgYTYCwcEEgcQshxnmnrXX9gO2lz2t2MnTdOy +1YIKYoVEEimZ8WT8/GY8nqIo8BPfVt3cVtcgIr0CTekHB0JupOBRWoMM10CxgxCkliFKUXwDH9KI +NE0RIUS0k+kJVx+HIYTx3oiUi5Igp3jcWK8pzv8g3sat9YGiAm+yYD18baUHiippaukpTm8kwEcm +tlwmN5+/oJrHhGxJls8KsizIJSaE9k9Jxgt/QjU4MDUYIaH3fx/k69GiSziH5UrtlBQyjmtNAztE +Gkw8tdL303AyPjJP02ZNke6L9YuLXsjiQ3QN1560GZklZexcwkZ9a6LUkBTOgwcFPCT1hqsAE9Hs +CMCjAP5FruH2P9d/nesT+/mP8X63/sd4/w3ANQThpcuVkiLQXKq+hr0MbYxdKax1JfcIbkG0kVcq +laBcueA2gslO9qLnzNdWsI0cbaavrdXgeJPWv43RBTqfC1tDBb4prW/mqYw2cG33b9cqFeaLxeJy +hVKw00RD4bt697ZYlaSwRnVIQ+SpfvLMQtU2LAEQN+BZ6+UZ02A8YjzbQbCtF8DyH2f7SEeDaUDZ +5mwPKQRtzo7+6DvTi7MhMmlC5EoxnfZZDh3qo2v7RBmiustF5njc9vFRshqVNctZyB44WI8z+8e8 +PqomP8/pKaNX5WJ2DKIBHR4BCNnD2G3uzNg9OKtyVS6foyCb3I2wG+goCofdy2T6FIVWa+47il/h +3LbgFDv8Zogfvltjctjj4KnHQdn4YF9+B8hhV5g0CQAA`) + rp, err := parseMetadataFile(bytes.NewReader(content)) + require.NoError(t, err) + assert.NotNil(t, rp) + + assert.Len(t, rp.Metadata.RuntimeDependencies, 3) + assert.Equal(t, "implicit-version", rp.Metadata.RuntimeDependencies[0].Name) + assert.Empty(t, rp.Metadata.RuntimeDependencies[0].Version) + assert.Equal(t, "explicit-version", rp.Metadata.RuntimeDependencies[1].Name) + assert.Empty(t, rp.Metadata.RuntimeDependencies[1].Version) + assert.Equal(t, "explicit-pessimistic-version", rp.Metadata.RuntimeDependencies[2].Name) + assert.Len(t, rp.Metadata.RuntimeDependencies[2].Version, 1) + assert.Equal(t, "~>", rp.Metadata.RuntimeDependencies[2].Version[0].Restriction) + assert.Equal(t, "0", rp.Metadata.RuntimeDependencies[2].Version[0].Version) +} diff --git a/modules/packages/swift/metadata.go b/modules/packages/swift/metadata.go index 34fc4f1784..094fa0c7a4 100644 --- a/modules/packages/swift/metadata.go +++ b/modules/packages/swift/metadata.go @@ -47,7 +47,7 @@ type Metadata struct { Keywords []string `json:"keywords,omitempty"` RepositoryURL string `json:"repository_url,omitempty"` License string `json:"license,omitempty"` - Author Person `json:"author,omitempty"` + Author Person `json:"author"` Manifests map[string]*Manifest `json:"manifests,omitempty"` } diff --git a/modules/private/serv.go b/modules/private/serv.go index fb8496930e..1e04b2ce29 100644 --- a/modules/private/serv.go +++ b/modules/private/serv.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "net/url" + "strings" asymkey_model "forgejo.org/models/asymkey" "forgejo.org/models/perm" @@ -47,17 +48,18 @@ type ServCommandResults struct { // ServCommand preps for a serv call func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, ResponseExtra) { - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", + var reqURL strings.Builder + fmt.Fprintf(&reqURL, "%sapi/internal/serv/command/%d/%s/%s?mode=%d", + setting.LocalURL, keyID, url.PathEscape(ownerName), url.PathEscape(repoName), - mode, - ) + mode) for _, verb := range verbs { if verb != "" { - reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) + fmt.Fprintf(&reqURL, "&verb=%s", url.QueryEscape(verb)) } } - req := newInternalRequest(ctx, reqURL, "GET") + req := newInternalRequest(ctx, reqURL.String(), "GET") return requestJSONResp(req, &ServCommandResults{}) } diff --git a/modules/proxyprotocol/conn.go b/modules/proxyprotocol/conn.go index beac5de120..8414b8af35 100644 --- a/modules/proxyprotocol/conn.go +++ b/modules/proxyprotocol/conn.go @@ -15,6 +15,7 @@ import ( "time" "forgejo.org/modules/log" + "forgejo.org/modules/setting" ) var ( @@ -46,6 +47,7 @@ func NewConn(conn net.Conn, timeout time.Duration) *Conn { bufReader: bufio.NewReader(conn), conn: conn, proxyHeaderTimeout: timeout, + acceptUnknown: setting.ProxyProtocolAcceptUnknown, } return pConn } @@ -257,9 +259,10 @@ func (p *Conn) readV2ProxyHeader() error { p.localAddr = p.conn.RemoteAddr() return nil case 0x1: - // - \x1 : PROXY : the connection was established on behalf of another node, - // and reflects the original connection endpoints. The receiver must then use - // the information provided in the protocol block to get original the address. + // - \x1 : PROXY : the connection was established on behalf of another node, + // and reflects the original connection endpoints. The receiver must then use + // the information provided in the protocol block to get original the address. + break default: // - other values are unassigned and must not be emitted by senders. Receivers // must drop connections presenting unexpected values here. @@ -456,7 +459,7 @@ func (p *Conn) readV1ProxyHeader() error { // Verify the type is known switch parts[1] { case "UNKNOWN": - if !p.acceptUnknown || len(parts) != 2 { + if !p.acceptUnknown { p.conn.Close() return &ErrBadHeader{[]byte(header)} } @@ -464,7 +467,9 @@ func (p *Conn) readV1ProxyHeader() error { p.localAddr = p.conn.RemoteAddr() return nil case "TCP4": + break case "TCP6": + break default: p.conn.Close() return &ErrBadAddressType{parts[1]} diff --git a/modules/proxyprotocol/conn_test.go b/modules/proxyprotocol/conn_test.go new file mode 100644 index 0000000000..39adc401a3 --- /dev/null +++ b/modules/proxyprotocol/conn_test.go @@ -0,0 +1,112 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package proxyprotocol_test + +import ( + "io" + "net" + "testing" + "time" + + "forgejo.org/modules/proxyprotocol" + "forgejo.org/modules/setting" + "forgejo.org/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var v2Header = []byte{0xd, 0xa, 0xd, 0xa, 0x0, 0xd, 0xa, 0x51, 0x55, 0x49, 0x54, 0xa} + +func testConnection(t *testing.T, input []byte) *proxyprotocol.Conn { + local, remote := net.Pipe() + conn := proxyprotocol.NewConn(remote, 10*time.Second) + + go func(t *testing.T, conn net.Conn) { + _, err := conn.Write(input) + require.NoError(t, err) + + err = conn.Close() + require.NoError(t, err) + }(t, local) + + return conn +} + +func assertUwu(t *testing.T, conn *proxyprotocol.Conn) { + buf := make([]byte, 3) + read, err := conn.Read(buf) + require.NoError(t, err) + + assert.Equal(t, 3, read) + assert.Equal(t, []byte("uwu"), buf) +} + +func TestProxyProtocolParse(t *testing.T) { + // Basic v4/v6 TCP + ipv4Conn := testConnection(t, []byte("PROXY TCP4 7.3.3.1 1.3.3.7 14231 443\r\nuwu")) + + assertUwu(t, ipv4Conn) + assert.Equal(t, "7.3.3.1:14231", ipv4Conn.RemoteAddr().String()) + assert.Equal(t, "1.3.3.7:443", ipv4Conn.LocalAddr().String()) + + ipv6Conn := testConnection(t, []byte("PROXY TCP6 fe80::2 fe80::1 28512 443\r\nuwu")) + + assertUwu(t, ipv6Conn) + assert.Equal(t, "[fe80::2]:28512", ipv6Conn.RemoteAddr().String()) + assert.Equal(t, "[fe80::1]:443", ipv6Conn.LocalAddr().String()) + + ipv4Conn = testConnection(t, append(v2Header, 0x21, 0x11, 0x0, 0xc, 0x7, 0x3, 0x3, 0x1, 0x1, 0x3, 0x3, 0x7, 0xd9, 0xec, 0x1, 0xbb, 0x75, 0x77, 0x75)) + + assertUwu(t, ipv4Conn) + assert.Equal(t, "7.3.3.1:55788", ipv4Conn.RemoteAddr().String()) + assert.Equal(t, "1.3.3.7:443", ipv4Conn.LocalAddr().String()) + + ipv6Conn = testConnection(t, append(v2Header, 0x21, 0x21, 0x0, 0x24, 0xfe, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, 0xfe, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0xd9, 0xec, 0x1, 0xbb, 0x75, 0x77, 0x75)) + + assertUwu(t, ipv6Conn) + assert.Equal(t, "[fe80::2]:55788", ipv6Conn.RemoteAddr().String()) + assert.Equal(t, "[fe80::1]:443", ipv6Conn.LocalAddr().String()) + + // Basic unknown + unknownConn := testConnection(t, []byte("PROXY UNKNOWN\r\nuwu")) + _, err := unknownConn.Read([]byte{}) + require.Error(t, err) + + // Accept unknown protocol types + defer test.MockVariableValue(&setting.ProxyProtocolAcceptUnknown, true)() + + unknownConn = testConnection(t, []byte("PROXY UNKNOWN\r\nuwu")) + + assertUwu(t, unknownConn) + assert.Equal(t, "pipe", unknownConn.RemoteAddr().String()) + assert.Equal(t, "pipe", unknownConn.LocalAddr().String()) + + // Discard any unknown information between "UNKNOWN" and CRLF + + unknownConn = testConnection(t, []byte("PROXY UNKNOWN look, I'm hinding in an unknown protocol \\o/\r\nuwu")) + + assertUwu(t, unknownConn) + assert.Equal(t, "pipe", unknownConn.RemoteAddr().String()) + assert.Equal(t, "pipe", unknownConn.LocalAddr().String()) + + // Basic local + unknownConnV2 := testConnection(t, append(v2Header, 0x20, 0x0, 0x0, 0x0, 0x75, 0x77, 0x75)) + + assertUwu(t, unknownConnV2) + assert.Equal(t, "pipe", unknownConnV2.RemoteAddr().String()) + assert.Equal(t, "pipe", unknownConnV2.LocalAddr().String()) +} + +func TestProxyProtocolInvalidHeader(t *testing.T) { + // Short prefix + conn := testConnection(t, []byte("PROXY\r\n")) + _, err := conn.Read([]byte{}) + require.ErrorIs(t, err, io.EOF) + + // Wrong prefix + conn = testConnection(t, []byte("PROXYv1337\r\n")) + _, err = conn.Read([]byte{}) + require.ErrorContains(t, err, "Unexpected proxy header") +} diff --git a/modules/proxyprotocol/listener.go b/modules/proxyprotocol/listener.go index ec85c425d3..500181cd6a 100644 --- a/modules/proxyprotocol/listener.go +++ b/modules/proxyprotocol/listener.go @@ -18,7 +18,6 @@ import ( type Listener struct { Listener net.Listener ProxyHeaderTimeout time.Duration - AcceptUnknown bool // allow PROXY UNKNOWN } // Accept implements the Accept method in the Listener interface @@ -31,7 +30,6 @@ func (p *Listener) Accept() (net.Conn, error) { } newConn := NewConn(conn, p.ProxyHeaderTimeout) - newConn.acceptUnknown = p.AcceptUnknown return newConn, nil } diff --git a/modules/proxyprotocol/util.go b/modules/proxyprotocol/util.go index a280663b27..5fc89d80a9 100644 --- a/modules/proxyprotocol/util.go +++ b/modules/proxyprotocol/util.go @@ -5,7 +5,7 @@ package proxyprotocol import "io" -var localHeader = append(v2Prefix, '\x20', '\x00', '\x00', '\x00', '\x00') +var localHeader = append(v2Prefix, '\x20', '\x00', '\x00', '\x00') // WriteLocalHeader will write the ProxyProtocol Header for a local connection to the provided writer func WriteLocalHeader(w io.Writer) error { diff --git a/modules/public/public.go b/modules/public/public.go index a7db5b62e9..52cb8757a0 100644 --- a/modules/public/public.go +++ b/modules/public/public.go @@ -45,7 +45,7 @@ func FileHandlerFunc() http.HandlerFunc { func parseAcceptEncoding(val string) container.Set[string] { parts := strings.Split(val, ";") types := make(container.Set[string]) - for _, v := range strings.Split(parts[0], ",") { + for v := range strings.SplitSeq(parts[0], ",") { types.Add(strings.TrimSpace(v)) } return types diff --git a/modules/queue/base_levelqueue_common.go b/modules/queue/base_levelqueue_common.go index 8b4f35c47d..c57bf8597b 100644 --- a/modules/queue/base_levelqueue_common.go +++ b/modules/queue/base_levelqueue_common.go @@ -83,7 +83,7 @@ func prepareLevelDB(cfg *BaseConfig) (conn string, db *leveldb.DB, err error) { } conn = cfg.ConnStr } - for i := 0; i < 10; i++ { + for range 10 { if db, err = nosql.GetManager().GetLevelDB(conn); err == nil { break } diff --git a/modules/queue/base_redis.go b/modules/queue/base_redis.go index ec3c6dc16d..8b20e0b443 100644 --- a/modules/queue/base_redis.go +++ b/modules/queue/base_redis.go @@ -49,7 +49,7 @@ func newBaseRedisGeneric(cfg *BaseConfig, unique bool, client nosql.RedisClient) } var err error - for i := 0; i < 10; i++ { + for range 10 { err = client.Ping(graceful.GetManager().ShutdownContext()).Err() if err == nil { break diff --git a/modules/queue/base_redis_test.go b/modules/queue/base_redis_test.go index bf3ad5b97b..297a49229a 100644 --- a/modules/queue/base_redis_test.go +++ b/modules/queue/base_redis_test.go @@ -7,18 +7,17 @@ import ( "context" "testing" - "forgejo.org/modules/queue/mock" + "forgejo.org/modules/nosql" + queue_mock "forgejo.org/modules/queue/mock" "forgejo.org/modules/setting" "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" - "go.uber.org/mock/gomock" ) type baseRedisUnitTestSuite struct { suite.Suite - - mockController *gomock.Controller } func TestBaseRedis(t *testing.T) { @@ -26,7 +25,6 @@ func TestBaseRedis(t *testing.T) { } func (suite *baseRedisUnitTestSuite) SetupSuite() { - suite.mockController = gomock.NewController(suite.T()) } func (suite *baseRedisUnitTestSuite) TestBasic() { @@ -71,39 +69,47 @@ func (suite *baseRedisUnitTestSuite) TestBasic() { } // Configure expectations. - mockRedisStore := mock.NewInMemoryMockRedis() - redisClient := mock.NewMockRedisClient(suite.mockController) + mockRedisStore := queue_mock.NewInMemoryMockRedis() + redisClient := nosql.NewMockRedisClient(suite.T()) redisClient.EXPECT(). - Ping(gomock.Any()). - Times(1). - Return(&redis.StatusCmd{}) + Ping(mock.Anything). + Return(&redis.StatusCmd{}). + Times(1) redisClient.EXPECT(). - LLen(gomock.Any(), testCase.QueueName). - Times(1). - DoAndReturn(mockRedisStore.LLen) + LLen(mock.Anything, testCase.QueueName). + RunAndReturn(mockRedisStore.LLen). + Times(1) redisClient.EXPECT(). - LPop(gomock.Any(), testCase.QueueName). - Times(1). - DoAndReturn(mockRedisStore.LPop) + LPop(mock.Anything, testCase.QueueName). + RunAndReturn(mockRedisStore.LPop). + Times(1) redisClient.EXPECT(). - RPush(gomock.Any(), testCase.QueueName, gomock.Any()). - Times(1). - DoAndReturn(mockRedisStore.RPush) + RPush(mock.Anything, testCase.QueueName, mock.Anything). + RunAndReturn(func(ctx context.Context, key string, values ...any) *redis.IntCmd { + return mockRedisStore.RPush(ctx, key, values[0].([]byte)) + }). + Times(1) if testCase.Unique { redisClient.EXPECT(). - SAdd(gomock.Any(), testCase.QueueName+"_unique", gomock.Any()). - Times(1). - DoAndReturn(mockRedisStore.SAdd) + SAdd(mock.Anything, testCase.QueueName+"_unique", mock.Anything). + RunAndReturn(func(ctx context.Context, key string, members ...any) *redis.IntCmd { + return mockRedisStore.SAdd(ctx, key, members[0].([]byte)) + }). + Times(1) redisClient.EXPECT(). - SRem(gomock.Any(), testCase.QueueName+"_unique", gomock.Any()). - Times(1). - DoAndReturn(mockRedisStore.SRem) + SRem(mock.Anything, testCase.QueueName+"_unique", mock.Anything). + RunAndReturn(func(ctx context.Context, key string, members ...any) *redis.IntCmd { + return mockRedisStore.SRem(ctx, key, members[0].([]byte)) + }). + Times(1) redisClient.EXPECT(). - SIsMember(gomock.Any(), testCase.QueueName+"_unique", gomock.Any()). - Times(2). - DoAndReturn(mockRedisStore.SIsMember) + SIsMember(mock.Anything, testCase.QueueName+"_unique", mock.Anything). + RunAndReturn(func(ctx context.Context, key string, member any) *redis.BoolCmd { + return mockRedisStore.SIsMember(ctx, key, member.([]byte)) + }). + Times(2) } client, err := newBaseRedisGeneric( diff --git a/modules/queue/base_test.go b/modules/queue/base_test.go index caa930158c..758faf1459 100644 --- a/modules/queue/base_test.go +++ b/modules/queue/base_test.go @@ -88,7 +88,7 @@ func testQueueBasic(t *testing.T, newFn func(cfg *BaseConfig) (baseQueue, error) // test blocking push if queue is full for i := 0; i < cfg.Length; i++ { - err = q.PushItem(ctx, []byte(fmt.Sprintf("item-%d", i))) + err = q.PushItem(ctx, fmt.Appendf(nil, "item-%d", i)) require.NoError(t, err) } ctxTimed, cancel = context.WithTimeout(ctx, 10*time.Millisecond) diff --git a/modules/queue/manager.go b/modules/queue/manager.go index 8f1a93f273..9c655b7fdc 100644 --- a/modules/queue/manager.go +++ b/modules/queue/manager.go @@ -5,6 +5,7 @@ package queue import ( "context" + "maps" "sync" "time" @@ -68,9 +69,7 @@ func (m *Manager) ManagedQueues() map[int64]ManagedWorkerPoolQueue { defer m.mu.Unlock() queues := make(map[int64]ManagedWorkerPoolQueue, len(m.Queues)) - for k, v := range m.Queues { - queues[k] = v - } + maps.Copy(queues, m.Queues) return queues } diff --git a/modules/queue/mock/redisuniversalclient.go b/modules/queue/mock/redisuniversalclient.go deleted file mode 100644 index a19e639ac1..0000000000 --- a/modules/queue/mock/redisuniversalclient.go +++ /dev/null @@ -1,344 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: forgejo.org/modules/nosql (interfaces: RedisClient) -// -// Generated by this command: -// -// mockgen -package mock -destination ./modules/queue/mock/redisuniversalclient.go forgejo.org/modules/nosql RedisClient -// - -// Package mock is a generated GoMock package. -package mock - -import ( - context "context" - reflect "reflect" - time "time" - - redis "github.com/redis/go-redis/v9" - gomock "go.uber.org/mock/gomock" -) - -// MockRedisClient is a mock of RedisClient interface. -type MockRedisClient struct { - ctrl *gomock.Controller - recorder *MockRedisClientMockRecorder - isgomock struct{} -} - -// MockRedisClientMockRecorder is the mock recorder for MockRedisClient. -type MockRedisClientMockRecorder struct { - mock *MockRedisClient -} - -// NewMockRedisClient creates a new mock instance. -func NewMockRedisClient(ctrl *gomock.Controller) *MockRedisClient { - mock := &MockRedisClient{ctrl: ctrl} - mock.recorder = &MockRedisClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockRedisClient) EXPECT() *MockRedisClientMockRecorder { - return m.recorder -} - -// Close mocks base method. -func (m *MockRedisClient) Close() error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Close") - ret0, _ := ret[0].(error) - return ret0 -} - -// Close indicates an expected call of Close. -func (mr *MockRedisClientMockRecorder) Close() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRedisClient)(nil).Close)) -} - -// DBSize mocks base method. -func (m *MockRedisClient) DBSize(ctx context.Context) *redis.IntCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DBSize", ctx) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// DBSize indicates an expected call of DBSize. -func (mr *MockRedisClientMockRecorder) DBSize(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBSize", reflect.TypeOf((*MockRedisClient)(nil).DBSize), ctx) -} - -// Decr mocks base method. -func (m *MockRedisClient) Decr(ctx context.Context, key string) *redis.IntCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Decr", ctx, key) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// Decr indicates an expected call of Decr. -func (mr *MockRedisClientMockRecorder) Decr(ctx, key any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decr", reflect.TypeOf((*MockRedisClient)(nil).Decr), ctx, key) -} - -// Del mocks base method. -func (m *MockRedisClient) Del(ctx context.Context, keys ...string) *redis.IntCmd { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range keys { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Del", varargs...) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// Del indicates an expected call of Del. -func (mr *MockRedisClientMockRecorder) Del(ctx any, keys ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, keys...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Del", reflect.TypeOf((*MockRedisClient)(nil).Del), varargs...) -} - -// Exists mocks base method. -func (m *MockRedisClient) Exists(ctx context.Context, keys ...string) *redis.IntCmd { - m.ctrl.T.Helper() - varargs := []any{ctx} - for _, a := range keys { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "Exists", varargs...) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// Exists indicates an expected call of Exists. -func (mr *MockRedisClientMockRecorder) Exists(ctx any, keys ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx}, keys...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockRedisClient)(nil).Exists), varargs...) -} - -// FlushDB mocks base method. -func (m *MockRedisClient) FlushDB(ctx context.Context) *redis.StatusCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FlushDB", ctx) - ret0, _ := ret[0].(*redis.StatusCmd) - return ret0 -} - -// FlushDB indicates an expected call of FlushDB. -func (mr *MockRedisClientMockRecorder) FlushDB(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushDB", reflect.TypeOf((*MockRedisClient)(nil).FlushDB), ctx) -} - -// Get mocks base method. -func (m *MockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", ctx, key) - ret0, _ := ret[0].(*redis.StringCmd) - return ret0 -} - -// Get indicates an expected call of Get. -func (mr *MockRedisClientMockRecorder) Get(ctx, key any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRedisClient)(nil).Get), ctx, key) -} - -// HDel mocks base method. -func (m *MockRedisClient) HDel(ctx context.Context, key string, fields ...string) *redis.IntCmd { - m.ctrl.T.Helper() - varargs := []any{ctx, key} - for _, a := range fields { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "HDel", varargs...) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// HDel indicates an expected call of HDel. -func (mr *MockRedisClientMockRecorder) HDel(ctx, key any, fields ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, key}, fields...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HDel", reflect.TypeOf((*MockRedisClient)(nil).HDel), varargs...) -} - -// HKeys mocks base method. -func (m *MockRedisClient) HKeys(ctx context.Context, key string) *redis.StringSliceCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "HKeys", ctx, key) - ret0, _ := ret[0].(*redis.StringSliceCmd) - return ret0 -} - -// HKeys indicates an expected call of HKeys. -func (mr *MockRedisClientMockRecorder) HKeys(ctx, key any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HKeys", reflect.TypeOf((*MockRedisClient)(nil).HKeys), ctx, key) -} - -// HSet mocks base method. -func (m *MockRedisClient) HSet(ctx context.Context, key string, values ...any) *redis.IntCmd { - m.ctrl.T.Helper() - varargs := []any{ctx, key} - for _, a := range values { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "HSet", varargs...) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// HSet indicates an expected call of HSet. -func (mr *MockRedisClientMockRecorder) HSet(ctx, key any, values ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, key}, values...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HSet", reflect.TypeOf((*MockRedisClient)(nil).HSet), varargs...) -} - -// Incr mocks base method. -func (m *MockRedisClient) Incr(ctx context.Context, key string) *redis.IntCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Incr", ctx, key) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// Incr indicates an expected call of Incr. -func (mr *MockRedisClientMockRecorder) Incr(ctx, key any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Incr", reflect.TypeOf((*MockRedisClient)(nil).Incr), ctx, key) -} - -// LLen mocks base method. -func (m *MockRedisClient) LLen(ctx context.Context, key string) *redis.IntCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LLen", ctx, key) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// LLen indicates an expected call of LLen. -func (mr *MockRedisClientMockRecorder) LLen(ctx, key any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LLen", reflect.TypeOf((*MockRedisClient)(nil).LLen), ctx, key) -} - -// LPop mocks base method. -func (m *MockRedisClient) LPop(ctx context.Context, key string) *redis.StringCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LPop", ctx, key) - ret0, _ := ret[0].(*redis.StringCmd) - return ret0 -} - -// LPop indicates an expected call of LPop. -func (mr *MockRedisClientMockRecorder) LPop(ctx, key any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPop", reflect.TypeOf((*MockRedisClient)(nil).LPop), ctx, key) -} - -// Ping mocks base method. -func (m *MockRedisClient) Ping(ctx context.Context) *redis.StatusCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Ping", ctx) - ret0, _ := ret[0].(*redis.StatusCmd) - return ret0 -} - -// Ping indicates an expected call of Ping. -func (mr *MockRedisClientMockRecorder) Ping(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockRedisClient)(nil).Ping), ctx) -} - -// RPush mocks base method. -func (m *MockRedisClient) RPush(ctx context.Context, key string, values ...any) *redis.IntCmd { - m.ctrl.T.Helper() - varargs := []any{ctx, key} - for _, a := range values { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "RPush", varargs...) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// RPush indicates an expected call of RPush. -func (mr *MockRedisClientMockRecorder) RPush(ctx, key any, values ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, key}, values...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPush", reflect.TypeOf((*MockRedisClient)(nil).RPush), varargs...) -} - -// SAdd mocks base method. -func (m *MockRedisClient) SAdd(ctx context.Context, key string, members ...any) *redis.IntCmd { - m.ctrl.T.Helper() - varargs := []any{ctx, key} - for _, a := range members { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "SAdd", varargs...) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// SAdd indicates an expected call of SAdd. -func (mr *MockRedisClientMockRecorder) SAdd(ctx, key any, members ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, key}, members...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SAdd", reflect.TypeOf((*MockRedisClient)(nil).SAdd), varargs...) -} - -// SIsMember mocks base method. -func (m *MockRedisClient) SIsMember(ctx context.Context, key string, member any) *redis.BoolCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SIsMember", ctx, key, member) - ret0, _ := ret[0].(*redis.BoolCmd) - return ret0 -} - -// SIsMember indicates an expected call of SIsMember. -func (mr *MockRedisClientMockRecorder) SIsMember(ctx, key, member any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SIsMember", reflect.TypeOf((*MockRedisClient)(nil).SIsMember), ctx, key, member) -} - -// SRem mocks base method. -func (m *MockRedisClient) SRem(ctx context.Context, key string, members ...any) *redis.IntCmd { - m.ctrl.T.Helper() - varargs := []any{ctx, key} - for _, a := range members { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "SRem", varargs...) - ret0, _ := ret[0].(*redis.IntCmd) - return ret0 -} - -// SRem indicates an expected call of SRem. -func (mr *MockRedisClientMockRecorder) SRem(ctx, key any, members ...any) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]any{ctx, key}, members...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SRem", reflect.TypeOf((*MockRedisClient)(nil).SRem), varargs...) -} - -// Set mocks base method. -func (m *MockRedisClient) Set(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Set", ctx, key, value, expiration) - ret0, _ := ret[0].(*redis.StatusCmd) - return ret0 -} - -// Set indicates an expected call of Set. -func (mr *MockRedisClientMockRecorder) Set(ctx, key, value, expiration any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockRedisClient)(nil).Set), ctx, key, value, expiration) -} diff --git a/modules/queue/workergroup.go b/modules/queue/workergroup.go index 3fb821ce69..87f01755aa 100644 --- a/modules/queue/workergroup.go +++ b/modules/queue/workergroup.go @@ -56,6 +56,7 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh full := false select { case q.batchChan <- batch: + break default: full = true } @@ -76,6 +77,7 @@ func (q *WorkerPoolQueue[T]) doDispatchBatchToWorker(wg *workerGroup[T], flushCh if full { select { case q.batchChan <- batch: + break case flush := <-flushChan: q.doWorkerHandle(batch) q.doFlush(wg, flush) @@ -105,7 +107,9 @@ func (q *WorkerPoolQueue[T]) doWorkerHandle(batch []T) { log.Error("Queue %q failed to handle batch of %d items, backoff for a few seconds", q.GetName(), len(batch)) select { case <-q.ctxRun.Done(): + break case <-time.After(time.Duration(unhandledItemRequeueDuration.Load())): + break } } for _, item := range unhandled { @@ -138,11 +142,7 @@ func (q *WorkerPoolQueue[T]) basePushForShutdown(items ...T) bool { // doStartNewWorker starts a new worker for the queue, the worker reads from worker's channel and handles the items. func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { - wp.wg.Add(1) - - go func() { - defer wp.wg.Done() - + wp.wg.Go(func() { log.Debug("Queue %q starts new worker", q.GetName()) defer log.Debug("Queue %q stops idle worker", q.GetName()) @@ -170,7 +170,9 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { t.Reset(workerIdleDuration) select { case <-t.C: + break default: + break } case <-t.C: q.workerNumMu.Lock() @@ -181,7 +183,7 @@ func (q *WorkerPoolQueue[T]) doStartNewWorker(wp *workerGroup[T]) { q.workerNumMu.Unlock() } } - }() + }) } // doFlush flushes the queue: it tries to read all items from the queue and handles them. @@ -296,6 +298,7 @@ func (q *WorkerPoolQueue[T]) doRun() { go func() { wg.wg.Wait(); close(workerDone) }() select { case <-workerDone: + break case <-time.After(shutdownTimeout): log.Error("Queue %q is shutting down, but workers are still running after timeout", q.GetName()) } diff --git a/modules/queue/workerqueue.go b/modules/queue/workerqueue.go index 6a71fc4fb4..c340bafa50 100644 --- a/modules/queue/workerqueue.go +++ b/modules/queue/workerqueue.go @@ -110,6 +110,7 @@ func (q *WorkerPoolQueue[T]) FlushWithContext(ctx context.Context, timeout time. // if it blocks, it means that there is a flush in progress or the queue hasn't been started yet select { case q.flushChan <- c: + break case <-ctx.Done(): return ctx.Err() case <-q.ctxRun.Done(): diff --git a/modules/queue/workerqueue_test.go b/modules/queue/workerqueue_test.go index 8d907ed8cd..da857b9405 100644 --- a/modules/queue/workerqueue_test.go +++ b/modules/queue/workerqueue_test.go @@ -78,17 +78,17 @@ func TestWorkerPoolQueueUnhandled(t *testing.T) { runCount := 2 // we can run these tests even hundreds times to see its stability t.Run("1/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { test(t, setting.QueueSettings{BatchLength: 1, MaxWorkers: 1}) } }) t.Run("3/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { test(t, setting.QueueSettings{BatchLength: 3, MaxWorkers: 1}) } }) t.Run("4/5", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { test(t, setting.QueueSettings{BatchLength: 4, MaxWorkers: 5}) } }) @@ -97,17 +97,17 @@ func TestWorkerPoolQueueUnhandled(t *testing.T) { func TestWorkerPoolQueuePersistence(t *testing.T) { runCount := 2 // we can run these tests even hundreds times to see its stability t.Run("1/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 1, MaxWorkers: 1, Length: 100}) } }) t.Run("3/1", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 3, MaxWorkers: 1, Length: 100}) } }) t.Run("4/5", func(t *testing.T) { - for i := 0; i < runCount; i++ { + for range runCount { testWorkerPoolQueuePersistence(t, setting.QueueSettings{BatchLength: 4, MaxWorkers: 5, Length: 100}) } }) @@ -142,7 +142,7 @@ func testWorkerPoolQueuePersistence(t *testing.T, queueSetting setting.QueueSett q, _ := newWorkerPoolQueueForTest("pr_patch_checker_test", queueSetting, testHandler, true) stop := runWorkerPoolQueue(q) - for i := 0; i < testCount; i++ { + for i := range testCount { _ = q.Push("task-" + strconv.Itoa(i)) } close(startWhenAllReady) @@ -187,7 +187,7 @@ func TestWorkerPoolQueueActiveWorkers(t *testing.T) { q, _ := newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 1, Length: 100}, handler, false) stop := runWorkerPoolQueue(q) - for i := 0; i < 5; i++ { + for i := range 5 { require.NoError(t, q.Push(i)) } @@ -203,7 +203,7 @@ func TestWorkerPoolQueueActiveWorkers(t *testing.T) { q, _ = newWorkerPoolQueueForTest("test-workpoolqueue", setting.QueueSettings{Type: "channel", BatchLength: 1, MaxWorkers: 3, Length: 100}, handler, false) stop = runWorkerPoolQueue(q) - for i := 0; i < 15; i++ { + for i := range 15 { require.NoError(t, q.Push(i)) } @@ -264,12 +264,12 @@ func TestWorkerPoolQueueWorkerIdleReset(t *testing.T) { stop := runWorkerPoolQueue(q) const workloadSize = 12 - for i := 0; i < workloadSize; i++ { + for i := range workloadSize { require.NoError(t, q.Push(i)) } workerIDs := make(map[string]struct{}) - for i := 0; i < workloadSize; i++ { + for i := range workloadSize { c := <-chGoroutineIDs workerIDs[c] = struct{}{} t.Logf("%d workers: overall=%d current=%d", i, len(workerIDs), q.GetWorkerNumber()) diff --git a/modules/repository/commits_test.go b/modules/repository/commits_test.go index 4b6d4bfe51..ef998f7916 100644 --- a/modules/repository/commits_test.go +++ b/modules/repository/commits_test.go @@ -5,7 +5,6 @@ package repository import ( - "strconv" "testing" "time" @@ -13,7 +12,6 @@ import ( repo_model "forgejo.org/models/repo" "forgejo.org/models/unittest" "forgejo.org/modules/git" - "forgejo.org/modules/setting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -126,7 +124,7 @@ func TestPushCommits_AvatarLink(t *testing.T) { } assert.Equal(t, - "/avatars/ab53a2911ddf9b4817ac01ddcd3d975f?size="+strconv.Itoa(28*setting.Avatar.RenderedSizeFactor), + "/avatars/ab53a2911ddf9b4817ac01ddcd3d975f", pushCommits.AvatarLink(db.DefaultContext, "user2@example.com")) assert.Equal(t, diff --git a/modules/repository/init.go b/modules/repository/init.go index 7b1442be93..16d366c6f0 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -138,8 +138,6 @@ func CheckInitRepository(ctx context.Context, owner, name, objectFormatName stri // Init git bare new repository. if err = git.InitRepository(ctx, repoPath, true, objectFormatName); err != nil { return fmt.Errorf("git.InitRepository: %w", err) - } else if err = CreateDelegateHooks(repoPath); err != nil { - return fmt.Errorf("createDelegateHooks: %w", err) } return nil } @@ -152,7 +150,7 @@ func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg } labels := make([]*issues_model.Label, len(list)) - for i := 0; i < len(list); i++ { + for i := range list { labels[i] = &issues_model.Label{ Name: list[i].Name, Exclusive: list[i].Exclusive, diff --git a/modules/repository/license_test.go b/modules/repository/license_test.go index 3195f15dda..5bd3846d61 100644 --- a/modules/repository/license_test.go +++ b/modules/repository/license_test.go @@ -33,11 +33,20 @@ func Test_getLicense(t *testing.T) { Copyright (c) 2023 Gitea -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. `, wantErr: require.NoError, }, diff --git a/modules/session/db.go b/modules/session/db.go index eea7e2136e..57f658dfd6 100644 --- a/modules/session/db.go +++ b/modules/session/db.go @@ -84,6 +84,11 @@ func (s *DBStore) Flush() error { return nil } +// True if no keys have been set +func (s *DBStore) Empty() bool { + return len(s.data) == 0 +} + // DBProvider represents a DB session provider implementation. type DBProvider struct { maxLifetime int64 diff --git a/modules/session/redis.go b/modules/session/redis.go index cf84ef21d9..1e8c61da8b 100644 --- a/modules/session/redis.go +++ b/modules/session/redis.go @@ -103,6 +103,11 @@ func (s *RedisStore) Flush() error { return nil } +// True if no keys have been set +func (s *RedisStore) Empty() bool { + return len(s.data) == 0 +} + // RedisProvider represents a redis session provider implementation. type RedisProvider struct { c nosql.RedisClient diff --git a/modules/session/virtual.go b/modules/session/virtual.go index 1c3e1c778b..fea7d11a44 100644 --- a/modules/session/virtual.go +++ b/modules/session/virtual.go @@ -64,7 +64,6 @@ func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) { return o.provider.Read(sid) } kv := make(map[any]any) - kv["_old_uid"] = "0" return NewVirtualStore(o, sid, kv), nil } @@ -77,7 +76,10 @@ func (o *VirtualSessionProvider) Exist(sid string) bool { func (o *VirtualSessionProvider) Destroy(sid string) error { o.lock.Lock() defer o.lock.Unlock() - return o.provider.Destroy(sid) + if o.provider.Exist(sid) { + return o.provider.Destroy(sid) + } + return nil } // Regenerate regenerates a session store from old session ID to new one. @@ -159,7 +161,7 @@ func (s *VirtualStore) Release() error { // Now need to lock the provider s.p.lock.Lock() defer s.p.lock.Unlock() - if oldUID, ok := s.data["_old_uid"]; (ok && (oldUID != "0" || len(s.data) > 1)) || (!ok && len(s.data) > 0) { + if len(s.data) > 0 { // Now ensure that we don't exist! realProvider := s.p.provider @@ -196,3 +198,8 @@ func (s *VirtualStore) Flush() error { s.data = make(map[any]any) return nil } + +// True if no keys have been set +func (s *VirtualStore) Empty() bool { + return len(s.data) == 0 +} diff --git a/modules/setting/actions.go b/modules/setting/actions.go index 5cdb2cbab4..930e0d0bed 100644 --- a/modules/setting/actions.go +++ b/modules/setting/actions.go @@ -7,6 +7,8 @@ import ( "fmt" "strings" "time" + + "forgejo.org/modules/jwtx" ) // Actions settings @@ -25,12 +27,16 @@ var ( SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"` LimitDispatchInputs int64 `ini:"LIMIT_DISPATCH_INPUTS"` ConcurrencyGroupQueueEnabled bool `ini:"CONCURRENCY_GROUP_QUEUE_ENABLED"` + IDTokenExpirationTime int64 `ini:"ID_TOKEN_EXPIRATION_TIME"` + + KeyCfg *jwtx.KeyCfg }{ Enabled: true, DefaultActionsURL: defaultActionsURLForgejo, SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"}, - LimitDispatchInputs: 10, + LimitDispatchInputs: 100, ConcurrencyGroupQueueEnabled: true, + IDTokenExpirationTime: 3600, } ) @@ -68,7 +74,8 @@ func (c logCompression) IsZstd() bool { } func loadActionsFrom(rootCfg ConfigProvider) error { - sec := rootCfg.Section("actions") + secName := "actions" + sec := rootCfg.Section(secName) err := sec.MapTo(&Actions) if err != nil { return fmt.Errorf("failed to map Actions settings: %v", err) @@ -104,5 +111,9 @@ func loadActionsFrom(rootCfg ConfigProvider) error { return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression) } + Actions.KeyCfg, err = loadKeyCfg(rootCfg, secName, "ID_TOKEN_", "RS256", "actions_id_token/private.pem", onlyAsymmetric()) + if err != nil { + return err + } return nil } diff --git a/modules/setting/actions_test.go b/modules/setting/actions_test.go index a3cd5ced44..1bbd4d0251 100644 --- a/modules/setting/actions_test.go +++ b/modules/setting/actions_test.go @@ -7,6 +7,8 @@ import ( "path/filepath" "testing" + "forgejo.org/modules/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -155,3 +157,80 @@ DEFAULT_ACTIONS_URL = https://example.com }) } } + +func Test_getIDTokenSettingsForActions(t *testing.T) { + defer test.MockVariableValue(&AppDataPath, "/home/app/data")() + + oldActions := Actions + oldAppURL := AppURL + defer func() { + Actions = oldActions + AppURL = oldAppURL + }() + + iniStr := ` + [actions] + ` + cfg, err := NewConfigProviderFromData(iniStr) + require.NoError(t, err) + require.NoError(t, loadActionsFrom(cfg)) + + assert.Equal(t, "RS256", Actions.KeyCfg.Signing.Algorithm) + assert.Equal(t, "/home/app/data/actions_id_token/private.pem", *Actions.KeyCfg.Signing.PrivateKeyPath) + assert.EqualValues(t, 3600, Actions.IDTokenExpirationTime) + + iniStr = ` + [actions] + ID_TOKEN_SIGNING_ALGORITHM = ES256 + ID_TOKEN_SIGNING_PRIVATE_KEY_FILE = /test/test.pem + ID_TOKEN_EXPIRATION_TIME = 120 + ` + cfg, err = NewConfigProviderFromData(iniStr) + require.NoError(t, err) + require.NoError(t, loadActionsFrom(cfg)) + + assert.Equal(t, "ES256", Actions.KeyCfg.Signing.Algorithm) + assert.Equal(t, "/test/test.pem", *Actions.KeyCfg.Signing.PrivateKeyPath) + assert.EqualValues(t, 120, Actions.IDTokenExpirationTime) + + iniStr = ` + [actions] + ID_TOKEN_SIGNING_ALGORITHM = EdDSA + ID_TOKEN_SIGNING_PRIVATE_KEY_FILE = ./test/test.pem + ID_TOKEN_EXPIRATION_TIME = 123 + ` + cfg, err = NewConfigProviderFromData(iniStr) + require.NoError(t, err) + require.NoError(t, loadActionsFrom(cfg)) + + assert.Equal(t, "EdDSA", Actions.KeyCfg.Signing.Algorithm) + assert.Equal(t, "/home/app/data/test/test.pem", *Actions.KeyCfg.Signing.PrivateKeyPath) + assert.EqualValues(t, 123, Actions.IDTokenExpirationTime) + + iniStr = ` + [actions] + ID_TOKEN_SIGNING_ALGORITHM = HS256 + ` + cfg, err = NewConfigProviderFromData(iniStr) + require.NoError(t, err) + err = loadActionsFrom(cfg) + require.ErrorContains(t, err, "[actions] Unexpected algorithm: ID_TOKEN_SIGNING_ALGORITHM = HS256, needs to be one of [RS256 RS384 RS512 ES256 ES384 ES512 EdDSA]") + + iniStr = ` + [actions] + ID_TOKEN_SECRET = ABC + ` + cfg, err = NewConfigProviderFromData(iniStr) + require.NoError(t, err) + err = loadActionsFrom(cfg) + require.ErrorContains(t, err, "[actions] Invalid config key: ID_TOKEN_SECRET - must be removed") + + iniStr = ` + [actions] + ID_TOKEN_SECRET_URI = ABC + ` + cfg, err = NewConfigProviderFromData(iniStr) + require.NoError(t, err) + err = loadActionsFrom(cfg) + require.ErrorContains(t, err, "[actions] Invalid config key: ID_TOKEN_SECRET_URI - must be removed") +} diff --git a/modules/setting/admin.go b/modules/setting/admin.go index 7a1e071bac..f383a5e382 100644 --- a/modules/setting/admin.go +++ b/modules/setting/admin.go @@ -26,7 +26,8 @@ func loadAdminFrom(rootCfg ConfigProvider) { } const ( - UserFeatureDeletion = "deletion" - UserFeatureManageSSHKeys = "manage_ssh_keys" - UserFeatureManageGPGKeys = "manage_gpg_keys" + UserFeatureDeletion = "deletion" + UserFeatureManageSSHKeys = "manage_ssh_keys" + UserFeatureManageGPGKeys = "manage_gpg_keys" + UserFeatureManagePassword = "manage_password" ) diff --git a/modules/setting/authorized_integration.go b/modules/setting/authorized_integration.go new file mode 100644 index 0000000000..0a84f44afe --- /dev/null +++ b/modules/setting/authorized_integration.go @@ -0,0 +1,23 @@ +// Copyright 2026 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package setting + +import "time" + +var AuthorizedIntegration = struct { + AllowedDomains string + BlockedDomains string + AllowLocalNetworks bool + RequestTimeout time.Duration + CacheTTL time.Duration +}{} + +func loadAuthorizedIntegrationFrom(rootCfg ConfigProvider) { + sec := rootCfg.Section("authorized_integration") + AuthorizedIntegration.AllowedDomains = sec.Key("ALLOWED_DOMAINS").MustString("") + AuthorizedIntegration.BlockedDomains = sec.Key("BLOCKED_DOMAINS").MustString("") + AuthorizedIntegration.AllowLocalNetworks = sec.Key("ALLOW_LOCALNETWORKS").MustBool(false) + AuthorizedIntegration.RequestTimeout = sec.Key("REQUEST_TIMEOUT").MustDuration(10 * time.Second) + AuthorizedIntegration.CacheTTL = sec.Key("CACHE_TTL").MustDuration(10 * time.Minute) +} diff --git a/modules/setting/cache.go b/modules/setting/cache.go index cdc7e1a971..519a236599 100644 --- a/modules/setting/cache.go +++ b/modules/setting/cache.go @@ -53,6 +53,7 @@ func loadCacheFrom(rootCfg ConfigProvider) { CacheService.Adapter = sec.Key("ADAPTER").In("memory", []string{"memory", "redis", "memcache", "twoqueue"}) switch CacheService.Adapter { case "memory": + break case "redis", "memcache": CacheService.Conn = strings.Trim(sec.Key("HOST").String(), "\" ") case "twoqueue": diff --git a/modules/setting/config.go b/modules/setting/config.go index 6299640e61..90f3d12d11 100644 --- a/modules/setting/config.go +++ b/modules/setting/config.go @@ -4,6 +4,7 @@ package setting import ( + "strings" "sync" "forgejo.org/modules/log" @@ -23,11 +24,11 @@ type OpenWithEditorApp struct { type OpenWithEditorAppsType []OpenWithEditorApp func (t OpenWithEditorAppsType) ToTextareaString() string { - ret := "" + var ret strings.Builder for _, app := range t { - ret += app.DisplayName + " = " + app.OpenURL + "\n" + ret.WriteString(app.DisplayName + " = " + app.OpenURL + "\n") } - return ret + return ret.String() } func DefaultOpenWithEditorApps() OpenWithEditorAppsType { diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index 458dbb51bb..68a7c94db2 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -51,10 +51,10 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) { for _, unescapeIdx := range escapeStringIndices { preceding := encoded[last:unescapeIdx[0]] if !inKey { - if splitter := strings.Index(preceding, "__"); splitter > -1 { - section += preceding[:splitter] + if before, after, ok := strings.Cut(preceding, "__"); ok { + section += before inKey = true - key += preceding[splitter+2:] + key += after } else { section += preceding } @@ -77,9 +77,9 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) { } remaining := encoded[last:] if !inKey { - if splitter := strings.Index(remaining, "__"); splitter > -1 { - section += remaining[:splitter] - key += remaining[splitter+2:] + if before, after, ok := strings.Cut(remaining, "__"); ok { + section += before + key += after } else { section += remaining } @@ -113,25 +113,24 @@ func decodeEnvironmentKey(prefixRegexp *regexp.Regexp, suffixFile, envKey string func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { prefixRegexp := regexp.MustCompile(EnvConfigKeyPrefixGitea) for _, kv := range envs { - idx := strings.IndexByte(kv, '=') - if idx < 0 { + before, after, ok0 := strings.Cut(kv, "=") + if !ok0 { continue } // parse the environment variable to config section name and key name - envKey := kv[:idx] - envValue := kv[idx+1:] + envKey := before + keyValue := after ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(prefixRegexp, EnvConfigKeySuffixFile, envKey) if !ok { continue } // use environment value as config value, or read the file content as value if the key indicates a file - keyValue := envValue if useFileValue { - fileContent, err := os.ReadFile(envValue) + fileContent, err := os.ReadFile(keyValue) if err != nil { - log.Error("Error reading file for %s : %v", envKey, envValue, err) + log.Error("Error reading file for %s : %v", envKey, keyValue, err) continue } if bytes.HasSuffix(fileContent, []byte("\r\n")) { diff --git a/modules/setting/database.go b/modules/setting/database.go index 5b8fdaa34c..4299044d1f 100644 --- a/modules/setting/database.go +++ b/modules/setting/database.go @@ -96,7 +96,7 @@ func loadDBSetting(rootCfg ConfigProvider) { Database.ConnMaxLifetime = sec.Key("CONN_MAX_LIFETIME").MustDuration(0) } Database.ConnMaxIdleTime = sec.Key("CONN_MAX_IDLETIME").MustDuration(0) - Database.MaxOpenConns = sec.Key("MAX_OPEN_CONNS").MustInt(100) + Database.MaxOpenConns = sec.Key("MAX_OPEN_CONNS").MustInt(30) Database.IterateBufferSize = sec.Key("ITERATE_BUFFER_SIZE").MustInt(50) Database.LogSQL = sec.Key("LOG_SQL").MustBool(false) diff --git a/modules/setting/f3.go b/modules/setting/f3.go index 31d12294b8..2412938a49 100644 --- a/modules/setting/f3.go +++ b/modules/setting/f3.go @@ -3,6 +3,9 @@ package setting import ( + "os" + "path/filepath" + "forgejo.org/modules/log" ) @@ -10,8 +13,10 @@ import ( var ( F3 = struct { Enabled bool + Path string }{ Enabled: false, + Path: "f3", } ) @@ -20,7 +25,17 @@ func LoadF3Setting() { } func loadF3From(rootCfg ConfigProvider) { - if err := rootCfg.Section("F3").MapTo(&F3); err != nil { + if err := rootCfg.Section("f3").MapTo(&F3); err != nil { log.Fatal("Failed to map F3 settings: %v", err) } + + if !filepath.IsAbs(F3.Path) { + F3.Path = filepath.Join(AppDataPath, F3.Path) + } else { + F3.Path = filepath.Clean(F3.Path) + } + + if err := os.MkdirAll(F3.Path, os.ModePerm); err != nil { + log.Fatal("Failed to create F3 path %s: %v", F3.Path, err) + } } diff --git a/modules/setting/f3_test.go b/modules/setting/f3_test.go new file mode 100644 index 0000000000..76c4fe015c --- /dev/null +++ b/modules/setting/f3_test.go @@ -0,0 +1,74 @@ +// Copyright 2024 The Forgejo Authors. +// SPDX-License-Identifier: GPLv3-or-later + +package setting + +import ( + "fmt" + "testing" + + "forgejo.org/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSettingF3(t *testing.T) { + restoreF3 := test.MockProtect(&F3) + restoreAppDataPath := test.MockProtect(&AppDataPath) + restore := func() { + restoreF3() + restoreAppDataPath() + } + defer restore() + + t.Run("enabled", func(t *testing.T) { + restore() + cfg, err := NewConfigProviderFromData(` +[f3] +ENABLED = true +`) + require.NoError(t, err) + loadF3From(cfg) + assert.True(t, F3.Enabled) + assert.DirExists(t, F3.Path) + }) + + t.Run("disabled by default", func(t *testing.T) { + restore() + cfg, err := NewConfigProviderFromData(` +[f3] +`) + require.NoError(t, err) + loadF3From(cfg) + assert.False(t, F3.Enabled) + }) + + t.Run("default f3 path", func(t *testing.T) { + restore() + cfg, err := NewConfigProviderFromData(` +[f3] +ENABLED = true +`) + require.NoError(t, err) + AppDataPath = t.TempDir() + loadF3From(cfg) + assert.Equal(t, AppDataPath+"/f3", F3.Path) + assert.DirExists(t, F3.Path) + }) + + t.Run("absolute f3 path", func(t *testing.T) { + restore() + other := t.TempDir() + cfg, err := NewConfigProviderFromData(fmt.Sprintf(` +[f3] +ENABLED = true +PATH = %[1]s +`, other)) + require.NoError(t, err) + AppDataPath = t.TempDir() + loadF3From(cfg) + assert.Equal(t, other, F3.Path) + assert.DirExists(t, F3.Path) + }) +} diff --git a/modules/setting/incoming_email.go b/modules/setting/incoming_email.go index a890a4a328..47fc2e560d 100644 --- a/modules/setting/incoming_email.go +++ b/modules/setting/incoming_email.go @@ -82,6 +82,7 @@ func checkReplyToAddress() error { case 0: return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder) case 1: + break default: return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder) } diff --git a/modules/setting/indexer.go b/modules/setting/indexer.go index b112a50cfa..948dae0bea 100644 --- a/modules/setting/indexer.go +++ b/modules/setting/indexer.go @@ -108,7 +108,7 @@ func loadIndexerFrom(rootCfg ConfigProvider) { // IndexerGlobFromString parses a comma separated list of patterns and returns a glob.Glob slice suited for repo indexing func IndexerGlobFromString(globstr string) []Glob { extarr := make([]Glob, 0, 10) - for _, expr := range strings.Split(strings.ToLower(globstr), ",") { + for expr := range strings.SplitSeq(strings.ToLower(globstr), ",") { expr = strings.TrimSpace(expr) if expr != "" { if g, err := glob.Compile(expr, '.', '/'); err != nil { diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index 452bfae737..a97ed8d40f 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "forgejo.org/modules/generate" + "forgejo.org/modules/jwtx" ) // LFS represents the server-side configuration for Git LFS. @@ -16,13 +16,13 @@ import ( // Could be refactored in the future while keeping backwards compatibility. var LFS = struct { StartServer bool `ini:"LFS_START_SERVER"` - JWTSecretBytes []byte `ini:"-"` HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"` MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"` LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"` MaxBatchSize int `ini:"LFS_MAX_BATCH_SIZE"` - Storage *Storage + SigningKey jwtx.SigningKey + Storage *Storage }{} // LFSClient represents configuration for Gitea's LFS clients, for example: mirroring upstream Git LFS @@ -77,21 +77,12 @@ func loadLFSFrom(rootCfg ConfigProvider) error { return nil } - jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") - LFS.JWTSecretBytes, err = generate.DecodeJwtSecret(jwtSecretBase64) + keyCfg, err := loadKeyCfg(rootCfg, "server", "LFS_JWT_", "HS256", "lfs/private.pem") + if err == nil { + LFS.SigningKey, err = jwtx.InitSigningKey(&keyCfg.Signing) + } if err != nil { - LFS.JWTSecretBytes, jwtSecretBase64 = generate.NewJwtSecret() - - // Save secret - saveCfg, err := rootCfg.PrepareSaving() - if err != nil { - return fmt.Errorf("error saving JWT Secret for custom config: %v", err) - } - rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64) - saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64) - if err := saveCfg.Save(); err != nil { - return fmt.Errorf("error saving JWT Secret for custom config: %v", err) - } + return fmt.Errorf("lfs key initialization failed: %v", err) } return nil diff --git a/modules/setting/log.go b/modules/setting/log.go index ecc591fd35..7799f8187b 100644 --- a/modules/setting/log.go +++ b/modules/setting/log.go @@ -269,8 +269,8 @@ func initLoggerByName(manager *log.LoggerManager, rootCfg ConfigProvider, logger } var eventWriters []log.EventWriter - modes := strings.Split(modeVal, ",") - for _, modeName := range modes { + modes := strings.SplitSeq(modeVal, ",") + for modeName := range modes { modeName = strings.TrimSpace(modeName) if modeName == "" { continue diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index b43484a90f..a84758c505 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -235,6 +235,7 @@ func loadMailerFrom(rootCfg ConfigProvider) { } } case "dummy": // just mention and do nothing + break } if MailService.From != "" { diff --git a/modules/setting/markup.go b/modules/setting/markup.go index 4ab9e7b2d1..0ece86dfd1 100644 --- a/modules/setting/markup.go +++ b/modules/setting/markup.go @@ -85,8 +85,8 @@ func loadMarkupFrom(rootCfg ConfigProvider) { func newMarkupSanitizer(name string, sec ConfigSection) { rule, ok := createMarkupSanitizerRule(name, sec) if ok { - if strings.HasPrefix(name, "sanitizer.") { - names := strings.SplitN(strings.TrimPrefix(name, "sanitizer."), ".", 2) + if after, ok0 := strings.CutPrefix(name, "sanitizer."); ok0 { + names := strings.SplitN(after, ".", 2) name = names[0] } for _, renderer := range ExternalMarkupRenderers { diff --git a/modules/setting/mirror.go b/modules/setting/mirror.go index 58c57c5c95..083c67db45 100644 --- a/modules/setting/mirror.go +++ b/modules/setting/mirror.go @@ -48,11 +48,7 @@ func loadMirrorFrom(rootCfg ConfigProvider) { Mirror.MinInterval = 1 * time.Minute } if Mirror.DefaultInterval < Mirror.MinInterval { - if time.Hour*8 < Mirror.MinInterval { - Mirror.DefaultInterval = Mirror.MinInterval - } else { - Mirror.DefaultInterval = time.Hour * 8 - } + Mirror.DefaultInterval = max(time.Hour*8, Mirror.MinInterval) log.Warn("Mirror.DefaultInterval is less than Mirror.MinInterval, set to %s", Mirror.DefaultInterval.String()) } } diff --git a/modules/setting/oauth2.go b/modules/setting/oauth2.go index 9e95e1c6a9..8598b7b24d 100644 --- a/modules/setting/oauth2.go +++ b/modules/setting/oauth2.go @@ -5,10 +5,10 @@ package setting import ( "math" - "path/filepath" "sync/atomic" "forgejo.org/modules/generate" + "forgejo.org/modules/jwtx" "forgejo.org/modules/log" ) @@ -96,18 +96,15 @@ var OAuth2 = struct { AccessTokenExpirationTime int64 RefreshTokenExpirationTime int64 InvalidateRefreshTokens bool - JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"` - JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"` MaxTokenLength int DefaultApplications []string EnableAdditionalGrantScopes bool + KeyCfg *jwtx.KeyCfg }{ Enabled: true, AccessTokenExpirationTime: 3600, RefreshTokenExpirationTime: 730, InvalidateRefreshTokens: true, - JWTSigningAlgorithm: "RS256", - JWTSigningPrivateKeyFile: "jwt/private.pem", MaxTokenLength: math.MaxInt16, DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"}, EnableAdditionalGrantScopes: false, @@ -126,30 +123,26 @@ func loadOAuth2From(rootCfg ConfigProvider) { OAuth2.Enabled = sec.Key("ENABLE").MustBool(OAuth2.Enabled) } - if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) { - OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile) - } - // FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET" // Because this secret is also used as GeneralTokenSigningSecret (as a quick not-that-breaking fix for some legacy problems). // Including: CSRF token, account validation token, etc ... // In main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...) - jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET") if InstallLock { - jwtSecretBytes, err := generate.DecodeJwtSecret(jwtSecretBase64) + signingKey, err := loadSymmeticSigningKeyCfg(rootCfg, sec, "JWT_") if err != nil { - jwtSecretBytes, jwtSecretBase64 = generate.NewJwtSecret() - saveCfg, err := rootCfg.PrepareSaving() - if err != nil { - log.Fatal("save oauth2.JWT_SECRET failed: %v", err) - } - rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64) - saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64) - if err := saveCfg.Save(); err != nil { - log.Fatal("save oauth2.JWT_SECRET failed: %v", err) - } + log.Fatal("%v", err) } - generalSigningSecret.Store(&jwtSecretBytes) + generalSigningSecret.Store(signingKey) + } + + if !OAuth2.Enabled { + return + } + + var err error + OAuth2.KeyCfg, err = loadKeyCfg(rootCfg, "oauth2", "JWT_", "RS256", "jwt/private.pem") + if err != nil { + log.Fatal("oauth2 key initialization failed: %v", err) } } diff --git a/modules/setting/pwa.go b/modules/setting/pwa.go new file mode 100644 index 0000000000..2c17efd715 --- /dev/null +++ b/modules/setting/pwa.go @@ -0,0 +1,64 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package setting + +import ( + "forgejo.org/modules/json" + "forgejo.org/modules/log" +) + +type PwaConfig struct { + Standalone bool +} + +var PWA = PwaConfig{ + Standalone: false, +} + +func loadPWAFrom(rootCfg ConfigProvider) { + sec := rootCfg.Section("pwa") + if err := sec.MapTo(&PWA); err != nil { + log.Fatal("Failed to map [pwa] settings: %v", err) + } +} + +type manifestIcon struct { + Src string `json:"src"` + Type string `json:"type"` + Sizes string `json:"sizes"` +} + +type manifestJSON struct { + Name string `json:"name"` + ShortName string `json:"short_name"` + StartURL string `json:"start_url"` + Icons []manifestIcon `json:"icons"` + Display string `json:"display,omitempty"` +} + +func GetManifestJSON() ([]byte, error) { + manifest := manifestJSON{ + Name: AppName, + ShortName: AppName, + StartURL: AppURL, + Icons: []manifestIcon{ + { + Src: AbsoluteAssetURL + "/assets/img/logo.png", + Type: "image/png", + Sizes: "512x512", + }, + { + Src: AbsoluteAssetURL + "/assets/img/logo.svg", + Type: "image/svg+xml", + Sizes: "512x512", + }, + }, + } + + if PWA.Standalone { + manifest.Display = "standalone" + } + + return json.Marshal(manifest) +} diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 7e774f0139..001032e3cb 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -96,7 +96,6 @@ var ( DefaultMergeMessageOfficialApproversOnly bool DefaultUpdateStyle string PopulateSquashCommentWithCommitMessages bool - AddCoCommitterTrailers bool RetargetChildrenOnMerge bool } `ini:"repository.pull-request"` @@ -227,7 +226,6 @@ var ( DefaultMergeMessageOfficialApproversOnly bool DefaultUpdateStyle string PopulateSquashCommentWithCommitMessages bool - AddCoCommitterTrailers bool RetargetChildrenOnMerge bool }{ WorkInProgressPrefixes: []string{"WIP:", "[WIP]"}, @@ -243,7 +241,6 @@ var ( DefaultMergeMessageOfficialApproversOnly: true, DefaultUpdateStyle: "merge", PopulateSquashCommentWithCommitMessages: false, - AddCoCommitterTrailers: true, RetargetChildrenOnMerge: true, }, diff --git a/modules/setting/security.go b/modules/setting/security.go index 347912f248..586989101d 100644 --- a/modules/setting/security.go +++ b/modules/setting/security.go @@ -4,12 +4,15 @@ package setting import ( + "fmt" "net/url" "os" + "path/filepath" "strings" "forgejo.org/modules/auth/password/hash" "forgejo.org/modules/generate" + "forgejo.org/modules/jwtx" "forgejo.org/modules/keying" "forgejo.org/modules/log" ) @@ -39,6 +42,31 @@ var ( DisableQueryAuthToken bool ) +/* + * key loading is a two-stage process to avoid complications for unit tests: + * + * For symmetric keys, we want to add a random key to the configuration. We would + * not want to change the configuration after loading has completed to maintain + * isolation. So from this perspective, we would want to initialize keys only + * during setting.load...From() + * + * For asymmetric keys, we want to create a random private key _file_. + * Doing so during the setting load phase, however, creates key files for unit + * tests, because they usually complete the full settings load phase, but do not + * initialize modules which they do not need. Depending on the test case, it + * might not provide a writable AppDataPath, or it would leave private key files + * in the source tree. All of this can be avoided with + * specifically adjusting the ini loaded for unit tests, but adds considerable + * friction. + * + * So to avoid all this, we split key loading in two phases: + * - settings parse the config and save missing symmetric keys + * - module init takes the parsed config, creates missing asymmetric keys and + * creates the actual signingkey objects + * + * jwtx.SigningKeyCfg and jwtx.KeyCfg are used for handover + */ + // loadSecret load the secret from ini by uriKey or verbatimKey, only one of them could be set // If the secret is loaded from uriKey (file), the file should be non-empty, to guarantee the behavior stable and clear. func loadSecret(sec ConfigSection, uriKey, verbatimKey string) string { @@ -53,16 +81,29 @@ func loadSecret(sec ConfigSection, uriKey, verbatimKey string) string { if uri == "" { return verbatim } + verbatim, err := loadSecretFromURI(uri) + if err == nil { + return verbatim + } + log.Fatal("%s: %w", uriKey, err) + // unreached + return "" +} +func loadSecretFromURI(uri string) (string, error) { tempURI, err := url.Parse(uri) if err != nil { - log.Fatal("Failed to parse %s (%s): %v", uriKey, uri, err) + return "", fmt.Errorf("Failed to parse %s: %v", uri, err) } switch tempURI.Scheme { case "file": - buf, err := os.ReadFile(tempURI.RequestURI()) + path := tempURI.RequestURI() + if !filepath.IsAbs(path) { + path = filepath.Join(AppDataPath, path) + } + buf, err := os.ReadFile(path) if err != nil { - log.Fatal("Failed to read %s (%s): %v", uriKey, tempURI.RequestURI(), err) + return "", fmt.Errorf("Failed to read %s: %v", path, err) } val := strings.TrimSpace(string(buf)) if val == "" { @@ -70,17 +111,135 @@ func loadSecret(sec ConfigSection, uriKey, verbatimKey string) string { // For example: if INTERNAL_TOKEN_URI=file:///empty-file, // Then if the token is re-generated during installation and saved to INTERNAL_TOKEN // Then INTERNAL_TOKEN and INTERNAL_TOKEN_URI both exist, that's a fatal error (they shouldn't) - log.Fatal("Failed to read %s (%s): the file is empty", uriKey, tempURI.RequestURI()) + return "", fmt.Errorf("Failed to read %s: the file is empty", path) } - return val + return val, nil // only file URIs are allowed default: - log.Fatal("Unsupported URI-Scheme %q (%q = %q)", tempURI.Scheme, uriKey, uri) - return "" + return "", fmt.Errorf("Unsupported URI-Scheme %q in %q", tempURI.Scheme, uri) } } +// createSymmeticSigningKey creates a new symmetric signing key and saves it to +// the setting named cfgSecret (usually [PFX_]SECRET) in section cfgSection +func createSymmeticSigningKeyCfg(rootCfg ConfigProvider, cfgSection, cfgSecret string) (*[]byte, error) { + jwtSecretBytes, jwtSecretBase64 := generate.NewJwtSecret() + saveCfg, err := rootCfg.PrepareSaving() + if err == nil { + rootCfg.Section(cfgSection).Key(cfgSecret).SetValue(jwtSecretBase64) + saveCfg.Section(cfgSection).Key(cfgSecret).SetValue(jwtSecretBase64) + err = saveCfg.Save() + } + if err != nil { + return nil, fmt.Errorf("save %s.%s failed: %v", cfgSection, cfgSecret, err) + } + return &jwtSecretBytes, nil +} + +// loadSymmeticSigningKey loads a signing key and creates it unless present +// loads from [pfx]SECRET_URI +// loads from or saves to [pfx]SECRET +// in section sec +func loadSymmeticSigningKeyCfg(rootCfg ConfigProvider, sec ConfigSection, pfx string) (*[]byte, error) { + cfgSecretURI := pfx + "SECRET_URI" + cfgSecret := pfx + "SECRET" + + secretBase64 := loadSecret(sec, cfgSecretURI, cfgSecret) + secret, err := generate.DecodeJwtSecret(secretBase64) + if err == nil { + return &secret, nil + } + + log.Info("[%s] %s or %s failed loading: %v - creating new key", sec.Name(), cfgSecret, cfgSecretURI, err) + return createSymmeticSigningKeyCfg(rootCfg, sec.Name(), cfgSecret) +} + +// loadAsymmeticSigningKey loads a signing key from [pfx]SIGNING_PRIVATE_KEY_FILE +// or creates it if it does not exist +func loadAsymmeticSigningKeyPath(sec ConfigSection, pfx, defaultFile string) *string { + cfgFile := pfx + "SIGNING_PRIVATE_KEY_FILE" + keyPath := sec.Key(cfgFile).MustString(defaultFile) + if !filepath.IsAbs(keyPath) { + keyPath = filepath.Join(AppDataPath, keyPath) + } + return &keyPath +} + +type checkKeyCfg func(rootCfg ConfigProvider, cfgSection, pfx string) error + +func onlyAsymmetric() checkKeyCfg { + return func(rootCfg ConfigProvider, cfgSection, pfx string) error { + sec := rootCfg.Section(cfgSection) + cfgAlg := pfx + "SIGNING_ALGORITHM" + + if sec.HasKey(cfgAlg) { + alg := sec.Key(cfgAlg).String() + if !jwtx.IsValidAsymmetricAlgorithm(alg) { + return fmt.Errorf("Unexpected algorithm: %s = %s, needs to be one of %v", + cfgAlg, alg, jwtx.ValidAsymmetricAlgorithms) + } + } + + noCfg := []string{ + pfx + "SECRET_URI", + pfx + "SECRET", + } + + for _, cfg := range noCfg { + if sec.HasKey(cfg) { + return fmt.Errorf("Invalid config key: %s - must be removed", cfg) + } + } + + return nil + } +} + +// loadSigningKey() loads a or creates signing key based on settings in section cfgSection +// [pfx]SIGNING_ALGORITHM determines the algorithm +// [pfx]SECRET is a literal secret for symmetric algorithms +// [pfx]SECRET_URI is the uri of a secret for symmetric algorithms +// [pfx]SIGNING_PRIVATE_KEY_FILE is a file with a private key for asymmetric algorithms +// +// [pfx]SECRET might get written to literally in the config if needed but not present + +func loadSigningKeyCfg(rootCfg ConfigProvider, cfgSection, pfx, defaultAlg, defaultPrivateKeyFile string, checks ...checkKeyCfg) (*jwtx.SigningKeyCfg, error) { + for _, check := range checks { + err := check(rootCfg, cfgSection, pfx) + if err != nil { + return nil, err + } + } + + sec := rootCfg.Section(cfgSection) + cfgAlg := pfx + "SIGNING_ALGORITHM" + + algorithm := sec.Key(cfgAlg).MustString(defaultAlg) + + cfg := jwtx.SigningKeyCfg{Algorithm: algorithm} + var err error + + if jwtx.IsValidSymmetricAlgorithm(algorithm) { + cfg.SecretBytes, err = loadSymmeticSigningKeyCfg(rootCfg, sec, pfx) + } else if jwtx.IsValidAsymmetricAlgorithm(algorithm) { + cfg.PrivateKeyPath = loadAsymmeticSigningKeyPath(sec, pfx, defaultPrivateKeyFile) + } else { + err = fmt.Errorf("invalid algorithm: %s = %s", cfgAlg, algorithm) + } + + return &cfg, err +} + +func loadKeyCfg(rootCfg ConfigProvider, cfgSection, pfx, defaultAlg, defaultPrivateKeyFile string, checks ...checkKeyCfg) (*jwtx.KeyCfg, error) { + signing, err := loadSigningKeyCfg(rootCfg, cfgSection, pfx, defaultAlg, defaultPrivateKeyFile, checks...) + if err != nil { + err = fmt.Errorf("[%s] %v", cfgSection, err) + return nil, err + } + return &jwtx.KeyCfg{Signing: signing}, nil +} + // generateSaveInternalToken generates and saves the internal token to app.ini func generateSaveInternalToken(rootCfg ConfigProvider) { token, err := generate.NewInternalToken() @@ -114,7 +273,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) { GlobalTwoFactorRequirement = NewTwoFactorRequirementType(sec.Key("GLOBAL_TWO_FACTOR_REQUIREMENT").String()) - CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("gitea_incredible") + CookieRememberName = sec.Key("COOKIE_REMEMBER_NAME").MustString("persistent") ReverseProxyAuthUser = sec.Key("REVERSE_PROXY_AUTHENTICATION_USER").MustString("X-WEBAUTH-USER") ReverseProxyAuthEmail = sec.Key("REVERSE_PROXY_AUTHENTICATION_EMAIL").MustString("X-WEBAUTH-EMAIL") diff --git a/modules/setting/server.go b/modules/setting/server.go index 3ff91d2cde..11c4ff8212 100644 --- a/modules/setting/server.go +++ b/modules/setting/server.go @@ -1,10 +1,10 @@ // Copyright 2023 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. // SPDX-License-Identifier: MIT package setting import ( - "encoding/base64" "net" "net/url" "path" @@ -13,7 +13,6 @@ import ( "strings" "time" - "forgejo.org/modules/json" "forgejo.org/modules/log" "forgejo.org/modules/util" @@ -111,50 +110,8 @@ var ( PerWritePerKbTimeout = 10 * time.Second StaticURLPrefix string AbsoluteAssetURL string - - ManifestData string ) -// MakeManifestData generates web app manifest JSON -func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte { - type manifestIcon struct { - Src string `json:"src"` - Type string `json:"type"` - Sizes string `json:"sizes"` - } - - type manifestJSON struct { - Name string `json:"name"` - ShortName string `json:"short_name"` - StartURL string `json:"start_url"` - Icons []manifestIcon `json:"icons"` - } - - bytes, err := json.Marshal(&manifestJSON{ - Name: appName, - ShortName: appName, - StartURL: appURL, - Icons: []manifestIcon{ - { - Src: absoluteAssetURL + "/assets/img/logo.png", - Type: "image/png", - Sizes: "512x512", - }, - { - Src: absoluteAssetURL + "/assets/img/logo.svg", - Type: "image/svg+xml", - Sizes: "512x512", - }, - }, - }) - if err != nil { - log.Error("unable to marshal manifest JSON. Error: %v", err) - return make([]byte, 0) - } - - return bytes -} - // MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string { parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/")) @@ -311,9 +268,6 @@ func loadServerFrom(rootCfg ConfigProvider) { AbsoluteAssetURL = MakeAbsoluteAssetURL(AppURL, StaticURLPrefix) AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed) - manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL) - ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes) - var defaultLocalURL string switch Protocol { case HTTPUnix: diff --git a/modules/setting/session.go b/modules/setting/session.go index e9ff9bf0bc..1d88ad334c 100644 --- a/modules/setting/session.go +++ b/modules/setting/session.go @@ -34,7 +34,7 @@ var SessionConfig = struct { // SameSite declares if your cookie should be restricted to a first-party or same-site context. Valid strings are "none", "lax", "strict". Default is "lax" SameSite http.SameSite }{ - CookieName: "i_like_gitea", + CookieName: "session", Gclifetime: 86400, Maxlifetime: 86400, SameSite: http.SameSiteLaxMode, @@ -48,7 +48,7 @@ func loadSessionFrom(rootCfg ConfigProvider) { if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) { SessionConfig.ProviderConfig = path.Join(AppWorkPath, SessionConfig.ProviderConfig) } - SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea") + SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("session") SessionConfig.CookiePath = AppSubURL if SessionConfig.CookiePath == "" { SessionConfig.CookiePath = "/" diff --git a/modules/setting/setting.go b/modules/setting/setting.go index 9644d9b83b..bd3dec1f84 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -113,13 +113,14 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error { // WARNING: don't change the sequence except you know what you are doing. loadRunModeFrom(cfg) loadLogGlobalFrom(cfg) + loadPWAFrom(cfg) loadServerFrom(cfg) loadSSHFrom(cfg) mustCurrentRunUserMatch(cfg) // it depends on the SSH config, only non-builtin SSH server requires this check - loadOAuth2From(cfg) loadSecurityFrom(cfg) + loadOAuth2From(cfg) if err := loadAttachmentFrom(cfg); err != nil { return err } @@ -166,7 +167,7 @@ func loadRunModeFrom(rootCfg ConfigProvider) { // The following is a purposefully undocumented option. Please do not run Forgejo as root. It will only cause future headaches. // Please don't use root as a bandaid to "fix" something that is broken, instead the broken thing should instead be fixed properly. unsafeAllowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") - unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() + unsafeAllowRunAsRoot = unsafeAllowRunAsRoot || optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).ValueOrDefault(false) RunMode = os.Getenv("GITEA_RUN_MODE") if RunMode == "" { RunMode = rootSec.Key("RUN_MODE").MustString("prod") @@ -225,6 +226,7 @@ func LoadSettings() { loadProjectFrom(CfgProvider) loadMimeTypeMapFrom(CfgProvider) loadF3From(CfgProvider) + loadAuthorizedIntegrationFrom(CfgProvider) } // LoadSettingsForInstall initializes the settings for install diff --git a/modules/setting/setting_test.go b/modules/setting/setting_test.go index 1fef9e068a..9867b94aaf 100644 --- a/modules/setting/setting_test.go +++ b/modules/setting/setting_test.go @@ -8,6 +8,7 @@ import ( "testing" "forgejo.org/modules/json" + "forgejo.org/modules/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,10 +30,20 @@ func TestMakeAbsoluteAssetURL(t *testing.T) { } func TestMakeManifestData(t *testing.T) { - jsonBytes := MakeManifestData(`Example App '\"`, "https://example.com", "https://example.com/foo/bar") + jsonBytes, err := GetManifestJSON() + require.NoError(t, err) assert.True(t, json.Valid(jsonBytes)) } +func TestMakeManifestDataStandalone(t *testing.T) { + defer test.MockVariableValue(&PWA.Standalone, true)() + + jsonBytes, err := GetManifestJSON() + require.NoError(t, err) + assert.True(t, json.Valid(jsonBytes)) + assert.Contains(t, string(jsonBytes), `"standalone"`) +} + func TestLoadServiceDomainListsForFederation(t *testing.T) { oldAppURL := AppURL oldFederation := Federation diff --git a/modules/setting/storage.go b/modules/setting/storage.go index 532842064c..93958219a8 100644 --- a/modules/setting/storage.go +++ b/modules/setting/storage.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "strings" ) @@ -27,12 +28,7 @@ var storageTypes = []StorageType{ // IsValidStorageType returns true if the given storage type is valid func IsValidStorageType(storageType StorageType) bool { - for _, t := range storageTypes { - if t == storageType { - return true - } - } - return false + return slices.Contains(storageTypes, storageType) } // MinioStorageConfig represents the configuration for a minio storage @@ -136,11 +132,11 @@ func getStorageSectionByType(rootCfg ConfigProvider, typ string) (ConfigSection, targetType := targetSec.Key("STORAGE_TYPE").String() if targetType == "" { if !IsValidStorageType(StorageType(typ)) { - return nil, 0, fmt.Errorf("unknow storage type %q", typ) + return nil, 0, fmt.Errorf("unknown storage type %q", typ) } targetSec.Key("STORAGE_TYPE").SetValue(typ) } else if !IsValidStorageType(StorageType(targetType)) { - return nil, 0, fmt.Errorf("unknow storage type %q for section storage.%v", targetType, typ) + return nil, 0, fmt.Errorf("unknown storage type %q for section storage.%v", targetType, typ) } return targetSec, targetSecIsTyp, nil @@ -166,7 +162,7 @@ func getStorageTargetSection(rootCfg ConfigProvider, name, typ string, sec Confi } } - // check stoarge name thirdly + // check storage name thirdly targetSec, _ := rootCfg.GetSection(storageSectionName + "." + name) if targetSec != nil { targetType := targetSec.Key("STORAGE_TYPE").String() diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 61378e0596..abe902dfc6 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -55,10 +55,12 @@ var UI = struct { } `ini:"ui.csv"` Admin struct { - UserPagingNum int - RepoPagingNum int - NoticePagingNum int - OrgPagingNum int + UserPagingNum int + RepoPagingNum int + NoticePagingNum int + OrgPagingNum int + FederationHostPagingNum int + FederationUserPagingNum int } `ini:"ui.admin"` User struct { RepoPagingNum int @@ -114,15 +116,19 @@ var UI = struct { MaxRows: 2500, }, Admin: struct { - UserPagingNum int - RepoPagingNum int - NoticePagingNum int - OrgPagingNum int + UserPagingNum int + RepoPagingNum int + NoticePagingNum int + OrgPagingNum int + FederationHostPagingNum int + FederationUserPagingNum int }{ - UserPagingNum: 50, - RepoPagingNum: 50, - NoticePagingNum: 25, - OrgPagingNum: 50, + UserPagingNum: 50, + RepoPagingNum: 50, + NoticePagingNum: 25, + OrgPagingNum: 50, + FederationHostPagingNum: 50, + FederationUserPagingNum: 50, }, User: struct { RepoPagingNum int diff --git a/modules/storage/helper_test.go b/modules/storage/helper_test.go index dd30c9b8ac..ae32700c09 100644 --- a/modules/storage/helper_test.go +++ b/modules/storage/helper_test.go @@ -40,7 +40,7 @@ func Test_discardStorage(t *testing.T) { { got, err := tt.URL("path", "name", nil) assert.Nil(t, got) - require.Errorf(t, err, string(tt)) + require.Error(t, err, string(tt)) } { err := tt.IterateObjects("", func(_ string, _ Object) error { return nil }) diff --git a/modules/storage/storage.go b/modules/storage/storage.go index db081e0768..fe9222060f 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -164,6 +164,7 @@ func initLFS() (err error) { LFS = DiscardStorage("LFS isn't enabled") return nil } + log.Info("Initialising LFS storage with type: %s", setting.LFS.Storage.Type) LFS, err = NewStorage(setting.LFS.Storage.Type, setting.LFS.Storage) return err diff --git a/modules/structs/action.go b/modules/structs/action.go index f47b228d75..82c530cdf9 100644 --- a/modules/structs/action.go +++ b/modules/structs/action.go @@ -10,8 +10,14 @@ import ( // ActionRunJob represents a job of a run // swagger:model type ActionRunJob struct { - // the action run job id + // Identifier of this job. ID int64 `json:"id"` + // Identifier of the workflow run this job belongs to. + RunID int64 `json:"run_id"` + // How many times the job has been attempted including the current attempt. + Attempt int64 `json:"attempt"` + // Opaque identifier that uniquely identifies a single attempt of a job. + Handle string `json:"handle"` // the repository id RepoID int64 `json:"repo_id"` // the owner id @@ -66,13 +72,13 @@ type ActionRun struct { // the current status of this run Status string `json:"status"` // when the action run was started - Started time.Time `json:"started,omitempty"` + Started time.Time `json:"started"` // when the action run was stopped - Stopped time.Time `json:"stopped,omitempty"` + Stopped time.Time `json:"stopped"` // when the action run was created - Created time.Time `json:"created,omitempty"` + Created time.Time `json:"created"` // when the action run was last updated - Updated time.Time `json:"updated,omitempty"` + Updated time.Time `json:"updated"` // how long the action run ran for Duration time.Duration `json:"duration,omitempty"` // the url of this action run @@ -84,3 +90,26 @@ type ListActionRunResponse struct { Entries []*ActionRun `json:"workflow_runs"` TotalCount int64 `json:"total_count"` } + +// ActionArtifact represents an artifact of a workflow run +// swagger:model +type ActionArtifact struct { + // the artifact's ID + ID int64 `json:"id"` + // the artifact's name + Name string `json:"name"` + // the total size of the artifact in bytes + SizeInBytes int64 `json:"size_in_bytes"` + // the URL to download the artifact zip archive + ArchiveDownloadURL string `json:"archive_download_url"` + // whether the artifact has expired + Expired bool `json:"expired"` + // the ID of the workflow run that produced this artifact + RunID int64 `json:"run_id"` + // swagger:strfmt date-time + CreatedAt time.Time `json:"created_at"` + // swagger:strfmt date-time + UpdatedAt time.Time `json:"updated_at"` + // swagger:strfmt date-time + ExpiresAt time.Time `json:"expires_at"` +} diff --git a/modules/structs/issue.go b/modules/structs/issue.go index 7b7397dc4b..37c71f5736 100644 --- a/modules/structs/issue.go +++ b/modules/structs/issue.go @@ -17,7 +17,7 @@ import ( type StateType string const ( - // StateOpen pr is opend + // StateOpen pr is opened StateOpen StateType = "open" // StateClosed pr is closed StateClosed StateType = "closed" @@ -204,7 +204,7 @@ func (l *IssueTemplateLabels) UnmarshalYAML(value *yaml.Node) error { if err != nil { return err } - for _, v := range strings.Split(str, ",") { + for v := range strings.SplitSeq(str, ",") { if v = strings.TrimSpace(v); v == "" { continue } diff --git a/modules/structs/issue_comment.go b/modules/structs/issue_comment.go index 9ecb4a1789..766442374d 100644 --- a/modules/structs/issue_comment.go +++ b/modules/structs/issue_comment.go @@ -9,33 +9,48 @@ import ( // Comment represents a comment on a commit or issue type Comment struct { - ID int64 `json:"id"` - HTMLURL string `json:"html_url"` - PRURL string `json:"pull_request_url"` - IssueURL string `json:"issue_url"` - Poster *User `json:"user"` - OriginalAuthor string `json:"original_author"` - OriginalAuthorID int64 `json:"original_author_id"` - Body string `json:"body"` - Attachments []*Attachment `json:"assets"` + // The identifier of the comment + ID int64 `json:"id"` + // The HTML URL of the comment + HTMLURL string `json:"html_url"` + // The HTML URL of the pull request if the comment is posted on a pull request, else empty string + PRURL string `json:"pull_request_url"` + // The HTML URL of the issue if the comment is posted on an issue, else empty string + IssueURL string `json:"issue_url"` + // The user that posted the comment if it was posted locally + Poster *User `json:"user"` + // The original author that posted the comment if it was not posted locally, else empty string + OriginalAuthor string `json:"original_author"` + // The ID of the original author that posted the comment if it was not posted locally, else 0 + OriginalAuthorID int64 `json:"original_author_id"` + // The body of the comment + Body string `json:"body"` + // The attachments to the comment + Attachments []*Attachment `json:"assets"` + // The time of the comment's creation // swagger:strfmt date-time Created time.Time `json:"created_at"` + // The time of the comment's update // swagger:strfmt date-time Updated time.Time `json:"updated_at"` } // CreateIssueCommentOption options for creating a comment on an issue type CreateIssueCommentOption struct { + // The body of the comment // required:true Body string `json:"body" binding:"Required"` + // The time of the comment's update, needs admin or repository owner permission // swagger:strfmt date-time Updated *time.Time `json:"updated_at"` } // EditIssueCommentOption options for editing a comment type EditIssueCommentOption struct { + // The body of the comment // required: true Body string `json:"body" binding:"Required"` + // The time of the comment's update, needs admin or repository owner permission // swagger:strfmt date-time Updated *time.Time `json:"updated_at"` } diff --git a/modules/structs/org_team.go b/modules/structs/org_team.go index a51dcf1d19..fd83a75739 100644 --- a/modules/structs/org_team.go +++ b/modules/structs/org_team.go @@ -17,7 +17,7 @@ type Team struct { // // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. Units []string `json:"units"` - // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"} + // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.pulls":"owner","repo.releases":"none","repo.wiki":"admin","repo.ext_wiki":"none","repo.projects":"none","repo.packages":"none","repo.actions":"none"} UnitsMap map[string]string `json:"units_map"` CanCreateOrgRepo bool `json:"can_create_org_repo"` } @@ -34,7 +34,7 @@ type CreateTeamOption struct { // // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. Units []string `json:"units"` - // example: {"repo.actions","repo.packages","repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"} + // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.pulls":"owner","repo.releases":"none","repo.wiki":"admin","repo.ext_wiki":"none","repo.projects":"none","repo.packages":"none","repo.actions":"none"} UnitsMap map[string]string `json:"units_map"` CanCreateOrgRepo bool `json:"can_create_org_repo"` } @@ -51,7 +51,7 @@ type EditTeamOption struct { // // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. Units []string `json:"units"` - // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.wiki":"admin","repo.pulls":"owner","repo.releases":"none","repo.projects":"none","repo.ext_wiki":"none"} + // example: {"repo.code":"read","repo.issues":"write","repo.ext_issues":"none","repo.pulls":"owner","repo.releases":"none","repo.wiki":"admin","repo.ext_wiki":"none","repo.projects":"none","repo.packages":"none","repo.actions":"none"} UnitsMap map[string]string `json:"units_map"` CanCreateOrgRepo *bool `json:"can_create_org_repo"` } diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 01cbf26f61..3fa43ce0cb 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -80,40 +80,45 @@ type Repository struct { // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time - Updated time.Time `json:"updated_at"` - ArchivedAt time.Time `json:"archived_at"` - Permissions *Permission `json:"permissions,omitempty"` - HasIssues bool `json:"has_issues"` - InternalTracker *InternalTracker `json:"internal_tracker,omitempty"` - ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"` - HasWiki bool `json:"has_wiki"` - ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` - WikiBranch string `json:"wiki_branch,omitempty"` - GloballyEditableWiki bool `json:"globally_editable_wiki"` - HasPullRequests bool `json:"has_pull_requests"` - HasProjects bool `json:"has_projects"` - HasReleases bool `json:"has_releases"` - HasPackages bool `json:"has_packages"` - HasActions bool `json:"has_actions"` - IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"` - AllowMerge bool `json:"allow_merge_commits"` - AllowRebase bool `json:"allow_rebase"` - AllowRebaseMerge bool `json:"allow_rebase_explicit"` - AllowSquash bool `json:"allow_squash_merge"` - AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"` - AllowRebaseUpdate bool `json:"allow_rebase_update"` - DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"` - DefaultMergeStyle string `json:"default_merge_style"` - DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"` - DefaultUpdateStyle string `json:"default_update_style"` - AvatarURL string `json:"avatar_url"` - Internal bool `json:"internal"` - MirrorInterval string `json:"mirror_interval"` + Updated time.Time `json:"updated_at"` + ArchivedAt time.Time `json:"archived_at"` + Permissions *Permission `json:"permissions,omitempty"` + HasIssues bool `json:"has_issues"` + InternalTracker *InternalTracker `json:"internal_tracker,omitempty"` + ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"` + // is the wiki enabled + HasWiki bool `json:"has_wiki"` + // have wiki pages ever been created + HasWikiContents bool `json:"has_wiki_contents"` + ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` + WikiBranch string `json:"wiki_branch,omitempty"` + WikiSSHURL string `json:"wiki_ssh_url"` + WikiCloneURL string `json:"wiki_clone_url"` + GloballyEditableWiki bool `json:"globally_editable_wiki"` + HasPullRequests bool `json:"has_pull_requests"` + HasProjects bool `json:"has_projects"` + HasReleases bool `json:"has_releases"` + HasPackages bool `json:"has_packages"` + HasActions bool `json:"has_actions"` + IgnoreWhitespaceConflicts bool `json:"ignore_whitespace_conflicts"` + AllowMerge bool `json:"allow_merge_commits"` + AllowRebase bool `json:"allow_rebase"` + AllowRebaseMerge bool `json:"allow_rebase_explicit"` + AllowSquash bool `json:"allow_squash_merge"` + AllowFastForwardOnly bool `json:"allow_fast_forward_only_merge"` + AllowRebaseUpdate bool `json:"allow_rebase_update"` + DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"` + DefaultMergeStyle string `json:"default_merge_style"` + DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"` + DefaultUpdateStyle string `json:"default_update_style"` + AvatarURL string `json:"avatar_url"` + Internal bool `json:"internal"` + MirrorInterval string `json:"mirror_interval"` // ObjectFormatName of the underlying git repository // enum: ["sha1", "sha256"] ObjectFormatName string `json:"object_format_name"` // swagger:strfmt date-time - MirrorUpdated time.Time `json:"mirror_updated,omitempty"` + MirrorUpdated time.Time `json:"mirror_updated"` RepoTransfer *RepoTransfer `json:"repo_transfer"` Topics []string `json:"topics"` } @@ -436,3 +441,12 @@ type UpdateRepoAvatarOption struct { // image must be base64 encoded Image string `json:"image" binding:"Required"` } + +type RepoTargetOption struct { + // Name of user or organisation that owns the repository + // required: true + Owner string `json:"owner"` + // Name of repository + // required: true + Name string `json:"name"` +} diff --git a/modules/structs/repo_actions.go b/modules/structs/repo_actions.go index b13f344738..324ea474fd 100644 --- a/modules/structs/repo_actions.go +++ b/modules/structs/repo_actions.go @@ -32,3 +32,54 @@ type ActionTaskResponse struct { Entries []*ActionTask `json:"workflow_runs"` TotalCount int64 `json:"total_count"` } + +type RunnerStatus int + +const ( + // RunnerStatusOffline signals that the runner is not connected to Forgejo. + RunnerStatusOffline RunnerStatus = iota + + // RunnerStatusIdle means that the runner is connected to Forgejo and waiting for jobs to run. + RunnerStatusIdle + + // RunnerStatusActive signifies that the runner is connected to Forgejo and running a job. + RunnerStatusActive +) + +var statusName = map[RunnerStatus]string{ + RunnerStatusOffline: "offline", + RunnerStatusIdle: "idle", + RunnerStatusActive: "active", +} + +func (status RunnerStatus) String() string { + return statusName[status] +} + +// ActionRunner represents a runner +// swagger:model +type ActionRunner struct { + // ID uniquely identifies this runner. + ID int64 `json:"id"` + // UUID uniquely identifies this runner. + UUID string `json:"uuid"` + // OwnerID is the identifier of the user or organization this runner belongs to. O if the runner is owned by a + // repository. + OwnerID int64 `json:"owner_id"` + // RepoID is the identifier of the repository this runner belongs to. 0 if the runner belongs to a user or + // organization. + RepoID int64 `json:"repo_id"` + // Name of the runner; not unique. + Name string `json:"name"` + // Status indicates whether this runner is offline, or active, for example. + // enum: ["offline", "idle", "active"] + Status string `json:"status"` + // Version is the self-reported version string of Forgejo Runner. + Version string `json:"version"` + // Labels is a list of labels attached to this runner. + Labels []string `json:"labels"` + // Description provides optional details about this runner. + Description string `json:"description"` + // Indicates if runner is ephemeral runner + Ephemeral bool `json:"ephemeral"` +} diff --git a/modules/structs/runner.go b/modules/structs/runner.go new file mode 100644 index 0000000000..5ac88f0488 --- /dev/null +++ b/modules/structs/runner.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package structs + +// RegisterRunnerOptions declares the accepted options for registering runners. +// swagger:model +type RegisterRunnerOptions struct { + // Name of the runner to register. The name of the runner does not have to be unique. + // + // required: true + Name string `json:"name" binding:"Required"` + + // Description of the runner to register. + // + // required: false + Description string `json:"description"` + + // Register as ephemeral runner https://forgejo.org/docs/latest/admin/actions/security/#ephemeral-runner + // + // required: false + Ephemeral bool `json:"ephemeral"` +} + +// RegisterRunnerResponse contains the details of the just registered runner. +// swagger:model +type RegisterRunnerResponse struct { + ID int64 `json:"id" binding:"Required"` + UUID string `json:"uuid" binding:"Required"` + Token string `json:"token" binding:"Required"` +} diff --git a/modules/structs/secret.go b/modules/structs/secret.go index a0673ca08c..acd5c36f06 100644 --- a/modules/structs/secret.go +++ b/modules/structs/secret.go @@ -14,10 +14,11 @@ type Secret struct { Created time.Time `json:"created_at"` } -// CreateOrUpdateSecretOption options when creating or updating secret +// CreateOrUpdateSecretOption defines the properties of the secret to create or update. // swagger:model type CreateOrUpdateSecretOption struct { - // Data of the secret to update + // Data of the secret. Special characters will be retained. Line endings will be normalized to LF to match the + // behaviour of browsers. Encode the data with Base64 if line endings should be retained. // // required: true Data string `json:"data" binding:"Required"` diff --git a/modules/structs/user.go b/modules/structs/user.go index 49e4c495cf..e0767071d0 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -34,9 +34,9 @@ type User struct { // Is the user an administrator IsAdmin bool `json:"is_admin"` // swagger:strfmt date-time - LastLogin time.Time `json:"last_login,omitempty"` + LastLogin time.Time `json:"last_login"` // swagger:strfmt date-time - Created time.Time `json:"created,omitempty"` + Created time.Time `json:"created"` // Is user restricted Restricted bool `json:"restricted"` // Is user active diff --git a/modules/structs/user_app.go b/modules/structs/user_app.go index 6592c1cd48..357f3aed0a 100644 --- a/modules/structs/user_app.go +++ b/modules/structs/user_app.go @@ -16,10 +16,12 @@ type AccessToken struct { Token string `json:"sha1"` TokenLastEight string `json:"token_last_eight"` Scopes []string `json:"scopes"` + // Indicates that an access token only has access to the specified repositories. Will be null if the access token + // is not limited to a set of specified repositories. + Repositories []*RepositoryMeta `json:"repositories"` } // AccessTokenList represents a list of API access token. -// swagger:response AccessTokenList type AccessTokenList []*AccessToken // CreateAccessTokenOption options when create access token @@ -29,6 +31,8 @@ type CreateAccessTokenOption struct { Name string `json:"name" binding:"Required"` // example: ["all", "read:activitypub","read:issue", "write:misc", "read:notification", "read:organization", "read:package", "read:repository", "read:user"] Scopes []string `json:"scopes"` + // If provided and not-empty, creates an access token with access only to specified repositories. + Repositories []*RepoTargetOption `json:"repositories"` } // CreateOAuth2ApplicationOptions holds options to create an oauth2 application diff --git a/modules/structs/user_gpgkey.go b/modules/structs/user_gpgkey.go index ff9b0aea1d..deae70de33 100644 --- a/modules/structs/user_gpgkey.go +++ b/modules/structs/user_gpgkey.go @@ -21,9 +21,9 @@ type GPGKey struct { CanCertify bool `json:"can_certify"` Verified bool `json:"verified"` // swagger:strfmt date-time - Created time.Time `json:"created_at,omitempty"` + Created time.Time `json:"created_at"` // swagger:strfmt date-time - Expires time.Time `json:"expires_at,omitempty"` + Expires time.Time `json:"expires_at"` } // GPGKeyEmail an email attached to a GPGKey diff --git a/modules/structs/user_key.go b/modules/structs/user_key.go index 08eed59a89..b92552b200 100644 --- a/modules/structs/user_key.go +++ b/modules/structs/user_key.go @@ -15,7 +15,7 @@ type PublicKey struct { Title string `json:"title,omitempty"` Fingerprint string `json:"fingerprint,omitempty"` // swagger:strfmt date-time - Created time.Time `json:"created_at,omitempty"` + Created time.Time `json:"created_at"` Owner *User `json:"user,omitempty"` ReadOnly bool `json:"read_only,omitempty"` KeyType string `json:"key_type,omitempty"` diff --git a/modules/structs/variable.go b/modules/structs/variable.go index cc846cf0ec..c88aad5138 100644 --- a/modules/structs/variable.go +++ b/modules/structs/variable.go @@ -3,21 +3,24 @@ package structs -// CreateVariableOption the option when creating variable +// CreateVariableOption defines the properties of the variable to create. // swagger:model type CreateVariableOption struct { - // Value of the variable to create + // Value of the variable to create. Special characters will be retained. Line endings will be normalized to LF to + // match the behaviour of browsers. Encode the data with Base64 if line endings should be retained. // // required: true Value string `json:"value" binding:"Required"` } -// UpdateVariableOption the option when updating variable +// UpdateVariableOption defines the properties of the variable to update. // swagger:model type UpdateVariableOption struct { - // New name for the variable. If the field is empty, the variable name won't be updated. + // New name for the variable. If the field is empty, the variable name won't be updated. Forgejo will convert it to + // uppercase. Name string `json:"name"` - // Value of the variable to update + // Value of the variable to update. Special characters will be retained. Line endings will be normalized to LF to + // match the behaviour of browsers. Encode the data with Base64 if line endings should be retained. // // required: true Value string `json:"value" binding:"Required"` diff --git a/modules/templates/eval/eval_test.go b/modules/templates/eval/eval_test.go index 3e68203638..6b13d14007 100644 --- a/modules/templates/eval/eval_test.go +++ b/modules/templates/eval/eval_test.go @@ -13,7 +13,7 @@ import ( ) func tokens(s string) (a []any) { - for _, v := range strings.Fields(s) { + for v := range strings.FieldsSeq(s) { a = append(a, v) } return a diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 848d4b4ad4..44f5da24cb 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -52,16 +52,17 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // html/template related functions - "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. - "Eval": Eval, - "SafeHTML": SafeHTML, - "HTMLFormat": HTMLFormat, - "HTMLEscape": HTMLEscape, - "QueryEscape": QueryEscape, - "JSEscape": JSEscapeSafe, - "SanitizeHTML": SanitizeHTML, - "URLJoin": util.URLJoin, - "DotEscape": DotEscape, + "dict": dict, // it's lowercase because this name has been widely used. Our other functions should have uppercase names. + "Eval": Eval, + "TrustHTML": TrustHTML, + "HTMLFormat": HTMLFormat, + "HTMLEscape": HTMLEscape, + "QueryEscape": QueryEscape, + "JSEscape": JSEscapeSafe, + "SanitizeHTML": SanitizeHTML, + "SanitizeHTMLStrict": SanitizeHTMLStrict, + "URLJoin": util.URLJoin, + "DotEscape": DotEscape, "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, @@ -229,6 +230,7 @@ func HTMLFormat(s string, rawArgs ...any) template.HTML { switch v := v.(type) { case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML: // for most basic types (including template.HTML which is safe), just do nothing and use it + break case string: args[i] = template.HTMLEscapeString(v) case fmt.Stringer: @@ -240,8 +242,8 @@ func HTMLFormat(s string, rawArgs ...any) template.HTML { return template.HTML(fmt.Sprintf(s, args...)) } -// SafeHTML render raw as HTML -func SafeHTML(s any) template.HTML { +// TrustHTML render raw as HTML +func TrustHTML(s any) template.HTML { switch v := s.(type) { case string: return template.HTML(v) @@ -256,6 +258,10 @@ func SanitizeHTML(s string) template.HTML { return template.HTML(markup.Sanitize(s)) } +func SanitizeHTMLStrict(s string) template.HTML { + return template.HTML(markup.SanitizeDescription(s)) +} + func HTMLEscape(s any) template.HTML { switch v := s.(type) { case string: diff --git a/modules/templates/htmlrenderer.go b/modules/templates/htmlrenderer.go index d60397df08..4290e1c29f 100644 --- a/modules/templates/htmlrenderer.go +++ b/modules/templates/htmlrenderer.go @@ -248,7 +248,7 @@ func extractErrorLine(code []byte, lineNum, posNum int, target string) string { b := bufio.NewReader(bytes.NewReader(code)) var line []byte var err error - for i := 0; i < lineNum; i++ { + for i := range lineNum { if line, err = b.ReadBytes('\n'); err != nil { if i == lineNum-1 && errors.Is(err, io.EOF) { err = nil diff --git a/modules/templates/scopedtmpl/scopedtmpl.go b/modules/templates/scopedtmpl/scopedtmpl.go index 41a8ca86e9..d9866b3513 100644 --- a/modules/templates/scopedtmpl/scopedtmpl.go +++ b/modules/templates/scopedtmpl/scopedtmpl.go @@ -7,6 +7,7 @@ import ( "fmt" "html/template" "io" + "maps" "reflect" "sync" texttemplate "text/template" @@ -40,9 +41,7 @@ func (t *ScopedTemplate) Funcs(funcMap template.FuncMap) { panic("cannot add new functions to frozen template set") } t.all.Funcs(funcMap) - for k, v := range funcMap { - t.parseFuncs[k] = v - } + maps.Copy(t.parseFuncs, funcMap) } func (t *ScopedTemplate) New(name string) *template.Template { @@ -159,9 +158,7 @@ func newScopedTemplateSet(all *template.Template, name string) (*scopedTemplateS textTmplPtr.muFuncs.Lock() ts.execFuncs = map[string]reflect.Value{} - for k, v := range textTmplPtr.execFuncs { - ts.execFuncs[k] = v - } + maps.Copy(ts.execFuncs, textTmplPtr.execFuncs) textTmplPtr.muFuncs.Unlock() var collectTemplates func(nodes []parse.Node) @@ -220,9 +217,7 @@ func (ts *scopedTemplateSet) newExecutor(funcMap map[string]any) TemplateExecuto tmpl := texttemplate.New("") tmplPtr := ptr[textTemplate](tmpl) tmplPtr.execFuncs = map[string]reflect.Value{} - for k, v := range ts.execFuncs { - tmplPtr.execFuncs[k] = v - } + maps.Copy(tmplPtr.execFuncs, ts.execFuncs) if funcMap != nil { tmpl.Funcs(funcMap) } diff --git a/modules/templates/util_date.go b/modules/templates/util_date.go index 839d2701ef..982a1b8050 100644 --- a/modules/templates/util_date.go +++ b/modules/templates/util_date.go @@ -75,6 +75,7 @@ func anyToTime(any any) (t time.Time, isZero bool) { switch v := any.(type) { case nil: // it is zero + break case *time.Time: if v != nil { t = *v diff --git a/modules/templates/util_misc.go b/modules/templates/util_misc.go index 60f918be47..8e1ef71f5d 100644 --- a/modules/templates/util_misc.go +++ b/modules/templates/util_misc.go @@ -17,12 +17,11 @@ import ( asymkey_model "forgejo.org/models/asymkey" repo_model "forgejo.org/models/repo" user_model "forgejo.org/models/user" - "forgejo.org/modules/git" - giturl "forgejo.org/modules/git/url" "forgejo.org/modules/json" "forgejo.org/modules/log" "forgejo.org/modules/repository" "forgejo.org/modules/svg" + mirror_service "forgejo.org/services/mirror" "github.com/editorconfig/editorconfig-core-go/v2" ) @@ -164,17 +163,11 @@ type remoteAddress struct { Password string } -func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteName string) remoteAddress { +func mirrorRemoteAddress(ctx context.Context, mirror *repo_model.Mirror) remoteAddress { ret := remoteAddress{} - remoteURL, err := git.GetRemoteAddress(ctx, m.RepoPath(), remoteName) + u, err := mirror_service.DecryptOrRecoverRemoteAddress(ctx, mirror) if err != nil { - log.Error("GetRemoteURL %v", err) - return ret - } - - u, err := giturl.Parse(remoteURL) - if err != nil { - log.Error("giturl.Parse %v", err) + log.Error("DecryptOrRecoverRemoteAddress %v", err) return ret } diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go index bec8d5f5e3..e1ad83b88d 100644 --- a/modules/templates/util_render.go +++ b/modules/templates/util_render.go @@ -162,8 +162,8 @@ func RenderLabel(ctx *Context, label *issues_model.Label) template.HTML { // Regular label labelColor := label.Color + hex.EncodeToString([]byte{GetLabelOpacityByte(label.IsArchived())}) - s := fmt.Sprintf("
%s
", - archivedCSSClass, textColor, labelColor, description, RenderEmoji(ctx, label.Name)) + s := fmt.Sprintf("
%s
", + archivedCSSClass, textColor, labelColor, description, description, RenderEmoji(ctx, label.Name)) return template.HTML(s) } @@ -200,11 +200,11 @@ func RenderLabel(ctx *Context, label *issues_model.Label) template.HTML { scopeColor := "#" + hex.EncodeToString(scopeBytes) itemColor := "#" + hex.EncodeToString(itemBytes) - s := fmt.Sprintf(""+ + s := fmt.Sprintf(""+ "
%s
"+ "
%s
"+ "
", - archivedCSSClass, description, + archivedCSSClass, description, description, textColor, scopeColor, scopeText, textColor, itemColor, itemText) return template.HTML(s) @@ -246,7 +246,8 @@ func RenderMarkdownToHtml(ctx context.Context, input string) template.HTML { //n } func RenderLabels(ctx *Context, labels []*issues_model.Label, repoLink string, isPull bool) template.HTML { - htmlCode := `` + var htmlCode strings.Builder + htmlCode.WriteString(``) for _, label := range labels { // Protect against nil value in labels - shouldn't happen but would cause a panic if so if label == nil { @@ -257,11 +258,11 @@ func RenderLabels(ctx *Context, labels []*issues_model.Label, repoLink string, i if isPull { issuesOrPull = "pulls" } - htmlCode += fmt.Sprintf("%s ", + fmt.Fprintf(&htmlCode, "%s ", repoLink, issuesOrPull, label.ID, RenderLabel(ctx, label)) } - htmlCode += "" - return template.HTML(htmlCode) + htmlCode.WriteString("") + return template.HTML(htmlCode.String()) } func RenderUser(ctx context.Context, user user_model.User) template.HTML { diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go index 3cfd572491..35412989e3 100644 --- a/modules/templates/util_render_test.go +++ b/modules/templates/util_render_test.go @@ -54,7 +54,7 @@ func TestApostrophesInMentions(t *testing.T) { assert.Equal(t, template.HTML("

@mention-user's comment

\n"), rendered) } -func TestNonExistantUserMention(t *testing.T) { +func TestNonExistentUserMention(t *testing.T) { rendered := RenderMarkdownToHtml(t.Context(), "@ThisUserDoesNotExist @mention-user") assert.Equal(t, template.HTML("

@ThisUserDoesNotExist @mention-user

\n"), rendered) } @@ -229,7 +229,8 @@ func TestRenderLabels(t *testing.T) { rendered := RenderLabels(ctx, []*issues_model.Label{label}, "user2/repo1", false) assert.Contains(t, rendered, "user2/repo1/issues?labels=1") assert.Contains(t, rendered, ">label1<") - assert.Contains(t, rendered, "title='First label'") + assert.Contains(t, rendered, "data-tooltip-content='First label'") + assert.Contains(t, rendered, "aria-description='First label'") rendered = RenderLabels(ctx, []*issues_model.Label{label}, "user2/repo1", true) assert.Contains(t, rendered, "user2/repo1/pulls?labels=1") assert.Contains(t, rendered, ">label1<") @@ -241,11 +242,11 @@ func TestRenderLabels(t *testing.T) { assert.Contains(t, rendered, "user2/repo1/issues?labels=11") assert.Contains(t, rendered, "> <script>malicious</script> <") assert.Contains(t, rendered, ">'?&<") - assert.Contains(t, rendered, "title='Malicious label ' <script>malicious</script>'") + assert.Contains(t, rendered, "data-tooltip-content='Malicious label ' <script>malicious</script>'") + assert.Contains(t, rendered, "aria-description='Malicious label ' <script>malicious</script>'") rendered = RenderLabels(ctx, []*issues_model.Label{labelArchived}, "user2/repo1", false) assert.Contains(t, rendered, "user2/repo1/issues?labels=12") assert.Contains(t, rendered, ">archived label<><") - assert.Contains(t, rendered, "title='repo.issues.archived_label_description'") } func TestRenderUser(t *testing.T) { diff --git a/modules/test/distant_federation_server_mock.go b/modules/test/distant_federation_server_mock.go index ea8a69e9b4..abbc82c196 100644 --- a/modules/test/distant_federation_server_mock.go +++ b/modules/test/distant_federation_server_mock.go @@ -4,29 +4,38 @@ package test import ( + "bytes" "fmt" "io" "net/http" "net/http/httptest" + "net/url" "strings" "testing" "forgejo.org/modules/util" + + ap "github.com/go-ap/activitypub" + "github.com/go-ap/jsonld" + "github.com/google/uuid" ) +type ApActorMock struct { + PrivKey string + PubKey string +} + type FederationServerMockPerson struct { ID int64 Name string PubKey string PrivKey string } + type FederationServerMockRepository struct { ID int64 } -type ApActorMock struct { - PrivKey string - PubKey string -} + type FederationServerMock struct { ApActor ApActorMock Persons []FederationServerMockPerson @@ -34,6 +43,35 @@ type FederationServerMock struct { LastPost string } +func NewApActorMock() ApActorMock { + priv, pub, _ := util.GenerateKeyPair(1024) + return ApActorMock{ + PrivKey: priv, + PubKey: pub, + } +} + +func (u *ApActorMock) KeyID(host string) string { + return fmt.Sprintf("%s/api/v1/activitypub/actor#main-key", host) +} + +func (u *ApActorMock) marshal(host string) string { + baseID := fmt.Sprintf("http://%s/api/v1/activitypub/actor", host) + + return fmt.Sprintf( + `{ "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"],`+ + `"id": "%[1]s",`+ + `"type": "Application",`+ + `"preferredUsername": "ghost",`+ + `"publicKey": {`+ + ` "id": "%[1]s#main-key",`+ + ` "owner": "%[1]s",`+ + ` "publicKeyPem": %[2]q }}`, + baseID, + u.PubKey, + ) +} + func NewFederationServerMockPerson(id int64, name string) FederationServerMockPerson { priv, pub, _ := util.GenerateKeyPair(3072) return FederationServerMockPerson{ @@ -48,24 +86,6 @@ func (p *FederationServerMockPerson) KeyID(host string) string { return fmt.Sprintf("%[1]v/api/v1/activitypub/user-id/%[2]v#main-key", host, p.ID) } -func NewFederationServerMockRepository(id int64) FederationServerMockRepository { - return FederationServerMockRepository{ - ID: id, - } -} - -func NewApActorMock() ApActorMock { - priv, pub, _ := util.GenerateKeyPair(1024) - return ApActorMock{ - PrivKey: priv, - PubKey: pub, - } -} - -func (u *ApActorMock) KeyID(host string) string { - return fmt.Sprintf("%[1]v/api/v1/activitypub/actor#main-key", host) -} - func (p FederationServerMockPerson) marshal(host string) string { return fmt.Sprintf(`{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1"],`+ `"id":"http://%[1]v/api/v1/activitypub/user-id/%[2]v",`+ @@ -80,6 +100,12 @@ func (p FederationServerMockPerson) marshal(host string) string { `"publicKeyPem":%[4]q}}`, host, p.ID, p.Name, p.PubKey) } +func NewFederationServerMockRepository(id int64) FederationServerMockRepository { + return FederationServerMockRepository{ + ID: id, + } +} + func NewFederationServerMock() *FederationServerMock { return &FederationServerMock{ ApActor: NewApActorMock(), @@ -103,6 +129,26 @@ func (mock *FederationServerMock) recordLastPost(t *testing.T, req *http.Request mock.LastPost = strings.ReplaceAll(buf.String(), req.Host, "DISTANT_FEDERATION_HOST") } +func (mock *FederationServerMock) FollowActorUnsigned(host string, localID int64, uri, inboxURL url.URL) error { + apID := fmt.Sprintf("%s/api/v1/activitypub/user-id/%d", host, localID) + + activity := ap.Follow{} + activity.Type = ap.FollowType + activity.ID = ap.IRI(apID + "/follows/" + uuid.New().String()) + activity.Actor = ap.IRI(apID) + activity.Object = ap.IRI(uri.String()) + + payload, err := jsonld.WithContext(jsonld.IRI(ap.ActivityBaseURI)).Marshal(activity) + if err != nil { + return err + } + + reader := bytes.NewReader(payload) + _, err = http.Post(inboxURL.String(), "application/activity+json", reader) + + return err +} + func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server { federatedRoutes := http.NewServeMux() @@ -122,6 +168,10 @@ func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server { }) for _, person := range mock.Persons { + federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/user-id/alias%v", person.ID), + func(res http.ResponseWriter, req *http.Request) { + fmt.Fprint(res, person.marshal(req.Host)) + }) federatedRoutes.HandleFunc(fmt.Sprintf("/api/v1/activitypub/user-id/%v", person.ID), func(res http.ResponseWriter, req *http.Request) { // curl -H "Accept: application/json" https://federated-repo.prod.meissa.de/api/v1/activitypub/user-id/2 @@ -139,10 +189,18 @@ func (mock *FederationServerMock) DistantServer(t *testing.T) *httptest.Server { mock.recordLastPost(t, req) }) } + + federatedRoutes.HandleFunc("GET /api/v1/activitypub/actor", + func(res http.ResponseWriter, req *http.Request) { + fmt.Fprint(res, mock.ApActor.marshal(req.Host)) + }) + federatedRoutes.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) { t.Errorf("Unhandled %v request: %q", req.Method, req.URL.EscapedPath()) }) + federatedSrv := httptest.NewServer(federatedRoutes) + return federatedSrv } diff --git a/modules/test/logchecker.go b/modules/test/logchecker.go index 8e8fc32216..af82ff0461 100644 --- a/modules/test/logchecker.go +++ b/modules/test/logchecker.go @@ -53,11 +53,11 @@ func (lc *LogChecker) checkLogEvent(event *log.EventFormatted) { } } -var checkerIndex int64 +var checkerIndex atomic.Int64 func NewLogChecker(namePrefix string, level log.Level) (logChecker *LogChecker, cancel func()) { logger := log.GetManager().GetLogger(namePrefix) - newCheckerIndex := atomic.AddInt64(&checkerIndex, 1) + newCheckerIndex := checkerIndex.Add(1) writerName := namePrefix + "-" + fmt.Sprint(newCheckerIndex) lc := &LogChecker{} diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index 772ae47e71..54f0462703 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -367,7 +367,7 @@ var ignoredErrorMessage = []string{ // Test_CmdForgejo_Actions `DB: No dedicated replica host defined; falling back to primary DSN for replica connections`, - // TestDevtestErrorpages + // TestDemoErrorPages `ErrorPage() [E] Example error: Example error`, } @@ -501,7 +501,7 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() { // Printf takes a format and args and prints the string to os.Stdout func Printf(format string, args ...any) { if log.CanColorStdout { - for i := 0; i < len(args); i++ { + for i := range args { args[i] = log.NewColoredValue(args[i]) } } diff --git a/modules/translation/i18n/i18n_ini_test.go b/modules/translation/i18n/i18n_ini_test.go new file mode 100644 index 0000000000..64c5d167c0 --- /dev/null +++ b/modules/translation/i18n/i18n_ini_test.go @@ -0,0 +1,206 @@ +// Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package i18n + +import ( + "html/template" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLocaleStoreINI(t *testing.T) { + testData1 := []byte(` +.dot.name = Dot Name +fmt = %[1]s %[2]s + +[section] +sub = Sub String +mixed = test value; %s +`) + + testData2 := []byte(` +fmt = %[2]s %[1]s + +[section] +sub = Changed Sub String +`) + + ls := NewLocaleStore() + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, UsedPluralFormsEnglish, testData1, nil)) + require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, UsedPluralFormsMock, testData2, nil)) + ls.SetDefaultLang("lang1") + + lang1, _ := ls.Locale("lang1") + lang2, _ := ls.Locale("lang2") + + result := lang1.TrString("fmt", "a", "b") + assert.Equal(t, "a b", result) + + result = lang2.TrString("fmt", "a", "b") + assert.Equal(t, "b a", result) + + result = lang1.TrString("section.sub") + assert.Equal(t, "Sub String", result) + + result = lang2.TrString("section.sub") + assert.Equal(t, "Changed Sub String", result) + + langNone, _ := ls.Locale("none") + result = langNone.TrString(".dot.name") + assert.Equal(t, "Dot Name", result) + + result2 := lang2.TrHTML("section.mixed", "a&b") + assert.EqualValues(t, `test value; a&b`, result2) + + langs, descs := ls.ListLangNameDesc() + assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs) + assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs) + + found := lang1.HasKey("no-such") + assert.False(t, found) + assert.Equal(t, "no-such", lang1.TrString("no-such")) + require.NoError(t, ls.Close()) +} + +func TestLocaleStoreMoreSource(t *testing.T) { + testData1 := []byte(` +a=11 +b=12 +`) + + testData2 := []byte(` +b=21 +c=22 +`) + + ls := NewLocaleStore() + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, testData1, testData2)) + lang1, _ := ls.Locale("lang1") + assert.Equal(t, "11", lang1.TrString("a")) + assert.Equal(t, "21", lang1.TrString("b")) + assert.Equal(t, "22", lang1.TrString("c")) +} + +type stringerPointerReceiver struct { + s string +} + +func (s *stringerPointerReceiver) String() string { + return s.s +} + +type stringerStructReceiver struct { + s string +} + +func (s stringerStructReceiver) String() string { + return s.s +} + +type errorStructReceiver struct { + s string +} + +func (e errorStructReceiver) Error() string { + return e.s +} + +type errorPointerReceiver struct { + s string +} + +func (e *errorPointerReceiver) Error() string { + return e.s +} + +func TestLocaleWithTemplate(t *testing.T) { + ls := NewLocaleStore() + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, []byte(`key=%s`), nil)) + lang1, _ := ls.Locale("lang1") + + tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML}) + tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`)) + + cases := []struct { + in any + want string + }{ + {"", "<str>"}, + {[]byte(""), "[60 98 121 116 101 115 62]"}, + {template.HTML(""), ""}, + {stringerPointerReceiver{""}, "{<stringerPointerReceiver>}"}, + {&stringerPointerReceiver{""}, "<stringerPointerReceiver ptr>"}, + {stringerStructReceiver{""}, "<stringerStructReceiver>"}, + {&stringerStructReceiver{""}, "<stringerStructReceiver ptr>"}, + {errorStructReceiver{""}, "<errorStructReceiver>"}, + {&errorStructReceiver{""}, "<errorStructReceiver ptr>"}, + {errorPointerReceiver{""}, "{<errorPointerReceiver>}"}, + {&errorPointerReceiver{""}, "<errorPointerReceiver ptr>"}, + } + + buf := &strings.Builder{} + for _, c := range cases { + buf.Reset() + require.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in})) + assert.Equal(t, c.want, buf.String()) + } +} + +func TestLocaleStoreQuirks(t *testing.T) { + const nl = "\n" + q := func(q1, s string, q2 ...string) string { + return q1 + s + strings.Join(q2, "") + } + testDataList := []struct { + in string + out string + hint string + }{ + {` xx`, `xx`, "simple, no quote"}, + {`" xx"`, ` xx`, "simple, double-quote"}, + {`' xx'`, ` xx`, "simple, single-quote"}, + {"` xx`", ` xx`, "simple, back-quote"}, + + {`x\"y`, `x\"y`, "no unescape, simple"}, + {q(`"`, `x\"y`, `"`), `"x\"y"`, "unescape, double-quote"}, + {q(`'`, `x\"y`, `'`), `x\"y`, "no unescape, single-quote"}, + {q("`", `x\"y`, "`"), `x\"y`, "no unescape, back-quote"}, + + {q(`"`, `x\"y`) + nl + "b=", `"x\"y`, "half open, double-quote"}, + {q(`'`, `x\"y`) + nl + "b=", `'x\"y`, "half open, single-quote"}, + {q("`", `x\"y`) + nl + "b=`", `x\"y` + nl + "b=", "half open, back-quote, multi-line"}, + + {`x ; y`, `x ; y`, "inline comment (;)"}, + {`x # y`, `x # y`, "inline comment (#)"}, + {`x \; y`, `x ; y`, `inline comment (\;)`}, + {`x \# y`, `x # y`, `inline comment (\#)`}, + } + + for _, testData := range testDataList { + ls := NewLocaleStore() + err := ls.AddLocaleByIni("lang1", "Lang1", nil, nil, []byte("a="+testData.in), nil) + lang1, _ := ls.Locale("lang1") + require.NoError(t, err, testData.hint) + assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint) + require.NoError(t, ls.Close()) + } + + // TODO: Crowdin needs the strings to be quoted correctly and doesn't like incomplete quotes + // and Crowdin always outputs quoted strings if there are quotes in the strings. + // So, Gitea's `key="quoted" unquoted` content shouldn't be used on Crowdin directly, + // it should be converted to `key="\"quoted\" unquoted"` first. + // TODO: We can not use UnescapeValueDoubleQuotes=true, because there are a lot of back-quotes in en-US.ini, + // then Crowdin will output: + // > key = "`x \" y`" + // Then Gitea will read a string with back-quotes, which is incorrect. + // TODO: Crowdin might generate multi-line strings, quoted by double-quote, it's not supported by LocaleStore + // LocaleStore uses back-quote for multi-line strings, it's not supported by Crowdin. + // TODO: Crowdin doesn't support back-quote as string quoter, it mainly uses double-quote + // so, the following line will be parsed as: value="`first", comment="second`" on Crowdin + // > a = `first; second` +} diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go index ac086d75d9..8dfdb36127 100644 --- a/modules/translation/i18n/i18n_test.go +++ b/modules/translation/i18n/i18n_test.go @@ -1,10 +1,9 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT +// Copyright 2024-2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later package i18n import ( - "html/template" "strings" "testing" @@ -37,24 +36,7 @@ var ( UsedPluralFormsMock = []PluralFormIndex{PluralFormZero, PluralFormOne, PluralFormFew, PluralFormOther} ) -func TestLocaleStore(t *testing.T) { - testData1 := []byte(` -.dot.name = Dot Name -fmt = %[1]s %[2]s - -[section] -sub = Sub String -mixed = test value; %s -`) - - testData2 := []byte(` -fmt = %[2]s %[1]s - -[section] -sub = Changed Sub String -commits = fallback value for commits -`) - +func TestLocaleStoreJSON(t *testing.T) { testDataJSON2 := []byte(` { "section.json": "the JSON is %s", @@ -90,35 +72,18 @@ commits = fallback value for commits `) ls := NewLocaleStore() - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, UsedPluralFormsEnglish, testData1, nil)) - require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, UsedPluralFormsMock, testData2, nil)) + + // Currently LocaleStore has to be first populated with langcodes via AddLocaleByIni + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, UsedPluralFormsEnglish, []byte(""), nil)) + require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, UsedPluralFormsMock, []byte(""), nil)) + require.NoError(t, ls.AddToLocaleFromJSON("lang1", testDataJSON1)) require.NoError(t, ls.AddToLocaleFromJSON("lang2", testDataJSON2)) - ls.SetDefaultLang("lang1") - lang1, _ := ls.Locale("lang1") + ls.SetDefaultLang("lang1") lang2, _ := ls.Locale("lang2") - result := lang1.TrString("fmt", "a", "b") - assert.Equal(t, "a b", result) - - result = lang2.TrString("fmt", "a", "b") - assert.Equal(t, "b a", result) - - result = lang1.TrString("section.sub") - assert.Equal(t, "Sub String", result) - - result = lang2.TrString("section.sub") - assert.Equal(t, "Changed Sub String", result) - - langNone, _ := ls.Locale("none") - result = langNone.TrString(".dot.name") - assert.Equal(t, "Dot Name", result) - - result2 := lang2.TrHTML("section.mixed", "a&b") - assert.EqualValues(t, `test value; a&b`, result2) - - result = lang2.TrString("section.json", "valid") + result := lang2.TrString("section.json", "valid") assert.Equal(t, "the JSON is valid", result) result = lang2.TrString("nested.outer.inner.json") @@ -127,7 +92,7 @@ commits = fallback value for commits result = lang2.TrString("section.commits") assert.Equal(t, "lots of %d commits", result) - result2 = lang2.TrPluralString(1, "section.commits", 1) + result2 := lang2.TrPluralString(1, "section.commits", 1) assert.EqualValues(t, "one 1 commit", result2) result2 = lang2.TrPluralString(3, "section.commits", 3) @@ -156,159 +121,34 @@ commits = fallback value for commits result2 = lang2.TrPluralString(7, "section.incomplete", 7) assert.EqualValues(t, "[untranslated] some 7 objects", result2) +} - langs, descs := ls.ListLangNameDesc() - assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs) - assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs) +func TestMissingTranslationHandling(t *testing.T) { + ls := NewLocaleStore() - // Test HasKey for JSON - found := lang2.HasKey("section.json") + // Currently LocaleStore has to be first populated with langcodes via AddLocaleByIni + require.NoError(t, ls.AddLocaleByIni("en-US", "English", MockPluralRuleEnglish, UsedPluralFormsEnglish, []byte(""), nil)) + require.NoError(t, ls.AddLocaleByIni("fun", "Funlang", MockPluralRule, UsedPluralFormsMock, []byte(""), nil)) + + require.NoError(t, ls.AddToLocaleFromJSON("en-US", []byte(` +{ + "incorrect_root_url": "This Forgejo instance...", + "meta.last_line": "Hi there!" +}`))) + require.NoError(t, ls.AddToLocaleFromJSON("fun", []byte(` +{ + "meta.last_line": "This language only has one line that is never used by the UI. It will never have a translation for incorrect_root_url" +}`))) + + ls.SetDefaultLang("en-US") + + // Get "fun" locale, make sure it's available + funLocale, found := ls.Locale("fun") assert.True(t, found) - // Test HasKey for INI - found = lang2.HasKey("section.sub") - assert.True(t, found) + // Get translation for a string that this locale doesn't have + s := funLocale.TrString("incorrect_root_url") - found = lang1.HasKey("no-such") - assert.False(t, found) - assert.Equal(t, "no-such", lang1.TrString("no-such")) - require.NoError(t, ls.Close()) -} - -func TestLocaleStoreMoreSource(t *testing.T) { - testData1 := []byte(` -a=11 -b=12 -`) - - testData2 := []byte(` -b=21 -c=22 -`) - - ls := NewLocaleStore() - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, testData1, testData2)) - lang1, _ := ls.Locale("lang1") - assert.Equal(t, "11", lang1.TrString("a")) - assert.Equal(t, "21", lang1.TrString("b")) - assert.Equal(t, "22", lang1.TrString("c")) -} - -type stringerPointerReceiver struct { - s string -} - -func (s *stringerPointerReceiver) String() string { - return s.s -} - -type stringerStructReceiver struct { - s string -} - -func (s stringerStructReceiver) String() string { - return s.s -} - -type errorStructReceiver struct { - s string -} - -func (e errorStructReceiver) Error() string { - return e.s -} - -type errorPointerReceiver struct { - s string -} - -func (e *errorPointerReceiver) Error() string { - return e.s -} - -func TestLocaleWithTemplate(t *testing.T) { - ls := NewLocaleStore() - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, UsedPluralFormsMock, []byte(`key=%s`), nil)) - lang1, _ := ls.Locale("lang1") - - tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML}) - tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`)) - - cases := []struct { - in any - want string - }{ - {"", "<str>"}, - {[]byte(""), "[60 98 121 116 101 115 62]"}, - {template.HTML(""), ""}, - {stringerPointerReceiver{""}, "{<stringerPointerReceiver>}"}, - {&stringerPointerReceiver{""}, "<stringerPointerReceiver ptr>"}, - {stringerStructReceiver{""}, "<stringerStructReceiver>"}, - {&stringerStructReceiver{""}, "<stringerStructReceiver ptr>"}, - {errorStructReceiver{""}, "<errorStructReceiver>"}, - {&errorStructReceiver{""}, "<errorStructReceiver ptr>"}, - {errorPointerReceiver{""}, "{<errorPointerReceiver>}"}, - {&errorPointerReceiver{""}, "<errorPointerReceiver ptr>"}, - } - - buf := &strings.Builder{} - for _, c := range cases { - buf.Reset() - require.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in})) - assert.Equal(t, c.want, buf.String()) - } -} - -func TestLocaleStoreQuirks(t *testing.T) { - const nl = "\n" - q := func(q1, s string, q2 ...string) string { - return q1 + s + strings.Join(q2, "") - } - testDataList := []struct { - in string - out string - hint string - }{ - {` xx`, `xx`, "simple, no quote"}, - {`" xx"`, ` xx`, "simple, double-quote"}, - {`' xx'`, ` xx`, "simple, single-quote"}, - {"` xx`", ` xx`, "simple, back-quote"}, - - {`x\"y`, `x\"y`, "no unescape, simple"}, - {q(`"`, `x\"y`, `"`), `"x\"y"`, "unescape, double-quote"}, - {q(`'`, `x\"y`, `'`), `x\"y`, "no unescape, single-quote"}, - {q("`", `x\"y`, "`"), `x\"y`, "no unescape, back-quote"}, - - {q(`"`, `x\"y`) + nl + "b=", `"x\"y`, "half open, double-quote"}, - {q(`'`, `x\"y`) + nl + "b=", `'x\"y`, "half open, single-quote"}, - {q("`", `x\"y`) + nl + "b=`", `x\"y` + nl + "b=", "half open, back-quote, multi-line"}, - - {`x ; y`, `x ; y`, "inline comment (;)"}, - {`x # y`, `x # y`, "inline comment (#)"}, - {`x \; y`, `x ; y`, `inline comment (\;)`}, - {`x \# y`, `x # y`, `inline comment (\#)`}, - } - - for _, testData := range testDataList { - ls := NewLocaleStore() - err := ls.AddLocaleByIni("lang1", "Lang1", nil, nil, []byte("a="+testData.in), nil) - lang1, _ := ls.Locale("lang1") - require.NoError(t, err, testData.hint) - assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint) - require.NoError(t, ls.Close()) - } - - // TODO: Crowdin needs the strings to be quoted correctly and doesn't like incomplete quotes - // and Crowdin always outputs quoted strings if there are quotes in the strings. - // So, Gitea's `key="quoted" unquoted` content shouldn't be used on Crowdin directly, - // it should be converted to `key="\"quoted\" unquoted"` first. - // TODO: We can not use UnescapeValueDoubleQuotes=true, because there are a lot of back-quotes in en-US.ini, - // then Crowdin will output: - // > key = "`x \" y`" - // Then Gitea will read a string with back-quotes, which is incorrect. - // TODO: Crowdin might generate multi-line strings, quoted by double-quote, it's not supported by LocaleStore - // LocaleStore uses back-quote for multi-line strings, it's not supported by Crowdin. - // TODO: Crowdin doesn't support back-quote as string quoter, it mainly uses double-quote - // so, the following line will be parsed as: value="`first", comment="second`" on Crowdin - // > a = `first; second` + // Verify fallback to English + assert.True(t, strings.HasPrefix(s, "This Forgejo instance...")) } diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go index fc27c75d13..d0f5841411 100644 --- a/modules/translation/i18n/localestore.go +++ b/modules/translation/i18n/localestore.go @@ -119,7 +119,7 @@ func (l *locale) LookupNewStyleMessage(trKey string) string { return "" } -func (l *locale) LookupPluralByCount(trKey string, count any) string { +func (l *locale) LookupPluralByCount(trKey string, count any, isDefaultLang bool) string { n, err := util.ToInt64(count) if err != nil { log.Error("Invalid plural count '%s'", count) @@ -127,10 +127,10 @@ func (l *locale) LookupPluralByCount(trKey string, count any) string { } pluralForm := l.pluralRule(n) - return l.LookupPluralByForm(trKey, pluralForm) + return l.LookupPluralByForm(trKey, pluralForm, isDefaultLang) } -func (l *locale) LookupPluralByForm(trKey string, pluralForm PluralFormIndex) string { +func (l *locale) LookupPluralByForm(trKey string, pluralForm PluralFormIndex, isDefaultLang bool) string { suffix := "" switch pluralForm { case PluralFormZero: @@ -145,6 +145,7 @@ func (l *locale) LookupPluralByForm(trKey string, pluralForm PluralFormIndex) st suffix = PluralFormSeparator + "many" case PluralFormOther: // No suffix for the "other" string. + break default: log.Error("Invalid plural form index %d", pluralForm) return "" @@ -154,7 +155,14 @@ func (l *locale) LookupPluralByForm(trKey string, pluralForm PluralFormIndex) st return result } - log.Error("Missing translation for plural form %s", suffix) + // Severify depends on the lang. A missing string in default lang will affect + // all translations, while community translations may just be incomplete + logFunc := log.Debug + if isDefaultLang { + logFunc = log.Error + } + + logFunc("Missing translation for key `%[1]s`, plural form `%[2]s`", trKey, suffix) return "" } @@ -248,6 +256,7 @@ func PrepareArgsForHTML(trArgs ...any) []any { switch v := v.(type) { case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML: // for most basic types (including template.HTML which is safe), just do nothing and use it + break case string: args[i] = template.HTMLEscapeString(v) case fmt.Stringer: @@ -264,11 +273,11 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { } func (l *locale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { - message := l.LookupPluralByCount(trKey, count) + message := l.LookupPluralByCount(trKey, count, false) if message == "" { if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok { - message = defaultLang.LookupPluralByCount(trKey, count) + message = defaultLang.LookupPluralByCount(trKey, count, true) } if message == "" { message = trKey @@ -292,7 +301,7 @@ func (l *locale) TrPluralStringAllForms(trKey string) ([]string, []string) { allPresent := true for i, form := range l.usedPluralForms { - result[i] = l.LookupPluralByForm(trKey, form) + result[i] = l.LookupPluralByForm(trKey, form, false) if result[i] == "" { allPresent = false } @@ -302,7 +311,7 @@ func (l *locale) TrPluralStringAllForms(trKey string) ([]string, []string) { if hasDefaultLang { fallback = make([]string, len(defaultLang.usedPluralForms)) for i, form := range defaultLang.usedPluralForms { - fallback[i] = defaultLang.LookupPluralByForm(trKey, form) + fallback[i] = defaultLang.LookupPluralByForm(trKey, form, true) } } else { log.Error("Plural set for '%s' is incomplete and no fallback language is set.", trKey) diff --git a/modules/translation/localeiter/utils.go b/modules/translation/localeiter/utils.go index de398258e2..cda20df1bb 100644 --- a/modules/translation/localeiter/utils.go +++ b/modules/translation/localeiter/utils.go @@ -56,6 +56,7 @@ func iterateMessagesNextInner(onMsgid func(string, string, string) error, data m pluralSuffix = key case "other": // do nothing + break default: realKey = fullKey } @@ -71,6 +72,7 @@ func iterateMessagesNextInner(onMsgid func(string, string, string) error, data m case nil: // do nothing + break default: return fmt.Errorf("Unexpected JSON type: %s - %T", fullKey, value) diff --git a/modules/translation/plural_rules_test.go b/modules/translation/plural_rules_test.go new file mode 100644 index 0000000000..d3a9a15905 --- /dev/null +++ b/modules/translation/plural_rules_test.go @@ -0,0 +1,120 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package translation + +import ( + "testing" + + "forgejo.org/modules/translation/i18n" + + "github.com/stretchr/testify/assert" +) + +func TestGetPluralRule(t *testing.T) { + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en")) + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en-US")) + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en_UK")) + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("nds")) + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("de-DE")) + + assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("zh")) + assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("ja")) + + assert.Equal(t, PluralRuleBengali, GetPluralRuleImpl("bn")) + + assert.Equal(t, PluralRuleIcelandic, GetPluralRuleImpl("is")) + + assert.Equal(t, PluralRuleFilipino, GetPluralRuleImpl("fil")) + + assert.Equal(t, PluralRuleCzech, GetPluralRuleImpl("cs")) + + assert.Equal(t, PluralRuleRussian, GetPluralRuleImpl("ru")) + + assert.Equal(t, PluralRulePolish, GetPluralRuleImpl("pl")) + + assert.Equal(t, PluralRuleLatvian, GetPluralRuleImpl("lv")) + + assert.Equal(t, PluralRuleLithuanian, GetPluralRuleImpl("lt")) + + assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("fr")) + + assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("ca")) + + assert.Equal(t, PluralRuleSlovenian, GetPluralRuleImpl("sl")) + + assert.Equal(t, PluralRuleArabic, GetPluralRuleImpl("ar")) + + assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("pt-PT")) + assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("pt-BR")) + + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("invalid")) +} + +func TestApplyPluralRule(t *testing.T) { + testCases := []struct { + expect i18n.PluralFormIndex + pluralRule int + values []int64 + }{ + {i18n.PluralFormOne, PluralRuleDefault, []int64{1}}, + {i18n.PluralFormOther, PluralRuleDefault, []int64{0, 2, 10, 256}}, + + {i18n.PluralFormOther, PluralRuleOneForm, []int64{0, 1, 2}}, + + {i18n.PluralFormOne, PluralRuleBengali, []int64{0, 1}}, + {i18n.PluralFormOther, PluralRuleBengali, []int64{2, 10, 256}}, + + {i18n.PluralFormOne, PluralRuleIcelandic, []int64{1, 21, 31}}, + {i18n.PluralFormOther, PluralRuleIcelandic, []int64{0, 2, 11, 15, 256}}, + + {i18n.PluralFormOne, PluralRuleFilipino, []int64{0, 1, 2, 3, 5, 7, 8, 10, 11, 12, 257}}, + {i18n.PluralFormOther, PluralRuleFilipino, []int64{4, 6, 9, 14, 16, 19, 256}}, + + {i18n.PluralFormOne, PluralRuleCzech, []int64{1}}, + {i18n.PluralFormFew, PluralRuleCzech, []int64{2, 3, 4}}, + {i18n.PluralFormOther, PluralRuleCzech, []int64{5, 0, 12, 78, 254}}, + + {i18n.PluralFormOne, PluralRuleRussian, []int64{1, 21, 31}}, + {i18n.PluralFormFew, PluralRuleRussian, []int64{2, 23, 34}}, + {i18n.PluralFormMany, PluralRuleRussian, []int64{0, 5, 11, 37, 111, 256}}, + + {i18n.PluralFormOne, PluralRulePolish, []int64{1}}, + {i18n.PluralFormFew, PluralRulePolish, []int64{2, 23, 34}}, + {i18n.PluralFormMany, PluralRulePolish, []int64{0, 5, 11, 21, 37, 256}}, + + {i18n.PluralFormZero, PluralRuleLatvian, []int64{0, 10, 11, 17}}, + {i18n.PluralFormOne, PluralRuleLatvian, []int64{1, 21, 71}}, + {i18n.PluralFormOther, PluralRuleLatvian, []int64{2, 7, 22, 23, 256}}, + + {i18n.PluralFormOne, PluralRuleLithuanian, []int64{1, 21, 31}}, + {i18n.PluralFormFew, PluralRuleLithuanian, []int64{2, 5, 9, 23, 34, 256}}, + {i18n.PluralFormMany, PluralRuleLithuanian, []int64{0, 10, 11, 18}}, + + {i18n.PluralFormOne, PluralRuleFrench, []int64{0, 1}}, + {i18n.PluralFormMany, PluralRuleFrench, []int64{1000000, 2000000}}, + {i18n.PluralFormOther, PluralRuleFrench, []int64{2, 4, 10, 256}}, + + {i18n.PluralFormOne, PluralRuleCatalan, []int64{1}}, + {i18n.PluralFormMany, PluralRuleCatalan, []int64{1000000, 2000000}}, + {i18n.PluralFormOther, PluralRuleCatalan, []int64{0, 2, 4, 10, 256}}, + + {i18n.PluralFormOne, PluralRuleSlovenian, []int64{1, 101, 201, 501}}, + {i18n.PluralFormTwo, PluralRuleSlovenian, []int64{2, 102, 202, 502}}, + {i18n.PluralFormFew, PluralRuleSlovenian, []int64{3, 103, 203, 503, 4, 104, 204, 504}}, + {i18n.PluralFormOther, PluralRuleSlovenian, []int64{0, 5, 11, 12, 20, 256}}, + + {i18n.PluralFormZero, PluralRuleArabic, []int64{0}}, + {i18n.PluralFormOne, PluralRuleArabic, []int64{1}}, + {i18n.PluralFormTwo, PluralRuleArabic, []int64{2}}, + {i18n.PluralFormFew, PluralRuleArabic, []int64{3, 4, 9, 10, 103, 104}}, + {i18n.PluralFormMany, PluralRuleArabic, []int64{11, 12, 13, 14, 17, 111, 256}}, + {i18n.PluralFormOther, PluralRuleArabic, []int64{100, 101, 102}}, + } + + for _, tc := range testCases { + for _, n := range tc.values { + assert.Equal(t, tc.expect, PluralRules[tc.pluralRule](n), "Testcase for plural rule %d, value %d", tc.pluralRule, n) + } + } +} diff --git a/modules/translation/translation.go b/modules/translation/translation.go index 17c7cc068b..f2d77ac340 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -1,4 +1,5 @@ // Copyright 2020 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package translation diff --git a/modules/translation/translation_test.go b/modules/translation/translation_test.go index 7584490941..34b1ebf91f 100644 --- a/modules/translation/translation_test.go +++ b/modules/translation/translation_test.go @@ -1,10 +1,9 @@ // Copyright 2023 The Gitea Authors. All rights reserved. +// Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package translation -// TODO: make this package friendly to testing - import ( "testing" @@ -48,111 +47,3 @@ func TestPrettyNumber(t *testing.T) { assert.Equal(t, "1,000,000", l.PrettyNumber(1000000)) assert.Equal(t, "1,000,000.1", l.PrettyNumber(1000000.1)) } - -func TestGetPluralRule(t *testing.T) { - assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en")) - assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en-US")) - assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en_UK")) - assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("nds")) - assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("de-DE")) - - assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("zh")) - assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("ja")) - - assert.Equal(t, PluralRuleBengali, GetPluralRuleImpl("bn")) - - assert.Equal(t, PluralRuleIcelandic, GetPluralRuleImpl("is")) - - assert.Equal(t, PluralRuleFilipino, GetPluralRuleImpl("fil")) - - assert.Equal(t, PluralRuleCzech, GetPluralRuleImpl("cs")) - - assert.Equal(t, PluralRuleRussian, GetPluralRuleImpl("ru")) - - assert.Equal(t, PluralRulePolish, GetPluralRuleImpl("pl")) - - assert.Equal(t, PluralRuleLatvian, GetPluralRuleImpl("lv")) - - assert.Equal(t, PluralRuleLithuanian, GetPluralRuleImpl("lt")) - - assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("fr")) - - assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("ca")) - - assert.Equal(t, PluralRuleSlovenian, GetPluralRuleImpl("sl")) - - assert.Equal(t, PluralRuleArabic, GetPluralRuleImpl("ar")) - - assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("pt-PT")) - assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("pt-BR")) - - assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("invalid")) -} - -func TestApplyPluralRule(t *testing.T) { - testCases := []struct { - expect i18n.PluralFormIndex - pluralRule int - values []int64 - }{ - {i18n.PluralFormOne, PluralRuleDefault, []int64{1}}, - {i18n.PluralFormOther, PluralRuleDefault, []int64{0, 2, 10, 256}}, - - {i18n.PluralFormOther, PluralRuleOneForm, []int64{0, 1, 2}}, - - {i18n.PluralFormOne, PluralRuleBengali, []int64{0, 1}}, - {i18n.PluralFormOther, PluralRuleBengali, []int64{2, 10, 256}}, - - {i18n.PluralFormOne, PluralRuleIcelandic, []int64{1, 21, 31}}, - {i18n.PluralFormOther, PluralRuleIcelandic, []int64{0, 2, 11, 15, 256}}, - - {i18n.PluralFormOne, PluralRuleFilipino, []int64{0, 1, 2, 3, 5, 7, 8, 10, 11, 12, 257}}, - {i18n.PluralFormOther, PluralRuleFilipino, []int64{4, 6, 9, 14, 16, 19, 256}}, - - {i18n.PluralFormOne, PluralRuleCzech, []int64{1}}, - {i18n.PluralFormFew, PluralRuleCzech, []int64{2, 3, 4}}, - {i18n.PluralFormOther, PluralRuleCzech, []int64{5, 0, 12, 78, 254}}, - - {i18n.PluralFormOne, PluralRuleRussian, []int64{1, 21, 31}}, - {i18n.PluralFormFew, PluralRuleRussian, []int64{2, 23, 34}}, - {i18n.PluralFormMany, PluralRuleRussian, []int64{0, 5, 11, 37, 111, 256}}, - - {i18n.PluralFormOne, PluralRulePolish, []int64{1}}, - {i18n.PluralFormFew, PluralRulePolish, []int64{2, 23, 34}}, - {i18n.PluralFormMany, PluralRulePolish, []int64{0, 5, 11, 21, 37, 256}}, - - {i18n.PluralFormZero, PluralRuleLatvian, []int64{0, 10, 11, 17}}, - {i18n.PluralFormOne, PluralRuleLatvian, []int64{1, 21, 71}}, - {i18n.PluralFormOther, PluralRuleLatvian, []int64{2, 7, 22, 23, 256}}, - - {i18n.PluralFormOne, PluralRuleLithuanian, []int64{1, 21, 31}}, - {i18n.PluralFormFew, PluralRuleLithuanian, []int64{2, 5, 9, 23, 34, 256}}, - {i18n.PluralFormMany, PluralRuleLithuanian, []int64{0, 10, 11, 18}}, - - {i18n.PluralFormOne, PluralRuleFrench, []int64{0, 1}}, - {i18n.PluralFormMany, PluralRuleFrench, []int64{1000000, 2000000}}, - {i18n.PluralFormOther, PluralRuleFrench, []int64{2, 4, 10, 256}}, - - {i18n.PluralFormOne, PluralRuleCatalan, []int64{1}}, - {i18n.PluralFormMany, PluralRuleCatalan, []int64{1000000, 2000000}}, - {i18n.PluralFormOther, PluralRuleCatalan, []int64{0, 2, 4, 10, 256}}, - - {i18n.PluralFormOne, PluralRuleSlovenian, []int64{1, 101, 201, 501}}, - {i18n.PluralFormTwo, PluralRuleSlovenian, []int64{2, 102, 202, 502}}, - {i18n.PluralFormFew, PluralRuleSlovenian, []int64{3, 103, 203, 503, 4, 104, 204, 504}}, - {i18n.PluralFormOther, PluralRuleSlovenian, []int64{0, 5, 11, 12, 20, 256}}, - - {i18n.PluralFormZero, PluralRuleArabic, []int64{0}}, - {i18n.PluralFormOne, PluralRuleArabic, []int64{1}}, - {i18n.PluralFormTwo, PluralRuleArabic, []int64{2}}, - {i18n.PluralFormFew, PluralRuleArabic, []int64{3, 4, 9, 10, 103, 104}}, - {i18n.PluralFormMany, PluralRuleArabic, []int64{11, 12, 13, 14, 17, 111, 256}}, - {i18n.PluralFormOther, PluralRuleArabic, []int64{100, 101, 102}}, - } - - for _, tc := range testCases { - for _, n := range tc.values { - assert.Equal(t, tc.expect, PluralRules[tc.pluralRule](n), "Testcase for plural rule %d, value %d", tc.pluralRule, n) - } - } -} diff --git a/modules/updatechecker/update_checker.go b/modules/updatechecker/update_checker.go index b0932ba663..8b524b6519 100644 --- a/modules/updatechecker/update_checker.go +++ b/modules/updatechecker/update_checker.go @@ -60,9 +60,9 @@ func getVersionDNS(domainEndpoint string) (version string, err error) { } for _, record := range records { - if strings.HasPrefix(record, "forgejo_versions=") { + if after, ok := strings.CutPrefix(record, "forgejo_versions="); ok { // Get all supported versions, separated by a comma. - supportedVersions := strings.Split(strings.TrimPrefix(record, "forgejo_versions="), ",") + supportedVersions := strings.Split(after, ",") // For now always return the latest supported version. return supportedVersions[len(supportedVersions)-1], nil } diff --git a/modules/util/remove.go b/modules/util/remove.go index 2a65a6b0aa..e2cffc92c9 100644 --- a/modules/util/remove.go +++ b/modules/util/remove.go @@ -12,7 +12,7 @@ import ( // Remove removes the named file or (empty) directory with at most 5 attempts. func Remove(name string) error { var err error - for i := 0; i < 5; i++ { + for range 5 { err = os.Remove(name) if err == nil { break @@ -35,7 +35,7 @@ func Remove(name string) error { // RemoveAll removes the named file or (empty) directory with at most 5 attempts. func RemoveAll(name string) error { var err error - for i := 0; i < 5; i++ { + for range 5 { err = os.RemoveAll(name) if err == nil { break @@ -58,7 +58,7 @@ func RemoveAll(name string) error { // Rename renames (moves) oldpath to newpath with at most 5 attempts. func Rename(oldpath, newpath string) error { var err error - for i := 0; i < 5; i++ { + for i := range 5 { err = os.Rename(oldpath, newpath) if err == nil { break diff --git a/modules/util/rotatingfilewriter/writer_test.go b/modules/util/rotatingfilewriter/writer_test.go index 5b3b351667..c3664d8c4f 100644 --- a/modules/util/rotatingfilewriter/writer_test.go +++ b/modules/util/rotatingfilewriter/writer_test.go @@ -24,7 +24,7 @@ func TestCompressOldFile(t *testing.T) { ng, err := os.OpenFile(nonGzip, os.O_CREATE|os.O_WRONLY, 0o660) require.NoError(t, err) - for i := 0; i < 999; i++ { + for range 999 { f.WriteString("This is a test file\n") ng.WriteString("This is a test file\n") } diff --git a/modules/util/timer_test.go b/modules/util/timer_test.go index 602800c248..1f9a4ac586 100644 --- a/modules/util/timer_test.go +++ b/modules/util/timer_test.go @@ -12,19 +12,19 @@ import ( ) func TestDebounce(t *testing.T) { - var c int64 + var c atomic.Int64 d := Debounce(50 * time.Millisecond) - d(func() { atomic.AddInt64(&c, 1) }) - assert.EqualValues(t, 0, atomic.LoadInt64(&c)) - d(func() { atomic.AddInt64(&c, 1) }) - d(func() { atomic.AddInt64(&c, 1) }) + d(func() { c.Add(1) }) + assert.EqualValues(t, 0, c.Load()) + d(func() { c.Add(1) }) + d(func() { c.Add(1) }) time.Sleep(100 * time.Millisecond) - assert.EqualValues(t, 1, atomic.LoadInt64(&c)) - d(func() { atomic.AddInt64(&c, 1) }) - assert.EqualValues(t, 1, atomic.LoadInt64(&c)) - d(func() { atomic.AddInt64(&c, 1) }) - d(func() { atomic.AddInt64(&c, 1) }) - d(func() { atomic.AddInt64(&c, 1) }) + assert.EqualValues(t, 1, c.Load()) + d(func() { c.Add(1) }) + assert.EqualValues(t, 1, c.Load()) + d(func() { c.Add(1) }) + d(func() { c.Add(1) }) + d(func() { c.Add(1) }) time.Sleep(100 * time.Millisecond) - assert.EqualValues(t, 2, atomic.LoadInt64(&c)) + assert.EqualValues(t, 2, c.Load()) } diff --git a/modules/util/truncate.go b/modules/util/truncate.go index 7207a89177..35836f745c 100644 --- a/modules/util/truncate.go +++ b/modules/util/truncate.go @@ -47,7 +47,7 @@ func SplitTrimSpace(input, sep string) []string { input = strings.ReplaceAll(input, "\r\n", "\n") var stringList []string - for _, s := range strings.Split(input, sep) { + for s := range strings.SplitSeq(input, sep) { // trim leading and trailing space stringList = append(stringList, strings.TrimSpace(s)) } diff --git a/modules/util/util.go b/modules/util/util.go index f21936fb5f..996a566830 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -215,11 +215,6 @@ func ToFloat64(number any) (float64, error) { return value, nil } -// ToPointer returns the pointer of a copy of any given value -func ToPointer[T any](val T) *T { - return &val -} - // Iif is an "inline-if", it returns "trueVal" if "condition" is true, otherwise "falseVal" func Iif[T any](condition bool, trueVal, falseVal T) T { if condition { diff --git a/modules/util/util_test.go b/modules/util/util_test.go index a85113b2f4..fae4f6673d 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -5,10 +5,10 @@ package util_test import ( - "bytes" "crypto/rand" "strings" "testing" + "testing/cryptotest" "forgejo.org/modules/test" "forgejo.org/modules/util" @@ -211,42 +211,25 @@ func TestToTitleCase(t *testing.T) { assert.Equal(t, `Foo Bar Baz`, util.ToTitleCase(`FOO BAR BAZ`)) } -func TestToPointer(t *testing.T) { - assert.Equal(t, "abc", *util.ToPointer("abc")) - assert.Equal(t, 123, *util.ToPointer(123)) - abc := "abc" - assert.NotSame(t, &abc, util.ToPointer(abc)) - val123 := 123 - assert.NotSame(t, &val123, util.ToPointer(val123)) -} - func TestReserveLineBreakForTextarea(t *testing.T) { assert.Equal(t, "test\ndata", util.ReserveLineBreakForTextarea("test\r\ndata")) assert.Equal(t, "test\ndata\n", util.ReserveLineBreakForTextarea("test\r\ndata\r\n")) } const ( - testPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAOhB7/zzhC+HXDdGOdLwJln5NYwm6UNXx3chmQSVTG4\n" + testPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHX8yEKexoMqBPPwG4pGAhhjo5CyiHLiJZ7p3jg0aJZM\n" testPrivateKey = `-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz -c2gtZWQyNTUxOQAAACADoQe/884Qvh1w3RjnS8CZZ+TWMJulDV8d3IZkElUxuAAA -AIggISIjICEiIwAAAAtzc2gtZWQyNTUxOQAAACADoQe/884Qvh1w3RjnS8CZZ+TW -MJulDV8d3IZkElUxuAAAAEAAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0e -HwOhB7/zzhC+HXDdGOdLwJln5NYwm6UNXx3chmQSVTG4AAAAAAECAwQF +c2gtZWQyNTUxOQAAACB1/MhCnsaDKgTz8BuKRgIYY6OQsohy4iWe6d44NGiWTAAA +AIhNLlZvTS5WbwAAAAtzc2gtZWQyNTUxOQAAACB1/MhCnsaDKgTz8BuKRgIYY6OQ +sohy4iWe6d44NGiWTAAAAEDZh37ObTaKrBpvQZ7GJ8drG/sfo3xBoR6kat1qSNiU +dHX8yEKexoMqBPPwG4pGAhhjo5CyiHLiJZ7p3jg0aJZMAAAAAAECAwQF -----END OPENSSH PRIVATE KEY-----` + "\n" ) func TestGeneratingEd25519Keypair(t *testing.T) { defer test.MockProtect(&rand.Reader)() - - // Only 32 bytes needs to be provided to generate a ed25519 keypair. - // And another 32 bytes are required, which is included as random value - // in the OpenSSH format. - b := make([]byte, 64) - for i := 0; i < 64; i++ { - b[i] = byte(i) - } - rand.Reader = bytes.NewReader(b) + cryptotest.SetGlobalRandom(t, 0) publicKey, privateKey, err := util.GenerateSSHKeypair() require.NoError(t, err) diff --git a/modules/validation/binding.go b/modules/validation/binding.go index 463e7e8f7a..23d0622de4 100644 --- a/modules/validation/binding.go +++ b/modules/validation/binding.go @@ -266,17 +266,17 @@ func addEmailBindingRules() { } func portOnly(hostport string) string { - colon := strings.IndexByte(hostport, ':') - if colon == -1 { + _, after, ok := strings.Cut(hostport, ":") + if !ok { return "" } - if i := strings.Index(hostport, "]:"); i != -1 { - return hostport[i+len("]:"):] + if _, after, ok := strings.Cut(hostport, "]:"); ok { + return after } if strings.Contains(hostport, "]") { return "" } - return hostport[colon+len(":"):] + return after } func validPort(p string) bool { diff --git a/modules/validation/helpers.go b/modules/validation/helpers.go index 4b28dead03..ce451b8ff4 100644 --- a/modules/validation/helpers.go +++ b/modules/validation/helpers.go @@ -7,6 +7,7 @@ import ( "net" "net/url" "regexp" + "slices" "strings" "forgejo.org/modules/setting" @@ -40,12 +41,7 @@ func IsValidSiteURL(uri string) bool { return false } - for _, scheme := range setting.Service.ValidSiteURLSchemes { - if scheme == u.Scheme { - return true - } - } - return false + return slices.Contains(setting.Service.ValidSiteURLSchemes, u.Scheme) } // IsAPIURL checks if URL is current Gitea instance API URL @@ -102,6 +98,21 @@ var ( // No consecutive or trailing non-alphanumeric chars, catches both cases invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) + + // This is intended to accept any character, in any language, with accent symbols, + // as well as an arbitrary amount of subdomains and an optional port number defined + // through `:12345`. + // + // This is intended to cover username cases from distant servers in the fediverse, which + // can have much laxer requirements than those of Forgejo. It is not intended to check for + // invalid, non-standard compliant domains. + // + // For instance, the following should work: + // @user.όνομαß_21__@subdomain1.subdomain2.example.tld:65536 + // @42@42.example.tld + // @user@example.tld:99999 (presumed to be an impossible case) + // @-@-.tld (also impossible) + validFediverseUsernamePattern = regexp.MustCompile(`^(@[\p{L}\p{M}0-9_\.\-]{1,})(@[\p{L}\p{M}0-9_\.\-]{1,})(:[1-9][0-9]{0,4})?$`) ) // IsValidUsername checks if username is valid @@ -114,3 +125,11 @@ func IsValidUsername(name string) bool { return validUsernamePatternWithoutDots.MatchString(name) && !invalidUsernamePattern.MatchString(name) } + +// IsValidActivityPubUsername checks whether the username can be a valid ActivityPub handle. +// +// Username refers to the Forgejo user account's username for consistency, and not +// e.g. "username" in @username@example.tld. +func IsValidActivityPubUsername(name string) bool { + return validFediverseUsernamePattern.MatchString(name) +} diff --git a/modules/validation/helpers_test.go b/modules/validation/helpers_test.go index 7e32184691..6bca3664e1 100644 --- a/modules/validation/helpers_test.go +++ b/modules/validation/helpers_test.go @@ -1,4 +1,5 @@ // Copyright 2018 The Gitea Authors. All rights reserved. +// Copyright 2025 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT package validation @@ -213,3 +214,109 @@ func TestIsValidUsernameBanDots(t *testing.T) { }) } } + +func TestIsValidActivityPubUsername(t *testing.T) { + cases := []struct { + description string + username string + valid bool + }{ + { + description: "Username without domain", + username: "@user", + valid: false, + }, + { + description: "Username with domain", + username: "@user@example.tld", + valid: true, + }, + { + description: "Numeric username with subdomain", + username: "@42@42.example.tld", + valid: true, + }, + { + description: "Username with two subdomains", + username: "@user@forgejo.activitypub.example.tld", + valid: true, + }, + { + description: "Username with domain and without port", + username: "@user@social.example.tld:", + valid: false, + }, + { + description: "Username with domain and invalid port 0", + username: "@user@social.example.tld:0", + valid: false, + }, + { + // We do not validate the port and assume that federationHost.HostPort + // cannot present such invalid ports. That also makes the previous case + // (port: 0) redundant, but it doesn't hurt. + description: "Username with domain and valid port", + username: "@user@social.example.tld:65536", + valid: true, + }, + { + description: "Username with Latin letters and special symbols", + username: "@$username$@example.tld", + valid: false, + }, + { + description: "Strictly numeric handle, domain, TLD", + username: "@0123456789@0123456789.0123456789.123", + valid: true, + }, + { + description: "Handle with Latin characters and dashes", + username: "@0-O@O-O.tld", + valid: true, + }, + // This is an impossible case, but we assume that this will never happen + // to begin with. + { + description: "Handle that only has dashes", + username: "@-@-.-", + valid: true, + }, + { + description: "Username with a mix of Latin and non-Latin letters containing accents", + username: "@usernäme.όνομαß_21__@example.tld", + valid: true, + }, + // Note: Our regex should accept any character, in any language and with accent symbols. + // The list is neither exhaustive, nor does it represent all possible cases. + // I chose some TLDs from https://en.wikipedia.org/wiki/Country_code_top-level_domain, + // although only one test case should suffice in theory. Nevertheless, to play it safe, + // I included four from different geographic regions whose scripts were legible using my + // IDE's default font to play it safe. + { + description: "Username, domain and ccTLD in Greek", + username: "@ευ@ευ.ευ", + valid: true, + }, + { + description: "Username, domain and ccTLD in Georgian (Mkhedruli)", + username: "@გე@გე.გე", + valid: true, + }, + { + description: "Username, domain and ccTLD of Malaysia (Arabic Jawi)", + username: "@مليسيا@ລمليسيا.مليسيا", + valid: true, + }, + { + description: "Username, domain and ccTLD of China (Simplified)", + username: "@中国@中国.中国", + valid: true, + }, + } + + for _, testCase := range cases { + t.Run(testCase.description, func(t *testing.T) { + assert.Equal(t, testCase.valid, IsValidActivityPubUsername(testCase.username)) + }) + } +} diff --git a/modules/validation/validatable.go b/modules/validation/validatable.go index 7bcca03bf8..6c58a9148e 100644 --- a/modules/validation/validatable.go +++ b/modules/validation/validatable.go @@ -6,6 +6,7 @@ package validation import ( "fmt" "reflect" + "slices" "strings" "unicode/utf8" @@ -34,9 +35,9 @@ type Validateable interface { } func IsValid(v Validateable) (bool, error) { - if valdationErrors := v.Validate(); len(valdationErrors) > 0 { + if validationErrors := v.Validate(); len(validationErrors) > 0 { typeof := reflect.TypeOf(v) - errString := strings.Join(valdationErrors, "\n") + errString := strings.Join(validationErrors, "\n") return false, ErrNotValid{fmt.Sprint(typeof, ": ", errString)} } @@ -69,6 +70,8 @@ func ValidateNotEmpty(value any, name string) []string { if v == 0 { isValid = false } + case ap.Typer: + isValid = len(value.(ap.Typer).AsTypes()) > 0 default: isValid = false } @@ -87,10 +90,8 @@ func ValidateMaxLen(value string, maxLen int, name string) []string { } func ValidateOneOf(value any, allowed []any, name string) []string { - for _, allowedElem := range allowed { - if value == allowedElem { - return []string{} - } + if slices.Contains(allowed, value) { + return []string{} } return []string{fmt.Sprintf("Field %s contains the value %v, which is not in allowed subset %v", name, value, allowed)} } diff --git a/modules/web/handler.go b/modules/web/handler.go index 4a7f28b1fa..e3f0b029fd 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -17,7 +17,7 @@ import ( var responseStatusProviders = map[reflect.Type]func(req *http.Request) types.ResponseStatusProvider{} func RegisterResponseStatusProvider[T any](fn func(req *http.Request) types.ResponseStatusProvider) { - responseStatusProviders[reflect.TypeOf((*T)(nil)).Elem()] = fn + responseStatusProviders[reflect.TypeFor[T]()] = fn } // responseWriter is a wrapper of http.ResponseWriter, to check whether the response has been written @@ -49,9 +49,9 @@ func (r *responseWriter) WriteHeader(statusCode int) { } var ( - httpReqType = reflect.TypeOf((*http.Request)(nil)) - respWriterType = reflect.TypeOf((*http.ResponseWriter)(nil)).Elem() - cancelFuncType = reflect.TypeOf((*goctx.CancelFunc)(nil)).Elem() + httpReqType = reflect.TypeFor[*http.Request]() + respWriterType = reflect.TypeFor[http.ResponseWriter]() + cancelFuncType = reflect.TypeFor[goctx.CancelFunc]() ) // preCheckHandler checks whether the handler is valid, developers could get first-time feedback, all mistakes could be found at startup diff --git a/modules/web/middleware/binding.go b/modules/web/middleware/binding.go index 123eb29015..06bf55b571 100644 --- a/modules/web/middleware/binding.go +++ b/modules/web/middleware/binding.go @@ -30,7 +30,7 @@ func AssignForm(form any, data map[string]any) { typ := reflect.TypeOf(form) val := reflect.ValueOf(form) - for typ.Kind() == reflect.Ptr { + for typ.Kind() == reflect.Pointer { typ = typ.Elem() val = val.Elem() } @@ -51,7 +51,7 @@ func AssignForm(form any, data map[string]any) { } func getRuleBody(field reflect.StructField, prefix string) string { - for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { + for rule := range strings.SplitSeq(field.Tag.Get("binding"), ";") { if strings.HasPrefix(rule, prefix) { return rule[len(prefix) : len(rule)-1] } @@ -99,7 +99,7 @@ func Validate(errs binding.Errors, data map[string]any, f any, l translation.Loc typ := reflect.TypeOf(f) - if typ.Kind() == reflect.Ptr { + if typ.Kind() == reflect.Pointer { typ = typ.Elem() } diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 4603e64052..c8bb8276c5 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -5,6 +5,7 @@ package middleware import ( "context" + "maps" "time" "forgejo.org/modules/setting" @@ -22,9 +23,7 @@ func (ds ContextData) GetData() ContextData { } func (ds ContextData) MergeFrom(other ContextData) ContextData { - for k, v := range other { - ds[k] = v - } + maps.Copy(ds, other) return ds } diff --git a/modules/web/route.go b/modules/web/route.go index ceb97ba333..dc83178f74 100644 --- a/modules/web/route.go +++ b/modules/web/route.go @@ -107,8 +107,8 @@ func (r *Route) Methods(methods, pattern string, h ...any) { middlewares, handlerFunc := r.wrapMiddlewareAndHandler(h) fullPattern := r.getPattern(pattern) if strings.Contains(methods, ",") { - methods := strings.Split(methods, ",") - for _, method := range methods { + methods := strings.SplitSeq(methods, ",") + for method := range methods { r.R.With(middlewares...).Method(strings.TrimSpace(method), fullPattern, handlerFunc) } } else { diff --git a/options/gitignore/AdventureGameStudio b/options/gitignore/AdventureGameStudio new file mode 100644 index 0000000000..27a089f475 --- /dev/null +++ b/options/gitignore/AdventureGameStudio @@ -0,0 +1,31 @@ +# Built things +_Debug/ +Compiled/ + +# AudioCache can be rebuilt from sources +AudioCache/ + +# Lockfile +_OpenInEditor.lock + +# User settings +Game.agf.user +*.crm.user + +# Backups +Game.agf.bak +backup_acsprset.spr + +# Memory dumps +*.dmp + +# Temporary files +# temporarily created during sprite or room background compression +~aclzw.tmp +# temporary, main game data, before getting packed into exe +game28.dta +# temporary build of the game before being moved to Compiled/ folder +*.exe + +# Log files +warnings.log diff --git a/options/gitignore/Alteryx b/options/gitignore/Alteryx index a8e1341ffe..8fe3c5cd71 100644 --- a/options/gitignore/Alteryx +++ b/options/gitignore/Alteryx @@ -29,7 +29,7 @@ CASS.ini *.gzlc ## gitignore reference sites -# https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#Ignoring-Files +# https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#_ignoring # https://git-scm.com/docs/gitignore # https://help.github.com/articles/ignoring-files/ diff --git a/options/gitignore/Android b/options/gitignore/Android index 347e252ef1..e5cbb64142 100644 --- a/options/gitignore/Android +++ b/options/gitignore/Android @@ -12,8 +12,9 @@ local.properties captures/ .externalNativeBuild/ .cxx/ +*.aab *.apk -output.json +output-metadata.json # IntelliJ *.iml diff --git a/options/gitignore/Angular b/options/gitignore/Angular new file mode 100644 index 0000000000..0383c3a577 --- /dev/null +++ b/options/gitignore/Angular @@ -0,0 +1,28 @@ +# Angular specific +/dist/ +/out-tsc/ +/tmp/ +/coverage/ +/e2e/test-output/ +/.angular/ +.angular/ + +# Node modules and dependency files +/node_modules/ +/package-lock.json +/yarn.lock + +# Environment files +/.env + +# Angular CLI and build artefacts +/.angular-cli.json +/.ng/ + +# TypeScript cache +*.tsbuildinfo + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/options/gitignore/Ansible b/options/gitignore/Ansible index a8b42eb6ee..7eaa6e286f 100644 --- a/options/gitignore/Ansible +++ b/options/gitignore/Ansible @@ -1 +1,2 @@ *.retry +.ansible/ diff --git a/options/gitignore/ArchLinuxPackages b/options/gitignore/ArchLinuxPackages index b73905529f..289fa5c677 100644 --- a/options/gitignore/ArchLinuxPackages +++ b/options/gitignore/ArchLinuxPackages @@ -3,6 +3,7 @@ *.jar *.exe *.msi +*.deb *.zip *.tgz *.log diff --git a/options/gitignore/AutomationStudio b/options/gitignore/AutomationStudio new file mode 100644 index 0000000000..b5552b17a0 --- /dev/null +++ b/options/gitignore/AutomationStudio @@ -0,0 +1,31 @@ +# gitignore template for B&R Automation Studio (AS) 4 +# website: https://www.br-automation.com/en-us/products/software/automation-software/automation-studio/ + +# AS temporary directories +Binaries/ +Diagnosis/ +Temp/ +TempObjects/ + +# AS transfer files +*artransfer.br +*arTrsfmode.nv + +# 'ignored' directory +ignored/ + +# ARNC0ext +*arnc0ext.br + +# AS File types +*.bak +*.isopen +*.orig +*.log +*.asar +*.csvlog* +*.set +!**/Physical/**/*.set + +# RevInfo variables +*RevInfo.var diff --git a/options/gitignore/Autotools b/options/gitignore/Autotools index 617156f819..9a47826430 100644 --- a/options/gitignore/Autotools +++ b/options/gitignore/Autotools @@ -31,7 +31,9 @@ autom4te.cache # https://www.gnu.org/software/libtool/ +/libtool /ltmain.sh +.libs/ # http://www.gnu.org/software/texinfo diff --git a/options/gitignore/Bazel b/options/gitignore/Bazel index bc3afc20ba..4e1d5a2ba0 100644 --- a/options/gitignore/Bazel +++ b/options/gitignore/Bazel @@ -6,7 +6,7 @@ /bazel-* # Directories for the Bazel IntelliJ plugin containing the generated -# IntelliJ project files and plugin configuration. Seperate directories are +# IntelliJ project files and plugin configuration. Separate directories are # for the IntelliJ, Android Studio and CLion versions of the plugin. /.ijwb/ /.aswb/ diff --git a/options/gitignore/C b/options/gitignore/C index c6127b38c1..845cda6a8a 100644 --- a/options/gitignore/C +++ b/options/gitignore/C @@ -50,3 +50,6 @@ modules.order Module.symvers Mkfile.old dkms.conf + +# debug information files +*.dwo diff --git a/options/gitignore/C++ b/options/gitignore/C++ index 259148fa18..bdde3b171b 100644 --- a/options/gitignore/C++ +++ b/options/gitignore/C++ @@ -11,10 +11,18 @@ *.gch *.pch +# Linker files +*.ilk + +# Debugger Files +*.pdb + # Compiled Dynamic libraries *.so *.dylib *.dll +*.so.* + # Fortran module files *.mod @@ -30,3 +38,32 @@ *.exe *.out *.app + +# Build directories +build/ +Build/ +build-*/ + +# CMake generated files +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +Makefile +install_manifest.txt +compile_commands.json + +# Temporary files +*.tmp +*.log +*.bak +*.swp + +# vcpkg +vcpkg_installed/ + +# debug information files +*.dwo + +# test output & cache +Testing/ +.cache/ \ No newline at end of file diff --git a/options/gitignore/CMake b/options/gitignore/CMake index 11c76431e1..1f99f9d2f5 100644 --- a/options/gitignore/CMake +++ b/options/gitignore/CMake @@ -10,3 +10,10 @@ compile_commands.json CTestTestfile.cmake _deps CMakeUserPresets.json + +# CLion +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#cmake-build-* diff --git a/options/gitignore/ColdBox b/options/gitignore/ColdBox new file mode 100644 index 0000000000..93f003fad3 --- /dev/null +++ b/options/gitignore/ColdBox @@ -0,0 +1,24 @@ +# Servelet Ignores +WEB-INF + +# Engines + Database + CBFS + Secrets +.tmp/** +.env +.engine/** +.cbfs/** + +# Logs + Test Results +logs/** +tests/results/** + +## Ignored Dependencies +effective-pom.xml +/coldbox/** +/testbox/** +/modules/** +/lib/java/** + +# NPM JS Assets (If applicable) +**/node_modules/* +npm-debug.log +yarn-error.log diff --git a/options/gitignore/Cursor b/options/gitignore/Cursor new file mode 100644 index 0000000000..234f905b0e --- /dev/null +++ b/options/gitignore/Cursor @@ -0,0 +1,2 @@ +.cursorignore +.cursorindexingignore diff --git a/options/gitignore/Dart b/options/gitignore/Dart index 3a83c2f087..3150b4060c 100644 --- a/options/gitignore/Dart +++ b/options/gitignore/Dart @@ -16,9 +16,11 @@ doc/api/ # Avoid committing generated Javascript files: *.dart.js -*.info.json # Produced by the --dump-info flag. -*.js # When generated by dart2js. Don't specify *.js if your - # project includes source files written in JavaScript. +# Produced by the --dump-info flag. +*.info.json +# When generated by dart2js. Don't specify *.js if your +# project includes source files written in JavaScript. +*.js *.js_ *.js.deps *.js.map diff --git a/options/gitignore/Delphi b/options/gitignore/Delphi index 8df99b676b..9db64f626d 100644 --- a/options/gitignore/Delphi +++ b/options/gitignore/Delphi @@ -68,6 +68,7 @@ *.projdata *.tvsconfig *.dsk +*.dsv # Delphi history and backups __history/ diff --git a/options/gitignore/Dotnet b/options/gitignore/Dotnet new file mode 100644 index 0000000000..7282dbf28f --- /dev/null +++ b/options/gitignore/Dotnet @@ -0,0 +1,57 @@ +## A streamlined .gitignore for modern .NET projects +## including temporary files, build results, and +## files generated by popular .NET tools. If you are +## developing with Visual Studio, the VS .gitignore +## https://github.com/github/gitignore/blob/main/VisualStudio.gitignore +## has more thorough IDE-specific entries. +## +## Get latest from https://github.com/github/gitignore/blob/main/Dotnet.gitignore + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg + +# dotenv environment variables file +.env + +# Others +~$* +*~ +CodeCoverage/ + +# MSBuild Binary and Structured Log +*.binlog + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml diff --git a/options/gitignore/Dotter b/options/gitignore/Dotter new file mode 100644 index 0000000000..86e82e8c5f --- /dev/null +++ b/options/gitignore/Dotter @@ -0,0 +1,6 @@ +# local files are for host-specific overrides +.dotter/local.toml + +# ignore caches +.dotter/cache.toml +.dotter/cache diff --git a/options/gitignore/Drupal b/options/gitignore/Drupal index faae808384..3856fe4634 100644 --- a/options/gitignore/Drupal +++ b/options/gitignore/Drupal @@ -25,12 +25,15 @@ /web/vendor /web/core /web/modules/README.txt +/web/modules/contrib /web/profiles/README.txt +/web/profiles/contrib /web/sites/development.services.yml /web/sites/example.settings.local.php /web/sites/example.sites.php /web/sites/README.txt /web/themes/README.txt +/web/themes/contrib /web/.csslintrc /web/.editorconfig /web/.eslintignore diff --git a/options/gitignore/Eclipse b/options/gitignore/Eclipse index acec74ac06..85723da801 100644 --- a/options/gitignore/Eclipse +++ b/options/gitignore/Eclipse @@ -48,7 +48,7 @@ local.properties # Annotation Processing .apt_generated/ -.apt_generated_test/ +.apt_generated_tests/ # Scala IDE specific (Scala & Java development for Eclipse) .cache-main diff --git a/options/gitignore/Elisp b/options/gitignore/Elisp index 206569dc66..adef969d3d 100644 --- a/options/gitignore/Elisp +++ b/options/gitignore/Elisp @@ -2,7 +2,13 @@ *.elc # Packaging -.cask +.cask/ +.eask/ +.eldev/ +.keg/ + +# Built distribution +dist/ # Backup files *~ diff --git a/options/gitignore/Emacs b/options/gitignore/Emacs index d40e86599b..489b89280c 100644 --- a/options/gitignore/Emacs +++ b/options/gitignore/Emacs @@ -47,3 +47,5 @@ flycheck_*.el # network security /network-security.data +# undo-tree +*.~undo-tree~ diff --git a/options/gitignore/Expo b/options/gitignore/Expo new file mode 100644 index 0000000000..164986e1ff --- /dev/null +++ b/options/gitignore/Expo @@ -0,0 +1,32 @@ +# .gitignore template for Expo +# website: https://expo.dev/ +# docs: https://docs.expo.dev/workflow/expo-cli/ +# +# Rationale: +# node_modules/ is always ignored +# .expo/, .expo-shared/ are Expo’s local state and project-settings cache (see docs) +#  Metro caches/logs are *.expo, *.tunnel, *.cache, *.tmp, *.log + +# Node modules +node_modules/ + +# Expo local state and caches +.expo/ # runtime state (Metro bundler, dev-client data, tunnels) +.expo-shared/ # shared project settings (app.json edits, etc.) + +# Metro bundler caches/logs +*.expo # generic Expo temp files +*.tunnel # Expo DevTools tunnels +*.cache # Metro cache folder +*.tmp # temp files created during bundling +*.log # build or Metro logs + +# Environment variables +.env +.env.local +.env.*.local + +# Package manager logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/options/gitignore/Firebase b/options/gitignore/Firebase new file mode 100644 index 0000000000..55b8b0ea7f --- /dev/null +++ b/options/gitignore/Firebase @@ -0,0 +1,28 @@ +# Firebase build and deployment files +/firebase-debug.log +/firebase-debug.*.log +.firebaserc + +# Firebase Hosting +/firebase.json +*.cache +hosting/.cache + +# Firebase Functions +/functions/node_modules/ +/functions/.env +/functions/package-lock.json + +# Firebase Emulators +/firebase-*.zip +/.firebase/ +/emulator-ui/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment files (local configs) +/.env.* diff --git a/options/gitignore/Fortran b/options/gitignore/Fortran index 259148fa18..bdde3b171b 100644 --- a/options/gitignore/Fortran +++ b/options/gitignore/Fortran @@ -11,10 +11,18 @@ *.gch *.pch +# Linker files +*.ilk + +# Debugger Files +*.pdb + # Compiled Dynamic libraries *.so *.dylib *.dll +*.so.* + # Fortran module files *.mod @@ -30,3 +38,32 @@ *.exe *.out *.app + +# Build directories +build/ +Build/ +build-*/ + +# CMake generated files +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +Makefile +install_manifest.txt +compile_commands.json + +# Temporary files +*.tmp +*.log +*.bak +*.swp + +# vcpkg +vcpkg_installed/ + +# debug information files +*.dwo + +# test output & cache +Testing/ +.cache/ \ No newline at end of file diff --git a/options/gitignore/GitHubPages b/options/gitignore/GitHubPages index 493e69ba39..807c07fc55 100644 --- a/options/gitignore/GitHubPages +++ b/options/gitignore/GitHubPages @@ -11,7 +11,7 @@ _site/ /vendor # Specific ignore for GitHub Pages -# GitHub Pages will always use its own deployed version of pages-gem +# GitHub Pages will always use its own deployed version of pages-gem # This means GitHub Pages will NOT use your Gemfile.lock and therefore it is # counterproductive to check this file into the repository. # Details at https://github.com/github/pages-gem/issues/768 diff --git a/options/gitignore/Gleam b/options/gitignore/Gleam new file mode 100644 index 0000000000..599be4eb92 --- /dev/null +++ b/options/gitignore/Gleam @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/options/gitignore/Go b/options/gitignore/Go index 6f72f89261..aaadf736e5 100644 --- a/options/gitignore/Go +++ b/options/gitignore/Go @@ -11,8 +11,11 @@ # Test binary, built with `go test -c` *.test -# Output of the go coverage tool, specifically when used with LiteIDE +# Code coverage profiles and other test artifacts *.out +coverage.* +*.coverprofile +profile.cov # Dependency directories (remove the comment below to include it) # vendor/ @@ -23,3 +26,7 @@ go.work.sum # env file .env + +# Editor/IDE +# .idea/ +# .vscode/ diff --git a/options/gitignore/Godot b/options/gitignore/Godot index d9aac213e7..e00df843c3 100644 --- a/options/gitignore/Godot +++ b/options/gitignore/Godot @@ -1,10 +1,12 @@ # Godot 4+ specific ignores .godot/ +.nomedia # Godot-specific ignores .import/ export.cfg -export_presets.cfg +export_credentials.cfg +*.tmp # Imported translations (automatically generated from CSV files) *.translation diff --git a/options/gitignore/Gradle b/options/gitignore/Gradle index a5b111377b..296d3f0021 100644 --- a/options/gitignore/Gradle +++ b/options/gitignore/Gradle @@ -1,6 +1,6 @@ .gradle **/build/ -!src/**/build/ +!**/src/**/build/ # Ignore Gradle GUI config gradle-app.setting diff --git a/options/gitignore/HIP b/options/gitignore/HIP new file mode 100644 index 0000000000..5f3324cf14 --- /dev/null +++ b/options/gitignore/HIP @@ -0,0 +1,50 @@ +# HIP.gitignore +# GitHub gitignore template for AMD HIP (ROCm) projects +# +# Reference: +# Official AMD ROCm HIP .gitignore: https://github.com/ROCm/hip/blob/amd-staging/.gitignore + +# 1. Build directories and files +/build/ # common build directory +/CMakeFiles/ # CMake internal files +/CMakeCache.txt # CMake cache file +/Makefile # autogenerated Makefile +/cmake_install.cmake # install script +/install_manifest.txt # install manifest list +*.ninja-dep # Ninja dependency files +*.ninja_log # Ninja log files +meson-logs/ # Meson log directory + +# 2. Compilation outputs and intermediates +*.o # object files +*.obj # Windows object files +*.so # shared libraries +*.a # static librarie +*.d # dependency files +*.gch # precompiled headers +*.ii # preprocessed output +*.ii.cpp # C++ preprocessed output +*.out # generic executable outputs +*.exe # Windows executables + +# 3. HIP/ROCm specific binaries and intermediates +*.hsaco # ROCm compiled binary +*.s # assembly output +*.kernels.cpp # autogenerated kernel sources +*.hip.cpp.* # hipcc intermediate outputs + +# 4. Official sample binaries and tutorial outputs +bin/hipInfo # sample binary +bin/hipBusBandwidth # sample binary +bin/hipDispatchLatency # sample binary +bin/hipify-clang # sample tool +samples/**/*.out # tutorial outputs +samples/**/*.code # ISA/code dumps +samples/**/*.hsaco # compiled binaries +samples/**/*.co # kernel code outputs + +# 5. Tags, logs and test outputs +tags # ctags index +*.log # log files +/tests_output/ # custom test output directory +/samples_output/ # custom sample output directory diff --git a/options/gitignore/Haxe b/options/gitignore/Haxe new file mode 100644 index 0000000000..efafc9e940 --- /dev/null +++ b/options/gitignore/Haxe @@ -0,0 +1,3 @@ +.haxelib/ +.haxelsp/recording/ +dump/ diff --git a/options/gitignore/IAR b/options/gitignore/IAR index e8938b31a4..cb3fdf2cf0 100644 --- a/options/gitignore/IAR +++ b/options/gitignore/IAR @@ -12,24 +12,24 @@ thumbs.db *.~* -# IAR Settings -**/settings/*.crun -**/settings/*.dbgdt -**/settings/*.cspy -**/settings/*.cspy.* -**/settings/*.xcl -**/settings/*.dni -**/settings/*.wsdt -**/settings/*.wspos +# IAR Settings +**/settings/*.crun +**/settings/*.dbgdt +**/settings/*.cspy +**/settings/*.cspy.* +**/settings/*.xcl +**/settings/*.dni +**/settings/*.wsdt +**/settings/*.wspos -# IAR Debug Exe -**/Exe/*.sim +# IAR Debug Exe +**/Exe/*.sim -# IAR Debug Obj -**/Obj/*.pbd -**/Obj/*.pbd.* -**/Obj/*.pbi -**/Obj/*.pbi.* +# IAR Debug Obj +**/Obj/*.pbd +**/Obj/*.pbd.* +**/Obj/*.pbi +**/Obj/*.pbi.* # IAR project "Debug" directory Debug/ diff --git a/options/gitignore/JetBrains b/options/gitignore/JetBrains index 3649d6dc25..0c1302b4c6 100644 --- a/options/gitignore/JetBrains +++ b/options/gitignore/JetBrains @@ -1,4 +1,4 @@ -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff @@ -63,6 +63,7 @@ atlassian-ide-plugin.xml # SonarLint plugin .idea/sonarlint/ +.idea/sonarlint.xml # see https://community.sonarsource.com/t/is-the-file-idea-idea-idea-sonarlint-xml-intended-to-be-under-source-control/121119 # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml @@ -70,8 +71,16 @@ crashlytics.properties crashlytics-build.properties fabric.properties -# Editor-based Rest Client +# Editor-based HTTP Client .idea/httpRequests +http-client.private.env.json # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser + +# Apifox Helper cache +.idea/.cache/.Apifox_Helper +.idea/ApifoxUploaderProjectSetting.xml + +# Github Copilot persisted session migrations, see: https://github.com/microsoft/copilot-intellij-feedback/issues/712#issuecomment-3322062215 +.idea/**/copilot.data.migration.*.xml diff --git a/options/gitignore/Julia b/options/gitignore/Julia index 29126e47b0..285da1ecd2 100644 --- a/options/gitignore/Julia +++ b/options/gitignore/Julia @@ -21,4 +21,8 @@ docs/site/ # It records a fixed state of all packages used by the project. As such, it should not be # committed for packages, but should be committed for applications that require a static # environment. -Manifest.toml +Manifest*.toml + +# File generated by the Preferences package to store local preferences +LocalPreferences.toml +JuliaLocalPreferences.toml diff --git a/options/gitignore/JupyterNotebooks b/options/gitignore/JupyterNotebooks index f27f90d665..f45b39dedb 100644 --- a/options/gitignore/JupyterNotebooks +++ b/options/gitignore/JupyterNotebooks @@ -8,5 +8,9 @@ profile_default/ ipython_config.py +# Jupyter lab virtual documents +# https://jupyterlab-lsp.readthedocs.io/en/2.x/Configuring.html#virtual_documents_dir +.virtual_documents/ + # Remove previous ipynb_checkpoints # git rm -r .ipynb_checkpoints/ diff --git a/options/gitignore/Katalon b/options/gitignore/Katalon new file mode 100644 index 0000000000..73a4938fc6 --- /dev/null +++ b/options/gitignore/Katalon @@ -0,0 +1,40 @@ +# Katalon Test Suite +# Compiled class file +*.class +*.swp +output +!output/.gitkeep +build + +Libs/TempTestCase* +Libs/TempTestSuite* +bin/lib/TempTestCase* +Reports/ +\.classpath +\.project +\.settings/ +bin/lib/ +Libs/ +.svn/ +.gradle + + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/options/gitignore/KiCad b/options/gitignore/KiCad index a63bc0e7f7..9d5df933c9 100644 --- a/options/gitignore/KiCad +++ b/options/gitignore/KiCad @@ -8,14 +8,19 @@ *.kicad_pcb-bak *.kicad_sch-bak *-backups -*.kicad_prl -*.sch-bak +*-cache* +*-bak +*-bak* *~ +~* _autosave-* +\#auto_saved_files\# *.tmp *-save.pro *-save.kicad_pcb fp-info-cache +~*.lck +\#auto_saved_files# # Netlist files (exported from Eeschema) *.net @@ -27,3 +32,9 @@ fp-info-cache # Exported BOM files *.xml *.csv + +# Archived Backups (KiCad 6.0) +**/*-backups/*.zip + +# Local project settings +*.kicad_prl diff --git a/options/gitignore/Kotlin b/options/gitignore/Kotlin index 524f0963bd..566e06bf99 100644 --- a/options/gitignore/Kotlin +++ b/options/gitignore/Kotlin @@ -22,3 +22,6 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects +.kotlin/ \ No newline at end of file diff --git a/options/gitignore/LangChain b/options/gitignore/LangChain new file mode 100644 index 0000000000..c76ebfd922 --- /dev/null +++ b/options/gitignore/LangChain @@ -0,0 +1,6 @@ +# gitignore template for LangChain products, e.g., LangGraph, LangSmith +# website: https://www.langchain.com/ +# website: https://www.langchain.com/langgraph + +# LangGraph +.langgraph_api/ diff --git a/options/gitignore/Laravel b/options/gitignore/Laravel index 297959a19e..d5673e321c 100644 --- a/options/gitignore/Laravel +++ b/options/gitignore/Laravel @@ -21,3 +21,10 @@ Homestead.yaml Homestead.json /.vagrant .phpunit.result.cache + +/public/build +/storage/pail +.env.backup +.env.production +.phpactor.json +auth.json diff --git a/options/gitignore/Lefthook b/options/gitignore/Lefthook new file mode 100644 index 0000000000..35409f0e02 --- /dev/null +++ b/options/gitignore/Lefthook @@ -0,0 +1,16 @@ +# https://lefthook.dev/configuration/#config-file-name +/.lefthook-local.json +/.lefthook-local.toml +/.lefthook-local.yaml +/.lefthook-local.yml +/lefthook-local.json +/lefthook-local.toml +/lefthook-local.yaml +/lefthook-local.yml +/.config/lefthook-local.json +/.config/lefthook-local.toml +/.config/lefthook-local.yaml +/.config/lefthook-local.yml + +# https://lefthook.dev/configuration/source_dir_local.html +/.lefthook-local/ diff --git a/options/gitignore/Linux b/options/gitignore/Linux index b56bf65d85..35ea8c6723 100644 --- a/options/gitignore/Linux +++ b/options/gitignore/Linux @@ -3,7 +3,7 @@ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* -# KDE directory preferences +# Metadata left by Dolphin file manager, which comes with KDE Plasma .directory # Linux trash folder which might appear on any partition or disk @@ -11,3 +11,6 @@ # .nfs files are created when an open file is removed but is still being accessed .nfs* + +# Log files created by default by the nohup command +nohup.out diff --git a/options/gitignore/Luau b/options/gitignore/Luau new file mode 100644 index 0000000000..f7ecbc96ae --- /dev/null +++ b/options/gitignore/Luau @@ -0,0 +1,14 @@ +# A fast, small, safe, gradually typed embeddable scripting language derived from Lua +# +# https://github.com/luau-lang/luau +# https://luau.org/ + +# Code coverage +coverage.out + +# Profiling +profile.out +profile.svg + +# Time trace +trace.json diff --git a/options/gitignore/MATLAB b/options/gitignore/MATLAB index 01d02dd2e4..92061b1d8e 100644 --- a/options/gitignore/MATLAB +++ b/options/gitignore/MATLAB @@ -1,31 +1,33 @@ -# Windows default autosave extension +# Autosave files *.asv - -# OSX / *nix default autosave extension *.m~ +*.autosave +*.slx.r* +*.mdl.r* -# Compiled MEX binaries (all platforms) +# Derived content-obscured files +*.p + +# Compiled MEX files *.mex* # Packaged app and toolbox files *.mlappinstall *.mltbx +# Deployable archives +*.ctf + # Generated helpsearch folders helpsearch*/ -# Simulink code generation folders +# Code generation folders slprj/ sccprj/ - -# Matlab code generation folders codegen/ -# Simulink autosave extension -*.autosave - -# Simulink cache files +# Cache files *.slxc -# Octave session info -octave-workspace +# Cloud based storage dotfile +.MATLABDriveTag diff --git a/options/gitignore/Maven b/options/gitignore/Maven index 2f4353087f..6d706b8df1 100644 --- a/options/gitignore/Maven +++ b/options/gitignore/Maven @@ -7,7 +7,7 @@ release.properties dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar +# https://maven.apache.org/wrapper/#usage-without-binary-jar .mvn/wrapper/maven-wrapper.jar # Eclipse m2e generated files diff --git a/options/gitignore/MetaTrader5 b/options/gitignore/MetaTrader5 new file mode 100644 index 0000000000..0e235ca712 --- /dev/null +++ b/options/gitignore/MetaTrader5 @@ -0,0 +1,57 @@ +# MetaTrader 5 and MQL5 gitignore template +# Project homepage: https://www.metatrader5.com/en + +# Compiled MQL5 executables (binaries) +# These are generated from .mq5 source files and should not be committed. +*.ex5 +*.ex4 # For MQL4 compatibility if you also manage MT4 projects in a similar structure + +# Log files +# Terminal logs, strategy tester logs, and custom logs from Print() functions. +*.log +*.slog # Strategy Tester logs + +# Strategy Tester specific files +# History data, optimization results, and temporary files used by the tester. +*.fxt # FXT files (history data for testing) +*.hst # History data files (can be large) +*.ini # Initialization files (often generated by tester or EAs) +*.dat # Data files (various purposes, often temporary) +*.csv # CSV export files (e.g., from tester reports) +*.jrn # Journal files (tester journal) + +# Market Watch sets and profiles +# User-specific lists of symbols in Market Watch, and terminal profiles. +*.set # Market Watch symbol sets +*.tpl # Chart templates +*.chr # Chart settings files (can be generated when saving templates or profiles) + +# External libraries (DLLs) +# If you use custom DLLs, you might want to ignore them if they are built separately +# and not part of your MQL5 source code repository. +*.dll + +# User-specific configuration and credentials +# Files containing sensitive information or local user settings. +.env # Environment variables (e.g., for Python integration credentials) +*.cfg # Configuration files (if not meant to be shared) +*.json # Be careful: if you have config JSONs you *do* want to commit, add specific exceptions. + # Example: !config.json (to include config.json but ignore other *.json) + +# Temporary files and backup files generated by MetaEditor +*.~* # Temporary files (e.g., ~MyScript.mq5) +*.bak # Backup files (e.g., MyScript.mq5.bak) +*.mqh.bak +*.mq5.bak + +# MetaEditor project files +# Project files for MetaEditor workspaces. +.mqproj + +# Python specific ignores (if you also keep Python scripts or Jupyter notebooks in this repository) +# These are relevant if your Git repo root is higher up (e.g., the terminal folder itself) +# or if you mix Python code within your MQL5 structure. +__pycache__/ # Python compiled bytecode cache +.ipynb_checkpoints/ # Jupyter Notebook checkpoints +*.pyc # Python compiled files +*.pyd # Python dynamic modules diff --git a/options/gitignore/Metals b/options/gitignore/Metals index 516e7e33f2..779e796bb8 100644 --- a/options/gitignore/Metals +++ b/options/gitignore/Metals @@ -1,5 +1,6 @@ - # Generated Metals (Scala Language Server) files - # Reference: https://scalameta.org/metals/ +# Metals (Scala Language Server) +# Reference: https://scalameta.org/metals/docs/editors/vscode#files-and-directories-to-include-in-your-gitignore .metals/ .bloop/ -project/metals.sbt +.ammonite/ +metals.sbt diff --git a/options/gitignore/MicrosoftOffice b/options/gitignore/MicrosoftOffice index ddcc9cf6e3..6501a7d322 100644 --- a/options/gitignore/MicrosoftOffice +++ b/options/gitignore/MicrosoftOffice @@ -2,6 +2,7 @@ # Word temporary ~$*.doc* +~$*.dot* # Word Auto Backup File Backup of *.doc* diff --git a/options/gitignore/Modelica b/options/gitignore/Modelica new file mode 100644 index 0000000000..aa2cc996da --- /dev/null +++ b/options/gitignore/Modelica @@ -0,0 +1,42 @@ +# Modelica - an object-oriented language for modeling of cyber-physical systems +# https://modelica.org/ +# Ignore temporary files, build results, simulation files + +## Modelica-specific files +*~ +*.bak +*.bak-mo +*.mof +\#*\# +*.moe +*.mol + +## Build artefacts +*.exe +*.exp +*.o +*.pyc + +## Simulation files +*.mat + +## Package files +*.gz +*.rar +*.tar +*.zip + +## Dymola-specific files +buildlog.txt +dsfinal.txt +dsin.txt +dslog.txt +dsmodel* +dsres.txt +dymosim* +request +stat +status +stop +success +*. diff --git a/options/gitignore/Move b/options/gitignore/Move new file mode 100644 index 0000000000..b7d406e7bb --- /dev/null +++ b/options/gitignore/Move @@ -0,0 +1,6 @@ +# Generated by Move +# will have compiled files +build/ + +# Remove possibly saving credentials to the git repository +.aptos/ diff --git a/options/gitignore/NasaSpecsIntact b/options/gitignore/NasaSpecsIntact index be53af0e0a..626d18167e 100644 --- a/options/gitignore/NasaSpecsIntact +++ b/options/gitignore/NasaSpecsIntact @@ -1,9 +1,9 @@ # gitignore template for Nasa SpecsIntact (SI) # Website: https://specsintact.ksc.nasa.gov/ # -# Recommended: +# Recommended: # MicrosoftOffice.gitignore -# +# # SpecsIntact (SI) Locking file; this would lock everyone out. *.se$ @@ -20,14 +20,14 @@ SUBMVER.* TTLDIFFS.* # SpecsIntact files that change a lot and don't actually affect SI -# PULL.TBL is an auto-generated file to help speed SI loading. +# PULL.TBL is an auto-generated file to help speed SI loading. PULL.TBL pulltbl.bck # Tailoring information. # Keep tailor.tag; it is a list of tailoring options in SI. -# JOB.OTL informs SI where a spec section came from. +# JOB.OTL informs SI where a spec section came from. # Keeping the old one isn't useful in git. JOB.OTL.OLD @@ -35,6 +35,6 @@ JOB.OTL.OLD # notebooks, and if so, OneNote will litter the SI folder with these. *.onetoc* -# Log files, typically tagfix or other auto generated logs that aren't useful +# Log files, typically tagfix or other auto generated logs that aren't useful # outside of the user that made them and clutter up the index. *.log diff --git a/options/gitignore/Nestjs b/options/gitignore/Nestjs new file mode 100644 index 0000000000..845341e446 --- /dev/null +++ b/options/gitignore/Nestjs @@ -0,0 +1,24 @@ +# Nestjs specific +/dist +/node_modules +/build +/tmp + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# dotenv environment variable files +.env +.env.development +.env.test +.env.production + +# temp directory +.temp +.tmp diff --git a/options/gitignore/Nextjs b/options/gitignore/Nextjs new file mode 100644 index 0000000000..45c1abce86 --- /dev/null +++ b/options/gitignore/Nextjs @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/options/gitignore/Node b/options/gitignore/Node index c6bba59138..2aa8c99a54 100644 --- a/options/gitignore/Node +++ b/options/gitignore/Node @@ -5,7 +5,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* -.pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json @@ -57,12 +56,6 @@ web_modules/ # Optional stylelint cache .stylelintcache -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - # Optional REPL history .node_repl_history @@ -74,10 +67,8 @@ web_modules/ # dotenv environment variable files .env -.env.development.local -.env.test.local -.env.production.local -.env.local +.env.* +!.env.example # parcel-bundler cache (https://parceljs.org/) .cache @@ -90,6 +81,7 @@ out # Nuxt.js build / generate output .nuxt dist +.output # Gatsby files .cache/ @@ -104,6 +96,15 @@ dist .temp .cache +# Sveltekit cache directory +.svelte-kit/ + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + # Docusaurus cache and generated files .docusaurus @@ -116,15 +117,28 @@ dist # DynamoDB Local files .dynamodb/ +# Firebase cache directory +.firebase/ + # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz +# pnpm +.pnpm-store + +# yarn v3 .pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Vite files +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +.vite/ diff --git a/options/gitignore/NotesAndExtendedConfiguration b/options/gitignore/NotesAndExtendedConfiguration index 3e0804f299..58c3f71e48 100644 --- a/options/gitignore/NotesAndExtendedConfiguration +++ b/options/gitignore/NotesAndExtendedConfiguration @@ -30,9 +30,7 @@ # contain metadata (manifest.json), application code (main.js), stylesheets # (styles.css), and user-configuration data (data.json). # We only want to track data.json, so we: -# 1. exclude everything under the plugins directory recursively, -# 2. unignore the plugin directories themselves, which then allows us to -# 3. unignore the data.json files -.obsidian/plugins/**/* -!.obsidian/plugins/*/ +# 1. exclude everything that the plugin folders contain, +# 2. unignore data.json in the plugin folders +.obsidian/plugins/*/** !.obsidian/plugins/*/data.json diff --git a/options/gitignore/OCaml b/options/gitignore/OCaml index a18e08402b..250caf74e7 100644 --- a/options/gitignore/OCaml +++ b/options/gitignore/OCaml @@ -8,7 +8,14 @@ *.cmxs *.cmxa -# ocamlbuild working directory +# Files containing detailed information about the compilation (generated +# by `ocamlc`/`ocamlopt` when invoked using the option `-bin-annot`). +# These files are typically useful for code inspection tools +# (e.g. Merlin). +*.cmt +*.cmti + +# ocamlbuild and Dune default working directory _build/ # ocamlbuild targets diff --git a/options/gitignore/Octave b/options/gitignore/Octave index 01d02dd2e4..92061b1d8e 100644 --- a/options/gitignore/Octave +++ b/options/gitignore/Octave @@ -1,31 +1,33 @@ -# Windows default autosave extension +# Autosave files *.asv - -# OSX / *nix default autosave extension *.m~ +*.autosave +*.slx.r* +*.mdl.r* -# Compiled MEX binaries (all platforms) +# Derived content-obscured files +*.p + +# Compiled MEX files *.mex* # Packaged app and toolbox files *.mlappinstall *.mltbx +# Deployable archives +*.ctf + # Generated helpsearch folders helpsearch*/ -# Simulink code generation folders +# Code generation folders slprj/ sccprj/ - -# Matlab code generation folders codegen/ -# Simulink autosave extension -*.autosave - -# Simulink cache files +# Cache files *.slxc -# Octave session info -octave-workspace +# Cloud based storage dotfile +.MATLABDriveTag diff --git a/options/gitignore/OpenTofu b/options/gitignore/OpenTofu new file mode 100644 index 0000000000..8a7f7b76a1 --- /dev/null +++ b/options/gitignore/OpenTofu @@ -0,0 +1,42 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tofu +override.tf.json +override.tofu.json +*_override.tf +*_override.tofu +*_override.tf.json +*_override.tofu.json + +# Ignore transient lock info files created by tofu apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf +# !example_override.tofu + +# Include tfplan files to ignore the plan output of command: tofu plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/options/gitignore/Packer b/options/gitignore/Packer index 2cbc1ad079..caa24ec789 100644 --- a/options/gitignore/Packer +++ b/options/gitignore/Packer @@ -5,9 +5,9 @@ packer_cache/ crash.log # https://www.packer.io/guides/hcl/variables -# Exclude all .pkrvars.hcl files, which are likely to contain sensitive data, -# such as password, private keys, and other secrets. These should not be part of -# version control as they are data points which are potentially sensitive and +# Exclude all .pkrvars.hcl files, which are likely to contain sensitive data, +# such as password, private keys, and other secrets. These should not be part of +# version control as they are data points which are potentially sensitive and # subject to change depending on the environment. # *.pkrvars.hcl diff --git a/options/gitignore/Perl b/options/gitignore/Perl index fb8b193173..79d77ce623 100644 --- a/options/gitignore/Perl +++ b/options/gitignore/Perl @@ -33,3 +33,9 @@ inc/ /MANIFEST.bak /pm_to_blib /*.zip + +# Carton/Carmel +/local/ +/.carmel/ +# cpanfile.snapshot should generally be ignored for library code, otherwise included +# cpanfile.snapshot diff --git a/options/gitignore/PlatformIO b/options/gitignore/PlatformIO new file mode 100644 index 0000000000..2de98aba16 --- /dev/null +++ b/options/gitignore/PlatformIO @@ -0,0 +1,6 @@ +.pio +.pioenvs +.piolibdeps +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json diff --git a/options/gitignore/Python b/options/gitignore/Python index 82f927558a..e15106e38f 100644 --- a/options/gitignore/Python +++ b/options/gitignore/Python @@ -1,6 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ -*.py[cod] +*.py[codz] *$py.class # C extensions @@ -27,8 +27,8 @@ share/python-wheels/ MANIFEST # PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec @@ -46,7 +46,7 @@ htmlcov/ nosetests.xml coverage.xml *.cover -*.py,cover +*.py.cover .hypothesis/ .pytest_cache/ cover/ @@ -92,25 +92,38 @@ ipython_config.py # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. -#Pipfile.lock +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock +# poetry.lock +# poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml .pdm-python .pdm-build/ +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -118,11 +131,25 @@ __pypackages__/ celerybeat-schedule celerybeat.pid +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + # SageMath parsed files *.sage.py # Environments .env +.envrc .venv env/ venv/ @@ -155,8 +182,35 @@ dmypy.json cython_debug/ # PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml diff --git a/options/gitignore/Rust b/options/gitignore/Rust index d01bd1a990..ad67955886 100644 --- a/options/gitignore/Rust +++ b/options/gitignore/Rust @@ -1,11 +1,7 @@ # Generated by Cargo # will have compiled files and executables -debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock +debug +target # These are backup files generated by rustfmt **/*.rs.bk @@ -13,9 +9,13 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out*/ + # RustRover # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ diff --git a/options/gitignore/SBT b/options/gitignore/SBT index 5ed6acb657..98ee5070d7 100644 --- a/options/gitignore/SBT +++ b/options/gitignore/SBT @@ -10,3 +10,4 @@ project/plugins/project/ .history .cache .lib/ +.bsp/ diff --git a/options/gitignore/SSDT-sqlproj b/options/gitignore/SSDT-sqlproj new file mode 100644 index 0000000000..36c16598dd --- /dev/null +++ b/options/gitignore/SSDT-sqlproj @@ -0,0 +1,31 @@ +## Ignore Visual Studio SSDT sqlproj specific temporary files, build results, etc +## +## +## Get latest from https://github.com/github/gitignore/blob/master/SSDT-sqlproj.gitignore +# Build output +bin/ +obj/ + +# DACPAC files +*.dacpac + +# Publish profiles (optional, if environment-specific) +*.publish.xml + +# SQL Server debug files +*.dbmdl +*.sqlcmdvars + +# Visual Studio settings +.vs/ + +# User-specific files +*.user +*.suo +*.userosscache +*.sln.docstates + +# Backup files +*.bak +*.log + diff --git a/options/gitignore/STM32CubeIDE b/options/gitignore/STM32CubeIDE new file mode 100644 index 0000000000..fc7ee5c7fd --- /dev/null +++ b/options/gitignore/STM32CubeIDE @@ -0,0 +1,51 @@ +# STM32CubeIDE specific files + +# Project-specific settings. Ignore it if developers have different preferences. +# However, if you want all team members to use the same code formatting and settings, +# consider including the .settings folder in the repository. + +# /.settings/ + +# Ignore Eclipse-based IDE launch configurations. +# Uncomment if you want each developer to have their own unique debug configurations. +#*.launch + +# Ignore any JLink-related files (debug configurations). +# Uncomment if you want each developer to have their own unique debug configurations. +#*.jlink + +# Ignore log files generated by the IDE. +# These are not necessary for version control. +*.log + +# Build files + +# Ignore build output directories. +# These are not needed in the repository as they are generated during the build process. +Debug/ +Release/ + +# Ignore common binary and object files generated during compilation. +# They should not be included in the repository as they are build artifacts. +*.elf +*.map +*.bin +*.hex +*.srec +*.lst +*.o +*.d +*.a +*.su +*.crl + +#TouchGFX files (in case your project has touchGFX) +TouchGFX/generated +TouchGFX/build +TouchGFX/simulator/msvs/.vs + +# Backup files + +# Ignore temporary and backup files generated by the operating system and editor. +# These are not needed in the repository. +*.bak diff --git a/options/gitignore/Salesforce b/options/gitignore/Salesforce new file mode 100644 index 0000000000..3547a96f6d --- /dev/null +++ b/options/gitignore/Salesforce @@ -0,0 +1,39 @@ +# This file is used for Git repositories to specify intentionally untracked files that Git should ignore. +# If you are not using git, you can delete this file. For more information see: https://git-scm.com/docs/gitignore +# For useful gitignore templates see: https://github.com/github/gitignore + +# Salesforce cache +.sf/ +.sfdx/ +.localdevserver/ +deploy-options.json +.localdev + +# LWC VSCode autocomplete +**/lwc/jsconfig.json + +# LWC Jest coverage reports +coverage/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Eslint cache +.eslintcache + +# Windows system files +Thumbs.db +ehthumbs.db +[Dd]esktop.ini +$RECYCLE.BIN/ + +# Salesforce Analyzer results +sca-results.csv +sfca_results.json + +# Local environment variables +.env diff --git a/options/gitignore/Solidity-Remix b/options/gitignore/Solidity-Remix new file mode 100644 index 0000000000..a49555a43a --- /dev/null +++ b/options/gitignore/Solidity-Remix @@ -0,0 +1,15 @@ +# Remix compiler artifacts +**/artifacts/ +**/artifacts/** + +# Remix plugin state folders +deps/ +states/ + +# Debug info +*.dbg.json +*.tsbuildinfo + +# Optional +.env +.env.local \ No newline at end of file diff --git a/options/gitignore/Stata b/options/gitignore/Stata index 07997bb120..288abf8a15 100644 --- a/options/gitignore/Stata +++ b/options/gitignore/Stata @@ -8,6 +8,7 @@ *.smcl *.stpr *.stsem +~*.stswp # Graphic export files from Stata # Stata command graph export: http://www.stata.com/manuals14/g-2graphexport.pdf diff --git a/options/gitignore/TeX b/options/gitignore/TeX index a1f5212090..9308a4b649 100644 --- a/options/gitignore/TeX +++ b/options/gitignore/TeX @@ -26,7 +26,9 @@ ## Bibliography auxiliary files (bibtex/biblatex/biber): *.bbl +*.bbl-SAVE-ERROR *.bcf +*.bcf-SAVE-ERROR *.blg *-blx.aux *-blx.bib @@ -57,6 +59,9 @@ acs-*.bib # amsthm *.thm +# attachfile2 +*.atfi + # beamer *.nav *.pre @@ -65,6 +70,7 @@ acs-*.bib # changes *.soc +*.loc # comment *.cut @@ -108,8 +114,11 @@ acs-*.bib *.acn *.acr *.glg +*.glg-abr *.glo +*.glo-abr *.gls +*.gls-abr *.glsdefs *.lzo *.lzs @@ -152,6 +161,9 @@ acs-*.bib # *.tikz *-tikzDictionary +# latexindent will create succesive backup files by default +#*.bak* + # listings *.lol @@ -174,6 +186,7 @@ acs-*.bib # minted _minted* +*.data.minted *.pyg # morewrites @@ -201,6 +214,10 @@ _minted* # scrwfile *.wrt +# spelling +*.spell.bad +*.spell.txt + # svg svg-inkscape/ @@ -266,6 +283,9 @@ TSWLatexianTemp* *.bak *.sav +# latexindent.pl +*.bak[0-9]* + # Texpad .texpadtmp diff --git a/options/gitignore/Terraform b/options/gitignore/Terraform index 2faf43d0a1..78e7733b54 100644 --- a/options/gitignore/Terraform +++ b/options/gitignore/Terraform @@ -1,5 +1,5 @@ # Local .terraform directories -**/.terraform/* +.terraform/ # .tfstate files *.tfstate @@ -10,8 +10,8 @@ crash.log crash.*.log # Exclude all .tfvars files, which are likely to contain sensitive data, such as -# password, private keys, and other secrets. These should not be part of version -# control as they are data points which are potentially sensitive and subject +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject # to change depending on the environment. *.tfvars *.tfvars.json @@ -35,3 +35,10 @@ override.tf.json # Ignore CLI configuration files .terraformrc terraform.rc + +# Optional: ignore graph output files generated by `terraform graph` +# *.dot + +# Optional: ignore plan files saved before destroying Terraform configuration +# Uncomment the line below if you want to ignore planout files. +# planout \ No newline at end of file diff --git a/options/gitignore/TestComplete b/options/gitignore/TestComplete new file mode 100644 index 0000000000..8378b9eedb --- /dev/null +++ b/options/gitignore/TestComplete @@ -0,0 +1,14 @@ +# Test Complete ignore files: https://support.smartbear.com/viewarticle/68002/ + +# Tester-specific Settings +*.tcCFGExtender +*.tcLS + +# Type library declarations +*.tlb + +# Log files +*.tcLogs + +# Backup files +*.bak diff --git a/options/gitignore/TwinCAT3 b/options/gitignore/TwinCAT3 index 7bd6f87505..6786d5e833 100644 --- a/options/gitignore/TwinCAT3 +++ b/options/gitignore/TwinCAT3 @@ -1,25 +1,57 @@ -# gitignore template for TwinCAT3 +### TwinCAT3 ### # website: https://www.beckhoff.com/twincat3/ -# -# Recommended: VisualStudio.gitignore -# TwinCAT files +# TwinCAT PLC +*.plcproj.bak +*.plcproj.orig *.tpy *.tclrs +*.library *.compiled-library *.compileinfo -# Don't include the tmc-file rule if either of the following is true: -# 1. You've got TwinCAT C++ projects, as the information in the TMC-file is created manually for the C++ projects (in that case, only (manually) ignore the tmc-files for the PLC projects) -# 2. You've created a standalone PLC-project and added events to it, as these are stored in the TMC-file. -*.tmc -*.tmcRefac -*.library -*.project.~u -*.tsproj.bak -*.xti.bak +*.asm +*.core LineIDs.dbg LineIDs.dbg.bak -_Boot/ -_CompileInfo/ -_Libraries/ -_ModuleInstall/ \ No newline at end of file + +# TwinCAT C++ and shared types +# ignoring the TMC file is only useful for plain PLC programming +# as soon as shared data types (via tmc), C++ or in general TcCom-Module are used, the TMC file has to be part of the repository +*.tmc +*.tmcRefac + +# TwinCAT project files +*.tsproj.bak +*.tsproj.b?k +*.tsproj.orig +*.tspproj.bak +*.xti.bak +*.xti.bk? +*.xti.orig +*.xtv +*.xtv.bak +*.xtv.bk? +*.xt?.bk? +*.xt?.orig + +# Multiuser specific +**/.TcGit/ + +# exclude not required folders +**/_Boot/ +**/_CompileInfo/ +**/_Libraries/ +**/_ModuleInstall/ +**/_Deployment/ +**/_Repository/ + + +# To include a specific library directory (i.e. third party/custom libs), +# use pattern `!/**/_Libraries//` i.e. `!/**/_Libraries/www.tcunit.org/` +# + +# VS Shell project specific files and folders +**/.vs/ +*.~u +*.project.~u +*.suo diff --git a/options/gitignore/UTAU b/options/gitignore/UTAU new file mode 100644 index 0000000000..173bc781f1 --- /dev/null +++ b/options/gitignore/UTAU @@ -0,0 +1,52 @@ +# Adobe Audition +*.pkf + +# UTAU Engines +*.ctspec +*.d4c +*.dio +*.frc +*.frt +*.frq +*.harvest +*.lessaudio +*.llsm +*.mrq +*.pitchtier +*.platinum +*.pmk +*.sc.npz +*.star +*.uspec +*.vs4ufrq + +# UTAU related tools +$read +*.setParam-Scache +*.lbp +*.lbp.caches/* + +# OpenUtau +errors.txt + +# Deepvocal +*.DVModel +*-log.txt +SKC +SKI +SKC_1 +SKC_2 +*.sksd + +# VocalSharp +*.scep +*.vssf +*.vsdx +*.vsdxindex + +# Binary Archive +*.7z +*.zip +*.rar +*.exe + diff --git a/options/gitignore/UiPath b/options/gitignore/UiPath index f0c2267b89..0948dcc846 100644 --- a/options/gitignore/UiPath +++ b/options/gitignore/UiPath @@ -1,4 +1,4 @@ -# gitignore template for RPA development using UiPath Studio +# gitignore template for RPA development using UiPath Studio # website: https://www.uipath.com/product/studio # # Recommended: n/a diff --git a/options/gitignore/Umbraco b/options/gitignore/Umbraco index 1dc3da526c..260c741209 100644 --- a/options/gitignore/Umbraco +++ b/options/gitignore/Umbraco @@ -21,7 +21,7 @@ ## The [Mm]edia/ folder contains content. Content may vary by environment and should therefore not be added to source control. ## Uncomment this line if you think it fits the way you work on your project. -## **/[Mm]edia/ +## **/[Mm]edia/ # Don't ignore Umbraco packages (VisualStudio.gitignore mistakes this for a NuGet packages folder) # Make sure to include details from VisualStudio.gitignore BEFORE this @@ -43,7 +43,7 @@ *.sqlite.db* #ignore umbraco data/views/settings -**/umbraco/ +**/umbraco/* #include default location for modelsbuilder output !**/umbraco/models diff --git a/options/gitignore/Unity b/options/gitignore/Unity index 58cbc8256e..9eb70ce1f5 100644 --- a/options/gitignore/Unity +++ b/options/gitignore/Unity @@ -2,6 +2,7 @@ # # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore # +.utmp/ /[Ll]ibrary/ /[Tt]emp/ /[Oo]bj/ @@ -9,6 +10,11 @@ /[Bb]uilds/ /[Ll]ogs/ /[Uu]ser[Ss]ettings/ +*.log + +# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control. +*.blend1 +*.blend1.meta # MemoryCaptures can get excessive in size. # They also could contain extremely sensitive data @@ -22,6 +28,8 @@ # Autogenerated Jetbrains Rider plugin /[Aa]ssets/Plugins/Editor/JetBrains* +# Jetbrains Rider personal-layer settings +*.DotSettings.user # Visual Studio cache directory .vs/ @@ -55,18 +63,37 @@ ExportedObj/ # Unity3D generated file on crash reports sysinfo.txt +# Mono auto generated files +mono_crash.* + # Builds *.apk *.aab *.unitypackage +*.unitypackage.meta *.app # Crashlytics generated file crashlytics-build.properties -# Packed Addressables -/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* +# TestRunner generated files +InitTestScene*.unity* -# Temporary auto-generated Android Assets -/[Aa]ssets/[Ss]treamingAssets/aa.meta -/[Aa]ssets/[Ss]treamingAssets/aa/* +# Addressables default ignores, before user customizations +/ServerData +/[Aa]ssets/StreamingAssets/aa* +/[Aa]ssets/AddressableAssetsData/link.xml* +/[Aa]ssets/Addressables_Temp* +# By default, Addressables content builds will generate addressables_content_state.bin +# files in platform-specific subfolders, for example: +# /Assets/AddressableAssetsData/OSX/addressables_content_state.bin +/[Aa]ssets/AddressableAssetsData/*/*.bin* + +# Visual Scripting auto-generated files +/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db +/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta +/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers +/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta + +# Auto-generated scenes by play mode tests +/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity* diff --git a/options/gitignore/UnrealEngine b/options/gitignore/UnrealEngine index 6e0d95fb31..b70ad5aae2 100644 --- a/options/gitignore/UnrealEngine +++ b/options/gitignore/UnrealEngine @@ -40,6 +40,7 @@ *.sdf *.VC.db *.VC.opendb +.vsconfig # Precompiled Assets SourceArt/**/*.png diff --git a/options/gitignore/VBA b/options/gitignore/VBA new file mode 100644 index 0000000000..710dab19de --- /dev/null +++ b/options/gitignore/VBA @@ -0,0 +1,40 @@ + +# Office temporary files +~$* + +# Access database lock files (laccdb, ldb) +*.[lL][aA][cC][cC][dD][bB] +*.[lL][dD][bB] + +# The following sections constitute a list of Office file extensions that support VBA. +# If you want to exclude Office files from your repo, uncomment the corresponding file extensions. + +# Excel (xls, xlsb, xlsm, xlt, xltm, xla, xlam) +#*.[xX][lL][sS] +#*.[xX][lL][sS][bB] +#*.[xX][lL][sS][mM] +#*.[xX][lL][tT] +#*.[xX][lL][tT][mM] +#*.[xX][lL][aA] +#*.[xX][lL][aA][mM] + +# Word (doc, docm, dot, dotm) +#*.[dD][oO][cC] +#*.[dD][oO][cC][mM] +#*.[dD][oO][tT] +#*.[dD][oO][tT][mM] + +# Access (accda, accdb, accde, mdb, mde) +#*.[aA][cC][cC][dD][aA] +#*.[aA][cC][cC][dD][bB] +#*.[aA][cC][cC][dD][eE] +#*.[mM][dD][bB] +#*.[mM][dD][eE] + +# PowerPoint (ppt, pptm, pot, potm, pps, ppsm) +#*.[pP][pP][tT] +#*.[pP][pP][tT][mM] +#*.[pP][oO][tT] +#*.[pP][oO][tT][mM] +#*.[pP][pP][sS] +#*.[pP][pP][sS][mM] diff --git a/options/gitignore/Vim b/options/gitignore/Vim index 19fa63264c..cb8a049960 100644 --- a/options/gitignore/Vim +++ b/options/gitignore/Vim @@ -1,6 +1,7 @@ # Swap [._]*.s[a-v][a-z] -!*.svg # comment out if you don't need vector files +# comment out the next line if you don't need vector files +!*.svg [._]*.sw[a-p] [._]s[a-rt-v][a-z] [._]ss[a-gi-z] diff --git a/options/gitignore/VirtualEnv b/options/gitignore/VirtualEnv index b2c22f2af7..d895d00efe 100644 --- a/options/gitignore/VirtualEnv +++ b/options/gitignore/VirtualEnv @@ -1,5 +1,5 @@ # Virtualenv -# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +# https://realpython.com/python-virtual-environments-a-primer/#the-virtualenv-project .Python [Bb]in [Ii]nclude diff --git a/options/gitignore/VisualStudio b/options/gitignore/VisualStudio index 8a30d258ed..47a94ef17f 100644 --- a/options/gitignore/VisualStudio +++ b/options/gitignore/VisualStudio @@ -9,6 +9,7 @@ *.user *.userosscache *.sln.docstates +*.env # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs @@ -21,17 +22,37 @@ mono_crash.* [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ -x64/ -x86/ + +[Dd]ebug/x64/ +[Dd]ebugPublic/x64/ +[Rr]elease/x64/ +[Rr]eleases/x64/ +bin/x64/ +obj/x64/ + +[Dd]ebug/x86/ +[Dd]ebugPublic/x86/ +[Rr]elease/x86/ +[Rr]eleases/x86/ +bin/x86/ +obj/x86/ + [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ bld/ -[Bb]in/ [Oo]bj/ +[Oo]ut/ [Ll]og/ [Ll]ogs/ +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot @@ -43,12 +64,16 @@ Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* +*.trx # NUnit *.VisualState.xml TestResult.xml nunit-*.xml +# Approval Tests result files +*.received.* + # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ @@ -75,6 +100,7 @@ StyleCopReport.xml *.ilk *.meta *.obj +*.idb *.iobj *.pch *.pdb @@ -82,6 +108,8 @@ StyleCopReport.xml *.pgc *.pgd *.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp *.sbr *.tlb *.tli @@ -153,6 +181,7 @@ coverage*.info # NCrunch _NCrunch_* +.NCrunch_* .*crunch*.local.xml nCrunchTemp_* @@ -294,9 +323,6 @@ node_modules/ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - # Visual Studio 6 workspace and project file (working project files containing files to include in project) *.dsw *.dsp @@ -314,22 +340,22 @@ node_modules/ _Pvt_Extensions # Paket dependency manager -.paket/paket.exe +**/.paket/paket.exe paket-files/ # FAKE - F# Make -.fake/ +**/.fake/ # CodeRush personal settings -.cr/personal +**/.cr/personal # Python Tools for Visual Studio (PTVS) -__pycache__/ +**/__pycache__/ *.pyc # Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +#tools/** +#!tools/packages.config # Tabs Studio *.tss @@ -351,15 +377,19 @@ ASALocalRun/ # MSBuild Binary and Structured Log *.binlog +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder -.mfractor/ +**/.mfractor/ # Local History for Visual Studio -.localhistory/ +**/.localhistory/ # Visual Studio History (VSHistory) files .vshistory/ @@ -371,7 +401,7 @@ healthchecksdb MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder -.ionide/ +**/.ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd @@ -382,17 +412,17 @@ FodyWeavers.xsd !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -*.code-workspace +!.vscode/*.code-snippets # Local History for Visual Studio Code .history/ +# Built Visual Studio Code Extensions +*.vsix + # Windows Installer files from build outputs *.cab *.msi *.msix *.msm *.msp - -# JetBrains Rider -*.sln.iml diff --git a/options/gitignore/VisualStudioCode b/options/gitignore/VisualStudioCode index 45fce1d71c..b72ba8b5bd 100644 --- a/options/gitignore/VisualStudioCode +++ b/options/gitignore/VisualStudioCode @@ -4,9 +4,7 @@ !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets - -# Local History for Visual Studio Code -.history/ +!*.code-workspace # Built Visual Studio Code Extensions *.vsix diff --git a/options/gitignore/Zig b/options/gitignore/Zig index 748837a058..0180838aed 100644 --- a/options/gitignore/Zig +++ b/options/gitignore/Zig @@ -1,5 +1,3 @@ .zig-cache/ zig-out/ -build/ -build-*/ -docgen_tmp/ \ No newline at end of file +*.o diff --git a/options/gitignore/libogc b/options/gitignore/libogc new file mode 100644 index 0000000000..facd77526f --- /dev/null +++ b/options/gitignore/libogc @@ -0,0 +1,91 @@ +# Ignore build directories +build/ + +# Ignore Wii-specific metadata files +meta.xml +icon.png + + +# Ignore editor or IDE-specific files +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + +# Ignore backup or temporary files +*~ +*.bak +*.swp +*.tmp + +# Ignore log files +*.log + +# Ignore libraries and dependencies +lib/ +deps/ +obj/ + +# Ignore operating system-specific files +$RECYCLE.BIN/ +.Trash-1000/ +.Spotlight-V100/ +.fseventsd/ +.DS_Store + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf +*.o +*.bin + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex +*.dol +*.elf + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf diff --git a/options/gitignore/macOS b/options/gitignore/macOS index 135767fc07..a4557fbaea 100644 --- a/options/gitignore/macOS +++ b/options/gitignore/macOS @@ -1,10 +1,9 @@ # General .DS_Store +__MACOSX/ .AppleDouble .LSOverride - -# Icon must end with two \r -Icon +Icon[ ] # Thumbnails ._* diff --git a/options/gitignore/mise b/options/gitignore/mise new file mode 100644 index 0000000000..2f44750e3a --- /dev/null +++ b/options/gitignore/mise @@ -0,0 +1,11 @@ +# https://mise.jdx.dev/configuration.html +# https://mise.jdx.dev/configuration/environments.html +.mise.*.local.toml +.mise.local.toml +mise.*.local.toml +mise.local.toml +.mise/*.local.toml +mise/*.local.toml + +# https://mise.jdx.dev/configuration.html#tool-versions +#.tool-versions diff --git a/options/license/ALGLIB-Documentation b/options/license/ALGLIB-Documentation new file mode 100644 index 0000000000..6b966d4b41 --- /dev/null +++ b/options/license/ALGLIB-Documentation @@ -0,0 +1,17 @@ + Copyright 1994-2009 Sergey Bochkanov, ALGLIB Project. All rights reserved. + +Redistribution and use of this document (ALGLIB Reference Manual) with or +without modification, are permitted provided that such redistributions will +retain the above copyright notice, this condition and the following disclaimer +as the first (or last) lines of this file. + +THIS DOCUMENTATION IS PROVIDED BY THE ALGLIB PROJECT "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE ALGLIB PROJECT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/options/license/Advanced-Cryptics-Dictionary b/options/license/Advanced-Cryptics-Dictionary new file mode 100644 index 0000000000..904b942ae6 --- /dev/null +++ b/options/license/Advanced-Cryptics-Dictionary @@ -0,0 +1,10 @@ +License text: Copyright (c) J Ross Beresford 1993-1999. All Rights Reserved. + +The following restriction is placed on the use of this publication: +if The UK Advanced Cryptics Dictionary is used in a software package +or redistributed in any form, the copyright notice must be +prominently displayed and the text of this document must be included +verbatim. + +There are no other restrictions: I would like to see the list +distributed as widely as possible. diff --git a/options/license/Artistic-dist b/options/license/Artistic-dist new file mode 100644 index 0000000000..ff0b79e470 --- /dev/null +++ b/options/license/Artistic-dist @@ -0,0 +1,125 @@ +The "Artistic License" + + Preamble + +The intent of this document is to state the conditions under which a +Package may be copied, such that the Copyright Holder maintains some +semblance of artistic control over the development of the Package, +while giving the users of the package the right to use and distribute +the Package in a more-or-less customary fashion, plus the right to make +reasonable modifications. + +It also grants you the rights to reuse parts of a Package in your own +programs without transferring this License to those programs, provided +that you meet some reasonable requirements. + +Definitions: + + "Package" refers to the collection of files distributed by the + Copyright Holder, and derivatives of that collection of files + created through textual modification. + + "Standard Version" refers to such a Package if it has not been + modified, or has been modified in accordance with the wishes + of the Copyright Holder as specified below. + + "Copyright Holder" is whoever is named in the copyright or + copyrights for the package. + + "You" is you, if you're thinking about copying or distributing + this Package. + + "Reasonable copying fee" is whatever you can justify on the + basis of media cost, duplication charges, time of people involved, + and so on. (You will not be required to justify it to the + Copyright Holder, but only to the computing community at large + as a market that must bear the fee.) + + "Freely Available" means that no fee is charged for the item + itself, though there may be fees involved in handling the item. + It also means that recipients of the item may redistribute it + under the same conditions they received it. + +1. You may make and give away verbatim copies of the source form of the +Standard Version of this Package without restriction, provided that you +duplicate all of the original copyright notices and associated disclaimers. + +2. You may apply bug fixes, portability fixes and other modifications +derived from the Public Domain or from the Copyright Holder. A Package +modified in such a way shall still be considered the Standard Version. + +3. You may otherwise modify your copy of this Package in any way, provided +that you insert a prominent notice in each changed file stating how and +when you changed that file, and provided that you do at least ONE of the +following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or + an equivalent medium, or placing the modifications on a major archive + site such as uunet.uu.net, or by allowing the Copyright Holder to include + your modifications in the Standard Version of the Package. + + b) use the modified Package only within your corporation or organization. + + c) rename any non-standard executables so the names do not conflict + with standard executables, which must also be provided, and provide + a separate manual page for each non-standard executable that clearly + documents how it differs from the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +4. You may distribute the programs of this Package in object code or +executable form, provided that you do at least ONE of the following: + + a) distribute a Standard Version of the executables and library files, + together with instructions (in the manual page or equivalent) on where + to get the Standard Version. + + b) accompany the distribution with the machine-readable source of + the Package with your modifications. + + c) give non-standard executables non-standard names, and clearly + document the differences in manual pages (or equivalent), together + with instructions on where to get the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +5. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this +Package. You may not charge a fee for this Package itself. However, +you may distribute this Package in aggregate with other (possibly +commercial) programs as part of a larger (possibly commercial) software +distribution provided that you do not advertise this Package as a +product of your own. + +6. The scripts and library files supplied as input to or produced as +output from the programs of this Package do not automatically fall +under the copyright of this Package, but belong to whoever generated +them, and may be sold commercially, and may be aggregated with this +Package. If such scripts or library files are aggregated with this +Package via the so-called "undump" or "unexec" methods of producing a +binary executable image, then distribution of such an image shall +neither be construed as a distribution of this Package nor shall it +fall under the restrictions of Paragraphs 3 and 4, provided that you do +not represent such an executable image as a Standard Version of this +Package. + +7. You may reuse parts of this Package in your own programs, provided that +you explicitly state where you got them from, in the source code (and, left +to your courtesy, in the documentation), duplicating all the associated +copyright notices and disclaimers. Besides your changes, if any, must be +clearly marked as such. Parts reused that way will no longer fall under this +license if, and only if, the name of your program(s) have no immediate +connection with the name of the Package itself or its associated programs. +You may then apply whatever restrictions you wish on the reused parts or +choose to place them in the Public Domain--this will apply only within the +context of your package. + +8. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written permission. + +9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +THE END diff --git a/options/license/Aspell-RU b/options/license/Aspell-RU new file mode 100644 index 0000000000..5cec17629b --- /dev/null +++ b/options/license/Aspell-RU @@ -0,0 +1,4 @@ +Permission to use, copy, redistribute is granted. +Permission to redistribute modifications in patch form is granted. +Permission to redistribute binaries made of modified sources is granted. +All other rights reserved. diff --git a/options/license/BSD-2-Clause-pkgconf-disclaimer b/options/license/BSD-2-Clause-pkgconf-disclaimer new file mode 100644 index 0000000000..4c3b8ae80e --- /dev/null +++ b/options/license/BSD-2-Clause-pkgconf-disclaimer @@ -0,0 +1,15 @@ +Copyright © 2001-2025 Audacious developers and others + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions, and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions, and the following disclaimer in the + documentation provided with the distribution. + +This software is provided “as is” and without any warranty, express or +implied. In no event shall the authors be liable for any damages arising +from the use of this software. diff --git a/options/license/BSD-3-Clause-Tso b/options/license/BSD-3-Clause-Tso new file mode 100644 index 0000000000..3f70dabf39 --- /dev/null +++ b/options/license/BSD-3-Clause-Tso @@ -0,0 +1,11 @@ +Copyright Theodore Ts'o, 1994, 1995, 1996, 1997, 1998, 1999. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, and the entire permission notice in its entirety, including the disclaimer of warranties. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ALL OF WHICH ARE HEREBY DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF NOT ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/options/license/BSD-Mark-Modifications b/options/license/BSD-Mark-Modifications new file mode 100644 index 0000000000..995bcb1eb9 --- /dev/null +++ b/options/license/BSD-Mark-Modifications @@ -0,0 +1,35 @@ +License text: Copyright 1993, Geoff Kuenning, Granada Hills, CA +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. All modifications to the source code must be clearly marked as +such. Binary redistributions based on modified source code +must be clearly marked as modified versions in the documentation +and/or other materials provided with the distribution. +(clause 4 removed with permission from Geoff Kuenning) + +4. The name of Geoff Kuenning may not be used to endorse or promote +products derived from this software without specific prior +written permission. + +THIS SOFTWARE IS PROVIDED BY GEOFF KUENNING AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL GEOFF KUENNING OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/options/license/Boehm-GC-without-fee b/options/license/Boehm-GC-without-fee new file mode 100644 index 0000000000..354d47017e --- /dev/null +++ b/options/license/Boehm-GC-without-fee @@ -0,0 +1,14 @@ +Copyright (c) 2000 +SWsoft company + +Modifications copyright (c) 2001, 2013. Oracle and/or its affiliates. +All rights reserved. + +This material is provided "as is", with absolutely no warranty expressed +or implied. Any use is at your own risk. + +Permission to use or copy this software for any purpose is hereby granted +without fee, provided the above notices are retained on all copies. +Permission to modify the code and to distribute modified code is granted, +provided the above notices are retained, and a notice that the code was +modified is included with the above copyright notice. diff --git a/options/license/Buddy b/options/license/Buddy new file mode 100644 index 0000000000..fe750b2e32 --- /dev/null +++ b/options/license/Buddy @@ -0,0 +1,26 @@ + Copyright (C) 1996-2002 by Jorn Lind-Nielsen + All rights reserved + +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, reproduce, prepare derivative +works, distribute, and display this software and its documentation +for any purpose, provided that (1) the above copyright notice and +the following two paragraphs appear in all copies of the source code +and (2) redistributions, including without limitation binaries, +reproduce these notices in the supporting documentation. Substantial +modifications to this software may be copyrighted by their authors +and need not follow the licensing terms described here, provided +that the new terms are clearly indicated in all files where they apply. + +IN NO EVENT SHALL JORN LIND-NIELSEN, OR DISTRIBUTORS OF THIS +SOFTWARE BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, +INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS +SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE AUTHORS OR ANY OF THE +ABOVE PARTIES HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +JORN LIND-NIELSEN SPECIFICALLY DISCLAIM ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO +OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR +MODIFICATIONS. diff --git a/options/license/CAPEC-tou b/options/license/CAPEC-tou new file mode 100644 index 0000000000..fe6ddd466e --- /dev/null +++ b/options/license/CAPEC-tou @@ -0,0 +1,6 @@ +LICENSE +The MITRE Corporation (MITRE) hereby grants you a non-exclusive, royalty-free license to use Common Attack Pattern Enumeration and Classification (CAPEC™) for research, development, and commercial purposes. Any copy you make for such purposes is authorized provided that you reproduce MITRE’s copyright designation and this license in any such copy. + +DISCLAIMERS + +ALL DOCUMENTS AND THE INFORMATION CONTAINED THEREIN ARE PROVIDED ON AN "AS IS" BASIS AND THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS OR IS SPONSORED BY (IF ANY), THE MITRE CORPORATION, ITS BOARD OF TRUSTEES, OFFICERS, AGENTS, AND EMPLOYEES, DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION THEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. diff --git a/options/license/CC-BY-ND-2.5 b/options/license/CC-BY-ND-2.5 index 2f6b29acfa..69e476834d 100644 --- a/options/license/CC-BY-ND-2.5 +++ b/options/license/CC-BY-ND-2.5 @@ -1,4 +1,4 @@ -Creative Commons Attribution-NoDerivs 2.5 +Creative Commons Attribution-NoDerivs 2.5 Generic CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. diff --git a/options/license/CC-PDM-1.0 b/options/license/CC-PDM-1.0 new file mode 100644 index 0000000000..1dc4e63b87 --- /dev/null +++ b/options/license/CC-PDM-1.0 @@ -0,0 +1,27 @@ +No Copyright + +This work has been identified as being free of known restrictions under +copyright law, including all related and neighboring rights. + + +You can copy, modify, distribute and perform the work, even for commercial +purposes, all without asking permission. See Other Information below. + +Other Information + +The work may not be free of known copyright restrictions in all jurisdictions . + +Persons may have other rights in or related to the work, such as patent or +trademark rights, and others may have rights in how the work is used, such as +publicity or privacy rights. + +In some jurisdictions moral rights of the author may persist beyond the term of +copyright. These rights may include the right to be identified as the author +and the right to object to derogatory treatments. + +Unless expressly stated otherwise, the person who identified the work makes no +warranties about the work, and disclaims liability for all uses of the work, to +the fullest extent permitted by applicable law. + +When using or citing the work, you should not imply endorsement by the author +or the person who identified the work. diff --git a/options/license/CC-SA-1.0 b/options/license/CC-SA-1.0 new file mode 100644 index 0000000000..1a810feaec --- /dev/null +++ b/options/license/CC-SA-1.0 @@ -0,0 +1,198 @@ + Creative Commons Legal Code + + ShareAlike 1.0 + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL +SERVICES. DISTRIBUTION OF THIS DRAFT LICENSE DOES NOT CREATE AN ATTORNEY-CLIENT +RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. +CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE INFORMATION PROVIDED, AND +DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE TO BE +BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Collective Work" means a work, such as a periodical issue, anthology or + encyclopedia, in which the Work in its entirety in unmodified form, along + with a number of other contributions, constituting separate and independent + works in themselves, are assembled into a collective whole. A work that + constitutes a Collective Work will not be considered a Derivative Work (as + defined below) for the purposes of this License. + b. "Derivative Work" means a work based upon the Work or upon the Work and + other pre-existing works, such as a translation, musical arrangement, + dramatization, fictionalization, motion picture version, sound recording, + art reproduction, abridgment, condensation, or any other form in which the + Work may be recast, transformed, or adapted, except that a work that + constitutes a Collective Work will not be considered a Derivative Work for + the purpose of this License. + c. "Licensor" means the individual or entity that offers the Work under the + terms of this License. + d. "Original Author" means the individual or entity who created the Work. + e. "Work" means the copyrightable work of authorship offered under the terms + of this License. + f. "You" means an individual or entity exercising rights under this License + who has not previously violated the terms of this License with respect to + the Work, or who has received express permission from the Licensor to + exercise rights under this License despite a previous violation. + +2. Fair Use Rights. Nothing in this license is intended to reduce, limit, or +restrict any rights arising from fair use, first sale or other limitations on +the exclusive rights of the copyright owner under copyright law or other +applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, Licensor +hereby grants You a worldwide, royalty-free, non-exclusive, perpetual (for the +duration of the applicable copyright) license to exercise the rights in the +Work as stated below: + + a. to reproduce the Work, to incorporate the Work into one or more Collective + Works, and to reproduce the Work as incorporated in the Collective Works; + b. to create and reproduce Derivative Works; + c. to distribute copies or phonorecords of, display publicly, perform + publicly, and perform publicly by means of a digital audio transmission the + Work including as incorporated in Collective Works; + d. to distribute copies or phonorecords of, display publicly, perform + publicly, and perform publicly by means of a digital audio transmission + Derivative Works; + +The above rights may be exercised in all media and formats whether now known or +hereafter devised. The above rights include the right to make such +modifications as are technically necessary to exercise the rights in other +media and formats. All rights not expressly granted by Licensor are hereby +reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may distribute, publicly display, publicly perform, or publicly + digitally perform the Work only under the terms of this License, and You + must include a copy of, or the Uniform Resource Identifier for, this + License with every copy or phonorecord of the Work You distribute, publicly + display, publicly perform, or publicly digitally perform. You may not offer + or impose any terms on the Work that alter or restrict the terms of this + License or the recipients' exercise of the rights granted hereunder. You + may not sublicense the Work. You must keep intact all notices that refer to + this License and to the disclaimer of warranties. You may not distribute, + publicly display, publicly perform, or publicly digitally perform the Work + with any technological measures that control access or use of the Work in a + manner inconsistent with the terms of this License Agreement. The above + applies to the Work as incorporated in a Collective Work, but this does not + require the Collective Work apart from the Work itself to be made subject + to the terms of this License. If You create a Collective Work, upon notice + from any Licensor You must, to the extent practicable, remove from the + Collective Work any reference to such Licensor or the Original Author, as + requested. If You create a Derivative Work, upon notice from any Licensor + You must, to the extent practicable, remove from the Derivative Work any + reference to such Licensor or the Original Author, as requested. + b. You may distribute, publicly display, publicly perform, or publicly + digitally perform a Derivative Work only under the terms of this License, + and You must include a copy of, or the Uniform Resource Identifier for, + this License with every copy or phonorecord of each Derivative Work You + distribute, publicly display, publicly perform, or publicly digitally + perform. You may not offer or impose any terms on the Derivative Works that + alter or restrict the terms of this License or the recipients' exercise of + the rights granted hereunder, and You must keep intact all notices that + refer to this License and to the disclaimer of warranties. You may not + distribute, publicly display, publicly perform, or publicly digitally + perform the Derivative Work with any technological measures that control + access or use of the Work in a manner inconsistent with the terms of this + License Agreement. The above applies to the Derivative Work as incorporated + in a Collective Work, but this does not require the Collective Work apart + from the Derivative Work itself to be made subject to the terms of this + License. + +5. Representations, Warranties and Disclaimer + + a. By offering the Work for public release under this License, Licensor + represents and warrants that, to the best of Licensor's knowledge after + reasonable inquiry: + i. Licensor has secured all rights in the Work necessary to grant the + license rights hereunder and to permit the lawful exercise of the + rights granted hereunder without You having any obligation to pay any + royalties, compulsory license fees, residuals or any other payments; + ii. The Work does not infringe the copyright, trademark, publicity rights, + common law rights or any other right of any third party or constitute + defamation, invasion of privacy or other tortious injury to any third + party. + b. EXCEPT AS EXPRESSLY STATED IN THIS LICENSE OR OTHERWISE AGREED IN WRITING + OR REQUIRED BY APPLICABLE LAW, THE WORK IS LICENSED ON AN "AS IS" BASIS, + WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, + WITHOUT LIMITATION, ANY WARRANTIES REGARDING THE CONTENTS OR ACCURACY OF + THE WORK. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, +AND EXCEPT FOR DAMAGES ARISING FROM LIABILITY TO A THIRD PARTY RESULTING FROM +BREACH OF THE WARRANTIES IN SECTION 5, IN NO EVENT WILL LICENSOR BE LIABLE TO +YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR +EXEMPLARY DAMAGES ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF +LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate automatically + upon any breach by You of the terms of this License. Individuals or + entities who have received Derivative Works or Collective Works from You + under this License, however, will not have their licenses terminated + provided such individuals or entities remain in full compliance with those + licenses. Sections 1, 2, 5, 6, 7, and 8 will survive any termination of + this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the Work + under different license terms or to stop distributing the Work at any time; + provided, however that any such election will not serve to withdraw this + License (or any other license that has been, or is required to be, granted + under the terms of this License), and this License will continue in full + force and effect unless terminated as stated above. + +8. Miscellaneous + + a. Each time You distribute or publicly digitally perform the Work or a + Collective Work, the Licensor offers to the recipient a license to the Work + on the same terms and conditions as the license granted to You under this + License. + b. Each time You distribute or publicly digitally perform a Derivative Work, + Licensor offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of the + remainder of the terms of this License, and without further action by the + parties to this agreement, such provision shall be reformed to the minimum + extent necessary to make such provision valid and enforceable. + d. No term or provision of this License shall be deemed waived and no breach + consented to unless such waiver or consent shall be in writing and signed + by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, agreements + or representations with respect to the Work not specified here. Licensor + shall not be bound by any additional provisions that may appear in any + communication from You. This License may not be modified without the mutual + written agreement of the Licensor and You. + +Creative Commons is not a party to this License, and makes no warranty +whatsoever in connection with the Work. Creative Commons will not be liable to +You or any party on any legal theory for any damages whatsoever, including +without limitation any general, special, incidental or consequential damages +arising in connection to this license. Notwithstanding the foregoing two (2) +sentences, if Creative Commons has expressly identified itself as the Licensor +hereunder, it shall have all rights and obligations of Licensor. + +Except for the limited purpose of indicating to the public that the Work is +licensed under the CCPL, neither party will use the trademark "Creative +Commons" or any related trademark or logo of Creative Commons without the prior +written consent of Creative Commons. Any permitted use will be in compliance +with Creative Commons' then-current trademark usage guidelines, as may be +published on its website or otherwise made available upon request from time to +time. + +Creative Commons may be contacted at http://creativecommons.org/. diff --git a/options/license/CGAL-linking-exception b/options/license/CGAL-linking-exception new file mode 100644 index 0000000000..c6dbd55ca6 --- /dev/null +++ b/options/license/CGAL-linking-exception @@ -0,0 +1,4 @@ +As a special exception, you have permission to link this library +with the CGAL library (http://www.cgal.org) and distribute executables, +as long as you follow the requirements of the GNU GPL in regard to +all of the software in the executable aside from CGAL. diff --git a/options/license/Classpath-exception-2.0-short b/options/license/Classpath-exception-2.0-short new file mode 100644 index 0000000000..2e7df40484 --- /dev/null +++ b/options/license/Classpath-exception-2.0-short @@ -0,0 +1,11 @@ +As a special exception, the copyright holders of this library give +you permission to link this library with independent modules to +produce an executable, regardless of the license terms of these +independent modules, and to copy and distribute the resulting +executable under terms of your choice, provided that you also +meet, for each linked independent module, the terms and conditions +of the license of that module. An independent module is a module +which is not derived from or based on this library. If you modify +this library, you may extend this exception to your version of +the library, but you are not obligated to do so. If you do not +wish to do so, delete this exception statement from your version. diff --git a/options/license/CryptoSwift b/options/license/CryptoSwift new file mode 100644 index 0000000000..71603206c5 --- /dev/null +++ b/options/license/CryptoSwift @@ -0,0 +1,11 @@ +Copyright (C) 2014-3099 Marcin Krzyżanowski +This software is provided 'as-is', without any express or implied warranty. + +In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +- The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +- Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +- This notice may not be removed or altered from any source or binary distribution. +- Redistributions of any form whatsoever must retain the following acknowledgment: 'This product includes software developed by the "Marcin Krzyzanowski" (http://krzyzanowskim.com/).' diff --git a/options/license/Digia-Qt-LGPL-exception-1.1 b/options/license/Digia-Qt-LGPL-exception-1.1 new file mode 100644 index 0000000000..66cddd38aa --- /dev/null +++ b/options/license/Digia-Qt-LGPL-exception-1.1 @@ -0,0 +1,9 @@ +Digia Qt LGPL Exception version 1.1 + +As a special exception to the GNU Lesser General Public License version 2.1, +the object code form of a "work that uses the Library" may incorporate material +from a header file that is part of the Library. You may distribute such object +code under terms of your choice, provided that the incorporated material (i) +does not exceed more than 5% of the total size of the Library; and (ii) is +limited to numerical parameters, data structure layouts, accessors, macros, +inline functions and templates. diff --git a/options/license/DocBook-DTD b/options/license/DocBook-DTD new file mode 100644 index 0000000000..a110d23e6f --- /dev/null +++ b/options/license/DocBook-DTD @@ -0,0 +1,24 @@ +Copyright 1992-2002 HaL Computer Systems, Inc., +O'Reilly & Associates, Inc., ArborText, Inc., Fujitsu Software +Corporation, Norman Walsh, Sun Microsystems, Inc., and the +Organization for the Advancement of Structured Information +Standards (OASIS). + +$Id: sdbcent.mod,v 1.13 2005/04/01 21:02:17 nwalsh Exp $ + +Permission to use, copy, modify and distribute the DocBook XML DTD +and its accompanying documentation for any purpose and without fee +is hereby granted in perpetuity, provided that the above copyright +notice and this paragraph appear in all copies. The copyright +holders make no representation about the suitability of the DTD for +any purpose. It is provided "as is" without expressed or implied +warranty. + +If you modify the Simplified DocBook DTD in any way, except for +declaring and referencing additional sets of general entities and +declaring additional notations, label your DTD as a variant of +DocBook. See the maintenance documentation for more information. + +Please direct all questions, bug reports, or suggestions for +changes to the docbook@lists.oasis-open.org mailing list. For more +information, see http://www.oasis-open.org/docbook/. diff --git a/options/license/DocBook-Schema b/options/license/DocBook-Schema new file mode 100644 index 0000000000..56203a0878 --- /dev/null +++ b/options/license/DocBook-Schema @@ -0,0 +1,22 @@ +Copyright 1992-2011 HaL Computer Systems, Inc., +O'Reilly & Associates, Inc., ArborText, Inc., Fujitsu Software +Corporation, Norman Walsh, Sun Microsystems, Inc., and the +Organization for the Advancement of Structured Information +Standards (OASIS). + +Permission to use, copy, modify and distribute the DocBook schema +and its accompanying documentation for any purpose and without fee +is hereby granted in perpetuity, provided that the above copyright +notice and this paragraph appear in all copies. The copyright +holders make no representation about the suitability of the schema +for any purpose. It is provided "as is" without expressed or implied +warranty. + +If you modify the DocBook schema in any way, label your schema as a +variant of DocBook. See the reference documentation +(http://docbook.org/tdg5/en/html/ch05.html#s-notdocbook) +for more information. + +Please direct all questions, bug reports, or suggestions for changes +to the docbook@lists.oasis-open.org mailing list. For more +information, see http://www.oasis-open.org/docbook/. diff --git a/options/license/DocBook-XML b/options/license/DocBook-XML new file mode 100644 index 0000000000..9553feee6b --- /dev/null +++ b/options/license/DocBook-XML @@ -0,0 +1,48 @@ +Copyright +--------- +Copyright (C) 1999-2007 Norman Walsh +Copyright (C) 2003 Jiří Kosek +Copyright (C) 2004-2007 Steve Ball +Copyright (C) 2005-2014 The DocBook Project +Copyright (C) 2011-2012 O'Reilly Media + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the ``Software''), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +Except as contained in this notice, the names of individuals +credited with contribution to this software shall not be used in +advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization +from the individuals in question. + +Any stylesheet derived from this Software that is publically +distributed will be identified with a different name and the +version strings in any derived Software will be changed so that +no possibility of confusion between the derived package and this +Software will exist. + +Warranty +-------- +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL NORMAN WALSH OR ANY OTHER +CONTRIBUTOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Contacting the Author +--------------------- +The DocBook XSL stylesheets are maintained by Norman Walsh, +, and members of the DocBook Project, + diff --git a/options/license/ESA-PL-permissive-2.4 b/options/license/ESA-PL-permissive-2.4 new file mode 100644 index 0000000000..5e25adc372 --- /dev/null +++ b/options/license/ESA-PL-permissive-2.4 @@ -0,0 +1,240 @@ +European Space Agency Public License (ESA-PL) Permissive (Type 3) – v2.4 + + + +1 Definitions + + + +1.1 “Contributor” means (a) the individual or legal entity that originally creates or later modifies the Software and (b) each subsequent individual or legal entity that creates or contributes to the creation of Modifications. + + + +1.2 “Contributor Version” means the version of the Software on which the Contributor based its Modifications. + + + +1.3 “Distribution” and “Distribute” means any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, physically or electronically or by any other means, copies of the Software or Modifications. + + + +1.4 “ESA” means the European Space Agency. + + + +1.5 “License” means this document. + + + +1.6 “Licensor” means the individual or legal entity that Distributes the Software under the License to You. + + + +1.7 “Modification” means any work or software created that is based upon or derived from the Software (or portions thereof) or a modification of the Software (or portions thereof). For the avoidance of doubt, linking a library to the Software results in a Modification. + + + +1.8 “Object Code” means any non-Source Code form of the Software and/or Modifications. + + + +1.9 “Patent Claims” (of a Contributor) means any patent claim(s), owned at the time of the Distribution or subsequently acquired, including without limitation, method, process and apparatus claims, in any patent licensable by a Contributor which would be infringed by making use of the rights granted under Sec. 2.1, including but not limited to make, have made, use, sell, offer for sale or import of the Contributor Version and/or such Contributor’s Modifications (if any), either alone or in combination with the Contributor Version. “Licensable” means having the right to grant, whether at the time of the Distribution or subsequently acquired, the rights conveyed herein. + + + +1.10 “Software” means the software Distributed under this License by the Licensor, in Source Code and/or Object Code form. + + + +1.11 “Source Code” means the preferred, usually human readable form of the Software and/or Modifications in which modifications are made and the associated documentation included in or with such code. + + + +1.12 “You” means an individual or legal entity exercising rights under this License (the licensee). + + + +2 Grant of Rights + + + +2.1 Copyright + + + +The Licensor, and each Contributor in respect of such Contributor’s Modifications, hereby grants You a world-wide, royalty-free, non-exclusive license under Copyright, subject to the terms and conditions of this License, to: + +· use the Software; + +· reproduce the Software by any or all means and in any or all form; + +· Modify the Software and create works based on the Software; + +· communicate to the public, including making available, display or perform the Software or copies thereof to the public; + +· Distribute, sublicense, lend and rent the Software. + + + +The license grant is perpetual and irrevocable, unless terminated pursuant to Sec. 8. + + + +2.2 Patents + + + +Each Contributor in respect of such Contributor’s Modifications, hereby grants You a world-wide, royalty-free, non-exclusive, sub-licensable license under Patent Claims to the extent necessary to make use of the rights granted under Sec. 2.1, including but not limited to make, have made, use, sell, offer for sale, import, export and Distribute such Contributor’s Modifications and the combination of such Contributor’s Modifications with the Contributor Version (collectively called the “Patent Licensed Version” of the Software). + + + +No patent license is granted for claims that are infringed: + +· only as a consequence of further modification of the Patent Licensed Version; or + +· by the combination of the Patent Licensed Version with other software or other devices or hardware, unless such combination was an intended use case of the Patent Licensed Version (e.g. a general purpose library is intended to be used with other software, a satellite navigation software is intended to be used with appropriate hardware); or + +· by a Modification under Patent Claims in the absence of the Contributor’s Modifications or by a combination of the Contributor’s Modifications with software other than the Patent Licensed Version or Modifications thereof. + + + +2.3 Trademark + + + +This License does not grant permission to use trade names, trademarks, services marks, logos or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Software and as reasonable necessary to comply with the obligations of this License (e.g. by reproducing the content of the notices). For the avoidance of doubt, upon Distribution of Modifications You must not use the Licensor’s or ESA’s trademarks, names or logos in any way that states or implies, or can be interpreted as stating or implying, that the final product is endorsed or created by the Licensor or ESA. + + + +3 Distribution + + + +3.1 No Copyleft + + + +You may Distribute the Software and/or Modifications, as Source Code or Object Code, under any license terms, provided that + +(a) notice is given of the use of the Software and the applicability of this License to the Software; and + +(b) You make best efforts to ensure that further Distribution of the Software and/or Modifications (including further Modifications) is subject to the obligations set forth in this Sec. 3.1 (a) and (b). + + + +4 Notices + + + +The following obligations apply in the event of any Distribution of the Software and/or Modifications as Source Code and/or Object Code: + + + +4.1 You must include a copy of this License and all of the notices set out in this Sec. 4. + + + +4.2 You may not remove or alter any copyright, patent, trademark and attribution notices nor any of the notices set out in this Sec. 4, except as necessary for your compliance with this License or otherwise permitted by this License, except for those notices that do not pertain to the Modifications You Distribute. + + + +4.3 Each Contributor must cause its Modification carrying prominent notices stating that the Software has been modified and the date of modification and identify itself as the originator of its Modifications in a manner that reasonably allows identification and contact with the Contributor. The aforementioned notices must at a minimum be in a text file included with the Distribution titled “CHANGELOG”. + + + +4.4 The Software may include a "NOTICE" text file containing general notices. Any Contributor can create such a NOTICE file or add notices to it, alongside or as an addendum to the original text, provided that such notices cannot be construed as modifying the License. + + + +4.5 Each Contributor must identify all of its Patent Claims by providing at a minimum the patent number and identification and contact information in a text file included with the Distribution titled "LEGAL". + + + +5 Warranty and Liability + + + +5.1 Each Contributor warrants and represents that it has sufficient rights to grant the rights to its Modifications conveyed by this License. + + + +5.2 Except as expressly set forth in this License, the Software is provided to You on an “as is” basis and without warranties of any kind, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy or non-infringement of intellectual property rights. Mandatory statutory warranty claims, e.g. in the event of wilful deception or fraudulent misrepresentation, shall remain unaffected. + + + +5.3 Except as expressly set forth in this License, neither Licensor nor any Contributor shall be liable, including, without limitation, for direct, indirect, incidental, or consequential damages (including without limitation loss of profit), however caused and on any theory of liability, arising in any way out of the use or Distribution of the Software or the exercise of any rights under this License, even if You have been advised of the possibility of such damages. Mandatory statutory liability claims, e.g. in the event of wilful misconduct, wilful deception or fraudulent misrepresentation, shall remain unaffected. + + + +6 Additional Agreements + + + +While Distributing the Software or Modifications, You may choose to conclude additional agreements, for free or for charge, regarding for example support, warranty, indemnity, liability or liability obligations and/or rights, provided such additional agreements are consistent with this License and do not effectively restrict the recipient’s rights under this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor or Licensor, and only if You agree to indemnify, defend, and hold each Contributor or Licensor harmless for any liability incurred by, or claims asserted against, such Contributor or Licensor by reason of your accepting any such warranty or additional liability. + + + +7 Infringements + + + +You acknowledge that continuing to use the Software knowing that such use infringes third party rights (e.g. after receiving a third party notification of infringement) would expose you to the risk of being considered as intentionally infringing third party rights. In such event You should acquire the respective rights or modify the Software so that the Modification is non-infringing. + + + +8 Termination + + + +8.1 This License and the rights granted hereunder will terminate automatically upon any breach by You with the terms of this License if you fail to cure such breach within 30 days of becoming aware of the breach. + + + +8.2 If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Software constitutes direct or contributory patent infringement, then any patent and copyright licenses granted to You under this License for the Software shall terminate as of the date such litigation is filed. + + + +8.3 Any licenses validly granted by You under the License prior to termination shall continue and survive termination. + + + +9 Applicable Law, Arbitration and Compliance + + + +9.1 This License is governed by the laws of the ESA Member State where the Licensor resides or has his registered office. “Member States” are the members of the European Space Agency pursuant to Art. 1 of the ESA Convention[1]. This licence shall be governed by German law if a dispute arises with the ESA as a Licensor or if the Licensor has no residence or registered office inside a Member State. + + + +9.2 Any dispute arising out of this License shall be finally settled in accordance with the Rules of Arbitration of the International Chamber of Commerce by one or more arbitrators designated in conformity with those rules. Arbitration proceedings shall take place in Cologne, Germany. The award shall be final and binding on the parties, no appeal shall lie against it. The enforcement of the award shall be governed by the rules of procedure in force in the state/country in which it is to be executed. + + + +9.3 For the avoidance of doubt, You are solely responsible for compliance with current applicable requirements of national laws. The Software can be subject to export control laws. If You export the Software it is your responsibility to comply with all export control laws. This may include different requirements, as e.g. registering the Software with the local authorities. + + + +9.4 If it is impossible for You to comply with any of the terms of this License due to statute, judicial order or regulation You must: + +(a) comply with the terms of this License to the maximum extent possible; and + +(b) describe the limitations and the Object Code and/or Source Code they affect. Such description must be included in the LEGAL notice described in Section 4. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for an average recipient to be able to understand it. + + + +10 Miscellaneous + + + +10.1 Only ESA has the right to modify or publish new versions of this License. ESA may assign this right to other individuals or legal entities. Each version will be given a distinguishing version number. + + + +10.2 This License represents the complete agreement concerning subject matter hereof. + + + +10.3 If any provision of this License is held invalid or unenforceable, the remaining provisions of this License shall not be affected. The invalid or unenforceable provision shall be construed and/or reformed to the extent necessary to make it enforceable and valid. + + +[1] As of January 2020 the Member States are Austria, Belgium, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Luxembourg, The Netherlands, Norway, Poland, Portugal, Romania, Slovenia, Spain, Sweden, Switzerland and the United Kingdom. diff --git a/options/license/ESA-PL-strong-copyleft-2.4 b/options/license/ESA-PL-strong-copyleft-2.4 new file mode 100644 index 0000000000..a007898657 --- /dev/null +++ b/options/license/ESA-PL-strong-copyleft-2.4 @@ -0,0 +1,200 @@ +European Space Agency Public License (ESA-PL) Strong Copyleft (Type 1) – v2.4 + + + +1 Definitions + + + +1.1 “Contributor” means (a) the individual or legal entity that originally creates or later modifies the Software and (b) each subsequent individual or legal entity that creates or contributes to the creation of Modifications. + +1.2 “Contributor Version” means the version of the Software on which the Contributor based its Modifications. + +1.3 “Distribution” and “Distribute” means any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, physically or electronically or by any other means, copies of the Software or Modifications. + +1.4 “ESA” means the European Space Agency. + +1.5 “License” means this document. + +1.6 “Licensor” means the individual or legal entity that Distributes the Software under the License to You. + +1.7 “Modification” means any work or software created that is based upon or derived from the Software (or portions thereof) or a modification of the Software (or portions thereof). For the avoidance of doubt, linking a library to the Software results in a Modification. + +1.8 “Object Code” means any non-Source Code form of the Software and/or Modifications. + +1.9 “Patent Claims” (of a Contributor) means any patent claim(s), owned at the time of the Distribution or subsequently acquired, including without limitation, method, process and apparatus claims, in any patent licensable by a Contributor which would be infringed by making use of the rights granted under Sec. 2.1, including but not limited to make, have made, use, sell, offer for sale or import of the Contributor Version and/or such Contributor’s Modifications (if any), either alone or in combination with the Contributor Version. “Licensable” means having the right to grant, whether at the time of the Distribution or subsequently acquired, the rights conveyed herein. + +1.10 “Software” means the software Distributed under this License by the Licensor, in Source Code and/or Object Code form. + +1.11 “Source Code” means the preferred, usually human readable form of the Software and/or Modifications in which modifications are made and the associated documentation included in or with such code. + +1.12 “You” means an individual or legal entity exercising rights under this License (the licensee). + + + +2 Grant of Rights + + + +2.1 Copyright + +The Licensor, and each Contributor in respect of such Contributor’s Modifications, hereby grants You a world-wide, royalty-free, non-exclusive license under Copyright, subject to the terms and conditions of this License, to: + +- use the Software; +- reproduce the Software by any or all means and in any or all form; +- Modify the Software and create works based on the Software; +- communicate to the public, including making available, display or perform the Software or copies thereof to the public; +- Distribute, sublicense, lend and rent the Software. + + +The license grant is perpetual and irrevocable, unless terminated pursuant to Sec. 8. + + + +2.2 Patents + +Each Contributor in respect of such Contributor’s Modifications, hereby grants You a world-wide, royalty-free, non-exclusive, sub-licensable license under Patent Claims to the extent necessary to make use of the rights granted under Sec. 2.1, including but not limited to make, have made, use, sell, offer for sale, import, export and Distribute such Contributor’s Modifications and the combination of such Contributor’s Modifications with the Contributor Version (collectively called the “Patent Licensed Version” of the Software). + + + +No patent license is granted for claims that are infringed: + +- only as a consequence of further modification of the Patent Licensed Version; or +- by the combination of the Patent Licensed Version with other software or other devices or hardware, unless such combination was an intended use case of the Patent Licensed Version (e.g. a general purpose library is intended to be used with other software, a satellite navigation software is intended to be used with appropriate hardware); or +- by a Modification under Patent Claims in the absence of the Contributor’s Modifications or by a combination of the Contributor’s Modifications with software other than the Patent Licensed Version or Modifications thereof. + + +2.3 Trademark + +This License does not grant permission to use trade names, trademarks, services marks, logos or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Software and as reasonable necessary to comply with the obligations of this License (e.g. by reproducing the content of the notices). For the avoidance of doubt, upon Distribution of Modifications You must not use the Licensor’s or ESA’s trademarks, names or logos in any way that states or implies, or can be interpreted as stating or implying, that the final product is endorsed or created by the Licensor or ESA. + + + +3 Distribution + + + +3.1 Copyleft Clause + +All Distribution of the Software and/or Modifications, as Source Code or Object Code, must be, as a whole, either under (a) the terms of this License or (b) any later version of this License unless the Software is expressly Distributed only under a specific version of the License by a Contributor. + + + +3.2 Copyleft exceptions + +3.2.1 Compilations. In the event of the Distribution of a compilation of Software and/or Modifications with other separate and independent works (for example in or on a volume of a storage or distribution medium), which are not by their nature extensions or other modifications of the Software and/or the Modifications, and which are not combined with it such as to form a larger program, Distribution of the compilation does not cause this License to apply to the other parts of the compilation. + +3.2.2 System Libraries. System Libraries used by a Modification need not be Distributed under the terms of this License and need not be included as part of the Source Code pursuant to Sec. 3.3. “System Library” means anything that is normally distributed (in either source or binary form) with the major components (kernel, window system etc.) of the operating system(s) on which the Software or Modification runs, or a compiler used to produce the Object Code, or an object code interpreter used to run it. + +3.2.3 External Modules. You may create a Modification by combining Software with an external module enabling supplementary functions or services and Distribute the external module under different license terms, provided that the external module and the Software run in separate address spaces, with one calling the other, or each other interfacing, when they are run. + + + +3.3 Communication of the Source Code + +If You Distribute the Software and/or Modifications as Object Code, You must: + +provide in addition a copy of the Source Code of the Software and/or Modifications to each recipient; or +make the Source Code of the Software and/or Modifications freely accessible by reasonable means for anyone who possesses the Object Code or received the Software and/or Modifications from You, and inform recipients how to obtain a copy of the Source Code. Such information needs to be included at a minimum in the “NOTICE” file pursuant to Sec. 4.4 You are obliged to make the Source Code accessible in accordance with this Section for as long as You continue to Distribute the Software and/or Modifications and at a minimum for a three year period following Your last Distribution of the Software and/or Modifications. + + +3.4 Service Provision + +If You provide access to the Software and/or Modifications or make its functionality available by any means or use it to provide services for any individual or legal entity other than You, e.g. by provision of software-as-a-service, You are obliged to make the Source Code of the Software and/or Modifications freely accessible by reasonable means to those individuals or legal entities and provide information on how to obtain a copy of the Source Code. You are obliged to make the Source Code accessible in accordance with this Section for as long as You continue to provide access to the Software and/or Modifications. + + + +3.5 Dual Licensing + +This License gives no permission to license the Software or Modifications in any other way, but it does not invalidate such permission if You have separately received it. + + + +4 Notices + +The following obligations apply in the event of any Distribution of the Software and/or Modifications as Source Code and/or Object Code: + + + +4.1 You must include a copy of this License and all of the notices set out in this Sec. 4. + +4.2 You may not remove or alter any copyright, patent, trademark and attribution notices nor any of the notices set out in this Sec. 4, except as necessary for your compliance with this License or otherwise permitted by this License, except for those notices that do not pertain to the Modifications You Distribute. + +4.3 Each Contributor must cause its Modification carrying prominent notices stating that the Software has been modified and the date of modification and identify itself as the originator of its Modifications in a manner that reasonably allows identification and contact with the Contributor. The aforementioned notices must at a minimum be in a text file included with the Distribution titled “CHANGELOG”. + +4.4 The Software may include a "NOTICE" text file containing general notices. Any Contributor can create such a NOTICE file or add notices to it, alongside or as an addendum to the original text, provided that such notices cannot be construed as modifying the License. + +4.5 Each Contributor must identify all of its Patent Claims by providing at a minimum the patent number and identification and contact information in a text file included with the Distribution titled "LEGAL". + + + +5 Warranty and Liability + + + +5.1 Each Contributor warrants and represents that it has sufficient rights to grant the rights to its Modifications conveyed by this License. + +5.2 Except as expressly set forth in this License, the Software is provided to You on an “as is” basis and without warranties of any kind, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy or non-infringement of intellectual property rights. Mandatory statutory warranty claims, e.g. in the event of wilful deception or fraudulent misrepresentation, shall remain unaffected. + +5.3 Except as expressly set forth in this License, neither Licensor nor any Contributor shall be liable, including, without limitation, for direct, indirect, incidental, or consequential damages (including without limitation loss of profit), however caused and on any theory of liability, arising in any way out of the use or Distribution of the Software or the exercise of any rights under this License, even if You have been advised of the possibility of such damages. Mandatory statutory liability claims, e.g. in the event of wilful misconduct, wilful deception or fraudulent misrepresentation, shall remain unaffected. + + + +6 Additional Agreements + + + +While Distributing the Software or Modifications, You may choose to conclude additional agreements, for free or for charge, regarding for example support, warranty, indemnity, liability or liability obligations and/or rights, provided such additional agreements are consistent with this License and do not effectively restrict the recipient’s rights under this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor or Licensor, and only if You agree to indemnify, defend, and hold each Contributor or Licensor harmless for any liability incurred by, or claims asserted against, such Contributor or Licensor by reason of your accepting any such warranty or additional liability. + + + +7 Infringements + + + +7.1 You acknowledge that continuing to use the Software knowing that such use infringes third party rights (e.g. after receiving a third party notification of infringement) would expose you to the risk of being considered as intentionally infringing third party rights. In such event You should acquire the respective rights or modify the Software so that the Modification is non-infringing. + + + +8 Termination + + + +8.1 This License and the rights granted hereunder will terminate automatically upon any breach by You with the terms of this License if you fail to cure such breach within 30 days of becoming aware of the breach. + +8.2 If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Software constitutes direct or contributory patent infringement, then any patent and copyright licenses granted to You under this License for the Software shall terminate as of the date such litigation is filed. + +8.3 Any licenses validly granted by You under the License prior to termination shall continue and survive termination. + + + +9 Applicable Law, Arbitration and Compliance + + + +9.1 This License is governed by the laws of the ESA Member State where the Licensor resides or has his registered office. “Member States” are the members of the European Space Agency pursuant to Art. 1 of the ESA Convention[1]. This licence shall be governed by German law if a dispute arises with the ESA as a Licensor or if the Licensor has no residence or registered office inside a Member State. + +9.2 Any dispute arising out of this License shall be finally settled in accordance with the Rules of Arbitration of the International Chamber of Commerce by one or more arbitrators designated in conformity with those rules. Arbitration proceedings shall take place in Cologne, Germany. The award shall be final and binding on the parties, no appeal shall lie against it. The enforcement of the award shall be governed by the rules of procedure in force in the state/country in which it is to be executed. + +9.3 For the avoidance of doubt, You are solely responsible for compliance with current applicable requirements of national laws. The Software can be subject to export control laws. If You export the Software it is your responsibility to comply with all export control laws. This may include different requirements, as e.g. registering the Software with the local authorities. + + + +9.4 If it is impossible for You to comply with any of the terms of this License due to statute, judicial order or regulation You must: + +comply with the terms of this License to the maximum extent possible; and +describe the limitations and the Object Code and/or Source Code they affect. Such description must be included in the LEGAL notice described in Section 4. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for an average recipient to be able to understand it. + + +10 Miscellaneous + + + +10.1 Only ESA has the right to modify or publish new versions of this License. ESA may assign this right to other individuals or legal entities. Each version will be given a distinguishing version number. + +10.2 This License represents the complete agreement concerning subject matter hereof. + +10.3 If any provision of this License is held invalid or unenforceable, the remaining provisions of this License shall not be affected. The invalid or unenforceable provision shall be construed and/or reformed to the extent necessary to make it enforceable and valid. + + +[1] As of June 2025 the Member States are Austria, Belgium, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Luxembourg, The Netherlands, Norway, Poland, Portugal, Romania, Slovenia, Spain, Sweden, Switzerland and the United Kingdom. diff --git a/options/license/ESA-PL-weak-copyleft-2.4 b/options/license/ESA-PL-weak-copyleft-2.4 new file mode 100644 index 0000000000..7c27dc2506 --- /dev/null +++ b/options/license/ESA-PL-weak-copyleft-2.4 @@ -0,0 +1,193 @@ +European Space Agency Public License (ESA-PL) Weak Copyleft (Type 2) – v2.4 + + + +1 Definitions + + + +1.1 “Contributor” means (a) the individual or legal entity that originally creates or later modifies the Software and (b) each subsequent individual or legal entity that creates or contributes to the creation of Modifications. + +1.2 “Contributor Version” means the version of the Software on which the Contributor based its Modifications. + +1.3 “Distribution” and “Distribute” means any act of selling, giving, lending, renting, distributing, communicating, transmitting, or otherwise making available, physically or electronically or by any other means, copies of the Software or Modifications. + +1.4 “ESA” means the European Space Agency. + +1.5 “License” means this document. + +1.6 “Licensor” means the individual or legal entity that Distributes the Software under the License to You. + +1.7 “Modification” means any work or software created that is based upon or derived from the Software (or portions thereof) or a modification of the Software (or portions thereof). For the avoidance of doubt, linking a library to the Software results in a Modification. + +1.8 “Object Code” means any non-Source Code form of the Software and/or Modifications. + +1.9 “Patent Claims” (of a Contributor) means any patent claim(s), owned at the time of the Distribution or subsequently acquired, including without limitation, method, process and apparatus claims, in any patent licensable by a Contributor which would be infringed by making use of the rights granted under Sec. 2.1, including but not limited to make, have made, use, sell, offer for sale or import of the Contributor Version and/or such Contributor’s Modifications (if any), either alone or in combination with the Contributor Version. “Licensable” means having the right to grant, whether at the time of the Distribution or subsequently acquired, the rights conveyed herein. + +1.10 “Software” means the software Distributed under this License by the Licensor, in Source Code and/or Object Code form. + +1.11 “Source Code” means the preferred, usually human readable form of the Software and/or Modifications in which modifications are made and the associated documentation included in or with such code. + +1.12 “You” means an individual or legal entity exercising rights under this License (the licensee). + + + +2 Grant of Rights + + + +2.1 Copyright + +The Licensor, and each Contributor in respect of such Contributor’s Modifications, hereby grants You a world-wide, royalty-free, non-exclusive license under Copyright, subject to the terms and conditions of this License, to: + +use the Software; +reproduce the Software by any or all means and in any or all form; +Modify the Software and create works based on the Software; +communicate to the public, including making available, display or perform the Software or copies thereof to the public; +Distribute, sublicense, lend and rent the Software. +The license grant is perpetual and irrevocable, unless terminated pursuant to Sec. 8. + + + +2.2 Patents + +Each Contributor in respect of such Contributor’s Modifications, hereby grants You a world-wide, royalty-free, non-exclusive, sub-licensable license under Patent Claims to the extent necessary to make use of the rights granted under Sec. 2.1, including but not limited to make, have made, use, sell, offer for sale, import, export and Distribute such Contributor’s Modifications and the combination of such Contributor’s Modifications with the Contributor Version (collectively called the “Patent Licensed Version” of the Software). + +No patent license is granted for claims that are infringed: + +only as a consequence of further modification of the Patent Licensed Version; or +by the combination of the Patent Licensed Version with other software or other devices or hardware, unless such combination was an intended use case of the Patent Licensed Version (e.g. a general purpose library is intended to be used with other software, a satellite navigation software is intended to be used with appropriate hardware); or +by a Modification under Patent Claims in the absence of the Contributor’s Modifications or by a combination of the Contributor’s Modifications with software other than the Patent Licensed Version or Modifications thereof. + + +2.3 Trademark + +This License does not grant permission to use trade names, trademarks, services marks, logos or names of the Licensor, except as required for reasonable and customary use in describing the origin of the Software and as reasonable necessary to comply with the obligations of this License (e.g. by reproducing the content of the notices). For the avoidance of doubt, upon Distribution of Modifications You must not use the Licensor’s or ESA’s trademarks, names or logos in any way that states or implies, or can be interpreted as stating or implying, that the final product is endorsed or created by the Licensor or ESA. + + + +3 Distribution + + + +3.1 Copyleft Clause + +All Distribution of the Software and/or Modifications, as Source Code or Object Code, must be, as a whole, either under (a) the terms of this License or the ESA-PL Strong Copyleft license v2.4 or (b) any later version of these Licenses unless the Software is expressly Distributed only under a specific version of the License by a Contributor or (c) the terms of a compatible license as listed in Appendix A to this License. Any obligation in this License to Distribute under the terms of this License, in particular as set out in Sec. 3.2, shall be construed as referring to “this License or a compatible license”. + +3.2 Copyleft exceptions + +3.2.1 Compilations. In the event of the Distribution of a compilation of Software and/or Modifications with other separate and independent works (for example in or on a volume of a storage or distribution medium), which are not by their nature extensions or other modifications of the Software and/or the Modifications, and which are not combined with it such as to form a single larger program, Distribution of the compilation does not cause this License to apply to the other parts of the compilation. + +3.2.2 System Libraries. System Libraries used by a Modification need not be Distributed under the terms of this License and need not be included as part of the Source Code pursuant to Sec. 3.3. “System Library” means anything that is normally distributed (in either source or binary form) with the major components (kernel, window system etc.) of the operating system(s) on which the Software or Modification runs, or a compiler used to produce the Object Code, or an object code interpreter used to run it. + +3.2.3 External Modules. You may create a Modification by combining Software with an external module enabling supplementary functions or services and Distribute the external module under different license terms, provided that the external module and the Software run in separate address spaces, with one calling the other, or each other interfacing, when they are run. + +3.2.4 Combinations. You may create a Modification (the “Combination”) by combining or linking the Software or Modifications thereof (the “Covered Code”) with additional code or software (the “External Code”) not governed by the terms of this License and Distribute the Combination + +in Object Code form under any license terms, and/or +in Source Code form with the External Code’s Source Code under any license terms and the Covered Code’s Source Code under this License, +provided that: +the Covered Code will be governed by this License and the different license terms effectively do not restrict the rights granted by this License; and +the External Code and its license terms are clearly identified and notice is given of the use of Covered Code and the applicability of this License; and +the External Code’s Source Code is clearly separated from the Covered Code’s Source Code (usually contained in different files); and +You communicate the Covered Code’s Source Code in accordance with Sec. 3.3. + + +3.3 Communication of the Source Code + +If You Distribute the Software and/or Modifications as Object Code, You must: + +provide in addition a copy of the Source Code of the Software and/or Modifications to each recipient; or +make the Source Code of the Software and/or Modifications freely accessible by reasonable means for anyone who possesses the Object Code or received the Software and/or Modifications from You, and inform recipients how to obtain a copy of the Source Code. Such information needs to be included at a minimum in the “NOTICE” file pursuant to Sec. 4.4 You are obliged to make the Source Code accessible in accordance with this Section for as long as You continue to Distribute the Software and/or Modifications and at a minimum for a three year period following Your last Distribution of the Software and/or Modifications. + + +3.4 Dual Licensing + +This License gives no permission to license the Software or Modifications in any other way, but it does not invalidate such permission if You have separately received it. + + + +4 Notices + +The following obligations apply in the event of any Distribution of the Software and/or Modifications as Source Code and/or Object Code: + +4.1 You must include a copy of this License and all of the notices set out in this Sec. 4. + +4.2 You may not remove or alter any copyright, patent, trademark and attribution notices nor any of the notices set out in this Sec. 4, except as necessary for your compliance with this License or otherwise permitted by this License, except for those notices that do not pertain to the Modifications You Distribute. + +4.3 Each Contributor must cause its Modification carrying prominent notices stating that the Software has been modified and the date of modification and identify itself as the originator of its Modifications in a manner that reasonably allows identification and contact with the Contributor. The aforementioned notices must at a minimum be in a text file included with the Distribution titled “CHANGELOG”. + +4.4 The Software may include a "NOTICE" text file containing general notices. Any Contributor can create such a NOTICE file or add notices to it, alongside or as an addendum to the original text, provided that such notices cannot be construed as modifying the License. + +4.5 Each Contributor must identify all of its Patent Claims by providing at a minimum the patent number and identification and contact information in a text file included with the Distribution titled "LEGAL". + + + +5 Warranty and Liability + +5.1 Each Contributor warrants and represents that it has sufficient rights to grant the rights to its Modifications conveyed by this License. + +5.2 Except as expressly set forth in this License, the Software is provided to You on an “as is” basis and without warranties of any kind, including without limitation merchantability, fitness for a particular purpose, absence of defects or errors, accuracy or non-infringement of intellectual property rights. Mandatory statutory warranty claims, e.g. in the event of wilful deception or fraudulent misrepresentation, shall remain unaffected. + +5.3 Except as expressly set forth in this License, neither Licensor nor any Contributor shall be liable, including, without limitation, for direct, indirect, incidental, or consequential damages (including without limitation loss of profit), however caused and on any theory of liability, arising in any way out of the use or Distribution of the Software or the exercise of any rights under this License, even if You have been advised of the possibility of such damages. Mandatory statutory liability claims, e.g. in the event of wilful misconduct, wilful deception or fraudulent misrepresentation, shall remain unaffected. + + + +6 Additional Agreements + +While Distributing the Software or Modifications, You may choose to conclude additional agreements, for free or for charge, regarding for example support, warranty, indemnity, liability or liability obligations and/or rights, provided such additional agreements are consistent with this License and do not effectively restrict the recipient’s rights under this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor or Licensor, and only if You agree to indemnify, defend, and hold each Contributor or Licensor harmless for any liability incurred by, or claims asserted against, such Contributor or Licensor by reason of your accepting any such warranty or additional liability. + + + +7 Infringements + +7.1 You acknowledge that continuing to use the Software knowing that such use infringes third party rights (e.g. after receiving a third party notification of infringement) would expose you to the risk of being considered as intentionally infringing third party rights. In such event You should acquire the respective rights or modify the Software so that the Modification is non-infringing. + + + +8 Termination + +8.1 This License and the rights granted hereunder will terminate automatically upon any breach by You with the terms of this License if you fail to cure such breach within 30 days of becoming aware of the breach. + +8.2 If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Software constitutes direct or contributory patent infringement, then any patent and copyright licenses granted to You under this License for the Software shall terminate as of the date such litigation is filed. + +8.3 Any licenses validly granted by You under the License prior to termination shall continue and survive termination. + + + +9 Applicable Law, Arbitration and Compliance + +9.1 This License is governed by the laws of the ESA Member State where the Licensor resides or has his registered office. “Member States” are the members of the European Space Agency pursuant to Art. 1 of the ESA Convention[1]. This licence shall be governed by German law if a dispute arises with the ESA as a Licensor or if the Licensor has no residence or registered office inside a Member State. + +9.2 Any dispute arising out of this License shall be finally settled in accordance with the Rules of Arbitration of the International Chamber of Commerce by one or more arbitrators designated in conformity with those rules. Arbitration proceedings shall take place in Cologne, Germany. The award shall be final and binding on the parties, no appeal shall lie against it. The enforcement of the award shall be governed by the rules of procedure in force in the state/country in which it is to be executed. + +9.3 For the avoidance of doubt, You are solely responsible for compliance with current applicable requirements of national laws. The Software can be subject to export control laws. If You export the Software it is your responsibility to comply with all export control laws. This may include different requirements, as e.g. registering the Software with the local authorities. + +9.4 If it is impossible for You to comply with any of the terms of this License due to statute, judicial order or regulation You must: + +comply with the terms of this License to the maximum extent possible; and +describe the limitations and the Object Code and/or Source Code they affect. Such description must be included in the LEGAL notice described in Section 4. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for an average recipient to be able to understand it. + + +10 Miscellaneous + +10.1 Only ESA has the right to modify or publish new versions of this License. ESA may assign this right to other individuals or legal entities. Each version will be given a distinguishing version number. + +10.2 This License represents the complete agreement concerning subject matter hereof. + +10.3 If any provision of this License is held invalid or unenforceable, the remaining provisions of this License shall not be affected. The invalid or unenforceable provision shall be construed and/or reformed to the extent necessary to make it enforceable and valid. + + + +Appendix A – List of compatible licenses + + + +Compatible licenses are: + +GNU General Public License (GPL) version 2 and any subsequent version +CeCILL version 2 and any subsequent version + + + +[1] As of January 2020 the Member States are Austria, Belgium, Czech Republic, Denmark, Estonia, Finland, France, Germany, Greece, Hungary, Ireland, Italy, Luxembourg, The Netherlands, Norway, Poland, Portugal, Romania, Spain, Sweden, Switzerland and the United Kingdom. diff --git a/options/license/Elastic-2.0 b/options/license/Elastic-2.0 index 809108b857..9496955678 100644 --- a/options/license/Elastic-2.0 +++ b/options/license/Elastic-2.0 @@ -2,18 +2,18 @@ Elastic License 2.0 URL: https://www.elastic.co/licensing/elastic-license -## Acceptance +Acceptance By using the software, you agree to all of the terms and conditions below. -## Copyright License +Copyright License The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below. -## Limitations +Limitations You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of @@ -27,7 +27,7 @@ You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. -## Patents +Patents The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for @@ -40,7 +40,7 @@ the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. -## Notices +Notices You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. @@ -53,7 +53,7 @@ software prominent notices stating that you have modified the software. These terms do not imply any licenses other than those expressly granted in these terms. -## Termination +Termination If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you @@ -63,31 +63,31 @@ reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently. -## No Liability +No Liability -*As far as the law allows, the software comes as is, without any warranty or +As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of -legal claim.* +legal claim. -## Definitions +Definitions -The **licensor** is the entity offering these terms, and the **software** is the +The licensor is the entity offering these terms, and the software is the software the licensor makes available under these terms, including any portion of it. -**you** refers to the individual or entity agreeing to these terms. +you refers to the individual or entity agreeing to these terms. -**your company** is any legal entity, sole proprietorship, or other kind of +your company is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that -organization. **control** means ownership of substantially all the assets of an +organization. control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. -**your licenses** are all the licenses granted to you for the software under +your licenses are all the licenses granted to you for the software under these terms. -**use** means anything you do with the software requiring one of your licenses. +use means anything you do with the software requiring one of your licenses. -**trademark** means trademarks, service marks, and similar rights. +trademark means trademarks, service marks, and similar rights. diff --git a/options/license/FSFULLRSD b/options/license/FSFULLRSD new file mode 100644 index 0000000000..abc104563a --- /dev/null +++ b/options/license/FSFULLRSD @@ -0,0 +1,4 @@ +This file is free software; the Free Software Foundation +gives unlimited permission to copy and/or distribute it, +with or without modifications, as long as this notice is preserved. +This file is offered as-is, without any warranty. diff --git a/options/license/FSL-1.1-ALv2 b/options/license/FSL-1.1-ALv2 new file mode 100644 index 0000000000..386880ecd6 --- /dev/null +++ b/options/license/FSL-1.1-ALv2 @@ -0,0 +1,105 @@ +# Functional Source License, Version 1.1, ALv2 Future License + +## Abbreviation + +FSL-1.1-ALv2 + +## Notice + +Copyright ${year} ${licensor name} + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the Apache License, Version 2.0 that is effective on the second anniversary of +the date we make the Software available. On or after that date, you may use the +Software under the Apache License, Version 2.0, in which case the following +will apply: + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. + +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/options/license/FSL-1.1-MIT b/options/license/FSL-1.1-MIT new file mode 100644 index 0000000000..571aa38073 --- /dev/null +++ b/options/license/FSL-1.1-MIT @@ -0,0 +1,110 @@ +# Functional Source License, Version 1.1, MIT Future License + +## Abbreviation + +FSL-1.1-MIT + +## Notice + +Copyright ${year} ${licensor name} + +## Terms and Conditions + +### Licensor ("We") + +The party offering the Software under these Terms and Conditions. + +### The Software + +The "Software" is each version of the software that we make available under +these Terms and Conditions, as indicated by our inclusion of these Terms and +Conditions with the Software. + +### License Grant + +Subject to your compliance with this License Grant and the Patents, +Redistribution and Trademark clauses below, we hereby grant you the right to +use, copy, modify, create derivative works, publicly perform, publicly display +and redistribute the Software for any Permitted Purpose identified below. + +### Permitted Purpose + +A Permitted Purpose is any purpose other than a Competing Use. A Competing Use +means making the Software available to others in a commercial product or +service that: + +1. substitutes for the Software; + +2. substitutes for any other product or service we offer using the Software + that exists as of the date we make the Software available; or + +3. offers the same or substantially similar functionality as the Software. + +Permitted Purposes specifically include using the Software: + +1. for your internal use and access; + +2. for non-commercial education; + +3. for non-commercial research; and + +4. in connection with professional services that you provide to a licensee + using the Software in accordance with these Terms and Conditions. + +### Patents + +To the extent your use for a Permitted Purpose would necessarily infringe our +patents, the license grant above includes a license under our patents. If you +make a claim against any party that the Software infringes or contributes to +the infringement of any patent, then your patent license to the Software ends +immediately. + +### Redistribution + +The Terms and Conditions apply to all copies, modifications and derivatives of +the Software. + +If you redistribute any copies, modifications or derivatives of the Software, +you must include a copy of or a link to these Terms and Conditions and not +remove any copyright notices provided in or with the Software. + +### Disclaimer + +THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR +PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT. + +IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE +SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES, +EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE. + +### Trademarks + +Except for displaying the License Details and identifying us as the origin of +the Software, you have no right under these Terms and Conditions to use our +trademarks, trade names, service marks or product names. + +## Grant of Future License + +We hereby irrevocably grant you an additional license to use the Software under +the MIT license that is effective on the second anniversary of the date we make +the Software available. On or after that date, you may use the Software under +the MIT license, in which case the following will apply: + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/options/license/Game-Programming-Gems b/options/license/Game-Programming-Gems new file mode 100644 index 0000000000..25549ebb38 --- /dev/null +++ b/options/license/Game-Programming-Gems @@ -0,0 +1,8 @@ +Original version Copyright (C) Scott Bilas, 2000. +All rights reserved worldwide. + +This software is provided "as is" without express or implied +warranties. You may freely copy and compile this source into +applications you distribute provided that the copyright text +below is included in the resulting source code, for example: +"Portions Copyright (C) Scott Bilas, 2000" diff --git a/options/license/HDF5 b/options/license/HDF5 new file mode 100644 index 0000000000..b4cb77559c --- /dev/null +++ b/options/license/HDF5 @@ -0,0 +1,96 @@ +Copyright Notice and License Terms for +HDF5 (Hierarchical Data Format 5) Software Library and Utilities +----------------------------------------------------------------------------- + +HDF5 (Hierarchical Data Format 5) Software Library and Utilities +Copyright 2006 by The HDF Group. + +NCSA HDF5 (Hierarchical Data Format 5) Software Library and Utilities +Copyright 1998-2006 by The Board of Trustees of the University of Illinois. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted for any purpose (including commercial purposes) +provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions, and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions, and the following disclaimer in the documentation + and/or materials provided with the distribution. + +3. Neither the name of The HDF Group, the name of the University, nor the + name of any Contributor may be used to endorse or promote products derived + from this software without specific prior written permission from + The HDF Group, the University, or the Contributor, respectively. + +DISCLAIMER: +THIS SOFTWARE IS PROVIDED BY THE HDF GROUP AND THE CONTRIBUTORS +"AS IS" WITH NO WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED. IN NO +EVENT SHALL THE HDF GROUP OR THE CONTRIBUTORS BE LIABLE FOR ANY DAMAGES +SUFFERED BY THE USERS ARISING OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You are under no obligation whatsoever to provide any bug fixes, patches, or +upgrades to the features, functionality or performance of the source code +("Enhancements") to anyone; however, if you choose to make your Enhancements +available either publicly, or directly to The HDF Group, without imposing a +separate written license agreement for such Enhancements, then you hereby +grant the following license: a non-exclusive, royalty-free perpetual license +to install, use, modify, prepare derivative works, incorporate into other +computer software, distribute, and sublicense such enhancements or derivative +works thereof, in binary and source code form. + +----------------------------------------------------------------------------- +----------------------------------------------------------------------------- + +Contributors: National Center for Supercomputing Applications (NCSA) at +the University of Illinois, Fortner Software, Unidata Program Center +(netCDF), The Independent JPEG Group (JPEG), Jean-loup Gailly and Mark Adler +(gzip), and Digital Equipment Corporation (DEC). + +----------------------------------------------------------------------------- + +Portions of HDF5 were developed with support from the Lawrence Berkeley +National Laboratory (LBNL) and the United States Department of Energy +under Prime Contract No. DE-AC02-05CH11231. + +----------------------------------------------------------------------------- + +Portions of HDF5 were developed with support from Lawrence Livermore +National Laboratory and the United States Department of Energy under +Prime Contract No. DE-AC52-07NA27344. + +----------------------------------------------------------------------------- + +Portions of HDF5 were developed with support from the University of +California, Lawrence Livermore National Laboratory (UC LLNL). +The following statement applies to those portions of the product and must +be retained in any redistribution of source code, binaries, documentation, +and/or accompanying materials: + + This work was partially produced at the University of California, + Lawrence Livermore National Laboratory (UC LLNL) under contract + no. W-7405-ENG-48 (Contract 48) between the U.S. Department of Energy + (DOE) and The Regents of the University of California (University) + for the operation of UC LLNL. + + DISCLAIMER: + THIS WORK WAS PREPARED AS AN ACCOUNT OF WORK SPONSORED BY AN AGENCY OF + THE UNITED STATES GOVERNMENT. NEITHER THE UNITED STATES GOVERNMENT NOR + THE UNIVERSITY OF CALIFORNIA NOR ANY OF THEIR EMPLOYEES, MAKES ANY + WARRANTY, EXPRESS OR IMPLIED, OR ASSUMES ANY LIABILITY OR RESPONSIBILITY + FOR THE ACCURACY, COMPLETENESS, OR USEFULNESS OF ANY INFORMATION, + APPARATUS, PRODUCT, OR PROCESS DISCLOSED, OR REPRESENTS THAT ITS USE + WOULD NOT INFRINGE PRIVATELY- OWNED RIGHTS. REFERENCE HEREIN TO ANY + SPECIFIC COMMERCIAL PRODUCTS, PROCESS, OR SERVICE BY TRADE NAME, + TRADEMARK, MANUFACTURER, OR OTHERWISE, DOES NOT NECESSARILY CONSTITUTE + OR IMPLY ITS ENDORSEMENT, RECOMMENDATION, OR FAVORING BY THE UNITED + STATES GOVERNMENT OR THE UNIVERSITY OF CALIFORNIA. THE VIEWS AND + OPINIONS OF AUTHORS EXPRESSED HEREIN DO NOT NECESSARILY STATE OR REFLECT + THOSE OF THE UNITED STATES GOVERNMENT OR THE UNIVERSITY OF CALIFORNIA, + AND SHALL NOT BE USED FOR ADVERTISING OR PRODUCT ENDORSEMENT PURPOSES. + +----------------------------------------------------------------------------- diff --git a/options/license/HIDAPI b/options/license/HIDAPI new file mode 100644 index 0000000000..e0b5d70c04 --- /dev/null +++ b/options/license/HIDAPI @@ -0,0 +1,2 @@ +This software may be used by anyone for any reason so long +as the copyright notice in the source files remains intact. diff --git a/options/license/HPND-Netrek b/options/license/HPND-Netrek new file mode 100644 index 0000000000..5c3cb650f4 --- /dev/null +++ b/options/license/HPND-Netrek @@ -0,0 +1,10 @@ +Copyright (C) 1995 S. M. Patel (smpatel@wam.umd.edu) + +Permission to use, copy, modify, and distribute this +software and its documentation for any purpose and without +fee is hereby granted, provided that the above copyright +notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting +documentation. No representations are made about the +suitability of this software for any purpose. It is +provided "as is" without express or implied warranty. diff --git a/options/license/HPND-SMC b/options/license/HPND-SMC new file mode 100644 index 0000000000..79c1e948e4 --- /dev/null +++ b/options/license/HPND-SMC @@ -0,0 +1,15 @@ +Copyright 2000, Mojam Media, Inc., all rights reserved. Author: Skip Montanaro + +Copyright 1999, Bioreason, Inc., all rights reserved. Author: Andrew Dalke + +Copyright 1995-1997, Automatrix, Inc., all rights reserved. Author: Skip Montanaro + +Copyright 1991-1995, Stichting Mathematisch Centrum, all rights reserved. + +Permission to use, copy, modify, and distribute this Python software and its +associated documentation for any purpose without fee is hereby granted, +provided that the above copyright notice appears in all copies, and that both +that copyright notice and this permission notice appear in supporting +documentation, and that the name of neither Automatrix, Bioreason or Mojam +Media be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. diff --git a/options/license/HPND-sell-variant-critical-systems b/options/license/HPND-sell-variant-critical-systems new file mode 100644 index 0000000000..b71219d4c2 --- /dev/null +++ b/options/license/HPND-sell-variant-critical-systems @@ -0,0 +1,20 @@ +Alan Cox +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of Red Hat, Alan Cox and Henrik Harmsen +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. Th authors make no +representations about the suitability of this software for any purpose. +It is provided "as is" without express or implied warranty. +THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL RICHARD HECKER BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +THIS SOFTWARE IS NOT DESIGNED FOR USE IN SAFETY CRITICAL SYSTEMS OF +ANY KIND OR FORM. diff --git a/options/license/ISO-permission b/options/license/ISO-permission new file mode 100644 index 0000000000..fc0ddeab3b --- /dev/null +++ b/options/license/ISO-permission @@ -0,0 +1,4 @@ +(C) International Organization for Standardization 1986 +Permission to copy in any form is granted for use with +conforming SGML systems and applications as defined +in ISO 8879, provided this notice is included in all copies. diff --git a/options/license/Independent-modules-exception b/options/license/Independent-modules-exception new file mode 100644 index 0000000000..8f66dba6ab --- /dev/null +++ b/options/license/Independent-modules-exception @@ -0,0 +1,18 @@ +This is the file COPYING.FPC, it applies to the Free Pascal Run-Time Library +(RTL) and packages (packages) distributed by members of the Free Pascal +Development Team. + +The source code of the Free Pascal Runtime Libraries and packages are +distributed under the Library GNU General Public License +(see the file COPYING) with the following modification: + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent modules, +and to copy and distribute the resulting executable under terms of your choice, +provided that you also meet, for each linked independent module, the terms +and conditions of the license of that module. An independent module is a module +which is not derived from or based on this library. If you modify this +library, you may extend this exception to your version of the library, but you are +not obligated to do so. If you do not wish to do so, delete this exception +statement from your version. diff --git a/options/license/InnoSetup b/options/license/InnoSetup new file mode 100644 index 0000000000..337584e6d1 --- /dev/null +++ b/options/license/InnoSetup @@ -0,0 +1,27 @@ +Inno Setup License +================== + +Except where otherwise noted, all of the documentation and software included in the Inno Setup +package is copyrighted by Jordan Russell. + +Copyright (C) 1997-2024 Jordan Russell. All rights reserved. +Portions Copyright (C) 2000-2024 Martijn Laan. All rights reserved. + +This software is provided "as-is," without any express or implied warranty. In no event shall the +author be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter and redistribute it, provided that the following conditions are met: + +1. All redistributions of source code files must retain all copyright notices that are currently in + place, and this list of conditions without modification. + +2. All redistributions in binary form must retain all occurrences of the above copyright notice and + web site addresses that are currently in place (for example, in the About boxes). + +3. The origin of this software must not be misrepresented; you must not claim that you wrote the + original software. If you use this software to distribute a product, an acknowledgment in the + product documentation would be appreciated but is not required. + +4. Modified versions in source or binary form must be plainly marked as such, and must not be + misrepresented as being the original software. diff --git a/options/license/MIPS b/options/license/MIPS new file mode 100644 index 0000000000..cf57a05639 --- /dev/null +++ b/options/license/MIPS @@ -0,0 +1,4 @@ +Copyright (c) 1992, 1991, 1990 MIPS Computer Systems, Inc. +MIPS Computer Systems, Inc. grants reproduction and use +rights to all parties, PROVIDED that this comment is +maintained in the copy. diff --git a/options/license/MIT b/options/license/MIT index 2071b23b0e..d817195dad 100644 --- a/options/license/MIT +++ b/options/license/MIT @@ -2,8 +2,17 @@ MIT License Copyright (c) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/options/license/MIT-STK b/options/license/MIT-STK new file mode 100644 index 0000000000..40397a37b1 --- /dev/null +++ b/options/license/MIT-STK @@ -0,0 +1,27 @@ +The Synthesis ToolKit in C++ (STK) + +Copyright (c) 1995-2023 Perry R. Cook and Gary P. Scavone + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +Any person wishing to distribute modifications to the Software is +asked to send the modifications to the original developer so that they +can be incorporated into the canonical version. This is, however, not +a binding provision of this license. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/options/license/MIT-open-group b/options/license/MIT-open-group index ff185d30ed..18ee4889bd 100644 --- a/options/license/MIT-open-group +++ b/options/license/MIT-open-group @@ -12,10 +12,10 @@ in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL BE LIABLE FOR ANY CLAIM, DAMAGES -OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR -THE USE OR OTHER DEALINGS IN THE SOFTWARE. +IN NO EVENT SHALL THE OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name of The Open Group shall not be used in advertising or otherwise to promote the sale, use diff --git a/options/license/MMPL-1.0.1 b/options/license/MMPL-1.0.1 new file mode 100644 index 0000000000..6556291ea2 --- /dev/null +++ b/options/license/MMPL-1.0.1 @@ -0,0 +1,87 @@ +Minecraft Mod Public License +============================ + +Version 1.0.1 + +0. Definitions +-------------- + +Minecraft: Denotes a copy of the Minecraft game licensed by Mojang AB + +User: Anybody that interacts with the software in one of the following ways: + - play + - decompile + - recompile or compile + - modify + - distribute + +Mod: The mod code designated by the present license, in source form, binary +form, as obtained standalone, as part of a wider distribution or resulting from +the compilation of the original or modified sources. + +Dependency: Code required for the mod to work properly. This includes +dependencies required to compile the code as well as any file or modification +that is explicitely or implicitely required for the mod to be working. + +1. Scope +-------- + +The present license is granted to any user of the mod. As a prerequisite, +a user must own a legally acquired copy of Minecraft + +2. Liability +------------ + +This mod is provided 'as is' with no warranties, implied or otherwise. The owner +of this mod takes no responsibility for any damages incurred from the use of +this mod. This mod alters fundamental parts of the Minecraft game, parts of +Minecraft may not work with this mod installed. All damages caused from the use +or misuse of this mad fall on the user. + +3. Play rights +-------------- + +The user is allowed to install this mod on a client or a server and to play +without restriction. + +4. Modification rights +---------------------- + +The user has the right to decompile the source code, look at either the +decompiled version or the original source code, and to modify it. + +5. Derivation rights +-------------------- + +The user has the rights to derive code from this mod, that is to say to +write code that extends or instanciate the mod classes or interfaces, refer to +its objects, or calls its functions. This code is known as "derived" code, and +can be licensed under a license different from this mod. + +6. Distribution of original or modified copy rights +--------------------------------------------------- + +Is subject to distribution rights this entire mod in its various forms. This +include: + - original binary or source forms of this mod files + - modified versions of these binaries or source files, as well as binaries + resulting from source modifications + - patch to its source or binary files + - any copy of a portion of its binary source files + +The user is allowed to redistribute this mod partially, in totality, or +included in a distribution. + +When distributing binary files, the user must provide means to obtain its +entire set of sources or modified sources at no costs. + +All distributions of this mod must remain licensed under the MMPL. + +All dependencies that this mod have on other mods or classes must be licensed +under conditions comparable to this version of MMPL, with the exception of the +Minecraft code and the mod loading framework (e.g. ModLoader, ModLoaderMP or +Bukkit). + +Modified version of binaries and sources, as well as files containing sections +copied from this mod, should be distributed under the terms of the present +license. diff --git a/options/license/Motosoto b/options/license/Motosoto index 4add8c6a39..a25cff026e 100644 --- a/options/license/Motosoto +++ b/options/license/Motosoto @@ -1,110 +1,372 @@ MOTOSOTO OPEN SOURCE LICENSE - Version 0.9.1 -This Motosoto Open Source License (the "License") applies to "Community Portal Server" and related software products as well as any updatesor maintenance releases of that software ("Motosoto Products") that are distributed by Motosoto.Com B.V. ("Licensor"). Any Motosoto Product licensed pursuant to this License is a "Licensed Product." Licensed Product, in its entirety, is protected by Dutch copyright law. This License identifies the terms under which you may use, copy, distribute or modify Licensed Product and has been submitted to the Open Software Initiative (OSI) for approval. +This Motosoto Open Source License (the "License") applies to "Community Portal Server" and related +software products as well as any updatesor maintenance releases of that software ("Motosoto +Products") that are distributed by Motosoto.Com B.V. ("Licensor"). Any Motosoto Product licensed +pursuant to this License is a "Licensed Product." Licensed Product, in its entirety, is protected +by Dutch copyright law. This License identifies the terms under which you may use, copy, distribute +or modify Licensed Product and has been submitted to the Open Software Initiative (OSI) for +approval. Preamble -This Preamble is intended to describe, in plain English, the nature and scope of this License. However, this Preamble is not a part of this license. The legal effect of this License is dependent only upon the terms of the License and not this Preamble. This License complies with the Open Source Definition and has been approved by Open Source Initiative. Software distributed under this License may be marked as "OSI Certified Open Source Software." +This Preamble is intended to describe, in plain English, the nature and scope of this License. +However, this Preamble is not a part of this license. The legal effect of this License is dependent +only upon the terms of the License and not this Preamble. This License complies with the Open +Source Definition and has been approved by Open Source Initiative. Software distributed under this +License may be marked as "OSI Certified Open Source Software." This License provides that: -1. You may use, sell or give away the Licensed Product, alone or as a component of an aggregate software distribution containing programs from several different sources. No royalty or other fee is required. +1. You may use, sell or give away the Licensed Product, alone or as a component of an aggregate +software distribution containing programs from several different sources. No royalty or other fee +is required. -2. Both Source Code and executable versions of the Licensed Product, including Modifications made by previous Contributors, are available for your use. (The terms "Licensed Product," "Modifications," "Contributors" and "Source Code" are defined in the License.) +2. Both Source Code and executable versions of the Licensed Product, including Modifications made +by previous Contributors, are available for your use. (The terms "Licensed Product," +"Modifications," "Contributors" and "Source Code" are defined in the License.) -3. You are allowed to make Modifications to the Licensed Product, and you can create Derivative Works from it. (The term "Derivative Works" is defined in the License.) +3. You are allowed to make Modifications to the Licensed Product, and you can create Derivative +Works from it. (The term "Derivative Works" is defined in the License.) -4. By accepting the Licensed Product under the provisions of this License, you agree that any Modifications you make to the Licensed Product and then distribute are governed by the provisions of this License. In particular, you must make the Source Code of your Modifications available to others. +4. By accepting the Licensed Product under the provisions of this License, you agree that any +Modifications you make to the Licensed Product and then distribute are governed by the provisions +of this License. In particular, you must make the Source Code of your Modifications available to +others. -5. You may use the Licensed Product for any purpose, but the Licensor is not providing you any warranty whatsoever, nor is the Licensor accepting any liability in the event that the Licensed Product doesn't work properly or causes you any injury or damages. +5. You may use the Licensed Product for any purpose, but the Licensor is not providing you any +warranty whatsoever, nor is the Licensor accepting any liability in the event that the Licensed +Product doesn't work properly or causes you any injury or damages. -6. If you sublicense the Licensed Product or Derivative Works, you may charge fees for warranty or support, or for accepting indemnity or liability obligations to your customers. You cannot charge for the Source Code. +6. If you sublicense the Licensed Product or Derivative Works, you may charge fees for warranty or +support, or for accepting indemnity or liability obligations to your customers. You cannot charge +for the Source Code. -7. If you assert any patent claims against the Licensor relating to the Licensed Product, or if you breach any terms of the License, your rights to the Licensed Product under this License automatically terminate. +7. If you assert any patent claims against the Licensor relating to the Licensed Product, or if you +breach any terms of the License, your rights to the Licensed Product under this License +automatically terminate. -You may use this License to distribute your own Derivative Works, in which case the provisions of this License will apply to your Derivative Works just as they do to the original Licensed Product. +You may use this License to distribute your own Derivative Works, in which case the provisions of +this License will apply to your Derivative Works just as they do to the original Licensed Product. -Alternatively, you may distribute your Derivative Works under any other OSI-approved Open Source license, or under a proprietary license of your choice. If you use any license other than this License, however, you must continue to fulfill the requirements of this License (including the provisions relating to publishing the Source Code) for those portions of your Derivative Works that consist of the Licensed Product, including the files containing Modifications. +Alternatively, you may distribute your Derivative Works under any other OSI-approved Open Source +license, or under a proprietary license of your choice. If you use any license other than this +License, however, you must continue to fulfill the requirements of this License (including the +provisions relating to publishing the Source Code) for those portions of your Derivative Works that +consist of the Licensed Product, including the files containing Modifications. -New versions of this License may be published from time to time. You may choose to continue to use the license terms in this version of the License or those from the new version. However, only the Licensor has the right to change the License terms as they apply to the Licensed Product. This License relies on precise definitions for certain terms. Those terms are defined when they are first used, and the definitions are repeated for your convenience in a Glossary at the end of the License. +New versions of this License may be published from time to time. You may choose to continue to use +the license terms in this version of the License or those from the new version. However, only the +Licensor has the right to change the License terms as they apply to the Licensed Product. This +License relies on precise definitions for certain terms. Those terms are defined when they are +first used, and the definitions are repeated for your convenience in a Glossary at the end of the +License. License Terms 1. Grant of License From Licensor. -Licensor hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims, to do the following: +Licensor hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third +party intellectual property claims, to do the following: - a. Use, reproduce, modify, display, perform, sublicense and distribute Licensed Product or portions thereof (including Modifications as hereinafter defined), in both Source Code or as an executable program. "Source Code" means the preferred form for making modifications to the Licensed Product, including all modules contained therein, plus any associated interface definition files, scripts used to control compilation and installation of an executable program, or a list of differential comparisons against the Source Code of the Licensed Product. + a. Use, reproduce, modify, display, perform, sublicense and distribute Licensed Product or +portions thereof (including Modifications as hereinafter defined), in both Source Code or as an +executable program. "Source Code" means the preferred form for making modifications to the Licensed +Product, including all modules contained therein, plus any associated interface definition files, +scripts used to control compilation and installation of an executable program, or a list of +differential comparisons against the Source Code of the Licensed Product. - b. Create Derivative Works (as that term is defined under Dutch copyright law) of Licensed Product by adding to or deleting from the substance or structure of said Licensed Product. + b. Create Derivative Works (as that term is defined under Dutch copyright law) of Licensed +Product by adding to or deleting from the substance or structure of said Licensed Product. - c. Under claims of patents now or hereafter owned or controlled by Licensor, to make, use, sell, offer for sale, have made, and/or otherwise dispose of Licensed Product or portions thereof, but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for sale, have made, and/or otherwise dispose of Licensed Product or portions thereof or Derivative Works thereof. + c. Under claims of patents now or hereafter owned or controlled by Licensor, to make, use, +sell, offer for sale, have made, and/or otherwise dispose of Licensed Product or portions thereof, +but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer +for sale, have made, and/or otherwise dispose of Licensed Product or portions thereof or Derivative +Works thereof. 2. Grant of License to Modifications From Contributor. -"Modifications" means any additions to or deletions from the substance or structure of (i) a file containing Licensed Product, or (ii) any new file that contains any part of Licensed Product. Hereinafter in this License, the term "Licensed Product" shall include all previous Modifications that you receive from any Contributor. By application of the provisions in Section 4(a) below, each person or entity who created or contributed to the creation of, and distributed, a Modification (a "Contributor") hereby grants you a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims, to do the following: +"Modifications" means any additions to or deletions from the substance or structure of (i) a file +containing Licensed Product, or (ii) any new file that contains any part of Licensed Product. +Hereinafter in this License, the term "Licensed Product" shall include all previous Modifications +that you receive from any Contributor. By application of the provisions in Section 4(a) below, each +person or entity who created or contributed to the creation of, and distributed, a Modification (a +"Contributor") hereby grants you a world-wide, royalty-free, non-exclusive license, subject to +third party intellectual property claims, to do the following: - a. Use, reproduce, modify, display, perform, sublicense and distribute any Modifications created by such Contributor or portions thereof, in both Source Code or as an executable program, either on an unmodified basis or as part of Derivative Works. + a. Use, reproduce, modify, display, perform, sublicense and distribute any Modifications +created by such Contributor or portions thereof, in both Source Code or as an executable program, +either on an unmodified basis or as part of Derivative Works. - b. Under claims of patents now or hereafter owned or controlled by Contributor, to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof, but solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof or Derivative Works thereof. + b. Under claims of patents now or hereafter owned or controlled by Contributor, to make, use, +sell, offer for sale, have made, and/or otherwise dispose of Modifications or portions thereof, but +solely to the extent that any such claim is necessary to enable you to make, use, sell, offer for +sale, have made, and/or otherwise dispose of Modifications or portions thereof or Derivative Works +thereof. 3. Exclusions From License Grant. -Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, trade secrets or any other intellectual property of Licensor or any Contributor except as expressly stated herein. No patent license is granted separate from the Licensed Product, for code that you delete from the Licensed Product, or for combinations of the Licensed Product with other software or hardware. No right is granted to the trademarks of Licensor or any Contributor even if such marks are included in the Licensed Product. Nothing in this License shall be interpreted to prohibit Licensor from licensing under different terms from this License any code that Licensor otherwise would have a right to license. +Nothing in this License shall be deemed to grant any rights to trademarks, copyrights, patents, +trade secrets or any other intellectual property of Licensor or any Contributor except as expressly +stated herein. No patent license is granted separate from the Licensed Product, for code that you +delete from the Licensed Product, or for combinations of the Licensed Product with other software +or hardware. No right is granted to the trademarks of Licensor or any Contributor even if such +marks are included in the Licensed Product. Nothing in this License shall be interpreted to +prohibit Licensor from licensing under different terms from this License any code that Licensor +otherwise would have a right to license. 4. Your Obligations Regarding Distribution. - a. Application of This License to Your Modifications. As an express condition for your use of the Licensed Product, you hereby agree that any Modifications that you create or to which you contribute, and which you distribute, are governed by the terms of this License including, without limitation, Section 2. Any Modifications that you create or to which you contribute may be distributed only under the terms of this License or a future version of this License released under Section 7. You must include a copy of this License with every copy of the Modifications you distribute. You agree not to offer or impose any terms on any Source Code or executable version of the Licensed Product or Modifications that alter or restrict the applicable version of this License or the recipients' rights hereunder. However, you may include an additional document offering the additional rights described in Section 4(e). + a. Application of This License to Your Modifications. As an express condition for your use of +the Licensed Product, you hereby agree that any Modifications that you create or to which you +contribute, and which you distribute, are governed by the terms of this License including, without +limitation, Section 2. Any Modifications that you create or to which you contribute may be +distributed only under the terms of this License or a future version of this License released under +Section 7. You must include a copy of this License with every copy of the Modifications you +distribute. You agree not to offer or impose any terms on any Source Code or executable version of +the Licensed Product or Modifications that alter or restrict the applicable version of this License +or the recipients' rights hereunder. However, you may include an additional document offering the +additional rights described in Section 4(e). - b. Availability of Source Code. You must make available, under the terms of this License, the Source Code of the Licensed Product and any Modifications that you distribute, either on the same media as you distribute any executable or other form of the Licensed Product, or via a mechanism generally accepted in the software development community for the electronic transfer of data (an "Electronic Distribution Mechanism"). The Source Code for any version of Licensed Product or Modifications that you distribute must remain available for at least twelve (12) months after the date it initially became available, or at least six (6) months after a subsequent version of said Licensed Product or Modifications has been made available. You are responsible for ensuring that the Source Code version remains available even if the Electronic Distribution Mechanism is maintained by a third party. + b. Availability of Source Code. You must make available, under the terms of this License, the +Source Code of the Licensed Product and any Modifications that you distribute, either on the same +media as you distribute any executable or other form of the Licensed Product, or via a mechanism +generally accepted in the software development community for the electronic transfer of data (an +"Electronic Distribution Mechanism"). The Source Code for any version of Licensed Product or +Modifications that you distribute must remain available for at least twelve (12) months after the +date it initially became available, or at least six (6) months after a subsequent version of said +Licensed Product or Modifications has been made available. You are responsible for ensuring that +the Source Code version remains available even if the Electronic Distribution Mechanism is +maintained by a third party. - c. Description of Modifications. You must cause any Modifications that you create or to which you contribute, and which you distribute, to contain a file documenting the additions, changes or deletions you made to create or contribute to those Modifications, and the dates of any such additions, changes or deletions. You must include a prominent statement that the Modifications are derived, directly or indirectly, from the Licensed Product and include the names of the Licensor and any Contributor to the Licensed Product in (i) the Source Code and (ii) in any notice displayed by a version of the Licensed Product you distribute or in related documentation in which you describe the origin or ownership of the Licensed Product. You may not modify or delete any preexisting copyright notices in the Licensed Product. + c. Description of Modifications. You must cause any Modifications that you create or to which +you contribute, and which you distribute, to contain a file documenting the additions, changes or +deletions you made to create or contribute to those Modifications, and the dates of any such +additions, changes or deletions. You must include a prominent statement that the Modifications are +derived, directly or indirectly, from the Licensed Product and include the names of the Licensor +and any Contributor to the Licensed Product in (i) the Source Code and (ii) in any notice displayed +by a version of the Licensed Product you distribute or in related documentation in which you +describe the origin or ownership of the Licensed Product. You may not modify or delete any +preexisting copyright notices in the Licensed Product. d. Intellectual Property Matters. - i. Third Party Claims. If you have knowledge that a license to a third party's intellectual property right is required to exercise the rights granted by this License, you must include a text file with the Source Code distribution titled "LEGAL" that describes the claim and the party making the claim in sufficient detail that a recipient will know whom to contact. If you obtain such knowledge after you make any Modifications available as described in Section 4(b), you shall promptly modify the LEGAL file in all copies you make available thereafter and shall take other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to inform those who received the Licensed Product from you that new knowledge has been obtained. + i. Third Party Claims. If you have knowledge that a license to a third party's +intellectual property right is required to exercise the rights granted by this License, you must +include a text file with the Source Code distribution titled "LEGAL" that describes the claim and +the party making the claim in sufficient detail that a recipient will know whom to contact. If you +obtain such knowledge after you make any Modifications available as described in Section 4(b), you +shall promptly modify the LEGAL file in all copies you make available thereafter and shall take +other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to +inform those who received the Licensed Product from you that new knowledge has been obtained. - ii. Contributor APIs. If your Modifications include an application programming interface ("API") and you have knowledge of patent licenses that are reasonably necessary to implement that API, you must also include this information in the LEGAL file. + ii. Contributor APIs. If your Modifications include an application programming interface +("API") and you have knowledge of patent licenses that are reasonably necessary to implement that +API, you must also include this information in the LEGAL file. - iii. Representations. You represent that, except as disclosed pursuant to 4(d)(i) above, you believe that any Modifications you distribute are your original creations and that you have sufficient rights to grant the rights conveyed by this License. + iii. Representations. You represent that, except as disclosed pursuant to 4(d)(i) above, +you believe that any Modifications you distribute are your original creations and that you have +sufficient rights to grant the rights conveyed by this License. - e. Required Notices. You must duplicate this License in any documentation you provide along with the Source Code of any Modifications you create or to which you contribute, and which you distribute, wherever you describe recipients' rights relating to Licensed Product. You must duplicate the notice contained in Exhibit A (the "Notice") in each file of the Source Code of any copy you distribute of the Licensed Product. If you created a Modification, you may add your name as a Contributor to the Notice. If it is not possible to put the Notice in a particular Source Code file due to its structure, then you must include such Notice in a location (such as a relevant directory file) where a user would be likely to look for such a notice. You may choose to offer, and charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Licensed Product. However, you may do so only on your own behalf, and not on behalf of the Licensor or any Contributor. You must make it clear that any such warranty, support, indemnity or liability obligation is offered by you alone, and you hereby agree to indemnify the Licensor and every Contributor for any liability incurred by the Licensor or such Contributor as a result of warranty, support, indemnity or liability terms you offer. + e. Required Notices. You must duplicate this License in any documentation you provide along +with the Source Code of any Modifications you create or to which you contribute, and which you +distribute, wherever you describe recipients' rights relating to Licensed Product. You must +duplicate the notice contained in Exhibit A (the "Notice") in each file of the Source Code of any +copy you distribute of the Licensed Product. If you created a Modification, you may add your name +as a Contributor to the Notice. If it is not possible to put the Notice in a particular Source Code +file due to its structure, then you must include such Notice in a location (such as a relevant +directory file) where a user would be likely to look for such a notice. You may choose to offer, +and charge a fee for, warranty, support, indemnity or liability obligations to one or more +recipients of Licensed Product. However, you may do so only on your own behalf, and not on behalf +of the Licensor or any Contributor. You must make it clear that any such warranty, support, +indemnity or liability obligation is offered by you alone, and you hereby agree to indemnify the +Licensor and every Contributor for any liability incurred by the Licensor or such Contributor as a +result of warranty, support, indemnity or liability terms you offer. - f. Distribution of Executable Versions. You may distribute Licensed Product as an executable program under a license of your choice that may contain terms different from this License provided (i) you have satisfied the requirements of Sections 4(a) through 4(e) for that distribution, (ii) you include a conspicuous notice in the executable version, related documentation and collateral materials stating that the Source Code version of the Licensed Product is available under the terms of this License, including a description of how and where you have fulfilled the obligations of Section 4(b), (iii) you retain all existing copyright notices in the Licensed Product, and (iv) you make it clear that any terms that differ from this License are offered by you alone, not by Licensor or any Contributor. You hereby agree to indemnify the Licensor and every Contributor for any liability incurred by Licensor or such Contributor as a result of any terms you offer. + f. Distribution of Executable Versions. You may distribute Licensed Product as an executable +program under a license of your choice that may contain terms different from this License provided +(i) you have satisfied the requirements of Sections 4(a) through 4(e) for that distribution, (ii) +you include a conspicuous notice in the executable version, related documentation and collateral +materials stating that the Source Code version of the Licensed Product is available under the terms +of this License, including a description of how and where you have fulfilled the obligations of +Section 4(b), (iii) you retain all existing copyright notices in the Licensed Product, and (iv) you +make it clear that any terms that differ from this License are offered by you alone, not by +Licensor or any Contributor. You hereby agree to indemnify the Licensor and every Contributor for +any liability incurred by Licensor or such Contributor as a result of any terms you offer. - g. Distribution of Derivative Works. You may create Derivative Works (e.g., combinations of some or all of the Licensed Product with other code) and distribute the Derivative Works as products under any other license you select, with the proviso that the requirements of this License are fulfilled for those portions of the Derivative Works that consist of the Licensed Product or any Modifications thereto. + g. Distribution of Derivative Works. You may create Derivative Works (e.g., combinations of +some or all of the Licensed Product with other code) and distribute the Derivative Works as +products under any other license you select, with the proviso that the requirements of this License +are fulfilled for those portions of the Derivative Works that consist of the Licensed Product or +any Modifications thereto. 5. Inability to Comply Due to Statute or Regulation. -If it is impossible for you to comply with any of the terms of this License with respect to some or all of the Licensed Product due to statute, judicial order, or regulation, then you must (i) comply with the terms of this License to the maximum extent possible, (ii) cite the statute or regulation that prohibits you from adhering to the License, and (iii) describe the limitations and the code they affect. Such description must be included in the LEGAL file described in Section 4(d), and must be included with all distributions of the Source Code. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill at computer programming to be able to understand it. +If it is impossible for you to comply with any of the terms of this License with respect to some or +all of the Licensed Product due to statute, judicial order, or regulation, then you must (i) comply +with the terms of this License to the maximum extent possible, (ii) cite the statute or regulation +that prohibits you from adhering to the License, and (iii) describe the limitations and the code +they affect. Such description must be included in the LEGAL file described in Section 4(d), and +must be included with all distributions of the Source Code. Except to the extent prohibited by +statute or regulation, such description must be sufficiently detailed for a recipient of ordinary +skill at computer programming to be able to understand it. 6. Application of This License. -This License applies to code to which Licensor or Contributor has attached the Notice in Exhibit A, which is incorporated herein by this reference. +This License applies to code to which Licensor or Contributor has attached the Notice in Exhibit A, +which is incorporated herein by this reference. 7. Versions of This License. - a. Version. The Motosoto Open Source License is derived from the Jabber Open Source License. All changes are related to applicable law and the location of court. + a. Version. The Motosoto Open Source License is derived from the Jabber Open Source License. +All changes are related to applicable law and the location of court. - b. New Versions. Licensor may publish from time to time revised and/or new versions of the License. + b. New Versions. Licensor may publish from time to time revised and/or new versions of the +License. - c. Effect of New Versions. Once Licensed Product has been published under a particular version of the License, you may always continue to use it under the terms of that version. You may also choose to use such Licensed Product under the terms of any subsequent version of the License published by Licensor. No one other than Lic ensor has the right to modify the terms applicable to Licensed Product created under this License. + c. Effect of New Versions. Once Licensed Product has been published under a particular version +of the License, you may always continue to use it under the terms of that version. You may also +choose to use such Licensed Product under the terms of any subsequent version of the License +published by Licensor. No one other than Lic ensor has the right to modify the terms applicable to +Licensed Product created under this License. - d. Derivative Works of this License. If you create or use a modified version of this License, which you may do only in order to apply it to software that is not already a Licensed Product under this License, you must rename your license so that it is not confusingly similar to this License, and must make it clear that your license contains terms that differ from this License. In so naming your license, you may not use any trademark of Licensor or any Contributor. + d. Derivative Works of this License. If you create or use a modified version of this License, +which you may do only in order to apply it to software that is not already a Licensed Product under +this License, you must rename your license so that it is not confusingly similar to this License, +and must make it clear that your license contains terms that differ from this License. In so naming +your license, you may not use any trademark of Licensor or any Contributor. 8. Disclaimer of Warranty. -LICENSED PRODUCT IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE LICENSED PRODUCT IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LICENSED PRODUCT IS WITH YOU. SHOULD LICENSED PRODUCT PROVE DEFECTIVE IN ANY RESPECT, YOU (AND NOT THE LICENSOR OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF LICENSED PRODUCT IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. +LICENSED PRODUCT IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, +EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE LICENSED PRODUCT IS +FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE LICENSED PRODUCT IS WITH YOU. SHOULD LICENSED PRODUCT PROVE +DEFECTIVE IN ANY RESPECT, YOU (AND NOT THE LICENSOR OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF +ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL +PART OF THIS LICENSE. NO USE OF LICENSED PRODUCT IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS +DISCLAIMER. 9. Termination. - a. Automatic Termination Upon Breach. This license and the rights granted hereunder will terminate automatically if you fail to comply with the terms herein and fail to cure such breach within thirty (30) days of becoming aware of the breach. All sublicenses to the Licensed Product that are properly granted shall survive any termination of this license. Provisions that, by their nature, must remain in effect beyond the termination of this License, shall survive. + a. Automatic Termination Upon Breach. This license and the rights granted hereunder will +terminate automatically if you fail to comply with the terms herein and fail to cure such breach +within thirty (30) days of becoming aware of the breach. All sublicenses to the Licensed Product +that are properly granted shall survive any termination of this license. Provisions that, by their +nature, must remain in effect beyond the termination of this License, shall survive. - b. Termination Upon Assertion of Patent Infringement. If you initiate litigation by asserting a patent infringement claim (excluding declaratory judgment actions) against Licensor or a Contributor (Licensor or Contributor against whom you file such an action is referred to herein as "Respondent") alleging that Licensed Product directly or indirectly infringes any patent, then any and all rights granted by such Respondent to you under Sections 1 or 2 of this License shall terminate prospectively upon sixty (60) days notice from Respondent (the "Notice Period") unless within that Notice Period you either agree in writing (i) to pay Respondent a mutually agreeable reasonably royalty for your past or future use of Licensed Product made by such Respondent, or (ii) withdraw your litigation claim with respect to Licensed Product against such Respondent. If within said Notice Period a reasonable royalty and payment arrangement are not mutually agreed upon in writing by the parties or the litigation claim is not withdrawn, the rights granted by Licensor to you under Sections 1 and 2 automatically terminate at the expiration of said Notice Period. + b. Termination Upon Assertion of Patent Infringement. If you initiate litigation by asserting +a patent infringement claim (excluding declaratory judgment actions) against Licensor or a +Contributor (Licensor or Contributor against whom you file such an action is referred to herein as +"Respondent") alleging that Licensed Product directly or indirectly infringes any patent, then any +and all rights granted by such Respondent to you under Sections 1 or 2 of this License shall +terminate prospectively upon sixty (60) days notice from Respondent (the "Notice Period") unless +within that Notice Period you either agree in writing (i) to pay Respondent a mutually agreeable +reasonably royalty for your past or future use of Licensed Product made by such Respondent, or (ii) +withdraw your litigation claim with respect to Licensed Product against such Respondent. If within +said Notice Period a reasonable royalty and payment arrangement are not mutually agreed upon in +writing by the parties or the litigation claim is not withdrawn, the rights granted by Licensor to +you under Sections 1 and 2 automatically terminate at the expiration of said Notice Period. - c. Reasonable Value of This License. If you assert a patent infringement claim against Respondent alleging that Licensed Product directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by said Respondent under Sections 1 and 2 shall be taken into account in determining the amount or value of any payment or license. + c. Reasonable Value of This License. If you assert a patent infringement claim against +Respondent alleging that Licensed Product directly or indirectly infringes any patent where such +claim is resolved (such as by license or settlement) prior to the initiation of patent infringement +litigation, then the reasonable value of the licenses granted by said Respondent under Sections 1 +and 2 shall be taken into account in determining the amount or value of any payment or license. - d. No Retroactive Effect of Termination. In the event of termination under Sections 9(a) or 9(b) above, all end user license agreements (excluding licenses to distributors and reselle rs) that have been validly granted by you or any distributor hereunder prior to termination shall survive termination. + d. No Retroactive Effect of Termination. In the event of termination under Sections 9(a) or +9(b) above, all end user license agreements (excluding licenses to distributors and reselle rs) +that have been validly granted by you or any distributor hereunder prior to termination shall +survive termination. 10. Limitation of Liability. -UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL THE LICENSOR, ANY CONTRIBUTOR, OR ANY DISTRIBUTOR OF LICENSED PRODUCT, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY +UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR +OTHERWISE, SHALL THE LICENSOR, ANY CONTRIBUTOR, OR ANY DISTRIBUTOR OF LICENSED PRODUCT, OR ANY +SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR +CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, +WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, +EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF +LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY's +NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE  +EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY  +NOT APPLY TO YOU. + +11. Responsibility for Claims.  + +As between Licensor and Contributors, each party is responsible for claims and damages arising,  +directly or indirectly, out of its utilization of rights under this License. You agree to work with  +Licensor and Contributors to distribute such responsibility on an equitable basis. Nothing herein is  +intended or shall be deemed to constitute any admission of liability. + +12. U.S. Government End Users.  + +The Licensed Product is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995),  +consisting of "commercial computer software" and "commercial computer software documentation,"  +as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and  +48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire  +Licensed Product with only those rights set forth herein. + +13. Miscellaneous.  +This License represents the complete agreement concerning the subject matter hereof. If any  +provision of this License is held to be unenforceable, such provision shall be reformed only  +to the extent necessary to make it enforceable. This License shall be governed by Dutch law  +provisions. The application of the United Nations Convention on Contracts for the International  +Sale of Goods is expressly excluded. You and Licensor expressly waive any rights to a jury trial  +in any litigation concerning Licensed Product or this License. Any law or regulation that provides  +that the language of a contract shall be construed against the drafter shall not apply to this License. + +14. Definition of "You" in This License.  +"You" throughout this License, whether in upper or lower case, means an individual or a legal entity  +exercising rights under, and complying with all of the terms of, this License or a future version of  +this License issued under Section 7. For legal entities, "you" includes any entity that controls, is  +controlled by, or is under common control with you. For purposes of this definition, "control" means  +(i) the power, direct or indirect, to cause the direction or management of such entity, whether by  +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares,  +or (iii) beneficial ownership of such entity. + +15. Glossary. +All defined terms in this License that are used in more than one Section of this License are  +repeated here, in alphabetical order, for the convenience of the reader. The Section of this  +License in which each defined term is first used is shown in parentheses.  + +Contributor: Each person or entity who created or contributed to the creation of, and distributed, a Modification. (See Section 2) + +Derivative Works: That term as used in this License is defined under Dutch copyright law. (See Section 1(b)) + +License: This Motosoto Open Source License. (See first paragraph of License) + +Licensed Product: Any Motosoto Product licensed pursuant to this License. The term +"Licensed Product" includes all previous Modifications from any Contributor that you receive.  +(See first paragraph of License and Section 2) + +Licensor: Motosoto.Com B.V.. (See first paragraph of License) + +Modifications: Any additions to or deletions from the substance or structure of (i) a file  +containing Licensed Product, or (ii) any new file that contains any part of Licensed Product. (See Section 2) + +Notice: The notice contained in Exhibit A. (See Section 4(e)) + +Source Code: The preferred form for making modifications to the Licensed Product, including  +all modules contained therein, plus any associated interface definition files, scripts used  +to control compilation and installation of an executable program, or a list of differential  +comparisons against the Source Code of the Licensed Product. (See Section 1(a)) + +You: This term is defined in Section 14 of this License. +  +EXHIBIT A +The Notice below must appear in each file of the Source Code of any copy you distribute of the Licensed Product or any Modifications thereto. Contributors to any Modifications may add their own copyright notices to identify their own contributions. + +License: +The contents of this file are subject to the Motosoto Open Source License Version 0.9 (the "License"). You may not copy or use this file, in either source code or executable form, except in compliance with the License. You may obtain a copy of the License at http://www.motosoto.com/license/ or at http://www.opensource.org/. + +Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. + +Copyrights: +Portions created by or assigned to Motosoto.com B.V. are Copyright (c) 2000-2001 Motosoto.com B.V. +All Rights Reserved. Contact information for Motosoto.com B.V. is available at http://www.motosoto.com/. + +Acknowledgements +Special thanks to the Motosoto Open Source Contributors for their suggestions and support of Motosoto. + +Modifications: diff --git a/options/license/NIST-PD-TNT b/options/license/NIST-PD-TNT new file mode 100644 index 0000000000..51202893ac --- /dev/null +++ b/options/license/NIST-PD-TNT @@ -0,0 +1,3 @@ +This software was developed at the National Institute of Standards and Technology (NIST) by employees of the Federal Government in the course of their official duties. Pursuant to title 17 Section 105 of the United States Code this software is not subject to copyright protection and is in the public domain. NIST assumes no responsibility whatsoever for its use by other parties, and makes no guarantees, expressed or implied, about its quality, reliability, or any other characteristic. + +We would appreciate acknowledgement if the software is incorporated in redistributable libraries or applications. diff --git a/options/license/NTIA-PD b/options/license/NTIA-PD new file mode 100644 index 0000000000..f451790073 --- /dev/null +++ b/options/license/NTIA-PD @@ -0,0 +1,13 @@ +SOFTWARE DISCLAIMER / RELEASE + +This software was developed by employees of the National Telecommunications and Information Administration (NTIA), an agency of the Federal Government and is provided to you as a public service. Pursuant to Title 15 United States Code Section 105, works of NTIA employees are not subject to copyright protection within the United States. + +The software is provided by NTIA “AS IS.” NTIA MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NTIA does not warrant or make any representations regarding the use of the software or the results thereof, including but not limited to the correctness, accuracy, reliability or usefulness of the software. + +To the extent that NTIA holds rights in countries other than the United States, you are hereby granted the non-exclusive irrevocable and unconditional right to print, publish, prepare derivative works and distribute the NTIA software, in any medium, or authorize others to do so on your behalf, on a royalty-free basis throughout the World. + +You may improve, modify, and create derivative works of the software or any portion of the software, and you may copy and distribute such modifications or works. Modified works should carry a notice stating that you changed the software and should note the date and nature of any such change. + +You are solely responsible for determining the appropriateness of using and distributing the software and you assume all risks associated with its use, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and the unavailability or interruption of operation. This software is not intended to be used in any situation where a failure could cause risk of injury or damage to property. + +Please provide appropriate acknowledgments of NTIA’s creation of the software in any copies or derivative works of this software. diff --git a/options/license/Net-SNMP b/options/license/Net-SNMP deleted file mode 100644 index 9ec271072f..0000000000 --- a/options/license/Net-SNMP +++ /dev/null @@ -1,107 +0,0 @@ - ---- Part 1: CMU/UCD copyright notice: (BSD like) ----- - - Copyright 1989, 1991, 1992 by Carnegie Mellon University - - Derivative Work - 1996, 1998-2000 Copyright 1996, 1998-2000 The Regents of the University of California - - All Rights Reserved - -Permission to use, copy, modify and distribute this software and its documentation for any purpose and without fee is hereby granted, provided that the above copyright notice appears in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of CMU and The Regents of the University of California not be used in advertising or publicity pertaining to distribution of the software without specific written permission. - -CMU AND THE REGENTS OF THE UNIVERSITY OF CALIFORNIA DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL CMU OR THE REGENTS OF THE UNIVERSITY OF CALIFORNIA BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM THE LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - ----- Part 2: Networks Associates Technology, Inc copyright notice (BSD) ----- - -Copyright (c) 2001-2003, Networks Associates Technology, Inc All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of the Networks Associates Technology, Inc nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- Part 3: Cambridge Broadband Ltd. copyright notice (BSD) ----- - -Portions of this code are copyright (c) 2001-2003, Cambridge Broadband Ltd. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * The name of Cambridge Broadband Ltd. may not be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- Part 4: Sun Microsystems, Inc. copyright notice (BSD) ----- - -Copyright © 2003 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, California 95054, U.S.A. All rights reserved. - -Use is subject to license terms below. - -This distribution may include materials developed by third parties. - -Sun, Sun Microsystems, the Sun logo and Solaris are trademarks or registered trademarks of Sun Microsystems, Inc. in the U.S. and other countries. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - - * Neither the name of the Sun Microsystems, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- Part 5: Sparta, Inc copyright notice (BSD) ----- - -Copyright (c) 2003-2009, Sparta, Inc All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Sparta, Inc nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- Part 6: Cisco/BUPTNIC copyright notice (BSD) ----- - -Copyright (c) 2004, Cisco, Inc and Information Network Center of Beijing University of Posts and Telecommunications. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Cisco, Inc, Beijing University of Posts and Telecommunications, nor the names of their contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- Part 7: Fabasoft R&D Software GmbH & Co KG copyright notice (BSD) ----- - -Copyright (c) Fabasoft R&D Software GmbH & Co KG, 2003 oss@fabasoft.com Author: Bernhard Penz - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - - * The name of Fabasoft R&D Software GmbH & Co KG or any of its subsidiaries, brand or product names may not be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- Part 8: Apple Inc. copyright notice (BSD) ----- - -Copyright (c) 2007 Apple Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - 3. Neither the name of Apple Inc. ("Apple") nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ----- Part 9: ScienceLogic, LLC copyright notice (BSD) ----- - -Copyright (c) 2009, ScienceLogic, LLC All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of ScienceLogic, LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/options/license/OSSP b/options/license/OSSP new file mode 100644 index 0000000000..92a1d4705d --- /dev/null +++ b/options/license/OSSP @@ -0,0 +1,26 @@ +COPYRIGHT AND LICENSE + + Copyright (c) 2001-2005 Ralf S. Engelschall + Copyright (c) 2001-2005 The OSSP Project + Copyright (c) 2001-2005 Cable & Wireless + + This file is part of OSSP var, a variable expansion + library which can be found at http://www.ossp.org/pkg/lib/var/. + + Permission to use, copy, modify, and distribute this software for + any purpose with or without fee is hereby granted, provided that + the above copyright notice and this permission notice appear in all + copies. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. diff --git a/options/license/OpenMDW-1.0 b/options/license/OpenMDW-1.0 new file mode 100644 index 0000000000..5166ede9b6 --- /dev/null +++ b/options/license/OpenMDW-1.0 @@ -0,0 +1,49 @@ +OpenMDW License Agreement, version 1.0 (OpenMDW-1.0) + +By exercising rights granted to you under this agreement, you accept and agree +to its terms. + +As used in this agreement, "Model Materials" means the materials provided to +you under this agreement, consisting of: (1) one or more machine learning +models (including architecture and parameters); and (2) all related artifacts +(including associated data, documentation and software) that are provided to +you hereunder. + +Subject to your compliance with this agreement, permission is hereby granted, +free of charge, to deal in the Model Materials without restriction, including +under all copyright, patent, database, and trade secret rights included or +embodied therein. + +If you distribute any portion of the Model Materials, you shall retain in your +distribution (1) a copy of this agreement, and (2) all copyright notices and +other notices of origin included in the Model Materials that are applicable to +your distribution. + +If you file, maintain, or voluntarily participate in a lawsuit against any +person or entity asserting that the Model Materials directly or indirectly +infringe any patent, then all rights and grants made to you hereunder are +terminated, unless that lawsuit was in response to a corresponding lawsuit +first brought against you. + +This agreement does not impose any restrictions or obligations with respect to +any use, modification, or sharing of any outputs generated by using the Model +Materials. + +THE MODEL MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE, NONINFRINGEMENT, ACCURACY, OR THE +ABSENCE OF LATENT OR OTHER DEFECTS OR ERRORS, WHETHER OR NOT DISCOVERABLE, ALL +TO THE GREATEST EXTENT PERMISSIBLE UNDER APPLICABLE LAW. + +YOU ARE SOLELY RESPONSIBLE FOR (1) CLEARING RIGHTS OF OTHER PERSONS THAT MAY +APPLY TO THE MODEL MATERIALS OR ANY USE THEREOF, INCLUDING WITHOUT LIMITATION +ANY PERSON'S COPYRIGHTS OR OTHER RIGHTS INCLUDED OR EMBODIED IN THE MODEL +MATERIALS; (2) OBTAINING ANY NECESSARY CONSENTS, PERMISSIONS OR OTHER RIGHTS +REQUIRED FOR ANY USE OF THE MODEL MATERIALS; OR (3) PERFORMING ANY DUE +DILIGENCE OR UNDERTAKING ANY OTHER INVESTIGATIONS INTO THE MODEL MATERIALS OR +ANYTHING INCORPORATED OR EMBODIED THEREIN. + +IN NO EVENT SHALL THE PROVIDERS OF THE MODEL MATERIALS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MODEL MATERIALS, THE +USE THEREOF OR OTHER DEALINGS THEREIN. diff --git a/options/license/ParaType-Free-Font-1.3 b/options/license/ParaType-Free-Font-1.3 new file mode 100644 index 0000000000..21f8140887 --- /dev/null +++ b/options/license/ParaType-Free-Font-1.3 @@ -0,0 +1,24 @@ +ParaType Free Font Licensing Agreement + +Copyright (c) 2009, ParaType Ltd. All Rights Reserved. + +LICENSING AGREEMENT +for the fonts with Original Name: PT Sans, PT Serif, PT Mono +Version 1.3 - January 20, 2012 + +GRANT OF LICENSE +ParaType Ltd grants you the right to use, copy, modify the fonts and distribute modified and unmodified copies of the fonts by any means, including placing on Web servers for free downloading, embedding in documents and Web pages, bundling with commercial and non commercial products, if it does not conflict with the conditions listed below: + +- You may bundle the fonts with commercial software, but you may not sell the fonts by themselves. They are free. + +- You may distribute the fonts in modified or unmodified versions only together with this Licensing Agreement and with above copyright notice. You have no right to modify the text of Licensing Agreement. It can be placed in a separate text file or inserted into the font file, but it must be easily viewed by users. + +- You may not distribute modified version of the font under the Original name or а combination of Original name with any other words without explicit written permission from ParaType. + +TERMINATION & TERRITORY +This license has no limits on time and territory, but it becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL PARATYPE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. + +ParaType Ltd diff --git a/options/license/Ruby-pty b/options/license/Ruby-pty new file mode 100644 index 0000000000..c817762f84 --- /dev/null +++ b/options/license/Ruby-pty @@ -0,0 +1,10 @@ +(c) Copyright 1998 by Akinori Ito. + +This software may be redistributed freely for this purpose, in full +or in part, provided that this entire copyright notice is included +on any copies of this software and applications and derivations thereof. + +This software is provided on an "as is" basis, without warranty of any +kind, either expressed or implied, as to any matter including, but not +limited to warranty of fitness of purpose, or merchantability, or +results obtained from use of this software. diff --git a/options/license/SGMLUG-PM b/options/license/SGMLUG-PM new file mode 100644 index 0000000000..552f8be94a --- /dev/null +++ b/options/license/SGMLUG-PM @@ -0,0 +1,43 @@ +LICENSE AND DISCLAIMER OF WARRANTIES + + Standard Generalized Markup Language Users' Group (SGMLUG) + SGML Parser Materials + + 1. License + +SGMLUG hereby grants to any user: (1) an irrevocable royalty-free, +worldwide, non-exclusive license to use, execute, reproduce, display, +perform and distribute copies of, and to prepare derivative works +based upon these materials; and (2) the right to authorize others to +do any of the foregoing. + + 2. Disclaimer of Warranties + +(a) The SGML Parser Materials are provided "as is" to any USER. USER +assumes responsibility for determining the suitability of the SGML +Parser Materials for its use and for results obtained. SGMLUG makes +no warranty that any errors have been eliminated from the SGML Parser +Materials or that they can be eliminated by USER. SGMLUG shall not +provide any support maintenance or other aid to USER or its licensees +with respect to SGML Parser Materials. SGMLUG shall not be +responsible for losses of any kind resulting from use of the SGML +Parser Materials including (without limitation) any liability for +business expense, machine downtime, or damages caused to USER or third +parties by any deficiency, defect, error, or malfunction. + +(b) SGMLUG DISCLAIMS ALL WARRANTIES, EXPRESSED OR IMPLIED, ARISING OUT +OF OR RELATING TO THE SGML PARSER MATERIALS OR ANY USE THEREOF, +INCLUDING (WITHOUT LIMITATION) ANY WARRANTY WHATSOEVER AS TO THE +FITNESS FOR A PARTICULAR USE OR THE MERCHANTABILITY OF THE SGML PARSER +MATERIALS. + +(c) In no event shall SGMLUG be liable to USER or third parties +licensed by USER for any indirect, special, incidental, or +consequential damages (including lost profits). +(d) SGMLUG has no knowledge of any conditions that would impair its right +to license the SGML Parser Materials. Notwithstanding the foregoing, +SGMLUG does not make any warranties or representations that the +SGML Parser Materials are free of claims by third parties of patent, +copyright infringement or the like, nor does SGMLUG assume any +liability in respect of any such infringement of rights of third +parties due to USER's operation under this license. diff --git a/options/license/SMAIL-GPL b/options/license/SMAIL-GPL new file mode 100644 index 0000000000..be799ec39d --- /dev/null +++ b/options/license/SMAIL-GPL @@ -0,0 +1,144 @@ +SMAIL GENERAL PUBLIC LICENSE + (Clarified 11 Feb 1988) + + Copyright (C) 1988 Landon Curt Noll & Ronald S. Karr + Copyright (C) 1992 Ronald S. Karr + Copyleft (GNU) 1988 Landon Curt Noll & Ronald S. Karr + + Everyone is permitted to copy and distribute verbatim copies + of this license, but changing it is not allowed. You can also + use this wording to make the terms for other programs. + + The license agreements of most software companies keep you at the +mercy of those companies. By contrast, our general public license is +intended to give everyone the right to share SMAIL. To make sure that +you get the rights we want you to have, we need to make restrictions +that forbid anyone to deny you these rights or to ask you to surrender +the rights. Hence this license agreement. + + Specifically, we want to make sure that you have the right to give +away copies of SMAIL, that you receive source code or else can get it +if you want it, that you can change SMAIL or use pieces of it in new +free programs, and that you know you can do these things. + + To make sure that everyone has such rights, we have to forbid you to +deprive anyone else of these rights. For example, if you distribute +copies of SMAIL, you must give the recipients all the rights that you +have. You must make sure that they, too, receive or can get the +source code. And you must tell them their rights. + + Also, for our own protection, we must make certain that everyone +finds out that there is no warranty for SMAIL. If SMAIL is modified by +someone else and passed on, we want its recipients to know that what +they have is not what we distributed, so that any problems introduced +by others will not reflect on our reputation. + + Therefore we (Landon Curt Noll and Ronald S. Karr) make the following +terms which say what you must do to be allowed to distribute or change +SMAIL. + + + COPYING POLICIES + + 1. You may copy and distribute verbatim copies of SMAIL source code +as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy a valid copyright notice "Copyright +(C) 1988 Landon Curt Noll & Ronald S. Karr" (or with whatever year is +appropriate); keep intact the notices on all files that refer to this +License Agreement and to the absence of any warranty; and give any +other recipients of the SMAIL program a copy of this License +Agreement along with the program. You may charge a distribution fee +for the physical act of transferring a copy. + + 2. You may modify your copy or copies of SMAIL or any portion of it, +and copy and distribute such modifications under the terms of +Paragraph 1 above, provided that you also do the following: + + a) cause the modified files to carry prominent notices stating + that you changed the files and the date of any change; and + + b) cause the whole of any work that you distribute or publish, + that in whole or in part contains or is a derivative of SMAIL or + any part thereof, to be licensed at no charge to all third + parties on terms identical to those contained in this License + Agreement (except that you may choose to grant more extensive + warranty protection to some or all third parties, at your option). + + c) You may charge a distribution fee for the physical act of + transferring a copy, and you may at your option offer warranty + protection in exchange for a fee. + +Mere aggregation of another unrelated program with this program (or its +derivative) on a volume of a storage or distribution medium does not bring +the other program under the scope of these terms. + + 3. You may copy and distribute SMAIL (or a portion or derivative of it, +under Paragraph 2) in object code or executable form under the terms of +Paragraphs 1 and 2 above provided that you also do one of the following: + + a) accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of + Paragraphs 1 and 2 above; or, + + b) accompany it with a written offer, valid for at least three + years, to give any third party free (except for a nominal + shipping charge) a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of + Paragraphs 1 and 2 above; or, + + c) accompany it with the information you received as to where the + corresponding source code may be obtained. (This alternative is + allowed only for non-commercial distribution and only if you + received the program in object code or executable form alone.) + +For an executable file, complete source code means all the source code for +all modules it contains; but, as a special exception, it need not include +source code for modules which are standard libraries that accompany the +operating system on which the executable file runs. + + 4. You may not copy, sublicense, distribute or transfer SMAIL +except as expressly provided under this License Agreement. Any attempt +otherwise to copy, sublicense, distribute or transfer SMAIL is void and +your rights to use the program under this License agreement shall be +automatically terminated. However, parties who have received computer +software programs from you with this License Agreement will not have +their licenses terminated so long as such parties remain in full compliance. + + 5. If you wish to incorporate parts of SMAIL into other free +programs whose distribution conditions are different, write to Landon +Curt Noll & Ronald S. Karr via the Free Software Foundation at 51 +Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. We have not yet +worked out a simple rule that can be stated here, but we will often +permit this. We will be guided by the two goals of preserving the +free status of all derivatives of our free software and of promoting +the sharing and reuse of software. + +Your comments and suggestions about our licensing policies and our +software are welcome! This contract was based on the contract made by +the Free Software Foundation. Please contact the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, +USA, or call (617) 542-5942 for details on copylefted material in +general. + + NO WARRANTY + + BECAUSE SMAIL IS LICENSED FREE OF CHARGE, WE PROVIDE ABSOLUTELY NO +WARRANTY, TO THE EXTENT PERMITTED BY APPLICABLE STATE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING, LANDON CURT NOLL & RONALD S. KARR AND/OR +OTHER PARTIES PROVIDE SMAIL "AS IS" WITHOUT WARRANTY OF ANY KIND, +EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF SMAIL IS WITH +YOU. SHOULD SMAIL PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL +NECESSARY SERVICING, REPAIR OR CORRECTION. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW WILL LANDON CURT NOLL & +RONALD S. KARR AND/OR ANY OTHER PARTY WHO MAY MODIFY AND REDISTRIBUTE +SMAIL AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +LOST PROFITS, LOST MONIES, OR OTHER SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE +(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS) SMAIL, EVEN IF YOU HAVE +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, OR FOR ANY CLAIM BY +ANY OTHER PARTY. diff --git a/options/license/SOFA b/options/license/SOFA new file mode 100644 index 0000000000..d9529497c3 --- /dev/null +++ b/options/license/SOFA @@ -0,0 +1,90 @@ +Copyright (C) 2023 +Standards of Fundamental Astronomy Board +of the International Astronomical Union. + +===================== +SOFA Software License +===================== + +NOTICE TO USER: + +BY USING THIS SOFTWARE YOU ACCEPT THE FOLLOWING SIX TERMS AND +CONDITIONS WHICH APPLY TO ITS USE. + +1. The Software is owned by the IAU SOFA Board ("SOFA"). + +2. Permission is granted to anyone to use the SOFA software for any +purpose, including commercial applications, free of charge and +without payment of royalties, subject to the conditions and +restrictions listed below. + +3. You (the user) may copy and distribute SOFA source code to others, +and use and adapt its code and algorithms in your own software, +on a world-wide, royalty-free basis. That portion of your +distribution that does not consist of intact and unchanged copies +of SOFA source code files is a "derived work" that must comply +with the following requirements: + + a) Your work shall be marked or carry a statement that it + (i) uses routines and computations derived by you from + software provided by SOFA under license to you; and + (ii) does not itself constitute software provided by and/or + endorsed by SOFA. + + b) The source code of your derived work must contain descriptions + of how the derived work is based upon, contains and/or differs + from the original SOFA software. + + c) The names of all routines in your derived work shall not + include the prefix "iau" or "sofa" or trivial modifications + thereof such as changes of case. + + d) The origin of the SOFA components of your derived work must + not be misrepresented; you must not claim that you wrote the + original software, nor file a patent application for SOFA + software or algorithms embedded in the SOFA software. + + e) These requirements must be reproduced intact in any source + distribution and shall apply to anyone to whom you have + granted a further right to modify the source code of your + derived work. + + Note that, as originally distributed, the SOFA software is + intended to be a definitive implementation of the IAU standards, + and consequently third-party modifications are discouraged. All + variations, no matter how minor, must be explicitly marked as + such, as explained above. + + 4. You shall not cause the SOFA software to be brought into + disrepute, either by misuse, or use for inappropriate tasks, or + by inappropriate modification. + + 5. The SOFA software is provided "as is" and SOFA makes no warranty + as to its use or performance. SOFA does not and cannot warrant + the performance or results which the user may obtain by using the + SOFA software. SOFA makes no warranties, express or implied, as + to non-infringement of third party rights, merchantability, or + fitness for any particular purpose. In no event will SOFA be + liable to the user for any consequential, incidental, or special + damages, including any lost profits or lost savings, even if a + SOFA representative has been advised of such damages, or for any + claim by any third party. + + 6. The provision of any version of the SOFA software under the terms + and conditions specified herein does not imply that future + versions will also be made available under the same terms and + conditions. + + In any published work or commercial product which uses the SOFA + software directly, acknowledgement (see www.iausofa.org) is + appreciated. + + Correspondence concerning SOFA software should be addressed as + follows: + By email: sofa@ukho.gov.uk + By post: IAU SOFA Center + HM Nautical Almanac Office + UK Hydrographic Office + Admiralty Way, Taunton + Somerset, TA1 2DN + United Kingdom diff --git a/options/license/SUL-1.0 b/options/license/SUL-1.0 new file mode 100644 index 0000000000..119672697d --- /dev/null +++ b/options/license/SUL-1.0 @@ -0,0 +1,73 @@ +Sustainable Use License + +Version 1.0 + +Acceptance + +By using the software, you agree to all of the terms and conditions below. + +Copyright License + +The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license +to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject +to the limitations below. + +Limitations + +You may use or modify the software only for your own internal business purposes or for non-commercial or +personal use. You may distribute the software or provide it to others only if you do so free of charge for +non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of +the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. + +Patents + +The licensor grants you a license, under any patent claims the licensor can license, or becomes able to +license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case +subject to the limitations and conditions in this license. This license does not cover any patent claims that +you cause to be infringed by modifications or additions to the software. If you or your company make any +written claim that the software infringes or contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If your company makes such a claim, your patent +license ends immediately for work on behalf of your company. + +Notices + +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these +terms. If you modify the software, you must include in any modified copies of the software a prominent notice +stating that you have modified the software. + +No Other Rights + +These terms do not imply any licenses other than those expressly granted in these terms. + +Termination + +If you use the software in violation of these terms, such use is not licensed, and your license will +automatically terminate. If the licensor provides you with a notice of your violation, and you cease all +violation of this license no later than 30 days after you receive that notice, your license will be reinstated +retroactively. However, if you violate these terms after such reinstatement, any additional violation of these +terms will cause your license to terminate automatically and permanently. + +No Liability + +As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will +not be liable to you for any damages arising out of these terms or the use or nature of the software, under +any kind of legal claim. + +Definitions + +The “licensor” is the entity offering these terms. + +The “software” is the software the licensor makes available under these terms, including any portion of it. + +“You” refers to the individual or entity agreeing to these terms. + +“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus +all organizations that have control over, are under the control of, or are under common control with that +organization. Control means ownership of substantially all the assets of an entity, or the power to direct its +management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +“Your license” is the license granted to you for the software under these terms. + +“Use” means anything you do with the software requiring your license. + +“Trademark” means trademarks, service marks, and similar rights. diff --git a/options/license/Sendmail-Open-Source-1.1 b/options/license/Sendmail-Open-Source-1.1 new file mode 100644 index 0000000000..054f719ee5 --- /dev/null +++ b/options/license/Sendmail-Open-Source-1.1 @@ -0,0 +1,75 @@ +SENDMAIL OPEN SOURCE LICENSE + +The following license terms and conditions apply to this open source +software ("Software"), unless a different license is obtained directly +from Sendmail, Inc. ("Sendmail") located at 6475 Christie Ave, Suite 350, +Emeryville, CA 94608, USA. + +Use, modification and redistribution (including distribution of any +modified or derived work) of the Software in source and binary forms is +permitted only if each of the following conditions of 1-6 are met: + +1. Redistributions of the Software qualify as "freeware" or "open + source software" under one of the following terms: + + (a) Redistributions are made at no charge beyond the reasonable + cost of materials and delivery; or + + (b) Redistributions are accompanied by a copy of the modified + Source Code (on an acceptable machine-readable medium) or by an + irrevocable offer to provide a copy of the modified Source Code + (on an acceptable machine-readable medium) for up to three years + at the cost of materials and delivery. Such redistributions must + allow further use, modification, and redistribution of the Source + Code under substantially the same terms as this license. For + the purposes of redistribution "Source Code" means the complete + human-readable, compilable, linkable, and operational source + code of the redistributed module(s) including all modifications. + +2. Redistributions of the Software Source Code must retain the + copyright notices as they appear in each Source Code file, these + license terms and conditions, and the disclaimer/limitation of + liability set forth in paragraph 6 below. Redistributions of the + Software Source Code must also comply with the copyright notices + and/or license terms and conditions imposed by contributors on + embedded code. The contributors' license terms and conditions + and/or copyright notices are contained in the Source Code + distribution. + +3. Redistributions of the Software in binary form must reproduce the + Copyright Notice described below, these license terms and conditions, + and the disclaimer/limitation of liability set forth in paragraph + 6 below, in the documentation and/or other materials provided with + the binary distribution. For the purposes of binary distribution, + "Copyright Notice" refers to the following language: "Copyright (c) + 1998-2009 Sendmail, Inc. All rights reserved." + +4. Neither the name, trademark or logo of Sendmail, Inc. (including + without limitation its subsidiaries or affiliates) or its contributors + may be used to endorse or promote products, or software or services + derived from this Software without specific prior written permission. + The name "sendmail" is a registered trademark and service mark of + Sendmail, Inc. + +5. We reserve the right to cancel this license if you do not comply with + the terms. This license is governed by California law and both of us + agree that for any dispute arising out of or relating to this Software, + that jurisdiction and venue is proper in San Francisco or Alameda + counties. These license terms and conditions reflect the complete + agreement for the license of the Software (which means this supercedes + prior or contemporaneous agreements or representations). If any term + or condition under this license is found to be invalid, the remaining + terms and conditions still apply. + +6. Disclaimer/Limitation of Liability: THIS SOFTWARE IS PROVIDED BY + SENDMAIL AND ITS CONTRIBUTORS "AS IS" WITHOUT WARRANTY OF ANY KIND + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT AND FITNESS FOR A + PARTICULAR PURPOSE ARE EXPRESSLY DISCLAIMED. IN NO EVENT SHALL SENDMAIL + OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + WITHOUT LIMITATION NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE + USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. diff --git a/options/license/Simple-Library-Usage-exception b/options/license/Simple-Library-Usage-exception new file mode 100644 index 0000000000..2db2bbe10f --- /dev/null +++ b/options/license/Simple-Library-Usage-exception @@ -0,0 +1,33 @@ +EXCEPTION NOTICE + +1. As a special exception, the copyright holders of this library give +permission for additional uses of the text contained in this release +of the library as licensed under the Simple Library Usage License, +applying either version 1 of the License, or (at your option) any +later version of the License as published by the copyright holders of +version 1 of the License document. + +2. The exception is that you may combine or link a "work that uses the +Library" (as defined by the LGPL) with the Library to produce a work +containing portions of the Library in binary form, and distribute that +work under terms of your choice, provided that: + + a) You give prominent notice with each copy of the work that the + Library is used in it. + + b) If the work during execution displays copyright notices, you must + include the copyright notice for the Library among them. + +3. If you copy code from files distributed under the terms of the GNU +General Public License or the GNU Lesser General Public License into a +copy of this library, as this license permits, the exception does not +apply to the code that you add in this way. To avoid misleading +anyone as to the status of such modified files, you must delete this +exception notice from such code and/or adjust the licensing conditions +notice accordingly. + +4. If you write modifications of your own for this library, it is your +choice whether to permit this exception to apply to your +modifications. If you do not wish that, you must delete the exception +notice from such code and/or adjust the licensing conditions notice +accordingly. diff --git a/options/license/TekHVC b/options/license/TekHVC new file mode 100644 index 0000000000..6a3958717a --- /dev/null +++ b/options/license/TekHVC @@ -0,0 +1,34 @@ +Code and supporting documentation (c) Copyright 1990 1991 Tektronix, Inc. + All Rights Reserved + +This file is a component of an X Window System-specific implementation +of Xcms based on the TekColor Color Management System. TekColor is a +trademark of Tektronix, Inc. The term "TekHVC" designates a particular +color space that is the subject of U.S. Patent No. 4,985,853 (equivalent +foreign patents pending). Permission is hereby granted to use, copy, +modify, sell, and otherwise distribute this software and its +documentation for any purpose and without fee, provided that: + +1. This copyright, permission, and disclaimer notice is reproduced in + all copies of this software and any modification thereof and in + supporting documentation; +2. Any color-handling application which displays TekHVC color + cooordinates identifies these as TekHVC color coordinates in any + interface that displays these coordinates and in any associated + documentation; +3. The term "TekHVC" is always used, and is only used, in association + with the mathematical derivations of the TekHVC Color Space, + including those provided in this file and any equivalent pathways and + mathematical derivations, regardless of digital (e.g., floating point + or integer) representation. + +Tektronix makes no representation about the suitability of this software +for any purpose. It is provided "as is" and with all faults. + +TEKTRONIX DISCLAIMS ALL WARRANTIES APPLICABLE TO THIS SOFTWARE, +INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. IN NO EVENT SHALL TEKTRONIX BE LIABLE FOR ANY +SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA, OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR THE PERFORMANCE OF THIS SOFTWARE. diff --git a/options/license/ThirdEye b/options/license/ThirdEye new file mode 100644 index 0000000000..ce75b566e3 --- /dev/null +++ b/options/license/ThirdEye @@ -0,0 +1,7 @@ +(C) Copyright 1984 by Third Eye Software, Inc. + +Third Eye Software, Inc. grants reproduction and use rights to +all parties, PROVIDED that this comment is maintained in the copy. + +Third Eye makes no claims about the applicability of this +symbol table to a particular use. diff --git a/options/license/Ubuntu-font-1.0 b/options/license/Ubuntu-font-1.0 new file mode 100644 index 0000000000..ae78a8f94e --- /dev/null +++ b/options/license/Ubuntu-font-1.0 @@ -0,0 +1,96 @@ +------------------------------- +UBUNTU FONT LICENCE Version 1.0 +------------------------------- + +PREAMBLE +This licence allows the licensed fonts to be used, studied, modified and +redistributed freely. The fonts, including any derivative works, can be +bundled, embedded, and redistributed provided the terms of this licence +are met. The fonts and derivatives, however, cannot be released under +any other licence. The requirement for fonts to remain under this +licence does not require any document created using the fonts or their +derivatives to be published under this licence, as long as the primary +purpose of the document is not to be a vehicle for the distribution of +the fonts. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this licence and clearly marked as such. This may +include source files, build scripts and documentation. + +"Original Version" refers to the collection of Font Software components +as received under this licence. + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to +a new environment. + +"Copyright Holder(s)" refers to all individuals and companies who have a +copyright ownership of the Font Software. + +"Substantially Changed" refers to Modified Versions which can be easily +identified as dissimilar to the Font Software by users of the Font +Software comparing the Original Version with the Modified Version. + +To "Propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification and with or without charging +a redistribution fee), making available to the public, and in some +countries other activities as well. + +PERMISSION & CONDITIONS +This licence does not grant any rights under trademark law and all such +rights are reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of the Font Software, to propagate the Font Software, subject to +the below conditions: + +1) Each copy of the Font Software must contain the above copyright +notice and this licence. These can be included either as stand-alone +text files, human-readable headers or in the appropriate machine- +readable metadata fields within text or binary files as long as those +fields can be easily viewed by the user. + +2) The font name complies with the following: +(a) The Original Version must retain its name, unmodified. +(b) Modified Versions which are Substantially Changed must be renamed to +avoid use of the name of the Original Version or similar names entirely. +(c) Modified Versions which are not Substantially Changed must be +renamed to both (i) retain the name of the Original Version and (ii) add +additional naming elements to distinguish the Modified Version from the +Original Version. The name of such Modified Versions must be the name of +the Original Version, with "derivative X" where X represents the name of +the new work, appended to that name. + +3) The name(s) of the Copyright Holder(s) and any contributor to the +Font Software shall not be used to promote, endorse or advertise any +Modified Version, except (i) as required by this licence, (ii) to +acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with +their explicit written permission. + +4) The Font Software, modified or unmodified, in part or in whole, must +be distributed entirely under this licence, and must not be distributed +under any other licence. The requirement for fonts to remain under this +licence does not affect any document created using the Font Software, +except any version of the Font Software extracted from a document +created using the Font Software may only be distributed under this +licence. + +TERMINATION +This licence becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER +DEALINGS IN THE FONT SOFTWARE. diff --git a/options/license/UnRAR b/options/license/UnRAR new file mode 100644 index 0000000000..645b6071c8 --- /dev/null +++ b/options/license/UnRAR @@ -0,0 +1,41 @@ +****** ***** ****** UnRAR - free utility for RAR archives + ** ** ** ** ** ** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ****** ******* ****** License for use and distribution of + ** ** ** ** ** ** ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ** ** ** ** ** ** FREE portable version + ~~~~~~~~~~~~~~~~~~~~~ + + The source code of UnRAR utility is freeware. This means: + + 1. All copyrights to RAR and the utility UnRAR are exclusively + owned by the author - Alexander Roshal. + + 2. UnRAR source code may be used in any software to handle + RAR archives without limitations free of charge, but cannot be + used to develop RAR (WinRAR) compatible archiver and to + re-create RAR compression algorithm, which is proprietary. + Distribution of modified UnRAR source code in separate form + or as a part of other software is permitted, provided that + full text of this paragraph, starting from "UnRAR source code" + words, is included in license, or in documentation if license + is not available, and in source code comments of resulting package. + + 3. The UnRAR utility may be freely distributed. It is allowed + to distribute UnRAR inside of other software packages. + + 4. THE RAR ARCHIVER AND THE UnRAR UTILITY ARE DISTRIBUTED "AS IS". + NO WARRANTY OF ANY KIND IS EXPRESSED OR IMPLIED. YOU USE AT + YOUR OWN RISK. THE AUTHOR WILL NOT BE LIABLE FOR DATA LOSS, + DAMAGES, LOSS OF PROFITS OR ANY OTHER KIND OF LOSS WHILE USING + OR MISUSING THIS SOFTWARE. + + 5. Installing and using the UnRAR utility signifies acceptance of + these terms and conditions of the license. + + 6. If you don't agree with terms of the license you must remove + UnRAR files from your storage devices and cease to use the + utility. + + Thank you for your interest in RAR and UnRAR. + + Alexander L. Roshal diff --git a/options/license/Unlicense-libtelnet b/options/license/Unlicense-libtelnet new file mode 100644 index 0000000000..18d1788030 --- /dev/null +++ b/options/license/Unlicense-libtelnet @@ -0,0 +1 @@ +The author or authors of this code dedicate any and all copyright interest in this code to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this code under copyright law. diff --git a/options/license/Unlicense-libwhirlpool b/options/license/Unlicense-libwhirlpool new file mode 100644 index 0000000000..11ead25c2d --- /dev/null +++ b/options/license/Unlicense-libwhirlpool @@ -0,0 +1,8 @@ +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any means. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. diff --git a/options/license/Vixie-Cron b/options/license/Vixie-Cron new file mode 100644 index 0000000000..3fefb02f27 --- /dev/null +++ b/options/license/Vixie-Cron @@ -0,0 +1,4 @@ +Copyright 1988,1990,1993 by Paul Vixie +All rights reserved + +Distribute freely, except: don't remove my name from the source or documentation (don't take credit for my work), mark your changes (don't get me blamed for your possible bugs), don't alter or remove this notice. May be sold if buildable source is provided to buyer. No warrantee of any kind, express or implied, is included with this software; use at your own risk, responsibility for damages (if any) to anyone resulting from the use of this software rests entirely with the user. diff --git a/options/license/WTFNMFPL b/options/license/WTFNMFPL new file mode 100644 index 0000000000..e76917a9bc --- /dev/null +++ b/options/license/WTFNMFPL @@ -0,0 +1,17 @@ +DO WHAT THE FUCK YOU WANT TO BUT IT'S NOT MY FAULT PUBLIC LICENSE + Version 1, October 2013 + + Copyright (C) 2013 Ben McGinnes + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO BUT IT'S NOT MY FAULT PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + + 1. Do not hold the author(s), creator(s), developer(s) or + distributor(s) liable for anything that happens or goes wrong + with your use of the work. diff --git a/options/license/WordNet b/options/license/WordNet new file mode 100644 index 0000000000..b937fa5cb3 --- /dev/null +++ b/options/license/WordNet @@ -0,0 +1,12 @@ +WordNet Release 3.0 +This software and database is being provided to you, the LICENSEE, by Princeton University under the following license. + +By obtaining, using and/or copying this software and database, you agree that you have read, understood, and will comply with these terms and conditions.: + +Permission to use, copy, modify and distribute this software and database and its documentation for any purpose and without fee or royalty is hereby granted, provided that you agree to comply with the following copyright notice and statements, including the disclaimer, and that the same appear on ALL copies of the software, database and documentation, including modifications that you make for internal use or for distribution. + +WordNet 3.0 Copyright 2006 by Princeton University. All rights reserved. + +THIS SOFTWARE AND DATABASE IS PROVIDED "AS IS" AND PRINCETON UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PRINCETON UNIVERSITY MAKES NO REPRESENTATIONS OR WARRANTIES OF MERCHANT- ABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE LICENSED SOFTWARE, DATABASE OR DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. + +The name of Princeton University or Princeton may not be used in advertising or publicity pertaining to distribution of the software and/or database. Title to copyright in this software, database and any associated documentation shall at all times remain with Princeton University and LICENSEE agrees to preserve same. diff --git a/options/license/X11-no-permit-persons b/options/license/X11-no-permit-persons new file mode 100644 index 0000000000..504b2eb7bd --- /dev/null +++ b/options/license/X11-no-permit-persons @@ -0,0 +1,23 @@ +Copyright (c) 1991, 1997 Digital Equipment Corporation, Maynard, Massachusetts. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR ANY CLAIM, DAMAGES, INCLUDING, +BUT NOT LIMITED TO CONSEQUENTIAL OR INCIDENTAL DAMAGES, OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of Digital Equipment Corporation +shall not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization from Digital +Equipment Corporation. diff --git a/options/license/X11-swapped b/options/license/X11-swapped new file mode 100644 index 0000000000..b023bd546e --- /dev/null +++ b/options/license/X11-swapped @@ -0,0 +1,23 @@ +Copyright (c) 2008-2010 Derick Eddington. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +Except as contained in this notice, the name(s) of the above copyright +holders shall not be used in advertising or otherwise to promote the sale, +use or other dealings in this Software without prior written authorization. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/options/license/any-OSI-perl-modules b/options/license/any-OSI-perl-modules new file mode 100644 index 0000000000..108db04581 --- /dev/null +++ b/options/license/any-OSI-perl-modules @@ -0,0 +1,11 @@ +This software may be redistributed under the terms of the GPL, LGPL, +modified BSD, or Artistic license, or any of the other OSI approved +licenses listed at http://www.opensource.org/licenses/alphabetical. +Distribution is allowed under all of these licenses, or any smaller +subset of multiple or just one of these licenses. + +When using a packaged version, please refer to the package metadata to see +under which license terms it was distributed. Alternatively, a distributor +may choose to replace the LICENSE section of the documentation and/or +include a LICENSE file to reflect the license(s) they chose to redistribute +under. diff --git a/options/license/erlang-otp-linking-exception b/options/license/erlang-otp-linking-exception new file mode 100644 index 0000000000..ca8b775480 --- /dev/null +++ b/options/license/erlang-otp-linking-exception @@ -0,0 +1,11 @@ +If you modify this Program, or any covered work, by linking or +combining it with runtime libraries of Erlang/OTP as released by +Ericsson on https://www.erlang.org (or a modified version of these +libraries), containing parts covered by the terms of the Erlang Public +License (https://www.erlang.org/EPLICENSE), the licensors of this +Program grant you additional permission to convey the resulting work +without the need to license the runtime libraries of Erlang/OTP under +the GNU Affero General Public License. Corresponding Source for a +non-source form of such a combination shall include the source code +for the parts of the runtime libraries of Erlang/OTP used as well as +that of the covered work. diff --git a/options/license/generic-xts b/options/license/generic-xts new file mode 100644 index 0000000000..bf08a2b421 --- /dev/null +++ b/options/license/generic-xts @@ -0,0 +1,17 @@ +Copyright (C) 2008, Damien Miller +Copyright (C) 2011, Alex Hornung + +Permission to use, copy, and modify this software with or without fee +is hereby granted, provided that this entire notice is included in +all copies of any software which is or includes a copy or +modification of this software. +You may use this code under the GNU public license if you so wish. Please +contribute changes back to the authors under this freer than GPL license +so that we may further the use of strong encryption without limitations to +all. + +THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR +IMPLIED WARRANTY. IN PARTICULAR, NONE OF THE AUTHORS MAKES ANY +REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE +MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR +PURPOSE. diff --git a/options/license/harbour-exception b/options/license/harbour-exception new file mode 100644 index 0000000000..25d75e9fc7 --- /dev/null +++ b/options/license/harbour-exception @@ -0,0 +1,23 @@ +As a special exception, the Harbour Project gives permission for +additional uses of the text contained in its release of Harbour. + +The exception is that, if you link the Harbour libraries with other +files to produce an executable, this does not by itself cause the +resulting executable to be covered by the GNU General Public License. +Your use of that executable is in no way restricted on account of +linking the Harbour library code into it. + +This exception does not however invalidate any other reasons why +the executable file might be covered by the GNU General Public License. + +This exception applies only to the code released by the Harbour +Project under the name Harbour. If you copy code from other +Harbour Project or Free Software Foundation releases into a copy of +Harbour, as the General Public License permits, the exception does +not apply to the code that you add in this way. To avoid misleading +anyone as to the status of such modified files, you must delete +this exception notice from them. + +If you write modifications of your own for Harbour, it is your choice +whether to permit this exception to apply to your modifications. +If you do not wish that, delete this exception notice. diff --git a/options/license/hyphen-bulgarian b/options/license/hyphen-bulgarian new file mode 100644 index 0000000000..6642d1c676 --- /dev/null +++ b/options/license/hyphen-bulgarian @@ -0,0 +1,8 @@ +This software may be used, modified, copied, distributed, and sold, +both in source and binary form provided that the above copyright +notice and these terms are retained. The name of the author may not +be used to endorse or promote products derived from this software +without prior permission. THIS SOFTWARE IS PROVIDES "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED. IN NO EVENT +SHALL THE AUTHOR BE LIABLE FOR ANY DAMAGES ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE. diff --git a/options/license/jove b/options/license/jove new file mode 100644 index 0000000000..edcc31667a --- /dev/null +++ b/options/license/jove @@ -0,0 +1,4 @@ +This program is Copyright (C) 1986-2002 by Jonathan Payne. JOVE is +provided by Jonathan and Jovehacks without charge and without +warranty. You may copy, modify, and/or distribute JOVE, provided that +this notice is included in all the source files and documentation. diff --git a/options/license/kvirc-openssl-exception b/options/license/kvirc-openssl-exception new file mode 100644 index 0000000000..beb605b8bd --- /dev/null +++ b/options/license/kvirc-openssl-exception @@ -0,0 +1,30 @@ + _OpenSSL Exception_ + +0. Definitions + +"KVIrc" means KVIrc software licensed under version 2 or any later +version of the GNU General Public License (collectively, "GPL"), or a +work based on such software and licensed under the GPL. + +"OpenSSL" means OpenSSL toolkit software distributed by the OpenSSL +Project and licensed under the OpenSSL Licenses, or a work based on such +software and licensed under the OpenSSL Licenses. + +"OpenSSL Licenses" means the OpenSSL License and Original SSLeay License +under which the OpenSSL Project distributes the OpenSSL toolkit software, +as those licenses appear in the file LICENSE-OPENSSL. + +1. Exception + +You have permission to copy, modify, propagate, and distribute a work +formed by combining OpenSSL with KVIrc, or a work derivative of such a +combination, even if such copying, modification, propagation, or +distribution would otherwise violate the terms of the GPL. You must +comply with the GPL in all respects for all of the code used other than +OpenSSL. + +You may include this OpenSSL Exception and its grant of permissions when +you distribute KVIrc. Inclusion of this notice with such a +distribution constitutes a grant of such permission. If you do not wish +to grant these permissions, remove this section entitled "OpenSSL +Exception" from your distribution. diff --git a/options/license/libpng-1.6.35 b/options/license/libpng-1.6.35 new file mode 100644 index 0000000000..cf3b98dfdb --- /dev/null +++ b/options/license/libpng-1.6.35 @@ -0,0 +1,96 @@ +PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) +----------------------------------------------------------------------- + +libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are +Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are +derived from libpng-1.0.6, and are distributed according to the same +disclaimer and license as libpng-1.0.6 with the following individuals +added to the list of Contributing Authors: + + Simon-Pierre Cadieux + Eric S. Raymond + Mans Rullgard + Cosmin Truta + Gilles Vollant + James Yu + Mandar Sahastrabuddhe + Google Inc. + Vadim Barkov + +and with the following additions to the disclaimer: + + There is no warranty against interference with your enjoyment of + the library or against infringement. There is no warranty that our + efforts or the library will fulfill any of your particular purposes + or needs. This library is provided with all faults, and the entire + risk of satisfactory quality, performance, accuracy, and effort is + with the user. + +Some files in the "contrib" directory and some configure-generated +files that are distributed with libpng have other copyright owners, and +are released under other open source licenses. + +libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are +Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from +libpng-0.96, and are distributed according to the same disclaimer and +license as libpng-0.96, with the following individuals added to the +list of Contributing Authors: + + Tom Lane + Glenn Randers-Pehrson + Willem van Schaik + +libpng versions 0.89, June 1996, through 0.96, May 1997, are +Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, +and are distributed according to the same disclaimer and license as +libpng-0.88, with the following individuals added to the list of +Contributing Authors: + + John Bowler + Kevin Bracey + Sam Bushell + Magnus Holmgren + Greg Roelofs + Tom Tanner + +Some files in the "scripts" directory have other copyright owners, +but are released under this license. + +libpng versions 0.5, May 1995, through 0.88, January 1996, are +Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +For the purposes of this copyright and license, "Contributing Authors" +is defined as the following set of individuals: + + Andreas Dilger + Dave Martindale + Guy Eric Schalnat + Paul Schmidt + Tim Wegner + +The PNG Reference Library is supplied "AS IS". The Contributing +Authors and Group 42, Inc. disclaim all warranties, expressed or +implied, including, without limitation, the warranties of +merchantability and of fitness for any purpose. The Contributing +Authors and Group 42, Inc. assume no liability for direct, indirect, +incidental, special, exemplary, or consequential damages, which may +result from the use of the PNG Reference Library, even if advised of +the possibility of such damage. + +Permission is hereby granted to use, copy, modify, and distribute this +source code, or portions hereof, for any purpose, without fee, subject +to the following restrictions: + + 1. The origin of this source code must not be misrepresented. + + 2. Altered versions must be plainly marked as such and must not + be misrepresented as being the original source. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + +The Contributing Authors and Group 42, Inc. specifically permit, +without fee, and encourage the use of this source code as a component +to supporting the PNG file format in commercial products. If you use +this source code in a product, acknowledgment is not required but would +be appreciated. diff --git a/options/license/man2html b/options/license/man2html new file mode 100644 index 0000000000..42d61d3af5 --- /dev/null +++ b/options/license/man2html @@ -0,0 +1,2 @@ +Permission is granted to distribute, modify and use this program +as long as this comment is not removed or changed. diff --git a/options/license/mxml-exception b/options/license/mxml-exception new file mode 100644 index 0000000000..32928e8dd6 --- /dev/null +++ b/options/license/mxml-exception @@ -0,0 +1,16 @@ +Mini-XML + +Copyright © 2003-2024 by Michael R Sweet + + +(Optional) Exceptions to the Apache 2.0 License: +================================================ + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 or LGPLv2 (“Combined Software”) and if +a court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2 or LGPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of the +License, but only in their entirety and only with respect to the Combined +Software. diff --git a/options/license/ngrep b/options/license/ngrep new file mode 100644 index 0000000000..6b970e466b --- /dev/null +++ b/options/license/ngrep @@ -0,0 +1,38 @@ +Copyright (c) 2017 Jordan Ritter. All rights reserved. + +Permission is granted to anyone to use this software for any purpose on +any computer system, and to alter it and redistribute it, subject +to the following restrictions: + +1. The origin of this software must not be misrepresented, either by + explicit claim or by omission. + +2. Altered versions must be plainly marked as such, and must not be + misrepresented as being the original software. Any altered version + must clearly and properly represent the origin of this software in + any accompanying documentation. + +3. All advertising materials which relate specifically to derivate + works of this software must display the following acknowledgement: + This product includes software developed by Jordan Ritter. + +4. The name of the Author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +5. This notice, and any references to this notice, in any original or + derived source distribution of or documentation for this software, + may not be removed or altered. + + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/options/license/polyparse-exception b/options/license/polyparse-exception new file mode 100644 index 0000000000..55ed918bb9 --- /dev/null +++ b/options/license/polyparse-exception @@ -0,0 +1,7 @@ +As a relaxation of clause 6 of the LGPL, the copyright holders of this +library give permission to use, copy, link, modify, and distribute, +binary-only object-code versions of an executable linked with the +original unmodified Library, without requiring the supply of any +mechanism to modify or replace the Library and relink (clauses 6a, +6b, 6c, 6d, 6e), provided that all the other terms of clause 6 are +complied with. diff --git a/options/license/romic-exception b/options/license/romic-exception new file mode 100644 index 0000000000..57def44818 --- /dev/null +++ b/options/license/romic-exception @@ -0,0 +1,6 @@ +Additional permission under the GNU Affero GPL version 3 section 7: + +If you modify this Program, or any covered work, by linking or +combining it with other code, such other code is not for that reason +alone subject to any of the requirements of the GNU Affero GPL +version 3. diff --git a/options/license/rsync-linking-exception b/options/license/rsync-linking-exception new file mode 100644 index 0000000000..ddad55cb28 --- /dev/null +++ b/options/license/rsync-linking-exception @@ -0,0 +1,6 @@ +In addition, as a special exception, the copyright holders give +permission to dynamically link rsync with the OpenSSL and xxhash +libraries when those libraries are being distributed in compliance +with their license terms, and to distribute a dynamically linked +combination of rsync and these libraries. This is also considered +to be covered under the GPL's System Libraries exception. diff --git a/options/license/wwl b/options/license/wwl new file mode 100644 index 0000000000..12486ff638 --- /dev/null +++ b/options/license/wwl @@ -0,0 +1,5 @@ +db@FreeBSD.ORG wrote this file. As long as you retain this notice you +can do whatever you want with this code, except you may not +license it under any form of the GPL. +A postcard or QSL card showing me you appreciate +this code would be nice. Diane Bruce va3db diff --git a/options/locale/locale_ar.ini b/options/locale/locale_ar.ini index 3130600db6..587ec0490c 100644 --- a/options/locale/locale_ar.ini +++ b/options/locale/locale_ar.ini @@ -1,9 +1,7 @@ [common] language = لغة passcode = رمز المرور -webauthn_error_timeout = طال وقت الوصول قبل أن يُقرأ مفتاحك. من فضلك أعد تحميل الصفحة وحاول مجدداً. cancel = ألغِ -webauthn_sign_in = اضغط الزر على مفتاحك الأمني إذا لم يكن لدى مفتاح الأمن الخاص بك أي زر، إعادة تشغيله. captcha = كابتشا create_new = أنشئ… preview = عاين @@ -22,7 +20,6 @@ mirrors = المرايا explore = إكتشف return_to_forgejo = العودة إلى فورجيو write = اكتب -webauthn_error_unknown = حدث خطأ غير معروف. من فضلك حاول مجدداً. twofa = المصادقة الثنائية version = الإصدار copy_success = تم النسخ! @@ -30,14 +27,12 @@ help = مساعدة loading = جارٍ التحميل… copy_type_unsupported = هذا النوع من الملفات لا يمكن نسخه pin = ثبّت -webauthn_error_empty = يلزم أن تضع اسم لهذا المفتاح. confirm_delete_selected = تأكيد حذف جميع العناصر المحددة؟ name = الاسم artifacts = الآثار sign_in_or = أو show_log_seconds = إظهار الثواني show_full_screen = املأ الشاشة -webauthn_use_twofa = استخدم رمز المصادقة الموجود على هاتفك unpin = ألغِ التثبيت edit = حرر concept_code_repository = المستودع @@ -60,12 +55,10 @@ admin_panel = إدارة الموقع copy_error = فشل النسخ new_mirror = مرآة جديدة re_type = تأكيد كلمة المرور -webauthn_unsupported_browser = متصفحك لا يدعم ويب آوثن حالياً. copy = انسخ enabled = مُفَعَّل rerun = أعِد التشغيل milestones = أهداف -webauthn_error_insecure = ويب آوثن يدعم فقط الاتصالات الآمنة. للاختبار على HTTP، يمكنك استخدام "localhost" أو "127.0.0.1" show_timestamps = إظهار الطوابع الزمنية rss_feed = موجز RSS never = أبدًا @@ -79,7 +72,6 @@ sources = المصادر notifications = التنبيهات pull_requests = طلبات السحب repository = مستودع -webauthn_error = تعذر قراءة مفتاحك الأمني. add_all = أضف الكل new_fork = اشتقاق جديد لمستودع new_project_column = عمود جديد @@ -89,17 +81,13 @@ organization = منظمة save = احفظ sign_in_with_provider = سجل الدخول بـ %s ok = وافق -webauthn_error_unable_to_process = الخادم لا يمكنه معالجة طلبك. register = سجل mirror = مرآة username = إسم المستخدم access_token = رمز الوصول download_logs = تنزيل السجلات -webauthn_insert_key = أدخل مفتاحك الأمني password = كلمة المرور -webauthn_error_duplicated = المفتاح الأمني غير مسموح له هذا الطلب. يرجى التأكد من أن المفتاح غير مسجل بالفعل. template = قالب -webauthn_press_button = من فضلك اضغط الزر على مفتاحك الأمني… unknown = مجهول signed_in_as = مسجل كـ sign_up = سجل @@ -272,7 +260,7 @@ buttons.list.task.tooltip = أضف قائمة مهام buttons.enable_monospace_font = فعّل الخط الثابت العرض buttons.mention.tooltip = اذكر مستخدمًا أو فريقًا buttons.italic.tooltip = أضف نصًا مائلًا (Ctrl+I / ⌘I) -buttons.link.tooltip = اضف رابط +buttons.link.tooltip = اضف رابط. buttons.disable_monospace_font = عطّل الخط الثابت العرض buttons.unindent.tooltip = ‪عناصر غير متساوية من نفس المستوى buttons.indent.tooltip = تداخل العناصر بنفس المستوى @@ -619,7 +607,6 @@ quota.rule.exceeded.helper = لقد تجاوز الحجم الإجمالي لل quota.rule.exceeded = تم تجاوزه quota.applies_to_user = تنطبق قواعد الحصص التالية على حسابك quota.sizes.wiki = الموسوعة - ssh_token_help_ssh_agent = أو، إذا كنت تستخدم وكيل SSH (مع تعيين متغير SSH_AUTH_SOCK): [org] @@ -723,12 +710,9 @@ issues.blocked_by_user = لا يمكنك أن ترسل مسألة في هذا ا mirror_sync = متزامن settings.archive.mirrors_unavailable = المرايا ليست متاحة إذا تم أرشفة المستودع. pulls.blocked_by_user = لا يمكنك أن ترسل طلب سحب في هذا المستودع لأنك محظور من قبل مالك المستودع. -migrate.migrating_milestones = ترحيل الأهداف -migrate_items_milestones = أهداف repo_size = حجم المستودع object_format = تنسيق الكائنات use_template = استخدم هذا القالب -migrate_items_merge_requests = طلبات الدمج repo_name = اسم المستودع template = القالب projects.modify = عدّل المشروع @@ -746,23 +730,19 @@ template.git_hooks = خطاطيف Git template_description = المستودع القالب هو مستودع يستخدمه المستخدمون لتوليد مستودعات جديدة لها الإعدادات والملفات وهيكلة المجلدات نفسها. projects.edit = عدّل المشروع template.avatar = الصورة الرمزية -migrate_items_wiki = الموسوعة repo_desc = الوصف template_select = اختر قالبا repo_name_helper = الأسماء الحسنة للمستودعات تستخدم كلمات مفتاحية قصيرة وسهلة التذكر وفريدة. default_branch = الفرع الافتراضي all_branches = كل الفروع -migrate_items_issues = البلاغات projects.deletion_desc = حذف مشروع يحذف كل المسائل المرتبطة به. أتريد الاستمرار؟ repo_desc_helper = أدخل وصفاً قصيراً (اختياري) create_repo = إنشاء مستودع -migrate_items_releases = الإصدارات already_forked = لقد اشتققت %s بالفعل license_helper = اختر ملف ترخيص. tree_path_not_found.tag = المسار %[1]s غير موجود في الوسم %[2]s object_format_helper = صيغة كائنات المستودع. لا يمكن تغييرها بعد ذلك. SHA1 هي الأكثر توافقية. forks = الاشتقاقات -migrate_items_pullrequests = طلبات السحب fork_to_different_account = اشتق إلى حساب مختلف tree_path_not_found.branch = المسار %[1]s غير موجود في الفرع %[2]s projects.edit_success = حدِّث المشروع "%s". @@ -886,7 +866,6 @@ release.publish = انشر الإصدار issues.lock_duplicate = لا يمكن إقفال مسألة مقفلة. issues.filter_type.assigned_to_you = كُلِّفت بها issues.save = احفظ -migrate_items_labels = تصنيفات issues.add_assignee_at = `كلّفه %s بها %s` milestones.filter_sort.least_complete = الأقل اكتمالا branch.create_branch = أنشئ الفرع %s @@ -1508,7 +1487,6 @@ transfer.accept = قبول النقل blame.ignore_revs.failed = فشل تجاهل المراجعات في .git-blame-ignore-revs. form.name_pattern_not_allowed = النمط ”%s“ غير مسموح به في اسم المستودع. migrate_options_lfs_endpoint.description = سيحاول الترحيل استخدام مسار Git البعيد (remote) لـ تحديد خادم LFS. يمكنك أيضًا تحديد نقطة نهاية مخصصة إذا كانت بيانات LFS للمستودع مخزنة في مكان آخر. -migrate_items = عناصر الترحيل adopt_preexisting_content = إنشاء مستودع من %s migrate_repo = ترحيل المستودع mirror_password_placeholder = (لم يتم تعديله) @@ -1527,16 +1505,12 @@ form.reach_limit_of_creation_1 = لقد وصل المالك بالفعل إلى form.name_reserved = تم حجز اسم المستودع ”%s“. mirror_address_url_invalid = عنوان URL المقدم غير صالح. تأكد من تحرير مكونات عنوان URL بشكل صحيح. migrate.migrate = الترحيل من %s -migrate.migrating_topics = ترحيل المواضيع -migrate.cancel_migrating_title = إلغاء الترحيل generated_from = تم توليده من star_guest_user = قم بتسجيل الدخول لإعطاء نجمة لهذا المستودع. subscribe.pull.guest.tooltip = سجّل الدخول للاشتراك في طلب السحب هذا. unwatch = إلغاء المشاهدة quick_guide = دليل سريع empty_message = لا يحتوي هذا المستودع على أي محتوى. -migrate.migrating_labels = ترحيل الوسوم -migrate.migrating_releases = ترحيل الإصدارات watch_guest_user = سجّل الدخول لمشاهدة هذا المستودع. star = نجمة cite_this_repo = الاستشهاد بهذا المستودع @@ -1550,8 +1524,6 @@ more_operations = المزيد من العمليات push_exist_repo = دفع مستودع موجود من موجّه الأوامر broken_message = لا يمكن قراءة بيانات Git التي يستند إليها هذا المستودع. اتصل بمسؤول هذا المثيل أو احذف هذا المستودع. watch = شاهد -migrate.migrating_git = ترحيل بيانات Git -migrate.cancel_migrating_confirm = تريد إلغاء عملية الترحيل هذه؟ migrate.permission_denied_blocked = لا يمكنك الاستيراد من مضيفين غير مسموح بهم، يُرجى الطلب من المسؤول التحقق من إعدادات ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS. migrate.migrating = الترحيل من %s … migrate.migrating_failed.error = أخفق الترحيل: %s @@ -1566,8 +1538,6 @@ create_new_repo_command = إنشاء مستودع جديد على موجّه ا fork = اشتقاق migrate.migrating_failed_no_addr = أخفق الترحيل. unstar = إزالة النجمة -migrate.migrating_issues = ترحيل البلاغات -migrate.migrating_pulls = ترحيل طلبات السحب code = الكود migrate.invalid_lfs_endpoint = نقطة نهاية LFS غير صالحة. migrate.migrating_failed = فشل الترحيل من %s. @@ -1582,19 +1552,15 @@ find_tag = البحث عن علامة commit_graph = الرسم البياني للإيداع editor.edit_this_file = عدل الملف editor.signoff_desc = أضف مقطورة موقّعة من قِبل المُجرّد في نهاية رسالة سجل الدخول. -n_release_one = %s إصدار symbolic_link = رابط رمزي editor.cannot_edit_lfs_files = لا يمكن تحرير ملفات LFS في واجهة الويب. editor.this_file_locked = الملف مقفل -n_commit_few = %s إيداعات editor.file_delete_success = تم حذف الملف "%s". editor.edit_file = عدّل الملف commit.contained_in_default_branch = هذا الإيداع جزء من الفرع الافتراضي line = سطر lines = أسطر normal_view = عرض عادي -n_branch_one = %s فرع -n_commit_one = %s إيداع view_git_blame = عرض مسؤول تعديل git editor.add_tmpl.filename = اسم الملف editor.name_your_file = اسم ملفك… @@ -1606,14 +1572,9 @@ no_eol.tooltip = هذا الملف لا يحتوي على نهاية لخط ال stored_lfs = مخزن مع Git LFS commit.contained_in = هذا الإيداع موجود في: file_view_rendered = عرض المُخرج النهائي -n_branch_few = %s فروع -n_tag_one = %s علامة -n_tag_few = ‪%s علامات -n_release_few = %s إصدارات file.title = %s عند %s vendored = مضمن file_follow = متابعة الروابط الرمزية - unescape_control_characters = الهروب blame = لوم editor.file_is_a_symlink = `"%s" هو رابط رمزي. لا يمكن تعديل الروابط الرمزية في محرر الويب.` @@ -1832,41 +1793,7 @@ use_onetime_code = استخدم رمزًا لمرة واحدة unauthorized_credentials = بيانات الاعتماد غير صحيحة أو انتهت صلاحيتها. أعد محاولة تنفيذ الأمر أو راجع %s لمزيد من المعلومات [packages] -rpm.repository.multiple_groups = هذه الحزمة متوفرة في مجموعات متعددة. -rpm.repository.architectures = بنيات -rpm.repository = معلومات المستودع -settings.delete.notice = أنت على وشك حذف %s (%s). هذه العملية لا رجعة فيها، هل أنت متأكد؟ -settings.link.select = اختر المستودع -settings.link.button = حدّث رابط المستودع -swift.install = اضف الحزمة إلى ملف Package.swift: -settings.delete = حذف الحزمة -settings.link.success = تم تحديث رابط المستودع بنجاح. -title = حزم -details.project_site = موقع المشروع -filter.type = النوع -details.author = الكاتب -details.repository_site = موقع المستودع -settings.link.description = اذا ربطت حزمة مع مستودع، الحزمة سوف تُدرع تحت قائمة الحزم لدى المستودع. -versions = الاصدارات -requirements = المتطلبات -installation = التثبيت -settings.delete.success = تم حذف الحزمة. -keywords = الكلمات المفتاحية -settings.link = اربط هذه الحزمة بمستودع -details.license = الترخيص -filter.type.all = الكل -settings.delete.error = فشل حذف الحزمة. -details = التفاصيل -about = عن هذه الحزمة -settings.link.error = فشل تحديث رابط المستودع. -empty = لا يوجد حزم بعد. -dependency.version = الاصدار -settings.delete.description = إن حذف الحزمة إجراء نهائي ولا يمكن عكسه. desc = إدارة حزم المستودع. -alpine.registry.key = نزّل مفتاح RSA العام للتسجيل في المجلد /etc/apk/keys/ للتحقق من توقيع الفهرس: -generic.download = نزّل الحزمة عبر سطر الأوامر: -filter.container.untagged = غير موسوم -filter.container.tagged = موسوم [heatmap] less = أقل @@ -1886,7 +1813,6 @@ dashboard.sync_repo_tags = زامن الوسوم من بيانات جِت إلى self_check = فحص ذاتي self_check.database_collation_case_insensitive = تستخدم قاعدة البيانات تجميع %s ، وهو تجميع غير حساس. على الرغم من أن فورجيو يمكن أن يعمل معها قد تكون هناك حالات نادرة لا تعمل كما هو متوقع. monitor.process.cancel_desc = قد يسبب إلغاء العملية فقدانًا للبيانات -monitor.queue.type = النوع monitor.process.cancel_notices = أتريد إلغاء: %s؟ dashboard.operations = عمليات الصيانة repositories = المستودعات @@ -1897,7 +1823,6 @@ monitor.desc = الوصف monitor.start = وقت البدء dashboard.system_status = حالة النظام dashboard.delete_generated_repository_avatars = احذف الصورة الرمزية المولّدة للمستودع -monitor.queue.name = الاسم monitor.process.cancel = ألغِ العملية monitor.last_execution_result = النتيجة users = حسابات المستخدمين @@ -1906,14 +1831,11 @@ dashboard.operation_name = اسم العملية notices.type_1 = المستودع notices.desc = الوصف notices.type = النوع -monitor.queue.settings.submit = حدّث الإعدادات -monitor.queue.settings.changed = تم تحديث الإعدادات auths.security_protocol = بروتوكول الأمان auths.port = المنفذ auths.attribute_username_placeholder = اتركه فارغاً لاستخدام اسم المستخدم المُدخل في فورجيو. auths.host = المضيف auths.domain = النطاق -users.list_status_filter.is_restricted = مقيَّد users.reserved = محجوز systemhooks.add_webhook = إضافة خطاف ويب النظام repos.owner = المالك @@ -1941,7 +1863,6 @@ auths.type = النوع users.purge_help = حذف مستخدم بالقوة وكل مستودعاته ومنظماته وحزمه. كل تعليقاته والمسائل التي أنشأها ستُحذف أيضا. users.admin = المدير emails.email_manage_panel = إدارة بريد المستخدم -users.list_status_filter.not_restricted = غير مقيد users.name = اسم المستخدم packages.repository = المستودع orgs.teams = الفِرق @@ -1959,18 +1880,14 @@ packages.package_manage_panel = إدارة الحزم auths.auth_name = اسم الاستيثاق users.details = تفاصيل المستخدم orgs.name = الاسم -users.list_status_filter.is_active = مفعّل users.repos = المستودعات defaulthooks.update_webhook = تحديث خطاف الويب المبدئي users.allow_git_hook = يستطيع عمل خطاطيف جت users.password_helper = اترك كلمة المرور فارغة لإبقائها بلا تغيير. dashboard.update_checker = فاحص التحديث -users.list_status_filter.not_active = معطّل users.update_profile = حدّث حساب المستخدم -users.list_status_filter.not_admin = غير مدير systemhooks = خطاطيف ويب النظام users.never_login = لم يلج قَط -users.list_status_filter.is_admin = مدير users.allow_create_organization = يستطيع إنشاء منظمات users.restricted = مقيَّد users.delete_account = احذف حساب المستخدم @@ -2119,70 +2036,9 @@ relevant_repositories = يتم اظهار المستودعات المتعلقة code_last_indexed_at = فُهرس آخر مرة %s [actions] -variables.none = لا توجد متغيرات بعد. -variables.deletion = أزل المتغير -runners.task_list.run = شغّل -runners.task_list.status = الحالة -runners.task_list.no_tasks = لا توجد مهام بعد. -variables.creation = أضف متغيرا -runners.runner_title = مشغّل -runners.task_list.commit = الإيداع -runners.task_list = المهام الأخيرة على هذا المشغّل -variables.management = إدارة المتغيرات -runners.task_list.repository = المستودع -variables = المتغيرات -variables.deletion.description = إزالة المتغيرات عملية نهائية لا يمكن التراجع عنها. أتريد الاستمرار؟ -status.failure = فشل -runners.status.idle = خامل -runners.task_list.done_at = تم عند -status.running = يعمل -runners.status.active = نشيط -runners.status = الحالة -runners.description = الوصف -runners.update_runner = حدّث التغييرات -runners.name = الاسم -runners.version = النسخة -runs.status = الحالة -status.unknown = مجهول -runners.owner_type = النوع -status.waiting = ينتظر -runners.labels = التصنيفات -runners.status.unspecified = مجهول -runs.commit = إيداع -status.success = نجح runs.empty_commit_message = (رسالة إيداع فارغة) -status.cancelled = ملغي -runs.status_no_select = كل الحالات -runs.scheduled = مُجدوَل -variables.edit = عدّل المتغير -variables.update.success = عُدِّل المتغير. -variables.update.failed = فشل تعديل المتغير. -variables.deletion.failed = فشل حذف المتغير. -variables.creation.failed = فشل إضافة المتغير. -variables.creation.success = تم إضافة المتغير "%s". -variables.deletion.success = تم حذف المتغير. variables.id_not_exist = المتغير ذو المعرّف %d ليس موجودا. -actions = الإجراءات unit.desc = أدر الإجراءات -status.skipped = متخطى -runners = المشغلون -runners.runner_manage_panel = إدارة المشغلين -runners.new = أنشئ مشغلا جديدا -runners.new_notice = كيف تبدأ مشغلا (بالإنجليزية) -runners.id = المعرّف -runners.last_online = آخر مرة كان متصلا -runners.none = لا مشغّل متاح -runners.status.offline = غير متصل -runs.pushed_by = دفعه -runs.no_matching_online_runner_helper = لا يوجد -runners.edit_runner = عدّل المشغّل -runners.update_runner_success = نجح تحديث المشغّل -runners.update_runner_failed = تعذر تحديث المشغّل -runners.delete_runner = احذف هذا المشغّل -runners.delete_runner_success = نجح حذف المشغّل -runners.delete_runner_failed = تعذر حذف المشغّل -runners.delete_runner_header = تأكيد حذف هذا المشغّل -variables.description = تمرر المتغيرات إلى إجراءات معينة ولا يمكن قراءتها بطريقة أخرى. [modal] no = لا @@ -2204,21 +2060,8 @@ changed_filemode = %[1]s → %[2]s symbolic_link = رابط رمزي [dropzone] -invalid_input_type = لا يمكنك رفع ملفات من هذا النوع. -default_message = اسحب الملفات أو اضغط هنا لرفعها. -file_too_big = حجم الملف ({{filesize}} مب) يتعدى الحد الأقصى ({{maxFilesize}} مب). -remove_file = أزل الملف [notification] -notifications = الإشعارات -unread = إلغِ القراءة -pin = ثبت التنبية -mark_as_unread = علّم كغير مقروء -no_read = لا يوجد تنبيهات مقروءه. -mark_as_read = علّم كمقروء -read = اقرأ -no_unread = لا يوجد تنبيهات غير مقروءه. -mark_all_as_read = علّم الكل كمقروء [tool] hours = %d ساعات @@ -2272,12 +2115,6 @@ error.no_unit_allowed_repo = ليس مسموحا لك الوصول إلى أي error.unit_not_allowed = ليس مسموحا لك الوصول إلى هذا القسم في المستودع. [gpg] -default_key = موقّع بالمفتاح المبدئي -error.extract_sign = تعذّر استخراج التوقيع -error.generate_hash = تعذّر إنشاء بصمة الإيداع -error.no_committer_account = لا حساب مرتبط ببريد المودِع -error.not_signed_commit = ليس إيداعًا موقّعًا -error.failed_retrieval_gpg_keys = تعذّر جلب مفتاح مرتبط بحساب المودِع [graphs] component_loading = يحمّل %s... diff --git a/options/locale/locale_arq.ini b/options/locale/locale_arq.ini new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/options/locale/locale_arq.ini @@ -0,0 +1 @@ + diff --git a/options/locale/locale_bg.ini b/options/locale/locale_bg.ini index 6cf85867bc..7e931f061d 100644 --- a/options/locale/locale_bg.ini +++ b/options/locale/locale_bg.ini @@ -111,8 +111,6 @@ toggle_menu = Превключване на менюто confirm_delete_artifact = Сигурни ли сте, че искате да изтриете артефакта „%s“? more_items = Още елементи twofa_scratch = Резервен код за двуфакторно удостоверяване -webauthn_use_twofa = Използвайте двуфакторен код от телефона си -webauthn_error_insecure = WebAuthn поддържа само сигурни връзки. За тестване през HTTP можете да използвате произход „localhost“ или „127.0.0.1“ error413 = Изчерпали сте квотата си. go_back = Връщане invalid_data = Невалидни данни: %v @@ -123,25 +121,13 @@ show_full_screen = Показване на цял екран show_timestamps = Показване на времеви отпечатъци rerun = Повторно изпълнение copy_type_unsupported = Този тип файл не може да бъде копиран -webauthn_error_unknown = Възникна неизвестна грешка. Моля, опитайте отново. -webauthn_error_unable_to_process = Сървърът не можа да обработи заявката ви. -webauthn_error_empty = Трябва да зададете име за този ключ. -webauthn_error_timeout = Времето за изчакване изтече преди ключът ви да бъде прочетен. Моля, презаредете страницата и опитайте отново. return_to_forgejo = Връщане към Forgejo unknown = Неизвестно confirm_delete_selected = Потвърждавате ли изтриването на всички избрани елементи? -webauthn_insert_key = Поставете вашия ключ за сигурност -webauthn_press_button = Моля, натиснете бутона на вашия ключ за сигурност… -webauthn_sign_in = Натиснете бутона на вашия ключ за сигурност. Ако ключът ви за сигурност няма бутон, поставете го отново. -webauthn_error = Неуспешно прочитане на вашия ключ за сигурност. -webauthn_unsupported_browser = Вашият браузър в момента не поддържа WebAuthn. -webauthn_error_duplicated = Ключът за сигурност не е разрешен за тази заявка. Моля, уверете се, че ключът не е вече регистриран. tracked_time_summary = Обобщение на проследеното време въз основа на филтрите в списъка със задачи - active_stopwatch = Активен тракер за време access_token = Токен за достъп passcode = Паскод - rerun_all = Повторно изпълнение на всички задания download_logs = Изтегляне на дневниците @@ -386,7 +372,6 @@ quota.sizes.wiki = Уики quota.sizes.all = Всички quota.sizes.repos.all = Хранилища quota.sizes.assets.attachments.issues = Прикачени файлове към задачи - openid_deletion = Премахване на OpenID адрес openid_deletion_desc = Премахването на този OpenID адрес от вашия акаунт ще ви попречи да влизате с него. Продължаване? openid_deletion_success = OpenID адресът е премахнат. @@ -445,8 +430,8 @@ access_token_regeneration = Повторно генериране на токе access_token_regeneration_desc = Повторното генериране на токен ще отнеме достъпа до вашия акаунт за приложенията, които го използват. Това не може да бъде отменено. Продължаване? regenerate_token_success = Токенът е генериран повторно. Приложенията, които го използват, вече нямат достъп до вашия акаунт и трябва да бъдат обновени с новия токен. select_permissions = Избор на разрешения -access_token_desc = Избраните разрешения на токена ограничават упълномощаването само до съответните API маршрути. Прочетете документацията за повече информация. at_least_one_permission = Трябва да изберете поне едно разрешение, за да създадете токен +access_token_desc = Избраните разрешения на токена ограничават упълномощаването само до съответните API маршрути. Прочетете документацията за повече информация. oauth2_confidential_client = Поверителен клиент. Изберете за приложения, които пазят тайната поверителна, като например уеб приложения. Не избирайте за нейтив приложения (настолни и мобилни). oauth2_redirect_uris = URI за пренасочване. Моля, използвайте нов ред за всеки URI. oauth2_client_id = ID на клиента @@ -471,168 +456,11 @@ remove_account_link_success = Свързаният акаунт е премах delete_with_all_comments = Вашият акаунт е създаден преди по-малко от %s. За да се избегнат коментари-призраци, всички коментари към задачи/заявки за сливане ще бъдат изтрити заедно с него. [packages] -container.labels.value = Стойност -alpine.repository.repositories = Хранилища -dependency.version = Версия -title = Пакети -empty = Все още няма пакети. -empty.documentation = За повече информация относно регистъра на пакетите вижте документацията. -container.labels.key = Ключ -requirements = Изисквания -details = Подробности -details.license = Лиценз -container.labels = Етикети -versions = Версии -empty.repo = Качихте ли пакет, но той не се показва тук? Отидете в настройките за пакети и го свържете към това хранилище. -keywords = Ключови думи -details.author = Автор -about = Относно този пакет -settings.delete.success = Пакетът е изтрит. -settings.delete = Изтриване на пакета -container.details.platform = Платформа -settings.delete.error = Неуспешно изтриване на пакет. -installation = Инсталация -versions.view_all = Вижте всички -dependencies = Зависимости -published_by_in = Публикуван %[1]s от %[3]s в %[5]s -published_by = Публикуван %[1]s от %[3]s -generic.download = Изтеглете пакета от командния ред: -container.details.type = Тип образ -alpine.repository = За хранилището -container.images.title = Образи -arch.version.description = Описание -search_in_external_registry = Търсене в %s -filter.type = Тип -filter.container.untagged = Без маркер -filter.type.all = Всички -registry.documentation = За повече информация относно регистъра %s, вижте документацията. -filter.no_result = Вашият филтър не даде резултати. -filter.container.tagged = С маркер -arch.pacman.repo.multi = %s има същата версия в различни дистрибуции. -arch.pacman.helper.gpg = Добавете доверителен сертификат за pacman: -alpine.repository.architectures = Архитектури -arch.version.provides = Доставя -arch.version.groups = Група -details.project_site = Уебсайт на проекта -arch.pacman.conf = Добавете сървър със свързаната дистрибуция и архитектура към /etc/pacman.conf : -arch.pacman.sync = Синхронизирайте пакета с pacman: -details.repository_site = Уебсайт на хранилището -arch.version.depends = Зависимости -arch.version.optdepends = Допълнителни зависимости -arch.version.replaces = Заменя -go.install = Инсталирайте пакета от командния ред: -cargo.registry = Настройте този регистър в конфигурационния файл на Cargo (например ~/.cargo/config.toml): -cargo.install = За да инсталирате пакета с Cargo, изпълнете следната команда: -details.documentation_site = Уебсайт на документацията -arch.version.conflicts = В конфликт -alpine.repository.branches = Клонове -arch.pacman.repo.multi.item = Конфигурация за %s -container.multi_arch = ОС / Архитектура -rpm.repository = Информация за хранилището -container.pull = Издърпайте образа от командния ред: -helm.registry = Настройте този регистър от командния ред: -debian.repository.distributions = Дистрибуции -npm.dependencies.optional = Опционални зависимости -owner.settings.cargo.title = Индекс на регистъра на Cargo -owner.settings.cleanuprules.keep.pattern.container = Версията latest винаги се запазва за Container пакети. -owner.settings.cleanuprules.remove.pattern = Премахване на версии, съответстващи на -rpm.distros.suse = на дистрибуции, базирани на SUSE -owner.settings.cleanuprules.preview.overview = %d пакета са насрочени за премахване. -owner.settings.cleanuprules.preview = Преглед на правило за почистване -arch.version.properties = Свойства на версията -conan.registry = Настройте този регистър от командния ред: conan.details.repository = Хранилище -composer.install = За да инсталирате пакета с Composer, изпълнете следната команда: -chef.install = За да инсталирате пакета, изпълнете следната команда: -chef.registry = Настройте този регистър във вашия файл ~/.chef/config.rb: -pub.install = За да инсталирате пакета с Dart, изпълнете следната команда: -npm.details.tag = Маркер -npm.install = За да инсталирате пакета с npm, изпълнете следната команда: -maven.registry = Настройте този регистър във файла на вашия проект pom.xml: -debian.repository.components = Компоненти -debian.install = За да инсталирате пакета, изпълнете следната команда: -cran.install = За да инсталирате пакета, изпълнете следната команда: -cran.registry = Настройте този регистър във вашия файл Rprofile.site: -rpm.distros.redhat = на дистрибуции, базирани на RedHat -alt.registry = Настройте този регистър от командния ред: -rpm.repository.architectures = Архитектури -alt.registry.install = За да инсталирате пакета, изпълнете следната команда: -alt.setup = Добавете хранилище към списъка със свързани хранилища (изберете необходимата архитектура вместо „_arch_“): -alt.repository = Информация за хранилището -owner.settings.cargo.initialize.error = Неуспешно инициализиране на индекса на Cargo: %v -owner.settings.cargo.initialize = Инициализиране на индекс -settings.delete.description = Изтриването на пакет е трайно и не може да бъде отменено. -alt.repository.multiple_groups = Този пакет е наличен в няколко групи. -alt.repository.architectures = Архитектури -owner.settings.chef.title = Регистър на Chef -owner.settings.cleanuprules.remove.days = Премахване на версии, по-стари от -owner.settings.cleanuprules.keep.pattern = Запазване на версии, съответстващи на owner.settings.cleanuprules.keep.count.n = %d версии на пакет owner.settings.cleanuprules.keep.count.1 = 1 версия на пакет -owner.settings.cleanuprules.keep.count = Запазване на най-новите owner.settings.cleanuprules.enabled = Включено -owner.settings.cleanuprules.preview.none = Правилото за почистване не съвпада с нито един пакет. -owner.settings.cleanuprules.none = Все още няма правила за почистване. -owner.settings.cleanuprules.add = Добавяне на правило за почистване -owner.settings.cleanuprules.title = Правила за почистване -owner.settings.cargo.rebuild.success = Индексът на Cargo беше успешно преизграден. -alpine.registry.key = Изтеглете публичния RSA ключ на регистъра в папката /etc/apk/keys/, за да проверите подписа на индекса: -alpine.registry.info = Изберете $branch и $repository от списъка по-долу. -arch.version.checkdepends = Зависимости за проверката -composer.dependencies = Зависимости -swift.install = Добавете пакета във вашия файл Package.swift: -settings.link.error = Неуспешно обновяване на връзката на хранилището. -swift.install2 = и изпълнете следната команда: -rpm.repository.multiple_groups = Този пакет е наличен в няколко групи. -conda.registry = Настройте този регистър като Conda хранилище във вашия файл .condarc: -conda.install = За да инсталирате пакета с Conda, изпълнете следната команда: -owner.settings.cargo.rebuild.error = Неуспешно преизграждане на индекса на Cargo: %v -owner.settings.cargo.rebuild = Преизграждане на индекс -settings.link.button = Обновяване на връзката на хранилището -settings.link.select = Изберете хранилище -debian.repository.architectures = Архитектури -rpm.registry = Настройте този регистър от командния ред: -debian.registry = Настройте този регистър от командния ред: -helm.install = За да инсталирате пакета, изпълнете следната команда: -swift.registry = Настройте този регистър от командния ред: -settings.link = Свързване на този пакет с хранилище -settings.link.description = Ако свържете пакет с хранилище, пакетът се изброява в списъка с пакети на хранилището. -settings.link.success = Връзката на хранилището беше успешно обновена. -owner.settings.cleanuprules.pattern_full_match = Прилагане на шаблона към пълното име на пакета -owner.settings.cleanuprules.keep.title = Версиите, които съответстват на тези правила, се запазват, дори ако съответстват на правило за премахване по-долу. -debian.repository = Информация за хранилището -maven.install = За да използвате пакета, включете следното в блока dependencies във файла pom.xml: -nuget.install = За да инсталирате пакета с NuGet, изпълнете следната команда: -alt.install = Инсталиране на пакет -owner.settings.cleanuprules.edit = Редактиране на правилото за почистване -rpm.install = За да инсталирате пакета, изпълнете следната команда: -pypi.install = За да инсталирате пакета с pip, изпълнете следната команда: -arch.version.makedepends = Зависимости за изграждането -alpine.install = За да инсталирате пакета, изпълнете следната команда: desc = Управление на пакетите на хранилището. -owner.settings.cargo.rebuild.no_index = Не може да се преизгради, няма инициализиран индекс. -owner.settings.cargo.rebuild.description = Преизграждането може да бъде полезно, ако индексът не е синхронизиран със съхранените Cargo пакети. -owner.settings.cargo.initialize.description = Необходимо е специално Git хранилище за индекс, за да се използва регистърът на Cargo. Използването на тази опция ще (пре)създаде хранилището и ще го конфигурира автоматично. -pypi.requires = Изисква Python -debian.registry.info = Изберете $distribution и $component от списъка по-долу. -alpine.registry = Настройте този регистър, като добавите URL адреса във вашия файл /etc/apk/repositories: -owner.settings.cargo.initialize.success = Индексът на Cargo беше успешно създаден. -npm.registry = Настройте този регистър във файла на вашия проект .npmrc: -owner.settings.chef.keypair = Генериране на двойка ключове -owner.settings.chef.keypair.description = Заявките, изпратени до регистъра на Chef, трябва да бъдат криптографски подписани като средство за удостоверяване. При генериране на двойка ключове, само публичният ключ се съхранява във Forgejo. Частният ключ ви се предоставя, за да се използва с knife. Генерирането на нова двойка ключове ще презапише предишната. -owner.settings.cleanuprules.remove.title = Версиите, които съответстват на тези правила, се премахват, освен ако правило по-горе не казва да се запазят. -nuget.registry = Настройте този регистър от командния ред: -owner.settings.cleanuprules.success.update = Правилото за почистване е обновено. -settings.delete.notice = На път сте да изтриете %s (%s). Тази операция е необратима, сигурни ли сте? -npm.install2 = или го добавете във файла package.json: -owner.settings.cleanuprules.success.delete = Правилото за почистване е изтрито. -vagrant.install = За да добавите Vagrant box, изпълнете следната команда: -nuget.dependency.framework = Целева платформа -maven.install2 = Изпълнете през командния ред: -maven.download = За да изтеглите зависимостта, изпълнете през командния ред: -container.layers = Слоеве на образа -conan.install = За да инсталирате пакета с Conan, изпълнете следната команда: -composer.registry = Настройте този регистър във вашия файл ~/.composer/config.json: [tool] hours = %d часа @@ -705,7 +533,6 @@ forks = Разклонения editor.or = или issues.new_label_desc_placeholder = Описание watch_guest_user = Влезте, за да наблюдавате това хранилище. -migrate_items_milestones = Етапи unstar = Премахване на звездата owner = Притежател issues.num_comments_1 = %d коментар @@ -783,7 +610,6 @@ readme_helper_desc = Това е мястото, където можете да repo_gitignore_helper = Изберете .gitignore шаблони auto_init = Да се инициализира хранилище template.issue_labels = Етикети за задачите -migrate_items_labels = Етикети issues.label_templates.title = Зареждане на предв. зададен набор от етикети issues.label_templates.helper = Изберете предв. зададен набор от етикети projects.template.desc = Шаблон @@ -817,8 +643,6 @@ milestones.new = Нов етап milestones.cancel = Отказ settings.http_method = HTTP метод clone_helper = Нуждаете се от помощ за клониране? Посетете Помощ. -migrate_items_pullrequests = Заявки за сливане -migrate_items_wiki = Уики quick_guide = Бързо ръководство clone_this_repo = Клонирайте това хранилище push_exist_repo = Изтласкване на съществуващо хранилище от командния ред @@ -884,7 +708,6 @@ wiki.desc = Пишете и споделяйте документация със wiki.default_commit_message = Напишете бележка относно това обновяване на страницата (опционално). release.releases = Издания wiki.last_commit_info = %s редактира тази страница %s -migrate_items_releases = Издания release = Издание releases = Издания settings.desc = Настройките са мястото, където можете да управлявате настройките за хранилището @@ -928,7 +751,7 @@ issues.filter_poster = Автор issues.commented_at = `коментира %s` settings.transfer_desc = Прехвърлете това хранилище на потребител или на организация, за които имате администраторски права. settings.archive.button = Архивиране на хранилището -issues.role.owner_helper = Този потребител е притежателят на това хранилище. +issues.role.owner_helper = Този потребител е притежател на това хранилище. settings.delete_notices_2 = - Тази операция ще изтрие перманентно хранилището %s, включително кода, задачите, коментарите, данните на уикито и настройките за сътрудници. settings.admin_settings = Администраторски настройки issues.role.owner = Притежател @@ -1064,8 +887,6 @@ download_tar = Изтегляне на TAR.GZ desc.public = Публично desc.archived = Архивирано desc.internal = Вътрешно -migrate_items_merge_requests = Заявки за сливане -migrate_items_issues = Задачи fork_guest_user = Влезте, за да разклоните това хранилище. actions = Действия more_operations = Още операции @@ -1349,16 +1170,10 @@ activity.git_stats_pushed_1 = е изтласкал activity.git_stats_push_to_branch = към %s и contributors.contribution_type.commits = Подавания stars = Звезди -n_commit_few = %s подавания -n_branch_one = %s клон -n_branch_few = %s клона -n_tag_one = %s маркер -n_tag_few = %s маркера commit_graph = Граф с подавания commits.renamed_from = Преименувано от %s commits.view_path = Преглед на този момент в историята commits.search_branch = Този клон -n_commit_one = %s подаване release.ahead.commits = %d подавания release.stable = Стабилно commits.gpg_key_id = ID на GPG ключ @@ -1481,8 +1296,6 @@ issues.review.option.show_outdated_comments = Показване на остар issues.content_history.delete_from_history_confirm = Да се изтрие ли от историята? project = Проекти issues.content_history.delete_from_history = Изтриване от историята -n_release_few = %s издания -n_release_one = %s издание editor.cannot_edit_non_text_files = Двоични файлове не могат да се редактират през уеб интерфейса. settings.mirror_settings.push_mirror.copy_public_key = Копиране на публичния ключ activity.published_tag_label = Маркер @@ -1611,7 +1424,6 @@ file_follow = Последване на символната връзка commitstatus.failure = Неуспех issues.filter_label_exclude = Използвайте Alt + Click, за да изключите етикети migrate.migrating_failed = Мигрирането от %s е неуспешно. -migrate.migrating_issues = Мигриране на задачи mirror_from = огледално на fork_from_self = Не можете да разклоните хранилище, което притежавате. commit_graph.hide_pr_refs = Скриване на заявките за сливане @@ -1628,12 +1440,9 @@ form.reach_limit_of_creation_1 = Притежателят вече е дости editor.patching = Прилагане на кръпка: editor.fail_to_apply_patch = Неуспешно прилагане на кръпка „%s“ commits.no_commits = Няма общи подавания. „%s“ и „%s“ имат напълно различни истории. -migrate.migrating_pulls = Мигриране на заявки за сливане -migrate.migrating_topics = Мигриране на теми projects.desc = Управлявайте задачи и заявки за сливане в проектни табла. issues.choose.invalid_templates = %v невалидни шаблона са намерени pulls.edit.already_changed = Неуспешно запазване на промените в заявката за сливане. Изглежда съдържанието вече е променено от друг потребител. Моля, презаредете страницата и опитайте да редактирате отново, за да избегнете презаписването на техните промени -migrate.migrating_git = Мигриране на Git данни commits.newer = По-нови issues.choose.blank_about = Създаване на задача от стандартен шаблон. issues.filter_no_results = Няма резултати @@ -1643,8 +1452,6 @@ transfer.no_permission_to_accept = Нямате разрешение да при transfer.no_permission_to_reject = Нямате разрешение да отхвърлите това прехвърляне. editor.file_changed_while_editing = Съдържанието на файла е променено, откакто сте го отворили. Щракнете тук, за да го видите, или Подайте промените отново, за да ги презапишете. sync_fork.button = Синхронизиране -migrate.migrating_labels = Мигриране на етикети -migrate.migrating_releases = Мигриране на издания editor.push_rejected_no_message = Промяната беше отхвърлена от сървъра без съобщение. Моля, проверете Git куките. issues.choose.open_external_link = Отваряне comments.edit.already_changed = Неуспешно запазване на промените в коментара. Изглежда съдържанието вече е променено от друг потребител. Моля, презаредете страницата и опитайте да редактирате отново, за да избегнете презаписването на техните промени @@ -1671,7 +1478,6 @@ editor.commit_id_not_matching = Файлът е променен, докато editor.user_no_push_to_branch = Потребителят не може да изтласква в клона archive.pull.noreview = Това хранилище е архивирано. Не можете да рецензирате заявки за сливане. migrate.migrating_failed.error = Неуспешно мигриране: %s -migrate.migrating_milestones = Мигриране на етапи migrate.failed = Мигрирането е неуспешно: %v pulls.nothing_to_compare_and_allow_empty_pr = Тези клонове са равни. Тази заявка за сливане ще бъде празна. pulls.has_pull_request = `Вече съществува заявка за сливане между тези клонове: %[2]s#%[3]d` @@ -1712,7 +1518,6 @@ issues.time_spent_from_all_authors = `Общо изразходвано врем issues.attachment.download = `Щракнете, за да изтеглите „%s“` issues.attachment.open_tab = `Щракнете, за да видите „%s“ в нов раздел` pulls.update_branch = Обновяване на клона чрез сливане -migrate_items = Елементи за мигриране commit.load_referencing_branches_and_tags = Зареждане на клонове и маркери, препращащи към това подаване pulls.files_conflicted = Тази заявка за сливане има промени, които са в конфликт с целевия клон. pulls.still_in_progress = Все още е в процес на работа? @@ -1734,8 +1539,6 @@ editor.cannot_edit_lfs_files = LFS файлове не могат да се ре commits.ssh_key_fingerprint = Отпечатък на SSH ключ issues.comment_on_locked = Не можете да коментирате заключена задача. commit.revert = Връщане -migrate.cancel_migrating_title = Отказ от миграцията -migrate.cancel_migrating_confirm = Искате ли да откажете тази миграция? issues.choose.invalid_config = Конфигурацията на задачите съдържа грешки: unit_disabled = Администраторът на сайта е изключил тази секция на хранилището. issues.blocked_by_user = Не можете да създавате задачи в това хранилище, защото сте блокирани от притежателя на хранилището. @@ -1899,12 +1702,9 @@ diff.image.side_by_side = Едно до друго release.summary_card_alt = Карта с обобщение на издание със заглавие „%s“ в хранилище %s release.asset_external_url = Външен URL адрес error.csv.too_large = Не може да се визуализира този файл, защото е твърде голям. - -commit.cherry-pick = Отбиране pulls.cmd_instruction_checkout_title = Изтегляне pulls.cmd_instruction_merge_title = Сливане -settings.branches.switch_default_branch = Превключване на стандартния клон -settings.branches.add_new_rule = Добавяне на ново правило +commit.cherry-pick = Отбиране settings.pulls.ignore_whitespace = Игнориране на празните знаци при конфликти settings.pulls.enable_autodetect_manual_merge = Включване на автоматично откриване на ръчно сливане (Бележка: В някои специални случаи може да възникнат грешни преценки) settings.pulls.allow_rebase_update = Включване на обновяването на клон на заявка за сливане чрез пребазиране @@ -1966,6 +1766,8 @@ settings.update_githook = Обновяване на куката settings.add_webhook_desc = Forgejo ще изпраща POST заявки с определен Content-Type до целевия URL адрес. Прочетете повече в ръководството за уеб-куки. settings.payload_url = Целеви URL адрес settings.secret = Тайна +settings.branches.switch_default_branch = Превключване на стандартния клон +settings.branches.add_new_rule = Добавяне на ново правило settings.protected_branch.save_rule = Запазване на правилото settings.protected_branch.delete_rule = Изтриване на правилото settings.protect_disable_push = Изключване на изтласкването @@ -1986,7 +1788,6 @@ settings.chat_id = ID на чата settings.thread_id = ID на нишката settings.matrix.room_id = ID на стаята settings.matrix.message_type = Тип съобщение - issues.label_open_issues = %d отворени задачи/заявки за сливане issues.summary_card_alt = Карта с обобщение на задача със заглавие „%s“ в хранилище %s release.type_attachment = Прикачен файл @@ -2010,7 +1811,7 @@ buttons.list.task.tooltip = Добавяне на списък със задач buttons.enable_monospace_font = Включване на равноширок шрифт buttons.mention.tooltip = Споменаване на потребител или екип buttons.italic.tooltip = Добавяне на курсив текст (Ctrl+I / ⌘I) -buttons.link.tooltip = Добавяне на връзка +buttons.link.tooltip = Добавяне на връзка (Ctrl+K / ⌘K) buttons.disable_monospace_font = Изключване на равноширокия шрифт buttons.ref.tooltip = Препратка към задача или заявка за сливане table_modal.label.columns = Колони @@ -2024,7 +1825,6 @@ link_modal.header = Добавяне на връзка buttons.indent.tooltip = Вмъкване на елементи с едно ниво buttons.unindent.tooltip = Изваждане на елементи с едно ниво link_modal.paste_reminder = Подсказка: С URL адрес в клипборда можете да поставите директно в редактора, за да създадете връзка. - link_modal.url = Адрес [org] @@ -2237,7 +2037,6 @@ reply = или отговорете директно на това ел. пис reset_password.text = Ако това сте вие, моля, щракнете върху следната връзка, за да възстановите акаунта си в рамките на %s: primary_mail_change.subject = Основният ви адрес за ел. поща е променен account_security_caution.text_2 = Ако това не сте били вие, акаунтът ви е компрометиран. Моля, свържете се с администраторите на този сайт. - totp_disabled.subject = TOTP е изключен totp_disabled.text_1 = Еднократната парола, базирана на време (TOTP), за вашия акаунт току-що беше изключена. totp_disabled.no_2fa = Вече няма конфигурирани други 2FA методи, което означава, че вече не е необходимо да влизате в акаунта си с 2FA. @@ -2349,7 +2148,6 @@ auths.port = Порт auths.type = Тип config.ssh_config = SSH конфигурация monitor.stats = Статистика -monitor.queue = Опашка: %s config = Конфигурация config.mailer_user = Потребител config.enable_captcha = Включване на CAPTCHA @@ -2359,7 +2157,6 @@ config.git_config = Git конфигурация config.mailer_protocol = Протокол users.bot = Бот config.db_path = Път -monitor.queues = Опашки config.server_config = Сървърна конфигурация packages.size = Размер settings = Админ. настройки @@ -2384,9 +2181,7 @@ packages.total_size = Общ размер: %s dashboard.new_version_hint = Forgejo %s вече е наличен, вие изпълнявате %s. Проверете блога за повече подробности. total = Общо: %d config.db_type = Тип -monitor.queue.type = Тип notices.type = Тип - users.prohibit_login = Замразен акаунт [error] @@ -2476,7 +2271,6 @@ repository_files_already_exist.delete = Вече съществуват файл invalid_gpg_key = Не може да се потвърди вашият GPG ключ: %s git_ref_name_error = ` трябва да е правилно форматирано име на Git препратка.` last_org_owner = Не можете да премахнете последния потребител от екипа на „притежателите“. Трябва да има поне един притежател за организация. - AccessToken = Токен за достъп CommitChoice = Избор на подаване username_claiming_cooldown = Потребителското име не може да бъде взето, тъй като периодът му на изчакване все още не е приключил. То може да бъде взето на %[1]s. @@ -2575,7 +2369,6 @@ change_unconfirmed_email_error = Неуспешна промяна на адре resend_mail = Щракнете тук, за повторно изпращане на ел. писмо за активация change_unconfirmed_email_summary = Промяна на адреса, на който се изпраща ел. писмо за активация. change_unconfirmed_email = Ако сте въвели грешен адрес за ел. поща по време на регистрацията, можете да го промените по-долу и потвърждение ще бъде изпратено на новия адрес. - prohibit_login = Акаунтът е замразен prohibit_login_desc = Вашият акаунт е замразен и не може да взаимодейства с инстанцията. Свържете се с администратора, за да възстановите достъпа си. non_local_account = Нелокални потребители не могат да обновяват паролата си чрез уеб интерфейса на Forgejo. @@ -2616,18 +2409,6 @@ platform_desc = Forgejo работи на свободни операционн license_desc = Вземете Forgejo! Присъединете се към нас, допринасяйки, за да направите този проект още по-добър. Не се колебайте да сътрудничите! [notification] -subscriptions = Абонаменти -unread = Непрочетени -no_subscriptions = Няма абонаменти -mark_as_unread = Отбелязване като непрочетено -no_read = Няма прочетени известия. -mark_as_read = Отбелязване като прочетено -notifications = Известия -read = Прочетени -watching = Наблюдавани -no_unread = Няма непрочетени известия. -mark_all_as_read = Отбелязване на всички като прочетени -pin = Закачване на известието [explore] go_to = Отиване към @@ -2640,34 +2421,8 @@ relevant_repositories = Показани са само подходящи хра relevant_repositories_tooltip = Хранилищата, които са разклонения или нямат тема, икона или описание, са скрити. [actions] -runners.version = Версия -variables = Променливи -runners.labels = Етикети -actions = Действия -variables.none = Все още няма променливи. -variables.creation.failed = Неуспешно добавяне на променлива. -variables.update.failed = Неуспешно редактиране на променлива. -variables.creation.success = Променливата „%s“ е добавена. -variables.deletion.success = Променливата е премахната. -variables.edit = Редактиране на променливата -variables.deletion = Премахване на променливата -variables.update.success = Променливата е редактирана. -variables.creation = Добавяне на променлива -variables.deletion.failed = Неуспешно премахване на променлива. -runners.task_list.repository = Хранилище -runners.description = Описание runs.no_workflows.help_no_write_access = За да научите повече за Forgejo Actions, вижте документацията. -variables.management = Управление на променливи -variables.not_found = Променливата не е открита. variables.id_not_exist = Променлива с идентификатор %d не съществува. -runners.owner_type = Тип -status.cancelled = Отменено -status.running = Изпълнява се -status.success = Успешно -status.waiting = Изчаква се -status.unknown = Неизвестно -status.failure = Неуспешно -status.skipped = Пропуснато unit.desc = Управление на интегрирани CI/CD pipelines с Forgejo Actions. [heatmap] @@ -2689,10 +2444,6 @@ submodule = Подмодул [dropzone] -default_message = Пуснете файлове тук или щракнете, за качване. -remove_file = Премахване на файла -file_too_big = Размерът на файла ({{filesize}} MB) надвишава максималния размер от ({{maxFilesize}} MB). -invalid_input_type = Не можете да качвате файлове от този тип. [graphs] component_loading_failed = Неуспешно зареждане на %s @@ -2733,37 +2484,17 @@ keyword_search_unavailable = Търсенето по ключова дума в union_tooltip = Включване на резултати, които съвпадат с някоя от ключовите думи, разделени с интервал union = Обединение type_tooltip = Тип търсене - runner_kind = Търсене на изпълнители… [markup] -filepreview.lines = Редове от %[1]d до %[2]d в %[3]s -filepreview.line = Ред %[1]d в %[2]s [munits.data] -b = Б -kib = КиБ -mib = МиБ -gib = ГиБ -tib = ТиБ -pib = ПиБ -eib = ЕиБ - [translation_meta] test = окей [gpg] -default_key = Подписано с ключ по подразбиране -error.no_gpg_keys_found = Не е намерен известен ключ за този подпис в базата данни -error.not_signed_commit = Не е подписано подаване -error.generate_hash = Неуспешно генериране на хеш на подаването -error.extract_sign = Неуспешно извличане на подпис -error.probable_bad_signature = ВНИМАНИЕ! Въпреки че има ключ с това ID в базата данни, той не потвърждава това подаване! Това подаване е ПОДОЗРИТЕЛНО. -error.failed_retrieval_gpg_keys = Неуспешно извличане на ключ, свързан с акаунта на подаващия -error.probable_bad_default_signature = ВНИМАНИЕ! Въпреки че ключът по подразбиране има това ID, той не потвърждава това подаване! Това подаване е ПОДОЗРИТЕЛНО. -error.no_committer_account = Няма акаунт, свързан с адреса за ел. поща на подаващия [repo.permissions] projects.read = Четене: Достъп до проектните табла на хранилището. diff --git a/options/locale/locale_bn.ini b/options/locale/locale_bn.ini index ebcad77f97..1b90534602 100644 --- a/options/locale/locale_bn.ini +++ b/options/locale/locale_bn.ini @@ -6,6 +6,6 @@ explore = এক্সপ্লোর logo = লোগো sign_in = সাইন ইন sign_in_or = বা -sign_in_with_provider = %s দিয়ে সাইন-ইন করুন sign_out = সাইন আউট +sign_in_with_provider = %s দিয়ে সাইন-ইন করুন sign_up = নিবন্ধন করুন \ No newline at end of file diff --git a/options/locale/locale_ca.ini b/options/locale/locale_ca.ini index 2f19427fa5..fc77e81569 100644 --- a/options/locale/locale_ca.ini +++ b/options/locale/locale_ca.ini @@ -1,6 +1,6 @@ [common] home = Inici -dashboard = Panell de control +dashboard = Tauler de control explore = Explorar help = Ajuda logo = Logotip @@ -37,17 +37,6 @@ captcha = CAPTCHA twofa = Autenticació de doble factor twofa_scratch = Codi de rascar de doble-factor passcode = Codi de pas -webauthn_insert_key = Inseriu la vostra clau de seguretat -webauthn_sign_in = Premeu el botó a la vostra clau de seguretat. Si no en té, torneu-la a inserir. -webauthn_press_button = Si us plau, premeu el botó a la vostra clau de seguretat… -webauthn_use_twofa = Utilitza un codi de doble factor des del teu mòbil -webauthn_error = No s'ha pogut llegir la clau de seguretat. -webauthn_unsupported_browser = El teu navegador no suprta WebAuthn. -webauthn_error_unknown = Hi ha hagut un error desconegut. Si us plau torneu-ho a intentar. -webauthn_error_insecure = WebAuthn només suporta connexions segures. Per provar sobre HTTP, podeu utilitzar l'origen "localhost" o "127.0.0.1" -webauthn_error_unable_to_process = El servidor no ha pogut processar la vostra sol·licitud. -webauthn_error_duplicated = La clau de seguretat no és permesa per aquesta sol·licitud. Si us plau, assegureu-vos que la clau encara no ha estat registrada. -webauthn_error_empty = S'ha d'anomenar aquesta clau. repository = Repositori organization = Organització mirror = Mirall @@ -85,7 +74,6 @@ disabled = Deshabilitat filter.public = Públic filter.private = Privat show_full_screen = Mostra a pantalla completa -webauthn_error_timeout = Temps d'espera finalitzar abans que la seva clau pogués ser llegida. Siusplau recarregueu la pàgina i torneu-ho a intentar. remove_label_str = Esborra l'element "%s" error413 = Ha exhaurit la quota. cancel = Canceŀlar @@ -414,7 +402,7 @@ buttons.quote.tooltip = Citar text buttons.enable_monospace_font = Habilitar la font monoespai buttons.disable_monospace_font = Deshabilita la font monoespai buttons.code.tooltip = Afegir codi -buttons.link.tooltip = Afegir un enllaç +buttons.link.tooltip = Afegir un enllaç (Ctrl+K / ⌘K) buttons.list.unordered.tooltip = Afegir un llista de punts buttons.list.ordered.tooltip = Afegir una llista enumerada buttons.list.task.tooltip = Afegir una llista de tasques @@ -616,15 +604,12 @@ username_error_no_dots = ` només pot contenir caràcters alfanumèrics ("0-9"," username_claiming_cooldown = No es pot reclamar el nom d'usuari perquè el seu període de temps de recuperació encara no ha acabat. Es podrà reclamar el %[1]s. invalid_group_team_map_error = ` el mapatge no és vàlid: %s` repository_force_private = S'ha activat "Forçar privat": els repositoris privats no es poden fer públics. - 2fa_auth_required = L'accés remot requereix una autenticació de doble factor. - visit_rate_limit = S'ha sobrepassat la taxa de visita remota. unset_password = L'usuari no ha establert una contrasenya. - +invalid_ssh_principal = Principal invàlid: %s team_no_units_error = Permet l'accés a una secció del repositori com a mínim. unsupported_login_type = El tipus d'accés no permet eliminar el compte. -invalid_ssh_principal = Principal invàlid: %s [settings] pronouns = Pronoms @@ -889,7 +874,6 @@ visibility.public = Públic visibility.limited = Limitat visibility.private = Privat primary = Principal - lookup_avatar_by_mail = Cercar l'avatar amb l'adreça de correu electrònic access_token_desc = Els permisos de testimoni seleccionats es limiten a les rutes API corresponents. Llegiu la documentació per a més informació. oauth2_confidential_client = Client confidencial. Seleccioneu aquesta opció per a aplicacions que mantinguin el secret confidencial, com les aplicacions web. No la seleccioneu pas per a aplicacions nadiues, tant d'escriptori com mòbils. @@ -910,12 +894,19 @@ quota.rule.no_limit = Il·limitat quota.sizes.all = Tot quota.sizes.assets.all = Recursos quota.sizes.wiki = Wiki - user_block_yourself = No us podeu bloquejar. quota.applies_to_user = Les regles de quota següents s'apliquen al vostre compte quota.applies_to_org = Les regles de quota següents s'apliquen a aquesta organització quota.rule.exceeded = Sobrepassat - +manage_ssh_principals = Gestiona els Certificats de Principals SSH +principal_desc = Aquests certificats principals d'SSH són associats al vostre compte i permeten accés complet als vostres repositoris. +add_new_principal = Afegir principal +ssh_principal_been_used = Aquest principal ja s'ha afegit al servidor. +add_principal_success = S'ha afegit el certificat principal SSH "%S". +ssh_principal_deletion = Eliminar Certificat Principal SSH +ssh_principal_deletion_desc = Eliminar un Certificat Principal SSH revoca el seu accés al vostre compte. Continuar? +ssh_principal_deletion_success = S'ha eliminat el principal. +principal_state_desc = Aquest principal s'ha usat en els darrers 7 dies change_username_redirect_prompt.with_cooldown.one = El nom d'usuari antic estarà disponible per a tothom després d'%[1]d dia. Podeu reclamar-lo abans que passi aquest temps. change_username_redirect_prompt.with_cooldown.few = El nom d'usuari antic estarà disponible per a tothom després de %[1]d dies. Podeu reclamar-lo abans que passi aquest temps. additional_repo_units_hint_description = Mostra un suggeriment per "Habilitar-ne més" pels repositoris que no tenen habilitades totes les unitats. @@ -925,22 +916,13 @@ primary_email = Fer-la principal activate_email = Enviar l'activació email_preference_set_success = S'ha configurat correctament la preferència de correu electrònic. keep_email_private_popup = La vostra adreça de correu electrònic no es mostrarà al vostre perfil i no serà la predeterminada pels commits fets mitjançat la interfície web, com ara pujades de fitxers, modificacions i commits de fusió. En el seu lloc, una adreça especial %s es pot fer servir per enllaçar commits al vostre compte. Aquesta opció no afectarà els commits ja existents. -manage_ssh_principals = Gestiona els Certificats de Principals SSH add_key = Afegir una clau -principal_desc = Aquests certificats principals d'SSH són associats al vostre compte i permeten accés complet als vostres repositoris. -add_new_principal = Afegir principal -ssh_principal_been_used = Aquest principal ja s'ha afegit al servidor. gpg_key_matched_identities_long = Les identitats incrustades en aquesta clau coincideixen amb les següents adreces de correu electrònic activades per aquest usuari. Els commits coincidents amb aquestes adreces de correu electrònic es poden verificar amb aquesta clau. gpg_token_signature = Firma GPG blindada ssh_token_signature = Firma SSH blindada -add_principal_success = S'ha afegit el certificat principal SSH "%S". -ssh_principal_deletion = Eliminar Certificat Principal SSH ssh_key_deletion_desc = Eliminar una clau SSH en revocarà l'accés al vostre compte. Voleu continuar? gpg_key_deletion_desc = Eliminar una clau GPG des-verificarà els commits firmats per ella. Voleu continuar? -ssh_principal_deletion_desc = Eliminar un Certificat Principal SSH revoca el seu accés al vostre compte. Continuar? -ssh_principal_deletion_success = S'ha eliminat el principal. valid_until_date = Vàlid fins %s -principal_state_desc = Aquest principal s'ha usat en els darrers 7 dies repo_and_org_access = Accés al repositori i a l'organització webauthn_delete_key = Eliminar la clau de seguretat webauthn_alternative_tip = Segurament voldreu configurar un mètode d'autenticació addicional. @@ -1136,11 +1118,6 @@ template.webhooks = Webhooks template.topics = Temes template.avatar = Avatar need_auth = Autorització -migrate_items_wiki = Wiki -migrate_items_milestones = Fites -migrate_items_labels = Etiquetes -migrate_items_issues = Problemes -migrate_items_releases = Publicacions unwatch = Deixa de seguir watch = Segueix unstar = Treu l'estrella @@ -1477,7 +1454,7 @@ issues.ref_closing_from = `ha fet referència a aquesta incidèn issues.ref_reopening_from = `ha fet referència a aquesta incidència en una sol·licitud d'extracció %[3]s que la reobrirà, %[1]s` issues.author.tooltip.issue = Aquest usuari és l'autor d'aquesta incidència. issues.author.tooltip.pr = Aquest usuari és l'autor d'aquesta sol·licitud d'extracció. -issues.role.owner_helper = Aquest usuari és el propietari d'aquest repositori. +issues.role.owner_helper = Aquest usuari és un propietari d'aquest repositori. issues.role.member_helper = Aquest usuari és membre de la organització a la qual pertany aquest repositori. issues.role.collaborator_helper = S'ha convidat aquest usuari a col·laborar al repositori. issues.role.first_time_contributor = Col·laborador per primera vegada @@ -1627,7 +1604,6 @@ activity.git_stats_addition_n = %d addicions activity.git_stats_deletion_1 = %d eliminació activity.git_stats_deletion_n = %d eliminacions settings.mirror_settings.docs = Configureu el vostre repositori per sincronitzar commits, etiquetes i branques automàticament amb un altre repositori. - rss.must_be_on_branch = Heu d'estar en una branca per a tenir un canal RSS. admin.manage_flags = Gestiona les marques admin.enabled_flags = Marques habilitades del repositori: @@ -1659,8 +1635,6 @@ form.string_too_long = El text introduït té més de %d caràcters. migrate_options = Opcions de migració migrate_options_mirror_helper = Aquest repositori serà un mirall migrate_options_lfs_endpoint.description.local = També s'accepta un camí al servidor local. -migrate_items = Elements de migració - open_with_editor = Obre amb %s mirror_prune_desc = Elimina les referències de seguiment remot obsoletes mirror_sync_on_commit = Sincronitza quan es pugin commits @@ -1687,388 +1661,10 @@ migrate_options_lfs = Migra els fitxers LFS migrate_options_lfs_endpoint.label = Punt final LFS migrate_options_lfs_endpoint.description = En migrar, s'intentarà utilitzar el vostre remot git per a determinar el servidor LFS. També podeu especificar un punt final personalitzat si les dades LFS del repositori són en un altre lloc. migrate_options_lfs_endpoint.placeholder = Si ho deixeu en blanc, el punt final derivarà de l'URL de clonació -migrate_items_pullrequests = Pull requests - -fork_repo = Bifurca el repositori -fork_from = Bifurcar des de -repo_gitignore_helper_desc = Escolliu de quins fitxers no s'ha de fer seguiment d'una llista de plantilles per llenguatges comuns. Els artefactes típics generats per les eines de construcció de cada llenguatge estan incloses al .gitignore de manera predeterminada. -readme_helper_desc = Aquí podeu escriure una descripció completa del vostre projecte. -default_branch_helper = La branca per defecte és la branca base pels pull requests i els commits. -mirror_interval = Interval de rèplica (les unitats de temps vàlides són "h", "m", "s"). 0 per desactivar la sincronització periòdica. (Interval mínim: %s) -mirror_use_ssh.helper = Si seleccioneu aquesta opció, Forgejo replicarà el repositori mitjançant Git per SSH i us crearà una parella de claus. Heu d'assegurar-vos que la clau pública generada estigui autoritzada per publicar al repositori de destí. No podreu fer servir autenticació basada en contrasenyes. -delete_preexisting_success = S'ha suprimit els fitxers no-adoptats a %s -blame_prior = Veure el «blame» anterior a aquest canvi -blame.ignore_revs = S'ignoren les revisions a .git-blame-ignore-revs. Fes clic aquí per veure-les. -blame.ignore_revs.failed = No s'han pogut ignorar les revisions a .git-blame-ignore-revs. -author_search_tooltip = Es mostra un màxim de 30 usuaris -summary_card_alt = Resum del repositori %s -tree_path_not_found.commit = El camí %[1]s no existeix al commit %[2]s -tree_path_not_found.branch = El camí %[1]s no existeix a la branca %[2]s -tree_path_not_found.tag = El camí %[1]s no existeix a l'etiqueta %[2]s -transfer.accept = Accepta la transferència -transfer.accept_desc = Transfereix a "%s" -transfer.reject = Denega la transferència -transfer.reject_desc = Cancel·la la transferència a "%s" -transfer.no_permission_to_accept = No teniu permisos per acceptar aquesta transferència. -transfer.no_permission_to_reject = No teniu permisos per denegar aquesta transferència. -template.items = Ítems de la plantilla -template.git_content = Contingut Git (Branca predeterminada) -template.git_hooks = Ganxos Git -migrate_items_merge_requests = Sol·licituds de fusió -migrate_repo = Migra el repositori -migrate.repo_desc_helper = Deixa-ho buit per importar la descripció existent -migrate.clone_address = Migra / Clona des d'una URL -migrate.clone_address_desc = L'URL d'HTTP(S) o de Git "clone" d'un repositori ja existent -migrate.github_token_desc = Podeu posar un o més testimonis separats amb comes per fer que la migració sigui més ràpida, evitant així el límit de taxa de l'API de GitHub. ATENCIÓ: Abusar aquesta funció pot anar en contra de la política de servei del proveïdor i pot comportar el bloqueig del/s vostre/s compte/s. -migrate.clone_local_path = o el camí a un servidor local -migrate.permission_denied = No podeu importar repositoris locals. -migrate.permission_denied_blocked = No podeu importar des d'amfitrions rebutjats, si us plau, demaneu a l'administrador que comprovi les configuracions ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS. -migrate.invalid_local_path = El camí local és invàlid. No existeix o no és un directori. -migrate.invalid_lfs_endpoint = El punt final LFS no és vàlid. -migrate.failed = La migració ha fallat: %v -migrate.migrate_items_options = Cal un testimoni d'accés per migrar els ítems addicionals -migrated_from = Migrat des de %[2]s -migrated_from_fake = Migrat des de %[1]s -migrate.migrate = Migra des de %s -migrate.migrating = S'està migrant des de %s … -migrate.migrating_failed = La migració des de %s ha fallat. -migrate.migrating_failed.error = No s'ha pogut migrar: %s -migrate.migrating_failed_no_addr = La migració ha fallat. -migrate.migrating_git = Migrant dades Git -migrate.migrating_topics = Migrant tòpics -migrate.migrating_milestones = Migrant fites -migrate.migrating_labels = Migrant etiquetes -migrate.migrating_releases = Migrant publicacions -migrate.migrating_issues = Migrant problemes -migrate.migrating_pulls = Migrant «pull requests» -migrate.cancel_migrating_title = Cancel·la la migració -migrate.cancel_migrating_confirm = Voleu cancel·lar aquesta migració? -mirror_from = mirall de -forked_from = bifurcat des de generated_from = generat des de -fork_from_self = No podeu bifurcar un repositori que us pertany. -fork_guest_user = Inicia sessió per bifurcar aquest repositori. -watch_guest_user = Inicia sessió per vigilar aquest repositori. -star_guest_user = Inicia sessió per destacar aquest repositori. -subscribe.issue.guest.tooltip = Inicia sessió per subscriure't a aquest problema. -subscribe.pull.guest.tooltip = Inicia sessió per subscriure't a aquesta «pull request». -more_operations = Més operacions -no_desc = Sense descripció -quick_guide = Guia ràpida -clone_this_repo = Clona aquest repositori -cite_this_repo = Cita aquest repositori -create_new_repo_command = Crea un nou repositori a la línia de comandes -push_exist_repo = Puja un repositori ja existent des de la línia de comandes -empty_message = Aquest repositori està buit. -broken_message = Les dades Git d'aquest repositori no es poden llegir. Contacta amb l'administrador d'aquesta instància o suprimeix aquest repositori. -code.desc = Accedeix al codi font, els fitxers, els commits i les branques. -clear_ref = `Esborra la referència actual` -filter_branch_and_tag = Filtra per branca o etiqueta -find_tag = Cerca etiqueta -n_commit_one = %s commit -n_commit_few = %s commits -n_branch_one = Branca %s -n_branch_few = Branques %s -n_tag_one = Etiqueta %s -n_tag_few = Etiquetes %s -n_release_one = Publicació %s -n_release_few = Publicacions %s -file.title = %s a %s -file_follow = Seguir l'enllaç simbòlic -file_view_rendered = Veure renderitzat -file_view_raw = Veure cru -escape_control_characters =Escapar -commit_graph = Gràfic de commits -commit.contained_in = Aquest commit és a: -no_eol.text = Sense EOL -editor.commit_signed_changes = Fes un commit signat dels canvis -editor.commit_changes = Fes un commit dels canvis -editor.add_tmpl.filename = nom del fitxer editor.update = Actualitzar %s -editor.patch = Aplicar pedaç -editor.patching = Aplicant el pedaç a: -editor.signoff_desc = Afegeix un "Signat per" seguit de l'autor al final del missatge del commit. -editor.commit_directly_to_this_branch = Feu un commit directament a la branca %[1]s. -editor.new_branch_name = Anomena la nova branca per a aquest commit -editor.file_already_exists = Ja hi ha un fitxer anomenat "%S" en aquest repositori. -editor.commit_id_not_matching = El fitxer ha canviat mentre l'editaves. Fes un commit a una nova branca i llavors fusiona-la. -editor.push_out_of_date = La pujada està desactualitzada. -editor.commit_empty_file_header = Fes un commit d'un fitxer buit -editor.user_no_push_to_branch = L'usuari no pot pujar a la branca -editor.cherry_pick = Triar a dit %s a: -editor.revert = Retorna %s a: -editor.commit_email = Adreça electrònica del commit -commits.no_commits = No hi ha commits en comú. "%s" i "%s" tenen històries completament diferents. -commits.signed_by_untrusted_user_unmatched = Signat per un usuari no fiable que no coincideix amb l'autor del commit -commits.view_path = Veure en aquest punt de la història -commits.view_single_diff = Veure els canvis fets en aquest fitxer introduïts en aquest commit -commit.revert-header = Retorna: %s -commit.revert-content = Selecciona una branca per retornar-hi: -commit.cherry-pick = Triar a dit -commit.cherry-pick-header = Triar a dit: %s -commit.cherry-pick-content = Seleccioneu una branca per triar-hi: -projects.new_subheader = Coordineu, feu seguiment, i actualitzeu el vostre treball en un sol lloc, per tal que els projectes es mantinguin transparents i dins del calendari previst. -projects.type.basic_kanban = Kanban bàsic -projects.type.bug_triage = Triatge d'errors -projects.column.set_default = Definir com a predeterminada -projects.column.set_default_desc = Estableix aquesta columna com a predeterminada per als problemes i pulls sense categoria -projects.column.assigned_to = Assignat a -projects.card_type.desc = Previsualització de targeta -issues.filter_assignees = Filtrar assignats -issues.filter_reviewers = Filtrar revisor -issues.new.open_projects = Projectes oberts -issues.new.open_milestone = Fites obertes -issues.new.clear_assignees = Esborra assignats -issues.new.no_assignees = Sense assignats -issues.new.assign_to_me = Assigna-m'ho -issues.new.no_reviewers = Sense revisors -issues.edit.already_changed = No s'han pogut desar els canvis al problema. Sembla que un altre usuari n'ha modificat el contingut. Si us plau, refresqueu la pàgina i intenteu-ho un altre cop per evitar reescriure els seus canvis -issues.choose.get_started = Començar -issues.choose.blank_about = Crea un problema a partir de la plantilla per defecte. -issues.choose.invalid_config = La configuració del problema té errors: -issues.label_templates.use = Utilitza una etiqueta predeterminada -issues.label_templates.fail_to_load_file = No s'ha pogut carregar el fitxer d'etiqueta predeterminada "%s": %v -issues.add_label = s'ha afegit l'etiqueta %s %s -issues.add_labels = s'han afegit les etiquetes %s %s -issues.remove_label = s'ha esborrat l'etiqueta %s %s -issues.remove_labels = s'han esborrat les etiquetes %s %s -issues.add_remove_labels = s'han afegit %s etiquetes i esborrat %s %s -issues.add_milestone_at = `s'ha afegit a la fita %s %s` -issues.add_project_at = `s'ha afegit al projecte %s %s` -issues.change_milestone_at = `s'ha modificat la fita des de %s a %s %s` -issues.change_project_at = `s'ha modificat el projecte des de %s a %s %s` -issues.remove_milestone_at = `s'ha eliminat de la fita %s %s` -issues.remove_project_at = `s'ha eliminat del projecte %s %s` -issues.self_assign_at = `s'ha auto-assignat aquest %s` -issues.add_assignee_at = `va ser assignat per %s %s` -issues.remove_assignee_at = `va ser desassignat per %s %s` -issues.remove_self_assignment = `s'ha eliminat la seva tasca %s` -issues.change_title_at = `s'ha canviat el títol de %s a %s %s` -issues.change_ref_at = `s'ha canviat la referència de %s a %s %s` -issues.remove_ref_at = `s'ha eliminat la referència %s %s` -issues.add_ref_at = `s'ha afegit la referència %s %s` -issues.delete_branch_at = `s'ha eliminat la branca %s %s` -issues.filter_label_no_select = Totes les etiquetes -issues.filter_label_select_no_label = Sense etiqueta -issues.filter_milestone_none = Sense fites -issues.filter_milestone_open = Fites obertes -issues.filter_milestone_closed = Fites tancades -issues.filter_project_all = Tots els projectes -issues.filter_project_none = Sense projecte -issues.filter_assginee_no_select = Tots els assignats -issues.filter_assginee_no_assignee = Sense assignat -issues.filter_poster_no_select = Tots els autors -issues.filter_type.all_pull_requests = Totes les «pull requests» -issues.filter_type.all_issues = Tots els problemes -issues.filter_type.assigned_to_you = Tasques que tens assignades -issues.filter_type.created_by_you = Creat per tu -issues.filter_type.mentioning_you = Que et mencionen -issues.filter_type.review_requested = S'ha demanat la revisió -issues.filter_type.reviewed_by_you = Revisats per tu -issues.filter_sort.relevance = Rellevància -issues.filter_sort.recentupdate = Actualitzats recentment -issues.filter_sort.leastupdate = Actualitzats menys recentment -issues.filter_sort.mostcomment = Les més comentades -issues.filter_sort.leastcomment = Menys comentats -issues.filter_sort.nearduedate = A prop de la data de venciment -issues.filter_sort.farduedate = Lluny de la data de venciment -issues.filter_sort.moststars = Més destacats -issues.filter_sort.feweststars = Menys destacats -issues.filter_sort.mostforks = Més bifurcacions -issues.filter_sort.fewestforks = Menys bifurcacions -issues.action_assignee_no_select = Sense assignat -pulls.merged_by = per %[3]s s'ha fusionat %[1]s -issues.closed_by = per %[3]s s'ha tancat %[1]s -issues.context.menu = Menú de comentari -issues.comment_pull_merged_at = s'ha fusionat el commit %[1]s a %[2]s %[3]s -issues.comment_manually_pull_merged_at = s'ha fusionat el commit %[1]s manualment a %[2]s %[3]s -issues.role.contributor_helper = Aquest usuari ja ha fet commits en aquest repositori. -issues.is_stale = Hi ha hagut canvis a aquest PR des de la revisió -issues.label_exclusive_desc = Doneu nom a l'àmbit/ítem de l'etiqueta per fer-la mútuament exclusiva amb altres àmbits/ etiquetes. -issues.label_exclusive_warning = Qualsevol etiqueta amb àmbits conflictius s'esborrarà quan s'editin les etiquetes d'un problema o «pull request». -issues.archived_label_description = (Arxivats) %s -issues.label.filter_sort.reverse_alphabetically = Capgira-ho alfabèticament -issues.label.filter_sort.by_size = Mida més petita -issues.label.filter_sort.reverse_by_size = Mida més gran -issues.num_participants_one = %d participant -issues.num_participants_few = %d participants -issues.tracker = Rastrejador de temps -issues.start_tracking_short = Inicia el temporitzador -issues.start_tracking = Inicia el rastreig de temps -issues.start_tracking_history = `s'ha començat %s` -issues.tracker_auto_close = El temporitzador s'aturarà automàticament quan el problema s'hagi resolt -issues.tracking_already_started = `Ja has començat a rastrejar el temps en un altre problema!` -issues.stop_tracking = Atura el temporitzador -issues.stop_tracking_history = `s'ha aturat %s` -issues.cancel_tracking_history = `s'ha cancel·lat el rastreig de temps %s` -issues.add_time = Afegeix el temps manualment -issues.del_time = Elimina aquest registre de temps -issues.add_time_short = Afegeix temps -issues.add_time_history = `s'ha afegit temps passat %s` -issues.del_time_history = `s'ha eliminat temps passat %s` -issues.add_time_sum_to_small = No s'ha introduït temps. -issues.time_spent_from_all_authors = `Temps total: %s` -issues.due_date = Data de venciment -issues.push_commit_1 = s'ha afegit %d commit %s -issues.push_commits_n = s'ha afegit %d commits %s -issues.force_push_codes = `s'ha forçat la pujada %[1]s des de %[2]s %[8]s a %[4]s %[9]s %[6]s` -issues.due_date_not_set = No s'ha establert una data de venciment. -issues.due_date_added = s'ha afegit la data de venciment %s %s -issues.due_date_modified = s'ha modificat la data de venciment de %[2]s a %[1]s %[3]s -issues.due_date_remove = s'ha eliminat la data de venciment %s %s -issues.due_date_invalid = La data de venciment no és vàlida o és fora de rang. Si us plau, useu el format "aaaa-mm-dd". -issues.dependency.issue_no_dependencies = No s'han establert dependències. -issues.dependency.pr_no_dependencies = No s'ha establert cap dependència. -issues.dependency.no_permission_1 = No teniu permisos per llegir %d dependència -issues.dependency.no_permission_n = No teniu permisos per llegir %d dependències -issues.dependency.no_permission.can_remove = No teniu permisos per llegir aquesta dependència, però la podeu eliminar -issues.dependency.add = Afegir dependència… -issues.dependency.remove_info = Eliminar aquesta dependència -issues.dependency.added_dependency = `s'ha afegit una nova dependència %s` -issues.dependency.removed_dependency = `s'ha eliminat una dependència %s` -issues.dependency.pr_closing_blockedby = No es pot tancar aquesta «pull request» perquè té els següents problemes -issues.dependency.issue_closing_blockedby = No es pot tancar aquest problema perquè té els següents problemes -issues.dependency.issue_close_blocks = Aquest problema bloqueja el tancament d'aquests altres problemes -issues.dependency.pr_close_blocks = Aquesta «pull request» bloqueja el tancament d'aquests problemes -issues.dependency.issue_close_blocked = Heu de tancar tots els problemes que en bloquegen el tancament d'aquest. -issues.dependency.issue_batch_close_blocked = No es poden tancar tots els problemes marcats perquè el problema #%d encara té dependències obertes -issues.dependency.pr_close_blocked = Heu de tancar tots els problemes que bloquegen aquesta «pull request» abans de fusionar-la. -issues.dependency.blocked_by_short = Depèn de -issues.dependency.remove_header = Eliminar dependència -issues.dependency.issue_remove_text = Això eliminarà la dependència d'aquest problema. Continuar? -issues.dependency.pr_remove_text = Això eliminarà la dependència d'aquesta «pull request». Continuar? -issues.dependency.setting = Activa les dependències per a problemes i «pull requests» -issues.dependency.add_error_same_issue = No podeu fer que un problema depengui d'ell mateix. -issues.dependency.add_error_dep_issue_not_exist = El problema que en depèn no existeix. -issues.dependency.add_error_dep_not_exist = La dependència no existeix. -issues.dependency.add_error_dep_exists = La dependència ja existeix. -issues.dependency.add_error_cannot_create_circular = No podeu crear una dependència amb dos problemes que es bloquegen mútuament. -issues.dependency.add_error_dep_not_same_repo = Els dos problemes han d'ésser al mateix repositori. -issues.review.self.approval = No podeu aprovar la vostra «pull request». -issues.review.self.rejection = No podeu sol·licitar canvis en la vostra «pull request». -issues.review.approve = ha aprovat aquests canvis %s -issues.review.comment = ha revisat %s -issues.review.dismissed = ha rebutjat la revisió de %s %s -issues.review.left_comment = ha fet un comentari -issues.review.content.empty = Heu de fer un comentari indicant els canvis que sol·liciteu. -issues.review.reject = canvis sol·licitats %s -issues.review.add_review_request = ha sol·licitat una revisió de %[1]s %[2]s -issues.review.add_review_requests = ha sol·licitat revisions de %[1]s %[2]s -issues.review.remove_review_request = ha eliminat la sol·licitud de revisió per a %[1]s %[2]s -issues.review.remove_review_requests = ha eliminat les sol·licituds de revisió per a %[1]s %[2]s -issues.review.remove_review_request_self = ha rebutjat la revisió %s -issues.review.add_remove_review_requests = ha demanat la revisió a %[1]s i ha eliminat les peticions de revisió a %[2]s %[3]s -issues.review.pending.tooltip = Actualment, la resta d'usuaris no poden veure aquest comentari. Per a publicar els comentaris pendents, selecciona "%s" -> "%s/%s/%s" més amunt. -issues.review.outdated_description = El contingut ha canviat des que es va publicar aquest comentari -issues.review.option.show_outdated_comments = Mostra els comentaris antics -issues.review.option.hide_outdated_comments = Amaga els comentaris antics -issues.review.show_outdated = Mostrar antics -issues.review.hide_outdated = Amagar antics -issues.review.show_resolved = Mostrar resolts -issues.review.hide_resolved = Amagar resolts -issues.review.resolve_conversation = Resol la conversa -issues.review.un_resolve_conversation = Desfés la resolució de la conversa -issues.review.resolved_by = ha marcat aquesta conversa com a resolta -issues.content_history.delete_from_history = Eliminar de la història -issues.content_history.delete_from_history_confirm = Eliminar de la història? -issues.blocked_by_user = No podeu crear problemes en aquest repositori perquè el seu propietari us ha bloquejat. -comment.blocked_by_user = No podeu fer comentaris perquè el propietari o autor del repositori us ha bloquejat. -issues.summary_card_alt = Targeta de resum d'un problema titulat "%s" al repositori %s -pulls.edit.already_changed = No s'han pogut desar els canvis a la «pull request». Sembla que un altre usuari n'ha modificat el contingut. Si us plau, refresqueu la pàgina i intenteu-ho un altre cop per evitar reescriure els seus canvis -pulls.viewed_files_label = s'ha revisat %[1]d / %[2]d fitxers -pulls.filter_branch = Filtra branca -pulls.showing_only_single_commit = Només es mostren els canvis del commit %[1]s -pulls.showing_specified_commit_range = Només es mostren els canvis entre %[1]s..%[2]s -pulls.filter_changes_by_commit = Filtrar per commit -pulls.tab_files = Fitxers modificats -pulls.title_wip_desc = `Comença el títol amb %s per evitar que la «pull request» sigui fusionada accidentalment.` -pulls.still_in_progress = Encara hi treballeu? -pulls.add_prefix = Afegeix el prefix %s -pulls.ready_for_review = A punt per revisar? -pulls.remove_prefix = Elimina el prefix %s -pulls.data_broken = Aquesta «pull request» és malmesa perquè hi falta la informació de la bifurcació. -pulls.required_status_check_failed = Algunes comprovacions no han sigut satisfactòries. -pulls.required_status_check_missing = Falten algunes comprovacions. -pulls.wrong_commit_id = L'ID del commit ha d'ésser a la branca objectiu -pulls.rebase_merge_pull_request = Canvia de base i avança ràpid -pulls.rebase_merge_commit_pull_request = Fes «rebase» i després el commit de fusió -pulls.squash_merge_pull_request = Fes un «squash commit» -pulls.fast_forward_only_merge_pull_request = Només avança ràpid -pulls.rebase_conflict = La fusió ha fallat: Hi ha hagut un conflicte mentre es feia el «rebase» del commit: %[1]s. Consell: Intenteu una altra estratègia -pulls.unrelated_histories = La fusió ha fallat: El cap (head) de la fusió i la base no tenen una història comú. Consell: Intenteu una altra estratègia -pulls.merge_out_of_date = La fusió ha fallat: La base s'ha actualitzat mentre es generava la fusió. Consell: Torneu-ho a intentar. -pulls.head_out_of_date = La fusió ha fallat: El cap (head) s'ha actualitzat mentre es generava la fusió. Consell: Torneu-ho a intentar. -pulls.has_merged = Fallit: La «pull request» ja s'ha fusionat, no podeu fusionar-la de nou ni canviar la branca objectiu. -pulls.push_rejected = La pujada ha fallat: La pujada (push) s'ha rebutjat. Revisa els ganxos Git d'aquest repositori. -pulls.push_rejected_summary = Missatge complet del rebuig -pulls.push_rejected_no_message = La pujada ha fallat: La pujada (push) s'ha rebutjat però no hi havia un missatge remot. Revisa els ganxos Git d'aquest repositori -pulls.open_unmerged_pull_exists = `No podeu reobrir perquè hi ha una «pull request» pendent (#%d) amb les mateixes característiques.` -pulls.status_checking = Hi ha algunes comprovacions pendents -pulls.status_checks_success = Totes les comprovacions s'han resolt -pulls.status_checks_warning = Algunes comprovacions tenen advertències -pulls.status_checks_failure = Algunes comprovacions han fallat -pulls.status_checks_error = Algunes comprovacions han donat errors -pulls.status_checks_hide_all = Amaga totes les comprovacions -pulls.status_checks_show_all = Mostra totes les comprovacions -pulls.update_branch = Actualitza la branca fusionant -pulls.update_branch_rebase = Actualitza la branca fent «rebase» -pulls.update_branch_success = S'ha actualitzat la branca correctament -pulls.update_not_allowed = No teniu permisos per actualitzar la branca -pulls.outdated_with_base_branch = Aquesta branca està desactualitzada amb la branca base -pulls.close = Tancar «pull request» -pulls.closed_at = `ha tancat aquesta «pull request» %s` -pulls.reopened_at = `ha reobert aquesta «pull request» %s` -pulls.commit_ref_at = `ha fet referència a aquesta «pull request» des d'un commit %s` -pulls.cmd_instruction_hint = Veure les instruccions de la línia de comandes -pulls.cmd_instruction_checkout_title = Canviar branca -pulls.cmd_instruction_checkout_desc = Des del repositori del vostre projecte, canvia a una nova branca i comprova els canvis. -pulls.cmd_instruction_merge_desc = Fusionar els canvis i actualitzar-los a Forgejo. -pulls.cmd_instruction_merge_warning = Atenció: La configuració de "Detecta automàticament la fusió manual" no està activa en aquest repositori, haureu de marcar després aquesta «pull request» com a fusionada manualment. -pulls.clear_merge_message = Neteja el missatge de fusió -pulls.clear_merge_message_hint = Netejar el missatge de fusió només esborrarà el contingut del missatge de commit i mantindrà la firma generada de git com "Co-Authored-By …". -pulls.reopen_failed.head_branch = La «pull request» no es pot reobrir perquè el cap (head) de la branca ja no existeix. -pulls.reopen_failed.base_branch = La «pull request» no es pot reobrir perquè la base de la branca ja no existeix. -pulls.agit_explanation = Creat amb el flux de treball d'AGit. L'AGit permet que els contribuents proposin canvis amb "git push" sense crear una bifurcació o branca. -pulls.editable = Editable -pulls.editable_explanation = Els mantenidors poden editar aquesta «pull request». Podeu editar-la directament. -pulls.auto_merge_button_when_succeed = (Quan les comprovacions siguin satisfactòries) -pulls.auto_merge_when_succeed = Fusiona automàticament quan totes les comprovacions siguin satisfactòries -pulls.auto_merge_newly_scheduled = S'ha programat la «pull request» per a ser fusionada quan totes les comprovacions siguin satisfactòries. -pulls.auto_merge_has_pending_schedule = %[1]s ha programat aquesta «pull request» per a ser fusionada quan totes les comprovacions siguin satisfactòries %[2]s. -pulls.auto_merge_cancel_schedule = Cancel·la la fusió automàtica -pulls.auto_merge_not_scheduled = Aquesta «pull request» no es fusionarà automàticament. -pulls.auto_merge_canceled_schedule = S'ha cancel·lat la fusió automàtica per a aquesta «pull request». -pulls.auto_merge_newly_scheduled_comment = `ha programat aquesta «pull request» per a ser fusionada quan totes les comprovacions siguin satisfactòries. %[1]s` -pulls.auto_merge_canceled_schedule_comment = `ha cancel·lat la fusió automàtica d'aquesta «pull request» quan totes les comprovacions siguin satisfactòries %[1]s` -pulls.delete_after_merge.head_branch.is_default = La branca cap (head) que voleu eliminar és la branca predeterminada i no es pot eliminar. -pulls.delete_after_merge.head_branch.is_protected = La branca cap (head) que voleu eliminar està protegida i no es pot eliminar. -pulls.delete_after_merge.head_branch.insufficient_branch = No teniu permisos per eliminar la branca cap (head). -pulls.delete.title = Eliminar aquesta «pull request»? -pulls.delete.text = Realment voleu eliminar aquesta «pull request»? (Això eliminarà permanentment tot el contingut. Considereu millor tancar-la, si la voleu arxivar) -pulls.recently_pushed_new_branches = Heu pujat a la branca %[1]s %[2]s -comments.edit.already_changed = No s'han pogut desar els canvis al comentari. Sembla que un altre usuari n'ha modificat el contingut. Si us plau, refresqueu la pàgina i intenteu-ho un altre cop per evitar reescriure els seus canvis -milestones.new = Nova fita -milestones.closed = Tancat %s -milestones.update_ago = Actualitzat %s -milestones.no_due_date = Sense data de venciment -milestones.new_subheader = Les fites us poden ajudar a organitzar els problemes i fer-ne el seguiment. -milestones.completeness = %d%% Completat -milestones.create = Crear fita -milestones.due_date = Data de venciment (opcional) -milestones.invalid_due_date_format = El format de la data de venciment ha de ser "aaaa-mm-dd". -milestones.create_success = S'ha creat la fita "%s". -milestones.edit = Editar fita -milestones.edit_subheader = Les fites organitzen els problemes i en fan el seguiment. -milestones.modify = Actualitzar fita -milestones.edit_success = S'ha actualitzat la fita "%s". -milestones.deletion = Eliminar fita -milestones.deletion_desc = Eliminar una fita n'esborra tots els problemes. Continuar? -milestones.deletion_success = S'ha eliminat la fita. -milestones.filter_sort.name = Nom -milestones.filter_sort.earliest_due_data = A prop de la data de venciment -milestones.filter_sort.latest_due_date = Lluny de la data de venciment -milestones.filter_sort.least_complete = Menys completa -milestones.filter_sort.most_complete = Més completa +diff.whitespace_ignore_at_eol = Ignorar els canvis en els espais en blanc al final de la línia +diff.stats_desc = %d fitxers modificats amb %d afegits i %d supressions diff.git-notes = Notes diff.git-notes.add = Afegir nota diff.git-notes.remove-header = Eliminar nota @@ -2082,8 +1678,6 @@ diff.show_unified_view = Vista única diff.whitespace_show_everything = Mostrar tots els canvis diff.whitespace_ignore_all_whitespace = Ignora els espais en blanc quan es comparin línies diff.whitespace_ignore_amount_changes = Ignora els canvis en el nombre d'espais en blanc -diff.whitespace_ignore_at_eol = Ignorar els canvis en els espais en blanc al final de la línia -diff.stats_desc = %d fitxers modificats amb %d afegits i %d supressions diff.stats_desc_file = %d canvis: %d afegits i %d supressions diff.bin_not_shown = No es mostra el fitxer binari. diff.view_file = Visualitza el fitxer @@ -2153,7 +1747,366 @@ release.tag_name_already_exist = Ja hi ha una publicació amb aquest nom d'etiqu release.tag_name_invalid = El nom d'etiqueta no és vàlid. release.tag_name_protected = El nom d'etiqueta és protegit. release.downloads = Baixades - +fork_repo = Bifurca el repositori +fork_from = Bifurcar des de +create_new_repo_command = Crea un nou repositori a la línia de comandes +push_exist_repo = Puja un repositori ja existent des de la línia de comandes +empty_message = Aquest repositori està buit. +broken_message = Les dades Git d'aquest repositori no es poden llegir. Contacta amb l'administrador d'aquesta instància o suprimeix aquest repositori. +code.desc = Accedeix al codi font, els fitxers, els commits i les branques. +clear_ref = `Esborra la referència actual` +filter_branch_and_tag = Filtra per branca o etiqueta +find_tag = Cerca etiqueta +delete_preexisting_success = S'ha suprimit els fitxers no-adoptats a %s +author_search_tooltip = Es mostra un màxim de 30 usuaris +summary_card_alt = Resum del repositori %s +tree_path_not_found.commit = El camí %[1]s no existeix al commit %[2]s +tree_path_not_found.branch = El camí %[1]s no existeix a la branca %[2]s +tree_path_not_found.tag = El camí %[1]s no existeix a l'etiqueta %[2]s +transfer.accept = Accepta la transferència +transfer.accept_desc = Transfereix a "%s" +transfer.reject = Denega la transferència +transfer.reject_desc = Cancel·la la transferència a "%s" +transfer.no_permission_to_accept = No teniu permisos per acceptar aquesta transferència. +transfer.no_permission_to_reject = No teniu permisos per denegar aquesta transferència. +template.items = Ítems de la plantilla +template.git_content = Contingut Git (Branca predeterminada) +template.git_hooks = Ganxos Git +migrate_repo = Migra el repositori +migrate.repo_desc_helper = Deixa-ho buit per importar la descripció existent +migrate.clone_address = Migra / Clona des d'una URL +migrate.clone_address_desc = L'URL d'HTTP(S) o de Git "clone" d'un repositori ja existent +migrate.permission_denied = No podeu importar repositoris locals. +migrate.invalid_local_path = El camí local és invàlid. No existeix o no és un directori. +migrate.invalid_lfs_endpoint = El punt final LFS no és vàlid. +migrate.failed = La migració ha fallat: %v +migrate.migrate_items_options = Cal un testimoni d'accés per migrar els ítems addicionals +migrated_from = Migrat des de %[2]s +migrated_from_fake = Migrat des de %[1]s +migrate.migrate = Migra des de %s +migrate.migrating = S'està migrant des de %s … +migrate.migrating_failed = La migració des de %s ha fallat. +migrate.migrating_failed.error = No s'ha pogut migrar: %s +migrate.migrating_failed_no_addr = La migració ha fallat. +mirror_from = mirall de +forked_from = bifurcat des de +fork_from_self = No podeu bifurcar un repositori que us pertany. +fork_guest_user = Inicia sessió per bifurcar aquest repositori. +watch_guest_user = Inicia sessió per vigilar aquest repositori. +star_guest_user = Inicia sessió per destacar aquest repositori. +subscribe.issue.guest.tooltip = Inicia sessió per subscriure't a aquest problema. +subscribe.pull.guest.tooltip = Inicia sessió per subscriure't a aquesta «pull request». +more_operations = Més operacions +no_desc = Sense descripció +quick_guide = Guia ràpida +clone_this_repo = Clona aquest repositori +cite_this_repo = Cita aquest repositori +repo_gitignore_helper_desc = Escolliu de quins fitxers no s'ha de fer seguiment d'una llista de plantilles per llenguatges comuns. Els artefactes típics generats per les eines de construcció de cada llenguatge estan incloses al .gitignore de manera predeterminada. +readme_helper_desc = Aquí podeu escriure una descripció completa del vostre projecte. +mirror_interval = Interval de rèplica (les unitats de temps vàlides són "h", "m", "s"). 0 per desactivar la sincronització periòdica. (Interval mínim: %s) +mirror_use_ssh.helper = Si seleccioneu aquesta opció, Forgejo replicarà el repositori mitjançant Git per SSH i us crearà una parella de claus. Heu d'assegurar-vos que la clau pública generada estigui autoritzada per publicar al repositori de destí. No podreu fer servir autenticació basada en contrasenyes. +migrate.github_token_desc = Podeu posar un o més testimonis separats amb comes per fer que la migració sigui més ràpida, evitant així el límit de taxa de l'API de GitHub. ATENCIÓ: Abusar aquesta funció pot anar en contra de la política de servei del proveïdor i pot comportar el bloqueig del/s vostre/s compte/s. +migrate.clone_local_path = o el camí a un servidor local +migrate.permission_denied_blocked = No podeu importar des d'amfitrions rebutjats, si us plau, demaneu a l'administrador que comprovi les configuracions ALLOWED_DOMAINS/ALLOW_LOCALNETWORKS/BLOCKED_DOMAINS. +file.title = %s a %s +file_view_rendered = Veure renderitzat +file_view_raw = Veure cru +editor.commit_directly_to_this_branch = Feu un commit directament a la branca %[1]s. +editor.new_branch_name = Anomena la nova branca per a aquest commit +editor.file_already_exists = Ja hi ha un fitxer anomenat "%S" en aquest repositori. +editor.commit_id_not_matching = El fitxer ha canviat mentre l'editaves. Fes un commit a una nova branca i llavors fusiona-la. +editor.push_out_of_date = La pujada està desactualitzada. +editor.commit_empty_file_header = Fes un commit d'un fitxer buit +editor.user_no_push_to_branch = L'usuari no pot pujar a la branca +editor.revert = Retorna %s a: +editor.commit_email = Adreça electrònica del commit +commits.no_commits = No hi ha commits en comú. "%s" i "%s" tenen històries completament diferents. +commits.view_path = Veure en aquest punt de la història +commits.view_single_diff = Veure els canvis fets en aquest fitxer introduïts en aquest commit +commit.revert-header = Retorna: %s +commit.revert-content = Selecciona una branca per retornar-hi: +projects.type.basic_kanban = Kanban bàsic +projects.column.set_default_desc = Estableix aquesta columna com a predeterminada per als problemes i pulls sense categoria +projects.card_type.desc = Previsualització de targeta +issues.filter_assignees = Filtrar assignats +issues.filter_reviewers = Filtrar revisor +issues.new.open_projects = Projectes oberts +issues.new.open_milestone = Fites obertes +issues.new.clear_assignees = Esborra assignats +issues.new.no_assignees = Sense assignats +issues.new.assign_to_me = Assigna-m'ho +issues.new.no_reviewers = Sense revisors +issues.edit.already_changed = No s'han pogut desar els canvis al problema. Sembla que un altre usuari n'ha modificat el contingut. Si us plau, refresqueu la pàgina i intenteu-ho un altre cop per evitar reescriure els seus canvis +issues.choose.get_started = Començar +issues.choose.blank_about = Crea un problema a partir de la plantilla per defecte. +issues.choose.invalid_config = La configuració del problema té errors: +issues.label_templates.use = Utilitza una etiqueta predeterminada +issues.label_templates.fail_to_load_file = No s'ha pogut carregar el fitxer d'etiqueta predeterminada "%s": %v +issues.add_label = s'ha afegit l'etiqueta %s %s +issues.add_labels = s'han afegit les etiquetes %s %s +issues.remove_label = s'ha esborrat l'etiqueta %s %s +issues.remove_labels = s'han esborrat les etiquetes %s %s +issues.add_remove_labels = s'han afegit %s etiquetes i esborrat %s %s +issues.add_milestone_at = `s'ha afegit a la fita %s %s` +issues.add_project_at = `s'ha afegit al projecte %s %s` +issues.change_milestone_at = `s'ha modificat la fita des de %s a %s %s` +issues.change_project_at = `s'ha modificat el projecte des de %s a %s %s` +issues.remove_milestone_at = `s'ha eliminat de la fita %s %s` +issues.remove_project_at = `s'ha eliminat del projecte %s %s` +issues.self_assign_at = `s'ha auto-assignat aquest %s` +issues.add_assignee_at = `va ser assignat per %s %s` +issues.remove_assignee_at = `va ser desassignat per %s %s` +issues.remove_self_assignment = `s'ha eliminat la seva tasca %s` +issues.change_title_at = `s'ha canviat el títol de %s a %s %s` +issues.change_ref_at = `s'ha canviat la referència de %s a %s %s` +issues.remove_ref_at = `s'ha eliminat la referència %s %s` +issues.add_ref_at = `s'ha afegit la referència %s %s` +issues.delete_branch_at = `s'ha eliminat la branca %s %s` +issues.filter_label_no_select = Totes les etiquetes +issues.filter_label_select_no_label = Sense etiqueta +issues.filter_milestone_none = Sense fites +issues.filter_milestone_open = Fites obertes +issues.filter_milestone_closed = Fites tancades +issues.filter_project_all = Tots els projectes +issues.filter_project_none = Sense projecte +issues.filter_assginee_no_select = Tots els assignats +issues.filter_assginee_no_assignee = Sense assignat +issues.filter_poster_no_select = Tots els autors +issues.filter_type.all_pull_requests = Totes les «pull requests» +issues.filter_type.all_issues = Tots els problemes +issues.filter_type.assigned_to_you = Tasques que tens assignades +issues.filter_type.created_by_you = Creat per tu +issues.filter_type.mentioning_you = Que et mencionen +issues.filter_type.review_requested = S'ha demanat la revisió +issues.filter_type.reviewed_by_you = Revisats per tu +issues.filter_sort.relevance = Rellevància +issues.filter_sort.recentupdate = Actualitzats recentment +issues.filter_sort.leastupdate = Actualitzats menys recentment +issues.filter_sort.leastcomment = Menys comentats +issues.filter_sort.nearduedate = A prop de la data de venciment +issues.filter_sort.farduedate = Lluny de la data de venciment +issues.filter_sort.moststars = Més destacats +issues.filter_sort.feweststars = Menys destacats +issues.action_assignee_no_select = Sense assignat +pulls.merged_by = per %[3]s s'ha fusionat %[1]s +issues.closed_by = per %[3]s s'ha tancat %[1]s +issues.context.menu = Menú de comentari +issues.comment_pull_merged_at = s'ha fusionat el commit %[1]s a %[2]s %[3]s +issues.comment_manually_pull_merged_at = s'ha fusionat el commit %[1]s manualment a %[2]s %[3]s +issues.is_stale = Hi ha hagut canvis a aquest PR des de la revisió +issues.archived_label_description = (Arxivats) %s +editor.signoff_desc = Afegeix un "Signat per" seguit de l'autor al final del missatge del commit. +issues.role.contributor_helper = Aquest usuari ja ha fet commits en aquest repositori. +issues.label.filter_sort.reverse_alphabetically = Capgira-ho alfabèticament +issues.label.filter_sort.by_size = Mida més petita +issues.label.filter_sort.reverse_by_size = Mida més gran +issues.num_participants_one = %d participant +issues.num_participants_few = %d participants +issues.tracker = Rastrejador de temps +issues.start_tracking_short = Inicia el temporitzador +issues.start_tracking = Inicia el rastreig de temps +issues.start_tracking_history = `s'ha començat %s` +issues.tracker_auto_close = El temporitzador s'aturarà automàticament quan el problema s'hagi resolt +issues.tracking_already_started = `Ja has començat a rastrejar el temps en un altre problema!` +issues.stop_tracking = Atura el temporitzador +issues.stop_tracking_history = `s'ha aturat %s` +issues.cancel_tracking_history = `s'ha cancel·lat el rastreig de temps %s` +issues.add_time = Afegeix el temps manualment +issues.del_time = Elimina aquest registre de temps +issues.add_time_short = Afegeix temps +issues.add_time_history = `s'ha afegit temps passat %s` +issues.del_time_history = `s'ha eliminat temps passat %s` +issues.add_time_sum_to_small = No s'ha introduït temps. +issues.time_spent_from_all_authors = `Temps total: %s` +issues.due_date = Data de venciment +issues.push_commit_1 = s'ha afegit %d commit %s +issues.push_commits_n = s'ha afegit %d commits %s +issues.force_push_codes = `s'ha forçat la pujada %[1]s des de %[2]s %[8]s a %[4]s %[9]s %[6]s` +issues.due_date_not_set = No s'ha establert una data de venciment. +issues.due_date_added = s'ha afegit la data de venciment %s %s +issues.due_date_modified = s'ha modificat la data de venciment de %[2]s a %[1]s %[3]s +issues.due_date_remove = s'ha eliminat la data de venciment %s %s +issues.due_date_invalid = La data de venciment no és vàlida o és fora de rang. Si us plau, useu el format "aaaa-mm-dd". +issues.dependency.issue_no_dependencies = No s'han establert dependències. +issues.dependency.no_permission_1 = No teniu permisos per llegir %d dependència +issues.dependency.no_permission_n = No teniu permisos per llegir %d dependències +issues.dependency.no_permission.can_remove = No teniu permisos per llegir aquesta dependència, però la podeu eliminar +issues.dependency.add = Afegir dependència… +issues.dependency.remove_info = Eliminar aquesta dependència +issues.dependency.added_dependency = `s'ha afegit una nova dependència %s` +issues.dependency.removed_dependency = `s'ha eliminat una dependència %s` +issues.dependency.pr_closing_blockedby = No es pot tancar aquesta «pull request» perquè té els següents problemes +issues.dependency.issue_closing_blockedby = No es pot tancar aquest problema perquè té els següents problemes +issues.dependency.issue_close_blocks = Aquest problema bloqueja el tancament d'aquests altres problemes +issues.dependency.pr_close_blocks = Aquesta «pull request» bloqueja el tancament d'aquests problemes +issues.dependency.issue_close_blocked = Heu de tancar tots els problemes que en bloquegen el tancament d'aquest. +issues.dependency.issue_batch_close_blocked = No es poden tancar tots els problemes marcats perquè el problema #%d encara té dependències obertes +issues.dependency.pr_close_blocked = Heu de tancar tots els problemes que bloquegen aquesta «pull request» abans de fusionar-la. +issues.dependency.blocked_by_short = Depèn de +issues.dependency.pr_remove_text = Això eliminarà la dependència d'aquesta «pull request». Continuar? +issues.dependency.setting = Activa les dependències per a problemes i «pull requests» +issues.dependency.add_error_same_issue = No podeu fer que un problema depengui d'ell mateix. +issues.dependency.add_error_dep_issue_not_exist = El problema que en depèn no existeix. +issues.dependency.add_error_dep_not_exist = La dependència no existeix. +issues.dependency.add_error_dep_exists = La dependència ja existeix. +issues.dependency.add_error_cannot_create_circular = No podeu crear una dependència amb dos problemes que es bloquegen mútuament. +issues.dependency.add_error_dep_not_same_repo = Els dos problemes han d'ésser al mateix repositori. +issues.review.self.approval = No podeu aprovar la vostra «pull request». +issues.review.self.rejection = No podeu sol·licitar canvis en la vostra «pull request». +issues.review.approve = ha aprovat aquests canvis %s +issues.review.comment = ha revisat %s +issues.review.dismissed = ha rebutjat la revisió de %s %s +issues.review.left_comment = ha fet un comentari +issues.review.content.empty = Heu de fer un comentari indicant els canvis que sol·liciteu. +issues.review.reject = canvis sol·licitats %s +issues.review.add_review_request = ha sol·licitat una revisió de %[1]s %[2]s +issues.review.add_review_requests = ha sol·licitat revisions de %[1]s %[2]s +issues.review.remove_review_request = ha eliminat la sol·licitud de revisió per a %[1]s %[2]s +issues.review.remove_review_requests = ha eliminat les sol·licituds de revisió per a %[1]s %[2]s +issues.review.option.show_outdated_comments = Mostra els comentaris antics +issues.review.option.hide_outdated_comments = Amaga els comentaris antics +issues.review.show_outdated = Mostrar antics +issues.review.hide_outdated = Amagar antics +issues.review.show_resolved = Mostrar resolts +issues.review.hide_resolved = Amagar resolts +issues.review.resolve_conversation = Resol la conversa +issues.review.un_resolve_conversation = Desfés la resolució de la conversa +issues.review.resolved_by = ha marcat aquesta conversa com a resolta +issues.content_history.delete_from_history = Eliminar de la història +issues.content_history.delete_from_history_confirm = Eliminar de la història? +issues.blocked_by_user = No podeu crear problemes en aquest repositori perquè el seu propietari us ha bloquejat. +comment.blocked_by_user = No podeu fer comentaris perquè el propietari o autor del repositori us ha bloquejat. +pulls.filter_branch = Filtra branca +pulls.showing_only_single_commit = Només es mostren els canvis del commit %[1]s +pulls.showing_specified_commit_range = Només es mostren els canvis entre %[1]s..%[2]s +pulls.tab_files = Fitxers modificats +pulls.title_wip_desc = `Comença el títol amb %s per evitar que la «pull request» sigui fusionada accidentalment.` +pulls.add_prefix = Afegeix el prefix %s +pulls.ready_for_review = A punt per revisar? +pulls.remove_prefix = Elimina el prefix %s +pulls.required_status_check_failed = Algunes comprovacions no han sigut satisfactòries. +pulls.required_status_check_missing = Falten algunes comprovacions. +pulls.wrong_commit_id = L'ID del commit ha d'ésser a la branca objectiu +blame_prior = Veure el «blame» anterior a aquest canvi +blame.ignore_revs = S'ignoren les revisions a .git-blame-ignore-revs. Fes clic aquí per veure-les. +blame.ignore_revs.failed = No s'han pogut ignorar les revisions a .git-blame-ignore-revs. +commit_graph = Gràfic de commits +commit.contained_in = Aquest commit és a: +no_eol.text = Sense EOL +editor.commit_signed_changes = Fes un commit signat dels canvis +editor.commit_changes = Fes un commit dels canvis +editor.add_tmpl.filename = nom del fitxer +editor.patch = Aplicar pedaç +editor.patching = Aplicant el pedaç a: +editor.cherry_pick = Triar a dit %s a: +commit.cherry-pick = Triar a dit +commit.cherry-pick-header = Triar a dit: %s +commit.cherry-pick-content = Seleccioneu una branca per triar-hi: +issues.filter_sort.mostforks = Més bifurcacions +issues.filter_sort.fewestforks = Menys bifurcacions +issues.label_exclusive_desc = Doneu nom a l'àmbit/ítem de l'etiqueta per fer-la mútuament exclusiva amb altres àmbits/ etiquetes. +issues.label_exclusive_warning = Qualsevol etiqueta amb àmbits conflictius s'esborrarà quan s'editin les etiquetes d'un problema o «pull request». +issues.dependency.pr_no_dependencies = No s'ha establert cap dependència. +issues.dependency.remove_header = Eliminar dependència +issues.dependency.issue_remove_text = Això eliminarà la dependència d'aquest problema. Continuar? +issues.review.remove_review_request_self = ha rebutjat la revisió %s +issues.review.add_remove_review_requests = ha demanat la revisió a %[1]s i ha eliminat les peticions de revisió a %[2]s %[3]s +issues.review.pending.tooltip = Actualment, la resta d'usuaris no poden veure aquest comentari. Per a publicar els comentaris pendents, selecciona "%s" -> "%s/%s/%s" més amunt. +issues.review.outdated_description = El contingut ha canviat des que es va publicar aquest comentari +issues.summary_card_alt = Targeta de resum d'un problema titulat "%s" al repositori %s +pulls.edit.already_changed = No s'han pogut desar els canvis a la «pull request». Sembla que un altre usuari n'ha modificat el contingut. Si us plau, refresqueu la pàgina i intenteu-ho un altre cop per evitar reescriure els seus canvis +pulls.viewed_files_label = s'ha revisat %[1]d / %[2]d fitxers +pulls.still_in_progress = Encara hi treballeu? +pulls.data_broken = Aquesta «pull request» és malmesa perquè hi falta la informació de la bifurcació. +pulls.rebase_merge_commit_pull_request = Fes «rebase» i després el commit de fusió +pulls.squash_merge_pull_request = Fes un «squash commit» +pulls.rebase_conflict = La fusió ha fallat: Hi ha hagut un conflicte mentre es feia el «rebase» del commit: %[1]s. Consell: Intenteu una altra estratègia +pulls.unrelated_histories = La fusió ha fallat: El cap (head) de la fusió i la base no tenen una història comú. Consell: Intenteu una altra estratègia +pulls.merge_out_of_date = La fusió ha fallat: La base s'ha actualitzat mentre es generava la fusió. Consell: Torneu-ho a intentar. +pulls.head_out_of_date = La fusió ha fallat: El cap (head) s'ha actualitzat mentre es generava la fusió. Consell: Torneu-ho a intentar. +pulls.has_merged = Fallit: La «pull request» ja s'ha fusionat, no podeu fusionar-la de nou ni canviar la branca objectiu. +pulls.push_rejected = La pujada ha fallat: La pujada (push) s'ha rebutjat. Revisa els ganxos Git d'aquest repositori. +pulls.push_rejected_summary = Missatge complet del rebuig +pulls.push_rejected_no_message = La pujada ha fallat: La pujada (push) s'ha rebutjat però no hi havia un missatge remot. Revisa els ganxos Git d'aquest repositori +pulls.open_unmerged_pull_exists = `No podeu reobrir perquè hi ha una «pull request» pendent (#%d) amb les mateixes característiques.` +pulls.status_checking = Hi ha algunes comprovacions pendents +pulls.status_checks_success = Totes les comprovacions s'han resolt +pulls.status_checks_warning = Algunes comprovacions tenen advertències +pulls.status_checks_failure = Algunes comprovacions han fallat +pulls.status_checks_error = Algunes comprovacions han donat errors +pulls.status_checks_hide_all = Amaga totes les comprovacions +pulls.status_checks_show_all = Mostra totes les comprovacions +pulls.update_branch = Actualitza la branca fusionant +pulls.update_branch_rebase = Actualitza la branca fent «rebase» +pulls.update_branch_success = S'ha actualitzat la branca correctament +pulls.update_not_allowed = No teniu permisos per actualitzar la branca +pulls.outdated_with_base_branch = Aquesta branca està desactualitzada amb la branca base +pulls.close = Tancar «pull request» +pulls.closed_at = `ha tancat aquesta «pull request» %s` +pulls.reopened_at = `ha reobert aquesta «pull request» %s` +pulls.commit_ref_at = `ha fet referència a aquesta «pull request» des d'un commit %s` +pulls.cmd_instruction_hint = Veure les instruccions de la línia de comandes +pulls.cmd_instruction_merge_desc = Fusionar els canvis i actualitzar-los a Forgejo. +pulls.cmd_instruction_merge_warning = Atenció: La configuració de "Detecta automàticament la fusió manual" no està activa en aquest repositori, haureu de marcar després aquesta «pull request» com a fusionada manualment. +pulls.clear_merge_message = Neteja el missatge de fusió +pulls.clear_merge_message_hint = Netejar el missatge de fusió només esborrarà el contingut del missatge de commit i mantindrà la firma generada de git com "Co-Authored-By …". +pulls.reopen_failed.head_branch = La «pull request» no es pot reobrir perquè el cap (head) de la branca ja no existeix. +pulls.reopen_failed.base_branch = La «pull request» no es pot reobrir perquè la base de la branca ja no existeix. +pulls.rebase_merge_pull_request = Canvia de base i avança ràpid +pulls.fast_forward_only_merge_pull_request = Només avança ràpid +pulls.cmd_instruction_checkout_title = Canviar branca +pulls.filter_changes_by_commit = Filtrar per commit +pulls.cmd_instruction_checkout_desc = Des del repositori del vostre projecte, canvia a una nova branca i comprova els canvis. +pulls.agit_explanation = Creat amb el flux de treball d'AGit. L'AGit permet que els contribuents proposin canvis amb "git push" sense crear una bifurcació o branca. +pulls.editable = Editable +pulls.editable_explanation = Els mantenidors poden editar aquesta «pull request». Podeu editar-la directament. +pulls.auto_merge_button_when_succeed = (Quan les comprovacions siguin satisfactòries) +pulls.auto_merge_when_succeed = Fusiona automàticament quan totes les comprovacions siguin satisfactòries +pulls.auto_merge_newly_scheduled = S'ha programat la «pull request» per a ser fusionada quan totes les comprovacions siguin satisfactòries. +pulls.auto_merge_has_pending_schedule = %[1]s ha programat aquesta «pull request» per a ser fusionada quan totes les comprovacions siguin satisfactòries %[2]s. +pulls.auto_merge_cancel_schedule = Cancel·la la fusió automàtica +pulls.auto_merge_not_scheduled = Aquesta «pull request» no es fusionarà automàticament. +pulls.auto_merge_canceled_schedule = S'ha cancel·lat la fusió automàtica per a aquesta «pull request». +pulls.auto_merge_newly_scheduled_comment = `ha programat aquesta «pull request» per a ser fusionada quan totes les comprovacions siguin satisfactòries. %[1]s` +pulls.auto_merge_canceled_schedule_comment = `ha cancel·lat la fusió automàtica d'aquesta «pull request» quan totes les comprovacions siguin satisfactòries %[1]s` +pulls.delete_after_merge.head_branch.is_default = La branca cap (head) que voleu eliminar és la branca predeterminada i no es pot eliminar. +pulls.delete_after_merge.head_branch.is_protected = La branca cap (head) que voleu eliminar està protegida i no es pot eliminar. +pulls.delete_after_merge.head_branch.insufficient_branch = No teniu permisos per eliminar la branca cap (head). +pulls.delete.title = Eliminar aquesta «pull request»? +pulls.delete.text = Realment voleu eliminar aquesta «pull request»? (Això eliminarà permanentment tot el contingut. Considereu millor tancar-la, si la voleu arxivar) +pulls.recently_pushed_new_branches = Heu pujat a la branca %[1]s %[2]s +comments.edit.already_changed = No s'han pogut desar els canvis al comentari. Sembla que un altre usuari n'ha modificat el contingut. Si us plau, refresqueu la pàgina i intenteu-ho un altre cop per evitar reescriure els seus canvis +milestones.new = Nova fita +milestones.closed = Tancat %s +milestones.update_ago = Actualitzat %s +milestones.no_due_date = Sense data de venciment +milestones.new_subheader = Les fites us poden ajudar a organitzar els problemes i fer-ne el seguiment. +milestones.completeness = %d%% Completat +milestones.create = Crear fita +milestones.due_date = Data de venciment (opcional) +milestones.invalid_due_date_format = El format de la data de venciment ha de ser "aaaa-mm-dd". +milestones.create_success = S'ha creat la fita "%s". +milestones.edit = Editar fita +milestones.edit_subheader = Les fites organitzen els problemes i en fan el seguiment. +milestones.modify = Actualitzar fita +milestones.edit_success = S'ha actualitzat la fita "%s". +milestones.deletion = Eliminar fita +milestones.deletion_desc = Eliminar una fita n'esborra tots els problemes. Continuar? +milestones.deletion_success = S'ha eliminat la fita. +milestones.filter_sort.name = Nom +milestones.filter_sort.earliest_due_data = A prop de la data de venciment +milestones.filter_sort.latest_due_date = Lluny de la data de venciment +milestones.filter_sort.least_complete = Menys completa +milestones.filter_sort.most_complete = Més completa +default_branch_helper = La branca per defecte és la branca base pels pull requests i els commits. +file_follow = Seguir l'enllaç simbòlic +escape_control_characters =Escapar +commits.signed_by_untrusted_user_unmatched = Signat per un usuari no fiable que no coincideix amb l'autor del commit +projects.new_subheader = Coordineu, feu seguiment, i actualitzeu el vostre treball en un sol lloc, per tal que els projectes es mantinguin transparents i dins del calendari previst. +projects.type.bug_triage = Triatge d'errors +projects.column.set_default = Definir com a predeterminada +projects.column.assigned_to = Assignat a +issues.filter_sort.mostcomment = Les més comentades signing.wont_sign.parentsigned = El commit no se signarà perquè el commit pare no està signat. signing.wont_sign.headsigned = La fusió no se signarà perquè el commit cap (head) no està signat. wiki.create_first_page = Crea la primera pàgina @@ -2261,14 +2214,12 @@ settings.add_webhook.invalid_path = El camí no pot contenir ".", ".." o una cad settings.webhook_deletion = Eliminar webhook settings.webhook_deletion_desc = Eliminar un webhook n'elimina la configuració i la història de lliurament. Continuar? settings.webhook_deletion_success = S'ha eliminat el webhook. - unescape_control_characters = Des-escapar view_git_blame = Veure git blame vendored = Proveïdor editor.propose_file_change = Proposa canvis en el fitxer editor.invalid_commit_mail = Adreça de correu electrònic incorrecta per crear un commit. editor.file_changed_while_editing = El contingut del fitxer ha canviat des que el vau obrir. Feu clic aquí per veure'l o feu commit dels canvis un altre cop per sobreescriure'l. -editor.commit_empty_file_text = L'arxiu que s'està a punt de cometre és buit. Procedir? editor.push_rejected_no_message = El servidor ha rebutjat el canvi sense més detalls. Si us plau, reviseu els ganxos de Git. editor.push_rejected = El servidor ha rebutjat el canvi. Si us plau, reviseu els ganxos de Git. editor.cannot_commit_to_protected_branch = No es pot fer commit a la branca protegida "%s". @@ -2302,7 +2253,6 @@ pulls.reject_count_1 = %d sol·licitud de canvi pulls.reject_count_n = %s sol·licituds de canvi pulls.waiting_count_1 = %d revisió en espera pulls.waiting_count_n = %d revisions en espera -pulls.no_merge_not_ready = Aquesta «pull request» no està llesta per a ser fusionada, comprova l'estat de revisió i les comprovacions d'estat. pulls.merge_pull_request = Crear un commit de fusió pulls.merge_commit_id = L'ID del commit de fusió pulls.require_signed_wont_sign = La branca requereix commits signats, però aquesta fusió no estarà signada @@ -2323,7 +2273,6 @@ wiki.back_to_wiki = Tornar a la pàgina de la wiki wiki.reserved_page = El nom de pàgina wiki "%s" està reservat. wiki.last_updated = Actualitzat per última vegada %s wiki.page_name_desc = Introduïu un nom per aquesta pàgina de la Wiki. Alguns noms especials són: "Home", "_Sidebar" i "_Footer". -activity.navbar.pulse = Pols activity.navbar.recent_commits = Commits recents activity.published_prerelease_label = Pre-publicació activity.git_stats_commit_1 = %d commit @@ -2399,14 +2348,6 @@ settings.event_pull_request_review_request = se sol·liciti revisió settings.event_pull_request_review_request_desc = se sol·licitin revisions a la «pull request» o se n'elimini la sol·licitud. settings.event_pull_request_approvals = s'accepti una «pull request» settings.event_pull_request_merge = es fusioni una «pull request» -settings.event_header_action = Esdeveniments d'execució d'accions -settings.event_action_failure = Fallada -settings.event_action_failure_desc = L'Acció ha fallat. -settings.event_action_recover_desc = La execució de l'acció ha finalitzat amb èxit després de que la última execució en el mateix flux fallés. -settings.event_action_success_desc = La execució de l'acció ha tingut èxit. -settings.event_package_desc = Paquet creat o eliminat en un repositori. -settings.branch_filter = Filtre de branca -settings.branch_filter_desc = Llista blanca de la branca per als esdeveniments de pujada i creació/eliminació de branques, especificada com un patró «glob». Si és buida o *, s'enregistraran els esdeveniments de totes les branques. Vegeu la documentació %[2]s per a la sintaxi. Per exemple: master, {master,release*}. settings.authorization_header = Capçalera d'Autorització settings.authorization_header_desc = S'inclourà com a capçalera d'autorització per a les «requests» quan hi sigui. Exemples: %s. settings.active_helper = La informació sobre els esdeveniments disparats s'enviarà a aquesta URL webhook. @@ -2418,9 +2359,6 @@ settings.recent_deliveries = Lliuraments recents settings.hook_type = Tipus de ganxo settings.add_web_hook_desc = Integrar %s al vostre repositori. settings.graphql_url = URL GraphQL -settings.web_hook_name_feishu = Feishu / Lark Suite -settings.web_hook_name_larksuite_only = Lark Suite -settings.web_hook_name_wechatwork = WeCom (Wechat Work) settings.packagist_username = Nom d'usuari Packagist settings.packagist_api_token = Testimoni API settings.packagist_package_url = URL del paquet Packagist @@ -2453,7 +2391,6 @@ settings.protect_enable_merge = Activar fusió settings.protect_enable_merge_desc = Tothom amb accés d'escriptura podrà fusionar «pull requests» a la branca. settings.protect_whitelist_committers = Pujada restringida settings.protect_whitelist_committers_desc = Només els usuaris i equips indicats a la llista blanca podran pujar (push) a la branca (però no «force push»). -settings.protect_whitelist_deploy_keys = Posar les claus d'implementació amb accés d'escriptura per pujar (push) a la llista blanca. settings.protect_whitelist_users = Llista blanca d'usuaris per pujar settings.protect_whitelist_teams = Llista blanca d'equips per pujar settings.protect_merge_whitelist_committers = Activar llista blanca de fusió @@ -2465,13 +2402,9 @@ settings.protect_status_check_patterns_desc = Introdueix patrons per especificar settings.protect_check_status_contexts_desc = Requerir comprovació d'estat abans de fusionar. Quan s'activa, els commits s'han de pujar primer en una altra branca, i llavors es podran fusionar o pujar directament a la branca que indica aquesta regla, un cop hagin passat les comprovacions. Si no s'indica cap context, l'últim commit ha de ser satisfactori, sigui quin sigui el context. settings.protect_check_status_contexts_list = Comprovacions d'estat de la darrera setmana d'aquest repositori settings.protect_invalid_status_check_pattern = Patró de comprovació invàlid: "%s". -settings.protect_no_valid_status_check_patterns = Sense patrons de comprovació d'estat vàlids. -settings.protect_required_approvals = Aprovacions necessàries -settings.protect_required_approvals_desc = Permet fusionar només «pull requests» amb prou revisions positives. -settings.protect_approvals_whitelist_enabled = Limita les aprovacions als usuaris o equips de la llista blanca -settings.protect_approvals_whitelist_enabled_desc = Només comptaran cap al total d'aprovacions necessàries les revisions d'usuaris i equips de la llista blanca. Si la llista és buida, en comptaran les revisions de tothom amb accés d'escriptura. -settings.protect_approvals_whitelist_users = Revisors en la llista blanca -settings.protect_approvals_whitelist_teams = Equips en la llista blanca per revisions +settings.event_header_action = Esdeveniments d'execució d'accions +settings.event_action_failure = Fallada +settings.event_action_failure_desc = L'Acció ha fallat. settings.dismiss_stale_approvals = Ignora les aprovacions estancades settings.dismiss_stale_approvals_desc = Quan es facin commits nous a la branca que canviïn el contingut de la «pull request», les aprovacions antigues es descartaran. settings.ignore_stale_approvals = Ignorar aprovacions estancades @@ -2490,21 +2423,8 @@ settings.remove_protected_branch_failed = No s'ha pogut eliminar la regla de pro settings.protected_branch_deletion = Protecció d'eliminació de branca settings.protected_branch_deletion_desc = Desactivar la protecció de branca permet als usuaris amb accés d'escriptura pujar a la branca. Continua? settings.block_rejected_reviews = Bloqueja la fusió en revisions rebutjades -settings.block_rejected_reviews_desc = No es podrà fusionar quan els revisors oficials en sol·licitin canvis, encara que hi hagi prou aprovacions. -settings.block_on_official_review_requests = Bloquejar fusió en sol·licituds de revisió oficials -settings.block_on_official_review_requests_desc = No es podrà fusionar quan tingui sol·licituds de revisió oficials, encara que hi hagi prou aprovacions. -settings.block_outdated_branch = Bloquejar fusió si la «pull request» és obsoleta -settings.block_outdated_branch_desc = No es podrà fusionar quan la branca cap (head) sigui darrera la branca base. -settings.enforce_on_admins = Aplica aquesta regla als administradors del repositori -settings.enforce_on_admins_desc = Els repositoris del repositori no poden saltar-se aquesta regla. -settings.default_branch_desc = Tria una branca per defecte del repositori per a «pull requests» i commits: settings.merge_style_desc = Estils de fusió settings.default_merge_style_desc = Estil de fusió predeterminat -settings.choose_branch = Tria una branca… -settings.no_protected_branch = No hi ha branques protegides. -settings.protected_branch_required_rule_name = Nom de regla obligatori -settings.protected_branch_duplicate_rule_name = Ja hi ha una regla per a aquest set de branques -settings.protected_branch_required_approvals_min = El nombre d'aprovacions necessàries no pot ser negatiu. settings.tags.protection = Protecció d'etiquetes settings.tags.protection.pattern = Patró d'etiquetes settings.tags.protection.allowed.users = Usuaris permesos @@ -2531,6 +2451,35 @@ settings.unarchive.text = Des-arxivar el repositori farà possible rebre-hi comm settings.unarchive.success = El repositori s'ha des-arxivat satisfactòriament. settings.unarchive.error = Hi ha hagut un error en des-arxivar el repositori. Vegeu el log per més detalls. settings.update_avatar_success = S'ha actualitzat l'avatar del repositori. +editor.commit_empty_file_text = L'arxiu que s'està a punt de cometre és buit. Procedir? +pulls.no_merge_not_ready = Aquesta «pull request» no està llesta per a ser fusionada, comprova l'estat de revisió i les comprovacions d'estat. +activity.navbar.pulse = Pols +settings.event_action_recover_desc = La execució de l'acció ha finalitzat amb èxit després de que la última execució en el mateix flux fallés. +settings.event_action_success_desc = La execució de l'acció ha tingut èxit. +settings.event_package_desc = Paquet creat o eliminat en un repositori. +settings.branch_filter = Filtre de branca +settings.branch_filter_desc = Llista blanca de la branca per als esdeveniments de pujada i creació/eliminació de branques, especificada com un patró «glob». Si és buida o *, s'enregistraran els esdeveniments de totes les branques. Vegeu la documentació %[2]s per a la sintaxi. Per exemple: master, {master,release*}. +settings.protect_whitelist_deploy_keys = Posar les claus d'implementació amb accés d'escriptura per pujar (push) a la llista blanca. +settings.protect_no_valid_status_check_patterns = Sense patrons de comprovació d'estat vàlids. +settings.protect_required_approvals = Aprovacions necessàries +settings.protect_required_approvals_desc = Permet fusionar només «pull requests» amb prou revisions positives. +settings.protect_approvals_whitelist_enabled = Limita les aprovacions als usuaris o equips de la llista blanca +settings.protect_approvals_whitelist_enabled_desc = Només comptaran cap al total d'aprovacions necessàries les revisions d'usuaris i equips de la llista blanca. Si la llista és buida, en comptaran les revisions de tothom amb accés d'escriptura. +settings.protect_approvals_whitelist_users = Revisors en la llista blanca +settings.protect_approvals_whitelist_teams = Equips en la llista blanca per revisions +settings.block_rejected_reviews_desc = No es podrà fusionar quan els revisors oficials en sol·licitin canvis, encara que hi hagi prou aprovacions. +settings.block_on_official_review_requests = Bloquejar fusió en sol·licituds de revisió oficials +settings.block_on_official_review_requests_desc = No es podrà fusionar quan tingui sol·licituds de revisió oficials, encara que hi hagi prou aprovacions. +settings.block_outdated_branch = Bloquejar fusió si la «pull request» és obsoleta +settings.block_outdated_branch_desc = No es podrà fusionar quan la branca cap (head) sigui darrera la branca base. +settings.enforce_on_admins = Aplica aquesta regla als administradors del repositori +settings.enforce_on_admins_desc = Els repositoris del repositori no poden saltar-se aquesta regla. +settings.default_branch_desc = Tria una branca per defecte del repositori per a «pull requests» i commits: +settings.choose_branch = Tria una branca… +settings.no_protected_branch = No hi ha branques protegides. +settings.protected_branch_required_rule_name = Nom de regla obligatori +settings.protected_branch_duplicate_rule_name = Ja hi ha una regla per a aquest set de branques +settings.protected_branch_required_approvals_min = El nombre d'aprovacions necessàries no pot ser negatiu. settings.lfs_filelist = Fitxers LFS desats en aquest repositori settings.lfs_no_lfs_files = No hi ha fitxers LFS desats en aquest repositori settings.lfs_findcommits = Cercar commits @@ -2548,12 +2497,10 @@ settings.lfs_lock_path = Camí al fitxer per bloquejar… settings.lfs_locks_no_locks = Sense bloqueigs settings.lfs_lock_file_no_exist = El fitxer bloquejat no existeix en la branca per defecte settings.lfs_force_unlock = Forçar el desbloqueig -settings.lfs_pointers.found = S'ha trobat %d punters de blob - %d associats, %d no associats (%d no són a l'emmagatzematge) settings.lfs_pointers.sha = Hash del «blob» settings.lfs_pointers.inRepo = En repositori settings.lfs_pointers.exists = Existeix en magatzem settings.lfs_pointers.accessible = Accessible per l'usuari -settings.lfs_pointers.associateAccessible = Associar %d OIDs accessibles settings.rename_branch_failed_protected = No es pot canviar el nom de la branca %s perquè està protegida. settings.rename_branch_failed_exist = No es pot canviar el nom de la branca perquè la branca objectiu %s existeix. settings.rename_branch_failed_not_exist = No es pot canviar el nom de la branca %s perquè no existeix. @@ -2574,13 +2521,13 @@ release.asset_name = Nom d'actiu release.asset_external_url = URL externa release.add_external_asset = Afegeix actiu extern release.invalid_external_url = URL externa invàlida: "%s" -release.summary_card_alt = Panell de resum d'una publicació titulada "%s" al repositori %s branch.name = Nom de branca branch.already_exists = Ja existeix una branca anomenada "%s". branch.delete = Eliminar branca "%s" branch.delete_html = Eliminar branca -branch.delete_desc = Eliminar una branca és permanent. Malgrat la branca eliminada pot seguir existint durant una estona abans d'eliminar-se completament, aquesta acció NO es pot desfer en la majoria de casos. Continuar? branch.deletion_success = S'ha eliminat la branca "%s". +release.summary_card_alt = Panell de resum d'una publicació titulada "%s" al repositori %s +branch.delete_desc = Eliminar una branca és permanent. Malgrat la branca eliminada pot seguir existint durant una estona abans d'eliminar-se completament, aquesta acció NO es pot desfer en la majoria de casos. Continuar? branch.deletion_failed = No s'ha pogut eliminar la branca "%s". branch.delete_branch_has_new_commits = No s'ha pogut eliminar la branca "%s" perquè s'han afegit nou commits després de fusionar-la. branch.create_branch = Crear branca %s @@ -2620,9 +2567,14 @@ find_file.no_matching = No s'ha trobat un fitxer coincident error.csv.too_large = No es pot mostrar aquest fitxer perquè és massa gran. error.csv.unexpected = No es pot mostrar aquest fitxer perquè conté un caràcter inesperat a la línia %d i columna %d. error.csv.invalid_field_count = No es pot mostrar aquest fitxer perquè té un nombre d'entrades incorrecte a la línia %d. - +settings.web_hook_name_feishu = Feishu / Lark Suite +settings.web_hook_name_larksuite_only = Lark Suite +settings.web_hook_name_wechatwork = WeCom (Wechat Work) +settings.lfs_pointers.found = S'ha trobat %d punters de blob - %d associats, %d no associats (%d no són a l'emmagatzematge) +settings.lfs_pointers.associateAccessible = Associar %d OIDs accessibles settings.matrix.access_token_helper = Es recomana muntar un compte de Matrix dedicat per això. El testimoni d'accés el podeu trobar al client web Element (en una pestanya privada/d'incògnit) > User menu (cantó superior esquerre) > All settings > Help & About > Advanced > Access Token (sota de l'URL del homeserver). Tanqueu la pestanya privada/d'incògnit (tancar la sessió invalidaria el testimoni). settings.matrix.room_id_helper = L'ID de sala el podeu trobar al client web Element > Room Settings > Advanced > Internal room ID. Per exemple: %s. +diff.parent = pare [user] unblock = Desbloquejar @@ -2664,22 +2616,16 @@ public_activity.visibility_hint.admin_private = Aquesta activitat és visible pe public_activity.visibility_hint.self_private_profile = La vostra activitat és visible només per vós i pels administradors de la instància perquè el vostre perfil és privat. Configurar. change_avatar = Canvieu el vostre avatar… joined_on = S'ha unit el %s - starred = Repositoris destacats [git.filemode] executable_file = Fitxer executable symbolic_link = Enllaç simbòlic submodule = Submòdul - -directory = Directori normal_file = Fitxer normal +directory = Directori [markup] -filepreview.truncated = La vista prèvia s'ha truncat - -filepreview.line = Línia %[1]d a %[2]s -filepreview.lines = Línies %[1]d a %[2]d en %[3]s [translation_meta] test = Aquest és un text de prova. No es mostra a l'interfície d'usuari de Forgejo, però s'utilitza amb finalitats de prova. Pots introduir "d'acord" per a estalviar temps (o alguna frase divertida al teu gust) i arribar a la dolça fita del 100% :) @@ -2736,16 +2682,16 @@ teams.add_team_member = Afegir un membre a l'equip teams.invite_team_member = Convidar a %s teams.invite_team_member.list = Invitacions pendents teams.delete_team_title = Eliminar l'equip - +org_desc = Descripció +team_desc = Descripció +team_name = Nom de l'equip +lower_members = membres members = Membres teams = Equips code = Codi -lower_members = membres lower_repositories = repositoris -org_desc = Descripció -team_name = Nom de l'equip -team_desc = Descripció - +settings.repoadminchangeteam = L'administrador del repositori pot donar-ne i treure'n l'accés als equips +members.leave = Marxar org_name_holder = Nom de l'organització org_full_name_holder = Nom complet de l'organització org_name_helper = Els noms d'organització han de ser curts i memorables. @@ -2763,14 +2709,12 @@ follow_blocked_user = No podeu seguir aquesta organització perquè us ha bloque form.name_reserved = El nom d'organització "%s" és reservat. form.name_pattern_not_allowed = El patró "%s" no està permès en un nom d'organització. form.create_org_not_allowed = No teniu permisos per crear una organització. -settings.repoadminchangeteam = L'administrador del repositori pot donar-ne i treure'n l'accés als equips settings.change_orgname_prompt = Nota: Canviar el nom de l'organització també en canviarà l'URL i alliberarà el nom antic. settings.change_orgname_redirect_prompt.with_cooldown.one = El nom antic de l'organització serà disponible a tothom després d'un període de protecció de %[1]d dia. Encara podreu reclamar el nom antic durant el període de protecció. settings.change_orgname_redirect_prompt.with_cooldown.few = El nom antic de l'organització serà disponible a tothom després d'un període de protecció de %[1]d dies. Encara podreu reclamar el nom antic durant el període de protecció. settings.hooks_desc = Afegir webhooks que s'activaran a tots els repositoris en aquesta organització. members.public_helper = Ocultar members.private_helper = Fer visible -members.leave = Marxar teams.admin_access_helper = Els membres poden pujar (push) i baixar (pull) als repositoris de l'equip i afegir-hi col·laboradors. teams.delete_team_desc = Eliminar un equip fa que els seus membres perdin accés al repositori. Continuar? teams.delete_team_success = S'ha eliminat l'equip. @@ -2799,53 +2743,51 @@ dashboard.delete_missing_repos = Suprimir tots els repositoris que no tinguin el dashboard.delete_missing_repos.started = S'ha iniciat la tasca per suprimir tots els repositoris que no tinguin els fitxers de Git. dashboard.update_mirrors = Actualitzar les rèpliques dashboard.archive_cleanup = Suprimir els arxius de repositori antics -dashboard.memory_allocate_times = Assignacions de memòria -repositories = Repositoris -hooks = Webhooks -users.name = Nom d'usuari -users.full_name = Nom complet -users.activated = Activat -users.admin = Administrador -users.list_status_filter.is_admin = Administrador -emails.activated = Activat -emails.filter_sort.email = Correu electrònic -emails.filter_sort.name = Nom d'usuari orgs.name = Nom orgs.teams = Equips -orgs.members = Membres -orgs.new_orga = Nova organització repos.name = Nom repos.issues = Problemes repos.size = Mida -packages.name = Nom -packages.version = Versió +auths.name = Nom +config.db_name = Nom +config.mailer_name = Nom +monitor.name = Nom +notices.type = Tipus +config.db_type = Tipus +auths.type = Tipus packages.type = Tipus packages.repository = Repositori +notices.type_1 = Repositori packages.size = Mida -auths.name = Nom -auths.type = Tipus auths.enabled = Habilitat +config.ssh_enabled = Habilitat +config.lfs_enabled = Habilitat +config.mailer_enabled = Habilitat +config.db_host = Amfitrió auths.host = Amfitrió auths.port = Port -config.ssh_enabled = Habilitat config.ssh_port = Port -config.lfs_enabled = Habilitat -config.db_type = Tipus -config.db_host = Amfitrió -config.db_name = Nom +users.name = Nom d'usuari +emails.filter_sort.name = Nom d'usuari config.db_user = Nom d'usuari config.db_schema = Esquema config.db_ssl_mode = SSL config.db_path = Camí -config.mailer_enabled = Habilitat -config.mailer_name = Nom -monitor.name = Nom -monitor.queue.name = Nom -monitor.queue.type = Tipus -notices.type = Tipus -notices.type_1 = Repositori +users.full_name = Nom complet +users.activated = Activat +emails.activated = Activat +emails.filter_sort.email = Correu electrònic +users.admin = Administrador notices.desc = Descripció - +packages.name = Nom +packages.version = Versió +orgs.members = Membres +orgs.new_orga = Nova organització +repositories = Repositoris +hooks = Webhooks +dashboard.delete_generated_repository_avatars = Suprimir els avatars de repositori generats +dashboard.sync_repo_tags = Sincronitzar les etiquetes de les dades de Git a la base de dades +dashboard.repo_health_check = Comprovar la salut de tots els repositoris dashboard = Tauler self_check = Comprovació automàtica identity_access = Identitat i accés @@ -2869,12 +2811,6 @@ dashboard.operations = Operacions de manteniment dashboard.system_status = Estat del sistema dashboard.operation_name = Nom d'operació dashboard.clean_unbind_oauth = Neteja les connexions OAuth desenllaçades -dashboard.delete_generated_repository_avatars = Suprimir els avatars de repositori generats -dashboard.sync_repo_tags = Sincronitzar les etiquetes de les dades de Git a la base de dades -dashboard.repo_health_check = Comprovar la salut de tots els repositoris - -notices = Notificacions del sistema -dashboard.operation_switch = Intercanvia dashboard.operation_run = Executar dashboard.clean_unbind_oauth_success = S'han eliminat totes les connexions OAuth desenllaçades. dashboard.task.started = Tasca iniciada: %[1]s @@ -2884,13 +2820,12 @@ dashboard.task.error = Error en la tasca: %[1]s: %[3]s dashboard.task.finished = Tasca: %[1] iniciada per %[2s] ha finalitzat dashboard.task.unknown = Tasca desconeguda: %[1]s dashboard.cron.started = Cron iniciat: %[1]s -dashboard.cron.process = Cron: %[1]s dashboard.cron.cancelled = Cron: %[1]s cancel·lat: %[3]s dashboard.cron.error = Error amb Cron: %s: %[3]s dashboard.sync_repo_branches = La sincronització ha ignorat branques de les dades Git a la base de dades dashboard.check_repo_stats = Comprovar totes les estadístiques del repositori dashboard.deleted_branches_cleanup = Netejar branques eliminades -dashboard.update_migration_poster_id = Actualitza les ID de l'autor de migració +dashboard.update_migration_poster_id =Actualitza les ID de l'autor de migració dashboard.git_gc_repos = Recull la brossa de tots els repositoris dashboard.resync_all_sshkeys = Actualitzar el fitxer ".ssh/authorized_keys" amb les claus SSH de Forgejo. dashboard.resync_all_sshprincipals = Actualitzar el fitxer ".ssh/authorized_principals" amb els principals SSH de Forgejo. @@ -2900,30 +2835,6 @@ dashboard.sync_external_users = Sincronitzar dades d'usuari externes dashboard.cleanup_hook_task_table = Netejar la taula hook_task dashboard.cleanup_packages = Netejar paquets caducats dashboard.cleanup_actions = Netejar registres i artefactes d'accions caducats -dashboard.server_uptime = Temps de funcionament del servidor -dashboard.current_goroutine = Goroutines actuals -dashboard.current_memory_usage = Ús de memòria actual -dashboard.total_memory_allocated = Total de memòria adjudicada -dashboard.memory_obtained = Memòria rebuda -dashboard.pointer_lookup_times = Temps de consulta dels punters -dashboard.memory_free_times = Alliberaments de memòria -dashboard.current_heap_usage = Ús del heap actual -dashboard.heap_memory_obtained = Memòria de heap rebuda -dashboard.heap_memory_idle = Memòria del heap en repòs -dashboard.heap_memory_in_use = Memòria del heap en ús -dashboard.heap_memory_released = Memòria del heap alliberada -dashboard.heap_objects = Objectes del heap -dashboard.bootstrap_stack_usage = Ús de l'arrancada de la pila (stack) -dashboard.stack_memory_obtained = Memòria de la pila (stack) rebuda -dashboard.mspan_structures_usage = Ús de les estructures MSpan -dashboard.mspan_structures_obtained = Estructures MSpan rebudes -dashboard.mcache_structures_usage = Ús de les estructures MCache -dashboard.mcache_structures_obtained = Estructures MCache rebudes -dashboard.gc_metadata_obtained = Metadades del GC rebudes -dashboard.next_gc_recycle = Proper cicle de GC -dashboard.last_gc_time = Temps des del darrer GC -dashboard.total_gc_pause = Pausa total de GC -dashboard.last_gc_pause = Darrera pausa de GC dashboard.delete_old_actions = Eliminar totes les activitats antigues de la base de dades dashboard.delete_old_actions.started = Eliminar totes les activitats antigues des que s'ha iniciat la base de dades. dashboard.delete_old_system_notices = Eliminar totes les notificacions de sistema antigues de la base de dades @@ -2933,6 +2844,8 @@ dashboard.stop_endless_tasks = Aturar tasques d'accions infinites dashboard.cancel_abandoned_jobs = Cancel·lar feines d'accions abandonades dashboard.sync_branch.started = S'ha iniciat la sincronització de branca dashboard.sync_tag.started = S'ha iniciat la sincronització d'etiquetes +dashboard.cron.process = Cron: %[1]s +notices = Notificacions del sistema dashboard.rebuild_issue_indexer = Reconstruir l'indexador d'incidències users.user_manage_panel = Gestionar comptes d'usuari users.new_account = Crear compte d'usuari @@ -2940,7 +2853,6 @@ users.restricted = Restringit users.reserved = Reservat users.bot = Bot users.remote = Remot -users.2fa = A2F users.repos = Repos users.created = Creats users.last_login = Darrer inici de sessió @@ -2979,18 +2891,6 @@ users.purge = Purgar usuari users.purge_help = Elimina forçosament l'usuari i tots els seus repositoris, organitzacions i paquets. També s'eliminaran tots els seus comentaris i incidències. users.still_own_packages = Aquest usuari encara té un o més paquets. Elimineu-los abans. users.deletion_success = S'ha eliminat l'usuari. -users.reset_2fa = Restableix l'A2F -users.list_status_filter.menu_text = Filtrar -users.list_status_filter.reset = Restaurar -users.list_status_filter.is_active = Actiu -users.list_status_filter.not_active = Inactiu -users.list_status_filter.not_admin = No administrador -users.list_status_filter.is_restricted = Restringit -users.list_status_filter.not_restricted = No restringit -users.list_status_filter.is_prohibit_login = Inici de sessió prohibit -users.list_status_filter.not_prohibit_login = Inici de sessió permès -users.list_status_filter.is_2fa_enabled = A2F activada -users.list_status_filter.not_2fa_enabled = A2F desactivada users.details = Detalls d'usuari emails.email_manage_panel = Gestionar correus d'usuari emails.primary = Principal @@ -3034,10 +2934,6 @@ auths.updated = Actualitzat auths.auth_type = Tipus d'autenticació auths.auth_name = Nom d'autenticació auths.security_protocol = Protocol de seguretat -auths.bind_dn = DN bind -auths.bind_password = Contrasenya bind -auths.user_base = Base de cerca d'usuari -auths.user_dn = DN d'usuari auths.attribute_username = Atribut de nom d'usuari auths.attribute_username_placeholder = Deixeu-ho buit per usar el nom d'usuari de Forgejo. auths.attribute_name = Atribut de nom @@ -3045,9 +2941,6 @@ auths.attribute_surname = Atribut de cognom auths.attribute_mail = Atribut de correu electrònic auths.attribute_ssh_public_key = Atribut de clau pública SSH auths.attribute_avatar = Atribut d'avatar -auths.attributes_in_bind = Recupera els atributs del context bind DN -auths.default_domain_name = Nom de domini per defecte usat per l'adreça de correu -auths.allow_deactivate_all = Permet que un resultat de cerca buit desactivi tots els usuaris auths.use_paged_search = Usar cerca en pàgines auths.search_page_size = Mida de pàgina auths.filter = Filtre d'usuari @@ -3055,11 +2948,6 @@ auths.admin_filter = Filtre d'administrador auths.restricted_filter = Filtre restringit auths.restricted_filter_helper = Deixeu-ho buit per no marcar cap usuari com a restringit. Useu un asterisc ("*") per marcar tots els usuaris que no coincideixin amb el Filtre d'administrador com a restringits. auths.verify_group_membership = Verificar l'adhesió de grup a LDAP (deixeu el filtre buit per ignorar-ho) -auths.group_search_base = DN base de cerca de grup -auths.group_attribute_list_users = Atribut de grup amb la llista d'usuaris -auths.user_attribute_in_group = Atribut d'usuari llistat en el grup -auths.map_group_to_team = Assigna els grups LDAP a equips d'organització (deixeu-ho buit per saltar-ho) -auths.map_group_to_team_removal = Elimina els usuaris dels equips sincronitzats si no pertanyen al grup LDAP corresponent auths.enable_ldap_groups = Activar grups LDAP auths.smtp_auth = Tipus d'autenticació SMTP auths.smtphost = Amfitrió SMTP @@ -3069,9 +2957,6 @@ auths.allowed_domains_helper = Deixeu-ho buit per permetre tots els dominis. Sep auths.skip_tls_verify = Salta't la verificació TLS auths.force_smtps = Força l'SMTPS auths.force_smtps_helper = L'SMTPS sempre usa el port 465. Indiqueu això per forçar l'SMTPS en un altre port. (Sinó s'usarà l'STARTTLS en altres ports, sempre que l'amfitrió ho suporti.) -auths.helo_hostname = Hostname HELO -auths.helo_hostname_helper = Hostname enviat amb HELO. Deixeu-ho en blanc per enviar el hostname actual. -auths.disable_helo = Deshabilita HELO auths.pam_service_name = Nom de servei PAM auths.pam_email_domain = Domini de correu PAM (opcional) auths.oauth2_provider = Proveïdor OAuth2 @@ -3084,26 +2969,6 @@ auths.oauth2_tokenURL = URL de testimoni (token) auths.oauth2_authURL = URL d'autorització auths.oauth2_profileURL = URL de perfil auths.oauth2_emailURL = URL de correu -auths.skip_local_two_fa = Salta l'A2F local -auths.skip_local_two_fa_helper = No marcar aquesta opció farà que els usuaris que tenen l'A2F definida igualment hauran de passar l'A2F per iniciar sessió -auths.oauth2_tenant = Tenant -auths.oauth2_scopes = Àmbits addicionals -auths.oauth2_required_claim_value_helper = Assigneu aquest valor per restringir l'inici de sessió des d'aquesta font als usuaris que declarin aquest nom i valor -auths.oauth2_map_group_to_team_removal = Elimina els usuaris dels equips sincronitzats si no pertanyen al grup corresponent. -auths.tips = Consells -auths.tips.gmail_settings = Configuracions de Gmail: -auths.tips.oauth2.general = Autenticació OAuth2 -auths.tips.oauth2.general.tip = Quan registreu una nova autenticació OAuth2, l'URL de crida/redirecció serà: -auths.tip.oauth2_provider = Proveïdor OAuth2 -auths.tip.bitbucket = Registreu un nou consumidor OAuth2 a %s i afegiu els permisos "Compte" - "Lectura" -auths.tip.nextcloud = Registreu un nou consumidor OAuth en la vostra instància amb el menú "Configuració -> Seguretat -> Client OAuth 2.0" -auths.tip.dropbox = Crea una nova aplicació a %s -auths.tip.facebook = Registreu una nova aplicació a %s i afegiu el producte "Facebook Login" -auths.tip.github = Registra una aplicació OAuth nova a %s -auths.tip.gitlab_new = Registra una aplicació nova a %s -auths.tip.google_plus = Obté les credencials del client OAuth2 amb la consola del Google API a %s -auths.tip.openid_connect = Usa l'URL d'OpenID Connect Discovery (/.well-known/openid-configuration) per a especificar els endpoints -auths.tip.mastodon = Introduïu l'URL a una instància diferent de Mastodon amb la que voleu autenticar-vos (o bé useu la instància per defecte) auths.edit = Editar font d'autenticació auths.activated = S'ha activat aquesta font d'autenticació auths.new_success = S'ha afegit l'autenticació "%s". @@ -3115,27 +2980,43 @@ auths.delete_auth_desc = Eliminar una font d'autenticació fa que els usuaris qu auths.still_in_used = Aquesta font d'autenticació encara s'utilitza. Convertiu o elimineu els usuaris que en fan ús abans. auths.deletion_success = S'ha eliminat la font d'autenticació. auths.login_source_exist = La font d'autenticació "%s" ja existeix. -auths.unable_to_initialize_openid = No s'ha pogut inicialitzar el proveïdor d'OpenID Connect: %s -auths.invalid_openIdConnectAutoDiscoveryURL = URL d'Auto Discovery invàlida (ha de ser una URL vàlida començant amb http:// o https://) config.server_config = Configuració de servidor config.app_name = Títol de la instància config.app_slogan = Eslògan de la instància config.app_ver = Versió de Forgejo config.app_url = URL base config.custom_conf = Camí al fitxer de configuració -config.custom_file_root_path = Camí arrel dels fitxers personalitzada config.domain = Domini del servidor config.offline_mode = Mode local -config.disable_router_log = Desactiva els registres de l'encaminador -config.run_user = Executa com a usuari -config.run_mode = Mode d'execució config.git_version = Versió de Git -config.app_data_path = Camí a les dades d'aplicació +config.ssh_config = Configuració SSH +config.run_user = Executa com a usuari +auths.tips.oauth2.general = Autenticació OAuth2 +auths.tips.oauth2.general.tip = Quan registreu una nova autenticació OAuth2, l'URL de crida/redirecció serà: +auths.tip.oauth2_provider = Proveïdor OAuth2 +auths.tip.bitbucket = Registreu un nou consumidor OAuth2 a %s i afegiu els permisos "Compte" - "Lectura" +auths.tip.nextcloud = Registreu un nou consumidor OAuth en la vostra instància amb el menú "Configuració -> Seguretat -> Client OAuth 2.0" +auths.tip.dropbox = Crea una nova aplicació a %s +auths.tip.facebook = Registreu una nova aplicació a %s i afegiu el producte "Facebook Login" config.repo_root_path = Camí arrel del repositori +auths.bind_dn = DN bind +auths.bind_password = Contrasenya bind +auths.user_base = Base de cerca d'usuari +auths.user_dn = DN d'usuari +auths.attributes_in_bind = Recupera els atributs del context bind DN +auths.default_domain_name = Nom de domini per defecte usat per l'adreça de correu +auths.allow_deactivate_all = Permet que un resultat de cerca buit desactivi tots els usuaris +auths.oauth2_required_claim_value_helper = Assigneu aquest valor per restringir l'inici de sessió des d'aquesta font als usuaris que declarin aquest nom i valor +auths.tip.mastodon = Introduïu l'URL a una instància diferent de Mastodon amb la que voleu autenticar-vos (o bé useu la instància per defecte) +auths.unable_to_initialize_openid = No s'ha pogut inicialitzar el proveïdor d'OpenID Connect: %s +auths.invalid_openIdConnectAutoDiscoveryURL = URL d'Auto Discovery invàlida (ha de ser una URL vàlida començant amb http:// o https://) +config.custom_file_root_path = Camí arrel dels fitxers personalitzada +config.disable_router_log = Desactiva els registres de l'encaminador +config.run_mode = Mode d'execució +config.app_data_path = Camí a les dades d'aplicació config.log_file_root_path = Camí de registres config.script_type = Tipus d'script config.reverse_auth_user = Usuari d'autenticació al servidor intermediari revers -config.ssh_config = Configuració SSH config.ssh_start_builtin_server = Usa el servidor integrat config.ssh_domain = Domini del servidor SSH config.ssh_listen_port = Port d'escolta @@ -3190,9 +3071,9 @@ config.send_test_mail_submit = Envia config.test_mail_failed = No s'ha pogut enviar el correu de prova a "%s": %v config.test_mail_sent = S'ha enviat un correu de prova a "%s". config.cache_config = -config.cache_adapter = Adaptador de la memòria cau -config.cache_interval = Interval de la memòria cau -config.cache_conn = Connexió de la memòria cau +config.cache_adapter =Adaptador de la memòria cau +config.cache_interval =Interval de la memòria cau +config.cache_conn =Connexió de la memòria cau config.cache_item_ttl = TTL dels ítems de la memòria cau config.cache_test = Prova la memòria cau config.cache_test_failed = No s'ha pogut examinar la memòria cau: %v. @@ -3208,62 +3089,39 @@ config.https_only = Només HTTPS config.picture_config = Configuració d'imatge i avatar config.disable_gravatar = Deshabilita Gravatar config.enable_federated_avatar = Habilita els avatars federats +dashboard.operation_switch = Intercanvia +users.2fa = A2F +users.reset_2fa = Restableix l'A2F +auths.group_search_base = DN base de cerca de grup +auths.group_attribute_list_users = Atribut de grup amb la llista d'usuaris +auths.user_attribute_in_group = Atribut d'usuari llistat en el grup +auths.map_group_to_team = Assigna els grups LDAP a equips d'organització (deixeu-ho buit per saltar-ho) +auths.map_group_to_team_removal = Elimina els usuaris dels equips sincronitzats si no pertanyen al grup LDAP corresponent +auths.helo_hostname = Hostname HELO +auths.helo_hostname_helper = Hostname enviat amb HELO. Deixeu-ho en blanc per enviar el hostname actual. +auths.disable_helo = Deshabilita HELO +auths.skip_local_two_fa = Salta l'A2F local +auths.skip_local_two_fa_helper = No marcar aquesta opció farà que els usuaris que tenen l'A2F definida igualment hauran de passar l'A2F per iniciar sessió +auths.oauth2_tenant = Tenant +auths.oauth2_scopes = Àmbits addicionals +auths.oauth2_map_group_to_team_removal = Elimina els usuaris dels equips sincronitzats si no pertanyen al grup corresponent. +auths.tips = Consells +auths.tips.gmail_settings = Configuracions de Gmail: +auths.tip.github = Registra una aplicació OAuth nova a %s +auths.tip.gitlab_new = Registra una aplicació nova a %s +auths.tip.google_plus = Obté les credencials del client OAuth2 amb la consola del Google API a %s +auths.tip.openid_connect = Usa l'URL d'OpenID Connect Discovery (/.well-known/openid-configuration) per a especificar els endpoints +dashboard.update_checker = Comprovador d'actualització [actions] -runners.name = Nom -runners.owner_type = Tipus -runners.description = Descripció -runners.labels = Etiquetes -runners.task_list.repository = Repositori -runners.task_list.commit = Commit -runners.version = Versió -runs.commit = Commit runs.no_workflows.help_write_access = No sabeu com començar amb Forgejo Actions? Feu un cop d'ull als primers passos a la documentació d'usuari per escriure el vostre primer flux de treball, llavors configureu un runner Forgejo per a executar els vostres treballs. runs.no_workflows.help_no_write_access = Per saber-ne més sobre Forgejo Actions, vegeu la documentació. -variables = Variables -variables.management = Gestionar variables -variables.creation = Afegir variables -variables.none = Encara no hi ha variables. -variables.deletion = Eliminar variable -variables.deletion.description = Eliminar una variable és permanent i no es pot desfer. Continua? -variables.description = Les variables es passaran a certes accions i no es poden llegir altrament. -variables.edit = Editar variable -variables.not_found = No s'ha trobat la variable. -variables.deletion.failed = No s'ha pogut eliminar la variable. -variables.deletion.success = S'ha canviar el nom de la variable. -variables.creation.failed = No s'ha pogut afegir la variable. -variables.creation.success = S'ha afegit la variable "%s". -variables.update.failed = No s'ha pogut editar la variable. -variables.update.success = S'ha editat la variable. -[repo.permissions] -code.read = Lectura: Accedir i clonar el codi del repositori. -code.write = Escriptura: Pujar (push) al repositori, crear branques i etiquetes. -issues.read = Lectura: Llegir i crear incidències i comentaris. -issues.write = Escriptura: Tancar incidències i gestionar metadades com etiquetes, fites, assignacions, terminis i dependències. -pulls.read = Lectura: Llegir i crear «pull requests». -pulls.write = Escriptura: Tancar «pull requests» i gestionar metadades com etiquetes, fites, assignacions, terminis i dependències. -releases.read = Lectura: Veure i baixar publicacions. -releases.write = Escriptura: Publicar, editar i eliminar publicacions i els seus recursos. -wiki.read = Lectura: Llegir la wiki integrada i el seu historial. -wiki.write = Escriptura: Crear, modificar i eliminar pàgines en la wiki integrada. -projects.read = Lectura: Accedir els taulells de projecte del repositori. -projects.write = Escriptura: Crear projectes i columnes, i editar-los. -packages.read = Lectura: Llegir i baixar paquets assignats al repositori. -packages.write = Escriptura: Publicar i eliminar paquets assignats al repositori. -actions.read = Lectura: Veure l'execució dels fluxos de treball i els seus registres (logs). -actions.write = Escriptura: Activar, reiniciar i cancel·lar fluxos de treball. Gestionar la delegació de confiança als autors de «pull requests». -ext_issues = Accedir a l'enllaç a un gestor d'incidències extern. Els permisos es gestionen allà. -ext_wiki = Accedir a l'enllaç a una wiki externa. Els permisos es gestionen allà. - -[graphs] -component_loading = Carregant %s… -component_loading_failed = No s'ha pogut carregar %s -component_loading_info = Això trigarà una estona… -component_failed_to_load = Ha passat un error inesperat. -code_frequency.what = freqüència de codi -contributors.what = contribucions -recent_commits.what = commits recents +[projects] +deleted.display_name = Projecte eliminat +type-1.display_name = Projecte individual +type-2.display_name = Projecte del repositori +type-3.display_name = Projecte de l'organització [secrets] secrets = Secrets @@ -3278,8 +3136,31 @@ deletion.success = S'ha eliminat el secret. deletion.failed = No s'ha pogut eliminar el secret. management = Gestionar secrets -[projects] -deleted.display_name = Projecte eliminat -type-1.display_name = Projecte individual -type-2.display_name = Projecte del repositori -type-3.display_name = Projecte de l'organització \ No newline at end of file +[repo.permissions] +code.read = Lectura: Accedir i clonar el codi del repositori. +issues.read = Lectura: Llegir i crear incidències i comentaris. +pulls.read = Lectura: Llegir i crear «pull requests». +releases.read = Lectura: Veure i baixar publicacions. +wiki.read = Lectura: Llegir la wiki integrada i el seu historial. +projects.read = Lectura: Accedir els taulells de projecte del repositori. +packages.read = Lectura: Llegir i baixar paquets assignats al repositori. +actions.read = Lectura: Veure l'execució dels fluxos de treball i els seus registres (logs). +code.write = Escriptura: Pujar (push) al repositori, crear branques i etiquetes. +issues.write = Escriptura: Tancar incidències i gestionar metadades com etiquetes, fites, assignacions, terminis i dependències. +pulls.write = Escriptura: Tancar «pull requests» i gestionar metadades com etiquetes, fites, assignacions, terminis i dependències. +releases.write = Escriptura: Publicar, editar i eliminar publicacions i els seus recursos. +wiki.write = Escriptura: Crear, modificar i eliminar pàgines en la wiki integrada. +projects.write = Escriptura: Crear projectes i columnes, i editar-los. +packages.write = Escriptura: Publicar i eliminar paquets assignats al repositori. +actions.write = Escriptura: Activar, reiniciar i cancel·lar fluxos de treball. Gestionar la delegació de confiança als autors de «pull requests». +ext_issues = Accedir a l'enllaç a un gestor d'incidències extern. Els permisos es gestionen allà. +ext_wiki = Accedir a l'enllaç a una wiki externa. Els permisos es gestionen allà. + +[graphs] +component_loading = Carregant %s… +component_loading_failed = No s'ha pogut carregar %s +component_loading_info = Això trigarà una estona… +component_failed_to_load = Ha passat un error inesperat. +code_frequency.what = freqüència de codi +contributors.what = contribucions +recent_commits.what = commits recents \ No newline at end of file diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index d3626ed677..95457b6dcb 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -37,18 +37,6 @@ twofa=Dvoufázové ověření twofa_scratch=Dvoufaktorový kód passcode=Přístupový kód -webauthn_insert_key=Vložte svůj bezpečnostní klíč -webauthn_sign_in=Stiskněte tlačítko na svém bezpečnostním klíči. Pokud bezpečnostní klíč nemá žádné tlačítko, vložte jej znovu. -webauthn_press_button=Stiskněte tlačítko na bezpečnostním klíči… -webauthn_use_twofa=Použít dvoufaktorový kód z vašeho telefonu -webauthn_error=Nepodařilo se přečíst váš bezpečnostní klíč. -webauthn_unsupported_browser=Váš prohlížeč momentálně nepodporuje WebAuthn. -webauthn_error_unknown=Došlo k neznámé chybě. Opakujte akci. -webauthn_error_insecure=WebAuthn podporuje pouze zabezpečená připojení. Pro testování přes HTTP můžete použít výchozí „localhost“ nebo „127.0.0.1“ -webauthn_error_unable_to_process=Server nemohl zpracovat váš požadavek. -webauthn_error_duplicated=Bezpečnostní klíč není pro tento požadavek povolen. Ujistěte se prosím, zda klíč již není registrován. -webauthn_error_empty=Musíte nastavit název tohoto klíče. -webauthn_error_timeout=Požadavek vypršel dříve, než se podařilo přečíst váš klíč. Znovu načtěte tuto stránku a akci opakujte. repository=Repozitář organization=Organizace mirror=Zrcadlo @@ -181,7 +169,7 @@ buttons.bold.tooltip=Přidat tučný text (Ctrl+B / ⌘B) buttons.italic.tooltip=Přidat kurzívu (Ctrl+I / ⌘I) buttons.quote.tooltip=Citace buttons.code.tooltip=Přidat kód -buttons.link.tooltip=Přidat odkaz +buttons.link.tooltip=Přidat odkaz (Ctrl+K / ⌘K) buttons.list.unordered.tooltip=Přidat odrážkový seznam buttons.list.ordered.tooltip=Přidat číslovaný seznam buttons.list.task.tooltip=Přidat seznam úkolů @@ -609,7 +597,7 @@ organization_leave_success=Úspěšně jste opustili organizaci %s. invalid_ssh_key=Nepodařilo se ověřit váš klíč SSH: %s invalid_gpg_key=Nepodařilo se ověřit váš klíč GPG: %s -invalid_ssh_principal=Neplatný SSH Principal certifikát: %s +invalid_ssh_principal=Neplatný principal: %s must_use_public_key=Zadaný klíč je soukromý klíč. Nenahrávejte svůj soukromý klíč nikde. Místo toho použijte váš veřejný klíč. unable_verify_ssh_key=Nepodařilo se ověřit klíč SSH, zkontrolujte, zda neobsahuje chyby. auth_failed=Ověření selhalo: %v @@ -805,7 +793,7 @@ key_content_gpg_placeholder=Začíná s „-----BEGIN PGP PUBLIC KEY BLOCK----- add_new_principal=Přidat principal ssh_key_been_used=Tento klíč SSH byl na server již přidán. ssh_key_name_used=U vašeho účtu již existuje klíč SSH se stejným názvem. -ssh_principal_been_used=Tento SSH Principal certifikát již byl přidán na server. +ssh_principal_been_used=Tento principal již byl přidán na server. gpg_key_id_used=Veřejný klíč GPG se stejným ID již existuje. gpg_no_key_email_found=Tento klíč GPG neodpovídá žádné aktivované e-mailové adrese spojené s vaším účtem. Může být stále přidán, pokud podepíšete zadaný token. gpg_key_matched_identities=Odpovídající identity: @@ -847,7 +835,7 @@ gpg_key_deletion_desc=Odstraněním klíče GPG zneplatníte ověření revizí, ssh_principal_deletion_desc=Odstranění SSH Principal certifikátu zruší jeho přístup k vašemu účtu. Pokračovat? ssh_key_deletion_success=Klíč SSH byl odstraněn. gpg_key_deletion_success=Klíč GPG byl odstraněn. -ssh_principal_deletion_success=SSH Principal certifikát byl odstraněn. +ssh_principal_deletion_success=Principal byl odstraněn. added_on=Přidáno %s valid_until_date=Platné do %s valid_forever=Platné navždy @@ -857,7 +845,7 @@ can_read_info=Čtení can_write_info=Zápis key_state_desc=Tento klíč byl použit během posledních 7 dní token_state_desc=Tento token byl použit během posledních 7 dní -principal_state_desc=Tento SSH Principal certifikát byl použit během posledních 7 dní +principal_state_desc=Tento principal certifikát byl použit během posledních 7 dní show_openid=Zobrazit na profilu hide_openid=Odstranit z profilu ssh_disabled=SSH je zakázáno @@ -1154,14 +1142,6 @@ migrate_options_lfs_endpoint.label=Endpoint LFS migrate_options_lfs_endpoint.description=Migrace se pokusí použít váš vzdálený Git pro určení LFS serveru. Můžete také zadat vlastní koncový bod, pokud jsou data LFS repozitáře uložena někde jinde. migrate_options_lfs_endpoint.description.local=Podporována je také cesta k lokálnímu serveru. migrate_options_lfs_endpoint.placeholder=Ponecháte-li prázdné, koncový bod bude odvozen z adresy URL klonu -migrate_items=Položky pro migrování -migrate_items_wiki=Wiki -migrate_items_milestones=Milníky -migrate_items_labels=Štítky -migrate_items_issues=Problémy -migrate_items_pullrequests=Žádosti o sloučení -migrate_items_merge_requests=Sloučit žádosti -migrate_items_releases=Vydání migrate_repo=Migrovat repozitář migrate.clone_address=Migrovat / klonovat z URL migrate.clone_address_desc=HTTP(S) nebo URL Git „clone“ existujícího repozitáře @@ -1180,16 +1160,6 @@ migrate.migrating=Probíhá migrace z %s … migrate.migrating_failed=Migrace z %s se nezdařila. migrate.migrating_failed.error=Nepodařilo se migrovat: %s migrate.migrating_failed_no_addr=Migrace se nezdařila. -migrate.migrating_git=Migrace dat z Gitu -migrate.migrating_topics=Migrace témat -migrate.migrating_milestones=Migrace milníků -migrate.migrating_labels=Migrace štítků -migrate.migrating_releases=Migrace vydání -migrate.migrating_issues=Migrace problémů -migrate.migrating_pulls=Migrace žádostí o sloučení -migrate.cancel_migrating_title=Zrušit migraci -migrate.cancel_migrating_confirm=Chcete zrušit tuto migraci? - mirror_from=zrcadlo forked_from=forknuto z generated_from=generováno z @@ -2381,7 +2351,7 @@ settings.matrix.room_id=ID místnosti settings.matrix.message_type=Typ zprávy settings.archive.button=Archivovat repozitář settings.archive.header=Archivovat tento repozitář -settings.archive.text = Archivováním repozitáře jej celý převedete do stavu pouze pro čtení. Bude skryt z nástěnky. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat problémy a žádosti o sloučení. +settings.archive.text = Archivováním repozitáře jej celý převedete do stavu pouze pro čtení. Bude skryt z nástěnky. Nikdo (ani vy!) nebude moci vytvářet nové revize ani otevírat problémy a žádosti o sloučení. Je doporučena dokumentace důvodu archivace pro budoucí vývojáře, kteří chtějí vytvořit fork repozitáře. settings.archive.success=Repozitář byl úspěšně archivován. settings.archive.error=Nastala chyba při archivování repozitáře. Prohlédněte si záznam pro více detailů. settings.archive.error_ismirror=Nemůžete archivovat zrcadlený repozitář. @@ -2647,12 +2617,6 @@ pulls.ready_for_review = Připraveni na posouzení? settings.rename_branch_failed_protected = Nepodařilo se přejmenovat větev %s, jelikož se jedná o chráněnou větev. editor.push_out_of_date = Push je nejspíše zastaralý. stars = Oblíbení -n_commit_one = %s revize -n_commit_few = %s revizí -n_branch_one = %s větev -n_tag_one = %s značka -n_tag_few = %s značek -n_branch_few = %s větví settings.event_pull_request_enforcement = Vynucení settings.enforce_on_admins = Vynutit toto pravidlo pro správce repozitáře settings.enforce_on_admins_desc = Správci repozitáře nemohou obejít toto pravidlo. @@ -2676,8 +2640,6 @@ settings.transfer.button = Převést vlastnictví settings.transfer.modal.title = Převést vlastnictví wiki.search = Hledat na wiki wiki.no_search_results = Žádné výsledky -n_release_one = %s vydání -n_release_few = %s vydání settings.federation_settings = Nastavení federace settings.federation_apapiurl = Adresa URL federace tohoto repozitáře. Zkopírujte a vložte tuto adresu do nastavení federace jiného repozitáře jako adresu sledovaného repozitáře. settings.federation_not_enabled = Na vaší instanci není dostupná federace. @@ -2961,34 +2923,6 @@ dashboard.reinit_missing_repos=Znovu inicializovat všechny chybějící repozit dashboard.sync_external_users=Synchronizovat externí uživatelská data dashboard.cleanup_hook_task_table=Vyčistit tabulku hook_task dashboard.cleanup_packages=Vyčistit prošlé balíčky -dashboard.server_uptime=Doba provozu serveru -dashboard.current_goroutine=Aktuální goroutines -dashboard.current_memory_usage=Aktuální využití paměti -dashboard.total_memory_allocated=Celková přidělená paměť -dashboard.memory_obtained=Získaná paměť -dashboard.pointer_lookup_times=Časy vyhledávání ukazatelů -dashboard.memory_allocate_times=Alokace paměti -dashboard.memory_free_times=Uvolnění paměti -dashboard.current_heap_usage=Aktuální využití paměti zásobníku -dashboard.heap_memory_obtained=Získaná paměť zásobníku -dashboard.heap_memory_idle=Nečinná paměť zásobníku -dashboard.heap_memory_in_use=Používaná paměť zásobníku -dashboard.heap_memory_released=Uvolněná paměť zásobníku -dashboard.heap_objects=Objekty zásobníku -dashboard.bootstrap_stack_usage=Využití zásobníku prvotního zavedení -dashboard.stack_memory_obtained=Celková získaná pamět zásobníku -dashboard.mspan_structures_usage=Užití struktur MSpan -dashboard.mspan_structures_obtained=Získané struktury MSpan -dashboard.mcache_structures_usage=Využití struktur MCache -dashboard.mcache_structures_obtained=Získané struktury MCache -dashboard.profiling_bucket_hash_table_obtained=Získaná profilovací bucket hash tabulka -dashboard.gc_metadata_obtained=Získaná metadata GC -dashboard.other_system_allocation_obtained=Získaná alokace ostatních systémových prostředků -dashboard.next_gc_recycle=Příští recyklace GC -dashboard.last_gc_time=Doba od posledního GC -dashboard.total_gc_pause=Celková pauza GC -dashboard.last_gc_pause=Poslední pauza GC -dashboard.gc_times=Časy GC dashboard.delete_old_actions=Odstranit všechny staré aktivity z databáze dashboard.delete_old_actions.started=Spuštěno odstraňování všech starých aktivit z databáze. dashboard.update_checker=Kontrola aktualizací @@ -3045,18 +2979,6 @@ users.purge_help=Vynuceně odstranit uživatele a všechny repositáře, organiz users.still_own_packages=Tento uživatel stále vlastní jeden nebo více balíčků, nejprve odstraňte tyto balíčky. users.deletion_success=Uživatelský účet byl smazán. users.reset_2fa=Resetovat 2FA -users.list_status_filter.menu_text=Filtr -users.list_status_filter.reset=Obnovit -users.list_status_filter.is_active=Aktivní -users.list_status_filter.not_active=Neaktivní -users.list_status_filter.is_admin=Administrátor -users.list_status_filter.not_admin=Není administrátor -users.list_status_filter.is_restricted=Omezeno -users.list_status_filter.not_restricted=Není omezen -users.list_status_filter.is_prohibit_login=Zakázat přihlášení -users.list_status_filter.not_prohibit_login=Povolit přihlášení -users.list_status_filter.is_2fa_enabled=2FA povoleno -users.list_status_filter.not_2fa_enabled=2FA zakázáno users.details=Podrobnosti o uživateli emails.email_manage_panel=Správa uživatelských e-mailů @@ -3370,24 +3292,6 @@ monitor.process.cancel_desc=Zrušení procesu může způsobit ztrátu dat monitor.process.cancel_notices=Zrušit: %s? monitor.process.children=Potomek -monitor.queues=Fronty -monitor.queue=Fronta: %s -monitor.queue.name=Název -monitor.queue.type=Typ -monitor.queue.exemplar=Typ vzoru -monitor.queue.numberworkers=Počet workerů -monitor.queue.maxnumberworkers=Maximální počet workerů -monitor.queue.numberinqueue=Číslo ve frontě -monitor.queue.review_add=Posoudit / přidat workery -monitor.queue.settings.title=Nastavení poolu -monitor.queue.settings.maxnumberworkers=Maximální počet workerů -monitor.queue.settings.maxnumberworkers.placeholder=V současné době %[1]d -monitor.queue.settings.maxnumberworkers.error=Maximální počet workerů musí být číslo -monitor.queue.settings.submit=Upravit nastavení -monitor.queue.settings.changed=Nastavení upravena -monitor.queue.settings.remove_all_items=Odstranit vše -monitor.queue.settings.remove_all_items_done=Všechny položky ve frontě byly odstraněny. - notices.system_notice_list=Systémová oznámení notices.view_detail_header=Podrobnosti oznámení notices.operations=Operace @@ -3403,7 +3307,6 @@ notices.desc=Popis notices.op=Op. notices.delete_success=Systémové upozornění bylo smazáno. dashboard.sync_repo_branches = Synchronizovat vynechané větve z dat Gitu do databáze -monitor.queue.activeworkers = Aktivní workery defaulthooks.desc = Webhooky automaticky vytvářejí žádosti HTTP POST na server, kde se spustí určité události Forgejo. Webhooky zde definované jsou výchozí a budou zkopírovány do všech nových repozitářů. Více informací zjistíte v návodu webhooků. systemhooks.desc = Webhooky automaticky vytvářejí žádosti HTTP POST na server, kde se spustí určité události Forgejo. Webhooky zde definované budou aktivní u všech repozitářů v systému, zvažte tedy prosím všechny vlivy na výkon, které může tato funkce způsobit. Více informací zjistíte v návodu webhooků. assets = Assety kódu @@ -3418,8 +3321,6 @@ self_check.database_fix_mysql=Pro uživatele MySQL/MariaDB můžete použít př self_check = Vlastní kontrola self_check.database_collation_case_insensitive=Databáze používá collation %s, což je collation nerozlišující velká a malá písmena. Ačkoli s ní Forgejo může pracovat, mohou se vyskytnout vzácné případy, kdy nebude fungovat podle očekávání. auths.oauth2_map_group_to_team = Zmapovat zabrané skupiny u týmů organizací (volitelné - vyžaduje název claimu výše) -monitor.queue.settings.desc = Pooly dynamicky rostou podle blokování fronty jejich workerů. - auths.tips.gmail_settings = Nastavení služby Gmail: config_summary = Souhrn config.open_with_editor_app_help = Editory v nabídce „Otevřít pomocí“ v nabídce klonování. Ponechte prázdné pro použití výchozího editoru (zobrazíte jej rozšířením). @@ -3497,35 +3398,10 @@ raw_seconds=sekund raw_minutes=minut [dropzone] -default_message=Přetáhněte soubory nebo klikněte sem pro nahrání. -invalid_input_type=Nemůžete nahrávat soubory tohoto typu. -file_too_big=Velikost souboru ({{filesize}} MB) je vyšší než maximální velikost ({{maxFilesize}} MB). -remove_file=Smazat soubor [notification] -notifications=Oznámení -unread=Nepřečtené -read=Přečtené -no_unread=Žádná nepřečtená oznámení. -no_read=Žádná přečtená oznámení. -pin=Připnout upozornění -mark_as_read=Označit jako přečtené -mark_as_unread=Označit jako nepřečtené -mark_all_as_read=Označit vše jako přečtené -subscriptions=Odběry -watching=Sledované -no_subscriptions=Žádné odběry [gpg] -default_key=Podepsáno výchozím klíčem -error.extract_sign=Selhalo získání podpisu -error.generate_hash=Selhalo vygenerování hashe revize -error.no_committer_account=Žádný účet není propojen s e-mailovou adresou přispěvatele -error.no_gpg_keys_found=V databázi nebyl nalezen žádný známý klíč pro tento podpis -error.not_signed_commit=Nepodepsaná revize -error.failed_retrieval_gpg_keys=Nepodařilo se získat žádný klíč propojený s účtem přispěvatele -error.probable_bad_signature=VAROVÁNÍ! Přestože v databázi existuje klíč s tímto ID, tato revize jím není ověřena! Tato revize je PODEZŘELÁ. -error.probable_bad_default_signature=VAROVÁNÍ! Přestože má výchozí klíč toto ID, tato revize jím není ověřena! Tato revize je PODEZŘELÁ. [units] unit=Jednotka @@ -3533,183 +3409,11 @@ error.no_unit_allowed_repo=Nejste oprávněni přistupovat k žádné části to error.unit_not_allowed=Nejste oprávněni přistupovat k této části repozitáře. [packages] -title=Balíčky desc=Správa balíčků repozitáře. -empty=Zatím zde nejsou žádné balíčky. -empty.documentation=Další informace o registru balíčků naleznete v dokumentaci. -empty.repo=Nahráli jste balíček, ale nezobrazil se zde? Přejděte na nastavení balíčku a propojte jej s tímto repozitářem. -registry.documentation=Další informace o registru %s naleznete v dokumentaci. -filter.type=Typ -filter.type.all=Vše -filter.no_result=Váš filtr nepřinesl žádné výsledky. -filter.container.tagged=Označeno -filter.container.untagged=Neoznačeno -published_by=Zveřejněno %[1]s od %[3]s -published_by_in=Zveřejněno %[1]s od %[3]s v %[5]s -installation=Instalace -about=O tomto balíčku -requirements=Požadavky -dependencies=Závislosti -keywords=Klíčová slova -details=Podrobnosti -details.author=Autor -details.project_site=Web projektu -details.repository_site=Web repositáře -details.documentation_site=Web dokumentace -details.license=Licence -assets=Prostředky -versions=Verze -versions.view_all=Zobrazit všechny -dependency.id=ID -dependency.version=Verze -alpine.registry=Nastavte tento registr přidáním URL do /etc/apk/repositories: -alpine.registry.key=Stáhněte si veřejný RSA klíč registru do složky /etc/apk/keys/ pro ověření podpisu indexu: -alpine.registry.info=Vyberte $branch a $repository ze seznamu níže. -alpine.install=Pro instalaci balíčku spusťte následující příkaz: -alpine.repository=Informace o repozitáři -alpine.repository.branches=Větve -alpine.repository.repositories=Repozitáře -alpine.repository.architectures=Architektury -cargo.registry=Nastavte tento registr v konfiguračním souboru Cargo (například ~/.cargo/config.toml): -cargo.install=Chcete-li nainstalovat balíček pomocí Cargo, spusťte následující příkaz: -chef.registry=Nastavit tento registr v souboru ~/.chef/config.rb: -chef.install=Pro instalaci balíčku spusťte následující příkaz: -composer.registry=Nastavit tento registr v souboru ~/.composer/config.json: -composer.install=Pro instalaci balíčku pomocí Compposer spusťte následující příkaz: -composer.dependencies=Závislosti -composer.dependencies.development=Vývojové závislosti conan.details.repository=Repozitář -conan.registry=Nastavte tento registr z příkazového řádku: -conan.install=Pro instalaci balíčku pomocí Conan spusťte následující příkaz: -conda.registry=Nastavte tento registr jako Conda repozitář ve vašem .condarc: -conda.install=Pro instalaci balíčku pomocí Conda spusťte následující příkaz: -container.details.type=Typ obrazu -container.details.platform=Platforma -container.pull=Stáhnout obraz z příkazové řádky: -container.digest=Výběr -container.multi_arch=OS/architektura -container.layers=Vrstvy obrazu -container.labels=Štítky -container.labels.key=Klíč -container.labels.value=Hodnota -cran.registry=Nastavte tento registr v souboru Rprofile.site: -cran.install=Pro instalaci balíčku spusťte následující příkaz: -debian.registry=Nastavte tento registr z příkazového řádku: -debian.registry.info=Vyberte $distribution a $component ze seznamu níže. -debian.install=Pro instalaci balíčku spusťte následující příkaz: -debian.repository=Informace o repozitáři -debian.repository.distributions=Distribuce -debian.repository.components=Komponenty -debian.repository.architectures=Architektury -generic.download=Stáhnout balíček z příkazové řádky: -go.install=Nainstalovat balíček z příkazové řádky: -helm.registry=Nastavte tento registr z příkazového řádku: -helm.install=Pro instalaci balíčku spusťte následující příkaz: -maven.registry=Nastavte tento registr ve vašem projektu pom.xml souboru: -maven.install=Pro použití balíčku uveďte následující v bloku dependencies v souboru pom.xml: -maven.install2=Spustit pomocí příkazové řádky: -maven.download=Chcete-li stáhnout závislost, spusťte přes příkazový řádek: -nuget.registry=Nastavte tento registr z příkazového řádku: -nuget.install=Chcete-li nainstalovat balíček pomocí NuGet, spusťte následující příkaz: -nuget.dependency.framework=Cílový Framework -npm.registry=Nastavte tento registr ve vašem projektu v souboru .npmrc: -npm.install=Pro instalaci balíčku pomocí npm spusťte následující příkaz: -npm.install2=nebo ho přidejte do souboru package.json: -npm.dependencies=Závislosti -npm.dependencies.development=Vývojové závislosti -npm.dependencies.peer=Vzájemné závislosti -npm.dependencies.optional=Volitelné závislosti -npm.details.tag=Značka -pub.install=Chcete-li nainstalovat balíček pomocí Dart, spusťte následující příkaz: -pypi.requires=Vyžaduje Python -pypi.install=Pro instalaci balíčku pomocí pip spusťte následující příkaz: -rpm.registry=Nastavte tento registr z příkazového řádku: -rpm.distros.redhat=na distribuce založené na RedHat -rpm.distros.suse=na distribuce založené na SUSE -rpm.install=Pro instalaci balíčku spusťte následující příkaz: -rpm.repository=Informace o repozitáři -rpm.repository.architectures=Architektury -rpm.repository.multiple_groups = Tento balíček je dostupný v několika skupinách. -rubygems.install=Pro instalaci balíčku pomocí gem spusťte následující příkaz: -rubygems.install2=nebo ho přidejte do Gemfie: -rubygems.dependencies.runtime=Běhové závislosti -rubygems.dependencies.development=Vývojové závislosti -rubygems.required.ruby=Vyžaduje verzi Ruby -rubygems.required.rubygems=Vyžaduje verzi RubyGem -swift.registry=Nastavte tento registr z příkazového řádku: -swift.install=Přidejte balíček do svého Package.swift souboru: -swift.install2=a spustit následující příkaz: -vagrant.install=Pro přidání Vagrant box spusťte následující příkaz: -settings.link=Propojit tento balíček s repozitářem -settings.link.description=Pokud propojíte balíček s repozitářem, je tento balíček uveden v seznamu balíčků repozitáře. -settings.link.select=Vybrat repozitář -settings.link.button=Aktualizovat odkaz na repozitář -settings.link.success=Odkaz na repozitář byl úspěšně aktualizován. -settings.link.error=Nepodařilo se aktualizovat odkaz na repozitář. -settings.delete=Odstranit balíček -settings.delete.description=Smazání balíčku je trvalé a nelze ho vrátit zpět. -settings.delete.notice=Chystáte se odstranit %s (%s). Tato operace je nevratná, jste si jisti? -settings.delete.success=Balíček byl odstraněn. -settings.delete.error=Nepodařilo se odstranit balíček. -owner.settings.cargo.title=Index registru Cargo -owner.settings.cargo.initialize=Inicializovat index -owner.settings.cargo.initialize.description=Pro použití registru Cargo je zapotřebí speciální index Git. Použití této možnosti (znovu) vytvoří repozitář a automaticky jej nastaví. -owner.settings.cargo.initialize.error=Nepodařilo se inicializovat Cargo index: %v -owner.settings.cargo.initialize.success=Index Cargo byl úspěšně vytvořen. -owner.settings.cargo.rebuild=Znovu vytvořit index -owner.settings.cargo.rebuild.error=Obnovení Cargo indexu se nezdařilo: %v -owner.settings.cargo.rebuild.success=Index Cargo byl úspěšně znovu sestaven. -owner.settings.cleanuprules.title=Pravidla čištění -owner.settings.cleanuprules.add=Přidat pravidlo pro čištění -owner.settings.cleanuprules.edit=Upravit pravidlo pro čištění -owner.settings.cleanuprules.none=Zatím nejsou k dispozici žádná pravidla čištění. -owner.settings.cleanuprules.preview=Náhled pravidla pro čištění -owner.settings.cleanuprules.preview.overview=%d balíčků má být odstraněno. -owner.settings.cleanuprules.preview.none=Pravidlo čištění neodpovídá žádným balíčkům. owner.settings.cleanuprules.enabled=Povolený -owner.settings.cleanuprules.pattern_full_match=Použít vzor na úplný název balíčku -owner.settings.cleanuprules.keep.title=Verze, které odpovídají těmto pravidlům, jsou zachovány, i když odpovídají níže uvedenému pravidlu pro odstranění. -owner.settings.cleanuprules.keep.count=Zachovat nejnovější owner.settings.cleanuprules.keep.count.1=1 verze na balíček owner.settings.cleanuprules.keep.count.n=%d verzí na balíček -owner.settings.cleanuprules.keep.pattern=Ponechat odpovídající verze -owner.settings.cleanuprules.keep.pattern.container=U balíčků Container je vždy zachována nejnovější verze. -owner.settings.cleanuprules.remove.title=Verze, které odpovídají těmto pravidlům, jsou odstraněny, pokud výše uvedené pravidlo neukládá jejich zachování. -owner.settings.cleanuprules.remove.days=Odstranit verze starší než -owner.settings.cleanuprules.remove.pattern=Odstranit odpovídající verze -owner.settings.cleanuprules.success.update=Pravidlo pro čištění bylo aktualizováno. -owner.settings.cleanuprules.success.delete=Pravidlo pro čištění bylo odstraněno. -owner.settings.chef.title=Registr Chef -owner.settings.chef.keypair=Generovat pár klíčů -owner.settings.chef.keypair.description=Žádosti odeslané do registru Chef musí být kryptograficky podepsané jako způsob ověření. Při generování páru klíčů je ve službě Forgejo uložen pouze veřejný klíč. Soukromý klíč je poskytnut vám, abyste jej mohli použít s programem knife. Vygenerováním nového páru klíčů přepíšete ten předchozí. -owner.settings.cargo.rebuild.description = Opětovné sestavení může být užitečné, pokud není index synchronizován s uloženými balíčky Cargo. -owner.settings.cargo.rebuild.no_index = Opětovné vytvoření selhalo, nebyl inicializován žádný index. -npm.dependencies.bundle = Přidružené závislosti -arch.pacman.helper.gpg = Přidat certifikát důvěryhodnosti do nástroje pacman: -arch.pacman.repo.multi = %s má stejnou verzi v různých distribucích. -arch.pacman.repo.multi.item = Nastavení pro %s -arch.pacman.conf = Přidejte server s odpovídající distribucí a architekturou do /etc/pacman.conf : -arch.pacman.sync = Synchronizace balíčku nástrojem pacman: -arch.version.properties = Vlastnosti verze -arch.version.description = Popis -arch.version.provides = Poskytuje -arch.version.groups = Skupina -arch.version.depends = Závislosti -arch.version.optdepends = Volitelné závislosti -arch.version.makedepends = Závislosti Make -arch.version.checkdepends = Závislosti Check -arch.version.conflicts = Konflikty -arch.version.replaces = Nahrazuje -arch.version.backup = Záloha -container.images.title = Obrázky -search_in_external_registry = Hledat v %s -alt.install = Instalovat balíček -alt.setup = Přidejte repozitář do seznamu připojených repozitářů (místo „_arch_“ zvolte potřebnou architekturu): -alt.repository = Informace o repozitáři -alt.repository.architectures = Architektury -alt.repository.multiple_groups = Tento balíček je dostupný v několika skupinách. -alt.registry = Nastavit tento registr z příkazové řádky: -alt.registry.install = Pro instalaci balíčku spusťte následující příkaz: [secrets] secrets=Tajné klíče @@ -3727,64 +3431,8 @@ deletion.failed=Nepodařilo se odstranit tajný klíč. management=Správa tajných klíčů [actions] -actions=Akce - unit.desc=Spravovat integrované pipeliny CI/CD pomocí funkce Forgejo Actions. -status.unknown=Neznámý -status.waiting=Čekání -status.running=Probíhá -status.success=Úspěch -status.failure=Chyba -status.cancelled=Zrušeno -status.skipped=Přeskočeno -status.blocked=Blokováno - -runners.new=Vytvořit nový runner -runners.new_notice=Jak spustit runner -runners.status=Status -runners.id=ID -runners.name=Název -runners.owner_type=Typ -runners.description=Popis -runners.labels=Štítky -runners.last_online=Naposledy online -runners.runner_title=Runner -runners.task_list=Nedávné úlohy na tomto runneru -runners.task_list.no_tasks=Zatím zde nejsou žádné úlohy. -runners.task_list.run=Spustit -runners.task_list.status=Status -runners.task_list.repository=Repozitář -runners.task_list.commit=Revize -runners.task_list.done_at=Dokončeno v -runners.edit_runner=Upravit Runner -runners.update_runner=Aktualizovat změny -runners.update_runner_success=Runner byl úspěšně aktualizován -runners.update_runner_failed=Aktualizace runneru se nezdařila -runners.delete_runner=Odstranit tento runner -runners.delete_runner_success=Runner byl úspěšně odstraněn -runners.delete_runner_failed=Odstranění runneru selhalo -runners.delete_runner_header=Potvrdit odstranění tohoto runneru -runners.delete_runner_notice=Pokud na tomto runneru běží úloha, bude ukončena a označena jako neúspěšná. Může dojít k přerušení vytváření workflow. -runners.status.unspecified=Neznámý -runners.status.idle=Nečinný -runners.status.active=Aktivní -runners.status.offline=Offline -runners.version=Verze -runners.reset_registration_token=Resetovat registrační token -runners.reset_registration_token_success=Registrační token runneru byl úspěšně obnoven - -runs.all_workflows=Všechny workflowy -runs.commit=Revize -runs.scheduled=Naplánováno -runs.invalid_workflow_helper=Konfigurační soubor pracovního postupu je neplatný. Zkontrolujte prosím konfigurační soubor: %s -runs.no_matching_online_runner_helper=Žádný odpovídající online runner s popiskem: %s -runs.actor=Aktér -runs.status=Status -runs.actors_no_select=Všichni aktéři -runs.status_no_select=Všechny stavy -runs.no_results=Nebyly nalezeny žádné výsledky. -runs.no_workflows=Zatím nebyly vytvořeny žádné pracovní postupy. runs.no_runs=Pracovní postup zatím nebyl spuštěn. runs.empty_commit_message=(prázdná zpráva revize) @@ -3795,29 +3443,8 @@ workflow.enable_success=Workflow „%s“ byl úspěšně aktivován. workflow.disabled=Workflow je zakázán. -variables=Proměnné -variables.management=Správa proměnných -variables.creation=Přidat proměnnou -variables.none=Zatím zde nejsou žádné proměnné. -variables.deletion=Odstranit proměnnou -variables.deletion.description=Odstranění proměnné je trvalé a nelze jej vrátit zpět. Pokračovat? -variables.description=Proměnné budou předány určitým akcím a nelze je přečíst jinak. variables.id_not_exist = Proměnná s id %d neexistuje. -variables.edit=Upravit proměnnou -variables.deletion.failed=Nepodařilo se odstranit proměnnou. -variables.deletion.success=Proměnná byla odstraněna. -variables.creation.failed=Přidání proměnné se nezdařilo. -variables.creation.success=Proměnná „%s“ byla přidána. -variables.update.failed=Úprava proměnné se nezdařila. -variables.update.success=Proměnná byla upravena. -runners.none = Nejsou dostupné žádné runnery -runs.workflow = Workflow -runners = Runnery -runs.pushed_by = pushnuta uživatelem need_approval_desc = Potřebovat schválení pro spouštění workflowů pro žádosti o sloučení forků. -runners.runner_manage_panel = Správa runnerů -runs.no_job_without_needs = Workflow musí obsahovat alespoň jednu práci bez závislostí. -runs.no_job = Workflow musí obsahovat alespoň jednu úlohu workflow.dispatch.use_from = Použít workflow z workflow.dispatch.run = Spustit workflow workflow.dispatch.input_required = Vyžadovaná hodnota pro vstup „%s“. @@ -3828,7 +3455,6 @@ workflow.dispatch.success = Žádost o spuštění workflow byla úspěšně ode runs.expire_log_message = Protokoly byly smazány, protože byly příliš staré. runs.no_workflows.help_no_write_access = Pro více informací o Forgejo Actions se podívejte do dokumentace. runs.no_workflows.help_write_access = Nevíte, jak začít s Forgejo Actions? Podívejte se na rychlý začátek v uživatelské dokumentaci pro vytvoření vašeho prvního workflow. Poté nastavte runner Forgejo pro provádění vašich úloh. -variables.not_found = Nepodařilo se najít proměnnou. [projects] type-1.display_name=Samostatný projekt @@ -3874,19 +3500,8 @@ regexp = RegExp regexp_tooltip = Interpretovat hledaný výraz jako regulární výraz [markup] -filepreview.lines = Řádky %[1]d až %[2]d v souboru %[3]s -filepreview.line = Řádek %[1]d v souboru %[2]s -filepreview.truncated = Náhled byl zkrácen [munits.data] -b = B -kib = KiB -mib = MiB -gib = GiB -tib = TiB -pib = PiB -eib = EiB - [translation_meta] test = diky vsem za pomoc :) diff --git a/options/locale/locale_da.ini b/options/locale/locale_da.ini index f2d4852f55..6efa9bcec3 100644 --- a/options/locale/locale_da.ini +++ b/options/locale/locale_da.ini @@ -35,14 +35,6 @@ re_type = Bekræft adgangskode captcha = CAPTCHA twofa = To-faktor autentificering twofa_scratch = To-faktor skrabekode -webauthn_sign_in = Tryk på knappen på din sikkerhedsnøgle. Hvis din sikkerhedsnøgle ikke har nogen knap, skal du indsætte den igen. -webauthn_use_twofa = Brug en tofaktorkode fra din telefon -webauthn_error = Kunne ikke læse din sikkerhedsnøgle. -webauthn_unsupported_browser = Din browser understøtter i øjeblikket ikke WebAuthn. -webauthn_error_unknown = Der opstod en ukendt fejl. Prøv venligst igen. -webauthn_error_unable_to_process = Serveren kunne ikke behandle din anmodning. -webauthn_error_duplicated = Sikkerhedsnøglen er ikke tilladt for denne anmodning. Sørg for, at nøglen ikke allerede er registreret. -webauthn_error_empty = Du skal angive et navn for denne nøgle. organization = Organisation mirror = Mirror new_mirror = Ny mirror @@ -112,11 +104,7 @@ show_timestamps = Vis tidsstempler show_log_seconds = Vis sekunder tracked_time_summary = Opsummering af sporet tid baseret på filtre af problemliste signed_in_as = Logget ind som -webauthn_error_insecure = WebAuthn understøtter kun sikre forbindelser. Til test over HTTP kan du bruge oprindelsen "localhost" eller "127.0.0.1" invalid_data = Ugyldige data: %v -webauthn_insert_key = Indsæt din sikkerhedsnøgle -webauthn_press_button = Tryk venligst på knappen på din sikkerhedsnøgle… -webauthn_error_timeout = Timeout nået, før din nøgle kunne læses. Genindlæs denne side og prøv igen. enabled = Aktiveret locked = Låst copy_hash = Kopiér hash @@ -1134,14 +1122,6 @@ migrate_options_mirror_helper = Dette depot vil være et spejl migrate_options_lfs = Migrer LFS-filer migrate_options_lfs_endpoint.label = LFS endepunkt migrate_options_lfs_endpoint.description.local = En lokal serversti understøttes også. -migrate_items = Migration Elementer -migrate_items_wiki = Wiki -migrate_items_milestones = Milepæle -migrate_items_labels = Etiketter -migrate_items_issues = Problemmer -migrate_items_pullrequests = Pull-anmodninger -migrate_items_merge_requests = Flet anmodninger -migrate_items_releases = Udgivelser migrate_repo = Migrer depot migrate.clone_address_desc = HTTP(S) eller Git "klone" URL'en for et eksisterende depot migrate.clone_local_path = eller en lokal serversti @@ -1157,14 +1137,6 @@ migrate.migrating = Migrerer fra %s … migrate.migrating_failed = Migrering fra %s mislykkedes. migrate.migrating_failed.error = Kunne ikke migrere: %s migrate.migrating_failed_no_addr = Migration mislykkedes. -migrate.migrating_git = Migrering af Git-data -migrate.migrating_topics = Migrering af emner -migrate.migrating_milestones = Migrerende milepæle -migrate.migrating_labels = Migrering af etiketter -migrate.migrating_releases = Migrering af udgivelser -migrate.migrating_issues = Migrering af problemer -migrate.migrating_pulls = Migrering af pull-anmodninger -migrate.cancel_migrating_title = Annuller migrering mirror_from = spejl af forked_from = forked fra generated_from = genereret fra @@ -1175,7 +1147,6 @@ migrate_options_lfs_endpoint.description = Migration vil forsøge at bruge din f form.name_pattern_not_allowed = Mønsteret "%s" er ikke tilladt i et depotnavn. migrate_options_lfs_endpoint.placeholder = Hvis det efterlades tomt, vil endepunktet blive afledt fra klonens URL migrate.clone_address = Migrer / Klon fra URL -migrate.cancel_migrating_confirm = Vil du annullere denne migrering? more_operations = Flere operationer new_from_template = Brug en skabelon new_from_template_description = Du kan vælge en eksisterende depotskabelon på denne instans og anvende dens indstillinger. @@ -1269,15 +1240,10 @@ packages = Pakker actions = Handlinger labels = Etiketter milestones = Milepæle -n_commit_few = %s commits -n_branch_one = %s gren -n_branch_few = %s grene org_labels_desc_manage = Styr commits = Commits commit = Commit org_labels_desc = Etiketter på organisationsniveau, der kan bruges med alle depoter under denne organisation -n_commit_one = %s commit -n_release_few = %s udgivelser released_this = udgivet dette file.title = %s ved %s file_raw = Rå @@ -1287,9 +1253,6 @@ editor.fail_to_update_file_summary = Fejlmeddelelse: editor.push_rejected_summary = Fuldstændig afvisningsmeddelelse: editor.add_subdir = Tilføj en mappe… editor.unable_to_upload_files = Kunne ikke uploade filer til "%s" med fejl: %v -n_tag_one = %s tag -n_tag_few = %s tags -n_release_one = %s udgivelse file_history = Historie file_view_source = Se kilde file_view_rendered = Vis gengivet @@ -1506,7 +1469,7 @@ issues.ref_closing_from = `henviste til dette problem fra en pul issues.author.tooltip.issue = Denne bruger er forfatteren til dette problem. issues.author.tooltip.pr = Denne bruger er forfatteren af denne pull-anmodning. issues.role.owner = Ejer -issues.role.owner_helper = Denne bruger er ejeren af dette depot. +issues.role.owner_helper = Denne bruger er en ejer af dette depot. issues.role.member = Medlem issues.filter_label = Etiket issues.filter_label_no_select = Alle etiketter @@ -2701,18 +2664,6 @@ settings.event_action_recover = Gendan issues.filter_type.all_pull_requests = Alle pull-anmodninger [notification] -watching = Overvåger -read = Læs -notifications = Notifikationer -no_unread = Ingen ulæste notifikationer. -unread = Ulæst -mark_as_read = Markér som læst -no_read = Ingen læste notifikationer. -mark_all_as_read = Markér alle som læste -mark_as_unread = Markér som ulæst -subscriptions = Abonnementer -no_subscriptions = Ingen abonnementer -pin = Fastgør notifikation [action] watched_repo = begyndte at overvåge %[2]s @@ -2891,7 +2842,6 @@ config.db_name = Navn users.full_name = Fulde navn users.activated = Aktiveret repos.name = Navn -monitor.queue.name = Navn repos.private = Privat config.default_enable_timetracking = Aktiver tidsregistrering som standard config.enable_timetracking = Aktiver tidsregistrering @@ -2900,11 +2850,6 @@ config.allow_dots_in_usernames = Tillad brugere at bruge prikker i deres brugern auths.oauth2_icon_url = Icon URL users.edit = Redigere users.auth_source = Godkendelseskilde -monitor.queue.settings.maxnumberworkers.placeholder = I øjeblikket %[1]d -monitor.queue.settings.submit = Opdater indstillinger -monitor.queue.settings.changed = Indstillinger opdateret -monitor.queue.settings.remove_all_items = Slet alle -monitor.queue.settings.remove_all_items_done = Alle varer i køen er blevet fjernet. notices.system_notice_list = Systemmeddelelser dashboard.delete_repo_archives = Slet alle depoters arkiver (ZIP, TAR.GZ osv..) organizations = Organisationer @@ -2935,8 +2880,6 @@ dashboard.clean_unbind_oauth = Rens ubundne OAuth-forbindelser dashboard.delete_inactive_accounts.started = Slet alle uaktiverede konti opgave startet. dashboard.delete_missing_repos = Slet alle depoter, der mangler deres Git-filer dashboard.update_migration_poster_id = Opdater migrationsplakat-id'er -dashboard.memory_obtained = Hukommelse opnået -dashboard.pointer_lookup_times = Pointer-opslagstider hooks = Webhooks dashboard.cron.finished = Cron: %[1]s er færdig dashboard.delete_inactive_accounts = Slet alle uaktiverede konti @@ -2945,10 +2888,6 @@ notices = Systemmeddelelser config_summary = Oversigt dashboard.system_status = System status dashboard.update_mirrors = Opdater spejle -dashboard.server_uptime = Server oppetid -dashboard.current_goroutine = Nuværende goroutiner -dashboard.current_memory_usage = Aktuel hukommelsesbrug -dashboard.total_memory_allocated = Samlet hukommelse tildelt integrations = Integrationer dashboard.operations = Vedligeholdelses operationer dashboard.repo_health_check = Sundhedstjek alle depoter @@ -2984,16 +2923,6 @@ users.update_profile = Opdater brugerkonto users.still_has_org = Denne bruger er medlem af en organisation. Fjern først brugeren fra enhver organisation. users.purge_help = Tvangsslet brugeren og eventuelle depoter, organisationer og pakker, der ejes af brugeren. Alle kommentarer og problemer indsendt af denne bruger vil også blive slettet. users.is_admin = Administrator konto -dashboard.mspan_structures_obtained = Mspan strukturer opnået -dashboard.mcache_structures_usage = MCache strukturer brug -dashboard.mspan_structures_usage = MSpan strukturer brug -dashboard.mcache_structures_obtained = MCache-strukturer opnået -dashboard.profiling_bucket_hash_table_obtained = Profilering bucket hash tabel opnået -dashboard.gc_metadata_obtained = GC-metadata opnået -dashboard.other_system_allocation_obtained = Anden systemallokering opnået -dashboard.next_gc_recycle = Næste GC genbrug -dashboard.total_gc_pause = Total GC-pause -dashboard.last_gc_time = Tid siden sidste GC dashboard.delete_old_system_notices = Slet alle gamle systemmeddelelser fra databasen dashboard.gc_lfs = Affaldssamler LFS-metaobjekter dashboard.start_schedule_tasks = Start planlæg handlingsopgaver @@ -3005,12 +2934,6 @@ users.created = Oprettet users.max_repo_creation = Maksimalt antal depoter users.prohibit_login = Suspenderet konto users.is_restricted = Begrænset konto -dashboard.memory_allocate_times = Hukommelsestildelinger -dashboard.memory_free_times = Hukommelses frigørelse -dashboard.current_heap_usage = Nuværende heap-brug -dashboard.heap_memory_obtained = Heap-hukommelse opnået -dashboard.heap_objects = Heap genstande -dashboard.bootstrap_stack_usage = Brug af bootstrap-stak dashboard.update_checker = Opdateringskontrol dashboard.delete_old_actions.started = Slet alle gamle aktiviteter fra den påbegyndte database. dashboard.stop_zombie_tasks = Stop zombiehandlingsopgaver @@ -3024,19 +2947,10 @@ users.organization_creation.description = Tillad oprettelse af nye organisatione users.delete_account = Slet brugerkonto users.cannot_delete_self = Du kan ikke slette dig selv users.still_own_repo = Denne bruger ejer stadig et eller flere arkiver. Slet eller overfør disse depoter først. -users.list_status_filter.is_admin = Admin users.block.description = Bloker denne bruger i at interagere med denne tjeneste via deres konto, og forbyd at logge ind. users.restricted.description = Tillad kun interaktion med de depoter og organisationer, hvor denne bruger er tilføjet som en samarbejdspartner. Dette forhindrer adgang til offentlige arkiver i denne instans. -users.list_status_filter.menu_text = Filter -dashboard.heap_memory_idle = Heap hukommelse inaktiv -dashboard.heap_memory_in_use = Heap hukommelse i brug -dashboard.heap_memory_released = Heap-hukommelse frigivet -dashboard.stack_memory_obtained = Stakhukommelse opnået -dashboard.last_gc_pause = Sidste GC-pause -dashboard.gc_times = GC times dashboard.delete_old_actions = Slet alle gamle aktiviteter fra databasen users.allow_create_organization = Kan skabe organisationer -users.list_status_filter.not_admin = Ikke admin users.allow_import_local = Kan importere lokale depoter users.send_register_notify = Giv besked om tilmelding via e-mail users.local = Lokal @@ -3051,9 +2965,6 @@ users.max_repo_creation_desc = (Indtast -1 for at bruge den globale standardgræ users.is_activated = Aktiveret konto users.edit_account = Rediger brugerkonto packages.version = Version -users.list_status_filter.reset = Nulstil -users.list_status_filter.is_active = Aktiv -users.list_status_filter.not_active = Inaktiv users.purge = Udrens bruger users.user_manage_panel = Administrer brugerkonti users.new_account = Opret brugerkonto @@ -3067,25 +2978,19 @@ emails.not_updated = Kunne ikke opdatere den anmodede e-mailadresse: %v packages.package_manage_panel = Administrer pakker packages.total_size = Samlet størrelse: %s packages.unreferenced_size = Ikke-referencestørrelse: %s -users.list_status_filter.is_2fa_enabled = 2FA aktiveret -users.list_status_filter.not_2fa_enabled = 2FA deaktiveret emails.filter_sort.email_reverse = E-mail (omvendt) emails.filter_sort.name_reverse = Brugernavn (omvendt) emails.delete = Slet e-mail emails.delete_desc = Er du sikker på, at du vil slette denne e-mailadresse? emails.deletion_success = E-mailadressen er blevet slettet. -users.list_status_filter.is_restricted = Begrænset emails.duplicate_active = Denne e-mailadresse er allerede aktiv for en anden bruger. emails.change_email_header = Opdater e-mail-egenskaber emails.change_email_text = Er du sikker på, at du vil opdatere denne e-mailadresse? repos.issues = Problemer repos.size = Størrelse repos.lfs_size = LFS størrelse -users.list_status_filter.not_restricted = Ikke begrænset users.details = Brugeroplysninger emails.email_manage_panel = Administrer bruger-e-mails -users.list_status_filter.is_prohibit_login = Forbyd login -users.list_status_filter.not_prohibit_login = Tillad login emails.delete_primary_email_error = Du kan ikke slette den primære e-mail. orgs.org_manage_panel = Administrer organisationer repos.repo_manage_panel = Administrer depoter @@ -3150,8 +3055,6 @@ self_check.database_collation_case_insensitive = Databasen bruger en sortering % config.git_max_diff_line_characters = Maks. diff-tegn pr. linje config.access_log_template = Skabelon til adgangslog monitor.process.children = Børn -monitor.queues = Køer -monitor.queue.settings.desc = Puljer vokser dynamisk som reaktion på deres blokering af arbejderkø. auths.auth_manage_panel = Administrer godkendelseskilder auths.auth_type = Godkendelsestype auths.map_group_to_team_removal = Fjern brugere fra synkroniserede teams, hvis brugeren ikke tilhører den tilsvarende LDAP-gruppe @@ -3178,8 +3081,6 @@ monitor.stacktrace = Stakspor monitor.execute_time = Udførelsestid monitor.last_execution_result = Resultat monitor.process.cancel_notices = Annuller: %s? -monitor.queue.type = Type -monitor.queue.exemplar = Eksempler type auths.auth_name = Godkendelsesnavn auths.attribute_username = Brugernavn attribut auths.attribute_username_placeholder = Lad stå tomt for at bruge brugernavnet indtastet i Forgejo. @@ -3222,13 +3123,11 @@ config.cache_test_slow = Cachetest lykkedes, men svaret er langsomt: %s. config.gc_interval_time = GC interval tid config.git_max_diff_files = Max diff filer vist config.git_gc_args = GC argumenter -monitor.queue.settings.maxnumberworkers.error = Max antal arbejdere skal være et tal notices.inverse_selection = Omvendt valg notices.delete_selected = Slet valgte auths.allowed_domains_helper = Lad være tomt for at tillade alle domæner. Adskil flere domæner med et komma (","). auths.skip_local_two_fa = Spring over lokal 2FA auths.oauth2_required_claim_value_helper = Indstil denne værdi for at begrænse login fra denne kilde til brugere med et krav med dette navn og denne værdi -monitor.queue.settings.maxnumberworkers = Max antal arbejdere notices.delete_all = Slet alle meddelelser auths.smtp_auth = SMTP-godkendelsestype config.default_visibility_organization = Standardsynlighed for nye organisationer @@ -3327,7 +3226,6 @@ auths.attributes_in_bind = Hent attributter i bind DN-kontekst config.app_name = Instans titel config.app_slogan = instans slogan config.cache_interval = Cache interval -monitor.queue.settings.title = Pool indstillinger notices.type = Type notices.type_2 = Opgave config.db_schema = Skematisk @@ -3346,12 +3244,6 @@ monitor.execute_times = Udførelser monitor.download_diagnosis_report = Hent diagnoserapport monitor.process.cancel = Annuller processen monitor.process.cancel_desc = Annullering af en proces kan medføre tab af data -monitor.queue = Kø: %s -monitor.queue.numberworkers = Antal arbejdere -monitor.queue.activeworkers = Aktive arbejdere -monitor.queue.maxnumberworkers = Max antal arbejdere -monitor.queue.numberinqueue = Nummer i kø -monitor.queue.review_add = Gennemgå / tilføj arbejdere config.git_pull_timeout = Pull Operation timeout config.git_clone_timeout = Klone Operation timeout config.git_gc_timeout = GC Operation timeout @@ -3377,235 +3269,17 @@ auths.restricted_filter = Begrænset filter auths.user_attribute_in_group = Brugerattribut angivet i gruppen [packages] -arch.version.description = Beskrivelse -container.labels = Etiketter -rubygems.dependencies.development = Udviklingsafhængigheder conan.details.repository = Depot -conan.registry = Konfigurer dette register fra kommandolinjen: -rubygems.dependencies.runtime = Kørselsafhængigheder -rubygems.install = For at installere pakken ved hjælp af gem skal du køre følgende kommando: -debian.repository = Depot info -npm.details.tag = Tag -chef.install = For at installere pakken skal du køre følgende kommando: -alpine.repository.architectures = Arkitekturer -composer.dependencies.development = Udviklingsafhængigheder -alt.repository.multiple_groups = Denne pakke er tilgængelig i flere grupper. owner.settings.cleanuprules.enabled = Aktiveret -helm.registry = Konfigurer dette register fra kommandolinjen: -alt.registry.install = For at installere pakken skal du køre følgende kommando: -helm.install = For at installere pakken skal du køre følgende kommando: -alt.repository.architectures = Arkitekturer -swift.registry = Konfigurer dette register fra kommandolinjen: -npm.dependencies.bundle = Samlede afhængigheder -debian.registry = Konfigurer dette register fra kommandolinjen: -cran.install = For at installere pakken skal du køre følgende kommando: -debian.install = For at installere pakken skal du køre følgende kommando: -pub.install = For at installere pakken ved hjælp af Dart skal du køre følgende kommando: -pypi.requires = Kræver Python -nuget.registry = Konfigurer dette register fra kommandolinjen: -debian.repository.distributions = Fordelinger -debian.repository.components = Komponenter -debian.repository.architectures = Arkitekturer -rpm.repository.multiple_groups = Denne pakke er tilgængelig i flere grupper. -rubygems.install2 = eller føj det til Gemfilen: -npm.dependencies.development = Udviklingsafhængigheder -npm.dependencies.peer = Peer-afhængigheder -npm.dependencies.optional = Valgfrie afhængigheder -rpm.registry = Konfigurer dette register fra kommandolinjen: -rpm.install = For at installere pakken skal du køre følgende kommando: -alpine.install = For at installere pakken skal du køre følgende kommando: -alpine.repository = Depot info -pypi.install = For at installere pakken ved hjælp af pip skal du køre følgende kommando: -rpm.repository = Depot info -rpm.repository.architectures = Arkitekturer -alt.registry = Konfigurer dette register fra kommandolinjen: -alt.repository = Depot info -alpine.repository.repositories = Depoter -search_in_external_registry = Søg i %s -dependency.version = Version -alpine.registry = Konfigurer dette register ved at tilføje url'en i din /etc/apk/repositories fil: -alpine.registry.key = Download den offentlige RSA-nøgle til registreringsdatabasen i mappen /etc/apk/keys/ for at bekræfte indekssignaturen: -alpine.registry.info = Vælg $branch og $repository fra listen nedenfor. -empty = Der er ingen pakker endnu. -filter.type.all = Alle -filter.container.untagged = Umærket -about = Om denne pakke -filter.no_result = Dit filter gav ingen resultater. -dependencies = Afhængigheder -empty.documentation = For mere information om pakkeregistret, se dokumentationen. -filter.type = Type -registry.documentation = For mere information om %s registreringsdatabasen, se dokumentationen. -title = Pakker desc = Administrer depotpakker. -empty.repo = Har du uploadet en pakke, men den vises ikke her? Gå til pakkeindstillinger og link den til denne repo. -filter.container.tagged = Tagget -published_by = Udgivet %[1]s af %[3]s -published_by_in = Udgivet %[1]s af %[3]s i %[5]s -installation = Installation -requirements = Krav -cran.registry = Konfigurer dette register i din Rprofile.site fil: -rubygems.required.rubygems = Kræver RubyGem version -owner.settings.chef.title = Kokkeregister -owner.settings.chef.keypair = Generer nøglepar -arch.pacman.repo.multi.item = Konfiguration for %s -arch.pacman.sync = Synkroniser pakke med pacman: -arch.version.properties = Versionsegenskaber -arch.version.provides = Forsyner -arch.version.checkdepends = Check afhænger -arch.version.replaces = Erstatter -conan.install = For at installere pakken ved hjælp af Conan skal du køre følgende kommando: -conda.registry = Konfigurer dette register som et Conda-depot i din .condarc-fil: -conda.install = For at installere pakken ved hjælp af Conda skal du køre følgende kommando: -container.images.title = Billeder -container.details.type = Billedtype -container.details.platform = Platform -container.pull = Træk billedet fra kommandolinjen: -container.digest = Fordøje -alt.setup = Tilføj et depot til listen over tilsluttede arkiver (vælg den nødvendige arkitektur i stedet for "_arch_"): -vagrant.install = For at tilføje en Vagrant-boks skal du køre følgende kommando: -swift.install2 = og kør følgende kommando: -settings.link = Link denne pakke til et depot -settings.link.success = Depotlinket blev opdateret. -owner.settings.cargo.initialize.description = Et særligt indeks Git-depot er nødvendigt for at bruge Cargo-registret. Brug af denne mulighed vil (gen-)oprette depotet og konfigurere det automatisk. -settings.delete.notice = Du er ved at slette %s (%s). Denne operation er uigenkaldeligt, er du sikker? -owner.settings.cargo.title = Lastregisterindeks -owner.settings.cargo.initialize = Initialiser indeks -owner.settings.cleanuprules.preview.none = Oprydningsreglen matcher ikke nogen pakker. -owner.settings.cleanuprules.none = Der er endnu ingen oprydningsregler. owner.settings.cleanuprules.keep.count.1 = 1 version pr. pakke -owner.settings.cleanuprules.preview.overview = %d pakker er planlagt til at blive fjernet. -owner.settings.cleanuprules.keep.pattern.container = Den seneste version bevares altid for containerpakker. -settings.delete = Slet pakke -settings.delete.description = Sletning af en pakke er permanent og kan ikke fortrydes. owner.settings.cleanuprules.keep.count.n = %d versioner pr. pakke -arch.version.makedepends = Gør afhænger -alt.install = Installer pakken -composer.registry = Konfigurer dette register i din ~/.composer/config.json fil: -composer.dependencies = Afhængigheder -settings.delete.success = Pakken er blevet slettet. -settings.delete.error = Kunne ikke slette pakken. -owner.settings.cargo.rebuild.error = Kunne ikke genopbygge Cargo-indeks: %v -owner.settings.cargo.rebuild = Genopbyg indeks -owner.settings.cleanuprules.preview = Forhåndsvisning af oprydningsregel -owner.settings.cleanuprules.keep.count = Behold den nyeste -owner.settings.cleanuprules.keep.pattern = Hold versionerne matchende -owner.settings.chef.keypair.description = Anmodninger sendt til Chef-registret skal være kryptografisk signeret som et middel til godkendelse. Når et nøglepar genereres, gemmes kun den offentlige nøgle på Forgejo. Den private nøgle gives til dig til brug med Knife. Generering af et nyt nøglepar vil overskrive det forrige. -maven.install = For at bruge pakken skal du inkludere følgende i blokken afhængigheder i filen pom.xml: -details = Detaljer -cargo.registry = Konfigurer dette register i Cargo-konfigurationsfilen (for eksempel ~/.cargo/config.toml): -cargo.install = For at installere pakken ved hjælp af Cargo skal du køre følgende kommando: -composer.install = For at installere pakken ved hjælp af Composer skal du køre følgende kommando: -container.multi_arch = OS / Arch -rubygems.required.ruby = Kræver Ruby version -swift.install = Tilføj pakken i din Package.swift-fil: -settings.link.select = Vælg depot -settings.link.button = Opdater depot link -settings.link.error = Kunne ikke opdatere depotlinket. -owner.settings.cargo.initialize.success = Cargo-indekset blev oprettet. -owner.settings.cargo.rebuild.description = Genopbygning kan være nyttig, hvis indekset ikke er synkroniseret med de lagrede Cargo-pakker. -owner.settings.cargo.rebuild.success = Cargo-indekset blev genopbygget med succes. -owner.settings.cleanuprules.add = Tilføj oprydningsregel -owner.settings.cleanuprules.edit = Rediger oprydningsregel -owner.settings.cleanuprules.title = Oprydningsregler -maven.registry = Konfigurer denne registreringsdatabasen i din projekt pom.xml fil: -npm.install2 = eller føj det til filen package.json: -nuget.dependency.framework = Mål Framework -npm.registry = Konfigurer denne registreringsdatabase i din projekt-.npmrc-fil: -nuget.install = For at installere pakken ved hjælp af NuGet skal du køre følgende kommando: -npm.dependencies = Afhængigheder -settings.link.description = Hvis du forbinder en pakke med et depot, vises pakken i depotets pakkeliste. -owner.settings.cargo.initialize.error = Kunne ikke initialisere Cargo index: %v -owner.settings.cleanuprules.keep.title = Versioner, der matcher disse regler, bevares, selvom de matcher en fjernelsesregel nedenfor. -generic.download = Download pakken fra kommandolinjen: -go.install = Installer pakken fra kommandolinjen: -container.layers = Billedlag -container.labels.key = Nøgle -container.labels.value = Værdi -debian.registry.info = Vælg $distribution og $component fra listen nedenfor. -maven.download = For at downloade afhængigheden skal du køre via kommandolinjen: -rpm.distros.suse = på SUSE-baserede distributioner -rpm.distros.redhat = på RedHat-baserede distributioner -owner.settings.cleanuprules.pattern_full_match = Anvend mønster på det fulde pakkenavn -details.author = Forfatter -details.repository_site = Depots hjemmeside -details.documentation_site = Dokumentations hjemmeside -details.license = Licens -assets = Aktiver -versions = Versioner -details.project_site = Projektets hjemmeside -versions.view_all = Se alle -dependency.id = ID -alpine.repository.branches = Grene -arch.version.optdepends = Valgfri afhænger -owner.settings.cleanuprules.remove.title = Versioner, der matcher disse regler, fjernes, medmindre en regel ovenfor siger, at de skal beholdes. -owner.settings.cleanuprules.remove.days = Fjern versioner ældre end -owner.settings.cleanuprules.remove.pattern = Fjern matchende versioner -owner.settings.cleanuprules.success.update = Oprydningsreglen er blevet opdateret. -owner.settings.cleanuprules.success.delete = Oprydningsregel er blevet slettet. -arch.version.backup = Backup -chef.registry = Konfigurer dette register i din ~/.chef/config.rb fil: -npm.install = For at installere pakken ved hjælp af npm skal du køre følgende kommando: -owner.settings.cargo.rebuild.no_index = Kan ikke genopbygge, intet indeks er initialiseret. -maven.install2 = Kør via kommandolinje: -keywords = Keywords -arch.pacman.helper.gpg = Tilføj tillidscertifikat til pacman: -arch.pacman.repo.multi = %s har den samme version i forskellige distributioner. -arch.pacman.conf = Tilføj server med relateret distribution og arkitektur til /etc/pacman.conf: -arch.version.groups = Gruppe -arch.version.depends = Afhænger -arch.version.conflicts = Konflikter [actions] -runners.description = Beskrivelse -runners.labels = Etiketter -runners.name = Navn -runners.task_list.repository = Depot -runners.status.active = Aktiv -runners.status.offline = Offline -runners.version = Version -runners.owner_type = Type -runners = Runners unit.desc = Administrer integrerede CI/CD-pipelines med Forgejo Actions. -status.unknown = Ukendt -runners.runner_title = Runner -runners.task_list = Seneste opgaver på denne løber -runners.task_list.run = Kør -runners.task_list.commit = Commit -runners.edit_runner = Rediger Runner -runs.commit = Commit -runs.scheduled = Planlagt -runs.pushed_by = pushed af -status.running = Kører -status.waiting = Venter -runners.new_notice = Hvordan man starter en runner -status.success = Succes -variables.not_found = Variablen kunne ikke findes. -runs.workflow = Arbejdsgang -runners.last_online = Sidste online tid -runners.task_list.done_at = Udført kl -runners.update_runner = Opdater ændringer -runners.update_runner_success = Runner blev opdateret -runners.update_runner_failed = Løberen kunne ikke opdateres -runners.delete_runner_failed = Runner kunne ikke slettes -runners.delete_runner_header = Bekræft for at slette denne runner -runners.status.idle = Tomgang -runs.no_job_without_needs = Arbejdsgangen skal indeholde mindst ét job uden afhængigheder. -runs.no_job = Arbejdsgangen skal indeholde mindst ét job -runs.no_results = Ingen resultater matchede. -runs.no_workflows = Der er endnu ingen arbejdsgange. workflow.enable = Aktiver arbejdsgang workflow.enable_success = Arbejdsgangen "%s" blev aktiveret. -variables.none = Der er ingen variabler endnu. -variables.edit = Rediger variabel -variables.deletion.success = Variablen er blevet fjernet. -variables.creation.failed = Kunne ikke tilføje variabel. -runners.delete_runner_notice = Hvis en opgave kører på denne runner, vil den blive afsluttet og markeret som mislykket. Det kan bryde bygningens arbejdsgang. runs.no_workflows.help_write_access = Ved du ikke, hvordan du starter med Forgejo Actions? Tjek hurtigstarten i brugerdokumentationen for at skrive dit første workflow, og opsæt en Forgejo-løber til at udføre dine opgaver. -runners.delete_runner_success = Runner blev slettet -variables.update.success = Variablen er blevet redigeret. -status.cancelled = Annulleret -status.skipped = Oversprunget -status.blocked = Blokeret workflow.disable_success = Arbejdsgangen "%s" blev deaktiveret. workflow.disable = Deaktiver arbejdsgang workflow.dispatch.use_from = Brug arbejdsgangen fra @@ -3615,41 +3289,12 @@ workflow.dispatch.warn_input_limit = Viser kun de første %d input. workflow.dispatch.success = Kørsel af arbejdsgang blev anmodet om. workflow.dispatch.input_required = Kræv værdi for input "%s". workflow.dispatch.invalid_input_type = Ugyldig inputtype "%s". -variables.creation = Tilføj variabel need_approval_desc = Har brug for godkendelse for at køre arbejdsgange for fork pull-anmodning. -runners.delete_runner = Slet denne runner -runners.status.unspecified = Ukendt -runners.reset_registration_token_success = Runner registreringstoken blev nulstillet -runs.all_workflows = Alle arbejdsgange -runners.reset_registration_token = Nulstil registreringstoken runs.empty_commit_message = (tom commit besked) runs.expire_log_message = Logfiler er blevet renset, fordi de var for gamle. -variables = Variabler -runs.actor = Aktør -actions = Handlinger -runners.status = Status -runners.task_list.status = Status -runners.id = ID -runners.task_list.no_tasks = Der er ingen opgave endnu. -runs.status = Status -runs.actors_no_select = Alle aktører -runs.status_no_select = Alle status -runners.none = Ingen runners tilgængelige -variables.management = Administrer variabler -variables.creation.success = Variablen "%s" er blevet tilføjet. -variables.update.failed = Variablen kunne ikke redigeres. runs.no_workflows.help_no_write_access = For at lære om Forgejo Actions, se dokumentationen. -variables.deletion = Fjern variabel variables.id_not_exist = Variabel med ID %d findes ikke. -variables.deletion.description = Fjernelse af en variabel er permanent og kan ikke fortrydes. Vil du fortsætte? -variables.description = Variabler vil blive videregivet til visse handlinger og kan ikke læses på anden vis. -variables.deletion.failed = Variablen kunne ikke fjernes. -runs.no_matching_online_runner_helper = Ingen matchende online-runner med etiket: %s -runners.runner_manage_panel = Administrer runners -runners.new = Opret ny runner workflow.dispatch.run = Kør arbejdsgang -runs.invalid_workflow_helper = Workflow-konfigurationsfilen er ugyldig. Tjek venligst din konfigurationsfil: %s -status.failure = Fiasko runs.no_runs = Workflowet har ingen kørsler endnu. [tool] @@ -3717,30 +3362,10 @@ none = Der er ingen hemmeligheder endnu. creation.failed = Kunne ikke tilføje hemmelighed. [dropzone] -invalid_input_type = Filer af denne type må ikke uploades. -remove_file = Fjern fil -default_message = Slip filer eller klik her for at uploade. -file_too_big = Filstørrelsen ({{filesize}} MB) overstiger den maksimale størrelse på ({{maxFilesize}} MB). [gpg] -default_key = Underskrevet med standardnøglen -error.generate_hash = Kunne ikke generere hash af commit -error.no_committer_account = Ingen konto knyttet til committers e-mailadresse -error.probable_bad_default_signature = ADVARSEL! Selvom standardnøglen har dette ID, bekræfter den ikke denne commit! Denne commit er MISTÆNLIG. -error.no_gpg_keys_found = Ingen kendt nøgle fundet for denne signatur i databasen -error.not_signed_commit = Ikke en underskrevet commit -error.failed_retrieval_gpg_keys = Kunne ikke hente nogen nøgle knyttet til committerens konto -error.probable_bad_signature = ADVARSEL! Selvom der er en nøgle med dette ID i databasen, bekræfter den ikke denne commit! Denne commit er MISTÆNLIG. -error.extract_sign = Kunne ikke udtrække signatur [munits.data] -kib = KiB -b = B -pib = PiB -mib = MiB -tib = TiB -eib = EiB -gib = GiB [units] error.no_unit_allowed_repo = Du har ikke tilladelse til at få adgang til nogen sektion af dette depot. @@ -3754,9 +3379,6 @@ deleted.display_name = Slettet projekt type-1.display_name = Individuelt projekt [markup] -filepreview.line = Linje %[1]d i %[2]s -filepreview.lines = Linjer %[1]d til %[2]d i %[3]s -filepreview.truncated = Forhåndsvisningen er blevet afkortet [git.filemode] symbolic_link = Symbolsk link diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index ae8246bb77..4a356b1f6e 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -36,18 +36,6 @@ twofa=Zwei-Faktor-Authentifizierung twofa_scratch=Zwei-Faktor-Einmalpasswort passcode=PIN -webauthn_insert_key=Hardware-Sicherheitsschlüssel einstecken -webauthn_sign_in=Drücke den Knopf auf deinem Sicherheitsschlüssel. Wenn dein Sicherheitsschlüssel keinen Knopf hat, stecke ihn erneut ein. -webauthn_press_button=Drücke den Knopf auf deinem Sicherheitsschlüssel … -webauthn_use_twofa=Zwei-Faktor-Authentifizierung via Handy verwenden -webauthn_error=Dein Sicherheitsschlüssel konnte nicht gelesen werden. -webauthn_unsupported_browser=Dein Browser unterstützt derzeit kein WebAuthn. -webauthn_error_unknown=Ein unbekannter Fehler ist aufgetreten. Bitte versuche es erneut. -webauthn_error_insecure=WebAuthn unterstützt nur sichere Verbindungen. Zum Testen über HTTP kannst du „localhost“ oder „127.0.0.1“ als Host verwenden -webauthn_error_unable_to_process=Der Server konnte deine Anfrage nicht bearbeiten. -webauthn_error_duplicated=Für diese Anfrage ist der Sicherheitsschlüssel nicht erlaubt. Bitte stell sicher, dass er nicht bereits registriert ist. -webauthn_error_empty=Du musst einen Namen für diesen Schlüssel festlegen. -webauthn_error_timeout=Das Zeitlimit wurde erreicht, bevor dein Schlüssel gelesen werden konnte. Bitte lade die Seite erneut. repository=Repository organization=Organisation mirror=Spiegel @@ -80,7 +68,7 @@ rerun_all=Alle Jobs neu starten save=Speichern add=Hinzufügen add_all=Alle hinzufügen -remove=Löschen +remove=Entfernen remove_all=Alle entfernen remove_label_str=Element „%s“ entfernen edit=Bearbeiten @@ -100,7 +88,7 @@ copy_type_unsupported=Dieser Dateityp kann nicht kopiert werden write=Verfassen preview=Vorschau -loading=Laden … +loading=Wird geladen … error=Fehler error404=Die Seite, die du versuchst aufzurufen, existiert nicht oder wurde entfernt, oder du bist nicht berechtigt, diese anzusehen. @@ -182,7 +170,7 @@ buttons.bold.tooltip=Fettschrift hinzufügen (Strg+B / ⌘B) buttons.italic.tooltip=Kursivschrift hinzufügen (Strg+I / ⌘I) buttons.quote.tooltip=Text zitieren buttons.code.tooltip=Code hinzufügen -buttons.link.tooltip=Link hinzufügen +buttons.link.tooltip=Link hinzufügen (Strg+K / ⌘K) buttons.list.unordered.tooltip=Liste hinzufügen buttons.list.ordered.tooltip=Nummerierte Liste hinzufügen buttons.list.task.tooltip=Aufgabenliste hinzufügen @@ -202,7 +190,7 @@ table_modal.label.columns = Spalten link_modal.header = Einen Link hinzufügen link_modal.url = URL link_modal.description = Beschreibung -link_modal.paste_reminder = Hinweis: Wenn du einen URL in der Zwischenablage hast, kannst du durch Einfügen im Editor direkt einen Link erstellen. +link_modal.paste_reminder = Hinweis: Wenn du eine URL in der Zwischenablage hast, kannst du durch Einfügen im Editor direkt einen Link erstellen. [filter] string.asc=A–Z @@ -257,7 +245,7 @@ err_admin_name_is_invalid=Administratornutzername ist ungültig general_title=Allgemeine Einstellungen app_name=Instanztitel -app_name_helper=Hier Ihren Instanznamen eingeben. Er wird auf jeder Seite angezeigt. +app_name_helper=Gib hier den Instanznamen ein. Er wird auf jeder Seite angezeigt. repo_path=Repository-Verzeichnis repo_path_helper=Remote-Git-Repositorys werden in diesem Verzeichnis gespeichert. lfs_path=Git-LFS-Wurzelpfad @@ -287,7 +275,7 @@ register_confirm=E-Mail-Bestätigung benötigt zum Registrieren mail_notify=E-Mail-Benachrichtigungen aktivieren server_service_title=Sonstige Server- und Drittserviceeinstellungen offline_mode=Offline-Modus aktivieren -offline_mode.description=Drittanbieter-CDNs deaktivieren und alle Ressourcen lokal bereitstellen. +offline_mode.description=Content Delivery Networks von Drittanbietern deaktivieren und alle Ressourcen lokal bereitstellen. disable_gravatar=Gravatar deaktivieren disable_gravatar.description=Gravatar und andere Drittanbieter-Avatar-Quellen deaktivieren. Ein Standardavatar wird verwendet, bis der Nutzer einen eigenen Avatar auf deren Instanz hochlädt. federated_avatar_lookup=Föderierte Profilbilder einschalten @@ -332,7 +320,7 @@ no_reply_address=Versteckte E-Mail-Domain no_reply_address_helper=Domain-Name für Benutzer mit einer versteckten Emailadresse. Zum Beispiel wird der Benutzername „Joe“ in Git als „joe@noreply.example.org“ protokolliert, wenn die versteckte E-Mail-Domain „noreply.example.org“ festgelegt ist. password_algorithm=Passwort-Hashing-Algorithmus 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. +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 env_config_keys=Umgebungskonfiguration env_config_keys_prompt=Die folgenden Umgebungsvariablen werden auch auf Ihre Konfigurationsdatei angewendet: @@ -385,10 +373,10 @@ remember_me=Dieses Gerät speichern forgot_password_title=Passwort vergessen forgot_password=Passwort vergessen? sign_up_successful=Konto wurde erfolgreich erstellt. Willkommen! -confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an %s gesendet. Um den Registrierungsprozess abzuschließen, überprüfe bitte deinen Posteingang und folge dem angegebenen Link innerhalb von: %s. Falls die E-Mail inkorrekt sein sollte, kannst du dich einloggen und anfragen, eine weitere Bestätigungs-E-Mail an eine andere Adresse zu senden. +confirmation_mail_sent_prompt=Eine neue Bestätigungs-E-Mail wurde an %s gesendet. Um den Registrierung abzuschließen, überprüfe bitte deinen Posteingang und folge dem angegebenen Link innerhalb von: %s. Falls die E-Mail inkorrekt sein sollte, kannst du dich einloggen und anfragen, eine weitere Bestätigungs-E-Mail an eine andere Adresse zu senden. must_change_password=Aktualisiere dein Passwort allow_password_change=Verlange vom Benutzer das Passwort zu ändern (empfohlen) -reset_password_mail_sent_prompt=Eine Bestätigungs-E-Mail wurde an %s gesendet. Um den Kontowiederherstellungsprozess abzuschließen, überprüfe bitte deinen Posteingang und folge dem angegebenen Link innerhalb von %s. +reset_password_mail_sent_prompt=Eine Bestätigungs-E-Mail wurde an %s gesendet. Um den Kontowiederherstellung abzuschließen, überprüfe bitte deinen Posteingang und folge dem angegebenen Link innerhalb von %s. active_your_account=Aktiviere dein Konto account_activated=Konto wurde aktiviert prohibit_login=Das Konto ist gesperrt @@ -448,7 +436,7 @@ back_to_sign_in = Zurück zur Anmeldung sign_in_openid = Mit OpenID fortfahren hint_login = Hast du bereits ein Konto? Jetzt anmelden! hint_register = Brauchst du ein Konto? Jetzt registrieren. -unauthorized_credentials = Die Zugangsdaten sind inkorrekt oder abgelaufen. Versuchen es erneut oder siehe %s für mehr Informationen +unauthorized_credentials = Die Zugangsdaten sind inkorrekt oder abgelaufen. Versuche es erneut oder siehe %s für mehr Informationen use_onetime_code = Einen Einmal-Code benutzen [mail] @@ -634,7 +622,7 @@ Biography = Biografie Website = Webseite Location = Ort To = Branchname -AccessToken = Zugangstoken +AccessToken = Zugriffstoken username_claiming_cooldown = Der Benutzername kann nicht beansprucht werden, weil seine Schutzzeit noch nicht vorbei ist. Er kann am %[1]s beansprucht werden. email_domain_is_not_allowed = Die Domain der E-Mail-Adresse des Benutzers %s steht in Konflikt mit EMAIL_DOMAIN_ALLOWLIST oder EMAIL_DOMAIN_BLOCKLIST. Bitte stelle sicher, dass du die E-Mail-Adresse richtig gesetzt hast. @@ -664,10 +652,10 @@ form.name_pattern_not_allowed=Das Muster „%s“ ist nicht in einem Benutzernam form.name_chars_not_allowed=Benutzername „%s“ enthält ungültige Zeichen. block_user = Benutzer blockieren block_user.detail = Bitte beachte, dass die Blockierung eines Benutzers auch andere Auswirkungen hat, so wie: -block_user.detail_2 = Dieser Benutzer wird nicht mehr nicht mit deinen Repositorys oder von dir erstellten Issues und Kommentaren interagieren können. +block_user.detail_2 = Dieser Benutzer wird nicht mehr mit deinen Repositorys oder von dir erstellten Issues und Kommentaren interagieren können. block_user.detail_1 = Ihr werdet euch nicht mehr gegenseitig folgen und es auch nicht mehr können. block = Blockieren -follow_blocked_user = Du kannst diesen Benutzer nicht folgen, weil du ihn blockiert hast, oder er dich blockiert hat. +follow_blocked_user = Du kannst diesem Benutzer nicht folgen, weil du ihn blockiert hast oder er dich blockiert hat. block_user.detail_3 = Ihr werdet nicht mehr in der Lage sein, euch gegenseitig als Repository-Mitarbeiter hinzuzufügen. unblock = Nicht mehr blockieren followers_one = %d Follower @@ -741,7 +729,7 @@ comment_type_group_issue_ref=Issue-Referenz saved_successfully=Die Einstellungen wurden erfolgreich gespeichert. privacy=Datenschutz keep_activity_private=Aktivität auf der Profilseite ausblenden -lookup_avatar_by_mail=Profilbild anhand der E-Mail-Addresse suchen +lookup_avatar_by_mail=Profilbild anhand der E-Mail-Adresse suchen enable_custom_avatar=Benutzerdefiniertes Profilbild verwenden choose_new_avatar=Neues Profilbild auswählen update_avatar=Profilbild aktualisieren @@ -763,7 +751,7 @@ manage_emails=E-Mail-Adressen verwalten manage_themes=Standard-Theme manage_openid=OpenID-Adressen email_desc=Deine primäre E-Mail-Adresse wird für Benachrichtigungen, Passwort-Wiederherstellung und, sofern sie nicht versteckt ist, web-basierte Git-Operationen verwendet. -theme_desc=Dieses Thema wird für die Weboberfläche verwendet, wenn du angemeldet bist. +theme_desc=Dieses Theme wird für die Weboberfläche verwendet, wenn du angemeldet bist. primary=Primär activated=Aktiviert requires_activation=Erfordert Aktivierung @@ -773,7 +761,7 @@ activations_pending=Aktivierung ausstehend can_not_add_email_activations_pending=Es gibt eine ausstehende Aktivierung, versuche es in ein paar Minuten erneut, wenn du eine neue E-Mail hinzufügen möchtest. delete_email=Löschen email_deletion=E-Mail-Adresse entfernen -email_deletion_desc=Diese E-Mail-Adresse und die damit verbundenen Informationen werden von deinem Konto entfernt. Git-Commits von dieser E-Mail-Addresse bleiben unverändert. Fortfahren? +email_deletion_desc=Diese E-Mail-Adresse und die damit verbundenen Informationen werden von deinem Konto entfernt. Git-Commits von dieser E-Mail-Adresse bleiben unverändert. Fortfahren? email_deletion_success=Die E-Mail-Adresse wurde entfernt. theme_update_success=Deine Theme-Auswahl wurde gespeichert. theme_update_error=Das ausgewählte Theme existiert nicht. @@ -808,7 +796,7 @@ ssh_key_been_used=Dieser SSH-Schlüssel wird auf diesem Server bereits verwendet ssh_key_name_used=Ein gleichnamiger SSH-Key existiert bereits in deinem Account. ssh_principal_been_used=Diese Identität ist bereits auf dem Server vorhanden. gpg_key_id_used=Ein öffentlicher GPG-Schlüssel mit der gleichen ID existiert bereits. -gpg_no_key_email_found=Dieser GPG-Schlüssel entspricht keiner mit deinem Konto verbundenen aktivierten E-Mail-Addresse. Er kann trotzdem hinzugefügt werden, wenn du den gegebenen Token signierst. +gpg_no_key_email_found=Dieser GPG-Schlüssel entspricht keiner mit deinem Konto verbundenen aktivierten E-Mail-Adresse. Er kann trotzdem hinzugefügt werden, wenn du den gegebenen Token signierst. gpg_key_matched_identities=Passende Identitäten: gpg_key_matched_identities_long=Die eingebetteten Identitäten in diesem Schlüssel stimmen mit den folgenden aktivierten E-Mail-Adressen für diesen Benutzer überein. Commits, die mit diesen E-Mail-Addressen committed wurden, können mit diesem Schlüssel verifiziert werden. gpg_key_verified=Verifizierter Schlüssel @@ -825,9 +813,9 @@ ssh_key_verified=Verifizierter Schlüssel ssh_key_verified_long=Der Schlüssel wurde mit einem Token verifiziert. Er kann verwendet werden, um Commits zu verifizieren, die mit irgendeiner für diesen Nutzer aktivierten E-Mail-Adresse und irgendeiner Identität dieses Schlüssels übereinstimmen. ssh_key_verify=Verifizieren ssh_invalid_token_signature=Der gegebene SSH-Schlüssel, Signatur oder Token stimmen nicht überein oder der Token ist veraltet. -ssh_token_required=Sie müssen eine Signatur für das Token unten angeben +ssh_token_required=Du musst eine Signatur für das folgende Token angeben ssh_token=Token -ssh_token_help=Sie können eine Signatur wie folgt generieren: +ssh_token_help=Du kannst eine Signatur wie folgt generieren: ssh_token_signature=SSH-Textsignatur (armored signature) key_signature_ssh_placeholder=Beginnt mit „-----BEGIN SSH SIGNATURE-----“ verify_ssh_key_success=SSH-Schlüssel „%s“ wurde verifiziert. @@ -864,7 +852,7 @@ hide_openid=Nicht im Profil anzeigen ssh_disabled=SSH ist deaktiviert ssh_signonly=SSH ist derzeit deaktiviert, sodass diese Schlüssel nur zur Commit-Signaturverifizierung verwendet werden. ssh_externally_managed=Dieser SSH-Schlüssel wird extern für diesen Benutzer verwaltet -manage_access_token=Zugriffstokens +manage_access_token=Zugriffstoken generate_new_token=Neuen Token erzeugen tokens_desc=Diese Tokens gewähren vollen Zugriff auf dein Konto via Forgejo-API. token_name=Token-Name @@ -917,7 +905,7 @@ revoke_oauth2_grant=Autorisierung widerrufen revoke_oauth2_grant_description=Wenn du die Autorisierung widerrufst, kann die Anwendung nicht mehr auf deine Daten zugreifen. Bist du dir sicher? revoke_oauth2_grant_success=Zugriff erfolgreich widerrufen. -twofa_desc=Um dein Konto gegen Passwortdiebstahl zu schützen, kannst du eine Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter („TOTP“) zu erhalten. +twofa_desc=Um dein Konto gegen Passwortdiebstahl zu schützen, kannst du ein Smartphone oder ein anderes Gerät verwenden, um zeitbasierte Einmalpasswörter („TOTP“) zu erhalten. twofa_is_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung eingeschaltet. twofa_not_enrolled=Für dein Konto ist die Zwei-Faktor-Authentifizierung momentan nicht eingeschaltet. twofa_disable=Zwei-Faktor-Authentifizierung deaktivieren @@ -935,7 +923,7 @@ passcode_invalid=Die PIN ist falsch. Probiere es erneut. twofa_enrolled=Die Zwei-Faktor-Authentifizierung wurde für dein Konto aktiviert. Bewahre deinen einmalig verwendbaren Wiederherstellungsschlüssel (%s) an einem sicheren Ort auf, da er nicht wieder angezeigt werden wird. twofa_failed_get_secret=Fehler beim Abrufen des Geheimnisses. -webauthn_desc=Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard „WebAuthn“ unterstützen. +webauthn_desc=Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard „WebAuthn“ unterstützen. webauthn_register_key=Sicherheitsschlüssel hinzufügen webauthn_nickname=Nickname webauthn_delete_key=Sicherheitsschlüssel entfernen @@ -974,7 +962,7 @@ visibility.limited_tooltip=Nur für angemeldete Benutzer sichtbar visibility.private=Privat visibility.private_tooltip=Sichtbar nur für Mitglieder von Organisationen, denen du beigetreten bist user_block_success = Dieser Benutzer wurde erfolgreich blockiert. -twofa_recovery_tip = Falls du dein Gerät verlierst, wirst du in der Lage sein, einen einmalig verwendbaren Wiederherstellungsschlüssel zu verwenden, um den auf dein Konto wiederherzustellen. +twofa_recovery_tip = Falls du dein Gerät verlierst, kannst du einen einmalig verwendbaren Wiederherstellungsschlüssel benutzen, um den Zugriff auf dein Konto wiederherzustellen. webauthn_alternative_tip = Du möchtest vielleicht eine zusätzliche Authentifizierungsmethode einrichten. blocked_users_none = Keine Benutzer blockiert. webauthn_key_loss_warning = Falls du deine Sicherheitsschlüssel verlierst, wirst du Zugang zu deinem Konto verlieren. @@ -990,7 +978,7 @@ additional_repo_units_hint_description = Einen „Mehr aktivieren“-Hinweis fü pronouns = Pronomen pronouns_unspecified = Nicht spezifiziert language.title = Standardsprache -keep_activity_private.description = Deine öffentliche Aktivität wird nur für dich selbst und die Instanzadminstratoren sichtbar sein. +keep_activity_private.description = Deine öffentliche Aktivität wird nur für dich selbst und die Instanzadministratoren sichtbar sein. language.localization_project = Hilf uns, Forgejo in deine Sprache zu übersetzen! Mehr erfahren. language.description = Diese Sprache wird in deinem Konto gespeichert und standardmäßig nach dem Anmelden benutzt. user_block_yourself = Du kannst dich nicht selbst blockieren. @@ -1019,10 +1007,10 @@ quota.sizes.assets.attachments.all = Anhänge quota.sizes.assets.packages.all = Pakete quota.sizes.wiki = Wiki regenerate_token_success = Der Token wurde regeneriert. Anwendungen, die ihn benutzen, haben nicht länger Zugriff auf dein Konto und müssen mit dem neuen Token aktualisiert werden. -access_token_regeneration = Zugangstoken regenerieren -access_token_regeneration_desc = Einen Token zu regenerieren, wird den Zugriff auf dein Konto von Anwendungen, die ihn nutzen, zurückziehen. Dies kann nicht rückgängig gemacht werden. Fortsetzen? -regenerate_token = Regenerieren -ssh_token_help_ssh_agent = , oder, falls Sie einen SSH-Agenten benutzen (mit der Variable SSH_AUTH_SOCK gesetzt): +access_token_regeneration = Zugriffstoken neu generieren +access_token_regeneration_desc = Wenn du einen Token neu generierst, verlieren Anwendungen, die ihn nutzen, den Zugriff auf dein Konto. Dies kann nicht rückgängig gemacht werden. Fortfahren? +regenerate_token = Neu generieren +ssh_token_help_ssh_agent = , oder, falls du einen SSH-Agenten benutzt (mit der Variable SSH_AUTH_SOCK gesetzt): [repo] owner=Besitzer @@ -1082,7 +1070,7 @@ mirror_address_desc=Gib alle erforderlichen Anmeldedaten im Abschnitt „Authent mirror_address_url_invalid=Die angegebene URL ist ungültig. Achte darauf, dass alle Komponenten der URL korrekt escapt wurden. mirror_address_protocol_invalid=Die angegebene URL ist ungültig. Nur Orte mit „http(s)://“ oder „git://“ können fürs Spiegeln benutzt werden. mirror_lfs=Großdatei-Speicher (LFS) -mirror_lfs_desc=Spiegeln von LFS-Dateien aktivieren. +mirror_lfs_desc=Spiegeln von LFS-Daten aktivieren. mirror_lfs_endpoint=LFS-Endpunkt mirror_lfs_endpoint_desc=Sync wird versuchen, die Klon-URL zu verwenden, um den LFS-Server zu bestimmen. Du kannst auch einen eigenen Endpunkt angeben, wenn die LFS-Dateien woanders gespeichert werden. mirror_last_synced=Zuletzt synchronisiert @@ -1141,7 +1129,7 @@ template.invalid=Es muss ein Vorlagen-Repository ausgewählt werden archive.title=Dieses Repository ist archiviert. Du kannst Dateien ansehen und es klonen, kannst aber seinen Status nicht verändern, zum Beispiel nichts pushen, keine Issues eröffnen und keine Pull-Requests oder Kommentare erstellen. archive.title_date=Dieses Repository wurde am %s archiviert. Du kannst Dateien ansehen und es klonen, kannst aber seinen Status nicht verändern, zum Beispiel nichts pushen, und keine Issues eröffnen oder Pull-Requests oder Kommentare erstellen. form.reach_limit_of_creation_1=Du hast bereits dein Limit von %d Repository erreicht. -form.reach_limit_of_creation_n=Du hast bereits dein Limit von %d Repositorys erreicht. +form.reach_limit_of_creation_n=Der Besitzer hat bereits das Limit von %d Repositorys erreicht. form.name_reserved=Der Repository-Name „%s“ ist reserviert. form.name_pattern_not_allowed=Das Muster „%s“ ist in Repository-Namen nicht erlaubt. @@ -1153,14 +1141,6 @@ migrate_options_lfs_endpoint.label=LFS-Endpunkt migrate_options_lfs_endpoint.description=Migration wird versuchen, über den entfernten Git-Server den LFS-Server zu bestimmen. Du kannst auch einen eigenen Endpunkt angeben, wenn die LFS-Dateien woanders gespeichert werden. migrate_options_lfs_endpoint.description.local=Ein lokaler Serverpfad wird ebenfalls unterstützt. migrate_options_lfs_endpoint.placeholder=Wenn leer gelassen, wird der Endpunkt von der Klon-URL abgeleitet -migrate_items=Migrationselemente -migrate_items_wiki=Wiki -migrate_items_milestones=Meilensteine -migrate_items_labels=Labels -migrate_items_issues=Issues -migrate_items_pullrequests=Pull-Requests -migrate_items_merge_requests=Merge-Requests -migrate_items_releases=Releases migrate_repo=Repository migrieren migrate.clone_address=Migrations-/Klon-URL migrate.clone_address_desc=Die HTTP(S)- oder „git clone“-URL eines bereits existierenden Repositorys @@ -1179,16 +1159,6 @@ migrate.migrating=Migriere von %s … migrate.migrating_failed=Migrieren von %s fehlgeschlagen. migrate.migrating_failed.error=Migration fehlgeschlagen: %s migrate.migrating_failed_no_addr=Migration fehlgeschlagen. -migrate.migrating_git=Git-Daten werden migriert -migrate.migrating_topics=Themen werden migriert -migrate.migrating_milestones=Meilensteine werden migriert -migrate.migrating_labels=Labels werden migriert -migrate.migrating_releases=Releases werden migriert -migrate.migrating_issues=Issues werden migriert -migrate.migrating_pulls=Pull-Requests werden migriert -migrate.cancel_migrating_title=Migration abbrechen -migrate.cancel_migrating_confirm=Möchtest du diese Migration abbrechen? - mirror_from=Spiegel von forked_from=geforkt von generated_from=erzeugt von @@ -1236,12 +1206,12 @@ commit=Commit release=Release releases=Releases tag=Tag -released_this=hat releast +released_this=hat es veröffentlicht file.title=%s an %s file_raw=Originalformat file_history=Verlauf file_view_source=Quelltext anzeigen -file_view_rendered=Ansicht rendern +file_view_rendered=Gerendert anzeigen file_view_raw=Originalformat anzeigen file_permalink=Permalink file_too_large=Die Datei ist zu groß zum Anzeigen. @@ -1564,7 +1534,7 @@ issues.ref_reopening_from=`referenzierte dieses Issue aus einem issues.ref_from=`von %[1]s` issues.author=Autor issues.role.owner=Besitzer -issues.role.owner_helper=Dieser Benutzer ist der Besitzer dieses Repositorys. +issues.role.owner_helper=Dieser Benutzer ist ein Besitzer dieses Repositorys. issues.role.member=Mitglied issues.role.member_helper=Dieser Benutzer ist Mitglied der Organisation, die dieses Repository besitzt. issues.role.collaborator=Mitarbeiter @@ -1624,7 +1594,7 @@ issues.unlock_comment=hat diese Diskussion %s entsperrt issues.lock_confirm=Sperren issues.unlock_confirm=Entsperren issues.lock.notice_1=- Andere Nutzer können keine neuen Kommentare beisteuern. -issues.lock.notice_2=– Du und andere Mitarbeiter mit Zugriff auf dieses Repository können weiterhin für andere sichtbare Kommentare hinterlassen. +issues.lock.notice_2=- Du und andere Mitarbeiter mit Zugriff auf dieses Repository können weiterhin für andere sichtbare Kommentare hinterlassen. issues.lock.notice_3=- Du kannst die Diskussion jederzeit wieder entsperren. issues.unlock.notice_1=- Jeder wird wieder in der Lage sein, zu diesem Issue zu kommentieren. issues.unlock.notice_2=- Du kannst den Issue jederzeit wieder sperren. @@ -1745,7 +1715,7 @@ pulls.compare_changes=Neuer Pull-Request pulls.allow_edits_from_maintainers=Änderungen von Maintainern erlauben pulls.allow_edits_from_maintainers_desc=Nutzer mit Schreibzugriff auf den Basisbranch können auch auf diesen Branch pushen pulls.allow_edits_from_maintainers_err=Aktualisieren fehlgeschlagen -pulls.compare_changes_desc=Wähle den Zielbranch, in das zusammengeführt werden soll, und den Quellbranch, von dem gepullt werden soll, aus. +pulls.compare_changes_desc=Wähle den Zielbranch, in den zusammengeführt werden soll, und den Quellbranch, von dem gepullt werden soll, aus. pulls.has_viewed_file=Gesehen pulls.has_changed_since_last_review=Seit deiner letzten Sichtung geändert pulls.viewed_files_label=%[1]d / %[2]d Dateien betrachtet @@ -2087,7 +2057,7 @@ settings.pulls.default_allow_edits_from_maintainers=Änderungen von Maintainern settings.releases_desc=Repository-Releases aktivieren settings.packages_desc=Repository-Paket-Registry aktivieren settings.projects_desc=Repository-Projekte aktivieren -settings.actions_desc=Aktiviere integrierte CI/CD-Pipelines mit Forgejo-Actions +settings.actions_desc=Integrierte CI/CD-Pipelines mit Forgejo-Actions aktivieren settings.admin_settings=Administratoreinstellungen settings.admin_enable_health_check=Repository-Health-Checks aktivieren (git fsck) settings.admin_code_indexer=Code-Indexer @@ -2192,7 +2162,7 @@ settings.githook_edit_desc=Wenn ein Hook nicht aktiv ist, wird der Standardinhal settings.githook_name=Hook-Name settings.githook_content=Hook-Inhalt settings.update_githook=Hook aktualisieren -settings.add_webhook_desc=Forgejo sendet eine POST-Anfrage mit festgelegtem Content-Type an die Ziel-URL. Mehr Informationen findest du in der Anleitung zu Webhooks (Englisch). +settings.add_webhook_desc=Forgejo sendet eine POST-Anfrage mit festgelegtem Content-Type an die Ziel-URL. Mehr dazu in der Anleitung zu Webhooks (auf Englisch). settings.payload_url=Ziel-URL settings.http_method=HTTP-Methode settings.content_type=POST-Content-Type @@ -2352,7 +2322,7 @@ settings.protected_branch_deletion_desc=Wenn du den Branch-Schutz deaktivierst, settings.block_rejected_reviews=Zusammenführung bei abgelehnten Sichtungen blockieren settings.block_rejected_reviews_desc=Zusammenführen ist nicht möglich, wenn Änderungen durch offizielle Prüfer angefragt werden, auch wenn genügend Genehmigungen existieren. settings.block_on_official_review_requests=Merge bei offiziellen Sichtungsanfragen blockieren -settings.block_on_official_review_requests_desc=Merge ist nicht möglich, wenn offizielle Sichtungsanfrangen vorliegen, selbst wenn genügend Genehmigungen existieren. +settings.block_on_official_review_requests_desc=Merge ist nicht möglich, wenn offizielle Sichtungsanfragen vorliegen, selbst wenn genügend Genehmigungen existieren. settings.block_outdated_branch=Merge blockieren, wenn der Pull-Request veraltet ist settings.block_outdated_branch_desc=Merge ist nicht möglich, wenn der Head-Branch hinter dem Basis-Branch ist. settings.default_branch_desc=Wähle einen Standardbranch für Pull-Requests und Code-Commits: @@ -2373,7 +2343,7 @@ settings.tags.protection.allowed.teams=Erlaubte Teams settings.tags.protection.allowed.noone=Niemand settings.tags.protection.create=Regel hinzufügen settings.tags.protection.none=Es gibt keine geschützten Tags. -settings.tags.protection.pattern.description=Du kannst einen einzigen Namen oder ein globales Schema oder einen regulären Ausdruck verwenden, um mehrere Tags zu schützen. Mehr dazu im Guide für geschützte Tags (Englisch). +settings.tags.protection.pattern.description=Du kannst einen einzigen Namen oder ein globales Schema oder einen regulären Ausdruck verwenden, um mehrere Tags zu schützen. Mehr dazu in der Anleitung für geschützte Tags (auf Englisch). settings.bot_token=Bot-Token settings.chat_id=Chat-ID settings.thread_id=Thread-ID @@ -2382,12 +2352,12 @@ settings.matrix.room_id=Raum-ID settings.matrix.message_type=Nachrichtentyp settings.archive.button=Repo archivieren settings.archive.header=Dieses Repo archivieren -settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird von der Übersichtsseite versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen. +settings.archive.text=Durch das Archivieren wird ein Repo vollständig schreibgeschützt. Es wird von der Übersichtsseite versteckt. Niemand (nicht einmal du!) wird in der Lage sein, neue Commits zu erstellen oder Issues oder Pull-Requests zu öffnen. Es wird empfohlen, den Archivierungsgrund zu dokumentieren, um zukünftigen Entwicklern, die planen, das Repository zu forken, zu helfen. settings.archive.success=Das Repo wurde erfolgreich archiviert. settings.archive.error=Beim Versuch, das Repository zu archivieren, ist ein Fehler aufgetreten. Weitere Details finden sich im Log. settings.archive.error_ismirror=Du kannst kein gespiegeltes Repo archivieren. -settings.archive.branchsettings_unavailable=Branch-Einstellungen sind nicht verfügbar in archivierten Repos. -settings.archive.tagsettings_unavailable=Tag-Einstellungen sind nicht verfügbar in archivierten Repos. +settings.archive.branchsettings_unavailable=In archivierten Repos sind Branch-Einstellungen nicht verfügbar. +settings.archive.tagsettings_unavailable=In archivierten Repos sind Tag-Einstellungen nicht verfügbar. settings.unarchive.button=Archivierung zurücksetzen settings.unarchive.header=Archivierung dieses Repositorys zurücksetzen settings.unarchive.text=Durch das Aufheben der Archivierung kann das Repo wieder Commits und Pushes sowie neue Issues und Pull-Requests empfangen. @@ -2555,7 +2525,7 @@ branch.included=Enthalten branch.create_new_branch=Branch aus Branch erstellen: branch.confirm_create_branch=Branch erstellen branch.warning_rename_default_branch=Du benennst den Standard-Branch um. -branch.rename_branch_to=Umbenennung des Zweigs "%s". +branch.rename_branch_to=Branch "%s" wird umbenannt. branch.create_branch_operation=Branch erstellen branch.new_branch=Neue Branch erstellen branch.new_branch_from=Neuen Branch von „%s“ erstellen @@ -2592,7 +2562,7 @@ settings.add_collaborator_blocked_them = Der Mitarbeiter konnte nicht hinzugefü settings.wiki_rename_branch_main = Den Wiki-Branch-Namen normalisieren settings.enter_repo_name = Gib den Besitzer- und den Repository-Namen genau wie angezeigt ein: settings.wiki_branch_rename_success = Der Branch-Name des Repository-Wikis wurde erfolgreich normalisiert. -settings.archive.mirrors_unavailable = Spiegel sind nicht verfügbar in archivierten Repos. +settings.archive.mirrors_unavailable = In archivierten Repos sind Spiegel nicht verfügbar. pulls.blocked_by_user = Du kannst keinen Pull-Request in diesem Repository erstellen, weil du vom Repository-Besitzer blockiert wurdest. settings.add_collaborator_blocked_our = Der Mitarbeiter konnte nicht hinzugefügt werden, weil der Repository-Besitzer ihn blockiert hat. issues.blocked_by_user = Du kannst keine Issues in diesem Repository erstellen, weil du vom Repository-Besitzer blockiert wurdest. @@ -2628,7 +2598,7 @@ settings.units.overview = Übersicht settings.wiki_rename_branch_main_notices_1 = Diese Aktion KANN NICHT rückgängig gemacht werden. settings.wiki_rename_branch_main_notices_2 = Dies wird den internen Branch des Repository-Wikis von %s permanent umbenennen. Existierende Checkouts müssen aktualisiert werden. settings.wiki_branch_rename_failure = Der Branch-Name des Repository-Wiki konnte nicht normalisiert werden. -settings.confirm_wiki_branch_rename = Den Wiki-Branch umbenenennen +settings.confirm_wiki_branch_rename = Den Wiki-Branch umbenennen activity.navbar.contributors = Mitwirkende contributors.contribution_type.deletions = Löschungen contributors.contribution_type.additions = Einfügungen @@ -2647,13 +2617,7 @@ pulls.ready_for_review = Bereit zur Sichtung? settings.rename_branch_failed_protected = Branch %s kann nicht umbenannt werden, weil er ein geschützter Branch ist. editor.commit_id_not_matching = Die Datei wurde geändert, während du sie bearbeitet hast. Committe in einen neuen Branch, dann führe einen Merge durch. editor.push_out_of_date = Der Push scheint veraltet zu sein. -n_commit_few = %s Commits -n_branch_one = %s Branch -n_branch_few = %s Branches -n_tag_one = %s Tag -n_tag_few = %s Tags stars = Favorisierungen -n_commit_one = %s Commit issues.num_participants_one = %d Beteiligter settings.enforce_on_admins_desc = Repositoryadministratoren können diese Regel nicht umgehen. settings.enforce_on_admins = Erzwinge diese Regel für alle Repositoryadministratoren @@ -2665,7 +2629,7 @@ settings.add_webhook.invalid_path = Der Pfad darf kein „.“ oder „..“ enh settings.sourcehut_builds.manifest_path = Build-Manifest-Pfad settings.sourcehut_builds.visibility = Job-Sichtbarkeit settings.sourcehut_builds.secrets = Geheimnisse -settings.sourcehut_builds.secrets_helper = Dem Job zugriff auf die Build-Geheimnisse geben (benötigt die SECRETS:RO-Berechtigung) +settings.sourcehut_builds.secrets_helper = Dem Job Zugriff auf die Build-Geheimnisse geben (benötigt die SECRETS:RO-Berechtigung) settings.web_hook_name_sourcehut_builds = SourceHut-Builds settings.graphql_url = GraphQL-URL settings.matrix.room_id_helper = Die Raum-ID kann über den Element-Webclient ermittelt werden: Raumeinstellungen > erweitert > interne Raum-ID. Beispielsweise %s. @@ -2677,8 +2641,6 @@ settings.transfer.button = Besitz übertragen settings.transfer.modal.title = Besitz übertragen wiki.no_search_results = Keine Ergebnisse wiki.search = Wiki durchsuchen -n_release_one = %s freigegeben -n_release_few = %s Veröffentlichungen form.string_too_long = Die Zeichenkette ist länger als %d Zeichen. settings.federation_settings = Föderationseinstellungen settings.federation_following_repos = URLs folgender Repositorys. Durch „;“ getrennt, keine Leerzeichen. @@ -2688,8 +2650,8 @@ project = Projekte comments.edit.already_changed = Die Änderungen an diesem Kommentar können nicht gespeichert werden. Es scheint, als seien die Inhalte bereits durch einen anderen Benutzer verändert worden. Bitte die Seite neu laden und das Bearbeiten erneut versuchen, um dessen Änderungen nicht zu überschreiben issues.edit.already_changed = Die Änderungen an diesem Issue können nicht gespeichert werden. Es scheint, als seien die Inhalte bereits durch einen anderen Benutzer verändert worden. Bitte die Seite neu laden und das Bearbeiten erneut versuchen, um deren Änderungen nicht zu überschreiben pulls.edit.already_changed = Die Änderungen an diesem Pull-Request können nicht gespeichert werden. Es scheint, als seien die Inhalte bereits durch einen anderen Benutzer verändert worden. Bitte die Seite neu laden und das Bearbeiten erneut versuchen, um dessen Änderungen nicht zu überschreiben -subscribe.pull.guest.tooltip = Anmelden, um diesen Pull-Request zu abbonieren. -subscribe.issue.guest.tooltip = Anmelden, um dieses Issue zu abbonieren. +subscribe.pull.guest.tooltip = Anmelden, um diesen Pull-Request zu abonnieren. +subscribe.issue.guest.tooltip = Anmelden, um dieses Issue zu abonnieren. issues.author.tooltip.pr = Dieser Benutzer ist der Autor dieses Pull-Requests. issues.author.tooltip.issue = Dieser Benutzer ist der Autor dieses Issues. activity.commit = Commit-Aktivität @@ -2702,8 +2664,8 @@ release.add_external_asset = Externes Asset hinzufügen release.invalid_external_url = Ungültige externe URL: „%s“ activity.published_prerelease_label = Pre-Release activity.published_tag_label = Tag -settings.pull_mirror_sync_quota_exceeded = Quota überschritten, Änderungen werden nicht gepullt. -settings.transfer_quota_exceeded = Der neue Eigentümer (%s) hat die Quota überschritten. Das Repository wurde nicht übertragen. +settings.pull_mirror_sync_quota_exceeded = Kontingent überschritten, Änderungen werden nicht gepullt. +settings.transfer_quota_exceeded = Der neue Eigentümer (%s) hat das Kontingent überschritten. Das Repository wurde nicht übertragen. no_eol.text = Kein EOL no_eol.tooltip = Diese Datei enthält am Ende kein Zeilenende-Zeichen (EOL). pulls.cmd_instruction_merge_warning = Achtung: Die Einstellung „Autoerkennung von manuellen Zusammenführungen“ ist für dieses Repository nicht aktiviert. Du musst hinterher diesen Pull-Request als manuell zusammengeführt markieren. @@ -2941,7 +2903,7 @@ dashboard.cron.error=Fehler in Cron: %s: %[3]s dashboard.cron.finished=Cron: %[1]s ist beendet dashboard.delete_inactive_accounts=Alle nicht aktivierten Konten löschen dashboard.delete_inactive_accounts.started=Löschen aller nicht aktivierten Account-Aufgabe gestartet. -dashboard.delete_repo_archives=Lösche alle Repository-Archive (ZIP, TAR.GZ, etc.) +dashboard.delete_repo_archives=Lösche alle Repository-Archive (ZIP, TAR.GZ usw.) dashboard.delete_repo_archives.started=Löschen aller Repository-Archive gestartet. dashboard.delete_missing_repos=Alle Repository-Datensätze mit verloren gegangenen Git-Dateien löschen dashboard.delete_missing_repos.started=Alle Repositorys löschen, die den Git-Dateien-Task nicht gestartet haben. @@ -2962,34 +2924,6 @@ dashboard.sync_external_users=Externe Benutzerdaten synchronisieren dashboard.cleanup_hook_task_table=Hook-Task-Tabelle bereinigen dashboard.cleanup_packages=Veraltete Pakete bereinigen dashboard.cleanup_actions=Abgelaufene Logs und Artefakte von Actions bereinigen -dashboard.server_uptime=Server-Uptime -dashboard.current_goroutine=Aktuelle Goroutinen -dashboard.current_memory_usage=Aktuelle Speichernutzung -dashboard.total_memory_allocated=Zugeteilter Gesamtspeicher -dashboard.memory_obtained=Erhaltener Speicher -dashboard.pointer_lookup_times=Anzahl Zeigerlookups -dashboard.memory_allocate_times=Speicheranforderungen -dashboard.memory_free_times=Speicherfreigaben -dashboard.current_heap_usage=Aktuelle Heap-Auslastung -dashboard.heap_memory_obtained=Erhaltener Heap-Arbeitsspeicher -dashboard.heap_memory_idle=Unbenutzter Heap-Arbeitsspeicher -dashboard.heap_memory_in_use=Benutzter-Heap-Arbeitsspeicher -dashboard.heap_memory_released=Freigegebener Heap-Arbeitsspeicher -dashboard.heap_objects=Heap-Objekte -dashboard.bootstrap_stack_usage=Bootstrap-Stack-Auslastung -dashboard.stack_memory_obtained=Erhaltener Stack-Arbeitsspeicher -dashboard.mspan_structures_usage=MSpan-Structures-Auslastung -dashboard.mspan_structures_obtained=Erhaltene MSpan-Structures -dashboard.mcache_structures_usage=MCache-Structures-Auslastung -dashboard.mcache_structures_obtained=Erhaltene MCache-Structures -dashboard.profiling_bucket_hash_table_obtained=Erhaltene Analysesatz-Hashtabellen -dashboard.gc_metadata_obtained=Erhaltene GC-Metadaten -dashboard.other_system_allocation_obtained=Andere erhaltene System-Allokationen -dashboard.next_gc_recycle=Nächster GC-Zyklus -dashboard.last_gc_time=Seit letztem GC-Zyklus -dashboard.total_gc_pause=Gesamte GC-Pause -dashboard.last_gc_pause=Letzte GC-Pause -dashboard.gc_times=GC-Zeiten dashboard.delete_old_actions=Alle alten Aktivitäten aus der Datenbank löschen dashboard.delete_old_actions.started=Löschen aller alten Aktivitäten aus der Datenbank gestartet. dashboard.update_checker=Update-Checker @@ -3046,18 +2980,6 @@ users.purge_help=Das Löschen des Benutzers inklusive all seiner Repositorys, Or users.still_own_packages=Dieser Benutzer besitzt noch ein oder mehrere Pakete, lösche diese Pakete zuerst. users.deletion_success=Das Konto wurde gelöscht. users.reset_2fa=2FA zurücksetzen -users.list_status_filter.menu_text=Filter -users.list_status_filter.reset=Zurücksetzen -users.list_status_filter.is_active=Aktiv -users.list_status_filter.not_active=Inaktiv -users.list_status_filter.is_admin=Administrator -users.list_status_filter.not_admin=Nicht-Administrator -users.list_status_filter.is_restricted=Eingeschränkt -users.list_status_filter.not_restricted=Unbegrenzt -users.list_status_filter.is_prohibit_login=Anmelden verbieten -users.list_status_filter.not_prohibit_login=Anmelden erlaubt -users.list_status_filter.is_2fa_enabled=2FA aktiviert -users.list_status_filter.not_2fa_enabled=2FA deaktiviert users.details=Benutzerdetails emails.email_manage_panel=Benutzer-E-Mails verwalten @@ -3184,8 +3106,8 @@ auths.oauth2_required_claim_value=Benötigter Claim-Wert auths.oauth2_required_claim_value_helper=Setze diesen Wert, damit Nutzer aus dieser Quelle sich nur anmelden dürfen, wenn sie einen Claim mit diesem Namen und Wert besitzen auths.oauth2_group_claim_name=Claim-Name, der Gruppennamen für diese Quelle angibt (optional). auths.oauth2_admin_group=Gruppen-Claim-Wert für Administratoren (optional – erfordert Claim-Namen oben). -auths.oauth2_restricted_group=Gruppen-Claim-Wert für eingeschränkte User. (Optional – erfordert Claim-Namen oben) -auths.oauth2_map_group_to_team=Gruppen aus OAuth-Claims den Organisationsteams zuordnen (optional – oben muss der Name des Claims angegeben werden). +auths.oauth2_restricted_group=Gruppen-Claim-Wert für eingeschränkte Benutzer (optional – erfordert Claim-Namen oben). +auths.oauth2_map_group_to_team=Gruppen aus OAuth-Claims den Organisationsteams zuordnen (optional – erfordert Claim-Namen oben). auths.oauth2_map_group_to_team_removal=Benutzer aus synchronisierten Teams entfernen, wenn der Benutzer nicht zur entsprechenden Gruppe gehört. auths.tips=Tipps auths.tips.oauth2.general=OAuth2-Authentifizierung @@ -3202,13 +3124,13 @@ auths.tip.twitter=Gehe auf %s, erstelle eine Anwendung und stelle sicher, dass d auths.tip.discord=Registriere unter %s eine neue Anwendung auths.tip.gitea=Registriere eine neue OAuth2-Anwendung. Eine Anleitung findest du unter %s auths.tip.yandex=`Erstelle eine neue Anwendung auf %s. Wähle folgende Berechtigungen aus dem Abschnitt „Yandex.Passport API“: „Zugriff auf E-Mail-Adresse“, „Zugriff auf Benutzeravatar“ und „Zugriff auf Benutzername, Vor- und Nachname, Geschlecht“` -auths.tip.mastodon=Gib eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder benutze die standardmäßige) -auths.edit=Authentifikationsquelle bearbeiten -auths.activated=Diese Authentifikationsquelle ist aktiviert +auths.tip.mastodon=Gib eine benutzerdefinierte URL für die Mastodon-Instanz ein, mit der du dich authentifizieren möchtest (oder verwende die Standard-URL) +auths.edit=Authentifizierungsquelle bearbeiten +auths.activated=Diese Authentifizierungsquelle ist aktiviert auths.new_success=Die Authentifizierung „%s“ wurde hinzugefügt. auths.update_success=Diese Authentifizierungsquelle wurde aktualisiert. auths.update=Authentifizierungsquelle aktualisieren -auths.delete=Authentifikationsquelle löschen +auths.delete=Authentifizierungsquelle löschen auths.delete_auth_title=Authentifizierungsquelle löschen auths.delete_auth_desc=Das Löschen einer Authentifizierungsquelle verhindert, dass Benutzer sich darüber anmelden können. Fortfahren? auths.still_in_used=Diese Authentifizierungsquelle wird noch verwendet. Bearbeite oder lösche zuerst alle Benutzer, die diese Authentifizierungsquelle benutzen. @@ -3228,7 +3150,7 @@ config.domain=Server-Domain config.offline_mode=Lokaler Modus config.disable_router_log=Router-Log deaktivieren config.run_user=Ausführen als -config.run_mode=Laufzeit-Modus +config.run_mode=Betriebsmodus config.git_version=Git-Version config.app_data_path=App-Datenpfad config.repo_root_path=Repository-Wurzelpfad @@ -3264,7 +3186,7 @@ config.db_ssl_mode=SSL config.db_path=Verzeichnis config.service_config=Service-Konfiguration -config.register_email_confirm=E-Mail-Bestätigung benötigt zum Registrieren +config.register_email_confirm=E-Mail-Bestätigung zur Registrierung erforderlich config.disable_register=Selbstregistrierung deaktivieren config.allow_only_internal_registration=Registrierung nur über Forgejo selbst erlauben config.allow_only_external_registration=Registrierung nur über externe Dienste erlauben @@ -3321,14 +3243,14 @@ config.cache_item_ttl=Cache-Item-TTL config.session_config=Session-Konfiguration config.session_provider=Session-Anbieter -config.provider_config=Provider-Einstellungen +config.provider_config=Anbieter-Einstellungen config.cookie_name=Cookie-Name config.gc_interval_time=GC-Intervall config.session_life_time=Session-Lebensdauer config.https_only=Nur HTTPS config.cookie_life_time=Cookie-Lebensdauer -config.picture_config=Bild-und-Profilbild-Konfiguration +config.picture_config=Bild- und Avatar-Konfiguration config.picture_service=Bilderdienst config.disable_gravatar=Gravatar deaktivieren config.enable_federated_avatar=Föderierte Profilbilder einschalten @@ -3375,26 +3297,6 @@ monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursac monitor.process.cancel_notices=Abbrechen: %s? monitor.process.children=Subprozesse -monitor.queues=Warteschlangen -monitor.queue=Warteschlange: %s -monitor.queue.name=Name -monitor.queue.type=Typ -monitor.queue.exemplar=Beispieltyp -monitor.queue.numberworkers=Anzahl der Worker -monitor.queue.activeworkers=Aktive Worker -monitor.queue.maxnumberworkers=Maximale Anzahl der Worker -monitor.queue.numberinqueue=Nummer in der Warteschlange -monitor.queue.review_add=Worker hinzufügen/prüfen -monitor.queue.settings.title=Pool-Einstellungen -monitor.queue.settings.desc=Pools wachsen dynamisch basierend auf der Blockierung der Arbeitswarteschlange. -monitor.queue.settings.maxnumberworkers=Maximale Anzahl an Workern -monitor.queue.settings.maxnumberworkers.placeholder=Derzeit %[1]d -monitor.queue.settings.maxnumberworkers.error=Die Anzahl der Worker muss eine Zahl sein -monitor.queue.settings.submit=Einstellungen aktualisieren -monitor.queue.settings.changed=Einstellungen aktualisiert -monitor.queue.settings.remove_all_items=Alle entfernen -monitor.queue.settings.remove_all_items_done=Alle Elemente in der Warteschlange wurden entfernt. - notices.system_notice_list=Systemmitteilungen notices.view_detail_header=Meldungsdetails ansehen notices.operations=Operationen @@ -3411,7 +3313,7 @@ notices.op=Aktion notices.delete_success=Diese Systemmeldung wurde gelöscht. self_check.database_fix_mysql = Für MySQL-/MariaDB-Benutzer: Du kannst den Befehl „forgejo doctor convert“ verwenden, um die Collation-Probleme zu lösen, oder du kannst das Problem mit „ALTER … COLLATE …“-SQLs manuell lösen. dashboard.sync_tag.started = Tag-Synchronisierung gestartet -self_check.database_collation_case_insensitive = Datenbank benutzt eine Collation %s, welcher der Groß-/Kleinschreibung egal ist. Obwohl Forgejo damit arbeiten könnte, könnte es ein paar seltene Fälle geben, bei denen es nicht wie erwartet funktioniert. +self_check.database_collation_case_insensitive = Die Datenbank verwendet die Collation %s, die nicht zwischen Groß- und Kleinschreibung unterscheidet. Forgejo kann damit arbeiten, es gibt aber seltene Fälle, in denen es nicht wie erwartet funktioniert. self_check = Selbstprüfung dashboard.sync_repo_tags = Tags aus Git-Daten zu Datenbank synchronisieren emails.change_email_text = Bist du dir sicher, dass du diese E-Mail-Addresse aktualisieren möchtest? @@ -3427,7 +3329,7 @@ auths.tip.gitlab_new = Registriere eine neue Anwendung auf %s auths.default_domain_name = Standarddomainname, der für die E-Mail-Adresse benutzt wird config.app_slogan = Instanz-Slogan config.cache_test_failed = Konnte den Cache nicht untersuchen: %v. -config.cache_test_succeeded = Cache-Test erfolgreich, eine Antwort erhalten in %s. +config.cache_test_succeeded = Cache-Test erfolgreich, Antwort in %s erhalten. config.cache_test = Cache testen config.cache_test_slow = Cache-Test erfolgreich, aber die Antwort ist langsam: %s. users.block.description = Diesem Benutzer verbieten, durch sein Konto mit diesem Dienst zu interagieren, und ihn am Einloggen hindern. @@ -3497,35 +3399,10 @@ raw_seconds=Sekunden raw_minutes=Minuten [dropzone] -default_message=Zum Hochladen hier klicken oder Datei ablegen. -invalid_input_type=Dateien dieses Dateityps können nicht hochgeladen werden. -file_too_big=Dateigröße ({{filesize}} MB) überschreitet die Maximalgröße ({{maxFilesize}} MB). -remove_file=Datei entfernen [notification] -notifications=Nachrichten -unread=Ungelesen -read=Gelesen -no_unread=Keine ungelesenen Benachrichtigungen. -no_read=Keine gelesenen Benachrichtigungen. -pin=Benachrichtigung pinnen -mark_as_read=Als gelesen markieren -mark_as_unread=Als ungelesen markieren -mark_all_as_read=Alle als gelesen markieren -subscriptions=Abonnements -watching=Beobachtet -no_subscriptions=Keine Abonnements [gpg] -default_key=Mit Standardschlüssel signiert -error.extract_sign=Die Signatur konnte nicht extrahiert werden -error.generate_hash=Es konnte kein Hash vom Commit generiert werden -error.no_committer_account=Es ist kein Account mit der E-Mail-Adresse des Committers verbunden -error.no_gpg_keys_found=Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden -error.not_signed_commit=Kein signierter Commit -error.failed_retrieval_gpg_keys=Fehler beim Abrufen eines Schlüssels des Committer-Kontos -error.probable_bad_signature=WARNHINWEIS! Obwohl ein Schlüssel mit dieser ID in der Datenbank existiert, verifiziert er nicht diesen Commit! Dieser Commit ist VERDÄCHTIG. -error.probable_bad_default_signature=WARNHINWEIS! Obwohl der Standardschlüssel diese ID hat, verifiziert er nicht diesen Commit! Dieser Commit ist VERDÄCHTIG. [units] unit=Einheit @@ -3533,183 +3410,11 @@ error.no_unit_allowed_repo=Du hast keine Berechtigung, auf etwas in diesem Repos error.unit_not_allowed=Du hast keine Berechtigung, um auf diesen Repository-Bereich zuzugreifen. [packages] -title=Pakete desc=Repository-Pakete verwalten. -empty=Noch keine Pakete vorhanden. -empty.documentation=Weitere Informationen zur Paket-Registry findest du in der Dokumentation. -empty.repo=Hast du ein Paket hochgeladen, das hier nicht angezeigt wird? Gehe zu den Paketeinstellungen und verlinke es mit diesem Repo. -registry.documentation=Für weitere Informationen zur %s-Registry, schaue in der Dokumentation nach. -filter.type=Typ -filter.type.all=Alle -filter.no_result=Keine Ergebnisse mit diesen Kriterien gefunden. -filter.container.tagged=Getaggt -filter.container.untagged=Nicht getaggt -published_by=%[1]s von %[3]s veröffentlicht -published_by_in=%[1]s von %[3]s in %[5]s veröffentlicht -installation=Installation -about=Über dieses Paket -requirements=Voraussetzungen -dependencies=Abhängigkeiten -keywords=Schlüsselwörter -details=Details -details.author=Autor -details.project_site=Projektwebseite -details.repository_site=Repository-Webseite -details.documentation_site=Dokumentationswebseite -details.license=Lizenz -assets=Assets -versions=Versionen -versions.view_all=Alle anzeigen -dependency.id=ID -dependency.version=Version -alpine.registry=Richte diese Registry ein, indem Du die URL in die /etc/apk/repositories-Datei hinzufügst: -alpine.registry.key=Lade den öffentlichen RSA-Key der Registry in den /etc/apk/keys/-Ordner, um die Signatur zu überprüfen: -alpine.registry.info=Wähle $branch und $repository aus der Liste unten. -alpine.install=Nutze folgenden Befehl, um das Paket zu installieren: -alpine.repository=Repository-Informationen -alpine.repository.branches=Branches -alpine.repository.repositories=Repositorys -alpine.repository.architectures=Architekturen -cargo.registry=Richte diese Registry in der Cargo-Konfigurationsdatei ein (z.B. ~/.cargo/config.toml): -cargo.install=Um das Paket mit Cargo zu installieren, führe den folgenden Befehl aus: -chef.registry=Richte diese Registry in deiner ~/.chef/config.rb-Datei ein: -chef.install=Nutze folgenden Befehl, um das Paket zu installieren: -composer.registry=Setze diese Paketverwaltung in deiner ~/.composer/config.json-Datei auf: -composer.install=Nutze folgenden Befehl, um das Paket mit Composer zu installieren: -composer.dependencies=Abhängigkeiten -composer.dependencies.development=Entwicklungsabhängigkeiten conan.details.repository=Repository -conan.registry=Diese Registry über die Kommandozeile einrichten: -conan.install=Um das Paket mit Conan zu installieren, führe den folgenden Befehl aus: -conda.registry=Richte diese Registry als Conda-Repository in deiner .condarc-Datei ein: -conda.install=Um das Paket mit Conda zu installieren, führe den folgenden Befehl aus: -container.details.type=Abbildtyp -container.details.platform=Plattform -container.pull=Lade das Container-Image von der Kommandozeile aus herunter: -container.digest=Prüfsumme -container.multi_arch=Betriebsystem/Architektur -container.layers=Abbildebenen -container.labels=Labels -container.labels.key=Schlüssel -container.labels.value=Wert -cran.registry=Richte diese Registry in deiner Rprofile.site-Datei ein: -cran.install=Nutze folgenden Befehl, um das Paket zu installieren: -debian.registry=Diese Registry über die Kommandozeile einrichten: -debian.registry.info=Wähle $distribution und $component aus der Liste unten. -debian.install=Nutze folgenden Befehl, um das Paket zu installieren: -debian.repository=Repository-Informationen -debian.repository.distributions=Distributionen -debian.repository.components=Komponenten -debian.repository.architectures=Architekturen -generic.download=Lade das Paket mit der Kommandozeile herunter: -go.install=Installiere das Paket über die Kommandozeile: -helm.registry=Diese Paketverwaltung über die Kommandozeile einrichten: -helm.install=Nutze folgenden Befehl, um das Paket zu installieren: -maven.registry=Setze diese Paketverwaltung in der pom.xml deines Projektes auf: -maven.install=Um das Paket zu verwenden, nimm Folgendes in den dependencies-Block in der pom.xml-Datei auf: -maven.install2=Über die Kommandozeile ausführen: -maven.download=Nutze folgendes Kommando, um die Abhängigkeit herunterzuladen: -nuget.registry=Diese Registry über die Kommandozeile einrichten: -nuget.install=Um das Paket mit NuGet zu installieren, führe den folgenden Befehl aus: -nuget.dependency.framework=Zielframework -npm.registry=Setze diese Paketverwaltung in der .npmrc deines Projektes auf: -npm.install=Um das Paket mit npm zu installieren, führe den folgenden Befehl aus: -npm.install2=oder füge es zur package.json-Datei hinzu: -npm.dependencies=Abhängigkeiten -npm.dependencies.development=Entwicklungsabhängigkeiten -npm.dependencies.peer=Peer-Abhängigkeiten -npm.dependencies.optional=Optionale Abhängigkeiten -npm.details.tag=Tag -pub.install=Um das Paket mit Dart zu installieren, führe den folgenden Befehl aus: -pypi.requires=Erfordert Python -pypi.install=Nutze folgenden Befehl, um das Paket mit pip zu installieren: -rpm.registry=Diese Registry über die Kommandozeile einrichten: -rpm.distros.redhat=auf RedHat-basierten Distributionen -rpm.distros.suse=auf SUSE-basierten Distributionen -rpm.install=Nutze folgenden Befehl, um das Paket zu installieren: -rpm.repository = Repository-Info -rpm.repository.architectures = Architekturen -rubygems.install=Um das Paket mit gem zu installieren, führe den folgenden Befehl aus: -rubygems.install2=oder füg es zum Gemfile hinzu: -rubygems.dependencies.runtime=Laufzeitabhängigkeiten -rubygems.dependencies.development=Entwicklungsabhängigkeiten -rubygems.required.ruby=Benötigt Ruby-Version -rubygems.required.rubygems=Benötigt RubyGem-Version -swift.registry=Diese Registry über die Kommandozeile einrichten: -swift.install=Füge das Paket deiner Package.swift-Datei hinzu: -swift.install2=und führe den folgenden Befehl aus: -vagrant.install=Um eine Vagrant-Box hinzuzufügen, führe den folgenden Befehl aus: -settings.link=Dieses Paket einem Repository zuweisen -settings.link.description=Wenn du ein Paket mit einem Repository verknüpfst, wird es in der Paketliste des Repositorys angezeigt. -settings.link.select=Repository auswählen -settings.link.button=Repository-Link aktualisieren -settings.link.success=Repository-Link wurde erfolgreich aktualisiert. -settings.link.error=Fehler beim Aktualisieren des Repository-Links. -settings.delete=Paket löschen -settings.delete.description=Das Löschen eines Pakets ist dauerhaft und kann nicht rückgängig gemacht werden. -settings.delete.notice=Du bist dabei, %s (%s) zu löschen. Dieser Vorgang ist unwiderruflich. Bist du sicher? -settings.delete.success=Das Paket wurde gelöscht. -settings.delete.error=Löschen des Pakets fehlgeschlagen. -owner.settings.cargo.title=Cargo-Registry-Index -owner.settings.cargo.initialize=Index initialisieren -owner.settings.cargo.initialize.description=Ein spezielles Index-Repository wird benötigt, um die Cargo-Registry zu nutzen. Diese Option wird dieses Repository (neu) erstellen und automatisch konfigurieren. -owner.settings.cargo.initialize.error=Cargo-Index konnte nicht initialisiert werden: %v -owner.settings.cargo.initialize.success=Der Cargo-Index wurde erfolgreich erstellt. -owner.settings.cargo.rebuild=Index neu erstellen -owner.settings.cargo.rebuild.description=Neubauen kann hilfreich sein, wenn der Index nicht mit den gespeicherten Cargo-Paketen synchronisiert ist. -owner.settings.cargo.rebuild.error=Cargo-Index konnte nicht neu erstellt werden: %v -owner.settings.cargo.rebuild.success=Der Cargo-Index wurde erfolgreich neu erstellt. -owner.settings.cleanuprules.title=Bereinigungsregeln -owner.settings.cleanuprules.add=Bereinigungsregel hinzufügen -owner.settings.cleanuprules.edit=Bereinigungsregel bearbeiten -owner.settings.cleanuprules.none=Es bestehen derzeit keine Bereinigungsregeln. -owner.settings.cleanuprules.preview=Vorschau der Bereinigungsregel -owner.settings.cleanuprules.preview.overview=%d Pakete sollen entfernt werden. -owner.settings.cleanuprules.preview.none=Bereinigungsregel stimmt mit keinem Paket überein. owner.settings.cleanuprules.enabled=Aktiviert -owner.settings.cleanuprules.pattern_full_match=Muster auf den vollständigen Paketnamen anwenden -owner.settings.cleanuprules.keep.title=Versionen, die diesen Regeln entsprechen, werden beibehalten, auch wenn sie mit einer Entfernungsregel unten übereinstimmen. -owner.settings.cleanuprules.keep.count=Behalte die aktuellsten owner.settings.cleanuprules.keep.count.1=1 Version pro Paket owner.settings.cleanuprules.keep.count.n=%d Versionen pro Paket -owner.settings.cleanuprules.keep.pattern=Behalte übereinstimmende Versionen -owner.settings.cleanuprules.keep.pattern.container=Die Version latest bei Container-Paketen wird immer behalten. -owner.settings.cleanuprules.remove.title=Versionen, die diesen Regeln entsprechen, werden entfernt, es sei denn, eine obige Regel besagt, sie zu behalten. -owner.settings.cleanuprules.remove.days=Entferne Versionen älter als -owner.settings.cleanuprules.remove.pattern=Entferne übereinstimmende Versionen -owner.settings.cleanuprules.success.update=Bereinigungsregel wurde aktualisiert. -owner.settings.cleanuprules.success.delete=Bereinigungsregel wurde gelöscht. -owner.settings.chef.title=Chef-Registry -owner.settings.chef.keypair=Schlüsselpaar generieren -owner.settings.chef.keypair.description=Anfragen an die Chef-Registry müssen zur Authentifizierung kryptografisch signiert werden. Beim Erstellen eines Schlüsselpaars wird nur der öffentliche Schlüssel in Forgejo gespeichert. Der private Schlüssel wird dir für die Verwendung mit knife bereitgestellt. Das Generieren eines neuen Schlüsselpaars überschreibt das vorherige. -rpm.repository.multiple_groups = Dieses Paket ist in mehreren Gruppen verfügbar. -owner.settings.cargo.rebuild.no_index = Kann nicht erneut erzeugen, es wurde kein Index initialisiert. -npm.dependencies.bundle = Gebündelte Abhängigkeiten -arch.pacman.helper.gpg = Trust-Zertifikat für pacman hinzufügen: -arch.pacman.repo.multi = %s hat die gleiche Version in verschiedenen Distributionen. -arch.pacman.repo.multi.item = Konfiguration für %s -arch.pacman.conf = Server mit verwandter Distribution und Architektur zu /etc/pacman.conf hinzufügen: -arch.pacman.sync = Paket mit pacman synchronisieren: -arch.version.properties = Versionseigenschaften -arch.version.description = Beschreibung -arch.version.provides = Bietet -arch.version.groups = Gruppe -arch.version.depends = Hängt ab von -arch.version.makedepends = Make-Abhängigkeit -arch.version.checkdepends = Prüfungs-Abhängigkeit -arch.version.conflicts = Konflikte -arch.version.replaces = Ersetzt -arch.version.backup = Backup -arch.version.optdepends = Optionale Abhängigkeit -container.images.title = Bilder -search_in_external_registry = In %s suchen -alt.registry = Diese Registry von der Befehlszeile aus einrichten: -alt.registry.install = Um das Paket zu installieren, folgenden Befehl ausführen: -alt.install = Paket installieren -alt.setup = Ein Repository zur Liste der verbundenen Repositorys hinzufügen (wähle die nötige Architektur anstelle von „_arch_“): -alt.repository = Repository-Infos -alt.repository.architectures = Architekturen -alt.repository.multiple_groups = Dieses Paket ist in verschiedenen Gruppen verfügbar. [secrets] secrets=Geheimnisse @@ -3727,66 +3432,8 @@ deletion.failed=Geheimnis konnte nicht entfernt werden. management=Geheimnisse verwalten [actions] -actions=Actions - unit.desc=Integrierte CI/CD-Pipelines mit Forgejo-Actions verwalten. -status.unknown=Unbekannt -status.waiting=Wartend -status.running=Laufend -status.success=Erfolg -status.failure=Fehler -status.cancelled=Abgebrochen -status.skipped=Übersprungen -status.blocked=Blockiert - -runners=Runner -runners.runner_manage_panel=Runner verwalten -runners.new=Neuen Runner erstellen -runners.new_notice=Wie man einen Runner startet -runners.status=Status -runners.id=ID -runners.name=Name -runners.owner_type=Typ -runners.description=Beschreibung -runners.labels=Labels -runners.last_online=Letzte Online-Zeit -runners.runner_title=Runner -runners.task_list=Letzte Aufgaben dieses Runners -runners.task_list.no_tasks=Es gibt noch keine Aufgabe. -runners.task_list.run=Ausführen -runners.task_list.status=Status -runners.task_list.repository=Repository -runners.task_list.commit=Commit -runners.task_list.done_at=Fertig um -runners.edit_runner=Runner bearbeiten -runners.update_runner=Änderungen anwenden -runners.update_runner_success=Runner erfolgreich aktualisiert -runners.update_runner_failed=Der Runner konnte nicht aktualisiert werden -runners.delete_runner=Diesen Runner löschen -runners.delete_runner_success=Runner erfolgreich gelöscht -runners.delete_runner_failed=Der Runner konnte nicht gelöscht werden -runners.delete_runner_header=Bestätigen, um diesen Runner zu löschen -runners.delete_runner_notice=Wenn eine Aufgabe auf diesem Runner ausgeführt wird, wird sie beendet und als fehlgeschlagen markiert. Dies könnte Workflows zerstören. -runners.none=Keine Runner verfügbar -runners.status.unspecified=Unbekannt -runners.status.idle=Inaktiv -runners.status.active=Aktiv -runners.status.offline=Offline -runners.version=Version -runners.reset_registration_token=Registrierungs-Token zurücksetzen -runners.reset_registration_token_success=Runner-Registrierungstoken erfolgreich zurückgesetzt - -runs.all_workflows=Alle Workflows -runs.commit=Commit -runs.scheduled=Geplant -runs.pushed_by=gepusht von -runs.invalid_workflow_helper=Die Workflow-Konfigurationsdatei ist ungültig. Bitte überprüfe deine Konfigurationsdatei: %s -runs.actor=Initiator -runs.status=Status -runs.actors_no_select=Alle Initiatoren -runs.status_no_select=Alle Status -runs.no_results=Keine passenden Ergebnisse gefunden. runs.no_runs=Der Workflow hat noch keine Ausführungen. workflow.disable=Workflow deaktivieren @@ -3797,27 +3444,8 @@ workflow.disabled=Workflow ist deaktiviert. need_approval_desc=Um Workflows für den Pull-Request eines Forks auszuführen, ist eine Genehmigung erforderlich. -variables=Variablen -variables.management=Variablen verwalten -variables.creation=Variable hinzufügen -variables.none=Es gibt noch keine Variablen. -variables.deletion=Variable entfernen -variables.deletion.description=Das Entfernen einer Variable ist dauerhaft und kann nicht rückgängig gemacht werden. Fortfahren? -variables.description=Variablen werden an bestimmte Aktionen übergeben und können nicht anderweitig gelesen werden. -variables.edit=Variable bearbeiten -variables.deletion.failed=Fehler beim Entfernen der Variable. -variables.deletion.success=Die Variable wurde entfernt. -variables.creation.failed=Fehler beim Hinzufügen der Variable. -variables.creation.success=Die Variable „%s“ wurde hinzugefügt. -variables.update.failed=Fehler beim Bearbeiten der Variable. -variables.update.success=Die Variable wurde bearbeitet. -runs.no_matching_online_runner_helper = Es existiert kein passender Online-Runner mit dem Label: %s -runs.no_workflows = Es existieren noch keine Workflows. runs.empty_commit_message = (leere Commit-Nachricht) variables.id_not_exist = Variable mit ID %d existiert nicht. -runs.workflow = Workflow -runs.no_job_without_needs = Der Workflow muss mindestens einen Job ohne Abhängigkeiten enthalten. -runs.no_job = Der Workflow muss mindestens einen Job enthalten workflow.dispatch.use_from = Workflow benutzen von workflow.dispatch.run = Workflow ausführen workflow.dispatch.input_required = Wert für Eingabe „%s“ erfordern. @@ -3827,8 +3455,7 @@ workflow.dispatch.trigger_found = Dieser Workflow hat einen workflow_dispatch workflow.dispatch.success = Ausführung des Workflows wurde erfolgreich angefragt. runs.expire_log_message = Logs wurden gelöscht, weil sie zu alt waren. runs.no_workflows.help_write_access = Keine Ahnung, wie man mit Forgejo Actions anfangen soll? Schau im Schnellstart in der Benutzerdokumentation vorbei, um deinen ersten Workflow zu schreiben, dann setze einen Forgejo-Runner auf, um deine Jobs auszuführen. -runs.no_workflows.help_no_write_access = Um über Forgejo Actions zu lernen, siehe die Dokumentation. -variables.not_found = Die Variable wurde nicht gefunden. +runs.no_workflows.help_no_write_access = Um mehr über Forgejo Actions zu erfahren, siehe die Dokumentation. [projects] type-1.display_name=Individuelles Projekt @@ -3862,7 +3489,7 @@ branch_kind = Branches suchen … commit_kind = Commits suchen … runner_kind = Runner suchen … no_results = Keine passenden Ergebnisse gefunden. -code_search_unavailable = Die Code-Suche ist momentan nicht verfügbar. Bitte kontaktiere den Webseitenadministrator. +code_search_unavailable = Die Code-Suche ist momentan nicht verfügbar. Bitte kontaktiere den Instanz-Administrator. keyword_search_unavailable = Die Suche mittels Schlüsselwort ist momentan nicht verfügbar. Bitte kontaktiere den Webseitenadministrator. exact = Exakt exact_tooltip = Nur Ergebnisse einbinden, die auf den exakten Suchbegriff passen @@ -3874,19 +3501,8 @@ regexp = RegExp regexp_tooltip = Suchbegriff als regulären Ausdruck interpretieren [markup] -filepreview.line = Zeile %[1]d in %[2]s -filepreview.truncated = Vorschau wurde gekürzt -filepreview.lines = Zeilen %[1]d bis %[2]d in %[3]s [munits.data] -gib = GiB -b = B -kib = KiB -tib = TiB -pib = PiB -mib = MiB -eib = EiB - [translation_meta] test = ok diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index 6e11db0cbf..4ad57c7bb5 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -37,18 +37,6 @@ twofa=Πιστοποίηση δύο παραγόντων twofa_scratch=Κωδικός μίας χρήσης (για πιστοποίηση δύο παραγόντων / 2FA) passcode=Κωδικός -webauthn_insert_key=Εισάγετε το κλειδί ασφαλείας σας -webauthn_sign_in=Πατήστε το κουμπί στο κλειδί ασφαλείας σας. Αν το κλειδί ασφαλείας σας δεν έχει κουμπί, αποσυνδέστε το και συνδέστε το ξανά. -webauthn_press_button=Παρακαλώ πατήστε το κουμπί στο κλειδί ασφαλείας… -webauthn_use_twofa=Χρησιμοποιήστε έναν κωδικό δύο παραγόντων από το τηλέφωνό σας -webauthn_error=Δεν ήταν δυνατή η επικοινωνία με το κλειδί ασφαλείας σας. -webauthn_unsupported_browser=Το πρόγραμμα περιήγησής σας δεν υποστηρίζει επί του παρόντος WebAuthn. -webauthn_error_unknown=Παρουσιάστηκε ένα άγνωστο σφάλμα. Παρακαλώ προσπαθήστε ξανά. -webauthn_error_insecure=Το WebAuthn υποστηρίζει μόνο ασφαλές συνδέσεις. Αν θέλετε να διεξάγετε δοκιμές μέσω HTTP, μπορείτε να χρησιμοποιήσετε την προέλευση «localhost» ή «127.0.0.1» -webauthn_error_unable_to_process=Ο διακομιστής δεν μπόρεσε να επεξεργαστεί το αίτημά σας. -webauthn_error_duplicated=Το κλειδί ασφαλείας δεν επιτρέπεται για αυτό το αίτημα. Βεβαιωθείτε ότι το κλειδί δεν έχει ήδη καταχωρηθεί. -webauthn_error_empty=Πρέπει να ορίσετε ένα όνομα για αυτό το κλειδί. -webauthn_error_timeout=Το χρονικό όριο έφτασε πριν το κλειδί να διαβαστεί. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά. repository=Αποθετήριο organization=Οργανισμός mirror=Αντίγραφο @@ -182,7 +170,7 @@ buttons.bold.tooltip=Προσθήκη έντονου κειμένου (Ctrl+B / buttons.italic.tooltip=Προσθήκη πλαγίου κειμένου (Ctrl+I / ⌘I) buttons.quote.tooltip=Παράθεση κειμένου buttons.code.tooltip=Προσθήκη κώδικα -buttons.link.tooltip=Προσθήκη συνδέσμου +buttons.link.tooltip=Προσθήκη συνδέσμου (Ctrl+K / ⌘K) buttons.list.unordered.tooltip=Προσθήκη απλής λίστας buttons.list.ordered.tooltip=Προσθήκη αριθμημένης λίστας buttons.list.task.tooltip=Προσθήκη λίστας εργασιών @@ -1154,14 +1142,6 @@ migrate_options_lfs_endpoint.label=Άκρο LFS migrate_options_lfs_endpoint.description=Η μεταφορά θα προσπαθήσει να χρησιμοποιήσει το Git remote για να καθορίσει τον διακομιστή LFS. Μπορείτε επίσης να καθορίσετε ένα δικό σας endpoint αν τα δεδομένα LFS του αποθετηρίου αποθηκεύονται κάπου αλλού. migrate_options_lfs_endpoint.description.local=Μια διαδρομή στο τοπικό διακομιστή επίσης υποστηρίζεται. migrate_options_lfs_endpoint.placeholder=Αν αφεθεί κενό, το άκρο θα προκύψει από το URL του κλώνου -migrate_items=Αντικείμενα μεταφοράς -migrate_items_wiki=Wiki -migrate_items_milestones=Ορόσημα -migrate_items_labels=Σήματα -migrate_items_issues=Ζητήματα -migrate_items_pullrequests=Pull requests -migrate_items_merge_requests=Merge requests -migrate_items_releases=Κυκλοφορίες migrate_repo=Μεταφορά αποθετηρίου migrate.clone_address=Μεταφορά / Κλωνοποίηση από το URL migrate.clone_address_desc=Το HTTP(S) ή το Git URL «κλωνοποίησης» ενός υπάρχοντος αποθετηρίου @@ -1180,16 +1160,6 @@ migrate.migrating=Γίνεται μεταφορά από το %s… migrate.migrating_failed=Η μεταφορά από το %s απέτυχε. migrate.migrating_failed.error=Αποτυχία μεταφοράς: %s migrate.migrating_failed_no_addr=Η μεταφορά απέτυχε. -migrate.migrating_git=Τα δεδομένα Git μεταφέρονται -migrate.migrating_topics=Τα θέματα μεταφέρονται -migrate.migrating_milestones=Τα ορόσημα μεταφέρονται -migrate.migrating_labels=Τα σήματα μεταφέρονται -migrate.migrating_releases=Οι κυκλοφορίες μεταφέρονται -migrate.migrating_issues=Τα ζητήματα μεταφέρονται -migrate.migrating_pulls=Μεταφέρονται τα pull requests -migrate.cancel_migrating_title=Ακύρωση μεταφοράς -migrate.cancel_migrating_confirm=Θέλετε να ακυρώσετε αυτή τη μεταφορά; - mirror_from=είδωλο του forked_from=forked από generated_from=παραγμένο από @@ -1565,7 +1535,7 @@ issues.ref_reopening_from=`αναφέρθηκε σε αυτό τ issues.ref_from=`από %[1]s` issues.author=Συγγραφέας issues.role.owner=Ιδιοκτήτης -issues.role.owner_helper=Αυτός ο χρήστης είναι ο ιδιοκτήτης αυτού του αποθετηρίου. +issues.role.owner_helper=Αυτός ο χρήστης είναι ένας ιδιοκτήτης αυτού του αποθετηρίου. issues.role.member=Μέλος issues.role.member_helper=Αυτός ο χρήστης είναι μέλος του οργανισμού, του οποίου ανήκει το repository. issues.role.collaborator=Συνεργάτης @@ -2634,14 +2604,8 @@ activity.navbar.pulse = Παλμός settings.add_collaborator_blocked_them = Δεν είναι δυνατή η προσθήκη του χρήστη ως συνεργάτη, καθώς έχει αποκλείσει τον κάτοχο του αποθετηρίου. settings.wiki_rename_branch_main_notices_2 = Θα αλλάξει το όνομα του εσωτερικού κλάδου για το wiki του αποθετηρίου %s. Οι υπάρχουσες υποβολές θα πρέπει να ενημερωθούν. settings.add_collaborator_blocked_our = Δεν είναι δυνατή η προσθήκη του χρήστη ως συνεργάτη, καθώς ο κάτοχος του αποθετηρίου τον έχει αποκλείσει. -n_branch_few = %s κλάδοι -n_tag_one = %s ετικέτα -n_tag_few = %s ετικέτες -n_commit_one = %s υποβολή stars = Αστέρια -n_branch_one = %s κλάδος commits.search_branch = Αυτός ο κλάδος -n_commit_few = %s υποβολές settings.sourcehut_builds.secrets = Μυστικά settings.add_webhook.invalid_path = Η τοποθεσία του αρχείου δεν μπορεί να περιέχει κενά, «.» ή «..». Δεν μπορεί να αρχίζει ή να τελειώνει με μία κάθετο. commits.browse_further = Περιήγηση περισσοτέρων @@ -2690,8 +2654,6 @@ settings.transfer.button = Παραχώρηση ιδιοκτησίας settings.matrix.room_id_helper = Το ID δωματίου μπορείτε να το βρείτε στο πρόγραμμα Element > Room Settings > Advanced > Internal room ID. Παράδειγμα: %s. wiki.search = Αναζήτηση wiki wiki.no_search_results = Κανένα αποτέλεσμα -n_release_one = %s κυκλοφορία -n_release_few = %s κυκλοφορίες release.hide_archive_links_helper = Απόκρυψη των αρχείων πηγαίου κώδικα που δημιουργούνται αυτόματα για αυτή την δημιοσίευση. Αυτή η ρύθμιση είναι χρήσιμη αν ανεβάζετε τα δικά σας αρχεία. release.type_attachment = Συνημμένο activity.published_prerelease_label = Προδημοσίευση @@ -2961,34 +2923,6 @@ dashboard.sync_external_users=Συγχρονισμός δεδομένων εξω dashboard.cleanup_hook_task_table=Εκκαθάριση πίνακα hook_task dashboard.cleanup_packages=Εκκαθάριση ληγμένων πακέτων dashboard.cleanup_actions=Καθαρισμός ληγμένων καταγραφών και συνημμένων από τις δράσεις -dashboard.server_uptime=Uptime διακομιστή -dashboard.current_goroutine=Τρέχουσες goroutines -dashboard.current_memory_usage=Τρέχουσα χρήση μνήμης -dashboard.total_memory_allocated=Συνολική χρησιμοποιούμενη μνήμη -dashboard.memory_obtained=Μνήμη που λαμβάνεται -dashboard.pointer_lookup_times=Πλήθος αναζητήσεων δείκτη -dashboard.memory_allocate_times=Κατανομές μνήμης -dashboard.memory_free_times=Ελευθερώσεις μνήμης -dashboard.current_heap_usage=Τρέχουσα χρήση heap -dashboard.heap_memory_obtained=Μνήμη heap που λαμβάνεται -dashboard.heap_memory_idle=Αδρανής μνήμη heap -dashboard.heap_memory_in_use=Μνήμη heap σε χρήση -dashboard.heap_memory_released=Μνήμη heap που απελευθερώθηκε -dashboard.heap_objects=Αντικείμενα στο heap -dashboard.bootstrap_stack_usage=Χρήση στοίβας bootstrap -dashboard.stack_memory_obtained=Μνήμη στοίβας που λαμβάνεται -dashboard.mspan_structures_usage=Χρήση δομών MSpan -dashboard.mspan_structures_obtained=Δομές MSpan που έχουν ληφθεί -dashboard.mcache_structures_usage=Χρήση δομών MCache -dashboard.mcache_structures_obtained=Δομές MCache που έχουν ληφθεί -dashboard.profiling_bucket_hash_table_obtained=Profiling bucket hash table που έχει ληφθεί -dashboard.gc_metadata_obtained=Μεταδεδομένα GC που έχουν ληφθεί -dashboard.other_system_allocation_obtained=Άλλες κατανομές συστήματος που έχουν ληφθεί -dashboard.next_gc_recycle=Επόμενη ανακύκλωση GC -dashboard.last_gc_time=Χρόνος από την τελευταία φορά που έγινε GC -dashboard.total_gc_pause=Σύνολο παύσης GC -dashboard.last_gc_pause=Τελευταία παύση GC -dashboard.gc_times=Χρόνοι GC dashboard.delete_old_actions=Διαγραφή όλων των παλιών δραστηριοτήτων από τη βάση δεδομένων dashboard.delete_old_actions.started=Ξεκίνησε η διαγραφή όλων των παλιών δραστηριοτήτων από τη βάση δεδομένων. dashboard.update_checker=Ελεγκτής ενημερώσεων @@ -3045,18 +2979,6 @@ users.purge_help=Εξαναγκαστική διαγραφή χρήστη καθ users.still_own_packages=Αυτός ο χρήστης εξακολουθεί να κατέχει ένα ή περισσότερα πακέτα, διαγράψτε αυτά τα πακέτα πρώτα. users.deletion_success=Ο λογαριασμός χρήστη έχει διαγραφεί. users.reset_2fa=Επαναφορά 2FA -users.list_status_filter.menu_text=Φίλτρο -users.list_status_filter.reset=Επαναφορά -users.list_status_filter.is_active=Ενεργό -users.list_status_filter.not_active=Ανενεργό -users.list_status_filter.is_admin=Διαχειριστής -users.list_status_filter.not_admin=Χωρίς δικαιώματα διαχειριστή -users.list_status_filter.is_restricted=Περιορισμένος -users.list_status_filter.not_restricted=Χωρίς περιορισμούς -users.list_status_filter.is_prohibit_login=Απαγορευμένη σύνδεση -users.list_status_filter.not_prohibit_login=Επιτρέπεται η σύνδεση -users.list_status_filter.is_2fa_enabled=Με ενεργοποιημένο 2FA -users.list_status_filter.not_2fa_enabled=Χωρίς 2FA users.details=Λεπτομέρειες χρήστη emails.email_manage_panel=Διαχείριση email χρηστών @@ -3376,26 +3298,6 @@ monitor.process.cancel_desc=Η ακύρωση μιας διαδικασίας μ monitor.process.cancel_notices=Ακύρωση: %s; monitor.process.children=Θυγατρικές -monitor.queues=Ουρές -monitor.queue=Ουρά: %s -monitor.queue.name=Όνομα -monitor.queue.type=Τύπος -monitor.queue.exemplar=Τύπος Υποδείγματος -monitor.queue.numberworkers=Αριθμός εργατών -monitor.queue.activeworkers=Ενεργοί εργάτες -monitor.queue.maxnumberworkers=Μέγιστος αριθμός εργατών -monitor.queue.numberinqueue=Πλήθος ουράς -monitor.queue.review_add=Εξέταση / προσθήκη εργατών -monitor.queue.settings.title=Ρυθμίσεις δεξαμενής -monitor.queue.settings.desc=Οι δεξαμενές αυξάνονται δυναμικά όταν υπάρχει φραγή της ουράς των εργατών τους. -monitor.queue.settings.maxnumberworkers=Μέγιστος Αριθμός Εργατών -monitor.queue.settings.maxnumberworkers.placeholder=Αυτή τη στιγμή %[1]d -monitor.queue.settings.maxnumberworkers.error=Ο μέγιστος αριθμός εργατών πρέπει να είναι αριθμός -monitor.queue.settings.submit=Ενημέρωση ρυθμίσεων -monitor.queue.settings.changed=Οι ρυθμίσεις ενημερώθηκαν -monitor.queue.settings.remove_all_items=Αφαίρεση όλων -monitor.queue.settings.remove_all_items_done=Όλα τα αντικείμενα στην ουρά αφαιρέθηκαν. - notices.system_notice_list=Ειδοποιήσεις συστήματος notices.view_detail_header=Προβολή λεπτομερειών ειδοποίησης notices.operations=Λειτουργίες @@ -3496,35 +3398,10 @@ raw_seconds=δευτερόλεπτα raw_minutes=λεπτά [dropzone] -default_message=Σύρετε αρχεία ή κάντε κλικ εδώ για να τα ανεβάσετε. -invalid_input_type=Δεν μπορείτε να ανεβάσετε αρχεία αυτού του τύπου. -file_too_big=Το μέγεθος αρχείου ({{filesize}} MB) υπερβαίνει το μέγιστο μέγεθος ({{maxFilesize}} MB). -remove_file=Αφαίρεση αρχείου [notification] -notifications=Ειδοποιήσεις -unread=Μη αναγνωσμένες -read=Αναγνωσμένες -no_unread=Καμία μη αναγνωσμένη ειδοποίηση. -no_read=Δεν υπάρχουν αναγνωσμένες ειδοποιήσεις. -pin=Καρφίτσωμα ειδοποίησης -mark_as_read=Σήμανση ως αναγνωσμένη -mark_as_unread=Σήμανση ως μη αναγνωσμένη -mark_all_as_read=Σήμανση όλων ως αναγνωσμένες -subscriptions=Συνδρομές -watching=Παρακολούθηση -no_subscriptions=Καμία συνδρομή [gpg] -default_key=Υπογραφή με το προεπιλεγμένο κλειδί -error.extract_sign=Αποτυχία εξαγωγής υπογραφής -error.generate_hash=Αποτυχία δημιουργίας hash της υποβολής -error.no_committer_account=Δεν υπάρχει λογαριασμός που συσχετίζεται με τη διεύθυνση email του υποβάλλοντα -error.no_gpg_keys_found=Δεν βρέθηκε γνωστό κλειδί για αυτήν την υπογραφή στη βάση δεδομένων -error.not_signed_commit=Η υποβολή δεν είναι υπογεγραμμένη -error.failed_retrieval_gpg_keys=Αποτυχία ανάκτησης κλειδιού που είναι συνδεδεμένο στο λογαριασμό του υποβάλλοντα -error.probable_bad_signature=ΠΡΟΣΟΧΗ! Αν και υπάρχει ένα κλειδί με αυτό το ID στη βάση δεδομένων δεν επαληθεύει αυτή την υποβολή! Αυτή η υποβολή είναι ΥΠΟΠΤΗ. -error.probable_bad_default_signature=ΠΡΟΣΟΧΗ! Αν και το προεπιλεγμένο κλειδί έχει αυτό το ID, δεν επαληθεύει αυτή την υποβολή! Αυτή η υποβολή είναι ΥΠΟΠΤΗ. [units] unit=Μονάδα @@ -3532,183 +3409,11 @@ error.no_unit_allowed_repo=Δεν σας επιτρέπεται η πρόσβα error.unit_not_allowed=Δεν σας επιτρέπεται η πρόσβαση σε αυτήν την μονάδα αποθετηρίου. [packages] -title=Πακέτα desc=Διαχείριση πακέτων μητρώου. -empty=Δεν υπάρχουν πακέτα ακόμα. -empty.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο πακέτων, συμβουλευτείτε τον οδηγό. -empty.repo=Μήπως ανεβάσατε ένα πακέτο, αλλά δεν εμφανίζεται εδώ; Πηγαίνετε στις ρυθμίσεις πακέτων και συνδέστε το σε αυτό το repository. -registry.documentation=Για περισσότερες πληροφορίες σχετικά με το μητρώο %s, συμβουλευτείτε τον οδηγό. -filter.type=Τύπος -filter.type.all=Όλα -filter.no_result=Το φίλτρο δεν παρήγαγε αποτελέσματα. -filter.container.tagged=Επισημάνθηκαν -filter.container.untagged=Χωρίς επισήμανση -published_by=Δημοσιεύθηκε %[1]s από %[3]s -published_by_in=Δημοσιεύθηκε %[1]s κατά %[3]s σε %[5]s -installation=Εγκατάσταση -about=Σχετικά με αυτό το πακέτο -requirements=Απαιτήσεις -dependencies=Εξαρτήσεις -keywords=Λέξεις κλειδιά -details=Λεπτομέρειες -details.author=Συγγραφέας -details.project_site=Ιστοσελίδα έργου -details.repository_site=Ιστοσελίδα αποθετηρίου -details.documentation_site=Ιστοσελίδα τεκμηρίωσης -details.license=Άδεια -assets=Πόροι -versions=Εκδόσεις -versions.view_all=Προβολή όλων -dependency.id=ID -dependency.version=Έκδοση -alpine.registry=Ρυθμίστε αυτό το μητρώο προσθέτοντας το url στο αρχείο /etc/apk/repositories: -alpine.registry.key=Αποθηκεύστε το δημόσιο κλειδί RSA του μητρώου στο φάκελο /etc/apk/keys/ για να επαληθεύσετε την υπογραφή ευρετηρίου: -alpine.registry.info=Επιλέξτε $branch και $repository από την παρακάτω λίστα. -alpine.install=Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή: -alpine.repository=Πληροφορίες αποθετηρίου -alpine.repository.branches=Κλάδοι -alpine.repository.repositories=Αποθετήρια -alpine.repository.architectures=Αρχιτεκτονικές -cargo.registry=Ρυθμίστε αυτό το μητρώο στις ρυθμίσεις του Cargo (για παράδειγμα ~/.cargo/config.toml): -cargo.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το Cargo, εκτελέστε την ακόλουθη εντολή: -chef.registry=Ρυθμίστε αυτό το μητρώο στο αρχείο ~/.chef/config.rb: -chef.install=Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή: -composer.registry=Ρυθμίστε αυτό το μητρώο στο αρχείο ~/.composer/config.json: -composer.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το Composer, εκτελέστε την ακόλουθη εντολή: -composer.dependencies=Εξαρτήσεις -composer.dependencies.development=Εξαρτήσεις ανάπτυξης conan.details.repository=Repository -conan.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών: -conan.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το Conan, εκτελέστε την ακόλουθη εντολή: -conda.registry=Ρυθμίστε αυτό το μητρώο ως repository Conda στο αρχείο .condarc: -conda.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το Conda, εκτελέστε την ακόλουθη εντολή: -container.details.type=Τύπος εικόνας -container.details.platform=Πλατφόρμα -container.pull=Κατεβάστε την εικόνα από τη γραμμή εντολών: -container.digest=Σύνοψη -container.multi_arch=ΛΣ / Αρχιτεκτονική -container.layers=Στρώματα εικόνας -container.labels=Ετικέτες -container.labels.key=Κλειδί -container.labels.value=Τιμή -cran.registry=Ρυθμίστε αυτό το μητρώο στο αρχείο Rprofile.site: -cran.install=Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή: -debian.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών: -debian.registry.info=Επιλέξτε $distribution και $component από την παρακάτω λίστα. -debian.install=Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή: -debian.repository=Πληροφορίες αποθετηρίου -debian.repository.distributions=Διανομές -debian.repository.components=Συστατικά -debian.repository.architectures=Αρχιτεκτονικές -generic.download=Λήψη πακέτου από τη γραμμή εντολών: -go.install=Εγκαταστήστε το πακέτο από τη γραμμή εντολών: -helm.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών: -helm.install=Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή: -maven.registry=Ρυθμίστε αυτό το μητρώο στο αρχείο pom.xml του έργου σας: -maven.install=Για να χρησιμοποιήσετε το πακέτο, συμπεριλάβετε τα ακόλουθα στη περιοχή dependencies στο αρχείο pom.xml: -maven.install2=Εκτέλεση μέσω γραμμής εντολών: -maven.download=Για να κατεβάσετε την εξάρτηση, εκτελέστε μέσω της γραμμής εντολών: -nuget.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών: -nuget.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το NuGet, εκτελέστε την ακόλουθη εντολή: -nuget.dependency.framework=Πλαίσιο Ανάπτυξης -npm.registry=Ρυθμίστε αυτό το μητρώο στο αρχείο .npmrc του έργου σας: -npm.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας npm, εκτελέστε την ακόλουθη εντολή: -npm.install2=ή προσθέστε το στο αρχείο package.json: -npm.dependencies=Εξαρτήσεις -npm.dependencies.development=Εξαρτήσεις ανάπτυξης -npm.dependencies.peer=Εξαρτήσεις ομότιμου -npm.dependencies.optional=Προαιρετικές εξαρτήσεις -npm.details.tag=Σήμανση -pub.install=Για να εγκαταστήσετε το πακέτο μέσω του Dart, εκτελέστε την ακόλουθη εντολή: -pypi.requires=Απαιτεί Python -pypi.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το pip, εκτελέστε την ακόλουθη εντολή: -rpm.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών: -rpm.distros.redhat=σε διανομές βασισμένες στο RedHat -rpm.distros.suse=σε διανομές με βάση το SUSE -rpm.install=Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή: -rpm.repository=Πληροφορίες αποθετηρίου -rpm.repository.architectures=Αρχιτεκτονικές -rubygems.install=Για να εγκαταστήσετε το πακέτο χρησιμοποιώντας το gem, εκτελέστε την ακόλουθη εντολή: -rubygems.install2=ή προσθέστε το στο Gemfile: -rubygems.dependencies.runtime=Εξαρτήσεις κατά την εκτέλεση -rubygems.dependencies.development=Εξαρτήσεις ανάπτυξης -rubygems.required.ruby=Απαιτεί την έκδοση Ruby -rubygems.required.rubygems=Απαιτεί έκδοση RubyGem -swift.registry=Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών: -swift.install=Προσθέστε το πακέτο στο αρχείο Package.swift: -swift.install2=και εκτελέστε την ακόλουθη εντολή: -vagrant.install=Για προσθήκη ενός κυτίου Vagrant, εκτελέστε την ακόλουθη εντολή: -settings.link=Σύνδεση αυτού του πακέτου με ένα repository -settings.link.description=Εάν συνδέσετε ένα πακέτο με ένα repository, το πακέτο περιλαμβάνεται στη λίστα πακέτων του repository. -settings.link.select=Επιλογή αποθετηρίου -settings.link.button=Ενημέρωση συνδέσμου αποθετηρίου -settings.link.success=Ο σύνδεσμος αποθετηρίου ενημερώθηκε επιτυχώς. -settings.link.error=Αποτυχία ενημέρωσης συνδέσμου αποθετηρίου. -settings.delete=Διαγραφή πακέτου -settings.delete.description=Η διαγραφή ενός πακέτου είναι μόνιμη και δεν μπορεί να αναιρεθεί. -settings.delete.notice=Πρόκειται να διαγράψετε το %s (%s). Αυτή η διαδικασία είναι μη αναστρέψιμη, είστε σίγουροι; -settings.delete.success=Το πακέτο έχει διαγραφεί. -settings.delete.error=Αποτυχία διαγραφής του πακέτου. -owner.settings.cargo.title=Ευρετήριο μητρώου Cargo -owner.settings.cargo.initialize=Αρχικοποίηση ευρετηρίου -owner.settings.cargo.initialize.description=Απαιτείται ένα ειδικό repository ευρετηρίου Git για τη χρήση του μητρώου Cargo. Χρησιμοποιώντας αυτή την επιλογή θα δημιουργηθεί ξανά το repository και θα ρυθμιστεί αυτόματα. -owner.settings.cargo.initialize.error=Αποτυχία αρχικοποίησης ευρετηρίου Cargo: %v -owner.settings.cargo.initialize.success=Ο ευρετήριο Cargo δημιουργήθηκε με επιτυχία. -owner.settings.cargo.rebuild=Ανανέωση ευρετηρίου -owner.settings.cargo.rebuild.description=Η ανοικοδόμηση μπορεί να είναι χρήσιμη εάν ο δείκτης δεν είναι συγχρονισμένος με τα αποθηκευμένα πακέτα Cargo. -owner.settings.cargo.rebuild.error=Αποτυχία αναδόμησης του ευρετηρίου Cargo: %v -owner.settings.cargo.rebuild.success=Το ευρετήριο Cargo αναπλάστηκε με επιτυχία. -owner.settings.cleanuprules.title=Κανόνες εκκαθάρισης -owner.settings.cleanuprules.add=Προσθήκη κανόνα εκκαθάρισης -owner.settings.cleanuprules.edit=Επεξεργασία κανόνα εκκαθάρισης -owner.settings.cleanuprules.none=Δεν υπάρχουν ακόμα κάποιοι κανόνες εκκαθάρισης. -owner.settings.cleanuprules.preview=Προεπισκόπηση κανόνα εκκαθάρισης -owner.settings.cleanuprules.preview.overview=%d πακέτα έχουν προγραμματιστεί να αφαιρεθούν. -owner.settings.cleanuprules.preview.none=Ο κανόνας εκκαθάρισης δεν ταιριάζει με κανένα πακέτο. owner.settings.cleanuprules.enabled=Ενεργοποιημένο -owner.settings.cleanuprules.pattern_full_match=Εφαρμογή μοτίβου στο πλήρες όνομα του πακέτου -owner.settings.cleanuprules.keep.title=Οι εκδόσεις που ταιριάζουν με αυτούς τους κανόνες παραμένουν, ακόμη και αν ταιριάζουν με έναν κανόνα αφαίρεσης παρακάτω. -owner.settings.cleanuprules.keep.count=Κράτησε το πιο πρόσφατο owner.settings.cleanuprules.keep.count.1=1 έκδοση ανά πακέτο owner.settings.cleanuprules.keep.count.n=%d εκδόσεις ανά πακέτο -owner.settings.cleanuprules.keep.pattern=Διατήρηση εκδόσεων που ταιριάζουν -owner.settings.cleanuprules.keep.pattern.container=Η τελευταία έκδοση διατηρείται πάντα για τα πακέτα Container. -owner.settings.cleanuprules.remove.title=Οι εκδόσεις που ταιριάζουν με αυτούς τους κανόνες καταργούνται, εκτός και αν ένας παραπάνω κανόνας ορίζει να μείνουν. -owner.settings.cleanuprules.remove.days=Αφαίρεση εκδόσεων παλαιότερων από -owner.settings.cleanuprules.remove.pattern=Αφαίρεση εκδόσεων που ταιριάζουν -owner.settings.cleanuprules.success.update=Ο κανόνας καθαρισμού ενημερώθηκε. -owner.settings.cleanuprules.success.delete=Ο κανόνας καθαρισμού διαγράφηκε. -owner.settings.chef.title=Μητρώο Chef -owner.settings.chef.keypair=Δημιουργία ζεύγους κλειδιών -owner.settings.chef.keypair.description=Οι αιτήσεις που στέλνονται στο μητρώο Chef πρέπει να ταυτοποιούνται με κρυπτογραφική υπογραφή. Όταν δημιουργείτε ένα ζεύγος κλειδιών, μόνο το δημόσιο κλειδί αποθηκεύεται στο Forgejo. Το ιδιωτικό κλειδί σας δίνεται για χρήση με το knife. Η αναδημιουργία ενός νέου ζεύγους κλειδιών αντικαθιστεί τα προηγούμενα. -rpm.repository.multiple_groups = Αυτό το πακέτο είναι διαθέσιμο σε πολλαπλά group. -owner.settings.cargo.rebuild.no_index = Η ανανέωση δεν είναι δυνατή, επειδή το ευρετήριο δεν έρχει αρχικοποιηθεί. -arch.version.conflicts = Συγκρούεται με -arch.version.replaces = Αντικαταστά -arch.pacman.repo.multi = Το %s έχει την ίδια έκδοση σε διαφορετικές διανομές. -arch.pacman.repo.multi.item = Ρυθμίσεις για %s -arch.pacman.helper.gpg = Προσθέστε το trust certificate για το pacman: -arch.pacman.conf = Προσθέστε τον server με τις σχετικές πληροφορίες διανομής και αρχιτεκτονικής επεξεργαστή στο /etc/pacman.conf: -arch.pacman.sync = «Συγχρονίστε» το πακέτο με το pacman: -arch.version.properties = Πληροφορίες έκδοσης -arch.version.description = Περιγραφή -arch.version.provides = Προσφέρει -arch.version.groups = Γκρουπ -arch.version.depends = Εξαρτάται -arch.version.optdepends = Εξαρτάται προαιρετικά από -arch.version.makedepends = Εξαρτήσεις make -arch.version.checkdepends = Εξαρτήσεις ελέγχου -container.images.title = Εικόνες -npm.dependencies.bundle = Ενσωματωμένες εξαρτήσεις -arch.version.backup = Αντίγραφο -alt.install = Εγκατάσταση πακέτου -alt.repository = Πληροφορίες αποθετηρίου -alt.repository.architectures = Αρχιτεκτονικές -alt.repository.multiple_groups = Αυτό το πακέτο είναι διαθέσιμο σε πολλαπλές ομάδες. -alt.registry.install = Για να εγκαταστήσετε το πακέτο, εκτελέστε την ακόλουθη εντολή: -alt.registry = Ρυθμίστε αυτό το μητρώο από τη γραμμή εντολών: -alt.setup = Προσθήκη ενός αποθετηρίου στη λίστα των συνδεδεμένων αποθετηρίων (επιλέξτε την απαραίτητη αρχιτεκτονική αντί του "_arch_"): -search_in_external_registry = Αναζήτηση σε %s [secrets] secrets=Μυστικά @@ -3726,68 +3431,8 @@ deletion.failed=Αποτυχία αφαίρεσης μυστικού. management=Διαχείριση μυστικών [actions] -actions=Actions - unit.desc=Διαχείριση δράσεων -status.unknown=Απροσδιόριστη -status.waiting=Σε αναμονή -status.running=Σε εκτέλεση -status.success=Επιτυχία -status.failure=Αποτυχία -status.cancelled=Ακυρώθηκε -status.skipped=Παρακάμφθηκε -status.blocked=Αποκλείστηκε - -runners=Εκτελεστές -runners.runner_manage_panel=Διαχείριση εκτελεστών -runners.new=Δημιουργία νέου εκτελεστή -runners.new_notice=Πώς να ξεκινήσετε έναν εκτελεστή -runners.status=Κατάσταση -runners.id=ID -runners.name=Όνομα -runners.owner_type=Τύπος -runners.description=Περιγραφή -runners.labels=Ετικέτες -runners.last_online=Τελευταία ώρα σύνδεσης -runners.runner_title=Εκτελεστής -runners.task_list=Πρόσφατες εργασίες στον εκτελεστή -runners.task_list.no_tasks=Δεν υπάρχει καμία εργασία ακόμα. -runners.task_list.run=Εκτέλεση -runners.task_list.status=Κατάσταση -runners.task_list.repository=Αποθετήριο -runners.task_list.commit=Υποβολή -runners.task_list.done_at=Έτοιμο στις -runners.edit_runner=Επεξεργασία Εκτελεστή -runners.update_runner=Ενημέρωση αλλαγών -runners.update_runner_success=Ο εκτελεστής ενημερώθηκε επιτυχώς -runners.update_runner_failed=Αποτυχία ενημέρωσης εκτελεστή -runners.delete_runner=Διαγραφή του εκτελεστή -runners.delete_runner_success=Ο εκτελεστής διαγράφηκε επιτυχώς -runners.delete_runner_failed=Αποτυχία διαγραφής εκτελεστή -runners.delete_runner_header=Επιβεβαιώστε για τη διαγραφή του εκτελεστή -runners.delete_runner_notice=Αν μια εργασία εκτελείται σε αυτόν τον εκτελεστή, θα τερματιστεί και θα επισημανθεί ως αποτυχημένη. Μπορεί να χαλάσει τη ροή εργασίας του χτισίματος. -runners.none=Δεν υπάρχουν διαθέσιμοι εκτελεστές -runners.status.unspecified=Άγνωστη -runners.status.idle=Αδρανής -runners.status.active=Ενεργό -runners.status.offline=Χωρίς Σύνδεση -runners.version=Έκδοση -runners.reset_registration_token=Επαναφορά διακριτικού εγγραφής -runners.reset_registration_token_success=Επιτυχής επανέκδοση διακριτικού εγγραφής του εκτελεστή - -runs.all_workflows=Όλες οι ροές εργασίας -runs.commit=Υποβολή -runs.scheduled=Προγραμματισμένα -runs.pushed_by=ωθήθηκε από -runs.invalid_workflow_helper=Το αρχείο ροής εργασίας δεν είναι έγκυρο. Ελέγξτε το αρχείο σας: %s -runs.no_matching_online_runner_helper=Κανένας δικτυακός δρομέας με ετικέτα: %s -runs.actor=Φορέας -runs.status=Κατάσταση -runs.actors_no_select=Όλοι οι φορείς -runs.status_no_select=Όλες οι καταστάσεις -runs.no_results=Δεν βρέθηκαν αποτελέσματα. -runs.no_workflows=Δεν υπάρχουν ροές εργασίας ακόμα. runs.no_runs=Η ροή εργασίας δεν έχει τρέξει ακόμα. runs.empty_commit_message=(κενό μήνυμα υποβολής) @@ -3799,24 +3444,7 @@ workflow.disabled=Η ροή εργασιών είναι απενεργοποιη need_approval_desc=Πρέπει να εγκριθεί η εκτέλεση ροών εργασίας για pull request από fork. -variables=Μεταβλητές -variables.management=Διαχείριση μεταβλητών -variables.creation=Προσθήκη μεταβλητή -variables.none=Δεν υπάρχουν ακόμα μεταβλητές. -variables.deletion=Αφαίρεση μεταβλητής -variables.deletion.description=Η αφαίρεση μιας μεταβλητής είναι μόνιμη και δεν μπορεί να αναιρεθεί. Συνέχεια; -variables.description=Η μεταβλητές θα δίνονται σε ορισμένες δράσεις και δεν μπορούν να διαβαστούν αλλιώς. -variables.edit=Επεξεργασία Μεταβλητής -variables.deletion.failed=Αποτυχία αφαίρεσης της μεταβλητής. -variables.deletion.success=Η μεταβλητή έχει αφαιρεθεί. -variables.creation.failed=Αποτυχία προσθήκης μεταβλητής. -variables.creation.success=Η μεταβλητή «%s» προστέθηκε. -variables.update.failed=Αποτυχία επεξεργασίας μεταβλητής. -variables.update.success=Η μεταβλητή έχει τροποποιηθεί. variables.id_not_exist = Η μεταβλητή με id %d δεν υπάρχει. -runs.workflow = Ροή εργασίας -runs.no_job_without_needs = Η ροή εργασίας πρέπει να περιέχει τουλάχιστον ένα έργο που δεν εξαρτάται από κάποιο άλλο έργο. -runs.no_job = Η ροή εργασιών πρέπει να περιέχει τουλάχιστον μία εργασία (job) workflow.dispatch.trigger_found = Αυτή η ροή εργασίας έχει ένα «event trigger» workflow_dispatch. workflow.dispatch.success = Η εκτέλεση της ροής εργασίας αιτήθηκε επιτυχώς. workflow.dispatch.input_required = Απαιτείται τιμή για την είσοδο «%s». @@ -3827,7 +3455,6 @@ workflow.dispatch.invalid_input_type = Μη έγκυρο είδος εισόδο runs.no_workflows.help_write_access = Δεν γνωρίζεται πώς να ξεκινήσετε με τις Δράσεις Forgejo; Δείτε την γρήγορη εκκίνηση στην τεκμηρίωση χρήστη για να γράψετε την πρώτη σας ροή εργασίας, στη συνέχεια ορίστε έναν εκτελεστή Forgejo για να εκτελέσετε τις εργασίες σας. runs.no_workflows.help_no_write_access = Για να μάθετε για τις Δράσεις Forgejo, δείτε την τεκμηρίωση. workflow.dispatch.warn_input_limit = Προβολή μόνο των πρώτων %d εισόδων. -variables.not_found = Αποτυχία εύρεσης της μεταβλητής. [projects] type-1.display_name=Ατομικό έργο @@ -3873,18 +3500,8 @@ regexp = Κανονική Έκφραση regexp_tooltip = Ερμηνεία του όρου αναζήτησης ως κανονική έκφραση [munits.data] -mib = MiB -gib = GiB -tib = TiB -pib = PiB -eib = EiB -b = B -kib = KiB [markup] -filepreview.line = Γραμμή %[1]d στο αρχείο %[2]s -filepreview.lines = Γραμμές %[1]d έως %[2]d στο αρχείο %[3]s -filepreview.truncated = Η προεπισκόπηση έχει περικοπεί [translation_meta] test = ok diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index ad560f9beb..a74cf843e9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -38,19 +38,6 @@ twofa = Two-factor authentication twofa_scratch = Two-factor scratch code passcode = Passcode -webauthn_insert_key = Insert your security key -webauthn_sign_in = Press the button on your security key. If your security key has no button, re-insert it. -webauthn_press_button = Please press the button on your security key… -webauthn_use_twofa = Use a two-factor code from your phone -webauthn_error = Could not read your security key. -webauthn_unsupported_browser = Your browser does not currently support WebAuthn. -webauthn_error_unknown = An unknown error occurred. Please retry. -webauthn_error_insecure = WebAuthn only supports secure connections. For testing over HTTP, you can use the origin "localhost" or "127.0.0.1" -webauthn_error_unable_to_process = The server could not process your request. -webauthn_error_duplicated = The security key is not permitted for this request. Please make sure that the key is not already registered. -webauthn_error_empty = You must set a name for this key. -webauthn_error_timeout = Timeout reached before your key could be read. Please reload this page and retry. - repository = Repository new_fork = New repository fork new_project_column = New column @@ -205,7 +192,7 @@ buttons.bold.tooltip = Add bold text (Ctrl+B / ⌘B) buttons.italic.tooltip = Add italic text (Ctrl+I / ⌘I) buttons.quote.tooltip = Quote text buttons.code.tooltip = Add code -buttons.link.tooltip = Add a link +buttons.link.tooltip = Add a link (Ctrl+K / ⌘K) buttons.list.unordered.tooltip = Add a bullet list buttons.list.ordered.tooltip = Add a numbered list buttons.list.task.tooltip = Add a list of tasks @@ -925,7 +912,7 @@ regenerate_token = Regenerate access_token_regeneration = Regenerate access token access_token_regeneration_desc = Regenerating a token will revoke access to your account for applications using it. This cannot be undone. Continue? regenerate_token_success = The token has been regenerated. Applications that use it no longer have access to your account and must be updated with the new token. -repo_and_org_access = Repository and Organization Access +repo_and_org_access = Repository and organization access permissions_public_only = Public only permissions_access_all = All (public, private, and limited) select_permissions = Select permissions @@ -1208,14 +1195,6 @@ migrate_options_lfs_endpoint.label = LFS endpoint migrate_options_lfs_endpoint.description = Migration will attempt to use your Git remote to determine the LFS server. You can also specify a custom endpoint if the repository LFS data is stored somewhere else. migrate_options_lfs_endpoint.description.local = A local server path is supported too. migrate_options_lfs_endpoint.placeholder = If left blank, the endpoint will be derived from the clone URL -migrate_items = Migration items -migrate_items_wiki = Wiki -migrate_items_milestones = Milestones -migrate_items_labels = Labels -migrate_items_issues = Issues -migrate_items_pullrequests = Pull requests -migrate_items_merge_requests = Merge requests -migrate_items_releases = Releases migrate_repo = Migrate repository migrate.repo_desc_helper = Leave empty to import existing description migrate.clone_address = Migrate / Clone from URL @@ -1235,16 +1214,6 @@ migrate.migrating = Migrating from %s … migrate.migrating_failed = Migrating from %s failed. migrate.migrating_failed.error = Failed to migrate: %s migrate.migrating_failed_no_addr = Migration failed. -migrate.migrating_git = Migrating Git data -migrate.migrating_topics = Migrating topics -migrate.migrating_milestones = Migrating milestones -migrate.migrating_labels = Migrating labels -migrate.migrating_releases = Migrating releases -migrate.migrating_issues = Migrating issues -migrate.migrating_pulls = Migrating pull requests -migrate.cancel_migrating_title = Cancel migration -migrate.cancel_migrating_confirm = Do you want to cancel this migration? - mirror_from = mirror of forked_from = forked from generated_from = generated from @@ -1289,15 +1258,6 @@ milestones = Milestones org_labels_desc = Organization level labels that can be used with all repositories under this organization org_labels_desc_manage = manage -n_commit_one=%s commit -n_commit_few=%s commits -n_branch_one=%s branch -n_branch_few=%s branches -n_tag_one=%s tag -n_tag_few=%s tags -n_release_one = %s release -n_release_few = %s releases - released_this = released this file.title = %s at %s file_raw = Raw @@ -1555,7 +1515,6 @@ issues.remove_ref_at = `removed reference %s %s` issues.add_ref_at = `added reference %s %s` issues.delete_branch_at = `deleted branch %s %s` issues.filter_label = Label -issues.filter_label_exclude = Use Alt + Click to exclude labels issues.filter_label_no_select = All labels issues.filter_label_select_no_label = No label issues.filter_milestone = Milestone @@ -1651,7 +1610,7 @@ issues.author = Author issues.author.tooltip.issue = This user is the author of this issue. issues.author.tooltip.pr = This user is the author of this pull request. issues.role.owner = Owner -issues.role.owner_helper = This user is the owner of this repository. +issues.role.owner_helper = This user is an owner of this repository. issues.role.member = Member issues.role.member_helper = This user is a member of the organization owning this repository. issues.role.collaborator = Collaborator @@ -2570,7 +2529,7 @@ settings.matrix.access_token_helper = It is recommended to setup a dedicated Mat settings.matrix.room_id_helper = The Room ID can be retrieved from the Element web client > Room Settings > Advanced > Internal room ID. Example: %s. settings.archive.button = Archive repo settings.archive.header = Archive this repo -settings.archive.text = Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any issues or pull requests. +settings.archive.text = Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any issues or pull requests. Documenting the archival reason is recommended to guide future developers who plan to fork the repository. settings.archive.success = The repo was successfully archived. settings.archive.error = An error occurred while trying to archive the repo. See the log for more details. settings.archive.error_ismirror = You cannot archive a mirrored repo. @@ -2825,7 +2784,6 @@ team_name = Team name team_desc = Description team_name_helper = Team names should be short and memorable. team_desc_helper = Describe the purpose or role of the team. -team_access_desc = Repository access team_permission_desc = Permission team_unit_desc = Allow access to repository sections team_unit_disabled = (Disabled) @@ -2982,34 +2940,6 @@ dashboard.sync_external_users = Synchronize external user data dashboard.cleanup_hook_task_table = Clean up hook_task table dashboard.cleanup_packages = Clean up expired packages dashboard.cleanup_actions = Clean up expired logs and artifacts from actions -dashboard.server_uptime = Server uptime -dashboard.current_goroutine = Current goroutines -dashboard.current_memory_usage = Current memory usage -dashboard.total_memory_allocated = Total memory allocated -dashboard.memory_obtained = Memory obtained -dashboard.pointer_lookup_times = Pointer lookup times -dashboard.memory_allocate_times = Memory allocations -dashboard.memory_free_times = Memory frees -dashboard.current_heap_usage = Current heap usage -dashboard.heap_memory_obtained = Heap memory obtained -dashboard.heap_memory_idle = Heap memory idle -dashboard.heap_memory_in_use = Heap memory in use -dashboard.heap_memory_released = Heap memory released -dashboard.heap_objects = Heap objects -dashboard.bootstrap_stack_usage = Bootstrap stack usage -dashboard.stack_memory_obtained = Stack memory obtained -dashboard.mspan_structures_usage = MSpan structures usage -dashboard.mspan_structures_obtained = MSpan structures obtained -dashboard.mcache_structures_usage = MCache structures usage -dashboard.mcache_structures_obtained = MCache structures obtained -dashboard.profiling_bucket_hash_table_obtained = Profiling bucket hash table obtained -dashboard.gc_metadata_obtained = GC metadata obtained -dashboard.other_system_allocation_obtained = Other system allocation obtained -dashboard.next_gc_recycle = Next GC recycle -dashboard.last_gc_time = Time since last GC -dashboard.total_gc_pause = Total GC pause -dashboard.last_gc_pause = Last GC pause -dashboard.gc_times = GC times dashboard.delete_old_actions = Delete all old activities from database dashboard.delete_old_actions.started = Delete all old activities from database started. dashboard.update_checker = Update checker @@ -3073,18 +3003,6 @@ users.purge_help = Forcibly delete user and any repositories, organizations, and users.still_own_packages = This user still owns one or more packages, delete these packages first. users.deletion_success = The user account has been deleted. users.reset_2fa = Reset 2FA -users.list_status_filter.menu_text = Filter -users.list_status_filter.reset = Reset -users.list_status_filter.is_active = Active -users.list_status_filter.not_active = Inactive -users.list_status_filter.is_admin = Admin -users.list_status_filter.not_admin = Not admin -users.list_status_filter.is_restricted = Restricted -users.list_status_filter.not_restricted = Not restricted -users.list_status_filter.is_prohibit_login = Prohibit login -users.list_status_filter.not_prohibit_login = Allow login -users.list_status_filter.is_2fa_enabled = 2FA enabled -users.list_status_filter.not_2fa_enabled = 2FA disabled users.details = User details emails.email_manage_panel = Manage user emails @@ -3135,13 +3053,13 @@ packages.published = Published defaulthooks = Default webhooks defaulthooks.desc = Webhooks automatically make HTTP POST requests to a server when certain Forgejo events trigger. Webhooks defined here are defaults and will be copied into all new repositories. Read more in the webhooks guide. -defaulthooks.add_webhook = Add Default Webhook -defaulthooks.update_webhook = Update Default Webhook +defaulthooks.add_webhook = Add default webhook +defaulthooks.update_webhook = Update default webhook systemhooks = System webhooks systemhooks.desc = Webhooks automatically make HTTP POST requests to a server when certain Forgejo events trigger. Webhooks defined here will act on all repositories on the system, so please consider any performance implications this may have. Read more in the webhooks guide. -systemhooks.add_webhook = Add System Webhook -systemhooks.update_webhook = Update System Webhook +systemhooks.add_webhook = Add system webhook +systemhooks.update_webhook = Update system webhook auths.auth_manage_panel = Manage authentication sources auths.new = Add authentication source @@ -3404,26 +3322,6 @@ monitor.process.cancel = Cancel process monitor.process.cancel_desc = Canceling a process may cause data loss monitor.process.cancel_notices = Cancel: %s? -monitor.queues = Queues -monitor.queue = Queue: %s -monitor.queue.name = Name -monitor.queue.type = Type -monitor.queue.exemplar = Exemplar Type -monitor.queue.numberworkers = Number of workers -monitor.queue.activeworkers = Active workers -monitor.queue.maxnumberworkers = Max Number of workers -monitor.queue.numberinqueue = Number in queue -monitor.queue.review_add = Review / add workers -monitor.queue.settings.title = Pool settings -monitor.queue.settings.desc = Pools dynamically grow in response to their worker queue blocking. -monitor.queue.settings.maxnumberworkers = Max Number of workers -monitor.queue.settings.maxnumberworkers.placeholder = Currently %[1]d -monitor.queue.settings.maxnumberworkers.error = Max number of workers must be a number -monitor.queue.settings.submit = Update settings -monitor.queue.settings.changed = Settings updated -monitor.queue.settings.remove_all_items = Remove all -monitor.queue.settings.remove_all_items_done = All items in the queue have been removed. - notices.system_notice_list = System notices notices.view_detail_header = Notice details notices.operations = Operations @@ -3439,7 +3337,7 @@ notices.desc = Description notices.op = Op. notices.delete_success = The system notices have been deleted. -self_check.no_problem_found = No problem found yet. +self_check.no_problem_found = No problems found yet. self_check.database_collation_mismatch = Expect database to use collation: %s self_check.database_collation_case_insensitive = Database is using a collation %s, which is an insensitive collation. Although Forgejo could work with it, there might be some rare cases which don't work as expected. self_check.database_inconsistent_collation_columns = Database is using collation %s, but these columns are using mismatched collations. It might cause some unexpected problems. @@ -3499,232 +3397,26 @@ raw_seconds = seconds raw_minutes = minutes [munits.data] -b = B -kib = KiB -mib = MiB -gib = GiB -tib = TiB -pib = PiB -eib = EiB [dropzone] -default_message = Drop files or click here to upload. -invalid_input_type = You cannot upload files of this type. -file_too_big = File size ({{filesize}} MB) exceeds the maximum size of ({{maxFilesize}} MB). -remove_file = Remove file [notification] -notifications = Notifications -unread = Unread -read = Read -no_unread = No unread notifications. -no_read = No read notifications. -pin = Pin notification -mark_as_read = Mark as read -mark_as_unread = Mark as unread -mark_all_as_read = Mark all as read -subscriptions = Subscriptions -watching = Watching -no_subscriptions = No subscriptions [gpg] -default_key=Signed with default key -error.extract_sign = Failed to extract signature -error.generate_hash = Failed to generate hash of commit -error.no_committer_account = No account linked to committer's email address -error.no_gpg_keys_found = No known key found for this signature in database -error.not_signed_commit = Not a signed commit -error.failed_retrieval_gpg_keys = Failed to retrieve any key attached to the committer's account -error.probable_bad_signature = WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS. -error.probable_bad_default_signature = WARNING! Although the default key has this ID it does not verify this commit! This commit is SUSPICIOUS. [units] unit = Unit error.no_unit_allowed_repo = You are not allowed to access any section of this repository. [packages] -title = Packages -empty = There are no packages yet. -empty.documentation = For more information on the package registry, see the documentation. -empty.repo = Did you upload a package, but it's not shown here? Go to package settings and link it to this repo. -registry.documentation = For more information on the %s registry, see the documentation. -filter.type = Type -filter.type.all = All -filter.no_result = Your filter produced no results. -filter.container.tagged = Tagged -filter.container.untagged = Untagged -published_by = Published %[1]s by %[3]s -published_by_in = Published %[1]s by %[3]s in %[5]s -installation = Installation -about = About this package -requirements = Requirements -dependencies = Dependencies -keywords = Keywords -details = Details -details.author = Author -details.project_site = Project website -details.repository_site = Repository website -details.documentation_site = Documentation website -details.license = License -assets = Assets -versions = Versions -versions.view_all = View all -dependency.id = ID -dependency.version = Version -search_in_external_registry = Search in %s -alpine.registry = Setup this registry by adding the url in your /etc/apk/repositories file: -alpine.registry.key = Download the registry public RSA key into the /etc/apk/keys/ folder to verify the index signature: -alpine.registry.info = Choose $branch and $repository from the list below. -alpine.install = To install the package, run the following command: -alpine.repository = Repository info -alpine.repository.branches = Branches -alpine.repository.repositories = Repositories -alpine.repository.architectures = Architectures -arch.pacman.helper.gpg = Add trust certificate for pacman: -arch.pacman.repo.multi = %s has the same version in different distributions. -arch.pacman.repo.multi.item = Configuration for %s -arch.pacman.conf = Add server with related distribution and architecture to /etc/pacman.conf : -arch.pacman.sync = Sync package with pacman: -arch.version.properties = Version properties -arch.version.description = Description -arch.version.provides = Provides -arch.version.groups = Group -arch.version.depends = Depends -arch.version.optdepends = Optional depends -arch.version.makedepends = Make depends -arch.version.checkdepends = Check depends -arch.version.conflicts = Conflicts -arch.version.replaces = Replaces -arch.version.backup = Backup -cargo.registry = Setup this registry in the Cargo configuration file (for example ~/.cargo/config.toml): -cargo.install = To install the package using Cargo, run the following command: -chef.registry = Setup this registry in your ~/.chef/config.rb file: -chef.install = To install the package, run the following command: -composer.registry = Setup this registry in your ~/.composer/config.json file: -composer.install = To install the package using Composer, run the following command: -composer.dependencies = Dependencies -composer.dependencies.development = Development dependencies -conan.registry = Setup this registry from the command line: -conan.install = To install the package using Conan, run the following command: -conda.registry = Setup this registry as a Conda repository in your .condarc file: -conda.install = To install the package using Conda, run the following command: -container.images.title = Images -container.details.type = Image type -container.details.platform = Platform -container.pull = Pull the image from the command line: -container.digest = Digest -container.multi_arch = OS / Arch -container.layers = Image layers -container.labels = Labels -container.labels.key = Key -container.labels.value = Value -cran.registry = Setup this registry in your Rprofile.site file: -cran.install = To install the package, run the following command: -debian.registry = Setup this registry from the command line: -debian.registry.info = Choose $distribution and $component from the list below. -debian.install = To install the package, run the following command: -debian.repository = Repository info -debian.repository.distributions = Distributions -debian.repository.components = Components -debian.repository.architectures = Architectures -generic.download = Download package from the command line: -go.install = Install the package from the command line: -helm.registry = Setup this registry from the command line: -helm.install = To install the package, run the following command: -maven.registry = Setup this registry in your project pom.xml file: -maven.install = To use the package include the following in the dependencies block in the pom.xml file: -maven.install2 = Run via command line: -maven.download = To download the dependency, run via command line: -nuget.registry = Setup this registry from the command line: -nuget.install = To install the package using NuGet, run the following command: -nuget.dependency.framework = Target Framework -npm.registry = Setup this registry in your project .npmrc file: -npm.install = To install the package using npm, run the following command: -npm.install2 = or add it to the package.json file: -npm.dependencies = Dependencies -npm.dependencies.development = Development dependencies -npm.dependencies.bundle = Bundled dependencies -npm.dependencies.peer = Peer dependencies -npm.dependencies.optional = Optional dependencies -npm.details.tag = Tag -pub.install = To install the package using Dart, run the following command: -pypi.requires = Requires Python -pypi.install = To install the package using pip, run the following command: -rpm.registry = Setup this registry from the command line: -rpm.distros.redhat = on RedHat based distributions -rpm.distros.suse = on SUSE based distributions -rpm.install = To install the package, run the following command: -rpm.repository = Repository info -rpm.repository.architectures = Architectures -rpm.repository.multiple_groups = This package is available in multiple groups. -alt.registry = Setup this registry from the command line: -alt.registry.install = To install the package, run the following command: -alt.install = Install package -alt.setup = Add a repository to the list of connected repositories (choose the necessary architecture instead of "_arch_"): -alt.repository = Repository info -alt.repository.architectures = Architectures -alt.repository.multiple_groups = This package is available in multiple groups. -rubygems.install = To install the package using gem, run the following command: -rubygems.install2 = or add it to the Gemfile: -rubygems.dependencies.runtime = Runtime dependencies -rubygems.dependencies.development = Development dependencies -rubygems.required.ruby = Requires Ruby version -rubygems.required.rubygems = Requires RubyGem version -swift.registry = Setup this registry from the command line: -swift.install = Add the package in your Package.swift file: -swift.install2 = and run the following command: -vagrant.install = To add a Vagrant box, run the following command: -settings.link = Link this package to a repository -settings.link.description = If you link a package with a repository, the package is listed in the repository's package list. -settings.link.select = Select repository -settings.link.button = Update repository link -settings.link.success = Repository link was successfully updated. -settings.link.error = Failed to update repository link. -settings.delete = Delete package -settings.delete.description = Deleting a package is permanent and cannot be undone. -settings.delete.notice = You are about to delete %s (%s). This operation is irreversible, are you sure? -settings.delete.success = The package has been deleted. -settings.delete.error = Failed to delete the package. -owner.settings.cargo.title = Cargo registry index -owner.settings.cargo.initialize = Initialize index -owner.settings.cargo.initialize.description = A special index Git repository is needed to use the Cargo registry. Using this option will (re-)create the repository and configure it automatically. -owner.settings.cargo.initialize.error = Failed to initialize Cargo index: %v -owner.settings.cargo.initialize.success = The Cargo index was successfully created. -owner.settings.cargo.rebuild = Rebuild index -owner.settings.cargo.rebuild.description = Rebuilding can be useful if the index is not synchronized with the stored Cargo packages. -owner.settings.cargo.rebuild.error = Failed to rebuild Cargo index: %v -owner.settings.cargo.rebuild.success = The Cargo index was successfully rebuilt. -owner.settings.cargo.rebuild.no_index = Cannot rebuild, no index is initialized. -owner.settings.cleanuprules.title = Cleanup rules -owner.settings.cleanuprules.add = Add cleanup rule -owner.settings.cleanuprules.edit = Edit cleanup rule -owner.settings.cleanuprules.none = There are no cleanup rules yet. -owner.settings.cleanuprules.preview = Cleanup rule preview -owner.settings.cleanuprules.preview.overview = %d packages are scheduled to be removed. -owner.settings.cleanuprules.preview.none = Cleanup rule does not match any packages. -owner.settings.cleanuprules.pattern_full_match = Apply pattern to full package name -owner.settings.cleanuprules.keep.title = Versions that match these rules are kept, even if they match a removal rule below. -owner.settings.cleanuprules.keep.count = Keep the most recent owner.settings.cleanuprules.keep.count.1 = 1 version per package owner.settings.cleanuprules.keep.count.n = %d versions per package -owner.settings.cleanuprules.keep.pattern = Keep versions matching -owner.settings.cleanuprules.keep.pattern.container = The latest version is always kept for Container packages. -owner.settings.cleanuprules.remove.title = Versions that match these rules are removed, unless a rule above says to keep them. -owner.settings.cleanuprules.remove.days = Remove versions older than -owner.settings.cleanuprules.remove.pattern = Remove versions matching -owner.settings.cleanuprules.success.update = Cleanup rule has been updated. -owner.settings.cleanuprules.success.delete = Cleanup rule has been deleted. -owner.settings.chef.title = Chef registry -owner.settings.chef.keypair = Generate key pair -owner.settings.chef.keypair.description = Requests sent to the Chef registry must be cryptographically signed as a means of authentication. When generating a keypair, only the public key is stored on Forgejo. The private key is provided to you to be used with knife. Generating a new keypair will overwrite the previous one. [secrets] secrets = Secrets description = Secrets will be passed to certain actions and cannot be read otherwise. none = There are no secrets yet. creation = Add secret -creation.name_placeholder = case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_ -creation.value_placeholder = Input any content. Whitespace at the start and end will be omitted. creation.success = The secret "%s" has been added. creation.failed = Failed to add secret. deletion = Remove secret @@ -3734,69 +3426,6 @@ deletion.failed = Failed to remove secret. management = Manage secrets [actions] -actions = Actions - -status.unknown = Unknown -status.waiting = Waiting -status.running = Running -status.success = Success -status.failure = Failure -status.cancelled = Canceled -status.skipped = Skipped -status.blocked = Blocked - -runners = Runners -runners.runner_manage_panel = Manage runners -runners.new = Create new runner -runners.new_notice = How to start a runner -runners.status = Status -runners.id = ID -runners.name = Name -runners.owner_type = Type -runners.description = Description -runners.labels = Labels -runners.last_online = Last online time -runners.runner_title = Runner -runners.task_list = Recent tasks on this runner -runners.task_list.no_tasks = There is no task yet. -runners.task_list.run = Run -runners.task_list.status = Status -runners.task_list.repository = Repository -runners.task_list.commit = Commit -runners.task_list.done_at = Done at -runners.edit_runner = Edit Runner -runners.update_runner = Update changes -runners.update_runner_success = Runner updated successfully -runners.update_runner_failed = Failed to update runner -runners.delete_runner = Delete this runner -runners.delete_runner_success = Runner deleted successfully -runners.delete_runner_failed = Failed to delete runner -runners.delete_runner_header = Confirm to delete this runner -runners.delete_runner_notice = If a task is running on this runner, it will be terminated and marked as failed. It may break building workflow. -runners.none = No runners available -runners.status.unspecified = Unknown -runners.status.idle = Idle -runners.status.active = Active -runners.status.offline = Offline -runners.version = Version -runners.reset_registration_token = Reset registration token -runners.reset_registration_token_success = Runner registration token reset successfully - -runs.all_workflows = All workflows -runs.commit = Commit -runs.scheduled = Scheduled -runs.pushed_by = pushed by -runs.workflow = Workflow -runs.invalid_workflow_helper = Workflow config file is invalid. Please check your config file: %s -runs.no_matching_online_runner_helper = No matching online runner with label: %s -runs.no_job_without_needs = The workflow must contain at least one job without dependencies. -runs.no_job = The workflow must contain at least one job -runs.actor = Actor -runs.status = Status -runs.actors_no_select = All actors -runs.status_no_select = All status -runs.no_results = No results matched. -runs.no_workflows = There are no workflows yet. runs.no_workflows.help_write_access = Don't know how to start with Forgejo Actions? Check out the quick start in the user documentation to write your first workflow, then set up a Forgejo runner to execute your jobs. runs.no_workflows.help_no_write_access = To learn about Forgejo Actions, see the documentation. runs.no_runs = The workflow has no runs yet. @@ -3818,22 +3447,6 @@ workflow.dispatch.warn_input_limit = Only displaying the first %d inputs. need_approval_desc = Need approval to run workflows for fork pull request. -variables = Variables -variables.management = Manage variables -variables.creation = Add variable -variables.none = There are no variables yet. -variables.deletion = Remove variable -variables.deletion.description = Removing a variable is permanent and cannot be undone. Continue? -variables.description = Variables will be passed to certain actions and cannot be read otherwise. -variables.edit = Edit Variable -variables.not_found = Failed to find the variable. -variables.deletion.failed = Failed to remove variable. -variables.deletion.success = The variable has been removed. -variables.creation.failed = Failed to add variable. -variables.creation.success = The variable "%s" has been added. -variables.update.failed = Failed to edit variable. -variables.update.success = The variable has been edited. - [projects] deleted.display_name = Deleted project type-1.display_name = Individual project @@ -3849,9 +3462,6 @@ symbolic_link = Symbolic link submodule = Submodule [markup] -filepreview.line = Line %[1]d in %[2]s -filepreview.lines = Lines %[1]d to %[2]d in %[3]s -filepreview.truncated = Preview has been truncated [translation_meta] test = This is a test string. It is not displayed in Forgejo UI but is used for testing purposes. Feel free to enter "ok" to save time (or a fun fact of your choice) to hit that sweet 100% completion mark :) diff --git a/options/locale/locale_eo.ini b/options/locale/locale_eo.ini index 213377e6bd..be27ad3749 100644 --- a/options/locale/locale_eo.ini +++ b/options/locale/locale_eo.ini @@ -2,8 +2,6 @@ tracked_time_summary = Resumo de trakita tempo bazita sur filtriloj de temolisto language = Lingvo passcode = Paskodo -webauthn_error_timeout = Tempo elĉerpiĝis antaŭ legiĝo de via ŝlosilo. Bonvolu reenlegi la retpaĝon kaj reprovi. -webauthn_sign_in = Premu la butonon sur via sekurŝlosilo. Se mankas butono, elprenu kaj reenmetu vian sekurŝlosilon. captcha = Testo de homeco create_new = Krei… licenses = Permesiloj @@ -11,13 +9,10 @@ sign_in = Saluti user_profile_and_more = Profilo kaj agordoj… explore = Esplori return_to_forgejo = Reiri al Forgejo -webauthn_error_unknown = Eraris pro nekonata kialo. Bonvolu reprovi. twofa = Duobla aŭtentikigo version = Versio help = Helpo -webauthn_error_empty = Vi devas agordi nomon por la ŝlosilo. sign_in_or = aŭ -webauthn_use_twofa = Uzi dumanieran salutkodon de via poŝtelefono page = Paĝo link_account = Ligi konton your_profile = Profilo @@ -26,23 +21,16 @@ settings = Agordoj logo = Emblemo toc = Enhavotabelo admin_panel = Retejadministrado -webauthn_unsupported_browser = Via retfoliumilo ne jam subtenas la salutmanieron WebAuthn. -webauthn_error_insecure = La salutmaniero WebAuthn sole subtenas sekurajn konektiĝojn. Testante HTTP’e, oni uzu la retadreson «localhost» aŭ «127.0.0.1» new_project = Novan projekton notifications = Sciigoj repository = Deponejo -webauthn_error = Ne povis legi vian sekurŝlosilon. active_stopwatch = Aktiva tempotrakilo organization = Organizaĵo sign_in_with_provider = Saluti per %s -webauthn_error_unable_to_process = La servilo ne povis trakti vian peton. register = Registriĝi username = Uzantonomo -webauthn_insert_key = Enmetu vian sekurŝlosilon password = Pasvorto -webauthn_error_duplicated = La sekurŝlosilo ne rajtas fari tiun ĉi peton. Bonvolu certigi ke la ŝlosilo estas ne jam registrita. template = Ŝablono -webauthn_press_button = Bonvolu premi la butonon sur via sekurŝlosilo… signed_in_as = Salutinta kiel sign_up = Registriĝi enable_javascript = Ĉi tiu retejo bezonas JavaSkripton. @@ -498,7 +486,6 @@ issue.action.review_dismissed = @%[1]s maldungis la lastan revizion de %[ repo.transfer.subject_to_you = %s volas reposedigi la deponejon "%s" al vi totp_enrolled.subject = Vi aktivigis TOTP-n kiel 2FA metodo issue_assigned.issue = @%[1]s asignis al vi ĉi tiun eraron %[2]s en la deponejo %[3]s. - repo.transfer.subject_to = %s volas transigi deponejon "%s" al %s [form] @@ -577,9 +564,7 @@ Location = Kieo To = Branĉonomo AccessToken = Atingoĵetono required_prefix = La enigaĵo devas komenciĝi per "%s" - username_error = ` enhavu sole literojn («a–z», «A–Z»), numerojn («0–9«), strekojn («-»), substrekojn («_») kaj punktojn («.»). Gi ne povas komenci kun ne-alfanumeraj signoj, kaj sinsekva ne-alfanumeraj signoj ankaŭ estas malpermesitaj.` - PayloadUrl = Utilaĵ-URL CommitChoice = Enmeto elekton @@ -882,8 +867,6 @@ form.name_pattern_not_allowed = La ŝablono "%s" ne estas permesata en uzantnomo editor.add_file = Aldoni dosieron settings.tags = Etikedoj archive.title_date = Ĉi tiu deponejo arĥiviĝis je %s. Vi povas vidi kaj elŝuti dosierojn, sed ne povas puŝi nek raporti problemojn nek tirpeti. -migrate_items_pullrequests = Tirpetoj -migrate.migrating_pulls = Migrante tirpetojn unwatch = Malspuri watch = Spuri star = Stelumi @@ -922,14 +905,15 @@ settings.default_branch_desc = Elekti implicutan deponejan branĉon por tirpetoj archive.title = Ĉi tiu deponejo estas arĥivigita. Vi povas vidi kaj elŝuti dosierojn, sed ne povas puŝi nek raporti problemojn nek tirpeti. activity.active_prs_count_n = %d aktivaj tirpetoj settings.archive.text = Arĥivigi la deponejon igus ĝin sole legebla. Ĝi kaŝiĝos de la labortablo. Neniu (ne eĉ vi!) povos fari enmetojn, raportojn, kaj tirpetojn. -migrate_items_releases = Eldonoj commits.commits = Enmetoj rss.must_be_on_branch = Vi devas esti en branĉo por havi RSS-fluon. - repo_name = Deponejonomo size_format = %[1]s: %[2]s; %[3]s: %[4]s template = Ŝablono template_select = Elektu ŝablonon +editor.name_your_file = Nomu vian dosieron… +editor.or = aŭ +editor.add_tmpl.filename = dosiernomo [org] code = Fontkodo @@ -937,8 +921,6 @@ settings = Agordoj teams.settings = Agordoj [packages] -npm.details.tag = Etikedo - [search] search = Serĉi… @@ -968,14 +950,8 @@ runner_kind = Serĉi rulantojn… pull_kind = Serĉi tirpetojn… [markup] -filepreview.truncated = La superrigardo estas mallongigita -filepreview.line = Linio %[1]d en %[2]s -filepreview.lines = Linioj %[1]d ĝis %[2]d en %[3]s [actions] -variables.creation.success = La variablo "%s" estas aldonita. -variables.update.failed = Ne eblas redakti la variablon. -variables.update.success = La variablo estas redaktita. [projects] deleted.display_name = Forigita projekto diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index dfb8452f69..b0a9f6ce09 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -36,18 +36,6 @@ twofa=Autenticación de doble factor twofa_scratch=Código de respaldo passcode=Código de acceso -webauthn_insert_key=Introduzca su clave de seguridad -webauthn_sign_in=Presione el botón en su clave de seguridad. Si su clave de seguridad no tiene ningún botón, vuelva a insertarla. -webauthn_press_button=Por favor, presione el botón en su clave de seguridad… -webauthn_use_twofa=Utilice un código de doble factor desde su teléfono móvil -webauthn_error=No se pudo leer su llave de seguridad. -webauthn_unsupported_browser=Actualmente su navegador no soporta WebAuthn. -webauthn_error_unknown=Ha ocurrido un error desconocido. Por favor, inténtelo de nuevo. -webauthn_error_insecure=WebAuthn sólo soporta conexiones seguras. Para probar sobre HTTP, puede utilizar el origen "localhost" o "127.0.0.1" -webauthn_error_unable_to_process=El servidor no pudo procesar su solicitud. -webauthn_error_duplicated=La clave de seguridad no está permitida para esta solicitud. Por favor, asegúrese de que la clave no está ya registrada. -webauthn_error_empty=Debe establecer un nombre para esta clave. -webauthn_error_timeout=Tiempo de espera máximo alcanzado antes de que su clave pudiese ser leída. Por favor, cargue la página y vuelva a intentarlo. repository=Repositorio organization=Organización mirror=Réplica @@ -80,9 +68,9 @@ rerun_all=Volver a ejecutar todos los trabajos save=Guardar add=Añadir add_all=Añadir todos -remove=Eliminar -remove_all=Eliminar todos -remove_label_str=`Eliminar elemento "%s"` +remove=Retirar +remove_all=Retirar todos +remove_label_str=`Retirar elemento "%s"` edit=Editar enabled=Activo @@ -103,7 +91,7 @@ preview=Vista previa loading=Cargando… error=Error -error404=La página a la que está intentando acceder no existe,ha sido eliminada o no está autorizado a verla. +error404=La página a la que está intentando acceder no existe,ha sido retirada o no está autorizado a verla. go_back=Volver never=Nunca @@ -182,7 +170,7 @@ buttons.bold.tooltip=Añadir texto en negrita (Ctrl+B / ⌘B) buttons.italic.tooltip=Añadir texto en cursiva (Ctrl+I / ⌘I) buttons.quote.tooltip=Citar texto buttons.code.tooltip=Añadir código -buttons.link.tooltip=Añadir un enlace +buttons.link.tooltip=Añadir un enlace (Ctrl+K / ⌘K) buttons.list.unordered.tooltip=Añadir una lista buttons.list.ordered.tooltip=Añadir una lista numerada buttons.list.task.tooltip=Añadir una lista de tareas @@ -202,7 +190,7 @@ table_modal.placeholder.content = Contenido link_modal.header = Añadir enlace link_modal.description = Descripción link_modal.paste_reminder = Pista: Con una URL en tu portapapeles, puedes pegar directamente en el editor para crear un enlace. -link_modal.url = URL +link_modal.url = Url [filter] string.asc=A - Z @@ -384,8 +372,8 @@ manual_activation_only=Póngase en contacto con el administrador del sitio para remember_me=Recordar este dispositivo forgot_password_title=Contraseña olvidada forgot_password=¿Has olvidado tu contraseña? -sign_up_successful=La cuenta se ha creado correctamente. ¡Le damos la bienvenida! -confirmation_mail_sent_prompt=Se ha enviado un nuevo correo de confirmación a %s. Para completar el proceso de registro, revisa tu bandeja de entrada y sigue el enlace proporcionado dentro de los próximos %s. Si la dirección no es correcto, puedes iniciar sesión y solicitar otro correo de confirmación para ser enviado a una dirección diferente. +sign_up_successful=La cuenta se ha creado correctamente. ¡Bienvenido! +confirmation_mail_sent_prompt=Se ha enviado un nuevo correo electrónico de confirmación a %s. Para completar el registro, comprueba tu bandeja de entrada y sigue el enlace que aparece en el correo en un plazo de: %s. Si la dirección de correo electrónico no es correcta, puedes iniciar sesión y solicitar que se envíe otro correo electrónico de confirmación a otra dirección. must_change_password=Actualizar su contraseña allow_password_change=Obligar al usuario a cambiar la contraseña (recomendado) reset_password_mail_sent_prompt=Se ha enviado un correo de confirmación a %s. Para completar el proceso de recuperación de la cuenta, consulta tu bandeja de entrada y sigue el enlace proporcionado dentro de los próximos %s. @@ -442,7 +430,7 @@ password_pwned_err=No se pudo completar la solicitud a HaveIBeenPwned change_unconfirmed_email = Si has proporcionado una dirección de correo electrónico errónea durante el registro, la puedes cambiar debajo y se enviará una confirmación a la nueva dirección. change_unconfirmed_email_error = No es posible cambiar la dirección de correo electrónico: %v change_unconfirmed_email_summary = Cambia la dirección de correo electrónico a quien se envía el correo de activación. -last_admin = No puedes eliminar al último admin (administrador). Debe haber, al menos, un admin. +last_admin = No puedes retirar el último admin (administrador). Debe haber, al menos, un admin. sign_up_button = Regístrate ahora. hint_login = ¿Ya tienes cuenta? ¡Ingresa ahora! hint_register = ¿Necesitas una cuenta? Regístrate ahora. @@ -516,7 +504,7 @@ admin.new_user.subject = Se acaba de registrar el nuevo usuario %s admin.new_user.user_info = Información del usuario admin.new_user.text = Por favor, pulsa aquí para gestionar este usuario desde el panel de administración. account_security_caution.text_1 = Si fuiste tú, puedes ignorar este correo. -removed_security_key.subject = Se ha eliminado una clave de seguridad +removed_security_key.subject = Se ha retirado una clave de seguridad removed_security_key.no_2fa = Ya no hay otros métodos 2FA configurados, lo que significa que ya no es necesario iniciar sesión en tu cuenta con 2FA. password_change.subject = Tu contraseña ha sido modificada password_change.text_1 = La contraseña de tu cuenta acaba de ser modificada. @@ -527,7 +515,7 @@ totp_disabled.no_2fa = Ya no hay otros métodos 2FA configurados, lo que signifi account_security_caution.text_2 = Si no fuiste tú, tu cuenta está comprometida. Ponte en contacto con los administradores de este sitio. totp_enrolled.subject = Has activado TOTP como método 2FA totp_enrolled.text_1.no_webauthn = Acabas de activar TOTP para tu cuenta. Esto significa que para todos los futuros inicios de sesión en tu cuenta, debes utilizar TOTP como método 2FA. -removed_security_key.text_1 = La clave de seguridad "%[1]s" acaba de ser eliminada de tu cuenta. +removed_security_key.text_1 = La clave de seguridad "%[1]s" acaba de ser retirada de tu cuenta. primary_mail_change.text_1 = El correo principal de su cuenta acaba de cambiar a %[1]s. Esto significa que esta dirección de correo electrónico ya no recibirá notificaciones por correo electrónico de su cuenta. totp_enrolled.text_1.has_webauthn = Acabas de activar TOTP para tu cuenta. Esto significa que para todos los futuros inicios de sesión en tu cuenta, podrás utilizar TOTP como método 2FA o bien utilizar cualquiera de tus claves de seguridad. @@ -603,7 +591,7 @@ enterred_invalid_owner_name=El nuevo nombre de usuario no es válido. enterred_invalid_password=La contraseña que ha introducido es incorrecta. user_not_exist=Este usuario no existe. team_not_exist=Este equipo no existe. -last_org_owner=No puedes eliminar al último usuario del equipo de "propietarios". Todas las organizaciones deben tener al menos un propietario. +last_org_owner=No puedes retirar el último usuario del equipo de "propietarios". Todas las organizaciones deben tener al menos un propietario. cannot_add_org_to_team=Una organización no puede ser añadida como miembro de un equipo. duplicate_invite_to_team=El usuario ya fue invitado como miembro del equipo. organization_leave_success=Ha abandonado correctamente la organización %s. @@ -622,7 +610,7 @@ org_still_own_repo=Esta organización todavía posee uno o más repositorios, el org_still_own_packages=Esta organización todavía posee uno o más paquetes, elimínalos primero. target_branch_not_exist=La rama de destino no existe. -admin_cannot_delete_self = No puedes eliminarte a ti mismo cuando eres un admin (administrador). Por favor, elimina primero tus privilegios de administrador. +admin_cannot_delete_self = No puedes eliminarte a ti mismo cuando eres un admin (administrador). Por favor, primero retire los privilegios de administrador. username_error_no_dots = ` solo puede contener carácteres alfanuméricos ("0-9","a-z","A-Z"), guiones ("-"), y guiones bajos ("_"). No puede empezar o terminar con carácteres no alfanuméricos y también están prohibidos los carácteres no alfanuméricos consecutivos.` unsupported_login_type = No se admite el tipo de inicio de sesión para eliminar la cuenta. required_prefix = La entrada debe empezar por "%s" @@ -738,7 +726,7 @@ comment_type_group_review_request=Revisión solicitada comment_type_group_pull_request_push=Confirmaciones añadidas comment_type_group_project=Proyecto comment_type_group_issue_ref=Referencia del incidente -saved_successfully=Su configuración se ha guardado correctamente. +saved_successfully=Tu configuración se guardó correctamente. privacy=Privacidad keep_activity_private=Ocultar actividad de la página de perfil lookup_avatar_by_mail=Buscar avatar por dirección de correo electrónico @@ -772,8 +760,8 @@ activate_email=Enviar activación activations_pending=Activaciones pendientes can_not_add_email_activations_pending=Hay una activación pendiente, inténtelo de nuevo en unos minutos si desea agregar un nuevo correo electrónico. delete_email=Eliminar -email_deletion=Eliminar dirección de correo electrónico -email_deletion_desc=Esta dirección de correo electrónico y la información relacionada se eliminará de su cuenta. Las confirmaciones de Git hechas por esta dirección de correo electrónico permanecerán intactas. ¿Desea Continuar? +email_deletion=Retirar dirección de correo-e +email_deletion_desc=Esta dirección de correo electrónico y la información relacionada se retirará de su cuenta. Las confirmaciones de Git hechas por esta dirección de correo electrónico permanecerán intactas. ¿Desea Continuar? email_deletion_success=La dirección de correo electrónico ha sido eliminada. theme_update_success=Su tema fue actualizado. theme_update_error=El tema seleccionado no existe. @@ -840,8 +828,8 @@ add_key_success=La clave SSH "%s" ha sido añadida. add_gpg_key_success=La clave GPG "%s" ha sido añadida. add_principal_success=El certificado SSH principal "%s" ha sido añadido. delete_key=Eliminar -ssh_key_deletion=Eliminar clave SSH -gpg_key_deletion=Eliminar clave GPG +ssh_key_deletion=Retirar clave SSH +gpg_key_deletion=Retirar clave GPG ssh_principal_deletion=Eliminar principal de certificado SSH ssh_key_deletion_desc=Eliminando una clave SSH se revoca su acceso a su cuenta. ¿Continuar? gpg_key_deletion_desc=Eliminando una clave GPG se des-verifican los commits firmados con ella. ¿Continuar? @@ -1022,7 +1010,6 @@ regenerate_token_success = El token se ha regenerado. Las aplicaciones que lo ut quota.applies_to_user = Las siguientes reglas de cuota se aplican a tu cuenta quota.applies_to_org = Las siguientes reglas de cuota se aplican a esta organización quota.rule.exceeded.helper = El tamaño total de los objetos para esta regla ha superado la cuota. - ssh_token_help_ssh_agent = o, si está usando un agente SSH (con la variable de entorno SSH_AUTH_SOCK): [repo] @@ -1154,14 +1141,6 @@ migrate_options_lfs_endpoint.label=Destino LFS migrate_options_lfs_endpoint.description=Migración intentará usar su mando Git para determinar el servidor LFS. También puede especificar un punto final personalizado si los datos LFS del repositorio se almacenan en otro lugar. migrate_options_lfs_endpoint.description.local=También se admite una ruta del servidor local. migrate_options_lfs_endpoint.placeholder=Si se deja en blanco, el punto final se derivará de la URL de clonación -migrate_items=Objetos de migración -migrate_items_wiki=Wiki -migrate_items_milestones=Hitos -migrate_items_labels=Etiquetas -migrate_items_issues=Incidencias -migrate_items_pullrequests=Solicitudes de extracción -migrate_items_merge_requests=Solicitudes de fusión -migrate_items_releases=Lanzamientos migrate_repo=Migrar repositorio migrate.clone_address=Migrar / Clonar desde URL migrate.clone_address_desc=La URL HTTP(S) o de Git "clone" de un repositorio existente @@ -1180,16 +1159,6 @@ migrate.migrating=Migrando desde %s… migrate.migrating_failed=La migración desde %s ha fallado. migrate.migrating_failed.error=Error al migrar: %s migrate.migrating_failed_no_addr=Migración fallida. -migrate.migrating_git=Migrando datos de Git -migrate.migrating_topics=Migrando temas -migrate.migrating_milestones=Migrando hitos -migrate.migrating_labels=Migrando etiquetas -migrate.migrating_releases=Migrar versiones -migrate.migrating_issues=Migrando incidencias -migrate.migrating_pulls=Migrando solicitudes de incorporación de cambios -migrate.cancel_migrating_title=Cancelar la migración -migrate.cancel_migrating_confirm=¿Quiere cancelar esta migración? - mirror_from=réplica de forked_from=bifurcado de generated_from=generado desde @@ -1565,7 +1534,7 @@ issues.ref_reopening_from=`hizo referencia a esta incidencia des issues.ref_from=`de %[1]s` issues.author=Autor issues.role.owner=Propietario -issues.role.owner_helper=Este usuario es el dueño de este repositorio. +issues.role.owner_helper=Este usuario es propietario de este repositorio. issues.role.member=Miembro issues.role.member_helper=Este usuario es miembro de la organización propietaria de este repositorio. issues.role.collaborator=Colaborador @@ -2384,7 +2353,7 @@ settings.matrix.room_id=ID de sala settings.matrix.message_type=Tipo de mensaje settings.archive.button=Archivar repositorio settings.archive.header=Archivar este repositorio -settings.archive.text=Archivar el repositorio lo hará de sólo lectura. Se ocultará del tablero. Nadie (¡ni siquiera tú!) será capaz de hacer nuevas confirmaciones, o abrir nuevas incidencias o solicitudes de incorporación de cambios. +settings.archive.text=Archivar el repositorio lo hará de sólo lectura. Se ocultará del tablero. Nadie (¡ni siquiera tú!) será capaz de hacer nuevas confirmaciones, o abrir nuevas incidencias o solicitudes de incorporación de cambios. Se recomienda documentar el motivo del archivado para orientar a futuros desarrolladores que planeen bifurcar el repositorio. settings.archive.success=El repositorio ha sido archivado exitosamente. settings.archive.error=Ha ocurrido un error al intentar archivar el repositorio. Vea el registro para más detalles. settings.archive.error_ismirror=No puede archivar un repositorio replicado. @@ -2584,24 +2553,16 @@ error.csv.invalid_field_count=No se puede procesar este archivo porque tiene un rss.must_be_on_branch = Debes estar en una rama para tener un feed RSS. desc.sha256 = SHA256 issues.num_participants_one = %d participante -n_commit_few = %s commits -n_branch_few = %s ramas -n_tag_one = %s etiqueta -n_tag_few = %s etiquetas file_follow = Seguir enlace simbólico vendored = Vendored open_with_editor = Abrir con %s -n_branch_one = %s rama issues.archived_label_description = (Archivado) %s -n_commit_one = %s commit generated = Generado pulls.nothing_to_compare_have_tag = Las ramas/etiquetas seleccionadas son iguales. commits.search_branch = Esta rama commits.renamed_from = Renombrado de %s form.string_too_long = El texto introducido tiene más de %d caracteres. object_format = Formato de objetos -n_release_one = %s lanzamiento -n_release_few = %s versiones stars = Estrellas editor.invalid_commit_mail = Correo no válido para crear un commit. project = Proyectos @@ -2741,7 +2702,6 @@ settings.pull_mirror_sync_quota_exceeded = Cuota excedida, no se empujan los cam archive.nocomment = No es posible hacer comentarios porque el repositorio está archivado. sync_fork.branch_behind_one = Esta rama esta %[1]d cambios detrás de %[2]s sync_fork.branch_behind_few = Esta rama está %[1]d confirmaciones detrás de %[2]s - migrate.repo_desc_helper = Deje vacío para importar la descripción existente commits.view_single_diff = Ver los cambios introducidos por la confirmación para este archivo issues.filter_type.all_pull_requests = Todas las solicitudes de incorporación de cambios @@ -2755,11 +2715,9 @@ settings.default_update_style_desc = El modo por defecto utilizado para actualiz settings.wiki_branch_rename_success = La wiki de la rama del repositorio ha sido normalizada correctamente. settings.wiki_branch_rename_failure = Hubo un error al normalizar el nombre de la rama de la wiki del repositorio. release.hide_archive_links_helper = Ocultar el archivo de código generado automáticamente para este lanzamiento. Por ejemplo, si estás subiendo el tuyo propio. - settings.event_action_recover = Recuperar settings.event_action_success = Éxito settings.event_action_success_desc = La ejecución de la acción se ejecutó con éxito. - settings.event_header_action = Eventos de la ejecución de la acción settings.event_action_failure = Fallo settings.event_action_failure_desc = La ejecución de la acción terminó con fallos. @@ -2772,6 +2730,7 @@ settings.enforce_on_admins = Aplicar esta regla para los administradores del rep settings.matrix.access_token_helper = Se recomienda configurar una cuenta de Matrix dedicada para esto. El token de acceso puede ser obtenido desde el cliente web Element (en una pestaña privada/incógnito) > Menú de usuario (arriba izquierda) > Todos los ajustes > Ayuda & Acerca de > Avanzado > Token de acceso (a la derecha, debajo de URL del servidor). Cierra la pestaña privada/incógnito (desconectarse invalidaría el token). settings.archive.mirrors_unavailable = Los espejos no están disponibles en repos archivados. release.summary_card_alt = Tarjeta de resumen de una release titulada "%s" en el repositorio %s +settings.matrix.room_id_helper = El ID de Sala puede ser obtenido desde el cliente web Element > Ajustes de Sala > Avanzado > ID de sala interna. Ejemplo: %s. [graphs] component_loading = Cargando %s… @@ -2819,7 +2778,7 @@ settings.permission=Permisos settings.repoadminchangeteam=El administrador del repositorio puede añadir y eliminar el acceso a equipos settings.visibility=Visibilidad settings.visibility.public=Público -settings.visibility.limited=Limitado (visible solo para usuarios autenticados) +settings.visibility.limited=Limitado (visible solo para usuarios accedidos) settings.visibility.limited_shortname=Limitado settings.visibility.private=Privado (Visible sólo para miembros de la organización) settings.visibility.private_shortname=Privado @@ -2964,41 +2923,13 @@ dashboard.sync_external_users=Sincronizar datos de usuario externo dashboard.cleanup_hook_task_table=Limpiar la tabla hook_task dashboard.cleanup_packages=Limpiar paquetes caducados dashboard.cleanup_actions=Acciones de limpieza de registros expirados y artefactos -dashboard.server_uptime=Tiempo de actividad del servidor -dashboard.current_goroutine=Gorutinas actuales -dashboard.current_memory_usage=Uso actual de memoria -dashboard.total_memory_allocated=Total de Memoria Reservada -dashboard.memory_obtained=Memoria obtenida -dashboard.pointer_lookup_times=Tiempos de búsqueda de punteros -dashboard.memory_allocate_times=Asignaciones de memoria -dashboard.memory_free_times=Liberaciones de memoria -dashboard.current_heap_usage=Uso actual de Heap -dashboard.heap_memory_obtained=Memoria de Heap obtenida -dashboard.heap_memory_idle=Memoria de Heap inactiva -dashboard.heap_memory_in_use=Memoria de Heap en uso -dashboard.heap_memory_released=Memoria de Heap liberada -dashboard.heap_objects=Objetos en el Heap -dashboard.bootstrap_stack_usage=Uso de la pila de Bootstrap -dashboard.stack_memory_obtained=Memoria de pila obtenida -dashboard.mspan_structures_usage=Uso de estructuras MSpan -dashboard.mspan_structures_obtained=Estructuras MSpan obtenidas -dashboard.mcache_structures_usage=Uso de estructuras MCache -dashboard.mcache_structures_obtained=Estructuras MCache obtenidas -dashboard.profiling_bucket_hash_table_obtained=Profiling bucket hash table obtenido -dashboard.gc_metadata_obtained=Metadatos obtenidos del recolector de basura -dashboard.other_system_allocation_obtained=Otros recursos asignados del sistema -dashboard.next_gc_recycle=Siguiente reciclaje del recolector de basura -dashboard.last_gc_time=Tiempo desde la última recolección de basura -dashboard.total_gc_pause=Pausa total por el recolector de basura -dashboard.last_gc_pause=Última pausa por el recolector de basura -dashboard.gc_times=Ejecuciones del recolector de basura dashboard.delete_old_actions=Eliminar todas las actividades antiguas de la base de datos dashboard.delete_old_actions.started=Eliminar todas las actividades antiguas de la base de datos iniciadas. dashboard.update_checker=Buscador de actualizaciones dashboard.delete_old_system_notices=Borrar todos los avisos antiguos del sistema de la base de datos dashboard.gc_lfs=Recoger basura meta-objetos LFS dashboard.stop_zombie_tasks=Detener tareas zombie -dashboard.stop_endless_tasks=Detener tareas interminables +dashboard.stop_endless_tasks=Detiene tareas de acciones interminables dashboard.cancel_abandoned_jobs=Cancelar trabajos abandonados dashboard.start_schedule_tasks=Iniciar tareas programadas dashboard.sync_branch.started=Inició la sincronización de ramas @@ -3048,18 +2979,6 @@ users.purge_help=Borrar forzosamente el usuario y cualquier repositorio, organiz users.still_own_packages=Este usuario todavía posee uno o más paquetes, elimina estos paquetes primero. users.deletion_success=La cuenta de usuario ha sido eliminada. users.reset_2fa=Reiniciar 2FA -users.list_status_filter.menu_text=Filtro -users.list_status_filter.reset=Reiniciar -users.list_status_filter.is_active=Activo -users.list_status_filter.not_active=Inactivo -users.list_status_filter.is_admin=Administrador -users.list_status_filter.not_admin=No admin -users.list_status_filter.is_restricted=Restringido -users.list_status_filter.not_restricted=No restringido -users.list_status_filter.is_prohibit_login=Prohibido el inicio de sesión -users.list_status_filter.not_prohibit_login=Permitir el inicio de sesión -users.list_status_filter.is_2fa_enabled=2FA habilitado -users.list_status_filter.not_2fa_enabled=2FA deshabilitado users.details=Detalles del usuario emails.email_manage_panel=Administrar direcciones de correo electrónico del usuario @@ -3128,16 +3047,16 @@ auths.host=Servidor auths.port=Puerto auths.bind_dn=Bind DN auths.bind_password=Contraseña Bind -auths.user_base=Base de búsqueda de usuarios +auths.user_base=Base de búsqueda del usuario auths.user_dn=DN de Usuario auths.attribute_username=Atributo nombre de usuario auths.attribute_username_placeholder=Dejar vacío para usar el nombre de usuario introducido en Forgejo. auths.attribute_name=Atributo nombre auths.attribute_surname=Atributo apellido -auths.attribute_mail=Atributo correo electrónico -auths.attribute_ssh_public_key=Atributo Clave Pública SSH -auths.attribute_avatar=Atributo del avatar -auths.attributes_in_bind=Obtener atributos en el contexto de Bind DN +auths.attribute_mail=Atributo correo-e +auths.attribute_ssh_public_key=Atributo clave pública SSH +auths.attribute_avatar=Atributo avatar +auths.attributes_in_bind=Obtiene atributos en el contexto de bind DN auths.allow_deactivate_all=Permitir un resultado de búsqueda vacío para desactivar todos los usuarios auths.use_paged_search=Usar búsqueda paginada auths.search_page_size=Tamaño de página @@ -3225,19 +3144,19 @@ config.app_name=Título de la instancia config.app_ver=Versión de Forgejo config.app_url=URL base config.custom_conf=Ruta del fichero de configuración -config.custom_file_root_path=Ruta raíz de los archivos personalizada -config.domain=Dominio del Servidor -config.offline_mode=Modo offline -config.disable_router_log=Deshabilitar Log del Router -config.run_user=Ejecutar como usuario +config.custom_file_root_path=Ruta raíz del archivo personalizado +config.domain=Dominio del servidor +config.offline_mode=Modo local +config.disable_router_log=Inhabilitar bitácora de enrutado +config.run_user=Usuario a ejecutar como config.run_mode=Modo de ejecución config.git_version=Versión de Git -config.app_data_path=Ruta de datos de Forgejo -config.repo_root_path=Ruta del Repositorio +config.app_data_path=Ruta de datos de App +config.repo_root_path=Ruta del repositorio raíz config.lfs_root_path=Ruta raíz de LFS -config.log_file_root_path=Ruta de ficheros de registro -config.script_type=Tipo de Script -config.reverse_auth_user=Autenticación Inversa de Usuario +config.log_file_root_path=Ruta bitácora +config.script_type=Tipo de guion +config.reverse_auth_user=Autenticación inversa de proxy del usuario config.ssh_config=Configuración SSH config.ssh_enabled=Habilitado @@ -3247,16 +3166,16 @@ config.ssh_port=Puerto config.ssh_listen_port=Puerto de escucha config.ssh_root_path=Ruta raíz config.ssh_key_test_path=Ruta de la clave de prueba -config.ssh_keygen_path=Ruta del generador de claves ('ssh-keygen') -config.ssh_minimum_key_size_check=Tamaño mínimo de la clave de verificación +config.ssh_keygen_path=Ruta del generador de claves ("ssh-keygen") +config.ssh_minimum_key_size_check=Comprobante de tamaño de clave mínimo config.ssh_minimum_key_sizes=Tamaños de clave mínimos config.lfs_config=Configuración LFS config.lfs_enabled=Habilitado config.lfs_content_path=Ruta de contenido LFS -config.lfs_http_auth_expiry=Caducidad de la autentificación HTTP LFS +config.lfs_http_auth_expiry=Caducidad de la autenticación HTTP LFS -config.db_config=Configuración de la Base de Datos +config.db_config=Configuración de base de datos config.db_type=Tipo config.db_host=Host config.db_name=Nombre @@ -3266,99 +3185,99 @@ config.db_ssl_mode=SSL config.db_path=Ruta config.service_config=Configuración del servicio -config.register_email_confirm=Requerir confirmación de correo electrónico para registrarse -config.disable_register=Deshabilitar auto-registro -config.allow_only_internal_registration=Permitir el registro solo desde Forgejo -config.allow_only_external_registration=Permitir el registro únicamente a través de servicios externos -config.enable_openid_signup=Habilitar el auto-registro con OpenID -config.enable_openid_signin=Habilitar el inicio de sesión con OpenID -config.show_registration_button=Mostrar Botón de Registro -config.require_sign_in_view=Requerir inicio de sesión obligatorio para ver páginas -config.mail_notify=Habilitar las notificaciones por correo electrónico +config.register_email_confirm=Requerir confirmación de correo-e para registrarse +config.disable_register=Inhabilitar auto-registro +config.allow_only_internal_registration=Concede registro solo por medio de Forgejo +config.allow_only_external_registration=Concede registro solo a través de servicios externos +config.enable_openid_signup=Habilitar auto-registro con OpenID +config.enable_openid_signin=Habilitar inicio de sesión con OpenID +config.show_registration_button=Mostrar botón de registro +config.require_sign_in_view=Requiere inicio de sesión para ver contenido +config.mail_notify=Habilitar notificaciones por correo-e config.enable_captcha=Activar CAPTCHA -config.active_code_lives=Habilitar Vida del Código -config.reset_password_code_lives=Caducidad del código de recuperación de cuenta -config.default_keep_email_private=Ocultar direcciones de correo electrónico por defecto -config.default_allow_create_organization=Permitir la creación de organizaciones por defecto +config.active_code_lives=Código de activación del tiempo de vida +config.reset_password_code_lives=Código de recuperación del tiempo de vida +config.default_keep_email_private=Ocultar direcciones de correo-e por defecto +config.default_allow_create_organization=Concede la creación de organizaciones por defecto config.enable_timetracking=Habilitar seguimiento de tiempo config.default_enable_timetracking=Habilitar seguimiento de tiempo por defecto config.allow_dots_in_usernames = Permite utilizar puntos en los nombres de usuario. No tiene efecto sobre cuentas existentes. config.default_allow_only_contributors_to_track_time=Deje que solo los colaboradores hagan un seguimiento del tiempo -config.no_reply_address=Dominio de correos electrónicos ocultos -config.default_visibility_organization=Visibilidad por defecto para nuevas organizaciones +config.no_reply_address=Dominio de correo-e ocultos +config.default_visibility_organization=Visibilidad por defecto para organizaciones nuevas config.default_enable_dependencies=Habilitar dependencias de incidencias por defecto config.webhook_config=Configuración de Webhooks -config.queue_length=Tamaño de Cola de Envío -config.deliver_timeout=Timeout de Entrega -config.skip_tls_verify=Saltar verificación TLS +config.queue_length=Longitud de cola +config.deliver_timeout=Vencimiento de entrega +config.skip_tls_verify=Omitir verificación TLS -config.mailer_config=Configuración del servidor de correo +config.mailer_config=Configuración del cartero config.mailer_enabled=Activado config.mailer_enable_helo=Habilitar HELO config.mailer_name=Nombre config.mailer_protocol=Protocolo -config.mailer_smtp_addr=Dirección SMTP +config.mailer_smtp_addr=Hospedaje SMTP config.mailer_smtp_port=Puerto SMTP config.mailer_user=Usuario config.mailer_use_sendmail=Usar Sendmail config.mailer_sendmail_path=Ruta de Sendmail config.mailer_sendmail_args=Argumentos adicionales por Sendmail -config.mailer_sendmail_timeout=Tiempo de espera de Sendmail +config.mailer_sendmail_timeout=Vencimiento de Sendmail config.mailer_use_dummy=Dummy config.test_email_placeholder=Correo electrónico (ej. test@ejemplo.com) -config.send_test_mail=Enviar prueba de correo +config.send_test_mail=Enviar prueba de correo-e config.send_test_mail_submit=Enviar -config.test_mail_failed=Fallo al enviar un correo electrónico de prueba a "%s": %v -config.test_mail_sent=Se ha enviado un correo electrónico de prueba a "%s". +config.test_mail_failed=Fallo al enviar una prueba de correo-e a «%s»: %v +config.test_mail_sent=Se ha enviado un correo-e de prueba a «%s». config.oauth_config=Configuración OAuth config.oauth_enabled=Activado -config.cache_config=Configuración de la Caché -config.cache_adapter=Adaptador de la Caché -config.cache_interval=Intervalo de la Caché -config.cache_conn=Conexión de la Caché -config.cache_item_ttl=Período de vida para elementos de caché +config.cache_config=Configuración de caché +config.cache_adapter=Adaptador de caché +config.cache_interval=Intervalo de caché +config.cache_conn=Conexión de caché +config.cache_item_ttl=TTL de elemento caché -config.session_config=Configuración de la Sesión -config.session_provider=Proveedor de la Sesión -config.provider_config=Configuración del Proveedor -config.cookie_name=Nombre de la Cookie -config.gc_interval_time=Intervalo de tiempo del GC -config.session_life_time=Tiempo de Vida de la Sesión -config.https_only=Sólo HTTPS +config.session_config=Configuración de sesión +config.session_provider=Proveedor de sesión +config.provider_config=Configuración de proveedor +config.cookie_name=Nombre de cookie +config.gc_interval_time=Tiempo de intervalo GC +config.session_life_time=Tiempo de vida de sesión +config.https_only=Solo HTTPS config.cookie_life_time=Tiempo de Vida de la Cookie config.picture_config=Configuración de imagen y avatar config.picture_service=Servicio de Imágen config.disable_gravatar=Desactivar Gravatar -config.enable_federated_avatar=Habilitar Avatares Federados +config.enable_federated_avatar=Habilitar avatares federados config.git_config=Configuración de Git -config.git_disable_diff_highlight=Desactivar resaltado de sintaxis del Diff -config.git_max_diff_lines=Líneas de Diff máximas (por un solo archivo) -config.git_max_diff_line_characters=Carácteres de Diff máximos (para una sola línea) -config.git_max_diff_files=Máximo de archivos de Diff (que se mostrarán) +config.git_disable_diff_highlight=Inhabilitar resaltado de diff de sintaxis +config.git_max_diff_lines=Líneas de diff máximas por archivo +config.git_max_diff_line_characters=Caracteres de diff máx por línea +config.git_max_diff_files=Diff de archivos máxima mostrada config.git_gc_args=Argumentos de GC -config.git_migrate_timeout=Tiempo de espera de migración -config.git_mirror_timeout=Tiempo de espera de actualización de réplicas -config.git_clone_timeout=Tiempo de espera de operación de clones -config.git_pull_timeout=Tiempo de espera de operación de pull -config.git_gc_timeout=Tiempo de espera de operación de GC +config.git_migrate_timeout=Vencimiento de migración +config.git_mirror_timeout=Vencimiento de actualización de réplica +config.git_clone_timeout=Vencimiento de operación de clonado +config.git_pull_timeout=Vencimiento de operación de pull +config.git_gc_timeout=Vencimiento de operación de GC -config.log_config=Configuración del Log +config.log_config=Configuración de bitácora config.logger_name_fmt=Registro: %s config.disabled_logger=Desactivado config.access_log_mode=Modo de registro del Acceso -config.access_log_template=Plantilla de registro de acceso +config.access_log_template=Plantilla de bitácora de acceso config.xorm_log_sql=Registrar SQL config.set_setting_failed=Error al configurar %s monitor.stats=Estadísticas -monitor.cron=Tareas de Cron +monitor.cron=Tareas de cron monitor.name=Nombre monitor.schedule=Agenda monitor.next=Siguiente @@ -3377,30 +3296,10 @@ monitor.process.cancel_desc=Cancelar un proceso puede ocasionar una pérdida de monitor.process.cancel_notices=Cancelar: %s? monitor.process.children=Hijos -monitor.queues=Colas -monitor.queue=Cola: %s -monitor.queue.name=Nombre -monitor.queue.type=Tipo -monitor.queue.exemplar=Ejemplo -monitor.queue.numberworkers=Número de trabajadores -monitor.queue.activeworkers=Trabajadores activos -monitor.queue.maxnumberworkers=Número máximo de trabajadores -monitor.queue.numberinqueue=Número en cola -monitor.queue.review_add=Revisar / Añadir Trabajadores -monitor.queue.settings.title=Ajustes del grupo -monitor.queue.settings.desc=Los grupos de trabajadores crecen dinámicamente en respuesta al bloqueo de cola de sus trabajadores. -monitor.queue.settings.maxnumberworkers=Número máximo de trabajadores -monitor.queue.settings.maxnumberworkers.placeholder=Actualmente %[1]d -monitor.queue.settings.maxnumberworkers.error=El número máximo de trabajadores debe ser un número -monitor.queue.settings.submit=Actualizar ajustes -monitor.queue.settings.changed=Ajustes actualizados -monitor.queue.settings.remove_all_items=Eliminar todo -monitor.queue.settings.remove_all_items_done=Todos los elementos en la cola han sido eliminados. - -notices.system_notice_list=Notificaciones del Sistema -notices.view_detail_header=Ver detalles de notificación +notices.system_notice_list=Notificaciones del sistema +notices.view_detail_header=Detalles de notificación notices.operations=Operaciones -notices.select_all=Sleccionar todo +notices.select_all=Seleccionar todo notices.deselect_all=Deseleccionar todo notices.inverse_selection=Selección inversa notices.delete_selected=Eliminar seleccionado @@ -3428,8 +3327,6 @@ monitor.duration = Duración (es) self_check = Autocomprobación config.app_slogan = Eslogan de la instancia dashboard.sync_tag.started = Sincronización de etiquetas iniciada - - dashboard.sync_repo_tags = Sincronizar etiquetas de Git con la base de datos users.block.description = Bloquear a este usuario para que no pueda interactuar a través de su cuenta e impedirle iniciar sesión. users.admin.description = Otorgar a este usuario acceso completo a todas las funciones administrativas disponibles a través de la interfaz de usuario web y en la API. @@ -3444,6 +3341,8 @@ self_check.database_collation_mismatch = Se espera que la base de datos use inte self_check.database_collation_case_insensitive = La base de datos está usando intercalación %s, que es una intercalación insensible. Aunque Forgejo podría funcionar, puede darse algún caso extraño en el que no funcione como se espera. self_check.database_inconsistent_collation_columns = La base de datos está usando intercalación %s, pero estas columnas están usando intercalaciones desparejadas. Esto puede causar algunos problemas inesperados. self_check.database_fix_mysql = Para usuarios de MySQL/MariaDB, podrían usar el comando "forgejo doctor convert" para arreglar los problemas de intercalación, o también podrían arreglarlos utilizando consultas SQL manuales, como "ALTER ... COLLATE ...". +config.cache_test_failed = Incorrecto al probar la caché: %v. + [action] create_repo=creó el repositorio %s @@ -3499,35 +3398,10 @@ raw_seconds=segundos raw_minutes=minutos [dropzone] -default_message=Suelte archivos o haga clic aquí para subir. -invalid_input_type=No puede subir archivos de este tipo. -file_too_big=El tamaño del archivo ({{filesize}} MB) excede el tamaño máximo de ({{maxFilesize}} MB). -remove_file=Eliminar archivo [notification] -notifications=Notificaciones -unread=Sin leer -read=Leídas -no_unread=No tiene notificaciones sin leer. -no_read=No hay notificaciones. -pin=Fijar notificación -mark_as_read=Marcar como leído -mark_as_unread=Marcar como no leído -mark_all_as_read=Marcar todo como leído -subscriptions=Suscripciones -watching=Siguiendo -no_subscriptions=Sin suscripciones [gpg] -default_key=Firmado con clave predeterminada -error.extract_sign=Error al extraer la firma -error.generate_hash=Error al generar hash of commit -error.no_committer_account=Ninguna cuenta vinculada a la dirección de correo electrónico del committer -error.no_gpg_keys_found=No se encontró ninguna clave conocida en la base de datos para esta firma -error.not_signed_commit=No es un commit firmado -error.failed_retrieval_gpg_keys=No se pudo recuperar cualquier clave adjunta a la cuenta del committer -error.probable_bad_signature=¡ADVERTENCIA! ¡Hay una clave con este ID en la base de datos, pero esa clave no verifica este commit! Este commit es SOSPECHOSO. -error.probable_bad_default_signature=¡ADVERTENCIA! ¡La clave por defecto tiene este ID pero esa clave no verifica este commit! Este commit es SOSPECHOSO. [units] unit=Unidad @@ -3535,181 +3409,11 @@ error.no_unit_allowed_repo=No tiene permisos para acceder a ninguna sección de error.unit_not_allowed=No tiene permisos para acceder a esta sección del repositorio. [packages] -title=Paquetes desc=Administrar paquetes del repositorio. -empty=Todavía no hay paquetes. -empty.documentation=Para más información sobre el registro de paquetes, consulte la documentación. -empty.repo=¿Has subido un paquete, pero no se muestra aquí? Ve a la configuración del paquete y añade el link a este repositorio. -registry.documentation=Para obtener más información sobre el registro %s, consulte la documentación. -filter.type=Tipo -filter.type.all=Todo -filter.no_result=El filtro no produjo ningún resultado. -filter.container.tagged=Etiquetado -filter.container.untagged=Etiqueta eliminada -published_by=Publicado %[1]s por %[3]s -published_by_in=Publicado %[1]s por %[3]s en %[5]s -installation=Instalación -about=Acerca de este paquete -requirements=Requisitos -dependencies=Dependencias -keywords=Palabras clave -details=Detalles -details.author=Autor -details.project_site=Sitio del proyecto -details.repository_site=Sitio del repositorio -details.documentation_site=Sitio de documentación -details.license=Licencia -assets=Activos -versions=Versiones -versions.view_all=Ver todo -dependency.id=Id. -dependency.version=Versión -alpine.registry=Configura este registro agregando la url en tu archivo ./apk/repositories: -alpine.registry.key=Descargue la clave RSA pública del registro en la carpeta ./apk/keys/ para verificar la firma del índice: -alpine.registry.info=Seleccione $branch y $repository de la siguiente lista. -alpine.install=Para instalar el paquete, ejecute el siguiente comando: -alpine.repository=Información del repositorio -alpine.repository.branches=Ramas -alpine.repository.repositories=Repositorios -alpine.repository.architectures=Arquitecturas -cargo.registry=Configurar este registro en el archivo de configuración de Cargo (por ejemplo ~/.cargo/config.toml): -cargo.install=Para instalar el paquete usando Cargo, ejecute el siguiente comando: -chef.registry=Configura este registro en tu archivo ~/.chef/config.rb: -chef.install=Para instalar el paquete, ejecute el siguiente comando: -composer.registry=Configura este registro en el archivo ~/.composer/config.json: -composer.install=Para instalar el paquete usando Composer, ejecute el siguiente comando: -composer.dependencies=Dependencias -composer.dependencies.development=Dependencias de desarrollo conan.details.repository=Repositorio -conan.registry=Configurar este registro desde la línea de comandos: -conan.install=Para instalar el paquete usando Conan, ejecuta el siguiente comando: -conda.registry=Configura este registro como un repositorio Conda en tu archivo .condarc: -conda.install=Para instalar el paquete usando Conda, ejecute el siguiente comando: -container.details.type=Tipo de imagen -container.details.platform=Plataforma -container.pull=Arrastra la imagen desde la línea de comandos: -container.digest=Resumen -container.multi_arch=SO / Arquitectura -container.layers=Capas de imagen -container.labels=Etiquetas -container.labels.key=Clave -container.labels.value=Valor -cran.registry=Configurar este registro en su archivo Rprofile.site: -cran.install=Para instalar el paquete, ejecute el siguiente comando: -debian.registry=Configurar este registro desde la línea de comandos: -debian.registry.info=Seleccione $distribution y $component de la siguiente lista. -debian.install=Para instalar el paquete, ejecute el siguiente comando: -debian.repository=Información del repositorio -debian.repository.distributions=Distribuciones -debian.repository.components=Componentes -debian.repository.architectures=Arquitecturas -generic.download=Descargar paquete desde la línea de comandos: -go.install=Instalar el paquete desde la línea de comandos: -helm.registry=Configurar este registro desde la línea de comandos: -helm.install=Para instalar el paquete, ejecute el siguiente comando: -maven.registry=Configure este registro en su proyecto pom.xml archivo: -maven.install=Para usar el paquete incluya lo siguiente en el bloque dependencias en el archivo pom.xml: -maven.install2=Ejecutar vía línea de comandos: -maven.download=Para descargar la dependencia, ejecute vía línea de comandos: -nuget.registry=Configurar este registro desde la línea de comandos: -nuget.install=Para instalar el paquete usando NuGet, ejecute el siguiente comando: -nuget.dependency.framework=Marco de destino -npm.registry=Configura este registro en tu proyecto .npmrc archivo: -npm.install=Para instalar el paquete usando npm, ejecute el siguiente comando: -npm.install2=o añádelo al archivo package.json: -npm.dependencies=Dependencias -npm.dependencies.development=Dependencias de desarrollo -npm.dependencies.peer=Dependencias de pares -npm.dependencies.optional=Dependencias opcionales -npm.details.tag=Etiqueta -pub.install=Para instalar el paquete usando Dart, ejecute el siguiente comando: -pypi.requires=Requiere Python -pypi.install=Para instalar el paquete usando pip, ejecute el siguiente comando: -rpm.registry=Configurar este registro desde la línea de comandos: -rpm.distros.redhat=en distribuciones basadas en RedHat -rpm.distros.suse=en distribuciones basadas en SUSE -rpm.install=Para instalar el paquete, ejecute el siguiente comando: -rpm.repository=Información del repositorio -rpm.repository.architectures=Arquitecturas -rubygems.install=Para instalar el paquete usando gem, ejecute el siguiente comando: -rubygems.install2=o añádelo al archivo Gemfile: -rubygems.dependencies.runtime=Dependencias en tiempo de ejecución -rubygems.dependencies.development=Dependencias de desarrollo -rubygems.required.ruby=Requiere versión Ruby -rubygems.required.rubygems=Requiere la versión de RubyGem -swift.registry=Configurar este registro desde la línea de comandos: -swift.install=Añade el paquete en tu archivo Package.swift: -swift.install2=y ejecuta el siguiente comando: -vagrant.install=Para añadir un paquete Vagrant, ejecuta el siguiente comando: -settings.link=Vincular este paquete a un repositorio -settings.link.description=Si enlaza un paquete con un repositorio, el paquete se enumera en la lista de paquetes del repositorio. -settings.link.select=Seleccionar repositorio -settings.link.button=Actualizar enlace de repositorio -settings.link.success=El enlace del repositorio se ha actualizado correctamente. -settings.link.error=Error al actualizar el enlace del repositorio. -settings.delete=Eliminar paquete -settings.delete.description=La eliminación de un paquete es permanente y no se puede deshacer. -settings.delete.notice=Está a punto de eliminar %s (%s). Esta operación es irreversible, ¿está seguro? -settings.delete.success=Se ha eliminado el paquete. -settings.delete.error=No se pudo eliminar el paquete. -owner.settings.cargo.title=Índice del registro Cargo -owner.settings.cargo.initialize=Inicializar índice -owner.settings.cargo.initialize.description=Se necesita un repositorio Git de índice especial para usar el registro de Cargo. Usar esta opción (re)creará el repositorio y lo configurará automáticamente. -owner.settings.cargo.initialize.error=Fallo al inicializar el índice de Cargo: %v -owner.settings.cargo.initialize.success=El índice de Cargo se ha creado correctamente. -owner.settings.cargo.rebuild=Reconstruir índice -owner.settings.cargo.rebuild.description=Reconstruir puede ser útil si el índice no se sincroniza con los paquetes de Cargo almacenados. -owner.settings.cargo.rebuild.error=Fallo al reconstruir el índice de Cargo: %v -owner.settings.cargo.rebuild.success=El índice de Cargo se ha reconstruido correctamente. -owner.settings.cleanuprules.title=Administrar reglas de limpieza -owner.settings.cleanuprules.add=Añadir regla de limpieza -owner.settings.cleanuprules.edit=Editar regla de limpieza -owner.settings.cleanuprules.none=No hay reglas de limpieza disponibles. Por favor, consulte la documentación. -owner.settings.cleanuprules.preview=Vista previa de regla de limpieza -owner.settings.cleanuprules.preview.overview=%d paquetes están programados para ser eliminados. -owner.settings.cleanuprules.preview.none=Regla de limpieza no coincide con ningún paquete. owner.settings.cleanuprules.enabled=Activo -owner.settings.cleanuprules.pattern_full_match=Aplicar patrón al nombre completo del paquete -owner.settings.cleanuprules.keep.title=Las versiones que coinciden con estas reglas son reales, incluso si coinciden con una regla de eliminación a continuación. -owner.settings.cleanuprules.keep.count=Mantener el más reciente owner.settings.cleanuprules.keep.count.1=1 versión por paquete owner.settings.cleanuprules.keep.count.n=%d versiones por paquete -owner.settings.cleanuprules.keep.pattern=Mantener las versiones coincidentes -owner.settings.cleanuprules.keep.pattern.container=La última versión siempre es guardada en los paquetes de contenedor. -owner.settings.cleanuprules.remove.title=Se eliminan las versiones que coinciden con estas reglas, a menos que una regla anterior indique mantenerlas. -owner.settings.cleanuprules.remove.days=Eliminar versiones anteriores a -owner.settings.cleanuprules.remove.pattern=Eliminar versiones coincidentes -owner.settings.cleanuprules.success.update=Regla de limpieza ha sido actualizada. -owner.settings.cleanuprules.success.delete=Regla de limpieza ha sido eliminada. -owner.settings.chef.title=Registro de Chef -owner.settings.chef.keypair=Generar par de claves -owner.settings.chef.keypair.description=Un par de claves es necesario para autenticarse en el registro del Chef. Si ha generado un par de claves antes, generar un nuevo par de claves descartará el par de claves antiguo. -search_in_external_registry = Buscar en %s -arch.pacman.sync = Sincronizar el paquete con pacman: -arch.version.properties = Propiedades de la versión -arch.pacman.repo.multi = %s tiene la misma versión en diferentes distribuciones. -arch.pacman.repo.multi.item = Configuración para %s -arch.pacman.conf = Añadir servidor con distribución y arquitectura relacionadas a /etc/pacman.conf : -arch.version.backup = Copia de seguridad -arch.version.depends = Depende -arch.version.groups = Grupo -rpm.repository.multiple_groups = Este paquete está disponible en múltiples grupos. -arch.version.conflicts = Conflictos -arch.version.replaces = Reemplazos -container.images.title = Imágenes -alt.registry = Configurar este registro desde la línea de comandos: -alt.registry.install = Para instalar el paquete, ejecute el siguiente comando: -alt.install = Instalar paquete -alt.repository = Información del repositorio -alt.repository.architectures = Arquitecturas -alt.repository.multiple_groups = Este paquete está disponible en múltiples grupos. -arch.version.description = Descripción -arch.version.provides = Proveedores -npm.dependencies.bundle = Empaquetar dependencias -arch.version.checkdepends = Comprobar dependencias -arch.version.optdepends = Dependencias opcionales -arch.version.makedepends = Construir dependencias -arch.pacman.helper.gpg = Añade el certificado de confianza para pacman: [secrets] secrets=Secretos @@ -3727,104 +3431,28 @@ deletion.failed=Error al eliminar secreto. management=Gestión de secretos [actions] -actions=Acciones - unit.desc=Gestione procesos CI/CD integrados con Forgejo Actions. -status.unknown=Desconocido -status.waiting=Esperando -status.running=Corriendo -status.success=Éxito -status.failure=Fallo -status.cancelled=Cancelado -status.skipped=Saltado -status.blocked=Bloqueado - -runners=Nodos -runners.runner_manage_panel=Gestión de nodos -runners.new=Crear nuevo nodo -runners.new_notice=Cómo iniciar un nodo -runners.status=Estado -runners.id=Id. -runners.name=Nombre -runners.owner_type=Tipo -runners.description=Descripción -runners.labels=Etiquetas -runners.last_online=Última hora en línea -runners.runner_title=Nodo -runners.task_list=Tareas recientes en este nodo -runners.task_list.no_tasks=Todavía no hay tarea. -runners.task_list.run=Ejecutar -runners.task_list.status=Estado -runners.task_list.repository=Repositorio -runners.task_list.commit=Confirmación -runners.task_list.done_at=Hecho en -runners.edit_runner=Editar nodo -runners.update_runner=Actualizar cambios -runners.update_runner_success=Nodo actualizado correctamente -runners.update_runner_failed=Fallo al actualizar el nodo -runners.delete_runner=Eliminar este nodo -runners.delete_runner_success=Nodo eliminado con éxito -runners.delete_runner_failed=Fallo al eliminar el nodo -runners.delete_runner_header=Confirma para eliminar el nodo -runners.delete_runner_notice=Si una tarea se está ejecutando en este nodo, se terminará y marcará como fallida. Puede romper el flujo de trabajo en curso. -runners.none=No hay nodos disponibles -runners.status.unspecified=Desconocido -runners.status.idle=Inactivo -runners.status.active=Activo -runners.status.offline=Desconectado -runners.version=Versión -runners.reset_registration_token=Restablecer token de registro -runners.reset_registration_token_success=Se ha restablecido correctamente el token de registro del nodo - -runs.all_workflows=Todos los flujos de trabajo -runs.commit=Commit -runs.scheduled=Programado -runs.pushed_by=push enviado por -runs.invalid_workflow_helper=El archivo de configuración del trabajo no es válido. Revisa tu archivo de configuración: %s -runs.actor=Actor -runs.status=Estado -runs.actors_no_select=Todos los actores -runs.status_no_select=Todo el estado -runs.no_results=No hay resultados coincidentes. runs.no_runs=El flujo de trabajo no tiene ejecuciones todavía. workflow.disable=Desactivar flujo de trabajo workflow.disable_success=Flujo de trabajo "%s" desactivado exitosamente. -workflow.enable=Activar flujo de trabajo -workflow.enable_success=Flujo de trabajo '%s' habilitado con éxito. +workflow.enable=Habilitar flujo +workflow.enable_success=Flujo «%s» habilitado correctamente. workflow.disabled=El flujo de trabajo está deshabilitado. need_approval_desc=Necesita aprobación para ejecutar flujos de trabajo para el pull request del fork. -variables=Variables -variables.management=Gestión de variables -variables.creation=Añadir variable -variables.none=Aún no hay variables. -variables.deletion=Eliminar variable -variables.deletion.description=Eliminar una variable es permanente y no se puede deshacer. ¿Continuar? -variables.description=Las variables se pasarán a ciertas acciones y no se podrán leer de otro modo. -variables.edit=Editar variable -variables.deletion.failed=No se pudo eliminar la variable. -variables.deletion.success=La variable ha sido eliminada. -variables.creation.failed=No se pudo agregar la variable. -variables.creation.success=La variable "%s" ha sido añadida. -variables.update.failed=Error al editar la variable. -variables.update.success=La variable ha sido editada. variables.id_not_exist = Variable con id %d no existe. runs.empty_commit_message = (mensaje de commit vacío) runs.expire_log_message = Los registros han sido eliminados porque eran demasiado antiguos. -runs.workflow = Flujo de trabajo workflow.dispatch.run = Correr flujo de trabajo workflow.dispatch.use_from = Usar el flujo de trabajo de workflow.dispatch.invalid_input_type = Tipo de entrada inválida "%s". -runs.no_workflows = Aún no hay flujos de trabajo. workflow.dispatch.success = La ejecución del flujo de trabajo se ha solicitado correctamente. -variables.not_found = No se ha encontrado la variable. workflow.dispatch.input_required = Se requiere valor para la entrada "%s". workflow.dispatch.trigger_found = Este flujo de trabajo tiene un disparador de eventos workflow_dispatch. workflow.dispatch.warn_input_limit = Sólo se muestran las primeras %d entradas. - runs.no_workflows.help_write_access = ¿No sabes cómo empezar con Forgejo Actions? Comprueba la guía de inicio rápido en la documentación de usuario para escribir tu primer workflow, luego configura un runner de Forgejo para ejecutar tus trabajos. runs.no_workflows.help_no_write_access = Para aprender sobre Forgejo Actions, véase la documentación. @@ -3832,7 +3460,7 @@ runs.no_workflows.help_no_write_access = Para aprender sobre Forgejo Actions, v type-1.display_name=Proyecto individual type-2.display_name=Proyecto de repositorio type-3.display_name=Proyecto de organización -deleted.display_name = Proyecto borrado +deleted.display_name = Proyecto eliminado [git.filemode] changed_filemode=%[1]s → %[2]s @@ -3872,9 +3500,6 @@ regexp_tooltip = Interpretar los términos de búsqueda como una expresión regu regexp = Expresión Regular [markup] -filepreview.lines = Líneas %[1]d a %[2]d en %[3]s -filepreview.line = Línea %[1]d en %[2]s -filepreview.truncated = La vista previa se ha truncado [repo.permissions] pulls.read = Lectura: Leer y crear pull requests. @@ -3893,19 +3518,11 @@ ext_wiki = Acceder al enlace de la wiki externa. Los permisos se gestionan de fo code.write = Escritura: Push al repositorio, crear ramas y tags. issues.write = Escritura: Cerrar incidencias y gestion de metadatos como etiquetas, hitos, asignaciones, fechas de vencimiento y dependencias. packages.write = Escritura: Publicar y eliminar paquetes asignados al repositorio. - - actions.read = Leer: visualizar ejecuciones del flujo de trabajo y sus logs. actions.write = Escribir: Ejecutar, reiniciar y cancelar flujos de trabajo. Gestionar la delegación de confianza a los publicadores de pull requests. + [munits.data] -eib = EiB -gib = GiB -pib = PiB -kib = KiB -tib = TiB -mib = MiB -b = B [translation_meta] test = Did you know that the Garoé, the sacred tree of El Hierro in the Canary Islands, was essentially a natural water fountain? :) \ No newline at end of file diff --git a/options/locale/locale_et.ini b/options/locale/locale_et.ini index eebdb3ab6f..6b3374ce4c 100644 --- a/options/locale/locale_et.ini +++ b/options/locale/locale_et.ini @@ -26,8 +26,6 @@ enable_javascript = See veebileht eeldab JavaScripti kasutamise lubamist. toc = Sisukord licenses = Litsentsid username = Kasutajanimi -webauthn_error_unable_to_process = Server ei saanud sinu päringut töödelda. -webauthn_error_duplicated = Turvavõti ei ole selle päringu puhul lubatud. Palun veendu, et võti ei ole juba registreeritud. return_to_forgejo = Tagasi Forgejo'sse toggle_menu = Lülita menüü sisse/välja more_items = Rohkem objekte @@ -38,16 +36,6 @@ re_type = Kinnita salasõna twofa = Kahefaktoriline autentimine twofa_scratch = Kahefaktoriline kriipsukood passcode = Salakood -webauthn_insert_key = Sisesta oma turvavõti -webauthn_sign_in = Vajuta turvavõtme nuppu. Kui sinu turvavõtmel ei ole nuppu, sisesta see uuesti. -webauthn_press_button = Palun vajuta turvavõtme nuppu… -webauthn_use_twofa = Sisesta oma telefonist kahefaktorilise autentimise kood -webauthn_error = Sinu turvavõtit ei saanud lugeda. -webauthn_unsupported_browser = Sinu veebibrauser ei toeta praegu WebAuthn-liidestust. -webauthn_error_unknown = Tekkis tundmatu viga. Palun proovi uuesti. -webauthn_error_insecure = WebAuthn toetab ainult turvalisi ühendusi. HTTP kaudu testimiseks võid kasutada lähteaadressina „localhost“ või „127.0.0.1“ -webauthn_error_empty = Palun lisa sellele võtmele täisnimi. -webauthn_error_timeout = Päring aegus enne võtme lugemist. Palun laadi see lehekülg uuesti ja proovi uuesti. repository = Hoidla organization = Organisatsioon new_fork = Uus lähekoodihoidla haru @@ -141,6 +129,7 @@ copy_path = Kopeeri asukoht captcha = Robotilõks unpin = Lõpeta esiletõstmine powered_by = Siin on kasutusel %s +collaborative = Koostöö [search] search = Otsi… @@ -189,7 +178,7 @@ buttons.heading.tooltip = Lisa pealkiri buttons.italic.tooltip = Lisa kaldkirjas tekst (Ctrl+I / ⌘I) buttons.quote.tooltip = Tsiteeri teksti buttons.code.tooltip = Lisa kood -buttons.link.tooltip = Lisa link +buttons.link.tooltip = Lisa link (Ctrl+K / ⌘K) buttons.list.ordered.tooltip = Lisa nummerdatud nimekiri buttons.list.unordered.tooltip = Lisa nimekiri buttons.list.task.tooltip = Lisa ülesannete nimekiri @@ -333,7 +322,6 @@ sqlite3_not_available = See Forgejo versioon ei toeta SQLite3 andmebaasi. Palun offline_mode.description = Lülitage kolmandate osapoolte sisuedastusvõrgud välja ja jaga kõiki ressursse kohalikust serverist. password_algorithm = Salasõna räsialgoritm invalid_password_algorithm = Vigane salasõna räsialgoritm - invalid_repo_path = Tarkvarahoidla juurkaust on vigane: %v invalid_app_data_path = Rakenduse andmete asukoht on vigane: %v @@ -342,7 +330,6 @@ allow_password_change = Eelda, et kasutajad muudavad oma salasõna (soovitatav) forgot_password_title = Ununenud salasõna must_change_password = Muuda oma salasõna forgot_password = Kas salasõna ununes? - verify = Verifitseeri openid_connect_submit = Ühenda @@ -350,7 +337,6 @@ openid_connect_submit = Ühenda password_change.subject = Sinu salasõna on muutunud password_change.text_1 = Sinu kasutajakonto salasõna on just muutunud. totp_disabled.text_1 = Lisaautentimine ehk ajapõhise salasõna (TOTP) kasutamine on sinu kasutajakontol just välja lülitatud. - release.note = Märkus: release.downloads = Allalaadimised: repo.transfer.to_you = sina @@ -363,7 +349,6 @@ username_password_incorrect = Kassutajanimi või salasõna pole õige. required_prefix = Sisendi alguses peab olema „&s“ To = Alamharu nimi NewBranchName = Alamharu uus nimi - UserName = Kasutajanimi Description = Kirjeldus Pronouns = Asesõnad @@ -371,7 +356,7 @@ Biography = Biograafia Website = Veebisait Location = Asukoht Content = Sisu - +target_branch_not_exist = Sihiks võetud alamharu pole olemas. password_complexity = Salasõna ei vasta keerukuse reeglitele: password_lowercase_one = Peaks olema vähemalt üks väiketäht password_uppercase_one = Peaks olema vähemalt üks suurtäht @@ -379,7 +364,6 @@ password_digit_one = Peaks olema vähemalt üks number password_special_one = Peaks olema vähemalt üks erimärk (kirjavahemärk, sulg, jutumärk, jne) enterred_invalid_password = Sinu sisestatud salasõna pole korrektne. unset_password = Siselogiv kasutaja pole lisanud oma kontole salasõna. -target_branch_not_exist = Sihiks võetud alamharu pole olemas. [settings] retype_new_password = Korda uut salasõna @@ -389,7 +373,6 @@ update_password = Muuda salasõna old_password = Senine salasõna new_password = Uus salasõna comment_type_group_branch = Alamharu - profile = Profiil account = Kasutajakonto appearance = Välimus @@ -434,7 +417,6 @@ permission_read = Lugemine permissions_list = Õigused: save_application = Salvesta oauth2_application_edit = Muuda - change_password = Salasõna muutmine password_change_disabled = Kohalikus serveris mitteleiduvad kasutajad ei saa oma salasõna Forgejo kasutajaliidesest muuta. email_desc = Sinu põhilist e-posti aadressi kasutatakse teavitusteks, salasõna taastamiseks ning kui ta pole peidetud, siis ka veebipõhisteks toiminguteks gitiga. @@ -475,8 +457,6 @@ default_branch_label = vaikimisi code.desc = Ligipääs lähtekoodile, failidele, sissekannetele ja alamharudele. filter_branch_and_tag = Filtreeri alamharu või sildi alusel branches = Alamharud -n_branch_one = %s alamharu -n_branch_few = %s alamharu commit_graph.select = Vali alamharud commit.contained_in_default_branch = See sissekanne on vaikimisi alamharu osa editor.new_branch_name_desc = Alamharu uus nimi… @@ -493,28 +473,23 @@ settings.event_delete_desc = Alamharu või silt on kustutatud. settings.branches = Alamharud settings.protected_branch.save_rule = Salvesta reegel settings.protected_branch.delete_rule = Kustuta reegel - -editor.add_file = Lisa fail editor.or = või editor.cancel_lower = Katkesta editor.add_tmpl.filename = failinimi pulls.editable = Muudetav - +editor.add_file = Lisa fail fork_repo = Tee lähtekoodihoidlast uus koodiharu fork_from = Tee koodiharu allikast already_forked = Sa juba oled siit teinud koodiharu: %s fork_to_different_account = Tee koodihatu teisele kasutajakontole fork_visibility_helper = Lähtekoodihoidla koodiharu nähtavust ei saa muuta. -fork_branch = Alamharu, millest tahad kloonid koodiharu -mirror_denied_combination = Salasõnapõhist ja avaliku võtme põhist autentimist ei saa samal ajal kasutada. forks = Koodharud -tree_path_not_found.branch = Asukohta „%[1]s“ pole olemas alamharus „%[2]s“ -template.git_content = Giti sisu (vaikimisi alamharu) sync_fork.button = Sünkrooni forked_from = koodiharu allikast fork_from_self = Sa ei saa endale kuuluvast lähtekoodi hoidlast koodiharu teha. fork_guest_user = Logi sisse sellest lähtekoodi hoidlast koodiharu tegemiseks. fork = Tee koodiharu +fork_branch = Alamharu, millest tahad kloonid koodiharu commit.load_referencing_branches_and_tags = Laadi alamharud ja sildid, mis viitavad sellele sissekandele editor.must_be_on_a_branch = Selle faili muutmiseks või muudatuste pakkumiseks pead asuma antud alamharus. editor.create_new_branch_np = Loo selle sissekande jaoks uus alamharu. @@ -526,14 +501,14 @@ commit.revert-content = Vali alamharu, kuhu tahad tagasi pöörata: issues.delete_branch_at = `kustutatud alamharu %s %s` pulls.filter_branch = Filtreeri alamharu alusel pulls.nothing_to_compare_have_tag = Valitud alamharud/sildid on võrdsed. -activity.git_stats_push_to_branch = alamharusse %s ja activity.git_stats_push_to_all_branches = kõikidesse alamharudesse. +activity.git_stats_push_to_branch = alamharusse %s ja activity.git_stats_on_default_branch = Alamharus %s, settings.branches.switch_default_branch = Vaheta vaikimisi alamharusse settings.branch_filter = Alamharude filter settings.protected_branch = Alamharu kaitse -settings.branch_protection = %s alamharu kaitsmise reeglid settings.protect_new_rule = Lisa uus alamharu kaitsmise reegel +settings.branch_protection = %s alamharu kaitsmise reeglid settings.rename_branch = Muuda alamharu nime branch.name = Alamharu nimi branch.already_exists = Alamharu nimega „%s“ on juba olemas. @@ -563,13 +538,11 @@ branch.rename_branch_to = Alamharu „%s“ nimi on muutmisel. branch.create_branch_operation = Loo alamharu branch.new_branch = Loo uus alamharu branch.new_branch_from = Loo uus alamharu siit: „%s“ +tree_path_not_found.branch = Asukohta „%[1]s“ pole olemas alamharus „%[2]s“ +template.git_content = Giti sisu (vaikimisi alamharu) +mirror_denied_combination = Salasõnapõhist ja avaliku võtme põhist autentimist ei saa samal ajal kasutada. [actions] -variables = Muutujad -variables.deletion = Eemalda muutuja -variables.creation = Lisa muutuja -variables.none = Muutujaid veel pole. -variables.management = Halda muutujaid [admin] users.password_helper = Kui sa ei taha salasõna muuta, siis jäta väli tühjaks. @@ -578,7 +551,6 @@ auths.bind_password = Seo salasõna [explore] users = Kasutajad - repos = Koodihoidlad organizations = Organisatsioonid code = Kood @@ -614,5 +586,4 @@ cancel = Katkesta [action] compare_branch = Võrdle -[packages] -alpine.repository.branches = Alamharud \ No newline at end of file +[packages] \ No newline at end of file diff --git a/options/locale/locale_eu.ini b/options/locale/locale_eu.ini index cb4fe075f7..af0a54e418 100644 --- a/options/locale/locale_eu.ini +++ b/options/locale/locale_eu.ini @@ -33,18 +33,6 @@ captcha = CAPTCHA twofa = Bi faktoreko autentifikazioa twofa_scratch = Bi faktoreko marradura-kodea passcode = Pasakodea -webauthn_insert_key = Sartu zure segurtasun-gakoa -webauthn_sign_in = Sakatu botoia zure segurtasun-gakoan. Zure segurtasun-gakoak ez badu botoirik, sartu berriro. -webauthn_press_button = Mesedez, sakatu botoia zure segurtasun-gakoan… -webauthn_use_twofa = Erabili zure telefonoko bi faktoreko kodea -webauthn_error = Ezin izan da irakurri zure segurtasun-gakoa. -webauthn_unsupported_browser = Zure nabigatzaileak ez du WebAuthn onartzen. -webauthn_error_unknown = Akats ezezagun bat gertatu da. Mesedez, saiatu berriro. -webauthn_error_insecure = WebAuthnek konexio seguruak baino ez ditu onartzen. HTTP bidez probatzeko, "localhost" edo "127.0.0.1" jatorria erabil daiteke -webauthn_error_unable_to_process = Zerbitzariak ezin izan du zure eskaera prozesatu. -webauthn_error_duplicated = Segurtasun-gakorik ez da onartzen eskaera honetarako. Mesedez, ziurtatu giltza ea dagoela erregistratuta. -webauthn_error_empty = Gako honetarako izen bat jarri behar duzu. -webauthn_error_timeout = Zure gakoa irakurri aurretik denbora-muga gainditu da. Kargatu berriro orrialde hau eta saiatu berriro. repository = Biltegia new_fork = Biltegi-adar berria new_project_column = Zutabe berria diff --git a/options/locale/locale_fa-IR.ini b/options/locale/locale_fa-IR.ini index 16c6b0228b..bf358b8a8d 100644 --- a/options/locale/locale_fa-IR.ini +++ b/options/locale/locale_fa-IR.ini @@ -7,7 +7,7 @@ sign_in=ورود sign_in_or=یا sign_out=خروج sign_up=ثبت نام -link_account=پیوند به حساب +link_account=پیوند به حساب‌کاربری register=ثبت نام version=نسخه powered_by=قدرت از %s @@ -98,10 +98,6 @@ name=نام logo = نشان sign_in_with_provider = ورود با %s enable_javascript = این وب‌گاه به جاوا اسکریپت نیاز دارد. -webauthn_press_button = لطفاً دکمۀ روی کلید امنیتی را فشار دهید… -webauthn_unsupported_browser = مرورگر شما در حال حاضر از WebAuthn پشتیبانی نمی‌کند. -webauthn_error_unknown = خطایی ناشناخته رخ داد. لطفاً دوباره سعی کنید. -webauthn_error_unable_to_process = کارساز نتوانست درخواست شما را پردازش کند. new_project_column = ستون جدید retry = سعی دوباره rerun = اجرای دوباره @@ -113,17 +109,10 @@ locked = قفل شده copy_hash = رونوشت هش unknown = نامشخص copy_type_unsupported = این نوع از فایل نمی‌تواند رونوشت شود -webauthn_insert_key = کلید امنیتی خود را وارد کنید -webauthn_sign_in = دکمۀ روی کلید امنیتی را فشار دهید. اگر کلید امنیتی شما دکمه‌ای ندارد، آن را دوباره وارد کنید. -webauthn_use_twofa = از یک کد دومرحله‌ای از تلفنتان استفاده کنید -webauthn_error = کلید امنیتی شما نتوانست خوانده شود. more_items = موارد بیشتر -webauthn_error_duplicated = کلید امنیتی برای این درخواست مجاز نیست. لطفاً مطمئن شوید که این کلید در حال حاضر ثبت نشده است. -webauthn_error_timeout = مهلت زمانی قبل از اینکه کلید شما خوانده شود تمام شد. لطفاً این صفحه را تازه‌سازی کرده و مجدد تلاش کنید. new_org.link = سازمان جدید new_org.title = سازمان جدید new_migrate.link = مهاجرت جدید -webauthn_error_empty = شما باید یک نام برای این کلید انتخاب کنید. new_repo.title = مخزن جدید new_migrate.title = مهاجرت جدید new_repo.link = مخزن جدید @@ -155,7 +144,6 @@ filter.not_archived = بایگانی نشده copy_content = رونوشت درون‌مایه invalid_data = داده نامعتبر: %v copy_generic = رونوشت در بریده‌دان -webauthn_error_insecure = احرازوب فقط از روش‌های ایمن ممکن است. برای آزمودن بر روی اچ‌تی‌تی‌پی می‌توانید از «میزبان‌محلی» یا «۱۲۷.۰.۰.۱» استفاده کنید. show_log_seconds = نمایش ثانیه‌ها confirm_delete_selected = تایید می‌کنید که همه موارد گزینش شده حذف شوند؟ filter.clear = پاک‌کردن پالایه‌ها @@ -883,14 +871,6 @@ migrate_options_lfs=مهاجرت فایلهای LFS migrate_options_lfs_endpoint.label=نشانهای پایانی LFS migrate_options_lfs_endpoint.description=Migration سعی خواهد کرد از کنترل از راه دور Git شما برای تعیین سرور LFS استفاده کند. همچنین اگر داده های LFS مخزن در جای دیگری ذخیره شده باشد، می توانید یک نقطه پایانی سفارشی را مشخص کنید. migrate_options_lfs_endpoint.description.local=مسیر سرور محلی نیز پشتیبانی می شود. -migrate_items=مولفه های مهاجرت -migrate_items_wiki=دانشنامه -migrate_items_milestones=نقاط عطف -migrate_items_labels=برچسب‌ها -migrate_items_issues=مسائل -migrate_items_pullrequests=تقاضاهای واکشی -migrate_items_merge_requests=تقاضاهای واکشی -migrate_items_releases=انتشارها migrate_repo=انتقال مخزن migrate.clone_address=انتقال / همسان‌سازی از نشانی migrate.clone_address_desc=HTTP(S) or Git 'همسان‌سازی' نشانی‌های موجود در این مخزن @@ -906,14 +886,6 @@ migrate.migrate=مهاجرت از %s migrate.migrating=مهاجرت از %s ... migrate.migrating_failed=مهاجرت از %s ناموفق بود. migrate.migrating_failed_no_addr=مهاجرت ناموفق بود. -migrate.migrating_git=انتقال داده های Git -migrate.migrating_topics=موضوعات مهاجرت -migrate.migrating_milestones=نقاط عطف مهاجرت -migrate.migrating_labels=برچسب های مهاجرت -migrate.migrating_releases=انتشارات مهاجرت -migrate.migrating_issues=مشکلات مهاجرت -migrate.migrating_pulls=مهاجرت درخواست های pull - mirror_from=قرینه از forked_from=انشعاب شده از generated_from=ساخته شده از @@ -2116,34 +2088,6 @@ dashboard.resync_all_hooks=همگام سازی مجدد hook های pre-receive dashboard.reinit_missing_repos=تمامی مخازنی که سوابقشان وجود دارند مجدداً گیت آنها مفقود شده است مجدداً مقدمات آنها فراهم شود dashboard.sync_external_users=همگام سازی اطلاعات کاربر خارجی dashboard.cleanup_hook_task_table=جدول hook_task تمیز کردن -dashboard.server_uptime=فعالیت بی‌وقفه سرور -dashboard.current_goroutine=Goroutine های فعلی -dashboard.current_memory_usage=میزان مصرف فعلی از حافظه -dashboard.total_memory_allocated=کل حافظه اختصاص داده شده -dashboard.memory_obtained=حافظه به دست آمده -dashboard.pointer_lookup_times=اشاره‌گر زمان‌های جستجو -dashboard.memory_allocate_times=تخصیص یافتن حافظه -dashboard.memory_free_times=آزادسازی حافظه -dashboard.current_heap_usage=میزان مصرف توده فعلی -dashboard.heap_memory_obtained=حافظه به دست آمده برای توده -dashboard.heap_memory_idle=بیکار بوده حافظه توده -dashboard.heap_memory_in_use=حافظه توده در حال استفاده -dashboard.heap_memory_released=حافظه توده آزاد شد -dashboard.heap_objects=شی توده یا Heap -dashboard.bootstrap_stack_usage=میزان استفاده از پشته توسط راهنداز -dashboard.stack_memory_obtained=حافظه پشته به دست آمده -dashboard.mspan_structures_usage=میزان استفاده‌ی ساختار های MSpan -dashboard.mspan_structures_obtained=ساختار های MSpan به دست آمده -dashboard.mcache_structures_usage=میزان استفاده از ساختار های MCache -dashboard.mcache_structures_obtained=ساختار های MCache به دست آمده -dashboard.profiling_bucket_hash_table_obtained=Profiling Bucket Hash Table به دست آمده -dashboard.gc_metadata_obtained=متادیتاهای بدست امده از GC -dashboard.other_system_allocation_obtained=تخصیص های حافظه در سایر قسمت های سیستم -dashboard.next_gc_recycle=بازیافت GC بعدی -dashboard.last_gc_time=زمان از آخرین GC -dashboard.total_gc_pause=کل زمان مکث GC -dashboard.last_gc_pause=واپسین مکث در GC -dashboard.gc_times=زمان های GC dashboard.delete_old_actions=تمام اقدامات قدیمی را از پایگاه داده حذف کنید dashboard.delete_old_actions.started=حذف تمام اقدامات قدیمی از پایگاه داده شروع شده است. @@ -2183,19 +2127,6 @@ users.still_own_repo=این کاربر هنوز صاحب یک یا چند مخز users.still_has_org=این کاربر عضو یک سازمان است. ابتدا کاربر را از هر سازمان متعلق حذف کنید. users.deletion_success=حساب کاربری حذف شد. users.reset_2fa=2FA را بازنشانی کنید -users.list_status_filter.menu_text=فیلتر -users.list_status_filter.reset=شروع دوباره -users.list_status_filter.is_active=فعال -users.list_status_filter.not_active=غیرفعال -users.list_status_filter.is_admin=ادمین -users.list_status_filter.not_admin=غیرادمین -users.list_status_filter.is_restricted=محصور -users.list_status_filter.not_restricted=محدود نشده است -users.list_status_filter.is_prohibit_login=ورود را ممنوع شود -users.list_status_filter.not_prohibit_login=اجازه ورود -users.list_status_filter.is_2fa_enabled=2FA فعال است -users.list_status_filter.not_2fa_enabled=2FA غیرفعال است - emails.email_manage_panel=مدیریت ایمیل کاربر emails.primary=اولیه emails.activated=فعال شده @@ -2463,20 +2394,6 @@ monitor.process.cancel_desc=لغو کردن یک فرآیند ممکن است ب monitor.process.cancel_notices=لغو: %s؟ monitor.process.children=فرزندان -monitor.queues=صف ها -monitor.queue=صف: %s -monitor.queue.name=نام -monitor.queue.type=نوع -monitor.queue.exemplar=نوع نمونه -monitor.queue.numberworkers=تعداد کارگران -monitor.queue.maxnumberworkers=بیشینه تعداد کارگران -monitor.queue.settings.title=تنظیمات استخر -monitor.queue.settings.maxnumberworkers=بیشینه تعداد کارگران -monitor.queue.settings.maxnumberworkers.placeholder=در حال حاضر %[1]v -monitor.queue.settings.maxnumberworkers.error=حداکثر تعداد کارگران باید یک عدد باشد -monitor.queue.settings.submit=بالا بردن ساماندهی -monitor.queue.settings.changed=تنظیمات تازه شد - notices.system_notice_list=هشدارهای سامانه notices.view_detail_header=مشاهده جزئیات اخطار notices.select_all=انتخاب همه @@ -2548,69 +2465,22 @@ raw_seconds=ثانیه raw_minutes=دقیقه [dropzone] -default_message=فایل را در این محل رها کنید یا دکمه ی آپلود یا بارگزاری را فشار دهید. -invalid_input_type=شما قادر به ارسال فایل های از این نوع نیستید. -file_too_big=حجم فایل ({{filesize}} MB) بیش از حداکثر مجاز است ({{maxFilesize}} مگابایت). -remove_file=حذف پرونده [notification] -notifications=اعلان‌ها -unread=خوانده نشده -read=خواندن -no_unread=اعلان خوانده نشده‌ای موجود نیست. -no_read=اعلان خوانده شده‌ای موجود نیست. -pin=سنجاق کردن اعلان -mark_as_read=علامتگذاری بعنوان خوانده شده -mark_as_unread=علامتگذاری بعنوان خوانده نشده -mark_all_as_read=علامت همه به عنوان خوانده شده [gpg] -default_key=ثبت شده با کلید پیش فرض -error.extract_sign=خطا در استخراج امضا -error.generate_hash=خطا در ساختن هش کامیت -error.no_committer_account=هیچ ایمیلی به حساب کاربری صاحب کامیت پیونده داده نشده است -error.no_gpg_keys_found=هیچ کلید شناخته شده ای برای این امضا در پایگاه داده ها یافت نشد -error.not_signed_commit=هیچ کامیتی تکلیف نشده است -error.failed_retrieval_gpg_keys=بازیابی هر کلیدی که به حساب کاربری کامیت دهنده پیوست شده بود ناموفق بود -error.probable_bad_signature=هشدار! اگرچه اینجا یک کلید با ID در پایگاه داده است این کامیت تایید نشده است! این کامیت مشـــکــــوک است. -error.probable_bad_default_signature=هشدار! اگرچه اینجا یک کلید پیش فرض با ID است این اما کامیت تایید نشده است! این کامیت مشـــکــــوک است. [units] error.no_unit_allowed_repo=شما اجازه دسترسی به هیچ قسمت از این مخزن را ندارید. error.unit_not_allowed=شما اجازه دسترسی به این قسمت مخزن را ندارید. [packages] -filter.type=نوع -alpine.repository.branches=شاخه‎ها -alpine.repository.repositories=مخازن conan.details.repository=مخزن owner.settings.cleanuprules.enabled=فعال شده [secrets] [actions] -runners.name=نام -runners.owner_type=نوع -runners.description=شرح -runners.task_list.run=اجرا -runners.task_list.repository=مخزن -runners.task_list.commit=کامیت -runners.status.active=فعال - -runs.commit=کامیت -variables.edit = تغییر متغیر -variables.deletion.success = متغیر حذف شد. -variables.deletion = حذف متغیر -variables.creation.success = متغیر "%s" ایجاد شد. -variables = متغیرها -variables.management = مدیریت متغیرها -variables.update.failed = عدم موفقیت در تغییر متغیر -variables.update.success = تغییر متغیر با موفقیت انجام شد. -variables.creation = افزودن متغیر -variables.none = هیچ متغیری هنوز وجود ندارد. - - - [projects] type-1.display_name = پروژه ی مستقل diff --git a/options/locale/locale_fi-FI.ini b/options/locale/locale_fi-FI.ini index e158ca0dff..5e89ce5b5d 100644 --- a/options/locale/locale_fi-FI.ini +++ b/options/locale/locale_fi-FI.ini @@ -33,18 +33,6 @@ twofa=Kaksivaiheinen todennus twofa_scratch=Kaksivaiheinen kertakäyttöinen koodi passcode=Pääsykoodi -webauthn_insert_key=Aseta turva-avaimesi -webauthn_sign_in=Paina turva-avaimesi painiketta. Jos turva-avaimessasi ei ole painiketta, irrota se ja aseta uudelleen. -webauthn_press_button=Paina turva-avaimesi painiketta… -webauthn_use_twofa=Käytä kaksivaihesta todennusta puhelimestasi -webauthn_error=Turva-avainta ei voitu lukea. -webauthn_unsupported_browser=Selaimesi ei tällä hetkellä tue WebAuthnia. -webauthn_error_unknown=Tuntematon virhe. Yritä uudelleen. -webauthn_error_insecure=WebAuthn tukee vain turvallisia yhteyksiä. Testaamista varten HTTP-välityksellä voit käyttää alkuperää "localhost" tai "127.0.0.1" -webauthn_error_unable_to_process=Palvelin ei pystynyt käsittelemään pyyntöä. -webauthn_error_duplicated=Turva-avainta ei ole sallittu tässä pyynnössä. Varmista, ettei avainta ole jo rekisteröity. -webauthn_error_empty=Sinun täytyy asettaa nimi tälle avaimelle. -webauthn_error_timeout=Aikakatkaisu ennen kuin avaintasi voitiin lukea. Lataa tämä sivu uudelleen ja yritä uudelleen. repository=Tietovarasto organization=Organisaatio mirror=Peili @@ -178,7 +166,7 @@ contributions_format = {contributions} {day}. {month} {year} [editor] buttons.code.tooltip = Lisää koodia -buttons.link.tooltip = Lisää linkki +buttons.link.tooltip = Lisää linkki (Ctrl+K / ⌘K) buttons.mention.tooltip = Mainitse käyttäjä tai tiimi buttons.list.task.tooltip = Lisää tehtävälista buttons.disable_monospace_font = Poista tasalevyinen fontti käytöstä @@ -1009,16 +997,13 @@ ssh_principal_deletion_success = Prinsipaali on poistettu. principal_state_desc = Tätä prinsipaalia on käytetty viimeisen seitsemän päivän aikana regenerate_token_success = Poletti on luotu uudelleen. Sovellukset, jotka käyttivät polettia, eivät enää pääse tilillesi. Kyseiset sovellukset tulee päivittää uudella poletilla. quota.sizes.assets.all = Resurssit - can_not_add_email_activations_pending = Aktivointi odottaa. Odota hetki ja yritä uudelleen muutaman minuutin kuluttua, jos haluat lisätä uuden sähköpostiosoitteen. gpg_invalid_token_signature = Annettu GPG-avain, allekirjoitus ja poletti eivät täsmää, tai poletti on vanhentunut. ssh_invalid_token_signature = Annettu SSH-avain, allekirjoitus tai poletti eivät täsmää, tai poletti on vanhentunut. access_token_desc = Valitut poletin käyttöoikeudet rajoittavat valtuuden vain vastaaville API-reiteille. Lue dokumentaatio saadaksesi lisätietoja. - oauth2_application_remove_description = OAuth2-sovelluksen poistaminen estää sitä pääsemästä valtuutettuihin käyttäjätileihin tässä instanssissa. Jatketaanko? oauth2_application_locked = Forgejo esirekisteröi joitain OAuth2-sovelluksia käynnistymisen yhteydessä, jos näin on määritetty kokoonpanon asetuksissa. Odottamattoman toiminnan estämiseksi näitä sovelluksia ei voi muokata tai poistaa. Lisätietoja on saatavilla OAuth2-dokumentaatiossa. quota.rule.exceeded.helper = Objektien yhteiskoko tälle säännölle on ylittänyt kiintiön. - regenerate_scratch_token_desc = Jos kadotit palautusavaimesi tai käytit sen jo sisäänkirjautumiseen, voit nollata palautusavaimen tästä. delete_with_all_comments = Tilisi on nuorempi kuin %s. Aavekommenttien välttämiseksi kaikki ongelma- ja vetopyyntökommentit poistetaan tilin mukana. quota.sizes.git.lfs = Git LFS @@ -1073,13 +1058,6 @@ template.issue_labels=Ongelmanimilaput -migrate_items=Migraation kohteet -migrate_items_wiki=Wiki -migrate_items_milestones=Merkkipaalut -migrate_items_labels=Nimilaput -migrate_items_issues=Ongelmat -migrate_items_pullrequests=Vetopyynnöt -migrate_items_releases=Julkaisut migrate_repo=Suorita tietovaraston migraatio migrate.clone_address=Migraatio/kloonaus URL-osoitteesta migrate.github_token_desc=Voit lisätä tähän yhden tai useamman tunnuksen pilkuilla erotettuna nopeuttaaksesi migraatiota kiertämällä GitHub API:n nopeusrajoituksen. Varoitus: Tämän ominaisuuden väärinkäyttö voi rikkoa palveluntarjoajan käytäntöä ja johtaa tiliesi sulkemiseen. @@ -1088,8 +1066,6 @@ migrate.failed=Migraatio epäonnistui: %v migrate.migrate_items_options=Lisäkohteiden migraatiota varten vaaditaan pääsypoletti migrate.migrating=Suoritetaan migraatio lähteestä %s … migrate.migrating_failed=Migraatio lähteestä %s epäonnistui. -migrate.migrating_git=Suoritetaan Git-datan migraatiota - mirror_from=peili kohteelle forked_from=forkattu tietovarastosta unwatch=Lopeta tarkkailu @@ -1683,7 +1659,6 @@ fork_to_different_account = Forkkaa toiselle tilille release.compare = Vertaa release.ahead.commits = %d kommittia all_branches = Kaikki haarat -n_tag_few = %s tagia settings.event_fork_desc = Tietovarasto forkattu. actions = Toiminnat fork_guest_user = Kirjaudu sisään forkataksesi tämän tietovaraston. @@ -1692,19 +1667,16 @@ visibility_fork_helper = (Tämän muuttaminen vaikuttaa kaikkien forkkien näkyv fork = Forkkaa activity.git_stats_commit_n = %d kommittia commits.search_branch = Tämä haara -n_branch_few = %s haaraa pulls.show_all_commits = Näytä kaikki kommitit commit_graph.select = Valitse haarat activity.navbar.recent_commits = Viimeaikaiset kommitit settings.branches.add_new_rule = Lisää uusi sääntö -n_commit_few = %s kommittia issues.force_push_compare = Vertaa commits.desc = Selaa lähdekoodin muutoshistoriaa. clone_helper = Tarvitseko apua kloonauksen kanssa? Siirry tukisivulle. settings.mirror_settings.push_mirror.copy_public_key = Kopioi julkinen avain object_format = Objektimuoto editor.fail_to_update_file_summary = Virheviesti: -n_branch_one = %s haara issues.content_history.delete_from_history_confirm = Poistetaanko historiasta? editor.new_patch = Uusi paikkaus pulls.merged_success = Vetopyyntö yhdistetty onnistuneesti ja suljettu @@ -1754,8 +1726,6 @@ issues.dependency.add_error_dep_exists = Riippuvuus on jo olemassa. wiki.page_content = Sivun sisältö wiki.page_title = Sivun otsikko activity.navbar.contributors = Avustajat -n_release_few = %s julkaisua -n_release_one = %s julkaisu symbolic_link = Symbolinen linkki mirror_last_synced = Viimeksi synkronoitu find_file.go_to_file = Löydä tiedosto @@ -1878,7 +1848,6 @@ issues.filter_label_select_no_label = Ei nimilappua projects.column.set_default = Aseta oletukseksi projects.edit_success = Projekti "%s" on päivitetty. desc.sha256 = SHA256 -n_commit_one = %s kommitti transfer.accept = Hyväksy siirto transfer.reject = Hylkää siirto default_branch_label = oletus @@ -1935,7 +1904,6 @@ milestones.deletion = Poista merkkipaalu more_operations = Lisää toimintoja settings.branches.switch_default_branch = Vaihda oletushaara tag.confirm_create_tag = Luo tagi -n_tag_one = %s tagi branch.renamed = Haaran %s uudeksi nimeksi asetettiin %s. release.tag_name_protected = Tagin nimi on suojattu. pulls.merge_conflict_summary = Virheviesti @@ -1988,7 +1956,6 @@ milestones.filter_sort.earliest_due_data = Lähin eräpäivä issues.filter_type.reviewed_by_you = Katselmoitu toimestasi settings.units.overview = Yleisnäkymä settings.remove_team_success = Tiimin pääsy tietovarastoon on poistettu. -migrate.cancel_migrating_confirm = Haluatko perua tämän migraation? settings.units.units = Yksiköt settings.update_settings_no_unit = Tietovaraston tulisi sallia edes jonkinlainen vuorovaikutus. settings.units.add_more = Ota lisää käyttöön @@ -2062,7 +2029,6 @@ release.title_empty = Nimi ei voi olla tyhjä. archive.title = Tämä tietovarasto on arkistoitu. Voit tarkastella sen tiedostoja ja kloonata sen, mutta et voi tehdä muutoksia sen tilaan, kuten tehdä työntöjä tai luoda uusia ongelmia, vetopyyntöjä tai kommentteja. reactions_more = ja %d lisää mirror_address = Kloonaa URL-osoitteesta -migrate_items_merge_requests = Yhdistämispyynnöt stars_remove_warning = Tämä poistaa kaikki tähdet tästä tietovarastosta. settings.webhook_deletion_desc = Webkoukun poistaminen poistaa sen asetukset ja toimitushistorian. Jatketaanko? settings.discord_icon_url.exceeds_max_length = Kuvakkeen URL-osoite voi sisältää enintään 2048 merkkiä @@ -2087,7 +2053,7 @@ settings.transfer_desc = Siirrä tämä tietovarasto käyttäjälle tai organisa settings.add_collaborator = Lisää avustaja settings.mirror_settings.push_mirror.none = Työntöpeilejä ei ole määritetty settings.collaborator_deletion_desc = Avustajan poistaminen peruuttaa hänen pääsynsä tähän tietovarastoon. Jatketaanko? -settings.archive.text = Tietovaraston arkistointi asettaa sen pelkkään lukutilaan. Se piilotetaan kojelaudalta. Kukaan ei voi tehdä (et edes sinä) uusia kommitteja, tai avata ongelmia tai vetopyyntöjä. +settings.archive.text = Tietovaraston arkistointi asettaa sen pelkkään lukutilaan. Se piilotetaan kojelaudalta. Kukaan ei voi tehdä (et edes sinä) uusia kommitteja, tai avata ongelmia tai vetopyyntöjä. Arkistoinnin syy on suositeltavaa kertoa, sillä kyseinen tieto auttaa tietovaraston haarauttamista harkitsevia kehittäjiä. settings.mirror_settings.docs = Määritä tietovarastosi synkronoimaan kommitit, tagit ja haarat automaattisesti toisen tietovaraston kanssa. settings.add_collaborator_duplicate = Avustaja on jo lisätty tähän tietovarastoon. settings.add_collaborator_blocked_them = Avustajaa ei voi lisätä, koska hän on estänyt tietovaraston omistajan. @@ -2231,8 +2197,6 @@ issues.dismiss_review_warning = Haluatko hylätä katselmoinnin? commit.operations = Toimenpiteet commits.view_single_diff = Näytä tässä kommitissa tähän tiedostoon kohdistuneet muutokset issues.choose.ignore_invalid_templates = Virheelliset mallipohjat on jätetty huomiotta -migrate.migrating_milestones = Suoritetaan merkkipaalujen migraatiota -migrate.migrating_issues = Suoritetaan ongelmien migraatiota migrate.clone_local_path = tai paikallisen palvelimen polku pulls.filter_changes_by_commit = Suodata kommitin perusteella pulls.show_changes_since_your_last_review = Näytä viimeisimmän katselmointisi jälkeiset muutokset @@ -2247,7 +2211,6 @@ migrate.invalid_lfs_endpoint = LFS-päätepiste ei ole kelvollinen. issues.new.clear_projects = Tyhjennä projektit mirror_denied_combination = Julkiseen avaimeen ja salasanaan pohjautuvaa todennusta ei voi käyttää yhdessä. template.git_content = Git-sisältö (Oletushaara) -migrate.migrating_releases = Suoritetaan julkaisujen migraatiota unit_disabled = Sivuston ylläpitäjä on poistanut käytöstä tämän tietovarasto-osion. issues.filter_sort.relevance = Asiaankuuluvuus pulls.reopen_to_merge = Avaa tämä vetopyyntö uudelleen suorittaaksesi yhdistämisen. @@ -2259,9 +2222,6 @@ issues.dismiss_review = Hylkää katselmointi editor.file_changed_while_editing = Tiedoston sisältö on muuttunut sen avaamisen jälkeen. Napsauta tästä nähdäksesi muutokset tai kommitoi muutokset uudelleen korvataksesi muutokset. sync_fork.button = Synkronoi migrated_from = Suoritettu migraatio lähteestä %[2]s -migrate.migrating_topics = Suoritetaan aiheiden migraatiota -migrate.migrating_pulls = Suoritetaan vetopyyntöjen migraatiota -migrate.cancel_migrating_title = Peruuta migraatio file_follow = Seuraa symbolista linkkiä commit.load_referencing_branches_and_tags = Lataa haarat ja tagit, jotka viittaavat tähän kommittiin editor.commit_id_not_matching = Tiedosto muuttui sillä aikaa, kun muokkasit sitä. Kommitoi uuteen haaraan ja yhdistä sen jälkeen. @@ -2285,7 +2245,6 @@ projects.column.deletion_desc = Projektin sarakkeen poistaminen siirtää kaikki issues.del_time = Poista tämä aikaloki migrated_from_fake = Suoritettu migraatio lähteestä %[1]s migrate.migrate = Tee migraatio lähteestä %s -migrate.migrating_labels = Suoritetaan nimilappujen migraatiota file_view_rendered = Näytä renderöitynä editor.invalid_commit_mail = Virheellinen sähköposti kommitin luomista varten. sync_fork.branch_behind_one = Tämä haara on %[1]d kommitin jäljessä %[2]s @@ -2559,19 +2518,13 @@ commitstatus.success = Onnistui projects.deletion_desc = Projektin poistaminen poistaa sen kaikilta siihen liittyviltä ongelmilta. Jatketaanko? issues.lock.unknown_reason = Ongelmaa ei voi lukita tuntemattomalla syyllä. issues.unlock_error = Ongelmaa, jota ei ole lukittu, ei voi avata lukituksesta. - - - issues.tracking_already_started = `Olet jo aloittanut ajanseurannan toisessa ongelmassa!` issues.cancel_tracking_history = `perui ajanseurannan %s` - summary_card_alt = Tietovaraston %s yhteenvetokortti migrate_options_lfs_endpoint.placeholder = Jos jätetty tyhjäksi, päätepiste johdetaan kloonaus-URL:stä broken_message = Tämän tietovaraston taustalla olevaa Git-dataa ei voi lukea. Ota yhteys tämän instanssin ylläpitoon tai poista tietovarasto. issues.edit.already_changed = Muutosten tallentaminen ongelmaan ei onnistu. Vaikuttaa siltä, että sisältöä on jo muutettu toisen käyttäjän toimesta. Päivitä sivu ja yritä muokata uudelleen välttääksesi muiden tekemien muutosten ylikirjoittamisen issues.choose.invalid_templates = Virheellisiä mallipohjia löytyi %v -issues.filter_assginee_no_assignee = Ei käsittelijää -issues.action_assignee_no_select = Ei käsittelijää issues.ref_issue_from = `viittasi tähän ongelmaan %[3]s %[1]s` issues.due_date_invalid = Eräpäivä on virheellinen tai ajanjakson ulkopuolella. Käytä muotoa "yyyy-mm-dd". issues.dependency.issue_close_blocked = Sinun täytyy sulkea kaikki tämän ongelman estävät ongelmat, ennen kuin voit sulkea tämän ongelman. @@ -2579,27 +2532,29 @@ issues.review.pending.tooltip = Tämä kommentti ei ole näkyvissä tällä hetk milestones.deletion_desc = Merkkipaalun poistaminen poistaa sen kaikista siihen liittyvistä ongelmista. Jatketaanko? activity.title.issues_closed_from = %s sulkenut %s settings.protected_branch_duplicate_rule_name = Tälle joukolle haaroja on jo olemassa sääntö - -commit.contained_in = Tämä kommitti sisältyy haaraan: +issues.filter_assginee_no_assignee = Ei käsittelijää +issues.action_assignee_no_select = Ei käsittelijää issues.ref_closing_from = `viittasi tähän ongelmaan vetopyynnöstä %[3]s, joka sulkee ongelman, %[1]s` issues.ref_reopening_from = `viittasi tähän ongelmaan vetopyynnöstä %[3]s, joka avaa ongelman uudelleen, %[1]s` -issues.lock_no_reason = lukitsi ja rajoitti keskustelun avustajille %s -issues.dependency.no_permission_1 = Sinulla ei ole oikeutta lukea %d riippuvuutta -issues.dependency.no_permission_n = Sinulla ei ole oikeutta lukea %d riippuvuutta -pulls.editable = Muokattavissa -settings.wiki_branch_rename_success = Tietovaraston wikin haaranimi on normalisoitu onnistuneesti. -settings.wiki_branch_rename_failure = Tietovaraston wikin haaranimen normalisointi epäonnistui. release.releases_for = Projektin %s julkaisut release.tags_for = Projektin %s tagit - +settings.wiki_branch_rename_success = Tietovaraston wikin haaranimi on normalisoitu onnistuneesti. +settings.wiki_branch_rename_failure = Tietovaraston wikin haaranimen normalisointi epäonnistui. +pulls.editable = Muokattavissa +issues.dependency.no_permission_1 = Sinulla ei ole oikeutta lukea %d riippuvuutta +issues.dependency.no_permission_n = Sinulla ei ole oikeutta lukea %d riippuvuutta +issues.lock_no_reason = lukitsi ja rajoitti keskustelun avustajille %s +commit.contained_in = Tämä kommitti sisältyy haaraan: +diff.file_suppressed = Tiedoston eroavaisuutta ei näytetä, koska se on liian suuri size_format = %[1]s: %[2]s, %[3]s: %[4]s transfer.accept_desc = Siirrä taholle "%s" transfer.reject_desc = Peru siirto taholle "%s" -invisible_runes_description = `Tämä tiedosto sisältää näkymättömiä Unicode-merkkejä, joita ihminen ei huomaa, mutta jotka mahdollisesti käsitellään eri tavalla tietokoneen toimesta. Jos olet sitä mieltä, että tämä on tarkoituksellista, voit ohittaa tämän varoituksen. Käytä eskapointipainiketta näyttääksesi ne.` ambiguous_runes_description = `Tämä tiedosto sisältää Unicode-merkkejä, jotka voi olla virheellisesti yhdistettävissä toisiin merkkeihin. Jos olet sitä mieltä, että tämä on tarkoituksellista, voit ohittaa tämän varoituksen. Käytä eskapointipainiketta näyttääksesi ne.` escape_control_characters = Eskapoi unescape_control_characters = Poista eskapointi -diff.file_suppressed = Tiedoston eroavaisuutta ei näytetä, koska se on liian suuri +invisible_runes_description = `Tämä tiedosto sisältää näkymättömiä Unicode-merkkejä, joita ihminen ei huomaa, mutta jotka mahdollisesti käsitellään eri tavalla tietokoneen toimesta. Jos olet sitä mieltä, että tämä on tarkoituksellista, voit ohittaa tämän varoituksen. Käytä eskapointipainiketta näyttääksesi ne.` + + [graphs] component_loading_info = Tämä saattaa kestää hetken… @@ -2619,7 +2574,7 @@ repo_updated=Päivitetty %s members=Jäsenet teams=Tiimit lower_members=jäsentä -lower_repositories=tietovarastot +lower_repositories=tietovarastoa create_new_team=Uusi tiimi create_team=Luo tiimi org_desc=Kuvaus @@ -2724,9 +2679,8 @@ settings.change_orgname_redirect_prompt.with_cooldown.one = Vanha organisaation settings.change_orgname_redirect_prompt.with_cooldown.few = Vanha organisaation nimi on kenen tahansa saatavilla %[1]d päivän suojaamisjakson jälkeen. Voit palauttaa organisaation nimen itsellesi suojaamisjakson aikana. teams.all_repositories_helper = Tiimillä on pääsy kaikkiin tietovarastoihin. Tämän valitseminen lisää kaikki olemassa olevat tietovarastot tiimiin. settings.labels_desc = Lisää nimilappuja, joita voi käyttää tämän organisaation kaikkien tietovarastojen ongelmissa. - -teams.general_access_helper = Jäsenten oikeudet päätetään alla olevan käyttöoikeustaulun perusteella. teams.invite_team_member = Kutsu tiimiin %s +teams.general_access_helper = Jäsenten oikeudet päätetään alla olevan käyttöoikeustaulun perusteella. [admin] dashboard=Kojelauta @@ -2750,33 +2704,6 @@ dashboard.operation_switch=Vaihda dashboard.operation_run=Suorita dashboard.delete_inactive_accounts=Poista kaikki aktivoimattomat tilit dashboard.delete_repo_archives=Poista kaikki tietovarastojen arkistot (ZIP, TAR.GZ, jne.) -dashboard.server_uptime=Palvelimen uptime -dashboard.current_goroutine=Nykyiset goroutinet -dashboard.current_memory_usage=Nykyinen muistinkäyttö -dashboard.total_memory_allocated=Yhteensä muistia varattu -dashboard.memory_obtained=Muistia saatu -dashboard.pointer_lookup_times=Osoittimen hakuajat -dashboard.current_heap_usage=Nykyinen heapin käyttö -dashboard.heap_memory_obtained=Heap-muisti saatu -dashboard.heap_memory_idle=Heap-muisti tyhjäkäynnillä -dashboard.heap_memory_in_use=Heap-muisti käytössä -dashboard.heap_memory_released=Heap-muisti vapautettu -dashboard.heap_objects=Heap-objektit -dashboard.bootstrap_stack_usage=Bootstrap-pinon käyttö -dashboard.stack_memory_obtained=Pinonmuisti saatu -dashboard.mspan_structures_usage=MSpan-rakenteiden käyttö -dashboard.mspan_structures_obtained=MSpan-rakenteet saatu -dashboard.mcache_structures_usage=MCache-rakenteiden käyttö -dashboard.mcache_structures_obtained=MCache-rakenteet saatu -dashboard.profiling_bucket_hash_table_obtained=Profilointiämpäritiivistetaulukko saatu -dashboard.gc_metadata_obtained=Roskienkeruumetatiedot saatu -dashboard.other_system_allocation_obtained=Muu järjestestelmän varaus saatu -dashboard.next_gc_recycle=Seuraava roskienkeruukierrätys -dashboard.last_gc_time=Aika edellisestä roskienkeruusta -dashboard.total_gc_pause=Yhteensä roskienkeruutauko -dashboard.last_gc_pause=Viimeinen roskienkeruutauko -dashboard.gc_times=Roskienkeruuajat - users.user_manage_panel=Käyttäjätilien hallinta users.new_account=Luo käyttäjätili users.name=Käyttäjänimi @@ -2804,19 +2731,6 @@ users.allow_git_hook=Voi luoda Git-koukkuja users.allow_create_organization=Voi luoda organisaatioita users.update_profile=Päivitä käyttäjätili users.delete_account=Poista käyttäjätili -users.list_status_filter.menu_text=Suodata -users.list_status_filter.reset=Tyhjennä -users.list_status_filter.is_active=Aktiivinen -users.list_status_filter.not_active=Ei-aktiivinen -users.list_status_filter.is_admin=Ylläpitäjä -users.list_status_filter.not_admin=Ei ylläpitäjä -users.list_status_filter.is_restricted=Rajoitettu -users.list_status_filter.not_restricted=Ei rajoitettu -users.list_status_filter.is_prohibit_login=Kirjautuminen estetty -users.list_status_filter.not_prohibit_login=Kirjautuminen sallittu -users.list_status_filter.is_2fa_enabled=2FA käytössä -users.list_status_filter.not_2fa_enabled=2FA ei käytössä - emails.email_manage_panel=Käyttäjien sähköpostien hallinta emails.primary=Ensisijainen emails.activated=Aktivoitu @@ -2971,12 +2885,6 @@ monitor.desc=Kuvaus monitor.start=Alkamisaika monitor.execute_time=Suoritusaika -monitor.queues=Jonot -monitor.queue=Jono: %s -monitor.queue.name=Nimi -monitor.queue.type=Tyyppi -monitor.queue.settings.submit=Päivitä asetukset - notices.system_notice_list=Järjestelmän ilmoitukset notices.select_all=Valitse kaikki notices.deselect_all=Poista kaikki valinnat @@ -3006,7 +2914,6 @@ users.new_success = Käyttäjätili "%s" on luotu. config.disable_register = Poista itserekisteröinti käytöstä config.enable_openid_signin = Ota OpenID-kirjautuminen käyttöön config.enable_openid_signup = Ota OpenID-itserekisteröinti käyttöön -monitor.queue.settings.changed = Asetukset päivitetty config.db_schema = Skeema settings = Ylläpitäjän asetukset emails.delete = Poista sähköpostiosoite @@ -3029,7 +2936,6 @@ auths.oauth2_provider = OAuth2-palveluntarjoaja auths.tips.gmail_settings = Gmail-asetukset: config.mailer_sendmail_path = Sendmail-polku config_settings = Asetukset -monitor.queue.settings.remove_all_items = Poista kaikki config.skip_tls_verify = Ohita TLS-vahvistus dashboard.new_version_hint = Forgejo %s on nyt saatavilla. Käytössäsi on %s. Lue lisätietoja blogista. defaulthooks.add_webhook = Lisää oletusarvoinen webkoukku @@ -3131,7 +3037,6 @@ dashboard.update_checker = Päivitysten tarkistaja auths.allowed_domains_helper = Jätä tyhjäksi salliaksesi kaikki verkkotunnukset. Erota useat verkkotunnukset pilkulla (","). auths.activated = Tämä todennuslähde on aktivoitu auths.login_source_of_type_exist = Tätä tyyppiä oleva todennuslähde on jo olemassa. -dashboard.memory_allocate_times = Muistiallokaatiot users.send_register_notify = Ilmoita rekisteröitymisestä sähköpostitse config.offline_mode = Paikallinen tila config.cache_item_ttl = Välimuistitietueen TTL @@ -3162,21 +3067,18 @@ monitor.process.cancel_notices = Perutaanko: %s? config.enable_federated_avatar = Ota federoidut profiilikuvat käyttöön notices.operations = Toimenpiteet config.xorm_log_sql = Lokita SQL -monitor.queue.settings.remove_all_items_done = Kaikki jonossa olleet tietueet on poistettu. config.logger_name_fmt = Lokittaja: %s config.git_max_diff_line_characters = Diff-merkkejä enintään riviä kohden config.git_max_diff_files = Diff-tiedostoja enintään näytettäväksi config.access_log_mode = Pääsylokin tila config.picture_config = Kuvan ja avatarin asetukset notices.delete_success = Järjestelmäilmoitukset on poistettu. -monitor.queue.settings.maxnumberworkers.placeholder = Tällä hetkellä %[1]d auths.force_smtps_helper = SMTPS:ää käytetään aina portissa 465. Aseta tämä pakottaaksesi SMTPS toisiin portteihin. (Muuten STARTTLS:ää käytetään toisiin portteihin, jos palvelin tukee sitä.) dashboard.resync_all_sshprincipals = Päivitä ".ssh/authorized_principals"-tiedosto Forgejon SSH-prinsipaaleilla. - - auths.verify_group_membership = Vahvista ryhmäjäsenyys LDAP:issa (jätä suodatin tyhjäksi ohittaaksesi) auths.oauth2_map_group_to_team_removal = Poista käyttäjät synkronoiduista tiimeistä, jos käyttäjä ei kuulu vastaavaan ryhmään. + [action] create_repo=loi tietovaraston %s rename_repo=asetti tietovaraston %[1]s uudeksi nimeksi %[3]s @@ -3226,35 +3128,10 @@ raw_seconds=sekuntia raw_minutes=minuuttia [dropzone] -default_message=Pudota tiedostot tähän tai napsauta tästä lähettääksesi tiedoston. -invalid_input_type=Tätä tyyppiä olevia tiedostoja ei voi lähettää. -remove_file=Poista tiedosto -file_too_big = Tiedoston koko ({{filesize}} Mt) ylittää enimmäisrajan ({{maxFilesize}} Mt). [notification] -notifications=Ilmoitukset -unread=Lukematon -read=Luettu -no_unread=Ei lukemattomia ilmoituksia. -no_read=Ei luettuja ilmoituksia. -pin=Kiinnitä ilmoitus -mark_as_read=Merkitse luetuksi -mark_as_unread=Merkitse lukemattomaksi -mark_all_as_read=Merkitse kaikki luetuiksi -watching = Tarkkaillaan -no_subscriptions = Ei tilauksia -subscriptions = Tilaukset [gpg] -error.no_committer_account=Kommitin tekijän sähköpostiosoitteeseen ei ole linkitetty tiliä -error.not_signed_commit=Kommitti ei ole allekirjoitettu -error.extract_sign = Allekirjoituksen purkaminen epäonnistui -default_key = Allekirjoitettu oletusavaimella -error.failed_retrieval_gpg_keys = Ei saatu yhtäkään kommitin tekijän tiliin liitettyä avainta -error.generate_hash = Kommitin tiivisteen luominen epäonnistui -error.probable_bad_signature = VAROITUS! Vaikka tietokannassa on avain tällä ID-tunnistella, se ei vahvista tätä kommittia! Tämä kommitti on EPÄILYTTÄVÄ. -error.probable_bad_default_signature = VAROITUS! Vaikka oletusavaimella on tämä ID-tunniste, se ei vahvista tätä kommittia! Tämä kommitti on EPÄILYTTÄVÄ. -error.no_gpg_keys_found = Tälle allekirjoitukselle ei löytynyt tunnettua avainta tietokannasta [units] unit = Yksikkö @@ -3262,163 +3139,11 @@ error.unit_not_allowed = Sinulla ei ole pääsyä tähän tietovaraston osioon. error.no_unit_allowed_repo = Sinulla ei ole pääsyä mihinkään tämän tietovaraston osioon. [packages] -title=Paketit desc=Hallitse tietovaraston paketteja. -empty=Täällä ei vielä ole paketteja. -filter.type=Tyyppi -filter.type.all=Kaikki -filter.no_result=Suodattimesi ei tuottanut tuloksia. -installation=Asennus -details.author=Tekijä -alpine.repository.branches=Haarat -alpine.repository.repositories=Tietovarastot conan.details.repository=Tietovarasto owner.settings.cleanuprules.enabled=Käytössä -details.license = Lisenssi -about = Tietoja tästä paketista -debian.install = Asenna paketti komennolla: -owner.settings.cleanuprules.edit = Muokkaa siivoussääntöä -arch.version.groups = Ryhmä -details.project_site = Projektin verkkosivusto -details.repository_site = Tietovaraston verkkosivusto -container.pull = Vedä levykuva komentoriviltä: -generic.download = Lataa paketti komentoriviltä: -dependency.version = Versio -keywords = Avainsanat -dependencies = Riippuvuudet -container.labels.key = Avain -container.labels.value = Arvo -pypi.install = Asenna paketti pipillä komennolla: -npm.install = Asenna paketti npm:llä komennolla: -npm.install2 = tai lisää se package.json-tiedostoon: -empty.documentation = Lisätietoja pakettirekisteristä on saatavilla dokumentaatiossa. -helm.install = Asenna paketti komennolla: -owner.settings.chef.keypair = Luo avainpari -settings.delete.error = Paketin poistaminen epäonnistui. -requirements = Vaatimukset -published_by_in = Julkaistu %[1]s, julkaisija %[3]s tietovarastossa %[5]s -pypi.requires = Vaatii Pythonin -alpine.install = Asenna paketti seuraavalla komennolla: -debian.repository.components = Komponentit -cran.install = Asenna paketti komennolla: -settings.link.select = Valitse tietovarasto -owner.settings.chef.title = Chef-rekisteri -owner.settings.cleanuprules.add = Lisää siivoussääntö -versions = Versiot -versions.view_all = Näytä kaikki -debian.repository.architectures = Arkkitehtuurit -container.details.type = Levykuvan tyyppi -arch.version.properties = Version ominaisuudet -rpm.install = Asenna paketti komennolla: -owner.settings.cleanuprules.none = Siivoussääntöjä ei vielä ole. -container.details.platform = Alusta -npm.dependencies = Riippuvuudet -owner.settings.cleanuprules.title = Siivoussäännöt -arch.version.depends = Riippuu -settings.delete = Poista paketti -arch.version.description = Kuvaus -settings.delete.success = Paketti on poistettu. -npm.dependencies.optional = Valinnaiset riippuvuudet -debian.repository.distributions = Jakelut -composer.dependencies = Riippuvuudet -chef.install = Asenna paketti komennolla: -details.documentation_site = Dokumentaation verkkosivusto -go.install = Asenna paketti komentoriviltä: -alpine.repository.architectures = Arkkitehtuurit -composer.registry = Määritä tämä rekisteri ~/.composer/config.json-tiedostossa: -debian.registry = Määritä tämä rekisteri komentoriviltä: -rpm.registry = Määritä rekisteri komentoriviltä: -maven.install = Käytä pakettia sisällyttämällä seuraava sisältö dependencies-lohkoon pom.xml-tiedostossa: -npm.registry = Määritä rekisteri projektin .npmrc-tiedostossa: -alpine.repository = Tietovaraston tiedot -cargo.registry = Määritä tämä rekisteri Cargon asetustiedostossa (esimerkiksi ~/.cargo/config.toml): -cargo.install = Asenna paketti Cargolla suorittamalla seuraava komento: -composer.install = Asenna paketti Composerilla suorittamalla komento: -rpm.distros.redhat = RedHatiin pohjautuvilla jakeluilla -rpm.distros.suse = SUSE:en pohjautuvilla jakeluilla -rpm.repository.architectures = Arkkitehtuurit -cran.registry = Määritä rekisteri Rprofile.site-tiedostossa: -swift.install2 = ja suorita komento: -maven.registry = Määritä tämä rekisteri projektin pom.xml-tiedostossa: -maven.install2 = Suorita komentoriviltä: -nuget.registry = Määritä rekisteri komentoriviltä: -nuget.install = Asenna paketti NuGetillä suorittamalla komento: -rubygems.install = Asenna paketti gemillä suorittamalla komento: -rubygems.install2 = tai lisää se Gemfileen: -swift.registry = Määritä rekisteri komentoriviltä: -swift.install = Lisää paketti Package.swift-tiedostoon: owner.settings.cleanuprules.keep.count.1 = 1 versio per paketti owner.settings.cleanuprules.keep.count.n = %d versiota per paketti -conan.install = Asenna paketti Conanilla suorittamalla komento: -chef.registry = Määritä tämä rekisteri ~/.chef/config.rb-tiedostossa: -conan.registry = Määritä tämä rekisteri komentoriviltä: -conda.install = Asenna paketti Condalla suorittamalla komento: -helm.registry = Määritä tämä rekisteri komentoriviltä: -pub.install = Asenna paketti Dartilla suorittamalla komento: -owner.settings.cargo.title = Cargo-rekisterin indeksi -settings.delete.description = Paketin poistaminen on peruuttamaton toimenpide, sitä ei voi perua. -settings.link.success = Tietovaraston linkki päivitettiin onnistuneesti. -settings.link.button = Päivitä tietovaraston linkki -owner.settings.cleanuprules.preview.overview = %d pakettia on ajastettu poistettavaksi. -owner.settings.cargo.initialize.success = Cargo-indeksi luotiin onnistuneesti. -vagrant.install = Lisää Vagrant-boksi suorittamalla komento: -rubygems.dependencies.development = Kehitysriippuvuudet -owner.settings.cleanuprules.preview = Siivoussäännön esikatselu -npm.dependencies.development = Kehitysriippuvuudet -composer.dependencies.development = Kehitysriippuvuudet -owner.settings.cleanuprules.success.update = Siivoussääntö on päivitetty. -owner.settings.cleanuprules.success.delete = Siivoussääntö on poistettu. -settings.link = Linkitä tämä paketti tietovarastoon -maven.download = Lataa riippuvuus suorittamalla komentorivillä: -registry.documentation = Lisätietoja %s-rekisteristä on dokumentaatiossa. -owner.settings.chef.keypair.description = Chef-rekisteriin lähetettävät pyynnöt on allekirjoitettava salauskirjoituksella todennuskeinona. Avainparia luotaessa vain julkinen avain tallennetaan Forgejoon. Yksityinen avain toimitetaan sinulle käytettäväksi knifen kanssa. Uuden avainparin luominen korvaa edellisen. -owner.settings.cleanuprules.keep.pattern = Säilytä kaavaa vastaavat versiot -owner.settings.cleanuprules.pattern_full_match = Toteuta kaava paketin koko nimeen -owner.settings.cleanuprules.keep.title = Näitä sääntöjä vastaavat versiot säilytetään, vaikka ne vastaisivat alla olevaa poistosääntöä. -owner.settings.cleanuprules.keep.count = Säilytä viimeisimmät -owner.settings.cleanuprules.remove.pattern = Poista kaavaa vastaavat versiot -owner.settings.cleanuprules.keep.pattern.container = Viimeisin (latest) versio säilytetään aina Container-paketeista. -owner.settings.cleanuprules.remove.title = Näitä sääntöjä vastaavat versiot poistetaan, ellei sääntö yläpuolella käske säilyttää niitä. -owner.settings.cleanuprules.remove.days = Poista versiot, jotka ovat vanhempia kuin -arch.pacman.helper.gpg = Lisää luottamusvarmenne pacmanille: -arch.pacman.sync = Synkronoi paketti pacmanin kanssa: -debian.registry.info = Valitse $distribution ja $component alla olevasta listasta. -rpm.repository.multiple_groups = Tämä paketti on saatavilla useissa ryhmissä. -owner.settings.cargo.rebuild.success = Cargo-indeksi rakennettiin uudelleen. -owner.settings.cleanuprules.preview.none = Siivoussääntö ei vastaa yhtäkään pakettia. -arch.pacman.conf = Lisää palvelin asiaan liittyvällä jakelulla ja arkkitehtuurilla tiedostoon /etc/pacman.conf : -published_by = Julkaistu %[1]s käyttäjän %[3]s toimesta -alpine.registry.key = Lataa rekisterin julkinen RSA-avain hakemistoon /etc/apk/keys/ vahvistaaksesi indeksin allekirjoituksen: -alpine.registry = Aseta tämä rekisteri lisäämällä URL-osoite tiedostoon /etc/apk/tietovarastot: -rubygems.dependencies.runtime = Ajonaikaiset riippuvuudet -owner.settings.cargo.rebuild.error = Cargo-indeksin rakentaminen uudelleen epäonnistui: %v -owner.settings.cargo.rebuild = Rakenna indeksi uudelleen -empty.repo = Lähetitkö paketin, mutta se ei näy täällä? Siirry paketin asetuksiin ja linkitä se tähän tietovarastoon. -alpine.registry.info = Valitse $branch ja $repository alla olevasta listasta. -container.images.title = Levykuvat -owner.settings.cargo.initialize = Alusta indeksi -owner.settings.cargo.initialize.description = Cargo-rekisterin käyttöä varten tarvitaan erityinen indeksi Git -tietovarasto. Tämän vaihtoehdon käyttäminen luo (uudelleen) tietovaraston ja määrittää sen automaattisesti. -settings.link.error = Tietovaraston linkin päivittäminen epäonnistui. -alt.repository.multiple_groups = Tämä paketti on saatavilla useissa ryhmissä. -alt.repository.architectures = Arkkitehtuurit -alt.install = Asenna paketti -alt.registry.install = Asenna paketti komennolla: -details = Yksityiskohdat -arch.version.provides = Tarjoaa -rpm.repository = Tietovaraston tiedot -rubygems.required.ruby = Vaatii Ruby-version -settings.delete.notice = Olet aikeissa poistaa %s (%s). Tätä toimenpidettä ei voi perua. Haluatko varmasti jatkaa? -owner.settings.cargo.initialize.error = Cargo-indeksin alustaminen epäonnistui: %v -owner.settings.cargo.rebuild.no_index = Ei voi rakentaa uudelleen, indeksiä ei ole alustettu. -rubygems.required.rubygems = Vaatii RubyGem-version -alt.registry = Määritä tämä rekisteri komentoriviltä: -alt.repository = Tietovaraston tiedot -arch.version.replaces = Korvaa -debian.repository = Tietovaraston tiedot -conda.registry = Määritä tämä rekisteri Conda-tietovarastoksi .condarc-tiedostossa: -container.labels = Nimilaput -settings.link.description = Jos linkität paketin tietovarastoon, paketti listataan tietovaraston pakettilistalla. -assets = Resurssit [secrets] creation.failed = Salaisuuden lisääminen epäonnistui. @@ -3436,105 +3161,27 @@ creation.name_placeholder = kirjoinkoolla ei merkitystä, vain aakkosnumeerisia creation.value_placeholder = Syötä mitä tahansa sisältöä. Tyhjätila alussa ja lopussa jätetään huomiotta. [actions] -runners.name=Nimi -runners.owner_type=Tyyppi -runners.description=Kuvaus -runners.task_list.run=Suorita -runners.task_list.repository=Tietovarasto -runners.task_list.commit=Kommitti - -runs.commit=Kommitti -status.success = Onnistunut -status.unknown = Tuntematon -status.waiting = Odotustilassa -status.running = Käynnissä -status.blocked = Estetty -status.failure = Epäonnistunut -status.cancelled = Peruttu -status.skipped = Ohitettu -runners.none = Testinajajia ei saatavilla -runners.status.unspecified = Tuntematon -runners.update_runner = Päivitä muutokset -runners.edit_runner = Muokkaa testinajajaa -runners.update_runner_success = Testinajaja päivitetty onnistuneesti -runners.delete_runner_success = Testinajaja poistettu onnistuneesti -runners.reset_registration_token = Uudelleenaseta rekisteröintipoletti -runs.scheduled = Ajastettu -runs.status = Tila runs.empty_commit_message = (tyhjä kommittiviesti) -variables.deletion = Poista muuttuja -runners.new_notice = Testinajajan aloitusohjeet workflow.dispatch.input_required = Arvo syötteelle "%s" vaadittu. -runners.status.active = Aktiivinen -variables.description = Muuttujat asetetaan tietyille toiminnoille eikä niitä voida lukea muutoin. -runners.labels = Nimilaput -runners.delete_runner_failed = Testinajajan poisto epäonnistui -runners.delete_runner_header = Varmista testinajajan poisto -runners.task_list.status = Tila -runners.reset_registration_token_success = Testiajajan rekisteröintipoletti asetettu uudelleen onnistuneesti -variables.none = Ei muuttujia vielä. -runners.id = Tunniste -runners.status = Tila -runners.task_list = Ajajan viimeisimmät tehtävät -runners.task_list.no_tasks = Tehtäviä ei ole vielä määritelty. -runners.last_online = Viimeisin käynnissäoloajankohta -runners.runner_title = Testinajaja -runners.task_list.done_at = Valmistunut ajankohtana -runs.no_matching_online_runner_helper = Testiajajaa nimilapulla %s ei löytynyt -runs.no_results = Ei tuloksia. -runners.delete_runner = Poista testinajaja -variables.deletion.description = Muuttujan poistaminen on lopullista, eikä sitä voi perua. Jatketaanko? workflow.dispatch.invalid_input_type = Syötetyyppi "%s" ei kelpaa. workflow.dispatch.warn_input_limit = Näytetään vain ensimmäiset %d syötettä. -runners.runner_manage_panel = Hallinnoi testinajajia -variables = Muuttujat -variables.management = Hallitse muuttujia -variables.creation = Lisää muuttuja -runners.new = Luo uusi testinajaja -runners.version = Versio runs.expire_log_message = Lokitiedostot on tyhjätty vanhenemisen vuoksi. -runners.delete_runner_notice = Jos tehtävä on käynnissä tällä testinajajalla, se lopetetaan ja merkitään epäonnistuneeksi. Se voi rikkoa koonnin työnkulun. -runners.update_runner_failed = Testinajajan päivitys epäonnistui -variables.deletion.success = Muuttuja poistettu. -variables.edit = Muokkaa muuttujaa -variables.creation.success = Muuttuja "%s" lisätty. -variables.deletion.failed = Muuttujan poisto epäonnistui. -variables.creation.failed = Muuttujan lisäys epäonnistui. -variables.update.failed = Muuttujan muokkaus epäonnistui. -variables.update.success = Muuttuja muokattu. variables.id_not_exist = Muuttujaa tunnisteella %d ei ole olemassa. -runs.all_workflows = Kaikki työnkulut workflow.dispatch.run = Suorita työnkulku workflow.enable = Ota työnkulku käyttöön -runs.no_workflows = Ei työnkulkuja vielä. -runs.actors_no_select = Kaikki toimijat -runs.workflow = Työnkulku workflow.enable_success = Työnkulku "%s" on otettu käyttöön. workflow.disabled = Työnkulku on poistettu käytöstä. -runs.actor = Toimija workflow.disable = Poista työnkulku käytöstä workflow.disable_success = Työnkulku "%s" on poistettu käytöstä. -runs.no_job = Työnkulun tulee sisältää vähintään yksi työ -runs.invalid_workflow_helper = Työnkulun asetustiedosto on virheellinen. Tarkista asetustiedosto: %s -runners = Ajajat -actions = Actions unit.desc = Hallitse integroituja CI-/CD-putkia Forgejo Actionsia hyödyntäen. -runs.pushed_by = työntänyt runs.no_workflows.help_no_write_access = Lisätietoja Forgejo Actionsista on saatavilla dokumentaatiosta. -runners.status.idle = Jouten -runners.status.offline = Ei-verkkotilassa -runs.no_job_without_needs = Työnkulun tulee sisältää vähintään yksi työ ilman riippuvuuksia. runs.no_runs = Työnkululla ei ole vielä suorituksia. -variables.not_found = Muuttujaa ei löytynyt. runs.no_workflows.help_write_access = Etkö tiedä, miten aloittaa Forgejo Actionsin käyttö? Lue pikaopas kirjoittaaksesi ensimmäisen työnkulun, sen jälkeen määritä Forgejo-ajaja suorittamaan asettamiasi töitä. - - - - -runs.status_no_select = Kaikki tilat - -workflow.dispatch.trigger_found = Tällä työnkululla on workflow_dispatch-tapahtumaliipaisin. workflow.dispatch.success = Työnkulun suoritusta pyydettiin onnistuneesti. +workflow.dispatch.trigger_found = Tällä työnkululla on workflow_dispatch-tapahtumaliipaisin. + + + [projects] type-1.display_name = Yksittäinen projekti @@ -3589,7 +3236,6 @@ projects.read = Lue: Pääsy tietovaraston projektitauluille. wiki.write = Kirjoita: Luo, päivitä ja poista integroidun wikin sivuja. [markup] -filepreview.truncated = Esikatselu on typistetty [translation_meta] test = Tämä on testimerkkijono. Sitä ei näytetä Forgejo-käyttöliittymässä, mutta sitä käytetään testaustarkoituksiin. Voit vapaasti kirjoittaa "selvä" säästääksesi aikaa (tai käyttää hauskaa faktaa) ja saavuttaaksesi sen makean 100 %:n valmistumisrajan :) \ No newline at end of file diff --git a/options/locale/locale_fil.ini b/options/locale/locale_fil.ini index c2cf9fd951..68a6efddee 100644 --- a/options/locale/locale_fil.ini +++ b/options/locale/locale_fil.ini @@ -43,9 +43,6 @@ sign_up = Magrehistro link_account = Mag-link ng account template = Template tracked_time_summary = Buod ng mga nakasubaybay na oras base sa filter ng listahan ng isyu -webauthn_sign_in = Pindutin ang button ng iyong security key. Kung walang button ang iyong security key, ilagay muli. -webauthn_error_insecure = Sinusuportahan lamang ng WebAuthn ang mga secure na koneksyon. Para sa pagsubok sa HTTP, pwede mo gamitin ang origin na "localhost" o "127.0.0.1" -webauthn_error_timeout = Naabot ang timeout bago mabasa ang iyong key. Mangyaring i-reload ang page na ito at subukan muli. view = Itignan disabled = Naka-disable copy_url = Kopyahin ang URL @@ -64,15 +61,6 @@ re_type = Kumpirmahin ang password captcha = CAPTCHA twofa = Authentikasyong two-factor passcode = Passcode -webauthn_insert_key = Ilagay ang iyong security key -webauthn_press_button = Pindutin ang button ng iyong security key… -webauthn_use_twofa = Gumamit ng two-factor code galing sa iyong telepono -webauthn_error = Hindi mabasa ang iyong security key. -webauthn_unsupported_browser = Kasalukuyang hindi sinusuportahan ng iyong browser ang WebAuthn. -webauthn_error_unknown = May nangyaring hindi alam na error. Magyaring subukan muli. -webauthn_error_unable_to_process = Hindi maproseso ng server ang iyong hiling. -webauthn_error_duplicated = Hindi pinapayagan ang security key na ito para sa hiling na ito. Mangyaring siguraduhin na ang key na ito ay hindi ito nakarehistro na. -webauthn_error_empty = Kailangan mong maglapat ng pangalan para sa key na ito. repository = Repositoryo organization = Organisasyon mirror = Salamin @@ -315,7 +303,7 @@ buttons.bold.tooltip = Magdagdag ng bold na text (Ctrl+B / ⌘B) buttons.italic.tooltip = Magdagdag ng italic text (Ctrl+I / ⌘I) buttons.quote.tooltip = I-quote ang text buttons.code.tooltip = Magdagdag ng code -buttons.link.tooltip = Magdagdag ng link +buttons.link.tooltip = Magdagdag ng link (Ctrl+K / ⌘K) buttons.list.unordered.tooltip = Magdagdag ng bullet na listahan buttons.list.task.tooltip = Magdagdag ng listahan ng mga gawain buttons.mention.tooltip = Magmensyon ng user o koponan @@ -832,7 +820,7 @@ generate_token_success = Nag-generate na ang iyong bagong token. Kopyahin ito ng delete_token = Burahin access_token_deletion = Burahin ang access token access_token_deletion_desc = Ang pagbura ng isang token ay babawiin ang pag-access sa iyong account para sa mga application gamit ito. Ang gawaing ito ay hindi pwedeng baguhin. Magpatuloy? -repo_and_org_access = Access sa Repositoryo at Organisasyon +repo_and_org_access = Access sa repositoryo at organisasyon permissions_public_only = Publiko lamang permissions_access_all = Lahat (publiko, pribado, at limitado) select_permissions = Pumili ng mga pahintulot @@ -1091,7 +1079,6 @@ delete_preexisting = Burahin ang mga dating umiiral na file delete_preexisting_content = Burahin ang mga file sa %s tree_path_not_found_commit = Hindi umiiral ang path na %[1]s sa commit %[2]s tree_path_not_found_branch = Hindi umiiral ang daanang %[1]s sa branch %[2]s -migrate_items_pullrequests = Mga hiling sa paghila archive.title = Naka-archive ang repositoryong ito. Maaari mong itignan ang mga file at i-clone ito, pero hindi ka makakagawa ng anumang pagbabago sa estado ito, tulad ng pagtulak at paggawa ng mga isyu, pull request o mga komento. archive.title_date = Naka-archive ang repositoryo na ito noong %s. Maaari mong itignan ang mga file at i-clone ito, pero hindi ka makakagawa ng anumang pagbabago sa estado nito, tulad ng pagtulak o paggawa ng mga bagong isyu, mga pull request, o komento. pulls = Mga hiling sa paghila @@ -1143,13 +1130,7 @@ form.name_pattern_not_allowed = Hindi pinapayagan ang pattern na "%s" sa pangala migrate_options_lfs = I-migrate ang mga LFS file migrate_options_lfs_endpoint.label = Endpoint ng LFS migrate_options_lfs_endpoint.placeholder = Kung iwanang blangko, ang endpoint ay magmumula sa clone URL -migrate_items_milestones = Mga milestone -migrate_items_labels = Mga label -migrate_items_issues = Mga isyu -migrate_items_merge_requests = Mga merge request migrate.clone_address = Magmigrate / Mag-clone mula sa URL -migrate_items = Mga item sa pagmigrate -migrate_items_releases = Mga paglabas migrate_repo = I-migrate ang repositoryo size_format = %[1]s: %[2]s, %[3]s: %[4]s template.git_hooks_tooltip = Kasalukuyang hindi mo mababago o matatanggal ang mga hook ng Git kapag nadagdag. Piliin lang ito kapag pinagkakatiwalaan mo ang template na repositoryo. @@ -1157,7 +1138,6 @@ template.webhooks = Mga webhook template.topics = Mga paksa template.issue_labels = Mga label ng isyu template.one_item = Kailangang pumili ng kahit isang template item -migrate_items_wiki = Wiki migrate.clone_local_path = o isang lokal na path ng server adopt_preexisting_success = Pinagtibay ang mga file at ginawa ang repositoryo mula sa %s delete_preexisting_success = Burahin ang mga hindi pinatibay na file sa %s @@ -1168,12 +1148,6 @@ migrate.invalid_local_path = Hindi wasto ang lokal na path. Hindi ito umiiral o migrate.invalid_lfs_endpoint = Hindi wasto ang LFS endpoint. migrate.migrating_failed = Nabigo ang pag-migrate mula sa %s. migrate.migrating_failed_no_addr = Nabigo ang pagmigrate. -migrate.migrating_topics = Nililipat ang mga paksa -migrate.migrating_milestones = Nililipat ang mga milestone -migrate.migrating_releases = Nililipat ang mga paglabas -migrate.migrating_pulls = Nililipat ang mga pull request -migrate.cancel_migrating_title = Kanselahin ang pag-migrate -migrate.cancel_migrating_confirm = Gusto mo bang kanselahin ang migration na ito? mirror_from = mirror ng forked_from = na-fork mula sa generated_from = na-generate mula sa @@ -1227,7 +1201,6 @@ from_comment = (komento) editor.add_file = Magdagdag ng file editor.upload_file = Mag-upload ng file editor.cannot_edit_lfs_files = Hindi mababago ang mga LFS file sa web interface. -migrate.migrating_issues = Nililipat ang mga isyu fork_from_self = Hindi ka makaka-fork ng repositoryo na minamay-ari mo. broken_message = Ang Git data na pinagbabatayan sa repositoryo na ito ay hindi mabasa. Makipag-ugnayan sa tagapangasiwa ng instansya na ito o burahin ang repositoryo na ito. file_history = Kasaysayan @@ -1235,7 +1208,6 @@ invisible_runes_header = `Nalalaman ng file na ito ng mga hindi nakikitang Unico file_too_large = Masyadong malaki ang file para ipakita. invisible_runes_description = `Ang file na ito ay naglalaman ng mga hindi nakikitang Unicode character na hindi nakikilala ng mga tao ngunit maaaring maproseso ng ibang paraan ng isang computer. Kung sa tingin mo ay sinasadya ito, maaari mong ligtas na hindi pansinin ang babala na ito. Gamitin ang I-escape na button para ipakita sila.` commit.contained_in_default_branch = Ang commit na ito ay bahagi ng default na branch -migrate.migrating_labels = Nililipat ang mga label filter_branch_and_tag = I-filter ang branch o tag clear_ref = `Burahin ang kasalukuyang sanggunian` actions = Mga Aksyon @@ -1248,12 +1220,6 @@ commits = Mga Commit file_permalink = Permalink migrate.migrating_failed.error = Nabigong magmigrate: %s unwatch = I-unwatch -n_commit_one = %s commit -n_commit_few = %s mga commit -n_branch_one = %s branch -n_branch_few = %s mga branch -n_tag_one = %s tag -n_tag_few = %s mga tag editor.cannot_edit_non_text_files = Hindi mababago ang mga binary file sa web interface. migrated_from = Na-migrate mula sa %[2]s migrated_from_fake = Na-migrate mula sa %[1]s @@ -1272,7 +1238,6 @@ unescape_control_characters = I-unescape invisible_runes_line = `Ang linya na ito ay may mga hindi nakikitang Unicode character` ambiguous_runes_line = `Ang linya na ito ay may mga hindi tiyak na Unicode character` tag = Tag -migrate.migrating_git = Nililipat ang Git data vendored = Naka-vendor editor.delete = Burahin ang %s editor.add = Idagdag ang %s @@ -1612,7 +1577,6 @@ issues.new.clear_assignees = I-clear ang mga mangangasiwa issues.new.no_assignees = Walang mga mangangasiwa issues.choose.invalid_config = Ang config ng isyu ay naglalaman ng mga error: issues.label_templates.info = Walang pang mga umiiral na label. Gumawa ng label gamit ang "Bagong label" o gumamit ng label preset: -n_release_one = %s release issues.action_label = Label issues.action_milestone = Milestone issues.action_milestone_no_select = Walang milestone @@ -1646,7 +1610,6 @@ issues.remove_project_at = `tinanggal ito mula sa %s na proyekto %s` issues.filter_label_select_no_label = Walang label issues.filter_sort.fewestforks = Pinakakaunting fork issues.add_assignee_at = `ay itinalaga ni/ng %s %s` -n_release_few = %s mga release issues.remove_ref_at = `tinanggal ang sangguni na %s %s` issues.add_ref_at = `idinagdag ang sangguni na %s %s` issues.filter_milestone = Milestone @@ -2478,7 +2441,7 @@ settings.chat_id = ID ng chat settings.matrix.message_type = Uri ng mensahe settings.tags.protection.allowed.noone = Walang sinuman settings.archive.header = I-archive ang repo na ito -settings.archive.text = Ang pag-archive ng repo ay gagawin itong buo na read-only. Itatago ito sa dashboard. Walang tao (kahit ikaw rin!) ay makakagawa ng bagong commit, o magbukas ng mga isyu o hiling sa paghila. +settings.archive.text = Gagawing read-only ang buong repositoryo ang pag-archive. Itatago ito sa dashboard. Hindi magagawa ng anumang tao (kahit ikaw rin!) ang paggawa ng mga bagong commit at makapagbukas ng mga isyu at hiling sa paghila. Inirerekomenda ang pagdokumento ng dahilan ng pagka-archive para bigyan ng tagubilin ang mga developer na nagpaplanong i-fork ang repositoryo sa lalong madaling panahon. settings.archive.error = May naganap na error habang sinusubukang i-archive ang repo. Tignan ang log para sa mga detalye. settings.archive.error_ismirror = Hindi ka makaka-archive ng naka-mirror na repo. settings.unarchive.button = I-unarchive ang repo @@ -2710,7 +2673,6 @@ emails.updated = Napalitan na ang email emails.not_updated = Nabigong baguhin ang hinihiling na email address: %v monitor.next = Susunod na oras monitor.last_execution_result = Resulta -dashboard.last_gc_time = Oras noong huling GC users.last_login = Huling nag sign-in first_page = Una last_page = Huli @@ -2729,8 +2691,6 @@ config_settings = Mga setting dashboard.statistic = Buod dashboard.task.error = Error sa Utos: %[1]s: %[3]s users.full_name = Buong pangalan -users.list_status_filter.menu_text = Isaasyos -users.list_status_filter.not_2fa_enabled = Naka-disable ang 2FA users.never_login = Hindi nag-sign in kailanman dashboard.system_status = Status ng sistema dashboard.operation_switch = Palitan @@ -2756,20 +2716,6 @@ dashboard.resync_all_hooks = I-resychronize ang mga Git hook ng lahat ng mga rep dashboard.cleanup_hook_task_table = Linisin ang hook_task table dashboard.cleanup_packages = Linisin ang mga na-expire na package dashboard.cleanup_actions = Linisin ang mga nag-expire na log at artifact mula sa mga aksyon -dashboard.server_uptime = Uptime ng server -dashboard.current_goroutine = Mga kasalukuyang goroutine -dashboard.total_memory_allocated = Kabuuan na na-allocate na memory -dashboard.memory_obtained = Mga nakuhang memory -dashboard.pointer_lookup_times = Oras ng pointer lookup -dashboard.memory_free_times = Mga memory free -dashboard.current_heap_usage = Kasalukuyang paggamit ng heap -dashboard.heap_memory_obtained = Nakuhang heap memory -dashboard.heap_memory_idle = Idle ng heap memory -dashboard.heap_objects = Mga heap object -dashboard.bootstrap_stack_usage = Paggamit ng bootstrap stack -dashboard.stack_memory_obtained = Nakuhang stack memory -dashboard.profiling_bucket_hash_table_obtained = Mga nakuhang profiling bucket hash table -dashboard.gc_metadata_obtained = Nakuhang GC metadata dashboard.delete_old_actions = Burahin ang lahat ng mga lumang aktibidad mula sa database dashboard.stop_endless_tasks = Itigil ang mga hindi natatapos na aksyon ng task dashboard.cancel_abandoned_jobs = Kanselahin ang mga naiwang aksyon ng trabaho @@ -2788,16 +2734,7 @@ users.update_profile = I-update ang user account users.delete_account = Burahin ang user account users.cannot_delete_self = Hindi mo maaaring burahin ang sarili mo users.still_own_repo = Ang user na ito ay nagmamay-ari pa ng isa o higit pang mga repositoryo. Burahin o ilipat sila muna. -users.list_status_filter.is_active = Aktibo -users.list_status_filter.not_active = Hindi aktibo -users.list_status_filter.is_admin = Tagapangasiwa -users.list_status_filter.not_admin = Hindi tagapangasiwa -users.list_status_filter.is_restricted = Pinaghihigpitan -users.list_status_filter.is_prohibit_login = Pinagbawalan ang pag-login -users.list_status_filter.not_prohibit_login = Pinapayagan ang pag-login -users.list_status_filter.is_2fa_enabled = Naka-enable ang 2FA users.details = Mga detalye ng user -dashboard.memory_allocate_times = Mga allocation ng memory users.edit = I-edit users = Mga user account organizations = Mga organisasyon @@ -2830,13 +2767,7 @@ dashboard.reinit_missing_repos = Muling pagsasaayos ng lahat ng nawawalang mga r users.allow_git_hook_tooltip = Ang mga Git hook ay tinatakbo bilang OS user na tinatakbo ang Forgejo at may katulad na level ng pag-access ng host. Bilang isang resulta, ang mga gumagamit na may espesyal na pribilehiyo ng Git hook na ito ay maaaring ma-access at baguhin ang lahat ng mga repositori ng Forgejo pati na rin ang database na ginamit ng Forgejo. Dahil dito nakakamit din nila ang mga pribilehiyo ng Forgejo Administrator. dashboard.delete_repo_archives = Burahin ang lahat ng mga archive ng mga repositoryo (ZIP, TAR.GZ, atbp..) dashboard.sync_external_users = I-synchronize ang panlabas na user data -dashboard.heap_memory_released = Mga na-release na heap memory -dashboard.other_system_allocation_obtained = Ibang allocation ng sistema na nakuha users.allow_git_hook = Makakagawa ng mga Git hook -dashboard.current_memory_usage = Kasalukuyang paggamit ng memory -dashboard.gc_times = Mga oras ng GC -users.list_status_filter.reset = I-reset -users.list_status_filter.not_restricted = Hindi pinaghihigpitan config_summary = Buod dashboard.new_version_hint = Available na ang Forgejo %s, tumatakbo ka ng %s. Suriin ang blog para sa karagdagang detalye. dashboard.operations = Mga operasyon ng pagpapanatili @@ -2848,11 +2779,6 @@ dashboard.sync_repo_tags = I-sync ang mga tag mula sa Git data sa database dashboard.update_mirrors = I-update ang mga mirror dashboard.repo_health_check = Suriin ang kalusugan ng lahat ng mga repositoryo dashboard.check_repo_stats = Suriin ang lahat ng istatistika ng repositoryo -dashboard.mspan_structures_usage = Paggamit ng MSpan structure -dashboard.mspan_structures_obtained = Mga nakuhang MSpan structure -dashboard.mcache_structures_usage = Paggamit ng MCache structure -dashboard.next_gc_recycle = Susunod na GC recycle -dashboard.mcache_structures_obtained = Mga nakuhang MCache structure dashboard.delete_old_actions.started = Nasimula na ang burahin ang lahat ng mga lumang aktibidad mula sa database. dashboard.update_checker = Tagasuri ng update dashboard.delete_old_system_notices = Burahin ang lahat ng mga lumang paunawa ng sistema mula sa database @@ -2868,11 +2794,8 @@ users.is_activated = Naka-activate na account users.prohibit_login = Sinuspending account emails.email_manage_panel = Ipamahala ang mga email ng user self_check = Pansariling pagsusuri -dashboard.total_gc_pause = Kabuuang GC pause -dashboard.last_gc_pause = Huling GC pause users.still_has_org = Ang user na ito ay isang miyembro ng isang organisasyon. Tanggalin ang user sa anumang mga organisasyon muna. users.deletion_success = Binura na ang user account. -dashboard.heap_memory_in_use = Ginagamit na heap memory emails.filter_sort.name = Username emails.primary = Pauna emails.filter_sort.email = Email @@ -3042,8 +2965,6 @@ auths.allowed_domains = Mga pinapayagang domain auths.helo_hostname_helper = Hostname na pinapadala sa pamamagitan ng HELO. Iwanang walang laman para ipadala ang kasalukuyang hostname. auths.disable_helo = I-disable ang HELO auths.oauth2_profileURL = URL ng profile -monitor.queue.settings.maxnumberworkers.placeholder = Kasalukuyang %[1]d -monitor.queue.settings.remove_all_items = Tanggalin lahat users.block.description = Harangan ang tagagamit na ito mula sa pag-interact sa serbisyong ito sa pamamagitan ng kanilang mga account at pagbawalan ang pag-sign in. monitor.cron = Mga cron task config.mailer_sendmail_timeout = Timeout ng Sendmail @@ -3062,13 +2983,9 @@ config.provider_config = Config ng provider config.cache_test_slow = Matagumpay ang pagsubok ng cache, ngunit mabagal ang tugon: %s. config.picture_config = Configuration ng larawan at avatar config.disabled_logger = Naka-disable -monitor.queue.settings.submit = I-update ang mga setting auths.tips = Mga tip config.test_mail_sent = Ang isang test email ay ipinadala kay "%s". monitor.process.cancel = Kanselahin ang proseso -monitor.queues = Mga queue -monitor.queue.exemplar = Uri ng Exemplar -monitor.queue.numberworkers = Numero ng mga worker config.enable_openid_signup = I-enable ang OpenID na pansariling pagrehistro config.session_config = Configuration ng session monitor.name = Pangalan @@ -3079,9 +2996,6 @@ config.git_gc_args = Mga argument ng GC config.git_migrate_timeout = Timeout ng paglipat config.cache_config = Configuration ng cache config.git_pull_timeout = Timeout ng operasyon ng paghila -monitor.queue.settings.maxnumberworkers = Pinakamaraming numero ng worker -monitor.queue.settings.title = Mga setting ng pool -monitor.queue.settings.desc = Dinamikang lumalaki ang mga pool tugon sa kanilang worker queue blocking. auths.tip.google_plus = Kumuha ng OAuth2 client credentials mula sa Google API console sa %s config.mail_notify = Paganahin ang mga email notification config.active_code_lives = Oras ng pag-expire ng activation code @@ -3091,7 +3005,6 @@ users.organization_creation.description = Payagan ang paggawa ng mga bagong orga auths.delete_auth_title = Burahin ang source ng authentikasyon auths.delete_auth_desc = Ang pagtanggal ng authentication source ay pumipigil sa mga user na gamitin ito upang mag-sign in. Magpatuloy? auths.login_source_of_type_exist = Umiiral na ang authentication source na may ganitong uri. -monitor.queue.settings.maxnumberworkers.error = Dapat numero ang pinakamaraming numero ng mga worker self_check.no_problem_found = Wala pang mga problema na nahanap. self_check.database_collation_mismatch = Inaasahan ang database na gamitin ang collation: %s auths.oauth2_admin_group = Group claim value para sa mga tagapangasiwa. (Opsyonal - kinakailangan ang claim name sa itaas) @@ -3150,11 +3063,6 @@ monitor.desc = Paglalarawan monitor.start = Oras ng pagsimula monitor.execute_time = Oras ng pag-execute monitor.process.children = Mga anak -monitor.queue = Queue: %s -monitor.queue.type = Uri -monitor.queue.maxnumberworkers = Pinakamaraming numero ng worker -monitor.queue.numberinqueue = Number sa queue -monitor.queue.review_add = Suriin / magdagdag ng mga worker self_check.database_collation_case_insensitive = Gumagamit ang database ng collation %s, na isang insensitive na collation. Bagama't maaaring gumana ang Forgejo dito, maaaring may ilang bihirang kaso na hindi gumagana gaya ng inaasahan. auths.tips.oauth2.general.tip = Kapag nagrerehistro ng bagong OAuth2 na authentication, ang callback/redirect URL ay dapat na: auths.activated = Na-activate na ang source ng authentikasyon @@ -3168,14 +3076,11 @@ config.default_enable_timetracking = I-enable ang pagsubaybay ng oras bilang def auths.deletion_success = Binura na ang authentication source. auths.login_source_exist = Umiiral na ang authentication source na "%s". auths.still_in_used = Ginagamit pa ang authentication source. I-convert o burahin ang mga user na gumagamit ng authentication source na ito muna. -monitor.queue.settings.remove_all_items_done = Tinanggal na ang lahat ng mga item sa queue. -monitor.queue.settings.changed = Na-update ang mga setting auths.oauth2_map_group_to_team_removal = Tanggalin ang user sa mga naka-synchronize na team kung ang user ay hindi kasama sa katumbas na groupo. config.cache_test_succeeded = Matagumpay ang pagsubok ng cache, nakakuha ng tugon sa %s. config.open_with_editor_app_help = Ang mga "Open with" editor para sa clone menu. Kung iniwang walang laman, ang default ang gagamitin. I-expand para makita ang default. config.set_setting_failed = Nabigo ang pagtakda ng setting na %s monitor.download_diagnosis_report = I-download ang ulat ng diagnosis -monitor.queue.activeworkers = Mga aktibong worker self_check.database_inconsistent_collation_columns = Gumagamit ang database ng collation %s, ngunit ang mga column na ito ay gumagamit ng hindi tugmang collations. Maaaring magdulot ito ng ilang hindi inaasahang problema. auths.tip.twitter = Pumunta sa %s, gumawa ng application at siguraduhing naka-enable ang "Payagan ang application na gamitin para Mag-sign in sa Twitter" na opsyon auths.tip.gitea = Magrehistro ng bagong OAuth2 na application. Ang guide ay mahahanap sa %s @@ -3200,7 +3105,6 @@ config.gc_interval_time = Oras ng pagitan ng GC config.cookie_life_time = Lifetime ng cookie config.git_clone_timeout = Timeout ng operasyon na pag-clone monitor.process.cancel_desc = Ang pagkansela ng proseso ay maaaring magdulot ng pagkawalan ng data -monitor.queue.name = Pangalan auths.oauth2_required_claim_value_helper = Itakda ang value na ito upang i-restrict ang pag-login mula sa pinagmulang ito sa mga user na may claim na may ganitong pangalan at value auths.tip.bitbucket = Magrehistro ng bagong OAuth consumer sa %s at idagdag ang pahintulot na "Account" - "Read" config.require_sign_in_view = Kailanganin ang pag-sign in para itignan ang nilalaman @@ -3335,280 +3239,33 @@ settings.change_orgname_redirect_prompt.with_cooldown.one = Magiging available a [packages] -alpine.repository.branches = Mga branch -owner.settings.cargo.initialize.success = Matagumpay na nagawa ang Cargo index. -details = Mga detalye -empty = Wala pang anumang mga package. -filter.container.tagged = Naka-tag -filter.container.untagged = Hindi naka-tag -filter.type = Uri -filter.type.all = Lahat -filter.no_result = Walang resulta ang iyong filter. -about = Tungkol sa package na ito -installation = Pag-install -details.repository_site = Website ng repositoryo -details.documentation_site = Website ng dokumentasyon -alpine.repository.repositories = Mga Repositoryo -alpine.repository.architectures = Mga architechture -chef.install = Para i-install ang package na ito, patakbuhin ang sumusunod na command: -composer.registry = I-setup ang registry na ito sa iyong ~/.composee/config.json file: -composer.install = Para i-install ang package gamit ang Composer, patakbuhin ang sumusunod na command: -empty.repo = Nag-upload ka ba ng package, ngunit hindi pinapakita dito? Pumunta sa mga setting ng package at i-link iyan sa repo na ito. -keywords = Mga keyword -versions = Mga bersyon -title = Mga package desc = Ipamahala ang mga package ng repositoryo. -registry.documentation = Para sa higit pang impormasyon tungkol sa %s registry, tignan ang dokumentasyon. -published_by = Na-publish ang %[1]s ni/ng %[3]s -requirements = Mga kinakailangan -dependencies = Mga dependency -details.author = May-akda -details.project_site = Website ng proyekto -details.license = Lisensya -versions.view_all = Tignan lahat -dependency.id = ID -dependency.version = Bersyon -alpine.registry = I-setup ang registry na ito sa pamamagitan ng pagdagdag ng url sa iyong /etc/apk/repositories file: -alpine.registry.info = Pumili ng $branch at $repository mula sa listahan sa ibaba. -alpine.install = Para i-install ang package, patakbuhin ang sumusunod na command: -alpine.repository = Info ng repositoryo -cargo.registry = I-setup ang registry na ito sa Cargo configuration file (halimbawa ~/.cargo/config.toml): -chef.registry = I-setup ang registry na ito sa iyong ~/.chef/config.rb file: -composer.dependencies = Mga dependency -composer.dependencies.development = Mga dependency ng pag-develop conan.details.repository = Repositoryo -conan.registry = I-setup ang registry na ito mula sa command line: -assets = Mga asset -empty.documentation = Para sa higit pang impormasyon sa package registry, tignan ang dokumentasyon. -cargo.install = Para i-install ang package gamit ang Cargo, patakbuhin ang sumusunod na command: -published_by_in = Na-publish ang %[1]s ni %[3]s sa %[5]s -alpine.registry.key = I-download ang registry public RSA key sa /etc/apk/keys folder para i-verify ang index signature: -swift.install2 = at patakbuhin ang sumusunod na utos: -arch.version.description = Paglalarawan -arch.version.properties = Mga katangian ng bersyon -settings.delete.description = Permanente ang pagbura ng package at hindi ito mababawi. -owner.settings.cargo.initialize.description = Ang isang espesyal na index Git repository ay kinakailangan para gamitin ang Cargo registry. Ang paggamit ng opsyon na ito ay (muling) gagawin ang repositoryo at awtomatikong i-configure. -owner.settings.cargo.rebuild.description = Maaaring maging kapaki-pakinabang ang muling pagtatayo kung ang index ay hindi naka-synchronize sa mga nakaimbak na Cargo package. -helm.install = Para i-install ang package na ito, patakbuhin ang sumusunod na command: -helm.registry = I-setup ang registry na ito mula sa command line: -owner.settings.cargo.rebuild.no_index = Hindi makaka-rebuild, walang index na naka-initialize. -pypi.install = Para i-install ang package gamit ang pip, patakbuhin ang sumusunod na command: owner.settings.cleanuprules.keep.count.n = %d mga bersyon kada package -owner.settings.cleanuprules.success.update = Na-update na cleanup rule. -arch.version.backup = Backup -owner.settings.cleanuprules.keep.pattern = Panatilihin ang mga bersyon na tumutugma -conda.install = Para i-install ang package gamit ang Conda, patakbuhin ang sumusunod na command: -container.multi_arch = OS / Arch -debian.repository.components = Mga component -conda.registry = I-set up ang registry na ito bilang Conda repository sa iyong .condarc file: -npm.registry = I-set up ang registry na ito sa .npmrc file ng iyong proyekto: -owner.settings.cargo.rebuild = I-rebuild ang index -cran.registry = I-set up ang registry na ito sa iyong Rprofile.site na file: -arch.version.depends = Dumedepende -arch.version.provides = Nagbibigay -arch.version.groups = Grupo -arch.version.optdepends = Opsyonal na dumepende -pub.install = Para i-install ang package gamit ang Dart, patakbuhin ang sumusunod na command: -pypi.requires = Kinakailangan ang Python -rubygems.dependencies.development = Mga development dependency owner.settings.cleanuprules.keep.count.1 = 1 bersyon kada package -owner.settings.cleanuprules.remove.pattern = Tanggalin ang mga bersyon na tumutugma sa -nuget.install = Para i-install ang package gamit ang NuGet, patakbuhin ang sumusunod na command: -container.labels = Mga Label -rpm.repository.multiple_groups = Available ang package na ito sa iba't ibang grupo. -settings.delete.error = Nabigong burahin ang package. -owner.settings.cargo.title = Index ng Cargo registry -debian.repository = Info ng repositoryo -owner.settings.cleanuprules.remove.title = Ang mga bersyon na tumutugma sa mga rule na ito ay tatanggalin, maliban mung may rule sa itaas ay sasabihin na panatilihin sila. -owner.settings.cleanuprules.remove.days = Tanggalin ang mga bersyon mas luma sa -container.details.platform = Platform -container.digest = Digest -container.details.type = Uri ng image -rubygems.required.ruby = Kinakailangan ang bersyon ng Ruby -npm.dependencies.bundle = Mga kasamang dependency -owner.settings.cleanuprules.success.delete = Nabura na ang cleanup rule. -owner.settings.chef.keypair.description = Ang mga hiling na pinapadala sa Chef registry ay dapat na cryptographically na nakalagda bilang paraan ng authentication. Kapag nag-ge-generate ng isang keypair, ang pampublikong key lamang ang iniimbak ng Forgejo. Ang pribadong key ay binibigay sa iyo na gagamitin sa knife. Ang pag-generate ng bagong keypair i io-overwrite ang luma. -npm.dependencies = Mga dependency -npm.dependencies.peer = Mga peer dependency -rubygems.install = Para i-install ang package sa pamamagitan ng gem, patakbuhin ang sumusunod na command: -settings.link.description = Kung mag-link ka ng package sa repository, ang package ay nakalista sa listahan ng mga package ng repositoryo. -settings.delete.success = Nabura na ang package. -arch.pacman.repo.multi.item = Configuration para sa %s -arch.pacman.conf = Idagdag ang server na may kaugnay na distribution at architechture sa /etc/pacman.conf: -arch.pacman.sync = I-sync ang package sa pamamagitan ng pacman: -arch.version.makedepends = Mga make depend -arch.version.checkdepends = Mga check depend -container.pull = Hilahin ang image mula sa command line: -container.labels.key = Key -cran.install = Para i-install ang package na ito, patakbuhin ang sumusunod na command: -debian.repository.distributions = Mga distribution -generic.download = I-download ang package mula sa command line: -go.install = I-install ang package mula sa command line: -maven.install2 = Patakbuhin sa pamamagitan ng command line: -maven.download = Para i-download ang dependency, patakbuhin sa pamamagitan ng command line: -nuget.registry = I-setup ang registry na ito mula sa command line: -nuget.dependency.framework = Target na Framework -npm.install = Para i-install ang package gamit ng npm, patakbuhin ang sumusunod na command: -npm.install2 = o idagdag ito sa package.json file: -npm.dependencies.development = Mga development dependency -npm.details.tag = Tag -swift.install = Idagdag ang package sa iyong Package.swift na file: -vagrant.install = Para magdagdag ng Vagrant box, patakbuhin ang sumusunod na command: -settings.link = I-link ang package na ito sa repository -settings.link.select = Pumili ng repositoryo -settings.link.button = I-update ang link ng repositoryo -settings.link.error = Nabigong i-update ang link ng repositoryo. -settings.delete = Burahin ang package -owner.settings.cargo.initialize = I-initialize ang index -owner.settings.cleanuprules.add = Magdagdag ng cleanup rule -owner.settings.cleanuprules.preview = Preview ng cleanup rule -owner.settings.cleanuprules.preview.overview = %d mga package ay naka-iskedyul para tanggalin. -owner.settings.cleanuprules.preview.none = Hindi tumutugma sa anumang mga package ang cleanup rule. owner.settings.cleanuprules.enabled = Naka-enable -owner.settings.cleanuprules.pattern_full_match = I-apply ang pattern sa punong pangalan ng package -owner.settings.chef.keypair = Gumawa ng key pair -arch.pacman.helper.gpg = Magdagdag ng trust certificate para sa pacman: -arch.pacman.repo.multi = Ang %s ay may katulad na beryson sa iba't ibang mga distribution. -arch.version.conflicts = Mga salungatan -arch.version.replaces = Pinapalit -npm.dependencies.optional = Mga opsyonal na dependency -rpm.distros.suse = sa mga SUSE based na distribution -rpm.install = Para i-install ang package na ito, patakbuhin ang sumusunod na command: -rpm.repository = Info ng repositoryo -rpm.repository.architectures = Mga architechture -rpm.registry = I-setup ang registry na ito mula sa command line: -rpm.distros.redhat = sa mga RedHat based na distribution -rubygems.dependencies.runtime = Mga runtime dependency -rubygems.install2 = o idagdag ito sa Gemfile: -debian.repository.architectures = Mga architechture -owner.settings.chef.title = Chef registry -rubygems.required.rubygems = Kinakailangan ang bersyon ng RubyGem -owner.settings.cleanuprules.edit = I-edit ang cleanup rule -settings.link.success = Matagumpay na na-update ang link ng repositoryo. -owner.settings.cleanuprules.keep.pattern.container = Ang latest na bersyon ay palaging pinapatilihin para sa mga Container package. -owner.settings.cleanuprules.none = Wala pang mga cleanup rule sa ngayon. -owner.settings.cleanuprules.keep.count = Panatilihin ang pinaka-recent -settings.delete.notice = Buburahin mo ang %s (%s). Hindi mababalikan ang aksyon na ito, sigurado ka ba? -owner.settings.cargo.initialize.error = Nabigong i-initialize ang Cargo index: %v -debian.install = Para i-install ang package na ito, patakbuhin ang sumusunod na command: -owner.settings.cargo.rebuild.error = Nabigong i-rebuild ang cargo index: %v -conan.install = Para i-install ang package gamit ang Conan, patakbuhin ang sumusunod na command: -swift.registry = I-setup ang registry na ito mula sa command line: -maven.registry = I-set up ang registry na ito sa iyong pom.xml ng iyong proyekto: -owner.settings.cleanuprules.keep.title = Ang mga bersyon na tumutugma sa mga rule na ito ay papanatilihin, kahit na tumutugma sila sa removal rule sa ibaba. -maven.install = Para gamitin ang package isama ang sumusunod sa dependencies block sa pom.xml file: -container.labels.value = Value -debian.registry = I-setup ang registry na ito mula sa command line: -debian.registry.info = Pumili ng $distribution at $component sa listahan sa ibaba. -owner.settings.cargo.rebuild.success = Matagumpay na na-rebuild ang Cargo index. -owner.settings.cleanuprules.title = Mga cleanup rule -container.layers = Mga layer ng image -container.images.title = Mga image -search_in_external_registry = Maghanap sa %s -alt.registry = I-setup ang registry na ito mula sa command line: -alt.registry.install = Para i-install ang package na ito, patakbuhin ang sumusunod na command: -alt.install = I-install ang package -alt.setup = Idagdag ang repositoryo sa listahan ng mga nakakonektang repositoryo (piliin ang kinakailangang architechture sa halip ng "_arch_"): -alt.repository = Info ng repositoryo -alt.repository.architectures = Mga architechture -alt.repository.multiple_groups = Available ang package na ito sa iba't ibang grupo. [actions] -runners.last_online = Huling oras na online -status.waiting = Hinihintay -runners.task_list.run = Patakbuhin -runners.description = Paglalarawan -runners.owner_type = Uri -runners.name = Pangalan -status.success = Tagumpay -runs.pushed_by = itinulak ni/ng -runners.status = Katayuan -status.failure = Nabigo -actions = Mga Aksyon -runs.no_job = Ang workflow ay dapat maglaman ng hindi bababa sa isang trabaho -runners = Mga Runner -runs.commit = Commit workflow.dispatch.trigger_found = Mayroong workflow_dispatch na trigger ang workflow na ito. unit.desc = Ipamahala ang mga pinag-sasamang CI/CD pipeline sa pamamagitan ng Forgejo Actions. -runners.edit_runner = Baguhin ang Runner -runners.update_runner = I-update ang mga pagbabago -variables.update.failed = Nabigong baguhin ang variable. -variables.update.success = Nabago na ang variable. -runs.no_results = Walang mga tumugmang resulta. -runners.delete_runner_success = Matagumpay na nabura ang runner -runs.all_workflows = Lahat ng mga workflow -runs.scheduled = Naka-iskedyul -runs.workflow = Workflow -variables.edit = Baguhin ang Variable workflow.enable = I-enable ang workflow workflow.disabled = Naka-disable ang workflow. need_approval_desc = Kailangan ng pag-apruba para tumakbo ng mga workflow para sa fork na hiling sa paghila. -variables = Mga variable -runners.status.active = Aktibo -runners.version = Bersyon -status.unknown = Hindi alam -runs.invalid_workflow_helper = Hindi wasto ang workflow config file. Pakisuri ang iyong config file: %s -runs.actors_no_select = Lahat ng mga actor -runners.runner_title = Runner -runners.task_list = Mga kamakailang trabaho sa runner na ito -runners.task_list.no_tasks = Wala pang mga trabaho sa ngayon. -runners.labels = Mga label -runs.no_matching_online_runner_helper = Walang tumutugmang online runner na may label: %s -runs.status = Status -runs.no_workflows = Wala pang mga workflow sa ngayon. runs.no_runs = Wala pang mga pagtatakbo ang workflow na ito sa ngayon. -variables.creation = Magdagdag ng variable -variables.none = Wala pang mga variable sa ngayon. -variables.deletion = Tanggalin ang variable -variables.deletion.description = Permanente ang pagtanggal ng isang variable at hindi ito mababalik. Magpatuloy? -status.running = Tumatakbo -runners.new_notice = Paano magsimula ng runner -runners.update_runner_success = Matagumpay na na-update ang runner -runners.delete_runner_notice = Kapag may trabaho na tumatakbo sa runner na ito, titigilan ito at mamarkahan bilang nabigo. Maaaring sirain ang building workflow. -runners.none = Walang mga available na runner -runs.status_no_select = Lahat ng status runs.empty_commit_message = (walang laman na mensahe ng commit) workflow.enable_success = Matagumpay na na-enable ang workflow na "%s". workflow.dispatch.run = Patakbuhin ang workflow workflow.dispatch.success = Matagumpay na nahiling ang pagtakbo ng workflow. -variables.management = Ipamahala ang mga variable -variables.deletion.failed = Nabigong tanggalin ang variable. -runners.status.unspecified = Hindi alam -runs.no_job_without_needs = Ang workflow ay dapat maglaman ng hindi bababa sa isang trabaho na walang dependencies. workflow.disable = I-disable ang workflow workflow.disable_success = Matagumpay na na-disable ang workflow na "%s". -runners.task_list.repository = Repositoryo -status.skipped = Nilaktawan -runners.runner_manage_panel = Ipamahala ang mga runner -runners.new = Gumawa ng bagong runner -variables.creation.failed = Nabigong idagdag ang variable. -runners.id = ID -runs.actor = Actor -runners.update_runner_failed = Nabigong i-update ang runner -runners.delete_runner = Burahin ang runner na ito -runners.delete_runner_failed = Nabigong burahin ang runner -runners.delete_runner_header = Kumpirmahin na burahin ang runner -status.blocked = Naharang -status.cancelled = Kinansela -runners.task_list.status = Status -runners.status.idle = Idle workflow.dispatch.use_from = Gamitin ang workflow mula sa -runners.reset_registration_token = I-reset ang token ng pagrehistro -runners.status.offline = Offline workflow.dispatch.invalid_input_type = Hindi wastong input type "%s". -runners.task_list.commit = Commit -runners.task_list.done_at = Natapos sa -runners.reset_registration_token_success = Matagumpay na na-reset ang token ng pagrehistro ng runner workflow.dispatch.input_required = Kumailangan ng value para sa input na "%s". workflow.dispatch.warn_input_limit = Pinapakita lamang ang unang %d na mga input. -variables.description = Ipapasa ang mga variable sa ilang mga aksyon at hindi mababasa kung hindi man. variables.id_not_exist = Hindi umiiral ang variable na may ID na %d. -variables.deletion.success = Tinanggal na ang variable. -variables.creation.success = Nadagdag na ang variable na "%s". runs.expire_log_message = Na-purge ang mga log dahil masyado silang luma. runs.no_workflows.help_write_access = Hindi alam kung paano magsimula sa Forgejo Actions? Tignan ang mabilisang pagsimula sa user documentation para magsimulang magsulat ng unang workflow, at mag-setup ng Forgejo runner para patakbuhin ang mga job. runs.no_workflows.help_no_write_access = Para matuto tungkol sa Forgejo Actions, tignan ang dokumentasyon. -variables.not_found = Nabigong hanapin ang variable. [action] commit_repo = itinulak sa %[3]s sa %[4]s @@ -3664,38 +3321,10 @@ raw_seconds = segundo raw_minutes = minuto [munits.data] -mib = MiB -gib = GiB -b = B -kib = KiB -tib = TiB -pib = PiB -eib = EiB [gpg] -error.not_signed_commit = Hindi isang naka-sign na commit -error.probable_bad_signature = BABALA! Bagaman na may key na may ID na ito sa database hindi nito pinapatunayan ang commit na ito! Ang commit na ito ay KAHINA-HINALA. -error.extract_sign = Nabigong i-extract ang signature -error.no_committer_account = Walang account na naka-link sa email address ng committer -error.no_gpg_keys_found = Walang kilalang key na nahanap para sa signature na ito sa database -default_key = Naka-sign gamit ang default key -error.generate_hash = Nabigong i-generate ang hash ng commit -error.failed_retrieval_gpg_keys = Nabigong kumuha ng anumang key na naka-attach sa account ng committer -error.probable_bad_default_signature = BABALA! Bagaman na ang default key ay may ID na ito hindi nito pinapatunayan ang commit na ito! Ang commit na ito ay KAHINA-HINALA. [notification] -unread = Hindi nabasa -read = Nabasa -no_unread = Walang mga hindi nabasang notification. -notifications = Mga abiso -no_read = Walang mga nabasang notification. -pin = I-pin ang notification -mark_as_read = Markahan bilang nabasa -mark_as_unread = Markahan bilang hindi nabasa -subscriptions = Mga subscription -watching = Pinapanood -no_subscriptions = Walang mga subscription -mark_all_as_read = Markahan lahat bilang nabasa [units] error.no_unit_allowed_repo = Hindi ka pinapayagang ma-access ang anumang seksyon ng repositoryong ito. @@ -3703,10 +3332,6 @@ unit = Yunit error.unit_not_allowed = Hindi ka pinapayagang ma-access ang seksyon ng repositoryong ito. [dropzone] -default_message = I-drop ang mga file o mag-click dito para mag-upload. -invalid_input_type = Hindi ka maaaring mag-upload ng mga file sa uri na ito. -file_too_big = Ang laki ng file ({{filesize}}) MB) ay lumalagpas sa pinakamataas na size na ({{maxFilesize}} MB). -remove_file = Tanggalin ang file [secrets] creation.success = Naidagdag na ang lihim na "%s". @@ -3724,9 +3349,6 @@ management = Ipamahala ang mga secret deletion.description = Ang pagtanggal ng sikreto ay permanente at hindi mababawi. Magpatuloy? [markup] -filepreview.line = Linya %[1]d sa %[2]s -filepreview.truncated = Na-truncate ang preview -filepreview.lines = Mga linya %[1]d hanggang %[2]d sa %[3]s [projects] deleted.display_name = Binurang proyekto diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index 72a4a14dd1..2558b99f74 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -37,18 +37,6 @@ twofa=Authentification à deux facteurs twofa_scratch=Code de secours pour l'authentification à deux facteurs passcode=Code d'accès -webauthn_insert_key=Insérez votre clé de sécurité -webauthn_sign_in=Appuyez sur le bouton de votre clé de sécurité. Si votre clé de sécurité n'a pas de bouton, réinsérez-la. -webauthn_press_button=Veuillez appuyer sur le bouton de votre clé de sécurité… -webauthn_use_twofa=Utilisez l'authentification à deux facteurs avec votre téléphone -webauthn_error=Impossible de lire votre clé de sécurité. -webauthn_unsupported_browser=Votre navigateur ne prend actuellement pas en charge WebAuthn. -webauthn_error_unknown=Une erreur indéterminée s'est produite. Veuillez réessayer. -webauthn_error_insecure=`WebAuthn ne prend en charge que les connexions sécurisées. Pour les tests via HTTP, vous pouvez utiliser l'origine "localhost" ou "127.0.0.1"` -webauthn_error_unable_to_process=Le serveur n'a pas pu traiter votre demande. -webauthn_error_duplicated=La clé de sécurité n'est pas autorisée pour cette demande. Veuillez vous assurer que la clé n'est pas déjà enregistrée. -webauthn_error_empty=Vous devez définir un nom pour cette clé. -webauthn_error_timeout=Le délai d'attente imparti a été atteint avant que votre clé ne puisse être lue. Veuillez recharger la page pour réessayer. repository=Dépôt organization=Organisation mirror=Miroir @@ -181,7 +169,7 @@ buttons.bold.tooltip=Ajouter du texte en gras (Ctrl+B / ⌘B) buttons.italic.tooltip=Ajouter du texte en italique (Ctrl+I / ⌘I) buttons.quote.tooltip=Citer le texte buttons.code.tooltip=Ajouter du code -buttons.link.tooltip=Ajouter un lien +buttons.link.tooltip=Ajouter un lien (Ctrl+K / ⌘K) buttons.list.unordered.tooltip=Ajouter une liste à puces buttons.list.ordered.tooltip=Ajouter une liste numérotée buttons.list.task.tooltip=Ajouter une liste de tâches @@ -873,7 +861,7 @@ delete_token=Supprimer access_token_deletion=Supprimer le jeton d'accès access_token_deletion_desc=Supprimer un jeton révoquera l'accès à votre compte pour toutes les applications l'utilisant. Cette action est irréversible. Continuer ? delete_token_success=Ce jeton a été supprimé. Les applications l'utilisant n'ont plus accès à votre compte. -repo_and_org_access=Accès aux Organisations et Dépôts +repo_and_org_access=Accès aux dépôts et organisations permissions_public_only=Publique uniquement permissions_access_all=Tout (public, privé et limité) select_permissions=Sélectionner les autorisations @@ -1153,14 +1141,6 @@ 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 -migrate_items_labels=Labels -migrate_items_issues=Tickets -migrate_items_pullrequests=Demandes d'ajout -migrate_items_merge_requests=Demandes de fusion -migrate_items_releases=Publications migrate_repo=Migrer le dépôt migrate.clone_address=Migrer/Cloner depuis une URL migrate.clone_address_desc=L'URL HTTP(S) ou Git "clone" d'un dépôt existant @@ -1179,16 +1159,6 @@ 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.migrating_git=Migration des données Git -migrate.migrating_topics=Migration des sujets -migrate.migrating_milestones=Migration des jalons -migrate.migrating_labels=Migration des labels -migrate.migrating_releases=Migration des publications -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 generated_from=généré depuis @@ -1566,7 +1536,7 @@ issues.ref_reopening_from=`a référencé ce ticket dans une pul issues.ref_from=`de %[1]s` issues.author=Auteur issues.role.owner=Propriétaire -issues.role.owner_helper=Cet utilisateur est le propriétaire de ce dépôt. +issues.role.owner_helper=Cet utilisateur est propriétaire de ce dépôt. issues.role.member=Membre issues.role.member_helper=Cet utilisateur est membre de l’organisation propriétaire de ce dépôt. issues.role.collaborator=Collaborateur @@ -2639,16 +2609,10 @@ file_follow = Suivre le lien symbolique settings.confirmation_string = Chaine de confirmation pulls.agit_explanation = Créé par le workflow AGit. AGit permet aux contributeurs de proposer des modifications en utilisant "git push" sans créer une bifurcation ou une nouvelle branche. stars = Étoiles -n_tag_few = %s étiquettes editor.commit_id_not_matching = Le fichier a été modifié pendant que vous l'éditiez. Appliquez les modifications à une nouvelle branche puis procédez à la fusion. commits.search_branch = Cette branche open_with_editor = Ouvrir avec %s pulls.ready_for_review = Prêt à être évalué ? -n_commit_one = %s commit -n_commit_few = %s commits -n_branch_one = %s branch -n_branch_few = %s branches -n_tag_one = %s étiquettes editor.push_out_of_date = Le push semble obsolète. issues.num_participants_one = %d participant issues.archived_label_description = (Archivé) %s @@ -2679,8 +2643,6 @@ settings.federation_settings = Paramètres de féderation project = Projets subscribe.issue.guest.tooltip = Authentifiez vous pour vous abonner à ce ticket. subscribe.pull.guest.tooltip = Authentifiez vous pour suivre cette demande d'ajout. -n_release_one = %s publication -n_release_few = %s publications issues.author.tooltip.pr = Cet utilisateur est l'auteur de cette pull request. issues.author.tooltip.issue = Cet utilisateur est l'auteur de ce ticket. issues.edit.already_changed = Impossible de sauvegarder les changements du ticket car son contenu a déjà été modifié par un autre utilisateur. Veuillez recharger la page et essayer de l'éditer à nouveau pour éviter d'écraser ses changements @@ -2962,34 +2924,6 @@ dashboard.sync_external_users=Synchroniser les données de l’utilisateur exter dashboard.cleanup_hook_task_table=Nettoyer la table hook_task dashboard.cleanup_packages=Nettoyer des paquets expirés dashboard.cleanup_actions=Nettoyer les journaux et les artefacts des actions obsolètes -dashboard.server_uptime=Uptime du serveur -dashboard.current_goroutine=Goroutines actuelles -dashboard.current_memory_usage=Utilisation Mémoire actuelle -dashboard.total_memory_allocated=Mémoire totale allouée -dashboard.memory_obtained=Mémoire obtenue -dashboard.pointer_lookup_times=Nombre de Consultations Pointeur -dashboard.memory_allocate_times=Allocations de mémoire -dashboard.memory_free_times=Nombre de libérations de mémoire -dashboard.current_heap_usage=Utilisation Tas (Heap) -dashboard.heap_memory_obtained=Mémoire tas (Heap) obtenue -dashboard.heap_memory_idle=Mémoire tas (Heap) au repos -dashboard.heap_memory_in_use=Utilisation mémoire tas (Heap) -dashboard.heap_memory_released=Mémoire tas (Heap) libérée -dashboard.heap_objects=Objets tas (Heap) -dashboard.bootstrap_stack_usage=Utilisation pile bootstrap -dashboard.stack_memory_obtained=Mémoire pile obtenue -dashboard.mspan_structures_usage=Utilisation des structures MSpan -dashboard.mspan_structures_obtained=Structures MSpan obtenues -dashboard.mcache_structures_usage=Utilisation des structures MCache -dashboard.mcache_structures_obtained=Structures MCache obtenues -dashboard.profiling_bucket_hash_table_obtained=Profilage de seau de table de hashage obtenu -dashboard.gc_metadata_obtained=Métadonnées GC obtenues -dashboard.other_system_allocation_obtained=Autres allocation système obtenue -dashboard.next_gc_recycle=Prochain recyclage GC -dashboard.last_gc_time=Temps depuis le dernier GC -dashboard.total_gc_pause=Pause totale GC -dashboard.last_gc_pause=Dernière pause GC -dashboard.gc_times=Nombres de GC dashboard.delete_old_actions=Supprimer toutes les anciennes activités de la base de données dashboard.delete_old_actions.started=Suppression de toutes les anciennes activités de la base de données démarrée. dashboard.update_checker=Vérificateur de mise à jour @@ -3047,18 +2981,6 @@ users.purge_help=Éradique l’utilisateur et tous ses dépôts, organisations e users.still_own_packages=Cet utilisateur possède encore un ou plusieurs paquets. Supprimez d’abord ces paquets. users.deletion_success=Le compte a été supprimé. users.reset_2fa=Réinitialiser l'authentification à deux facteurs -users.list_status_filter.menu_text=Filtrer -users.list_status_filter.reset=Réinitialiser -users.list_status_filter.is_active=Actif -users.list_status_filter.not_active=Inactif -users.list_status_filter.is_admin=Administrateur -users.list_status_filter.not_admin=Non administrateur -users.list_status_filter.is_restricted=Restreint -users.list_status_filter.not_restricted=Non restreint -users.list_status_filter.is_prohibit_login=Interdit de connexion -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é users.details=Informations de l’utilisateur emails.email_manage_panel=Gestion des courriels des utilisateurs @@ -3378,26 +3300,6 @@ monitor.process.cancel_desc=L'annulation d'un processus peut entraîner une pert monitor.process.cancel_notices=Annuler : %s ? monitor.process.children=Enfant -monitor.queues=Files d'attente -monitor.queue=File d'attente : %s -monitor.queue.name=Nom -monitor.queue.type=Type -monitor.queue.exemplar=Type d'exemple -monitor.queue.numberworkers=Nombre de processus -monitor.queue.activeworkers=Processus actifs -monitor.queue.maxnumberworkers=Nombre maximal de processus -monitor.queue.numberinqueue=Position dans la queue -monitor.queue.review_add=Examiner / ajouter des processus -monitor.queue.settings.title=Paramètres du réservoir -monitor.queue.settings.desc=Les bassins croissent proportionnellement au besoin de leurs exécuteurs. -monitor.queue.settings.maxnumberworkers=Nombre maximale de processus -monitor.queue.settings.maxnumberworkers.placeholder=Actuellement %[1]d -monitor.queue.settings.maxnumberworkers.error=Le nombre de processus doit être un nombre -monitor.queue.settings.submit=Appliquer les paramètres -monitor.queue.settings.changed=Paramètres mis à jour -monitor.queue.settings.remove_all_items=Tout effacer -monitor.queue.settings.remove_all_items_done=Tous les éléments de la file d'attente ont été effacés. - notices.system_notice_list=Notifications systèmes notices.view_detail_header=Voir les détails de la notification notices.operations=Opérations @@ -3494,35 +3396,10 @@ raw_seconds=secondes raw_minutes=minutes [dropzone] -default_message=Déposez les fichiers ou cliquez ici pour téléverser. -invalid_input_type=Vous ne pouvez pas téléverser des fichiers de ce type. -file_too_big=La taille du fichier ({{filesize}} Mo) dépasse la taille maximale ({{maxFilesize}} Mo). -remove_file=Supprimer le fichier [notification] -notifications=Notifications -unread=Non lue(s) -read=Lue(s) -no_unread=Aucune notification non lue. -no_read=Aucune notification lue. -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 -subscriptions=Abonnements -watching=Suivi -no_subscriptions=Pas d'abonnements [gpg] -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é n'a été trouvée pour cette signature dans la base de données -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 ce commit ! Ce commit est SUSPECT. -error.probable_bad_default_signature=AVERTISSEMENT ! Bien que la clé par défaut ait cet ID, elle ne vérifie pas ce commit ! Ce commit est SUSPECT. [units] unit=Unité @@ -3530,183 +3407,11 @@ error.no_unit_allowed_repo=Vous n'êtes pas autorisé à accéder à n'importe q error.unit_not_allowed=Vous n'êtes pas autorisé à accéder à cette section du dépôt. [packages] -title=Paquets 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. -filter.container.tagged=Balisé -filter.container.untagged=Débalisé -published_by=%[1]s publié par %[3]s -published_by_in=%[1]s publié par %[3]s en %[5]s -installation=Installation -about=À propos de ce paquet -requirements=Exigences -dependencies=Dépendances -keywords=Mots-clés -details=Détails -details.author=Auteur -details.project_site=Site du projet -details.repository_site=Site du dépôt -details.documentation_site=Site de la documentation -details.license=Licence -assets=Ressources -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 : -chef.registry=Configurer ce registre dans votre fichier ~/.chef/config.rb : -chef.install=Pour installer le paquet, exécutez la commande suivante : -composer.registry=Configurez ce registre dans votre fichier ~/.composer/config.json : -composer.install=Pour installer le paquet en utilisant Composer, exécutez la commande suivante : -composer.dependencies=Dépendances -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 : -container.details.type=Type d'image -container.details.platform=Plateforme -container.pull=Tirez l'image depuis un terminal : -container.digest=Empreinte -container.multi_arch=SE / Arch -container.layers=Calques d'image -container.labels=Labels -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 : -maven.install=Pour utiliser le paquet, inclure ce qui suit dans le bloc dependencies dans le fichier pom.xml : -maven.install2=Exécuter dans un terminal : -maven.download=Pour télécharger la dépendance, exécutez dans un terminal : -nuget.registry=Configurer ce registre à partir d'un terminal : -nuget.install=Pour installer le paquet en utilisant NuGet, exécutez la commande suivante : -nuget.dependency.framework=Cadriciel cible -npm.registry=Configurez ce registre dans le fichier .npmrc de votre projet : -npm.install=Pour installer le paquet en utilisant npm, exécutez la commande suivante : -npm.install2=ou ajoutez-le au fichier package.json : -npm.dependencies=Dépendances -npm.dependencies.development=Dépendances de développement -npm.dependencies.peer=Dépendances de pairs -npm.dependencies.optional=Dépendances optionnelles -npm.details.tag=Balise -pub.install=Pour installer le paquet en utilisant Dart, exécutez la commande suivante : -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 : -rpm.repository = Information sur le dépôt -rpm.repository.architectures = Architectures -rpm.repository.multiple_groups = Ce paquet est disponible dans plusieurs groupes. -rubygems.install=Pour installer le paquet en utilisant gem, exécutez la commande suivante : -rubygems.install2=ou ajoutez-le au Gemfile : -rubygems.dependencies.runtime=Dépendances d'exécution -rubygems.dependencies.development=Dépendances de développement -rubygems.required.ruby=Nécessite Ruby en version -rubygems.required.rubygems=Nécessite RubyGem en version -swift.registry=Configurez ce registre à partir d'un terminal : -swift.install=Ajoutez le paquet dans votre fichier Package.swift : -swift.install2=et exécutez la commande suivante : -vagrant.install=Pour ajouter une machine Vagrant, exécutez la commande suivante : -settings.link=Lier ce paquet à un dépôt -settings.link.description=Si vous liez un paquet à dépôt, le paquet sera inclus dans sa liste des paquets. -settings.link.select=Sélectionner un dépôt -settings.link.button=Actualiser le lien du dépôt -settings.link.success=Le lien du dépôt a été mis à jour avec succès. -settings.link.error=Impossible de mettre à jour le lien du dépôt. -settings.delete=Supprimer le paquet -settings.delete.description=Supprimer un paquet est permanent et irréversible. -settings.delete.notice=Vous êtes sur le point de supprimer %s (%s). Cette opération est irréversible, êtes-vous sûr ? -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=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. owner.settings.cleanuprules.enabled=Activé -owner.settings.cleanuprules.pattern_full_match=Appliquer le motif au nom complet du paquet -owner.settings.cleanuprules.keep.title=Les versions qui correspondent à ces règles sont conservées, même si elles correspondent à une règle de suppression ci-dessous. -owner.settings.cleanuprules.keep.count=Garder le plus récent owner.settings.cleanuprules.keep.count.1=1 version par paquet owner.settings.cleanuprules.keep.count.n=%d versions par paquet -owner.settings.cleanuprules.keep.pattern=Garder les versions correspondantes -owner.settings.cleanuprules.keep.pattern.container=La version latest est toujours conservée pour les paquets Container. -owner.settings.cleanuprules.remove.title=Les versions qui correspondent à ces règles sont supprimées, sauf si une règle ci-dessus dit de les garder. -owner.settings.cleanuprules.remove.days=Supprimer les versions antérieures à -owner.settings.cleanuprules.remove.pattern=Supprimer les versions correspondantes -owner.settings.cleanuprules.success.update=La règle de nettoyage a été mise à jour. -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=Les requêtes envoyées au registre Chef doivent être signées cryptographiquement à des fin d'authentification. Lorsqu'une paire de clés est générée, seule la clé publique est conservée dans Forgejo. La clé privée est fournie afin que vous puissiez l'utiliser avec knife. La génération d'une nouvelle clé remplace la précédente. -owner.settings.cargo.rebuild.no_index = Incapable de reconstruire, index non initialisé. -npm.dependencies.bundle = Bundles de dépendances -arch.pacman.helper.gpg = Ajouter un certificat de confiance pour pacman : -arch.pacman.repo.multi = %s a la même version dans différentes distributions. -arch.pacman.repo.multi.item = Configuration pour %s -arch.pacman.conf = Ajouter un serveur associées à la distribution et l'architecture dans /etc/pacman.conf : -arch.pacman.sync = Synchroniser le paquet avec pacman : -arch.version.properties = Propriétés de version -arch.version.description = Description -arch.version.provides = Fournit -arch.version.groups = Groupe -arch.version.depends = Dépend -arch.version.optdepends = Dépendances optionnelles -arch.version.checkdepends = Vérifier les dépendances -arch.version.conflicts = Conflits -arch.version.replaces = Remplace -arch.version.backup = Sauvegarde -arch.version.makedepends = Faire des dépendances -container.images.title = Images -search_in_external_registry = Rechercher dans %s -alt.repository = Informations sur le dépôt -alt.repository.architectures =Architectures -alt.registry = Configurez ce registre à partir d'un terminal : -alt.registry.install = Pour installer le paquet, exécutez la commande suivante : -alt.install = Installer le paquet -alt.repository.multiple_groups = Ce paquet est disponible dans plusieurs groupes. -alt.setup = Ajouter un dépôt à la liste des dépôts connecté (choisissez l'architecture nécessaire à la place de "_arch") : [secrets] secrets=Secrets @@ -3724,68 +3429,8 @@ deletion.failed=Impossible de supprimer le secret. management=Gestion des secrets [actions] -actions=Actions - unit.desc=Gérer l'intégration continue avec Forgejo Actions. -status.unknown=Inconnu -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é - -runners=Exécuteurs -runners.runner_manage_panel=Gestion des exécuteurs -runners.new=Créer un nouvel exécuteur -runners.new_notice=Comment démarrer un exécuteur -runners.status=Statut -runners.id=ID -runners.name=Nom -runners.owner_type=Type -runners.description=Description -runners.labels=Labels -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=Révision -runners.task_list.done_at=Fait à -runners.edit_runner=Éditer l'Exécuteur -runners.update_runner=Appliquer les modifications -runners.update_runner_success=Exécuteur mis à jour avec succès -runners.update_runner_failed=Impossible d'actualiser l'Exécuteur -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=Confirmer la suppression de 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 -runners.status.active=Actif -runners.status.offline=Hors-ligne -runners.version=Version -runners.reset_registration_token=Réinitialiser le jeton d'enregistrement -runners.reset_registration_token_success=Le jeton d’inscription de l’exécuteur a été réinitialisé avec succès - -runs.all_workflows=Tous les workflows -runs.commit=Révision -runs.scheduled=Planifié -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_online_runner_helper=Aucun exécuteur en ligne correspondant au libellé %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_workflows=Il n'y a pas encore de workflows. runs.no_runs=Le flux de travail n'a pas encore d'exécution. runs.empty_commit_message=(message de révision vide) @@ -3797,25 +3442,8 @@ workflow.disabled=Le flux de travail est désactivé. 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. -runs.workflow = Workflow -runs.no_job_without_needs = Le workflow doit contenir au moins une tâche sans dépendances. workflow.dispatch.use_from = Utiliser un workflow depuis -runs.no_job = Le workflow doit au moins contenir une tâche workflow.dispatch.trigger_found = Ce workflow a un déclencheur d'événement workflow_dispatch. workflow.dispatch.run = Exécuter le workflow workflow.dispatch.success = L'exécution du workflow a bien été demandée. @@ -3825,7 +3453,6 @@ workflow.dispatch.warn_input_limit = Affichage des %d premiers champs seulement. runs.expire_log_message = Les journaux ont été purgés car ils étaient trop anciens. runs.no_workflows.help_write_access = Vous ne savez pas par où commencer avec Forgejo Actions ? Regardez la section démarrage rapide dans la documentation utilisateur pour écrire votre premier workflow, puis mettre en place un Forgejo runner pour exécuter vos jobs. runs.no_workflows.help_no_write_access = Pour en savoir plus sur Forgejo Actions, consultez la documentation. -variables.not_found = La variable n'a pas été trouvée. [projects] type-1.display_name=Projet personnel @@ -3872,18 +3499,8 @@ regexp = RegExp [munits.data] -b = o -mib = Mio -kib = Kio -gib = Gio -tib = Tio -pib = Pio -eib = Eio [markup] -filepreview.line = Ligne %[1]d dans %[2]s -filepreview.lines = Lignes %[1]d jusqu'à %[2]d dans %[3]s -filepreview.truncated = L'aperçu a été tronqué [repo.permissions] pulls.write = Écrire : Fermer des demandes de tirage et gérer les métadonnées telles que les étiquettes, les jalons, les assignés, les dates d'échéance et les dépendances. diff --git a/options/locale/locale_fur.ini b/options/locale/locale_fur.ini new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/options/locale/locale_fur.ini @@ -0,0 +1 @@ + diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index f7d795e372..76ebcd1b91 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -35,18 +35,6 @@ captcha = CAPTCHA twofa = Fíordheimhniú Dhá-Fhachtóir twofa_scratch = Cód Scratch Dhá-Fhachtóra passcode = Paschód -webauthn_insert_key = Cuir isteach d'eochair slándála -webauthn_sign_in = Brúigh an cnaipe ar d'eochair slándála. Mura bhfuil aon chnaipe ag d'eochair slándála, cuir isteach é arís. -webauthn_press_button = Brúigh an cnaipe ar d'eochair slándála le do thoil… -webauthn_use_twofa = Úsáid cód dhá fhachtóir ó do ghuthán -webauthn_error = Ní fhéadfaí do eochair slándála a léamh. -webauthn_unsupported_browser = Ní thacaíonn do bhrabhsálaí le WebAuthn faoi láthair. -webauthn_error_unknown = Tharla earráid anaithnid. Déan iarracht arís. -webauthn_error_insecure = Ní thacaíonn WebAuthn ach le naisc slán. Le haghaidh tástála thar HTTP, is féidir leat an bunús “localhost” nó "127.0.0.1" a úsáid -webauthn_error_unable_to_process = Ní fhéadfadh an freastalaí d'iarratas a phróiseáil. -webauthn_error_duplicated = Ní cheadaítear an eochair slándála don iarratas seo. Déan cinnte le do thoil nach bhfuil an eochair cláraithe cheana féin. -webauthn_error_empty = Ní mór duit ainm a shocrú don eochair seo. -webauthn_error_timeout = Sroicheadh an teorainn ama sula bhféadfaí d’eochair a léamh. Athlódáil an leathanach seo, le do thoil, agus déan iarracht arís. repository = Stór organization = Eagraíocht mirror = Scáthán @@ -96,7 +84,7 @@ copy_error = Theip ar an gcóipeáil copy_type_unsupported = Ní féidir an cineál comhaid seo a chóipeáil write = Scríobh preview = Réamhamharc -loading = Á lódáil... +loading = Á lódáil… error = Earráid error404 = Níl an leathanach atá tú ag iarraidh a bhaint amach ann, nó baineadh éníl údarú agat é a fheiceáil. go_back = Ar ais @@ -124,7 +112,6 @@ filter.is_archived = Cartlannaithe filter.not_archived = Gan Cartlannaithe filter.public = Poiblí filter.private = Príobháideach - return_to_forgejo = Fill ar ais go Forgejo toggle_menu = Roghchlár scoránaigh new_repo.title = Stórlann nua @@ -165,7 +152,6 @@ no_results = Níl aon torthaí meaitseála le fáil. issue_kind = Saincheisteanna cuardaigh… pull_kind = Cuardaigh iarratais tarraingthe… keyword_search_unavailable = Níl cuardach de réir eochairfhocal ar fáil faoi láthair. Déan teagmháil le riarthóir an láithreáin. - union = Aontas union_tooltip = Cuir torthaí san áireamh a mheaitseálann aon cheann de na heochairfhocail scartha le spás bán regexp = RegExp @@ -175,14 +161,12 @@ regexp_tooltip = Léirmhínigh an téarma cuardaigh mar ghnáthléiriú navbar = Barra Nascleanúint footer = Buntásc footer.links = Naisc - footer.software = Maidir leis an mbogearra seo [heatmap] number_of_contributions_in_the_last_12_months = %s ranníocaíochtaí le 12 mhí anuas less = Níos lú more = Níos mó - contributions_zero = Gan aon ranníocaíochtaí contributions_format = {contributions} ar {month} {day}, {year} contributions_one = ranníocaíocht @@ -194,7 +178,7 @@ buttons.bold.tooltip = Cuir téacs trom leis (Ctrl+B / ⌘B) buttons.italic.tooltip = Cuir téacs iodálach leis (Ctrl+I / ⌘I) buttons.quote.tooltip = Téacs luaigh buttons.code.tooltip = Cuir cód leis -buttons.link.tooltip = Cuir nasc leis +buttons.link.tooltip = Cuir nasc leis (Ctrl+K / ⌘K) buttons.list.unordered.tooltip = Cuir liosta piléar leis buttons.list.ordered.tooltip = Cuir liosta uimhrithe buttons.list.task.tooltip = Cuir liosta tascanna leis @@ -203,7 +187,6 @@ buttons.ref.tooltip = Déan tagairt d'eisiúint nó iarratas tarraingthe buttons.switch_to_legacy.tooltip = Úsáid an eagarthóir oidhreachta ina ionad buttons.enable_monospace_font = Cumasaigh cló monospace buttons.disable_monospace_font = Díchumasaigh cló monospace - buttons.indent.tooltip = Míreanna neadaithe leibhéal amháin buttons.unindent.tooltip = Díneadaigh míreanna leibhéal amháin buttons.new_table.tooltip = Cuir tábla leis @@ -225,7 +208,6 @@ string.desc = Z - A occurred = Tharla earráid not_found = Ní raibh an sprioc in ann a fháil. network_error = Earráid líonra - report_message = Má chreideann tú gur fabht Forgejo atá ann, déan cuardach ar shaincheisteanna ar Codeberg nó oscail saincheist nua más gá. server_internal = Earráid inmheánach freastalaí @@ -236,7 +218,6 @@ install_desc = Níl ort ach Forgejo! Bí linn trí cur leis chun an tionscadal seo a dhéanamh níos fearr fós. Ná bíodh drogall ort a bheith i do rannpháirtí! @@ -268,7 +249,7 @@ repo_path = Cosán Fréimhe an Stór repo_path_helper = Sábhálfar stórais iargúlta Git chuig an eolaire seo. lfs_path = Cosán Fréamh Git LFS lfs_path_helper = Stórálfar comhaid a rianóidh Git LFS san eolaire seo. Fág folamh le díchumasú. -domain = Fearann ​​Freastalaí +domain = Fearann freastalaí domain_helper = Seoladh fearainn nó óstach don fhreastalaí. ssh_port = Port Freastalaí SSH app_url_helper = Seoladh bonn le haghaidh URLanna clóin HTTP(S) agus fógraí ríomhphoist. @@ -312,12 +293,49 @@ invalid_log_root_path = Tá an cosán logála neamhbhailí:%v no_reply_address = Fearann Ríomhphoist Folaite password_algorithm = Algartam Hais Pasfhocal invalid_password_algorithm = Algartam hais pasfhocail neamhbhailí -password_algorithm_helper = Socraigh an algartam hashing pasfhocal. Tá riachtanais agus neart éagsúla ag halgartaim. Tá an algartam argon2 sách slán ach úsáideann sé go leor cuimhne agus d'fhéadfadh sé a bheith míchuí do chórais bheaga. +password_algorithm_helper = Socraigh an algartam haiseála pasfhocail. Bíonn riachtanais agus láidreachtaí difriúla ag halgartaim. Tá an algartam argon2 sách slán ach úsáideann sé go leor cuimhne agus d'fhéadfadh sé a bheith mí-oiriúnach do chórais bheaga. enable_update_checker = Cumasaigh Seiceoir Nuashonraithe env_config_keys = Cumraíocht Comhshaoil env_config_keys_prompt = Cuirfear na hathróga comhshaoil seo a leanas i bhfeidhm ar do chomhad cumraíochta freisin: - docker_helper = Má ritheann tú Forgejo taobh istigh de Docker, léigh an doiciméadú le do thoil sula n-athraíonn tú aon socruithe. +require_db_desc = Éilíonn Forgejo MySQL, PostgreSQL, SQLite3 nó TiDB (prótacal MySQL). +sqlite_helper = Cosán comhaid don bhunachar sonraí SQLite3.
Cuir isteach cosán absalóideach má ritheann tú Forgejo mar sheirbhís. +reinstall_error = Tá tú ag iarraidh suiteáil i mbunachar sonraí Forgejo atá ann cheana féin +reinstall_confirm_message = Is féidir go leor fadhbanna a chruthú má athshuiteáiltear é le bunachar sonraí Forgejo atá ann cheana féin. I bhformhór na gcásanna, ba chóir duit d'"app.ini" atá ann cheana a úsáid chun Forgejo a rith. Má tá a fhios agat cad atá á dhéanamh agat, deimhnigh an méid seo a leanas: +reinstall_confirm_check_3 = Deimhníonn tú go bhfuil tú cinnte go hiomlán go bhfuil an Forgejo seo ag rith leis an suíomh app.ini ceart agus go bhfuil tú cinnte go gcaithfidh tú athshuiteáil a dhéanamh. Deimhníonn tú go n-aithníonn tú na rioscaí thuas. +app_name = Teideal an sampla +app_name_helper = Cuir isteach ainm d’eiseamláire anseo. Taispeánfar é ar gach leathanach. +app_slogan = Slogan samplach +app_slogan_helper = Cuir isteach mana do shampla anseo. Fág folamh chun é a dhíchumasú. +run_user = Úsáideoir le rith mar +run_user_helper = Ainm úsáideora an chórais oibriúcháin a ritheann Forgejo mar úsáid. Tabhair faoi deara go gcaithfidh rochtain a bheith ag an úsáideoir seo ar chonair fréimhe an stórais. +ssh_port_helper = Uimhir an phoirt a úsáidfidh an freastalaí SSH. Fág folamh chun an freastalaí SSH a dhíchumasú. +http_port = Port éisteachta HTTP +http_port_helper = Uimhir an phoirt a úsáidfidh freastalaí gréasáin Forgejo. +app_url = Bun-URL +smtp_from_helper = Seoladh ríomhphoist a úsáidfidh Forgejo. Cuir isteach seoladh ríomhphoist simplí nó bain úsáid as an bhformáid "Ainm" . +offline_mode.description = Díchumasaigh líonraí seachadta ábhair tríú páirtí agus freastalaigh na hacmhainní go léir go háitiúil. +disable_gravatar.description = Díchumasaigh úsáid Gravatar nó foinsí avatar tríú páirtí eile. Úsáidfear íomhánna réamhshocraithe d’avatars úsáideoirí mura n-uaslódálann siad a n-avatar féin chuig an gcás. +federated_avatar_lookup.description = Cuardaigh avatars ag baint úsáide as Libravatar. +disable_registration.description = Ní bheidh ach riarthóirí samplaí in ann cuntais úsáideora nua a chruthú. Moltar go mór clárú a choinneáil díchumasaithe mura bhfuil sé ar intinn agat sampla poiblí a óstáil do gach duine agus má tá tú réidh le déileáil le líon mór cuntas turscair. +allow_only_external_registration = Ceadaigh clárú trí sheirbhísí seachtracha amháin +allow_only_external_registration.description = Ní bheidh úsáideoirí in ann cuntais nua a chruthú ach trí sheirbhísí seachtracha cumraithe a úsáid. +openid_signin.description = Ceadaigh d’úsáideoirí síniú isteach trí OpenID. +openid_signup.description = Ceadaigh d’úsáideoirí cuntais a chruthú trí OpenID má tá féinchlárú cumasaithe. +enable_captcha.description = Éiligh ar úsáideoirí CAPTCHA a úsáid chun cuntais a chruthú. +require_sign_in_view = Éilítear síniú isteach chun ábhar an sampla a fheiceáil +require_sign_in_view.description = Teorainn a chur le rochtain ar ábhar d'úsáideoirí atá sínithe isteach. Ní bheidh aíonna in ann cuairt a thabhairt ach ar na leathanaigh fíordheimhnithe. +default_keep_email_private.description = Cumasaigh seoladh ríomhphoist a cheilt d’úsáideoirí nua de réir réamhshocraithe ionas nach sceithfear an fhaisnéis seo díreach tar éis clárúcháin. +default_allow_create_organization.description = Ceadaigh d’úsáideoirí nua eagraíochtaí a chruthú de réir réamhshocraithe. Nuair a bhíonn an rogha seo díchumasaithe, beidh ar riarthóir cead a thabhairt d’úsáideoirí nua eagraíochtaí a chruthú. +default_enable_timetracking.description = Ceadaigh úsáid ghné rianaithe ama do stórtha nua de réir réamhshocraithe. +admin_setting.description = Is rogha é cuntas riarthóra a chruthú. Beidh an chéad úsáideoir cláraithe ina riarthóir go huathoibríoch. +config_location_hint = Sábhálfar na roghanna cumraíochta seo i: +install_btn_confirm = Suiteáil Forgejo +test_git_failed = Níorbh fhéidir an t-ordú "git" a thástáil: %v +sqlite3_not_available = Ní thacaíonn an leagan seo de Forgejo le SQLite3. Íoslódáil an leagan dénártha oifigiúil ó %s (ní an leagan "gobuild"). +run_user_not_match = Ní hé an t-ainm úsáideora "úsáideoir le rith mar" an t-ainm úsáideora reatha: %s -> %s +enable_update_checker_helper_forgejo = Déanfaidh sé seiceáil tréimhsiúil le haghaidh leaganacha nua de Forgejo trí thaifead DNS TXT a sheiceáil ag release.forgejo.org. +no_reply_address_helper = Ainm fearainn d'úsáideoirí a bhfuil seoladh ríomhphoist i bhfolach acu. Mar shampla, logálfar an t-ainm úsáideora "joe" i Git mar "joe@noreply.example.org" má shocraítear an fearann ríomhphoist i bhfolach go "noreply.example.org". [home] uname_holder = Ainm Úsáideora nó Seoladh Ríomhphoist @@ -336,6 +354,7 @@ show_both_private_public = Ag taispeáint poiblí agus príobháideach araon show_only_private = Ag taispeáint príobháideach amháin show_only_public = Ag taispeáint poiblí amháin issues.in_your_repos = I do stórais +my_orgs = Eagraíochtaí [explore] repos = Stórais @@ -350,7 +369,7 @@ relevant_repositories = Níl ach stórtha ábhartha á dtaispeáint, Sínigh isteach anois! +hint_register = An bhfuil cuntas uait? Cláraigh anois. +sign_up_button = Cláraigh anois. +confirmation_mail_sent_prompt = Tá ríomhphost deimhnithe nua seolta chuig %s. Chun an próiseas clárúcháin a chríochnú, seiceáil do bhosca isteach agus lean an nasc a cuireadh ar fáil laistigh den chéad %s eile. Mura bhfuil an ríomhphost ceart, is féidir leat logáil isteach agus ríomhphost deimhnithe eile a iarraidh chuig seoladh difriúil. +reset_password_mail_sent_prompt = Tá ríomhphost deimhnithe seolta chuig %s. Chun an próiseas aisghabhála cuntais a chríochnú, seiceáil do bhosca isteach agus lean an nasc a chuirtear ar fáil laistigh den chéad %s eile. +prohibit_login = Tá an cuntas ar fionraí +prohibit_login_desc = Tá do chuntas curtha ar fionraí ó idirghníomhú leis an gcás. Téigh i dteagmháil le riarthóir an chás chun rochtain a fháil ar ais. +change_unconfirmed_email_summary = Athraigh an seoladh ríomhphoist a seoltar ríomhphost gníomhachtaithe chuige. +change_unconfirmed_email = Má thug tú an seoladh ríomhphoist mícheart le linn clárúcháin, is féidir leat é a athrú thíos, agus seolfar dearbhú chuig an seoladh nua ina ionad. +change_unconfirmed_email_error = Ní féidir an seoladh ríomhphoist a athrú: %v +send_reset_mail = Seol ríomhphost aisghabhála +non_local_account = Ní féidir le húsáideoirí nach áitiúla iad a bpasfhocal a nuashonrú tríd an gcomhéadan gréasáin Forgejo. +unauthorized_credentials = Tá na dintiúir mícheart nó imithe in éag. Déan iarracht d’ordú a athdhéanamh nó féach %s le haghaidh tuilleadh eolais +use_onetime_code = Úsáid cód aonuaire +oauth_signin_tab = Nasc le cuntas atá ann cheana féin +authorize_application_description = Má dheonaíonn tú rochtain, beidh sé in ann rochtain a fháil ar fhaisnéis do chuntais go léir agus scríobh chuici, lena n-áirítear stórtha príobháideacha agus eagraíochtaí. +sign_in_openid = Lean ar aghaidh le OpenID [mail] view_it_on = Féach air ar %s @@ -447,6 +483,32 @@ team_invite.subject = Tá cuireadh tugtha agat ag %[1]s chun dul le heagraíocht team_invite.text_1 = Tá cuireadh tugtha ag %[1]s duit chun dul le foireann %[2]s in eagraíocht %[3]s. team_invite.text_2 = Cliceáil ar an nasc seo a leanas le do thoil chun dul isteach san fhoireann: team_invite.text_3 = Nóta: Bhí an cuireadh seo beartaithe do %[1]s. Mura raibh tú ag súil leis an gcuireadh seo, is féidir leat neamhaird a dhéanamh den ríomhphost seo. +link_not_working_do_paste = Nach bhfuil an nasc ag obair? Bain triail as é a chóipeáil agus a ghreamú i mbarra URL do bhrabhsálaí. +admin.new_user.subject = Úsáideoir nua %s díreach cláraithe +admin.new_user.user_info = Faisnéis úsáideora +admin.new_user.text = Le do thoil, cliceáil anseo chun an t-úsáideoir seo a bhainistiú ón bpainéal riaracháin. +register_notify.text_2 = Is féidir leat síniú isteach i do chuntas ag baint úsáide as d'ainm úsáideora: %s +register_notify.text_3 = Más duine eile a chruthaigh an cuntas seo duit, beidh ort do phasfhocal a shocrú ar dtús. +reset_password.text = Más tusa a bhí ann, cliceáil an nasc seo a leanas le do chuntas a aisghabháil laistigh de %s: +password_change.subject = Tá d’fhocal faire athraithe +password_change.text_1 = Athraíodh pasfhocal do chuntais díreach anois. +primary_mail_change.subject = Tá do phríomhphost athraithe +primary_mail_change.text_1 = Athraíodh príomhsheoladh ríomhphoist do chuntais go %[1]s díreach anois. Ciallaíonn sé seo nach bhfaighidh an seoladh ríomhphoist seo fógraí ríomhphoist a thuilleadh chun do chuntas. +totp_disabled.subject = Tá TOTP díchumasaithe +totp_disabled.text_1 = Díchumasaíodh pasfhocal aonuaire bunaithe ar am (TOTP) ar do chuntas díreach anois. +totp_disabled.no_2fa = Níl aon mhodhanna 2FA eile cumraithe a thuilleadh, rud a chiallaíonn nach gá logáil isteach i do chuntas le 2FA a thuilleadh. +removed_security_key.subject = Baineadh eochair slándála +removed_security_key.text_1 = Baineadh an eochair slándála "%[1]s" as do chuntas díreach anois. +removed_security_key.no_2fa = Níl aon mhodhanna 2FA eile cumraithe a thuilleadh, rud a chiallaíonn nach gá logáil isteach i do chuntas le 2FA a thuilleadh. +account_security_caution.text_1 = Más tusa a bhí ann, is féidir leat neamhaird a dhéanamh den ríomhphost seo go sábháilte. +account_security_caution.text_2 = Mura tusa a bhí ann, tá do chuntas i mbaol. Téigh i dteagmháil le riarthóirí an tsuímh seo, le do thoil. +totp_enrolled.subject = Tá TOTP gníomhachtaithe agat mar mhodh 2FA +totp_enrolled.text_1.no_webauthn = Tá TOTP cumasaithe agat chun do chuntas díreach anois. Ciallaíonn sé seo go gcaithfidh tú TOTP a úsáid mar mhodh 2FA i ngach logáil isteach chuig do chuntas amach anseo. +totp_enrolled.text_1.has_webauthn = Tá TOTP cumasaithe agat chun do chuntas díreach anois. Ciallaíonn sé seo, i gcás gach logáil isteach chuig do chuntas amach anseo, gur féidir leat TOTP a úsáid mar mhodh 2FA nó aon cheann de do chuid eochracha slándála a úsáid. +repo.transfer.subject_to = Tá %s ag iarraidh stórlann "%s" a aistriú go %s +repo.transfer.subject_to_you = Tá %s ag iarraidh stórlann "%s" a aistriú chugat +repo.collaborator.added.subject = Chuir %s le %s thú mar chomhoibrí +repo.collaborator.added.text = Cuireadh leis an stór thú mar chomhoibrí: [modal] yes = Tá @@ -475,7 +537,7 @@ require_error = ` ní féidir a bheith folamh.` git_ref_name_error = ` caithfidh gur ainm tagartha Git dea-chruthaithe é.` size_error = ` ní mór méid %s.` min_size_error = ` ní mór go mbeadh carachtar %s ar a laghad ann.` -max_size_error = caithfidh %s carachtar ar a mhéad a bheith ann. +max_size_error = `ní mór %s carachtar ar a mhéad a bheith ann.` email_error = `ní seoladh ríomhphoist bailí é.` url_error = `ní URL bailí é "%s".` include_error = ` ní mór fotheaghrán a bheith ann "%s".` @@ -526,6 +588,28 @@ must_use_public_key = Is eochair phríobháideach an eochair a sholáthraíonn t auth_failed = Theip ar fhíordheimhniú:%v target_branch_not_exist = Níl spriocbhrainse ann. admin_cannot_delete_self = Ní féidir leat tú féin a scriosadh nuair is riarachán tú. Bain do phribhléidí riaracháin ar dtús. +FullName = Ainm iomlán +Description = Cur síos +Pronouns = Forainmneacha +Biography = Beathaisnéis +Website = Suíomh Gréasáin +Location = Suíomh +To = Ainm na brainse +AccessToken = Comhartha rochtana +alpha_dash_error = `Níor cheart ach carachtair alfa-uimhriúla, fleascáin ("-") agus fo-líne ("_") a bheith sa.` +alpha_dash_dot_error = `Níor cheart ach carachtair alfa-uimhriúla, fleasc ("-"), fo-líne ("_") agus ponc (".") a bheith sa.` +username_error = `Ní féidir ach carachtair alfa-uimhriúla ("0-9", "a-z", "A-Z"), fleasc ("-"), fo-líne ("_") agus ponc (".") a bheith ann. Ní féidir é a thosú ná a chríochnú le carachtair neamh-alfa-uimhriúla, agus tá cosc ar charachtair neamh-alfa-uimhriúla as a chéile ach an oiread.` +username_error_no_dots = `Ní féidir ach carachtair alfa-uimhriúla ("0-9", "a-z", "A-Z"), fleasc ("-") agus fo-líne ("_") a bheith ann. Ní féidir é a thosú ná a chríochnú le carachtair neamh-alfa-uimhriúla, agus tá cosc ar charachtair neamh-alfa-uimhriúla as a chéile ach an oiread.` +username_claiming_cooldown = Ní féidir an t-ainm úsáideora a éileamh, mar níl a thréimhse fuaraithe thart fós. Is féidir é a éileamh ar %[1]s. +email_domain_is_not_allowed = Tá coimhlint idir fearann seoladh ríomhphoist an úsáideora %s agus EMAIL_DOMAIN_ALLOWLIST nó EMAIL_DOMAIN_BLOCKLIST. Cinntigh go bhfuil an seoladh ríomhphoist socraithe i gceart agat. +last_org_owner = Ní féidir leat an t-úsáideoir deireanach a bhaint den fhoireann "úinéirí". Ní mór úinéir amháin ar a laghad a bheith ann d'eagraíocht. +unable_verify_ssh_key = Ní féidir an eochair SSH a fhíorú, déan seiceáil dhúbailte uirthi le haghaidh earráidí. +still_own_repo = Tá stór amháin nó níos mó i do chuntas, scrios nó aistrigh iad ar dtús. +still_has_org = Is ball d'eagraíocht amháin nó níos mó do chuntas, fág iad ar dtús. +still_own_packages = Tá pacáiste amháin nó níos mó i do chuntas, scrios iad ar dtús. +org_still_own_repo = Tá stór amháin nó níos mó fós faoi úinéireacht na heagraíochta seo, scrios nó aistrigh iad ar dtús. +org_still_own_packages = Tá pacáiste amháin nó níos mó fós ag an eagraíocht seo, scrios iad ar dtús. +required_prefix = Ní mór an ionchur a thosú le "%s" [user] change_avatar = Athraigh do abhatár… @@ -546,6 +630,28 @@ settings = Socruithe Úsáideora disabled_public_activity = Dhíchumasaigh an t-úsáideoir seo infheictheacht phoiblí na gníomhaíochta. form.name_reserved = Tá an t-ainm úsáideora "%s" in áirithe. form.name_pattern_not_allowed = Ní cheadaítear an patrún "%s" in ainm úsáideora. +followers.title.one = Leantóir +followers.title.few = Leantóirí +following.title.one = Ag leanúint +following.title.few = Ag leanúint +followers_one = %d leantóir +followers_few = %d leantóirí +following_one = %d ag leanúint +following_few = %d ag leanúint +block_user = Úsáideoir blocáilte +block_user.detail = Tabhair faoi deara go bhfuil éifeachtaí eile ag baint le húsáideoir a bhlocáil, amhail: +block_user.detail_1 = Scoirfidh sibh de bheith ag leanúint a chéile agus ní bheidh sibh in ann leanúint a chéile. +block_user.detail_2 = Ní bheidh an t-úsáideoir seo in ann idirghníomhú leis na stórtha atá i do sheilbh, ná leis na saincheisteanna agus na tuairimí atá cruthaithe agat. +block_user.detail_3 = Ní bheidh sibh in ann a chéile a chur leis mar chomhoibritheoirí stóir. +follow_blocked_user = Ní féidir leat an t-úsáideoir seo a leanúint mar gur chuir tú bac air nó mar gur chuir an t-úsáideoir seo bac ort. +block = Bloc +unblock = Díbhlocáil +public_activity.visibility_hint.self_public = Tá do ghníomhaíocht le feiceáil ag gach duine, seachas idirghníomhaíochtaí i spásanna príobháideacha. Cumraigh. +public_activity.visibility_hint.admin_public = Tá an ghníomhaíocht seo le feiceáil ag gach duine, ach mar riarthóir is féidir leat idirghníomhaíochtaí i spásanna príobháideacha a fheiceáil freisin. +public_activity.visibility_hint.self_private = Ní féidir ach leatsa agus le riarthóirí an cháis do ghníomhaíocht a fheiceáil. Cumraigh. +public_activity.visibility_hint.admin_private = Tá an ghníomhaíocht seo le feiceáil agat mar is riarthóir thú, ach ba mhaith leis an úsáideoir go bhfanfadh sí príobháideach. +public_activity.visibility_hint.self_private_profile = Ní féidir ach leatsa agus le riarthóirí an cháis do ghníomhaíocht a fheiceáil mar go bhfuil do phróifíl príobháideach. Cumraigh. +form.name_chars_not_allowed = Tá carachtair neamhbhailí san ainm úsáideora "%s". [settings] profile = Próifíl @@ -573,13 +679,13 @@ update_language_success = Tá an teanga nuashonraithe. update_profile_success = Nuashonraíodh do phróifíl. change_username = Tá d'ainm úsáideora athraithe. change_username_prompt = Nóta: Athraíonn athrú d'ainm úsáideora URL do chuntais freisin. -change_username_redirect_prompt = Athreoróidh an sean-ainm úsáideora go dtí go n-éilíonn duine é +change_username_redirect_prompt = Déanfar an seanainm úsáideora a atreorú go dtí go n-éileoidh duine éigin é. continue = Lean ar aghaidh cancel = Cealaigh language = Teanga ui = Téama hidden_comment_types = Cineálacha tráchtaireachta ceilte -hidden_comment_types.ref_tooltip = Tuairimí ina dtagraíodh an tsaincheist seo ó shaincheiste/coiste eile... +hidden_comment_types.ref_tooltip = Tráchtanna inar tagraíodh don cheist seo ó cheist/tiomantas/… eile hidden_comment_types.issue_ref_tooltip = Tuairimí ina n-athraíonn an t-úsáideoir an brainse/clib a bhaineann leis an tsaincheist comment_type_group_reference = Tagairt comment_type_group_label = Lipéad @@ -611,7 +717,7 @@ new_password = Pasfhocal Nua retype_new_password = Deimhnigh Pasfhocal Nua password_incorrect = Tá an pasfhocal reatha mícheart. manage_emails = Bainistigh Seoltaí Ríomhphoist -email_desc = Úsáidfear do phríomhsheoladh ríomhphoist le haghaidh fógraí, aisghabháil pasfhocal agus, ar choinníoll nach bhfuil sé i bhfolach, oibríochtaí Git bunaithe ar an ngréas +email_desc = Úsáidfear do phríomhsheoladh ríomhphoist le haghaidh fógraí, aisghabháil pasfhocail agus, ar choinníoll nach bhfuil sé i bhfolach, oibríochtaí Git gréasánbhunaithe. primary = Príomhúil activated = Gníomhachtaithe requires_activation = Éilíonn gníomhachtú @@ -621,7 +727,7 @@ activations_pending = Gníomhartha ar Feitheamh can_not_add_email_activations_pending = Tá gníomhachtú ar feitheamh, déan iarracht arís i gceann cúpla nóiméad más mian leat ríomhphost nua a chur leis. delete_email = Bain email_deletion = Bain Seoladh R-phoist -email_deletion_desc = Bainfear an seoladh ríomhphoist agus an fhaisnéis ghaolmhar as do chuntas. Ní bheidh na tiomáintí Git a bhaineann leis an seoladh ríomhphoist seo athraithe. Lean ar aghaidh? +email_deletion_desc = Bainfear an seoladh ríomhphoist seo agus faisnéis ghaolmhar as do chuntas. Fanfaidh na gealltanais Git ón seoladh ríomhphoist seo gan athrú. Ar mhaith leat leanúint ar aghaidh? email_deletion_success = Tá an seoladh ríomhphoist bainte. theme_update_success = Nuashonraíodh do théama. theme_update_error = Níl an téama roghnaithe ann. @@ -703,12 +809,12 @@ generate_new_token = Gin Comhartha Nua token_name = Ainm Comhartha generate_token = Gin Comhartha generate_token_success = Gintear do chomhartha nua. Cóipeáil é anois mar ní thaispeánfar é arís. -generate_token_name_duplicate = Úsáideadh %s mar ainm feidhmchláir cheana féin. Úsáid ceann nua le do thoil. +generate_token_name_duplicate = Tá %s in úsáid mar ainm feidhmchláir cheana féin. Bain úsáid as ceann nua le do thoil. delete_token = Scrios access_token_deletion = Scrios Comhartha Rochtana access_token_deletion_desc = Cúlghairfear rochtain ar do chuntas le haghaidh feidhmchláir a úsáideann é a scriosadh comhartha. Ní féidir é seo a chur ar ais. Lean ar aghaidh? delete_token_success = Tá an comhartha scriosta. Níl rochtain ag iarratais a úsáideann é ar do chuntas a thuilleadh. -repo_and_org_access = Rochtain Stórála agus Eagraíochta +repo_and_org_access = Rochtain ar stórtha agus ar eagraíochta permissions_public_only = Poiblí amháin permissions_access_all = Gach (poiblí, príobháideach agus teoranta) select_permissions = Roghnaigh ceadanna @@ -761,7 +867,7 @@ then_enter_passcode = Agus cuir isteach an paschód a léirítear san fheidhmchl passcode_invalid = Tá an pascód mícheart. Bain triail as arís. twofa_enrolled = Tá do chuntas cláraithe go rathúil. Stóráil d'eochair aisghabhála aonúsáide (%s) in áit shábháilte, mar ní thaispeánfar é arís. twofa_failed_get_secret = Theip ar rún a fháil. -webauthn_desc = Is feistí crua-earraí iad eochracha slándála ina bhfuil eochracha cripte Is féidir iad a úsáid le haghaidh fíordheimhniú dhá fhachtóir. Caithfidh eochracha slándála tacú le caigh deán Fíordheimhnithe WebAuthn +webauthn_desc = Is gléasanna crua-earraí iad eochracha slándála ina bhfuil eochracha cripteagrafacha. Is féidir iad a úsáid le haghaidh fíordheimhniú dhá fhachtóir. Ní mór d'eochracha slándála tacú leis an gcaighdeán WebAuthn Authenticator. webauthn_register_key = Cuir Eochair Slándála webauthn_nickname = Leasainm webauthn_delete_key = Bain Eochair Slándála @@ -791,6 +897,88 @@ visibility.public_tooltip = Infheicthe do gach duine visibility.limited = Teoranta visibility.private = Príobháideach visibility.private_tooltip = Ní fheictear ach do bhaill d'eagraíochtaí a chuaigh tú isteach +orgs = Eagraíochtaí +blocked_users = Úsáideoirí blocáilte +storage_overview = Forbhreathnú ar stóráil +quota = Cuóta +biography_placeholder = Inis beagán do dhaoine eile fút féin! (Tacaítear le Markdown) +profile_desc = Fút féin +password_username_disabled = Ní cheadaítear d’úsáideoirí nach úsáideoirí áitiúla iad a n-ainm úsáideora a athrú. Téigh i dteagmháil le riarthóir do shuíomh le haghaidh tuilleadh sonraí. +pronouns = Forainmneacha +pronouns_unspecified = Gan sonrú +update_theme = Athraigh téama +update_language = Athraigh teanga +change_username_redirect_prompt.with_cooldown.one = Beidh an seanainm úsáideora ar fáil do gach duine tar éis tréimhse fuaraithe %[1]d lá. Is féidir leat an seanainm úsáideora a éileamh ar ais fós le linn na tréimhse fuaraithe. +change_username_redirect_prompt.with_cooldown.few = Beidh an seanainm úsáideora ar fáil do gach duine tar éis tréimhse fuaraithe %[1]d lá. Is féidir leat an seanainm úsáideora a éileamh ar ais fós le linn na tréimhse fuaraithe. +language.title = Teanga réamhshocraithe +language.description = Sábhálfar an teanga seo chuig do chuntas agus úsáidfear í mar an teanga réamhshocraithe tar éis duit logáil isteach. +language.localization_project = Cabhraigh linn Forgejo a aistriú go do theanga féin! Foghlaim tuilleadh. +hints = Leideanna +additional_repo_units_hint = Moltar aonaid stórtha breise a chumasú +additional_repo_units_hint_description = Taispeáin leid "Cumasaigh níos mó" do stórtha nach bhfuil na haonaid uile atá ar fáil cumasaithe iontu. +update_hints = Leideanna nuashonraithe +update_hints_success = Tá na leideanna nuashonraithe. +hidden_comment_types_description = Ní thaispeánfar cineálacha tráchta atá seiceáilte anseo laistigh de leathanaigh eagráin. Má sheiceálann tú "Lipéad", mar shampla, baintear na tráchtanna uile a dúirt " chuir
{{end}} + {{if .EnableFederation}} +
+ {{ctx.Locale.Tr "admin.federation.federation"}} + +
+ {{end}}
{{ctx.Locale.Tr "admin.config"}}
- {{ctx.Locale.Tr "packages.settings.delete.notice" (``|SafeHTML) (``|SafeHTML)}} + {{ctx.Locale.Tr "packages.settings.delete.notice" (``|TrustHTML) (``|TrustHTML)}}
{{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/queue_manage.tmpl b/templates/admin/queue_manage.tmpl index a793fe1350..7686755d6d 100644 --- a/templates/admin/queue_manage.tmpl +++ b/templates/admin/queue_manage.tmpl @@ -44,7 +44,7 @@ {{ctx.Locale.Tr "admin.monitor.queue.settings.title"}}
-

{{ctx.Locale.Tr "admin.monitor.queue.settings.desc"}}

+

{{ctx.Locale.Tr "admin.monitor.queue.settings.description"}}

diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 8f3f41cea9..465c51ddcf 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -103,7 +103,7 @@

{{ctx.Locale.Tr "repo.settings.delete_desc"}}

- {{ctx.Locale.Tr "repo.settings.delete_notices_2" (``|SafeHTML)}}
+ {{ctx.Locale.Tr "repo.settings.delete_notices_2" (``|TrustHTML)}}
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
{{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl index d7e57c7c14..bc74f24a58 100644 --- a/templates/admin/repo/unadopted.tmpl +++ b/templates/admin/repo/unadopted.tmpl @@ -24,37 +24,37 @@ {{svg "octicon-file-directory-fill"}} {{$dir}}
- + +
+
{{ctx.Locale.Tr "repo.adopt_preexisting"}}
+
+

{{ctx.Locale.Tr "repo.adopt_preexisting_content" $dir}}

+
+
+ + + + + {{template "base/modal_actions_confirm"}} +
+
+
- + +
+
{{ctx.Locale.Tr "repo.delete_preexisting"}}
+
+

{{ctx.Locale.Tr "repo.delete_preexisting_content" $dir}}

+
+
+ + + + + {{template "base/modal_actions_confirm"}} +
+
+
{{end}} diff --git a/templates/admin/runners/create.tmpl b/templates/admin/runners/create.tmpl new file mode 100644 index 0000000000..5d3bc739ac --- /dev/null +++ b/templates/admin/runners/create.tmpl @@ -0,0 +1,5 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}} +
+ {{template "shared/actions/runner_create" .}} +
+{{template "admin/layout_footer" .}} diff --git a/templates/admin/runners/details.tmpl b/templates/admin/runners/details.tmpl new file mode 100644 index 0000000000..47eb552952 --- /dev/null +++ b/templates/admin/runners/details.tmpl @@ -0,0 +1,5 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}} +
+ {{template "shared/actions/runner_details" .}} +
+{{template "admin/layout_footer" .}} diff --git a/templates/admin/runners/edit.tmpl b/templates/admin/runners/edit.tmpl index 1165c84b79..6a4b696696 100644 --- a/templates/admin/runners/edit.tmpl +++ b/templates/admin/runners/edit.tmpl @@ -1,5 +1,5 @@ {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}} -
- {{template "shared/actions/runner_edit" .}} -
+
+ {{template "shared/actions/runner_edit" .}} +
{{template "admin/layout_footer" .}} diff --git a/templates/admin/runners/setup.tmpl b/templates/admin/runners/setup.tmpl new file mode 100644 index 0000000000..6fa9f69a96 --- /dev/null +++ b/templates/admin/runners/setup.tmpl @@ -0,0 +1,5 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin runners")}} +
+ {{template "shared/actions/runner_setup" .}} +
+{{template "admin/layout_footer" .}} diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index 57c0c210cc..b89140483c 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -40,7 +40,7 @@ {{ctx.Locale.Tr "admin.monitor.process.cancel"}}
-

{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (``|SafeHTML)}}

+

{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (``|TrustHTML)}}

{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}

{{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/system_status.tmpl b/templates/admin/system_status.tmpl index d9856ccd0b..c1dd838290 100644 --- a/templates/admin/system_status.tmpl +++ b/templates/admin/system_status.tmpl @@ -1,62 +1,62 @@
-
{{ctx.Locale.Tr "admin.dashboard.server_uptime"}}
+
{{ctx.Locale.Tr "admin.system_status.server_uptime"}}
{{.SysStatus.StartTime}}
-
{{ctx.Locale.Tr "admin.dashboard.current_goroutine"}}
+
{{ctx.Locale.Tr "admin.system_status.current_goroutine"}}
{{.SysStatus.NumGoroutine}}
-
{{ctx.Locale.Tr "admin.dashboard.current_memory_usage"}}
+
{{ctx.Locale.Tr "admin.system_status.current_memory_usage"}}
{{ctx.Locale.TrSize .SysStatus.MemAllocated}}
-
{{ctx.Locale.Tr "admin.dashboard.total_memory_allocated"}}
+
{{ctx.Locale.Tr "admin.system_status.total_memory_allocated"}}
{{ctx.Locale.TrSize .SysStatus.MemTotal}}
-
{{ctx.Locale.Tr "admin.dashboard.memory_obtained"}}
+
{{ctx.Locale.Tr "admin.system_status.memory_obtained"}}
{{ctx.Locale.TrSize .SysStatus.MemSys}}
-
{{ctx.Locale.Tr "admin.dashboard.pointer_lookup_times"}}
+
{{ctx.Locale.Tr "admin.system_status.pointer_lookup_times"}}
{{.SysStatus.Lookups}}
-
{{ctx.Locale.Tr "admin.dashboard.memory_allocate_times"}}
+
{{ctx.Locale.Tr "admin.system_status.memory_allocate_times"}}
{{.SysStatus.MemMallocs}}
-
{{ctx.Locale.Tr "admin.dashboard.memory_free_times"}}
+
{{ctx.Locale.Tr "admin.system_status.memory_free_times"}}
{{.SysStatus.MemFrees}}
-
{{ctx.Locale.Tr "admin.dashboard.current_heap_usage"}}
+
{{ctx.Locale.Tr "admin.system_status.current_heap_usage"}}
{{ctx.Locale.TrSize .SysStatus.HeapAlloc}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_memory_obtained"}}
+
{{ctx.Locale.Tr "admin.system_status.heap_memory_obtained"}}
{{ctx.Locale.TrSize .SysStatus.HeapSys}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_memory_idle"}}
+
{{ctx.Locale.Tr "admin.system_status.heap_memory_idle"}}
{{ctx.Locale.TrSize .SysStatus.HeapIdle}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_memory_in_use"}}
+
{{ctx.Locale.Tr "admin.system_status.heap_memory_in_use"}}
{{ctx.Locale.TrSize .SysStatus.HeapInuse}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_memory_released"}}
+
{{ctx.Locale.Tr "admin.system_status.heap_memory_released"}}
{{ctx.Locale.TrSize .SysStatus.HeapReleased}}
-
{{ctx.Locale.Tr "admin.dashboard.heap_objects"}}
+
{{ctx.Locale.Tr "admin.system_status.heap_objects"}}
{{.SysStatus.HeapObjects}}
-
{{ctx.Locale.Tr "admin.dashboard.bootstrap_stack_usage"}}
+
{{ctx.Locale.Tr "admin.system_status.bootstrap_stack_usage"}}
{{ctx.Locale.TrSize .SysStatus.StackInuse}}
-
{{ctx.Locale.Tr "admin.dashboard.stack_memory_obtained"}}
+
{{ctx.Locale.Tr "admin.system_status.stack_memory_obtained"}}
{{ctx.Locale.TrSize .SysStatus.StackSys}}
-
{{ctx.Locale.Tr "admin.dashboard.mspan_structures_usage"}}
+
{{ctx.Locale.Tr "admin.system_status.mspan_structures_usage"}}
{{ctx.Locale.TrSize .SysStatus.MSpanInuse}}
-
{{ctx.Locale.Tr "admin.dashboard.mspan_structures_obtained"}}
+
{{ctx.Locale.Tr "admin.system_status.mspan_structures_obtained"}}
{{ctx.Locale.TrSize .SysStatus.MSpanSys}}
-
{{ctx.Locale.Tr "admin.dashboard.mcache_structures_usage"}}
+
{{ctx.Locale.Tr "admin.system_status.mcache_structures_usage"}}
{{ctx.Locale.TrSize .SysStatus.MCacheInuse}}
-
{{ctx.Locale.Tr "admin.dashboard.mcache_structures_obtained"}}
+
{{ctx.Locale.Tr "admin.system_status.mcache_structures_obtained"}}
{{ctx.Locale.TrSize .SysStatus.MCacheSys}}
-
{{ctx.Locale.Tr "admin.dashboard.profiling_bucket_hash_table_obtained"}}
+
{{ctx.Locale.Tr "admin.system_status.profiling_bucket_hash_table_obtained"}}
{{ctx.Locale.TrSize .SysStatus.BuckHashSys}}
-
{{ctx.Locale.Tr "admin.dashboard.gc_metadata_obtained"}}
+
{{ctx.Locale.Tr "admin.system_status.gc_metadata_obtained"}}
{{ctx.Locale.TrSize .SysStatus.GCSys}}
-
{{ctx.Locale.Tr "admin.dashboard.other_system_allocation_obtained"}}
+
{{ctx.Locale.Tr "admin.system_status.other_system_allocation_obtained"}}
{{ctx.Locale.TrSize .SysStatus.OtherSys}}
-
{{ctx.Locale.Tr "admin.dashboard.next_gc_recycle"}}
+
{{ctx.Locale.Tr "admin.system_status.next_gc_recycle"}}
{{ctx.Locale.TrSize .SysStatus.NextGC}}
-
{{ctx.Locale.Tr "admin.dashboard.last_gc_time"}}
+
{{ctx.Locale.Tr "admin.system_status.last_gc_time"}}
{{.SysStatus.LastGCTime}}
-
{{ctx.Locale.Tr "admin.dashboard.total_gc_pause"}}
+
{{ctx.Locale.Tr "admin.system_status.total_gc_pause"}}
{{.SysStatus.PauseTotalNs}}
-
{{ctx.Locale.Tr "admin.dashboard.last_gc_pause"}}
+
{{ctx.Locale.Tr "admin.system_status.last_gc_pause"}}
{{.SysStatus.PauseNs}}
-
{{ctx.Locale.Tr "admin.dashboard.gc_times"}}
+
{{ctx.Locale.Tr "admin.system_status.gc_times"}}
{{.SysStatus.NumGC}}
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl index 1d29a991cd..f18317e694 100644 --- a/templates/admin/user/edit.tmpl +++ b/templates/admin/user/edit.tmpl @@ -224,24 +224,23 @@ -
{{else if .IsSigned}} {{if EnableTimetracking}} - -
+ +
{{svg "octicon-stopwatch"}}
@@ -113,7 +113,7 @@ {{end}}
-
+
{{svg "octicon-bell"}} {{$notificationUnreadCount}}
@@ -122,7 +122,7 @@