diff --git a/package/gitea/default.nix b/package/gitea/default.nix index cce0ff0a..3cb126b1 100644 --- a/package/gitea/default.nix +++ b/package/gitea/default.nix @@ -1,6 +1,6 @@ { lib, - buildGoModule, + buildGo126Module, makeWrapper, git, bash, @@ -10,7 +10,7 @@ openssh, fetchPnpmDeps, pnpmConfigHook, - pnpm, + pnpm_10, stdenv, sqliteSupport ? true, nixosTests, @@ -18,8 +18,9 @@ let pname = "gitea"; - version = "1.25.4"; + version = "1.26.2"; src = ./source; + pnpm = pnpm_10; frontend = stdenv.mkDerivation { pname = "gitea-frontend"; @@ -28,8 +29,9 @@ let pnpmDeps = fetchPnpmDeps { pname = "gitea-frontend"; inherit version src; - fetcherVersion = 2; - hash = "sha256-0p7P68BvO3hv0utUbnPpHSpGLlV7F9HHmOITvJAb/ww="; + inherit pnpm; + fetcherVersion = 3; + hash = "sha256-Qo0DLuZv+2GVLsBfCv/6CC9E/qhSE4HwV4StQL4HX4Y="; }; nativeBuildInputs = [ @@ -48,11 +50,11 @@ let ''; }; in -buildGoModule rec { +buildGo126Module rec { inherit pname version src; proxyVendor = true; - vendorHash = "sha256-y7HurJg+/V1cn8iKDXepk/ie/iNgiJXsQbDi1dhgark="; + vendorHash = "sha256-7+M1n8RSgB3gZ/2na4RF9kYOf90H0bnsJZMDKpgAy64="; outputs = [ "out" @@ -61,10 +63,17 @@ buildGoModule rec { patches = [ ./static-root-path.patch ]; - overrideModAttrs = _: { postPatch = null; }; + overrideModAttrs = _: { + postPatch = '' + substituteInPlace go.mod \ + --replace-fail "go 1.26.3" "go 1.26" + ''; + }; postPatch = '' substituteInPlace modules/setting/server.go --subst-var data + substituteInPlace go.mod \ + --replace-fail "go 1.26.3" "go 1.26" ''; subPackages = [ "." ]; diff --git a/package/gitea/source/.air.toml b/package/gitea/source/.air.toml index 8854041a..6e3c5bdc 100644 --- a/package/gitea/source/.air.toml +++ b/package/gitea/source/.air.toml @@ -4,7 +4,7 @@ tmp_dir = ".air" [build] pre_cmd = ["killall -9 gitea 2>/dev/null || true"] # kill off potential zombie processes from previous runs cmd = "make --no-print-directory backend" -bin = "gitea" +entrypoint = ["./gitea"] delay = 2000 include_ext = ["go", "tmpl"] include_file = ["main.go"] diff --git a/package/gitea/source/.devcontainer/devcontainer.json b/package/gitea/source/.devcontainer/devcontainer.json index 0b20da7c..f8e5972a 100644 --- a/package/gitea/source/.devcontainer/devcontainer.json +++ b/package/gitea/source/.devcontainer/devcontainer.json @@ -13,14 +13,13 @@ "ghcr.io/devcontainers/features/git-lfs:1.2.5": {}, "ghcr.io/jsburckhardt/devcontainer-features/uv:1": {}, "ghcr.io/devcontainers/features/python:1": { - "version": "3.13" + "version": "3.14" }, "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {} }, "customizations": { "vscode": { "settings": {}, - // same extensions as Gitpod, should match /.gitpod.yml "extensions": [ "editorconfig.editorconfig", "dbaeumer.vscode-eslint", diff --git a/package/gitea/source/.dockerignore b/package/gitea/source/.dockerignore index 8e0d6b36..c88fb144 100644 --- a/package/gitea/source/.dockerignore +++ b/package/gitea/source/.dockerignore @@ -74,6 +74,9 @@ cpu.out /VERSION /.air /.go-licenses +/Dockerfile +/Dockerfile.rootless +/.venv # Files and folders that were previously generated /public/assets/img/webpack diff --git a/package/gitea/source/.gitattributes b/package/gitea/source/.gitattributes index e218bbe2..afd02555 100644 --- a/package/gitea/source/.gitattributes +++ b/package/gitea/source/.gitattributes @@ -8,3 +8,4 @@ /vendor/** -text -eol linguist-vendored /web_src/js/vendor/** -text -eol linguist-vendored Dockerfile.* linguist-language=Dockerfile +Makefile.* linguist-language=Makefile diff --git a/package/gitea/source/.github/dependabot.yml b/package/gitea/source/.github/dependabot.yml new file mode 100644 index 00000000..be33b897 --- /dev/null +++ b/package/gitea/source/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 + +updates: + - package-ecosystem: github-actions + labels: [modifies/dependencies] + directory: / + schedule: + interval: daily + cooldown: + default-days: 5 diff --git a/package/gitea/source/.github/labeler.yml b/package/gitea/source/.github/labeler.yml index 49679d28..0f3c5080 100644 --- a/package/gitea/source/.github/labeler.yml +++ b/package/gitea/source/.github/labeler.yml @@ -43,15 +43,13 @@ modifies/internal: - ".editorconfig" - ".eslintrc.cjs" - ".golangci.yml" - - ".gitpod.yml" - ".markdownlint.yaml" - ".spectral.yaml" - - "stylelint.config.js" + - "stylelint.config.*" - ".yamllint.yaml" - ".github/**" - ".gitea/**" - ".devcontainer/**" - - "build.go" - "build/**" - "contrib/**" @@ -85,9 +83,9 @@ docs-update-needed: topic/code-linting: - changed-files: - any-glob-to-any-file: - - ".eslintrc.cjs" - ".golangci.yml" - ".markdownlint.yaml" - ".spectral.yaml" - ".yamllint.yaml" - - "stylelint.config.js" + - "eslint*.config.*" + - "stylelint.config.*" diff --git a/package/gitea/source/.github/workflows/cron-flake-updater.yml b/package/gitea/source/.github/workflows/cron-flake-updater.yml new file mode 100644 index 00000000..c9a1f22a --- /dev/null +++ b/package/gitea/source/.github/workflows/cron-flake-updater.yml @@ -0,0 +1,22 @@ +name: cron-flake-updater + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # runs weekly on Sunday at 00:00 + +jobs: + nix-flake-update: + permissions: + contents: write + issues: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: DeterminateSystems/determinate-nix-action@v3 + - uses: DeterminateSystems/update-flake-lock@main + with: + pr-title: "Update Nix flake" + pr-labels: | + dependencies diff --git a/package/gitea/source/.github/workflows/cron-licenses.yml b/package/gitea/source/.github/workflows/cron-licenses.yml index b176db2c..ee1c3e0c 100644 --- a/package/gitea/source/.github/workflows/cron-licenses.yml +++ b/package/gitea/source/.github/workflows/cron-licenses.yml @@ -9,8 +9,10 @@ jobs: cron-licenses: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' + permissions: + contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -18,7 +20,7 @@ jobs: - run: make generate-gitignore timeout-minutes: 40 - name: push translations to repo - uses: appleboy/git-push-action@v0.0.3 + uses: appleboy/git-push-action@v1.2.0 with: author_email: "teabot@gitea.io" author_name: GiteaBot diff --git a/package/gitea/source/.github/workflows/cron-translations.yml b/package/gitea/source/.github/workflows/cron-translations.yml index f1b51deb..56a30fb5 100644 --- a/package/gitea/source/.github/workflows/cron-translations.yml +++ b/package/gitea/source/.github/workflows/cron-translations.yml @@ -9,9 +9,11 @@ jobs: crowdin-pull: runs-on: ubuntu-latest if: github.repository == 'go-gitea/gitea' + permissions: + contents: write steps: - - uses: actions/checkout@v4 - - uses: crowdin/github-action@v1 + - uses: actions/checkout@v6 + - uses: crowdin/github-action@v2 with: upload_sources: true upload_translations: false @@ -27,7 +29,7 @@ jobs: - name: update locales run: ./build/update-locales.sh - name: push translations to repo - uses: appleboy/git-push-action@v0.0.3 + uses: appleboy/git-push-action@v1.2.0 with: author_email: "teabot@gitea.io" author_name: GiteaBot diff --git a/package/gitea/source/.github/workflows/files-changed.yml b/package/gitea/source/.github/workflows/files-changed.yml index edceef00..55d206bb 100644 --- a/package/gitea/source/.github/workflows/files-changed.yml +++ b/package/gitea/source/.github/workflows/files-changed.yml @@ -19,11 +19,15 @@ on: value: ${{ jobs.detect.outputs.swagger }} yaml: value: ${{ jobs.detect.outputs.yaml }} + json: + value: ${{ jobs.detect.outputs.json }} jobs: detect: runs-on: ubuntu-latest timeout-minutes: 3 + permissions: + contents: read outputs: backend: ${{ steps.changes.outputs.backend }} frontend: ${{ steps.changes.outputs.frontend }} @@ -33,9 +37,10 @@ jobs: docker: ${{ steps.changes.outputs.docker }} swagger: ${{ steps.changes.outputs.swagger }} yaml: ${{ steps.changes.outputs.yaml }} + json: ${{ steps.changes.outputs.json }} steps: - - uses: actions/checkout@v4 - - uses: dorny/paths-filter@v3 + - uses: actions/checkout@v6 + - uses: dorny/paths-filter@v4 id: changes with: filters: | @@ -48,7 +53,7 @@ jobs: - "Makefile" - ".golangci.yml" - ".editorconfig" - - "options/locale/locale_en-US.ini" + - "options/locale/locale_en-US.json" frontend: - "*.js" @@ -80,6 +85,7 @@ jobs: - "uv.lock" docker: + - ".github/workflows/pull-docker-dryrun.yml" - "Dockerfile" - "Dockerfile.rootless" - "docker/**" @@ -98,3 +104,6 @@ jobs: - "**/*.yaml" - ".yamllint.yaml" - "pyproject.toml" + + json: + - "**/*.json" diff --git a/package/gitea/source/.github/workflows/pull-compliance.yml b/package/gitea/source/.github/workflows/pull-compliance.yml index 15055020..c93aed05 100644 --- a/package/gitea/source/.github/workflows/pull-compliance.yml +++ b/package/gitea/source/.github/workflows/pull-compliance.yml @@ -10,13 +10,17 @@ concurrency: jobs: files-changed: uses: ./.github/workflows/files-changed.yml + permissions: + contents: read lint-backend: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -30,14 +34,18 @@ jobs: if: needs.files-changed.outputs.templates == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v6 - - run: uv python install 3.12 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v5 + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.0.0 + - run: uv python install 3.14 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 with: node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - run: make deps-py - run: make deps-frontend - run: make lint-templates @@ -46,23 +54,44 @@ jobs: if: needs.files-changed.outputs.yaml == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v6 - - run: uv python install 3.12 + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.0.0 + - run: uv python install 3.14 - run: make deps-py - run: make lint-yaml + lint-json: + if: needs.files-changed.outputs.json == 'true' + needs: files-changed + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24 + - run: make deps-frontend + - run: make lint-json + lint-swagger: if: needs.files-changed.outputs.swagger == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v5 + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 with: node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - run: make deps-frontend - run: make lint-swagger @@ -70,8 +99,10 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -82,8 +113,10 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -99,8 +132,10 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -114,8 +149,10 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -127,12 +164,16 @@ jobs: if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v5 + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 with: node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - run: make deps-frontend - run: make lint-frontend - run: make checks-frontend @@ -143,8 +184,10 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -175,12 +218,16 @@ jobs: if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v5 + - uses: actions/checkout@v6 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 with: node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - run: make deps-frontend - run: make lint-md @@ -188,8 +235,10 @@ jobs: if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod diff --git a/package/gitea/source/.github/workflows/pull-db-tests.yml b/package/gitea/source/.github/workflows/pull-db-tests.yml index fd1faf80..d168c2ec 100644 --- a/package/gitea/source/.github/workflows/pull-db-tests.yml +++ b/package/gitea/source/.github/workflows/pull-db-tests.yml @@ -10,11 +10,15 @@ concurrency: jobs: files-changed: uses: ./.github/workflows/files-changed.yml + permissions: + contents: read test-pgsql: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read services: pgsql: image: postgres:14 @@ -38,7 +42,7 @@ jobs: ports: - "9000:9000" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -59,37 +63,39 @@ jobs: RACE_ENABLED: true TEST_TAGS: gogit TEST_LDAP: 1 - USE_REPO_TEST_DIR: 1 test-sqlite: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true - run: make deps-backend - - run: make backend + - run: GOEXPERIMENT='' make backend env: TAGS: bindata gogit sqlite sqlite_unlock_notify - name: run migration tests run: make test-sqlite-migration - name: run tests - run: make test-sqlite + run: GOEXPERIMENT='' make test-sqlite timeout-minutes: 50 env: TAGS: bindata gogit sqlite sqlite_unlock_notify RACE_ENABLED: true TEST_TAGS: gogit sqlite sqlite_unlock_notify - USE_REPO_TEST_DIR: 1 test-unit: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read services: elasticsearch: image: elasticsearch:7.5.0 @@ -124,7 +130,7 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -142,7 +148,7 @@ jobs: RACE_ENABLED: true GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} - name: unit-tests-gogit - run: make unit-test-coverage test-check + run: GOEXPERIMENT='' make unit-test-coverage test-check env: TAGS: bindata gogit RACE_ENABLED: true @@ -152,6 +158,8 @@ jobs: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read services: mysql: # the bitnami mysql image has more options than the official one, it's easier to customize @@ -177,7 +185,7 @@ jobs: - "587:587" - "993:993" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -196,13 +204,14 @@ jobs: env: TAGS: bindata RACE_ENABLED: true - USE_REPO_TEST_DIR: 1 TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" test-mssql: if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read services: mssql: image: mcr.microsoft.com/mssql/server:2019-latest @@ -217,7 +226,7 @@ jobs: ports: - 10000:10000 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -234,4 +243,3 @@ jobs: timeout-minutes: 50 env: TAGS: bindata - USE_REPO_TEST_DIR: 1 diff --git a/package/gitea/source/.github/workflows/pull-docker-dryrun.yml b/package/gitea/source/.github/workflows/pull-docker-dryrun.yml index f74277de..201825cc 100644 --- a/package/gitea/source/.github/workflows/pull-docker-dryrun.yml +++ b/package/gitea/source/.github/workflows/pull-docker-dryrun.yml @@ -10,26 +10,31 @@ concurrency: jobs: files-changed: uses: ./.github/workflows/files-changed.yml + permissions: + contents: read - regular: - if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true' + container: + if: needs.files-changed.outputs.docker == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v5 + - uses: actions/checkout@v6 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 + - name: Build regular container image + uses: docker/build-push-action@v7 with: + context: . + platforms: linux/amd64,linux/arm64,linux/riscv64 push: false - tags: gitea/gitea:linux-amd64 - - rootless: - if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true' - needs: files-changed - runs-on: ubuntu-latest - steps: - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v5 + cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful + - name: Build rootless container image + uses: docker/build-push-action@v7 with: + context: . push: false + platforms: linux/amd64,linux/arm64,linux/riscv64 file: Dockerfile.rootless - tags: gitea/gitea:linux-amd64 + cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless diff --git a/package/gitea/source/.github/workflows/pull-e2e-tests.yml b/package/gitea/source/.github/workflows/pull-e2e-tests.yml index f83d9745..3472d517 100644 --- a/package/gitea/source/.github/workflows/pull-e2e-tests.yml +++ b/package/gitea/source/.github/workflows/pull-e2e-tests.yml @@ -10,26 +10,34 @@ concurrency: jobs: files-changed: uses: ./.github/workflows/files-changed.yml + permissions: + contents: read test-e2e: - # the "test-e2e" won't pass, and it seems that there is no useful test, so skip - # if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' - if: false + if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' needs: files-changed runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v5 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 with: node-version: 24 - - run: make deps-frontend frontend deps-backend - - run: pnpm exec playwright install --with-deps - - run: make test-e2e-sqlite - timeout-minutes: 40 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + - run: make deps-frontend + - run: make frontend + - run: make deps-backend + - run: make gitea-e2e + - run: make playwright + - run: make test-e2e + timeout-minutes: 10 env: - USE_REPO_TEST_DIR: 1 + FORCE_COLOR: 1 + GITEA_TEST_E2E_DEBUG: 1 diff --git a/package/gitea/source/.github/workflows/pull-labeler.yml b/package/gitea/source/.github/workflows/pull-labeler.yml index 812819b5..d05483e5 100644 --- a/package/gitea/source/.github/workflows/pull-labeler.yml +++ b/package/gitea/source/.github/workflows/pull-labeler.yml @@ -15,6 +15,6 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/labeler@v5 + - uses: actions/labeler@v6 with: sync-labels: true diff --git a/package/gitea/source/.github/workflows/release-nightly.yml b/package/gitea/source/.github/workflows/release-nightly.yml index f8b55aa2..eaebccd7 100644 --- a/package/gitea/source/.github/workflows/release-nightly.yml +++ b/package/gitea/source/.github/workflows/release-nightly.yml @@ -11,8 +11,10 @@ concurrency: jobs: nightly-binary: runs-on: namespace-profile-gitea-release-binary + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -20,10 +22,12 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v5 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 with: node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - run: make deps-frontend deps-backend # xgo build - run: make release @@ -31,7 +35,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -48,7 +52,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -56,88 +60,76 @@ jobs: - name: upload binaries to s3 run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress - nightly-docker-rootful: + + nightly-container: runs-on: namespace-profile-gitea-release-docker permissions: + contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - check-latest: true - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 - name: Get cleaned branch name id: clean_name run: | REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" + - uses: docker/metadata-action@v6 + id: meta + with: + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea + tags: | + type=raw,value=${{ steps.clean_name.outputs.branch }} + annotations: | + org.opencontainers.image.authors="maintainers@gitea.io" + - uses: docker/metadata-action@v6 + id: meta_rootless + with: + images: |- + gitea/gitea + ghcr.io/go-gitea/gitea + # each tag below will have the suffix of -rootless + flavor: | + suffix=-rootless + tags: | + type=raw,value=${{ steps.clean_name.outputs.branch }} + annotations: | + org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: fetch go modules - run: make vendor - - name: build rootful docker image - uses: docker/build-push-action@v5 + - name: build regular docker image + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 push: true - tags: |- - gitea/gitea:${{ steps.clean_name.outputs.branch }} - ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }} - nightly-docker-rootless: - runs-on: namespace-profile-gitea-release-docker - permissions: - packages: write # to publish to ghcr.io - steps: - - uses: actions/checkout@v4 - # fetch all commits instead of only the last as some branches are long lived and could have many between versions - # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - - run: git fetch --unshallow --quiet --tags --force - - uses: actions/setup-go@v6 - with: - go-version-file: go.mod - check-latest: true - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - name: Get cleaned branch name - id: clean_name - run: | - REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//') - echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT" - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Login to GHCR using PAT - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: fetch go modules - run: make vendor + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful + cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max - name: build rootless docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64,linux/arm64,linux/riscv64 push: true file: Dockerfile.rootless - tags: |- - gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless - ghcr.io/go-gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless + tags: ${{ steps.meta_rootless.outputs.tags }} + annotations: ${{ steps.meta_rootless.outputs.annotations }} + cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless + cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootless,mode=max diff --git a/package/gitea/source/.github/workflows/release-tag-rc.yml b/package/gitea/source/.github/workflows/release-tag-rc.yml index b57e5e60..248fa532 100644 --- a/package/gitea/source/.github/workflows/release-tag-rc.yml +++ b/package/gitea/source/.github/workflows/release-tag-rc.yml @@ -12,8 +12,10 @@ concurrency: jobs: binary: runs-on: namespace-profile-gitea-release-binary + permissions: + contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -21,10 +23,12 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v5 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 with: node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - run: make deps-frontend deps-backend # xgo build - run: make release @@ -32,7 +36,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -49,7 +53,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -58,7 +62,7 @@ jobs: run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress - name: Install GH CLI - uses: dev-hanz-ops/install-gh-cli-action@v0.1.0 + uses: dev-hanz-ops/install-gh-cli-action@v0.2.1 with: gh-cli-version: 2.39.1 - name: create github release @@ -66,18 +70,20 @@ jobs: gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/* env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - docker-rootful: + + container: runs-on: namespace-profile-gitea-release-docker permissions: + contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 + - uses: docker/metadata-action@v6 id: meta with: images: |- @@ -88,38 +94,10 @@ jobs: # 1.2.3-rc0 tags: | type=semver,pattern={{version}} - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Login to GHCR using PAT - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: build rootful docker image - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64,linux/arm64,linux/riscv64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - docker-rootless: - runs-on: namespace-profile-gitea-release-docker - permissions: - packages: write # to publish to ghcr.io - steps: - - uses: actions/checkout@v4 - # fetch all commits instead of only the last as some branches are long lived and could have many between versions - # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 - id: meta + annotations: | + org.opencontainers.image.authors="maintainers@gitea.io" + - uses: docker/metadata-action@v6 + id: meta_rootless with: images: |- gitea/gitea @@ -131,23 +109,33 @@ jobs: # 1.2.3-rc0 tags: | type=semver,pattern={{version}} + annotations: | + org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: build rootless docker image - uses: docker/build-push-action@v5 + - name: build regular container image + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/riscv64 + push: true + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + - name: build rootless container image + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 push: true file: Dockerfile.rootless - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta_rootless.outputs.tags }} + annotations: ${{ steps.meta_rootless.outputs.annotations }} diff --git a/package/gitea/source/.github/workflows/release-tag-version.yml b/package/gitea/source/.github/workflows/release-tag-version.yml index c40aa8f0..1e84ae17 100644 --- a/package/gitea/source/.github/workflows/release-tag-version.yml +++ b/package/gitea/source/.github/workflows/release-tag-version.yml @@ -15,9 +15,10 @@ jobs: binary: runs-on: namespace-profile-gitea-release-binary permissions: + contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force @@ -25,10 +26,12 @@ jobs: with: go-version-file: go.mod check-latest: true - - uses: pnpm/action-setup@v4 - - uses: actions/setup-node@v5 + - uses: pnpm/action-setup@v5 + - uses: actions/setup-node@v6 with: node-version: 24 + cache: pnpm + cache-dependency-path: pnpm-lock.yaml - run: make deps-frontend deps-backend # xgo build - run: make release @@ -36,7 +39,7 @@ jobs: TAGS: bindata sqlite sqlite_unlock_notify - name: import gpg key id: import_gpg - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 with: gpg_private_key: ${{ secrets.GPGSIGN_KEY }} passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }} @@ -53,7 +56,7 @@ jobs: echo "Cleaned name is ${REF_NAME}" echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT" - name: configure aws - uses: aws-actions/configure-aws-credentials@v4 + uses: aws-actions/configure-aws-credentials@v6 with: aws-region: ${{ secrets.AWS_REGION }} aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -62,7 +65,7 @@ jobs: run: | aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress - name: Install GH CLI - uses: dev-hanz-ops/install-gh-cli-action@v0.1.0 + uses: dev-hanz-ops/install-gh-cli-action@v0.2.1 with: gh-cli-version: 2.39.1 - name: create github release @@ -70,18 +73,20 @@ jobs: gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/* env: GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - docker-rootful: + + container: runs-on: namespace-profile-gitea-release-docker permissions: + contents: read packages: write # to publish to ghcr.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 # fetch all commits instead of only the last as some branches are long lived and could have many between versions # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 + - uses: docker/metadata-action@v6 id: meta with: images: |- @@ -96,36 +101,10 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}} type=semver,pattern={{major}}.{{minor}} - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Login to GHCR using PAT - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: build rootful docker image - uses: docker/build-push-action@v5 - with: - context: . - platforms: linux/amd64,linux/arm64,linux/riscv64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - docker-rootless: - runs-on: namespace-profile-gitea-release-docker - steps: - - uses: actions/checkout@v4 - # fetch all commits instead of only the last as some branches are long lived and could have many between versions - # fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567 - - run: git fetch --unshallow --quiet --tags --force - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@v3 - - uses: docker/metadata-action@v5 - id: meta + annotations: | + org.opencontainers.image.authors="maintainers@gitea.io" + - uses: docker/metadata-action@v6 + id: meta_rootless with: images: |- gitea/gitea @@ -142,23 +121,33 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}} type=semver,pattern={{major}}.{{minor}} + annotations: | + org.opencontainers.image.authors="maintainers@gitea.io" - name: Login to Docker Hub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR using PAT - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: build rootless docker image - uses: docker/build-push-action@v5 + - name: build regular container image + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64,linux/riscv64 + push: true + tags: ${{ steps.meta.outputs.tags }} + annotations: ${{ steps.meta.outputs.annotations }} + - name: build rootless container image + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64,linux/riscv64 push: true file: Dockerfile.rootless - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} + tags: ${{ steps.meta_rootless.outputs.tags }} + annotations: ${{ steps.meta_rootless.outputs.annotations }} diff --git a/package/gitea/source/.gitignore b/package/gitea/source/.gitignore index a580861a..019ee94c 100644 --- a/package/gitea/source/.gitignore +++ b/package/gitea/source/.gitignore @@ -25,6 +25,9 @@ __debug_bin* # Visual Studio /.vs/ +# mise version managment tool +mise.toml + *.cgo1.go *.cgo2.c _cgo_defun.c @@ -52,6 +55,7 @@ cpu.out *.log.*.gz /gitea +/gitea-e2e /gitea-vet /debug /integrations.test @@ -64,13 +68,9 @@ cpu.out /indexers /log /public/assets/img/avatar +/tests/e2e-output /tests/integration/gitea-integration-* /tests/integration/indexers-* -/tests/e2e/gitea-e2e-* -/tests/e2e/indexers-* -/tests/e2e/reports -/tests/e2e/test-artifacts -/tests/e2e/test-snapshots /tests/*.ini /tests/**/*.git/**/*.sample /node_modules @@ -79,6 +79,7 @@ cpu.out /yarn-error.log /npm-debug.log* /.pnpm-store +/public/assets/.vite /public/assets/js /public/assets/css /public/assets/fonts @@ -86,10 +87,7 @@ cpu.out /vendor /VERSION /.air -/.go-licenses -# Files and folders that were previously generated -/public/assets/img/webpack # Snapcraft /gitea_a*.txt @@ -118,7 +116,10 @@ prime/ /.goosehints /.windsurfrules /.github/copilot-instructions.md -/AGENT.md -/CLAUDE.md /llms.txt +# Ignore worktrees when working on multiple branches +.worktrees/ + +# A Makefile for custom make targets +Makefile.local diff --git a/package/gitea/source/.gitpod.yml b/package/gitea/source/.gitpod.yml deleted file mode 100644 index 8671edc4..00000000 --- a/package/gitea/source/.gitpod.yml +++ /dev/null @@ -1,51 +0,0 @@ -tasks: - - name: Setup - init: | - cp -r contrib/ide/vscode .vscode - make deps - make build - command: | - gp sync-done setup - exit 0 - - name: Run backend - command: | - gp sync-await setup - - # Get the URL and extract the domain - url=$(gp url 3000) - domain=$(echo $url | awk -F[/:] '{print $4}') - - if [ -f custom/conf/app.ini ]; then - sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini - sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini - sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini - sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini - else - mkdir -p custom/conf/ - echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini - echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini - fi - export TAGS="sqlite sqlite_unlock_notify" - make watch-backend - - name: Run frontend - command: | - gp sync-await setup - make watch-frontend - openMode: split-right - -vscode: - extensions: - - editorconfig.editorconfig - - dbaeumer.vscode-eslint - - golang.go - - stylelint.vscode-stylelint - - DavidAnson.vscode-markdownlint - - Vue.volar - - ms-azuretools.vscode-docker - - vitest.explorer - - cweijan.vscode-database-client2 - - GitHub.vscode-pull-request-github - -ports: - - name: Gitea - port: 3000 diff --git a/package/gitea/source/.golangci.yml b/package/gitea/source/.golangci.yml index 2ad39fba..afd91d65 100644 --- a/package/gitea/source/.golangci.yml +++ b/package/gitea/source/.golangci.yml @@ -6,15 +6,19 @@ linters: default: none enable: - bidichk + - bodyclose - depguard - dupl - errcheck - forbidigo + - gocheckcompilerdirectives - gocritic - govet - ineffassign - mirror + - modernize - nakedret + - nilnil - nolintlint - perfsprint - revive @@ -45,6 +49,8 @@ linters: desc: do not use the ini package, use gitea's config system instead - pkg: gitea.com/go-chi/cache desc: do not use the go-chi cache package, use gitea's cache system + - pkg: github.com/pkg/errors + desc: use builtin errors package instead nolintlint: allow-unused: false require-explanation: true @@ -55,38 +61,28 @@ linters: disabled-checks: - ifElseChain - singleCaseSwitch # Every time this occurred in the code, there was no other way. + - deprecatedComment # conflicts with go-swagger comments revive: severity: error rules: - - name: atomic - - name: bare-return - name: blank-imports - name: constant-logical-expr - name: context-as-argument - name: context-keys-type - name: dot-imports - - name: duplicated-imports - name: empty-lines - - name: error-naming - name: error-return - name: error-strings - - name: errorf - name: exported - name: identical-branches - name: if-return - name: increment-decrement - - name: indent-error-flow - name: modifies-value-receiver - name: package-comments - - name: range - - name: receiver-naming - name: redefines-builtin-id - - name: string-of-int - name: superfluous-else - name: time-naming - - name: unconditional-recursion - name: unexported-return - - name: unreachable-code - name: var-declaration - name: var-naming arguments: @@ -107,6 +103,12 @@ linters: - require-error usetesting: os-temp-dir: true + perfsprint: + concat-loop: false + govet: + enable: + - nilness + - unusedwrite exclusions: generated: lax presets: @@ -118,16 +120,12 @@ linters: - linters: - dupl - errcheck - - gocyclo - - gosec - staticcheck - unparam path: _test\.go - linters: - dupl - errcheck - - gocyclo - - gosec path: models/migrations/v - linters: - forbidigo @@ -139,12 +137,8 @@ linters: - gocritic text: (?i)`ID' should not be capitalized - linters: - - deadcode - unused text: (?i)swagger - - linters: - - staticcheck - text: (?i)argument x is overwritten before first use - linters: - gocritic text: '(?i)commentFormatting: put a space between `//` and comment text' @@ -153,6 +147,7 @@ linters: text: '(?i)exitAfterDefer:' paths: - node_modules + - .venv - public - web_src - third_party$ @@ -172,6 +167,7 @@ formatters: generated: lax paths: - node_modules + - .venv - public - web_src - third_party$ diff --git a/package/gitea/source/.yamllint.yaml b/package/gitea/source/.yamllint.yaml index 5a1e1e87..1d33ecee 100644 --- a/package/gitea/source/.yamllint.yaml +++ b/package/gitea/source/.yamllint.yaml @@ -21,9 +21,7 @@ rules: comments-indentation: level: error - document-start: - level: error - present: false + document-start: disable document-end: present: false diff --git a/package/gitea/source/CHANGELOG.md b/package/gitea/source/CHANGELOG.md index ceb6d4a8..c7ab42f6 100644 --- a/package/gitea/source/CHANGELOG.md +++ b/package/gitea/source/CHANGELOG.md @@ -4,7 +4,538 @@ This changelog goes through the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.com). -## [1.25.4](https://github.com/go-gitea/gitea/releases/tag/1.25.4) - 2026-01-15 +## [1.26.2](https://github.com/go-gitea/gitea/releases/tag/1.26.2) - 2026-05-20 + +* SECURITY + * fix(permissions): Fix reading permission (#37769) + * fix(actions): make artifact signature payloads unambiguous (#37707) + * fix: Unify public-only token filtering in API queries and repo access checks (#37118) + * fix: Add missed token scope checking (#37735) + * fix(oauth): bind token exchanges to the original client request (#37704) + * fix(oauth): strengthen PKCE validation and refresh token replay protection (#37706) + * fix(web): enforce token scopes on raw, media, and attachment downloads (#37698) + * fix(security): enforce wiki git writes and LFS token access at request time (#37695) + * feat(api): encrypt AWS creds (#37679) + * fix(deps): update dependency mermaid to v11.15.0 [security], add e2e test + * fix(packages): Add label for private and internal package and fix composor package source permission check (#37610) + * fix(git): Fix smart http request scope bug (#37583) + * Fix basic auth bug (#37503) + * Fix allow maintainer edit permission check (#37479) (#37484) + * Fix URL sanitization to handle schemeless credentials (#37440) (#37471) + * Fix attachment Content-Security-Policy (#37455) (#37464) + * chore(deps): bump go-git/go-git/v5 to 5.19.0 (#37608) + +* BUGFIXES + * fix(pull): handle empty pull request files view to allow reviews (#37783) + * fix(markup): make RenderString never fail (#37779) + * fix: add natural sort to sortTreeViewNodes (#37772) + * fix: package creation unique conflict (#37774) + * fix!: add DEFAULT_TITLE_SOURCE setting for pull request title default behavior (#37465) + * fix: Allow direct commits for unprotected files with push restrictions (#37657) + * fix(actions): wrong assumption that run id always >= job id (#37737) + * fix(auth): set User-Agent on avatar fetch and sync avatar on link-account register (#37564) (#37588) + * fix(actions): deadlock between PrepareRunAndInsert and UpdateTaskByState (#37692) + * fix(repo): /generate must sync the branch table for the new repo (#37693) + * build: Fix snap build (1.26) + * fix(actions): run TransferLogs on UpdateLog{Rows:[], NoMore:true} (#37631) + * fix show correct mergebase + * fix: make clone URL respect public URL detection setting (#37615) + * fix: "run as root" check (#37622) + * chore(deps): update dependency go to v1.26.3 (#37601) + * Compare dropdown fails when selecting branch with no common merge-base (#37470) + * fix: treat email addresses case-insensitively (#37600) + * fix(actions): fix blank lines after ::endgroup:: (#37597) + * fix(actions): report individual step status in workflow job API response (#37592) + * fix: Invalid UTF-8 commit messages in JSON API responses (#37542) + * fix: use consistent GetUser family functions (#37553) + * fix(api): return 409 message instead of empty JSON for wrong commit id (#37572) + * fix(actions): prevent panic when workflow contains null jobs (#37570) + * Make ServeSetHeaders default to download attachment if filename exists (#37552) (#37555) + * Fix(actions): validate workflow param to prevent 500 error (#37546) (#37554) + * Don't unblock run-level-concurrency-blocked runs in the resolver (#37461) (#37538) + * Fix(packages): use file names for generic web downloads (#37514) (#37520) + * Fix merge autodetect can't close other PRs but only the last one when multiple PRs are pushed at once (#37512) (#37516) + * Fix update branch protection order (#37508) (#37513) + * Fix mCaptcha broken after Vite migration (#37492) (#37509) + * Fix review submission from single-commit PR view (#37475) (#37485) + * Fix scheduled action panic with null event payload (#37459) (#37466) + * Make GetPossibleUserByID can handle deleted user (#37430) (#37431) + * Remove excessive quote from terraform instructions (#37424) (#37426) + * Fix color regressions, add `priority` color (#37417) (#37421) + +* MISC + * Add CurrentURL template variable back (#37444) (#37449) + +## [1.26.1](https://github.com/go-gitea/gitea/releases/tag/v1.26.1) - 2026-04-21 + +* BUGFIXES + * Add event.schedule context for schedule actions task (#37320) (#37348) + * Fix an issue where changing an organization's visibility caused problems when users had forked its repositories. (#37324) (#37344) + * Use modern "git update-index --cacheinfo" syntax to support more file names (#37338) (#37343) + * Fix URL related escaping for oauth2 (#37334) (#37340) + * When the requested arch rpm is missing fall back to noarch (#37236) (#37339) + * Fix actions concurrency groups cross-branch leak (#37311) (#37331) + * Fix bug when accessing user badges (#37321) (#37329) + * Fix AppFullLink (#37325) (#37328) + * Fix container auth for public instance (#37290) (#37294) + * Enhance GetActionWorkflow to support fallback references (#37189) (#37283) + * Fix vite manifest update masking build errors (#37279) (#37310) + * Fix Mermaid diagrams failing when node labels contain line breaks (#37296) (#37299) + * Use TriggerEvent instead of Event in workflow runs API response for scheduled runs (#37288) #37360 + * Add URL to Learn more about blocking a user. (#37355) #37367 + * Fix button layout shift when collapsing file tree in editor (#37363) #37375 + * Fix org team assignee/reviewer lookups for team member permissions (#37365) #37391 + * Fix repo init README EOL (#37388) #37399 + * Fix: dump with default zip type produces uncompressed zip (#37401)#37402 + +## [1.26.0](https://github.com/go-gitea/gitea/releases/tag/v1.26.0) - 2026-04-17 + +* BREAKING + * Correct swagger annotations for enums, status codes, and notification state (#37030) + * Remove GET API registration-token (#36801) + * Support Actions `concurrency` syntax (#32751) + * Make PUBLIC_URL_DETECTION default to "auto" (#36955) +* SECURITY + * Bound PageSize in `ListUnadoptedRepositories` (#36884) +* FEATURES + * Support Actions `concurrency` syntax (#32751) + * Add terraform state registry (#36710) + * Instance-wide (global) info banner and maintenance mode (#36571) + * Support rendering OpenAPI spec (#36449) + * Add keyboard shortcuts for repository file and code search (#36416) + * Add support for archive-upload rpc (#36391) + * Add ability to download subpath archive (#36371) + * Add workflow dependencies visualization (#26062) (#36248) & Restyle Workflow Graph (#36912) + * Automatic generation of release notes (#35977) + * Add "Go to file", "Delete Directory" to repo file list page (#35911) + * Introduce "config edit-ini" sub command to help maintaining INI config file (#35735) + * Add button to re-run failed jobs in Actions (#36924) + * Support actions and reusable workflows from private repos (#32562) + * Add summary to action runs view (#36883) + * Add user badges (#36752) + * Add configurable permissions for Actions automatic tokens (#36173) + * Add per-runner "Disable/Pause" (#36776) + * Feature non-zipped actions artifacts (action v7 / nodejs / npm v6.2.0) (#36786) +* PERFORMANCE + * WorkflowDispatch API optionally return runid (#36706) + * Add render cache for SVG icons (#36863) + * Load `mentionValues` asynchronously (#36739) + * Lazy-load some Vue components, fix heatmap chunk loading on every page (#36719) + * Load heatmap data asynchronously (#36622) + * Use prev/next pagination for user profile activities page to speed up (#36642) + * Refactor cat-file batch operations and support `--batch-command` approach (#35775) + * Use merge tree to detect conflicts when possible (#36400) +* ENHANCEMENTS + * Implement logout redirection for reverse proxy auth setups (#36085) (#37171) + * Adds option to force update new branch in contents routes (#35592) + * Add viewer controller for mermaid (zoom, drag) (#36557) + * Add code editor setting dropdowns (#36534) + * Add `elk` layout support to mermaid (#36486) + * Add resolve/unresolve review comment API endpoints (#36441) + * Allow configuring default PR base branch (fixes #36412) (#36425) + * Add support for RPM Errata (updateinfo.xml) (#37125) + * Require additional user confirmation for making repo private (#36959) + * Add `actions.WORKFLOW_DIRS` setting (#36619) + * Avoid opening new tab when downloading actions logs (#36740) + * Implements OIDC RP-Initiated Logout (#36724) + * Show workflow link (#37070) + * Desaturate dark theme background colors (#37056) + * Refactor "org teams" page and help new users to "add member" to an org (#37051) + * Add webhook name field to improve webhook identification (#37025) (#37040) + * Make task list checkboxes clickable in the preview tab (#37010) + * Improve severity labels in Actions logs and tweak colors (#36993) + * Linkify URLs in Actions workflow logs (#36986) + * Allow text selection on checkbox labels (#36970) + * Support dark/light theme images in markdown (#36922) + * Enable native dark mode for swagger-ui (#36899) + * Rework checkbox styling, remove `input` border hover effect (#36870) + * Refactor storage content-type handling of ServeDirectURL (#36804) + * Use "Enable Gravatar" but not "Disable" (#36771) + * Use case-insensitive matching for Git error "Not a valid object name" (#36728) + * Add "Copy Source" to markup comment menu (#36726) + * Change image transparency grid to CSS (#36711) + * Add "Run" prefix for unnamed action steps (#36624) + * Persist actions log time display settings in `localStorage` (#36623) + * Use first commit title for multi-commit PRs and fix auto-focus title field (#36606) + * Improve BuildCaseInsensitiveLike with lowercase (#36598) + * Improve diff highlighting (#36583) + * Exclude cancelled runs from failure-only email notifications (#36569) + * Use full-file highlighting for diff sections (#36561) + * Color command/error logs in Actions log (#36538) + * Add paging headers (#36521) + * Improve timeline entries for WIP prefix changes in pull requests (#36518) + * Add FOLDER_ICON_THEME configuration option (#36496) + * Normalize guessed languages for code highlighting (#36450) + * Add chunked transfer encoding support for LFS uploads (#36380) + * Indicate when only optional checks failed (#36367) + * Add 'allow_maintainer_edit' API option for creating a pull request (#36283) + * Support closing keywords with URL references (#36221) + * Improve diff file headers (#36215) + * Fix and enhance comment editor monospace toggle (#36181) + * Add git.DIFF_RENAME_SIMILARITY_THRESHOLD option (#36164) + * Add matching pair insertion to markdown textarea (#36121) + * Add sorting/filtering to admin user search API endpoint (#36112) + * Allow action user have read permission in public repo like other user (#36095) + * Disable matchBrackets in monaco (#36089) + * Use GitHub-style commit message for squash merge (#35987) + * Make composer registry support tar.gz and tar.bz2 and fix bugs (#35958) + * Add GITEA_PR_INDEX env variable to githooks (#35938) + * Add proper error message if session provider can not be created (#35520) + * Add button to copy file name in PR files (#35509) + * Move `X_FRAME_OPTIONS` setting from `cors` to `security` section (#30256) + * Add placeholder content for empty content page (#37114) + * Add `DEFAULT_DELETE_BRANCH_AFTER_MERGE` setting (#36917) + * Redirect to the only OAuth2 provider when no other login methods and fix various problems (#36901) + * Add admin badge to navbar avatar (#36790) + * Add `never` option to `PUBLIC_URL_DETECTION` configuration (#36785) + * Add background and run count to actions list page (#36707) + * Add icon to buttons "Close with Comment", "Close Pull Request", "Close Issue" (#36654) + * Add support for in_progress event in workflow_run webhook (#36979) + * Report commit status for pull_request_review events (#36589) + * Render merged pull request title as such in dashboard feed (#36479) + * Feature to be able to filter project boards by milestones (#36321) + * Use user id in noreply emails (#36550) + * Enable pagination on GiteaDownloader.getIssueReactions() (#36549) + * Remove striped tables in UI (#36509) + * Improve control char rendering and escape button styling (#37094) + * Support legacy run/job index-based URLs and refactor migration 326 (#37008) + * Add date to "No Contributions" tooltip (#36190) + * Show edit page confirmation dialog on tree view file change (#36130) + * Mention proc-receive in text for dashboard.resync_all_hooks func (#35991) + * Reuse selectable style for wiki (#35990) + * Support blue yellow colorblind theme (#35910) + * Support selecting theme on the footer (#35741) + * Improve online runner check (#35722) + * Add quick approve button on PR page (#35678) + * Enable commenting on expanded lines in PR diffs (#35662) + * Print PR-Title into tooltip for actions (#35579) + * Use explicit, stronger defaults for newly generated repo signing keys for Debian (#36236) + * Improve the compare page (#36261) + * Unify repo names in system notices (#36491) + * Move package settings to package instead of being tied to version (#37026) + * Add Actions API rerun endpoints for runs and jobs (#36768) + * Add branch_count to repository API (#35351) (#36743) + * Add created_by filter to SearchIssues (#36670) + * Allow admins to rename non-local users (#35970) + * Support updating branch via API (#35951) + * Add an option to automatically verify SSH keys from LDAP (#35927) + * Make "update file" API can create a new file when SHA is not set (#35738) + * Update issue.go with labels documentation (labels content, not ids) (#35522) + * Expose content_version for optimistic locking on issue and PR edits (#37035) + * Pass ServeHeaderOptions by value instead of pointer, fine tune httplib tests (#36982) +* BUGFIXES + * Frontend iframe renderer framework: 3D models, OpenAPI (#37233) (#37273) + * Fix CODEOWNERS absolute path matching. (#37244) (#37264) + * Swift registry metadata: preserve more JSON fields and accept empty metadata (#37254) (#37261) + * Fix user ssh key exporting and tests (#37256) (#37258) + * Fix team member avatar size and add tooltip (#37253) + * Fix commit title rendering in action run and blame (#37243) (#37251) + * Fix corrupted JSON caused by goccy library (#37214) (#37220) + * Add test for "fetch redirect", add CSS value validation for external render (#37207) (#37216) + * Fix incorrect concurrency check (#37205) (#37215) + * Fix handle missing base branch in PR commits API (#37193) (#37203) + * Fix encoding for Matrix Webhooks (#37190) (#37201) + * Fix handle fork-only commits in compare API (#37185) (#37199) + * Indicate form field readonly via background, fix RunUser config (#37175, #37180) (#37178) + * Report structurally invalid workflows to users (#37116) (#37164) + * Fix API not persisting pull request unit config when has_pull_requests is not set (#36718) + * Rename CSS variables and improve colorblind themes (#36353) + * Hide `add-matcher` and `remove-matcher` from actions job logs (#36520) + * Prevent navigation keys from triggering actions during IME composition (#36540) + * Fix vertical alignment of `.commit-sign-badge` children (#36570) + * Fix duplicate startup warnings in admin panel (#36641) + * Fix CODEOWNERS review request attribution using comment metadata (#36348) + * Fix HTML tags appearing in wiki table of contents (#36284) + * Fix various bugs (#37096) + * Fix various legacy problems (#37092) + * Fix RPM Registry 404 when package name contains 'package' (#37087) + * Merge some standalone Vite entries into index.js (#37085) + * Fix various problems (#37077) + * Fix issue label deletion with Actions tokens (#37013) + * Hide delete branch or tag buttons in mirror or archived repositories. (#37006) + * Fix org contact email not clearable once set (#36975) + * Fix a bug when forking a repository in an organization (#36950) + * Preserve sort order of exclusive labels from template repo (#36931) + * Make container registry support Apple Container (basic auth) (#36920) + * Fix the wrong push commits in the pull request when force push (#36914) + * Add class "list-header-filters" to the div for projects (#36889) + * Fix dbfs error handling (#36844) + * Fix incorrect viewed files counter if reverted change was viewed (#36819) + * Refactor avatar package, support default avatar fallback (#36788) + * Fix README symlink resolution in subdirectories like .github (#36775) + * Fix CSS stacking context issue in actions log (#36749) + * Add gpg signing for merge rebase and update by rebase (#36701) + * Delete non-exist branch should return 404 (#36694) + * Fix `TestActionsCollaborativeOwner` (#36657) + * Fix multi-arch Docker build SIGILL by splitting frontend stage (#36646) + * Fix linguist-detectable attribute being ignored for configuration files (#36640) + * Fix state desync in ComboMarkdownEditor (#36625) + * Unify DEFAULT_SHOW_FULL_NAME output in templates and dropdown (#36597) + * Pull Request Pusher should be the author of the merge (#36581) + * Fix various version parsing problems (#36553) + * Fix highlight diff result (#36539) + * Fix mirror sync parser and fix mirror messages (#36504) + * Fix bug when list pull request commits (#36485) + * Fix various bugs (#36446) + * Fix issue filter menu layout (#36426) + * Restrict branch naming when new change matches with protection rules (#36405) + * Fix link/origin referrer and login redirect (#36279) + * Generate IDs for HTML headings without id attribute (#36233) + * Use a migration test instead of a wrong test which populated the meta test repositories and fix a migration bug (#36160) + * Fix issue close timeline icon (#36138) + * Fix diff blob excerpt expansion (#35922) + * Fix external render (#35727) + * Fix review request webhook bug (#35339) (#35723) + * Fix shutdown waitgroup panic (#35676) + * Cleanup ActionRun creation (#35624) + * Fix possible bug when migrating issues/pull requests (#33487) + * Various fixes (#36697) + * Apply notify/register mail flags during install load (#37120) + * Repair duration display for bad stopped timestamps (#37121) + * Fix(upgrade.sh): use HTTPS for GPG key import and restore SELinux context after upgrade (#36930) + * Fix various trivial problems (#36921) + * Fix various trivial problems (#36953) + * Fix NuGet package upload error handling (#37074) + * Fix CodeQL code scanning alerts (#36858) + * Refactor issue sidebar and fix various problems (#37045) + * Fix various problems (#37029) + * Fix relative-time RangeError (#37021) + * Fix chroma lexer mapping (#36629) + * Fix typos and grammar in English locale (#36751) + * Fix milestone/project text overflow in issue sidebar (#36741) + * Fix `no-content` message not rendering after comment edit (#36733) + * Fix theme loading in development (#36605) + * Fix workflow run jobs API returning null steps (#36603) + * Fix timeline event layout overflow with long content (#36595) + * Fix minor UI issues in runner edit page (#36590) + * Fix incorrect vendored detections (#36508) + * Fix editorconfig not respected in PR Conversation view (#36492) + * Don't create self-references in merged PRs (#36490) + * Fix potential incorrect runID in run status update (#36437) + * Fix file-tree ui error when adding files to repo without commits (#36312) + * Improve image captcha contrast for dark mode (#36265) + * Fix panic in blame view when a file has only a single commit (#36230) + * Fix spelling error in migrate-storage cmd utility (#36226) + * Fix code highlighting on blame page (#36157) + * Fix nilnil in onedev downloader (#36154) + * Fix actions lint (#36029) + * Fix oauth2 session gob register (#36017) + * Fix Arch repo pacman.conf snippet (#35825) + * Fix a number of `strictNullChecks`-related issues (#35795) + * Fix URLJoin, markup render link reoslving, sign-in/up/linkaccount page common data (#36861) + * Hide delete directory button for mirror or archive repository and disable the menu item if user have no permission (#36384) + * Update message severity colors, fix navbar double border (#37019) + * Inline and lazy-load EasyMDE CSS, fix border colors (#36714) + * Closed milestones with no issues now show as 100% completed (#36220) + * Add test for ExtendCommentTreePathLength migration and fix bugs (#35791) + * Only turn links to current instance into hash links (#36237) + * Fix typos in code comments: doesnt, dont, wont (#36890) +* REFACTOR + * Clean up and improve non-gitea js error filter (#37148) (#37155) + * Always show owner/repo name in compare page dropdowns (#37172) (#37200) + * Remove dead CSS rules (#37173) (#37177) + * Replace Monaco with CodeMirror (#36764) + * Replace CSRF cookie with `CrossOriginProtection` (#36183) + * Replace index with id in actions routes (#36842) + * Remove unnecessary function parameter (#35765) + * Move jobparser from act repository to Gitea (#36699) + * Refactor compare router param parse (#36105) + * Optimize 'refreshAccesses' to perform update without removing then adding (#35702) + * Clean up checkbox cursor styles (#37016) + * Remove undocumented support of signing key in the repository git configuration file (#36143) + * Switch `cmd/` to use constructor functions. (#36962) + * Use `relative-time` to render absolute dates (#36238) + * Some refactors about GetMergeBase (#36186) + * Some small refactors (#36163) + * Use gitRepo as parameter instead of repopath when invoking sign functions (#36162) + * Move blame to gitrepo (#36161) + * Move some functions to gitrepo package to reduce RepoPath reference directly (#36126) + * Use gitrepo's clone and push when possible (#36093) + * Remove mermaid margin workaround (#35732) + * Move some functions to gitrepo package (#35543) + * Move GetDiverging functions to gitrepo (#35524) + * Use global lock instead of status pool for cron lock (#35507) + * Use explicit mux instead of DefaultServeMux (#36276) + * Use gitrepo's push function (#36245) + * Pass request context to generateAdditionalHeadersForIssue (#36274) + * Move assign project when creating pull request to the same database transaction (#36244) + * Move catfile batch to a sub package of git module (#36232) + * Use gitrepo.Repository instead of wikipath (#35398) + * Use experimental go json v2 library (#35392) + * Refactor template render (#36438) + * Refactor GetRepoRawDiffForFile to avoid unnecessary pipe or goroutine (#36434) + * Refactor text utility classes to Tailwind CSS (#36703) + * Refactor git command stdio pipe (#36422) + * Refactor git command context & pipeline (#36406) + * Refactor git command stdio pipe (#36393) + * Remove unused functions (#36672) + * Refactor Actions Token Access (#35688) + * Move commit related functions to gitrepo package (#35600) + * Move archive function to repo_model and gitrepo (#35514) + * Move some functions to gitrepo package (#35503) + * Use git model to detect whether branch exist instead of gitrepo method (#35459) + * Some refactor for repo path (#36251) + * Extract helper functions from SearchIssues (#36158) + * Refactor merge conan and container auth preserve actions taskID (#36560) + * Refactor Nuget Auth to reuse Basic Auth Token Validation (#36558) + * Refactor ActionsTaskID (#36503) + * Refactor auth middleware (#36848) + * Refactor code render and render control chars (#37078) + * Clean up AppURL, remove legacy origin-url webcomponent (#37090) + * Remove `util.URLJoin` and replace all callers with direct path concatenation (#36867) + * Replace legacy tw-flex utility classes with flex-text-block/inline (#36778) + * Mark unused&immature activitypub as "not implemented" (#36789) +* TESTING + * Add e2e tests for server push events (#36879) + * Rework e2e tests (#36634) + * Add e2e reaction test, improve accessibility, enable parallel testing (#37081) + * Increase e2e test timeouts on CI to fix flaky tests (#37053) +* BUILD + * Upgrade go-git to v5.18.0 (#37269) + * Replace rollup-plugin-license with rolldown-license-plugin (#37130) (#37158) + * Bump min go version to 1.26.2 (#37139) (#37143) + * Convert locale files from ini to json format (#35489) + * Bump golangci-lint to 2.7.2, enable modernize stringsbuilder (#36180) + * Port away from `flake-utils` (#35675) + * Remove nolint (#36252) + * Update the Unlicense copy to latest version (#36636) + * Update to go 1.26.0 and golangci-lint 2.9.0 (#36588) + * Replace `google/go-licenses` with custom generation (#36575) + * Update go dependencies (#36548) + * Bump appleboy/git-push-action from 1.0.0 to 1.2.0 (#36306) + * Remove fomantic form module (#36222) + * Bump setup-node to v6, re-enable cache (#36207) + * Bump crowdin/github-action from 1 to 2 (#36204) + * Revert "Bump alpine to 3.23 (#36185)" (#36202) + * Update chroma to v2.21.1 (#36201) + * Bump astral-sh/setup-uv from 6 to 7 (#36198) + * Bump docker/build-push-action from 5 to 6 (#36197) + * Bump aws-actions/configure-aws-credentials from 4 to 5 (#36196) + * Bump dev-hanz-ops/install-gh-cli-action from 0.1.0 to 0.2.1 (#36195) + * Add JSON linting (#36192) + * Enable dependabot for actions (#36191) + * Bump alpine to 3.23 (#36185) + * Update chroma to v2.21.0 (#36171) + * Update JS deps and eslint enhancements (#36147) + * Update JS deps (#36091) + * update golangci-lint to v2.7.0 (#36079) + * Update JS deps, fix deprecations (#36040) + * Update JS deps (#35978) + * Add toolchain directive to go.mod (#35901) + * Move `gitea-vet` to use `go tool` (#35878) + * Update to go 1.25.4 (#35877) + * Enable TypeScript `strictNullChecks` (#35843) + * Enable `vue/require-typed-ref` eslint rule (#35764) + * Update JS dependencies (#35759) + * Move `codeformat` folder to tools (#35758) + * Update dependencies (#35733) + * Bump happy-dom from 20.0.0 to 20.0.2 (#35677) + * Bump setup-go to v6 (#35660) + * Update JS deps, misc tweaks (#35643) + * Bump happy-dom from 19.0.2 to 20.0.0 (#35625) + * Use bundled version of spectral (#35573) + * Update JS and PY deps (#35565) + * Bump github.com/wneessen/go-mail from 0.6.2 to 0.7.1 (#35557) + * Migrate from webpack to vite (#37002) + * Update JS dependencies and misc tweaks (#37064) + * Update to eslint 10 (#36925) + * Optimize Docker build with dependency layer caching (#36864) + * Update JS deps (#36850) + * Update tool dependencies and fix new lint issues (#36702) + * Remove redundant linter rules (#36658) + * Move Fomantic dropdown CSS to custom module (#36530) + * Remove and forbid `@ts-expect-error` (#36513) + * Refactor git command stderr handling (#36402) + * Enable gocheckcompilerdirectives linter (#36156) + * Replace `lint-go-gopls` with additional `govet` linters (#36028) + * Update golangci-lint to v2.6.0 (#35801) + * Misc tool tweaks (#35734) + * Add cache to container build (#35697) + * Upgrade vite (#37126) + * Update `setup-uv` to v8.0.0 (#37101) + * Upgrade `go-git` to v5.17.2 and related dependencies (#37060) + * Raise minimum Node.js version to 22.18.0 (#37058) + * Upgrade `golang.org/x/image` to v0.38.0 (#37054) + * Update minimum go version to 1.26.1, golangci-lint to 2.11.2, fix test style (#36876) + * Enable eslint concurrency (#36878) + * Vendor relative-time-element as local web component (#36853) + * Update material-icon-theme v5.32.0 (#36832) + * Update Go dependencies (#36781) + * Upgrade minimatch (#36760) + * Remove i18n backport tool at the moment because of translation format changed (#36643) + * Update emoji data for Unicode 16 (#36596) + * Update JS dependencies, adjust webpack config, misc fixes (#36431) + * Update material-icon-theme to v5.31.0 (#36427) + * Update JS and PY deps (#36383) + * Bump alpine to 3.23, add platforms to `docker-dryrun` (#36379) + * Update JS deps (#36354) + * Update goldmark to v1.7.16 (#36343) + * Update chroma to v2.22.0 (#36342) +* DOCS + * Update AI Contribution Policy (#37022) + * Update AGENTS.md with additional guidelines (#37018) + * Add missing cron tasks to example ini (#37012) + * Add AI Contribution Policy to CONTRIBUTING.md (#36651) + * Minor punctuation improvement in CONTRIBUTING.md (#36291) + * Add documentation for markdown anchor post-processing (#36443) +* MISC + * Correct spelling (#36783) + * Update Nix flake (#37110) + * Update Nix flake (#37024) + * Add valid github scopes (#36977) + * Update Nix flake (#36943) + * Update Nix flake (#36902) + * Update Nix flake (#36857) + * Update Nix flake (#36787) + +## [1.25.5](https://github.com/go-gitea/gitea/releases/tag/v1.25.5) - 2026-03-10 + +* SECURITY + * Toolchain Update to Go 1.25.6 (#36480) (#36487) + * Adjust the toolchain version (#36537) (#36542) + * Update toolchain to 1.25.8 for v1.25 (#36888) + * Prevent redirect bypasses via backslash-encoded paths (#36660) (#36716) + * Fix get release draft permission check (#36659) (#36715) + * Fix a bug user could change another user's primary email (#36586) (#36607) + * Fix OAuth2 authorization code expiry and reuse handling (#36797) (#36851) + * Add validation constraints for repository creation fields (#36671) (#36757) + * Fix bug to check whether user can update pull request branch or rebase branch (#36465) (#36838) + * Add migration http transport for push/sync mirror lfs (#36665) (#36691) + * Fix track time list permission check (#36662) (#36744) + * Fix track time issue id (#36664) (#36689) + * Fix path resolving (#36734) (#36746) + * Fix dump release asset bug (#36799) (#36839) + * Fix org permission API visibility checks for hidden members and private orgs (#36798) (#36841) + * Fix forwarded proto handling for public URL detection (#36810) (#36836) + * Add a git grep search timeout (#36809) (#36835) + * Fix oauth2 s256 (#36462) (#36477) +* ENHANCEMENTS + * Make `security-check` informational only (#36681) (#36852) + * Upgrade to github.com/cloudflare/circl 1.6.3, svgo 4.0.1, markdownlint-cli 0.48.0 (#36840) + * Add some validation on values provided to USER_DISABLED_FEATURES and EXTERNAL_USER_DISABLED_FEATURES (#36688) (#36692) + * Upgrade gogit to 5.16.5 (#36687) + * Add wrap to runner label list (#36565) (#36574) + * Add dnf5 command for Fedora in RPM package instructions (#36527) (#36572) + * Allow scroll propagation outside code editor (#36502) (#36510) +* BUGFIXES + * Fix non-admins unable to automerge PRs from forks (#36833) (#36843) + * Fix bug when pushing mirror with wiki (#36795) (#36807) + * Fix artifacts v4 backend upload problems (#36805) (#36834) + * Fix CRAN package version validation to allow more than 4 version components (#36813) (#36821) + * Fix force push time-line commit comments of pull request (#36653) (#36717) + * Fix SVG height calculation in diff viewer (#36748) (#36750) + * Fix push time bug (#36693) (#36713) + * Fix bug the protected branch rule name is conflicted with renamed branch name (#36650) (#36661) + * Fix bug when do LFS GC (#36500) (#36608) + * Fix focus lost bugs in the Monaco editor (#36609) + * Reprocess htmx content after loading more files (#36568) (#36577) + * Fix assignee sidebar links and empty placeholder (#36559) (#36563) + * Fix issues filter dropdown showing empty label scope section (#36535) (#36544) + * Fix various mermaid bugs (#36547) (#36552) + * Fix data race when uploading container blobs concurrently (#36524) (#36526) + * Correct spacing between username and bot label (#36473) (#36484) + +## [1.25.4](https://github.com/go-gitea/gitea/releases/tag/v1.25.4) - 2026-01-15 * SECURITY * Release attachments must belong to the intended repo (#36347) (#36375) @@ -20,7 +551,8 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Add more routes to the "expensive" list (#36290) * Make "commit statuses" API accept slashes in "ref" (#36264) (#36275) * BUGFIXES - * Fix markdown newline handling during IME composition (#36421) #36424 + * Fix git http service handling (#36396) + * Fix markdown newline handling during IME composition (#36421) (#36424) * Fix missing repository id when migrating release attachments (#36389) * Fix bug when compare in the pull request (#36363) (#36372) * Fix incorrect text content detection (#36364) (#36369) @@ -35,7 +567,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Fix regression in writing authorized principals (#36213) (#36218) * Fix WebAuthn error checking (#36219) (#36235) -## [1.25.3](https://github.com/go-gitea/gitea/releases/tag/1.25.3) - 2025-12-17 +## [1.25.3](https://github.com/go-gitea/gitea/releases/tag/v1.25.3) - 2025-12-17 * SECURITY * Bump toolchain to go1.25.5, misc fixes (#36082) @@ -62,7 +594,7 @@ been added to each release, please refer to the [blog](https://blog.gitea.com). * Fix error handling in mailer and wiki services (#36041) (#36053) * Fix bugs when comparing and creating pull request (#36166) (#36144) -## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/1.25.2) - 2025-11-23 +## [1.25.2](https://github.com/go-gitea/gitea/releases/tag/v1.25.2) - 2025-11-23 * SECURITY * Upgrade golang.org/x/crypto to 0.45.0 (#35985) (#35988) diff --git a/package/gitea/source/CLAUDE.md b/package/gitea/source/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/package/gitea/source/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/package/gitea/source/CONTRIBUTING.md b/package/gitea/source/CONTRIBUTING.md index 96e05c57..4b32631d 100644 --- a/package/gitea/source/CONTRIBUTING.md +++ b/package/gitea/source/CONTRIBUTING.md @@ -4,6 +4,7 @@ - [Contribution Guidelines](#contribution-guidelines) - [Introduction](#introduction) + - [AI Contribution Policy](#ai-contribution-policy) - [Issues](#issues) - [How to report issues](#how-to-report-issues) - [Types of issues](#types-of-issues) @@ -67,6 +68,21 @@ Sensitive security-related issues should be reported to [security@gitea.io](mail For configuring IDEs for Gitea development, see the [contributed IDE configurations](contrib/ide/). +## AI Contribution Policy + +Contributions made with the assistance of AI tools are welcome, but contributors must use them responsibly and disclose that use clearly. + +1. Review AI-generated code closely before marking a pull request ready for review. +2. Manually test the changes and add appropriate automated tests where feasible. +3. Only use AI to assist in contributions that you understand well enough to explain, defend, and revise yourself during review. +4. Disclose AI-assisted content clearly. +5. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model. +6. AI may be used to help draft issues and pull requests, but contributors remain responsible for the accuracy, completeness, and intent of what they submit. + +Maintainers reserve the right to close pull requests and issues that do not disclose AI assistance, that appear to be low-quality AI-generated content, or where the contributor cannot explain or defend the proposed changes themselves. + +We welcome new contributors, but cannot sustain the effort of supporting contributors who primarily defer to AI rather than engaging substantively with the review process. + ## Issues ### How to report issues @@ -80,7 +96,7 @@ The more detailed and specific you are, the faster we can fix the issue. \ It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. , as perhaps your problem has already been fixed on a current version. \ Please follow the guidelines described in [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) for your report. -Please be kind, remember that Gitea comes at no cost to you, and you're getting free help. +Please be kind—remember that Gitea comes at no cost to you, and you're getting free help. ### Types of issues @@ -166,24 +182,32 @@ Here's how to run the test suite: - code lint -| | | -| :-------------------- | :---------------------------------------------------------------- | +| | | +| :-------------------- | :--------------------------------------------------------------------------- | |``make lint`` | lint everything (not needed if you only change the front- **or** backend) | -|``make lint-frontend`` | lint frontend files | -|``make lint-backend`` | lint backend files | +|``make lint-frontend`` | lint frontend files | +|``make lint-backend`` | lint backend files | - run tests (we suggest running them on Linux) -| Command | Action | | -| :------------------------------------- | :----------------------------------------------- | ------------ | -|``make test[\#SpecificTestName]`` | run unit test(s) | | -|``make test-sqlite[\#SpecificTestName]``| run [integration](tests/integration) test(s) for SQLite |[More details](tests/integration/README.md) | -|``make test-e2e-sqlite[\#SpecificTestName]``| run [end-to-end](tests/e2e) test(s) for SQLite |[More details](tests/e2e/README.md) | +| Command | Action | | +| :------------------------------------------ | :------------------------------------------------------- | ------------------------------------------- | +|``make test[\#SpecificTestName]`` | run unit test(s) | | +|``make test-sqlite[\#SpecificTestName]`` | run [integration](tests/integration) test(s) for SQLite | [More details](tests/integration/README.md) | +|``make test-e2e`` | run [end-to-end](tests/e2e) test(s) using Playwright | | + +- E2E test environment variables + +| Variable | Description | +| :-------------------------------- | :---------------------------------------------------------- | +| ``GITEA_TEST_E2E_DEBUG`` | When set, show Gitea server output | +| ``GITEA_TEST_E2E_FLAGS`` | Additional flags passed to Playwright, for example ``--ui`` | +| ``GITEA_TEST_E2E_TIMEOUT_FACTOR`` | Timeout multiplier (default: 3 on CI, 1 locally) | ## Translation All translation work happens on [Crowdin](https://translate.gitea.com). -The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini). +The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.json). It is synced regularly with Crowdin. \ Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \ Once a language has reached a **satisfactory percentage** of translated keys (~25%), it will be synced back into this repo and included in the next released version. diff --git a/package/gitea/source/Dockerfile b/package/gitea/source/Dockerfile index 78a55649..323f0612 100644 --- a/package/gitea/source/Dockerfile +++ b/package/gitea/source/Dockerfile @@ -1,8 +1,15 @@ -# Build stage -FROM docker.io/library/golang:1.25-alpine3.22 AS build-env +# syntax=docker/dockerfile:1 +# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build +RUN apk --no-cache add build-base git nodejs pnpm +WORKDIR /src +COPY package.json pnpm-lock.yaml .npmrc ./ +RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile +COPY --exclude=.git/ . . +RUN make frontend -ARG GOPROXY -ENV GOPROXY=${GOPROXY:-direct} +# Build backend for each target platform +FROM docker.io/library/golang:1.26-alpine3.23 AS build-env ARG GITEA_VERSION ARG TAGS="sqlite sqlite_unlock_notify" @@ -12,37 +19,32 @@ ARG CGO_EXTRA_CFLAGS # Build deps RUN apk --no-cache add \ build-base \ - git \ - nodejs \ - npm \ - && npm install -g pnpm@10 \ - && rm -rf /var/cache/apk/* + git -# Setup repo -COPY . ${GOPATH}/src/code.gitea.io/gitea WORKDIR ${GOPATH}/src/code.gitea.io/gitea +COPY go.mod go.sum ./ +RUN go mod download +# Use COPY instead of bind mount as read-only one breaks makefile state tracking and read-write one needs binary to be moved as it's discarded. +# ".git" directory is mounted separately later only for version data extraction. +COPY --exclude=.git/ . . +COPY --from=frontend-build /src/public/assets public/assets -# Checkout version if set -RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ - && make clean-all build +# Build gitea, .git mount is required for version data +RUN --mount=type=cache,target="/root/.cache/go-build" \ + --mount=type=bind,source=".git/",target=".git/" \ + make backend -# Begin env-to-ini build -RUN go build contrib/environment-to-ini/environment-to-ini.go - -# Copy local files COPY docker/root /tmp/local -# Set permissions +# Set permissions for builds that made under windows which strips the executable bit from file RUN chmod 755 /tmp/local/usr/bin/entrypoint \ - /tmp/local/usr/local/bin/gitea \ + /tmp/local/usr/local/bin/* \ /tmp/local/etc/s6/gitea/* \ /tmp/local/etc/s6/openssh/* \ /tmp/local/etc/s6/.s6-svscan/* \ - /go/src/code.gitea.io/gitea/gitea \ - /go/src/code.gitea.io/gitea/environment-to-ini + /go/src/code.gitea.io/gitea/gitea -FROM docker.io/library/alpine:3.22 -LABEL maintainer="maintainers@gitea.io" +FROM docker.io/library/alpine:3.23 AS gitea EXPOSE 22 3000 @@ -57,8 +59,7 @@ RUN apk --no-cache add \ s6 \ sqlite \ su-exec \ - gnupg \ - && rm -rf /var/cache/apk/* + gnupg RUN addgroup \ -S -g 1000 \ @@ -72,14 +73,14 @@ RUN addgroup \ git && \ echo "git:*" | chpasswd -e +COPY --from=build-env /tmp/local / +COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea + ENV USER=git ENV GITEA_CUSTOM=/data/gitea VOLUME ["/data"] +# HINT: HEALTH-CHECK-ENDPOINT: don't use HEALTHCHECK, search this hint keyword for more information ENTRYPOINT ["/usr/bin/entrypoint"] CMD ["/usr/bin/s6-svscan", "/etc/s6"] - -COPY --from=build-env /tmp/local / -COPY --from=build-env /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea -COPY --from=build-env /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini diff --git a/package/gitea/source/Dockerfile.rootless b/package/gitea/source/Dockerfile.rootless index e83c1af3..83c69cbd 100644 --- a/package/gitea/source/Dockerfile.rootless +++ b/package/gitea/source/Dockerfile.rootless @@ -1,46 +1,45 @@ -# Build stage -FROM docker.io/library/golang:1.25-alpine3.22 AS build-env +# syntax=docker/dockerfile:1 +# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem +FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build +RUN apk --no-cache add build-base git nodejs pnpm +WORKDIR /src +COPY package.json pnpm-lock.yaml .npmrc ./ +RUN --mount=type=cache,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile +COPY --exclude=.git/ . . +RUN make frontend -ARG GOPROXY -ENV GOPROXY=${GOPROXY:-direct} +# Build backend for each target platform +FROM docker.io/library/golang:1.26-alpine3.23 AS build-env ARG GITEA_VERSION ARG TAGS="sqlite sqlite_unlock_notify" ENV TAGS="bindata timetzdata $TAGS" ARG CGO_EXTRA_CFLAGS -#Build deps +# Build deps RUN apk --no-cache add \ build-base \ - git \ - nodejs \ - npm \ - && npm install -g pnpm@10 \ - && rm -rf /var/cache/apk/* + git -# Setup repo -COPY . ${GOPATH}/src/code.gitea.io/gitea WORKDIR ${GOPATH}/src/code.gitea.io/gitea +COPY go.mod go.sum ./ +RUN go mod download +# See the comments in Dockerfile +COPY --exclude=.git/ . . +COPY --from=frontend-build /src/public/assets public/assets -# Checkout version if set -RUN if [ -n "${GITEA_VERSION}" ]; then git checkout "${GITEA_VERSION}"; fi \ - && make clean-all build +# Build gitea, .git mount is required for version data +RUN --mount=type=cache,target="/root/.cache/go-build" \ + --mount=type=bind,source=".git/",target=".git/" \ + make backend -# Begin env-to-ini build -RUN go build contrib/environment-to-ini/environment-to-ini.go - -# Copy local files COPY docker/rootless /tmp/local -# Set permissions -RUN chmod 755 /tmp/local/usr/local/bin/docker-entrypoint.sh \ - /tmp/local/usr/local/bin/docker-setup.sh \ - /tmp/local/usr/local/bin/gitea \ - /go/src/code.gitea.io/gitea/gitea \ - /go/src/code.gitea.io/gitea/environment-to-ini +# Set permissions for builds that made under windows which strips the executable bit from file +RUN chmod 755 /tmp/local/usr/local/bin/* \ + /go/src/code.gitea.io/gitea/gitea -FROM docker.io/library/alpine:3.22 -LABEL maintainer="maintainers@gitea.io" +FROM docker.io/library/alpine:3.23 AS gitea-rootless EXPOSE 2222 3000 @@ -52,8 +51,7 @@ RUN apk --no-cache add \ git \ curl \ gnupg \ - openssh-keygen \ - && rm -rf /var/cache/apk/* + openssh-keygen RUN addgroup \ -S -g 1000 \ @@ -71,7 +69,6 @@ RUN chown git:git /var/lib/gitea /etc/gitea COPY --from=build-env /tmp/local / COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/gitea /app/gitea/gitea -COPY --from=build-env --chown=root:root /go/src/code.gitea.io/gitea/environment-to-ini /usr/local/bin/environment-to-ini # git:git USER 1000:1000 @@ -86,5 +83,6 @@ ENV HOME="/var/lib/gitea/git" VOLUME ["/var/lib/gitea", "/etc/gitea"] WORKDIR /var/lib/gitea +# HINT: HEALTH-CHECK-ENDPOINT: don't use HEALTHCHECK, search this hint keyword for more information ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"] CMD [] diff --git a/package/gitea/source/MAINTAINERS b/package/gitea/source/MAINTAINERS index 1c7afc6f..26192eff 100644 --- a/package/gitea/source/MAINTAINERS +++ b/package/gitea/source/MAINTAINERS @@ -64,3 +64,4 @@ metiftikci (@metiftikci) Christopher Homberger (@ChristopherHX) Tobias Balle-Petersen (@tobiasbp) TheFox (@TheFox0x7) +Nicolas (@bircni) \ No newline at end of file diff --git a/package/gitea/source/Makefile b/package/gitea/source/Makefile index be3e723d..8c73dc35 100644 --- a/package/gitea/source/Makefile +++ b/package/gitea/source/Makefile @@ -1,22 +1,9 @@ -ifeq ($(USE_REPO_TEST_DIR),1) - -# This rule replaces the whole Makefile when we're trying to use /tmp repository temporary files -location = $(CURDIR)/$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)) -self := $(location) - -%: - @tmpdir=`mktemp --tmpdir -d` ; \ - echo Using temporary directory $$tmpdir for test repositories ; \ - USE_REPO_TEST_DIR= $(MAKE) -f $(self) --no-print-directory REPO_TEST_DIR=$$tmpdir/ $@ ; \ - STATUS=$$? ; rm -r "$$tmpdir" ; exit $$STATUS - -else - -# This is the "normal" part of the Makefile - DIST := dist DIST_DIRS := $(DIST)/binaries $(DIST)/release -IMPORT := code.gitea.io/gitea + +# By default use go's 1.25 experimental json v2 library when building +# TODO: remove when no longer experimental +export GOEXPERIMENT ?= jsonv2 GO ?= go SHASUM ?= shasum -a 256 @@ -27,17 +14,14 @@ XGO_VERSION := go-1.25.x AIR_PACKAGE ?= github.com/air-verse/air@v1 EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/v3/cmd/editorconfig-checker@v3 -GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.1 -GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 +GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.9.2 +GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15 -MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 -SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@717e3cb29becaaf00e56953556c6d80f8a01b286 +MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.8.0 +SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest -GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 -ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.8 -GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.20.0 -GOPLS_MODERNIZE_PACKAGE ?= golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@v0.20.0 +ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.11 DOCKER_IMAGE ?= gitea/gitea DOCKER_TAG ?= latest @@ -69,9 +53,11 @@ endif ifeq ($(IS_WINDOWS),yes) GOFLAGS := -v -buildmode=exe EXECUTABLE ?= gitea.exe + EXECUTABLE_E2E ?= gitea-e2e.exe else GOFLAGS := -v EXECUTABLE ?= gitea + EXECUTABLE_E2E ?= gitea-e2e endif ifeq ($(shell sed --version 2>/dev/null | grep -q GNU && echo gnu),gnu) @@ -82,7 +68,6 @@ endif EXTRA_GOFLAGS ?= -MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1) MAKE_EVIDENCE_DIR := .make_evidence GOTESTFLAGS ?= @@ -96,15 +81,6 @@ STORED_VERSION_FILE := VERSION GITHUB_REF_TYPE ?= branch GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) -# Enable typescript support in Node.js before 22.18 -# TODO: Remove this once we can raise the minimum Node.js version to 22.18 (alpine >= 3.23) -NODE_VERSION := $(shell printf "%03d%03d%03d" $(shell node -v 2>/dev/null | cut -c2- | tr '.' ' ')) -ifeq ($(shell test "$(NODE_VERSION)" -lt "022018000"; echo $$?),0) - NODE_VARS := NODE_OPTIONS="--experimental-strip-types" -else - NODE_VARS := -endif - ifneq ($(GITHUB_REF_TYPE),branch) VERSION ?= $(subst v,,$(GITHUB_REF_NAME)) GITEA_VERSION ?= $(VERSION) @@ -128,17 +104,18 @@ ifeq ($(VERSION),main) VERSION := main-nightly endif -LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" +LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64 -GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) +GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/)) MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) -WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f) -WEBPACK_CONFIGS := webpack.config.ts tailwind.config.ts -WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css -WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts +FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f) +FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts +FRONTEND_DEST := public/assets/.vite/manifest.json +FRONTEND_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/.vite +FRONTEND_DEV_LOG_LEVEL ?= warn BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.* @@ -148,7 +125,6 @@ SVG_DEST_DIR := public/assets/img/svg AIR_TMP_DIR := .air -GO_LICENSE_TMP_DIR := .go-licenses GO_LICENSE_FILE := assets/go-licenses.json TAGS ?= @@ -157,24 +133,21 @@ TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify -TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR) +TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) -GO_DIRS := build cmd models modules routers services tests +GO_DIRS := build cmd models modules routers services tests tools WEB_DIRS := web_src/js web_src/css ESLINT_FILES := web_src/js tools *.ts tests/e2e STYLELINT_FILES := web_src/css web_src/js/components/*.vue -SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.ini .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml)) $(filter-out tools/misspellings.csv, $(wildcard tools/*)) -EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.ini +SPELLCHECK_FILES := $(GO_DIRS) $(WEB_DIRS) templates options/locale/locale_en-US.json .github $(filter-out CHANGELOG.md, $(wildcard *.go *.md *.yml *.yaml *.toml)) +EDITORCONFIG_FILES := templates .github/workflows options/locale/locale_en-US.json GO_SOURCES := $(wildcard *.go) GO_SOURCES += $(shell find $(GO_DIRS) -type f -name "*.go") GO_SOURCES += $(GENERATED_GO_DEST) -# Force installation of playwright dependencies by setting this flag -ifdef DEPS_PLAYWRIGHT - PLAYWRIGHT_FLAGS += --with-deps -endif +ESLINT_CONCURRENCY ?= 2 SWAGGER_SPEC := templates/swagger/v1_json.tmpl SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json @@ -195,26 +168,20 @@ TEST_MSSQL_DBNAME ?= gitea TEST_MSSQL_USERNAME ?= sa TEST_MSSQL_PASSWORD ?= MwantsaSecurePassword1 +# Include local Makefile +# Makefile.local is listed in .gitignore +sinclude Makefile.local + .PHONY: all all: build .PHONY: help help: Makefile ## print Makefile help information. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m[TARGETS] default target: build\033[0m\n\n\033[35mTargets:\033[0m\n"} /^[0-9A-Za-z._-]+:.*?##/ { printf " \033[36m%-45s\033[0m %s\n", $$1, $$2 }' Makefile #$(MAKEFILE_LIST) - @printf " \033[36m%-46s\033[0m %s\n" "test-e2e[#TestSpecificName]" "test end to end using playwright" + @printf " \033[36m%-46s\033[0m %s\n" "test-e2e" "test end to end using playwright" @printf " \033[36m%-46s\033[0m %s\n" "test[#TestSpecificName]" "run unit test" @printf " \033[36m%-46s\033[0m %s\n" "test-sqlite[#TestSpecificName]" "run integration test for sqlite" -.PHONY: go-check -go-check: - $(eval MIN_GO_VERSION_STR := $(shell grep -Eo '^go\s+[0-9]+\.[0-9]+' go.mod | cut -d' ' -f2)) - $(eval MIN_GO_VERSION := $(shell printf "%03d%03d" $(shell echo '$(MIN_GO_VERSION_STR)' | tr '.' ' '))) - $(eval GO_VERSION := $(shell printf "%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9]+' | tr '.' ' ');)) - @if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \ - echo "Gitea requires Go $(MIN_GO_VERSION_STR) or greater to build. You can get it at https://go.dev/dl/"; \ - exit 1; \ - fi - .PHONY: git-check git-check: @if git lfs >/dev/null 2>&1 ; then : ; else \ @@ -222,39 +189,24 @@ git-check: exit 1; \ fi -.PHONY: node-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 PNPM_MISSING := $(shell hash pnpm > /dev/null 2>&1 || echo 1)) - @if [ "$(NODE_VERSION)" -lt "$(MIN_NODE_VERSION)" ]; then \ - echo "Gitea requires Node.js $(MIN_NODE_VERSION_STR) or greater to build. You can get it at https://nodejs.org/en/download/"; \ - exit 1; \ - fi - @if [ "$(PNPM_MISSING)" = "1" ]; then \ - echo "Gitea requires pnpm to build. You can install it at https://pnpm.io/installation"; \ - exit 1; \ - fi - .PHONY: clean-all clean-all: clean ## delete backend, frontend and integration files - rm -rf $(WEBPACK_DEST_ENTRIES) node_modules + rm -rf $(FRONTEND_DEST_ENTRIES) node_modules .PHONY: clean clean: ## delete backend and integration files - rm -rf $(EXECUTABLE) $(DIST) $(BINDATA_DEST_WILDCARD) \ + rm -rf $(EXECUTABLE) $(EXECUTABLE_E2E) $(DIST) $(BINDATA_DEST_WILDCARD) \ integrations*.test \ - e2e*.test \ tests/integration/gitea-integration-* \ tests/integration/indexers-* \ - tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \ + tests/sqlite.ini tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \ tests/e2e/gitea-e2e-*/ \ tests/e2e/indexers-*/ \ tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/ .PHONY: fmt fmt: ## format the Go and template code - @GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run build/code-batch-process.go gitea-fmt -w '{file-list}' + @GOFUMPT_PACKAGE=$(GOFUMPT_PACKAGE) $(GO) run tools/code-batch-process.go gitea-fmt -w '{file-list}' $(eval TEMPLATES := $(shell find templates -type f -name '*.tmpl')) @# strip whitespace after '{{' or '(' and before '}}' or ')' unless there is only @# whitespace before it @@ -272,19 +224,6 @@ fmt-check: fmt exit 1; \ fi -.PHONY: fix -fix: ## apply automated fixes to Go code - $(GO) run $(GOPLS_MODERNIZE_PACKAGE) -fix ./... - -.PHONY: fix-check -fix-check: fix - @diff=$$(git diff --color=always $(GO_SOURCES)); \ - if [ -n "$$diff" ]; then \ - echo "Please run 'make fix' and commit the result:"; \ - printf "%s" "$${diff}"; \ - exit 1; \ - fi - .PHONY: $(TAGS_EVIDENCE) $(TAGS_EVIDENCE): @mkdir -p $(MAKE_EVIDENCE_DIR) @@ -324,7 +263,7 @@ checks: checks-frontend checks-backend ## run various consistency checks checks-frontend: lockfile-check svg-check ## check frontend files .PHONY: checks-backend -checks-backend: tidy-check swagger-check fmt-check fix-check swagger-validate security-check ## check backend files +checks-backend: tidy-check swagger-check fmt-check swagger-validate security-check ## check backend files .PHONY: lint lint: lint-frontend lint-backend lint-spell ## lint everything @@ -339,44 +278,48 @@ lint-frontend: lint-js lint-css ## lint frontend files lint-frontend-fix: lint-js-fix lint-css-fix ## lint frontend files and fix issues .PHONY: lint-backend -lint-backend: lint-go lint-go-gitea-vet lint-go-gopls lint-editorconfig ## lint backend files +lint-backend: lint-go lint-go-gitea-vet lint-editorconfig ## lint backend files .PHONY: lint-backend-fix lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues .PHONY: lint-js -lint-js: node_modules ## lint js files - $(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES) - $(NODE_VARS) pnpm exec vue-tsc +lint-js: node_modules ## lint js and ts files + pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES) + pnpm exec vue-tsc .PHONY: lint-js-fix -lint-js-fix: node_modules ## lint js files and fix issues - $(NODE_VARS) pnpm exec eslint --color --max-warnings=0 --flag unstable_native_nodejs_ts_config $(ESLINT_FILES) --fix - $(NODE_VARS) pnpm exec vue-tsc +lint-js-fix: node_modules ## lint js and ts files and fix issues + pnpm exec eslint --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) $(ESLINT_FILES) --fix + pnpm exec vue-tsc .PHONY: lint-css lint-css: node_modules ## lint css files - $(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) + pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) .PHONY: lint-css-fix lint-css-fix: node_modules ## lint css files and fix issues - $(NODE_VARS) pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix + pnpm exec stylelint --color --max-warnings=0 $(STYLELINT_FILES) --fix .PHONY: lint-swagger lint-swagger: node_modules ## lint swagger files - $(NODE_VARS) pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC) + pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC) .PHONY: lint-md lint-md: node_modules ## lint markdown files - $(NODE_VARS) pnpm exec markdownlint *.md + pnpm exec markdownlint *.md + +.PHONY: lint-md-fix +lint-md-fix: node_modules ## lint markdown files and fix issues + pnpm exec markdownlint --fix *.md .PHONY: lint-spell lint-spell: ## lint spelling - @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -error $(SPELLCHECK_FILES) + @git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error .PHONY: lint-spell-fix lint-spell-fix: ## lint spelling and fix issues - @go run $(MISSPELL_PACKAGE) -dict tools/misspellings.csv -w $(SPELLCHECK_FILES) + @git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -w .PHONY: lint-go lint-go: ## lint go files @@ -396,13 +339,7 @@ lint-go-windows: .PHONY: lint-go-gitea-vet lint-go-gitea-vet: ## lint go files with gitea-vet @echo "Running gitea-vet..." - @GOOS= GOARCH= $(GO) build code.gitea.io/gitea-vet - @$(GO) vet -vettool=gitea-vet ./... - -.PHONY: lint-go-gopls -lint-go-gopls: ## lint go files with gopls - @echo "Running gopls check..." - @GO=$(GO) GOPLS_PACKAGE=$(GOPLS_PACKAGE) tools/lint-go-gopls.sh $(GO_SOURCES) + @$(GO) vet -vettool="$(shell GOOS= GOARCH= go tool -n gitea-vet)" ./... .PHONY: lint-editorconfig lint-editorconfig: @@ -422,17 +359,24 @@ lint-templates: .venv node_modules ## lint template files lint-yaml: .venv ## lint yaml files @uv run --frozen yamllint -s . +.PHONY: lint-json +lint-json: node_modules ## lint json files + pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) + +.PHONY: lint-json-fix +lint-json-fix: node_modules ## lint and fix json files + pnpm exec eslint -c eslint.json.config.ts --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) --fix + .PHONY: watch watch: ## watch everything and continuously rebuild @bash tools/watch.sh .PHONY: watch-frontend -watch-frontend: node-check node_modules ## watch frontend files and continuously rebuild - @rm -rf $(WEBPACK_DEST_ENTRIES) - NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret +watch-frontend: node_modules ## start vite dev server for frontend + NODE_ENV=development pnpm exec vite --logLevel $(FRONTEND_DEV_LOG_LEVEL) .PHONY: watch-backend -watch-backend: go-check ## watch backend files and continuously rebuild +watch-backend: ## watch backend files and continuously rebuild GITEA_RUN_MODE=dev $(GO) run $(AIR_PACKAGE) -c .air.toml .PHONY: test @@ -445,7 +389,7 @@ test-backend: ## test backend files .PHONY: test-frontend test-frontend: node_modules ## test frontend files - $(NODE_VARS) pnpm exec vitest + pnpm exec vitest .PHONY: test-check test-check: @@ -468,7 +412,7 @@ test\#%: coverage: grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' coverage.out > coverage-bodged.out grep '^\(mode: .*\)\|\(.*:[0-9]\+\.[0-9]\+,[0-9]\+\.[0-9]\+ [0-9]\+ [0-9]\+\)$$' integration.coverage.out > integration.coverage-bodged.out - $(GO) run build/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all + $(GO) run tools/gocovmerge.go integration.coverage-bodged.out coverage-bodged.out > coverage.all .PHONY: unit-test-coverage unit-test-coverage: @@ -498,25 +442,20 @@ tidy-check: tidy go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses $(GO_LICENSE_FILE): go.mod go.sum - @rm -rf $(GO_LICENSE_FILE) - $(GO) install $(GO_LICENSES_PACKAGE) - -GOOS=linux CGO_ENABLED=1 go-licenses save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null - $(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE) - @rm -rf $(GO_LICENSE_TMP_DIR) + GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE) generate-ini-sqlite: - sed -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \ + sed -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-sqlite|g' \ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ - -e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \ tests/sqlite.ini.tmpl > tests/sqlite.ini .PHONY: test-sqlite test-sqlite: integrations.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test + GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test .PHONY: test-sqlite\#% test-sqlite\#%: integrations.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*) + GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*) .PHONY: test-sqlite-migration test-sqlite-migration: migrations.sqlite.test migrations.individual.sqlite.test @@ -526,18 +465,17 @@ generate-ini-mysql: -e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \ -e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \ -e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \ - -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \ + -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mysql|g' \ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ - -e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \ tests/mysql.ini.tmpl > tests/mysql.ini .PHONY: test-mysql test-mysql: integrations.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test + GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test .PHONY: test-mysql\#% test-mysql\#%: integrations.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*) + GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*) .PHONY: test-mysql-migration test-mysql-migration: migrations.mysql.test migrations.individual.mysql.test @@ -549,18 +487,17 @@ generate-ini-pgsql: -e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \ -e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \ -e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \ - -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \ + -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-pgsql|g' \ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ - -e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \ tests/pgsql.ini.tmpl > tests/pgsql.ini .PHONY: test-pgsql test-pgsql: integrations.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test + GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test .PHONY: test-pgsql\#% test-pgsql\#%: integrations.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*) + GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*) .PHONY: test-pgsql-migration test-pgsql-migration: migrations.pgsql.test migrations.individual.pgsql.test @@ -570,89 +507,53 @@ generate-ini-mssql: -e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \ -e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \ -e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \ - -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \ + -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mssql|g' \ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ - -e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \ tests/mssql.ini.tmpl > tests/mssql.ini .PHONY: test-mssql test-mssql: integrations.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test + GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test .PHONY: test-mssql\#% test-mssql\#%: integrations.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*) + GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*) .PHONY: test-mssql-migration test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test .PHONY: playwright playwright: deps-frontend - $(NODE_VARS) pnpm exec playwright install $(PLAYWRIGHT_FLAGS) - -.PHONY: test-e2e% -test-e2e%: TEST_TYPE ?= e2e - # Clear display env variable. Otherwise, chromium tests can fail. - DISPLAY= + @# on GitHub Actions VMs, playwright's system deps are pre-installed + @pnpm exec playwright install $(if $(GITHUB_ACTIONS),,--with-deps) chromium firefox $(PLAYWRIGHT_FLAGS) .PHONY: test-e2e -test-e2e: test-e2e-sqlite - -.PHONY: test-e2e-sqlite -test-e2e-sqlite: playwright e2e.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test - -.PHONY: test-e2e-sqlite\#% -test-e2e-sqlite\#%: playwright e2e.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestE2e/$* - -.PHONY: test-e2e-mysql -test-e2e-mysql: playwright e2e.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test - -.PHONY: test-e2e-mysql\#% -test-e2e-mysql\#%: playwright e2e.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test -test.run TestE2e/$* - -.PHONY: test-e2e-pgsql -test-e2e-pgsql: playwright e2e.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test - -.PHONY: test-e2e-pgsql\#% -test-e2e-pgsql\#%: playwright e2e.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test -test.run TestE2e/$* - -.PHONY: test-e2e-mssql -test-e2e-mssql: playwright e2e.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test - -.PHONY: test-e2e-mssql\#% -test-e2e-mssql\#%: playwright e2e.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test -test.run TestE2e/$* +test-e2e: playwright $(EXECUTABLE_E2E) + @EXECUTABLE=$(EXECUTABLE_E2E) ./tools/test-e2e.sh $(GITEA_TEST_E2E_FLAGS) .PHONY: bench-sqlite bench-sqlite: integrations.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . + GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: bench-mysql bench-mysql: integrations.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . + GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: bench-mssql bench-mssql: integrations.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . + GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: bench-pgsql bench-pgsql: integrations.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . + GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: integration-test-coverage integration-test-coverage: integrations.cover.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out + GITEA_TEST_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out .PHONY: integration-test-coverage-sqlite integration-test-coverage-sqlite: integrations.cover.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out + GITEA_TEST_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out integrations.mysql.test: git-check $(GO_SOURCES) $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql.test @@ -675,66 +576,54 @@ integrations.cover.sqlite.test: git-check $(GO_SOURCES) .PHONY: migrations.mysql.test migrations.mysql.test: $(GO_SOURCES) generate-ini-mysql $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql.test - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./migrations.mysql.test + GITEA_TEST_CONF=tests/mysql.ini ./migrations.mysql.test .PHONY: migrations.pgsql.test migrations.pgsql.test: $(GO_SOURCES) generate-ini-pgsql $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.pgsql.test - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./migrations.pgsql.test + GITEA_TEST_CONF=tests/pgsql.ini ./migrations.pgsql.test .PHONY: migrations.mssql.test migrations.mssql.test: $(GO_SOURCES) generate-ini-mssql $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mssql.test - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./migrations.mssql.test + GITEA_TEST_CONF=tests/mssql.ini ./migrations.mssql.test .PHONY: migrations.sqlite.test migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)' - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./migrations.sqlite.test + GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.test .PHONY: migrations.individual.mysql.test -migrations.individual.mysql.test: $(GO_SOURCES) - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) +migrations.individual.mysql.test: $(GO_SOURCES) generate-ini-mysql + GITEA_TEST_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.sqlite.test\#% migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* + GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* .PHONY: migrations.individual.pgsql.test -migrations.individual.pgsql.test: $(GO_SOURCES) - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) +migrations.individual.pgsql.test: $(GO_SOURCES) generate-ini-pgsql + GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.pgsql.test\#% migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* + GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* .PHONY: migrations.individual.mssql.test migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) + GITEA_TEST_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.mssql.test\#% migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* + GITEA_TEST_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* .PHONY: migrations.individual.sqlite.test migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) + GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.sqlite.test\#% migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* - -e2e.mysql.test: $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mysql.test - -e2e.pgsql.test: $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.pgsql.test - -e2e.mssql.test: $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mssql.test - -e2e.sqlite.test: $(GO_SOURCES) - $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.sqlite.test -tags '$(TEST_TAGS)' + GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* .PHONY: check check: test @@ -747,10 +636,10 @@ install: $(wildcard *.go) build: frontend backend ## build everything .PHONY: frontend -frontend: $(WEBPACK_DEST) ## build frontend files +frontend: $(FRONTEND_DEST) ## build frontend files .PHONY: backend -backend: go-check generate-backend $(EXECUTABLE) ## build backend files +backend: generate-backend $(EXECUTABLE) ## build backend files # We generate the backend before the frontend in case we in future we want to generate things in the frontend from generated files in backend .PHONY: generate @@ -766,14 +655,17 @@ generate-go: $(TAGS_PREREQ) .PHONY: security-check security-check: - go run $(GOVULNCHECK_PACKAGE) -show color ./... + GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... || true $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) - $(error pam support set via TAGS doesn't support static builds) + $(error pam support set via TAGS does not support static builds) endif CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ +$(EXECUTABLE_E2E): $(GO_SOURCES) $(FRONTEND_DEST) + CGO_ENABLED=1 $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TEST_TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ + .PHONY: release release: frontend generate release-windows release-linux release-darwin release-freebsd release-copy release-compress vendor release-sources release-check @@ -844,15 +736,12 @@ deps-tools: ## install tool dependencies $(GO) install $(MISSPELL_PACKAGE) & \ $(GO) install $(SWAGGER_PACKAGE) & \ $(GO) install $(XGO_PACKAGE) & \ - $(GO) install $(GO_LICENSES_PACKAGE) & \ $(GO) install $(GOVULNCHECK_PACKAGE) & \ $(GO) install $(ACTIONLINT_PACKAGE) & \ - $(GO) install $(GOPLS_PACKAGE) & \ - $(GO) install $(GOPLS_MODERNIZE_PACKAGE) & \ wait node_modules: pnpm-lock.yaml - $(NODE_VARS) pnpm install --frozen-lockfile + pnpm install --frozen-lockfile @touch node_modules .venv: uv.lock @@ -860,36 +749,41 @@ node_modules: pnpm-lock.yaml @touch .venv .PHONY: update -update: update-js update-py ## update js and py dependencies +update: update-go update-js update-py ## update dependencies + +.PHONY: update-go +update-go: ## update go dependencies + $(GO) get -u ./... + $(MAKE) tidy .PHONY: update-js -update-js: node-check | node_modules ## update js dependencies - $(NODE_VARS) pnpm exec updates -u -f package.json +update-js: node_modules ## update js dependencies + pnpm exec updates -u -f package.json rm -rf node_modules pnpm-lock.yaml - $(NODE_VARS) pnpm install - $(NODE_VARS) pnpm exec nolyfill install - $(NODE_VARS) pnpm install + pnpm install + pnpm exec nolyfill install + pnpm install @touch node_modules .PHONY: update-py -update-py: node-check | node_modules ## update py dependencies - $(NODE_VARS) pnpm exec updates -u -f pyproject.toml +update-py: node_modules ## update py dependencies + pnpm exec updates -u -f pyproject.toml rm -rf .venv uv.lock uv sync @touch .venv -.PHONY: webpack -webpack: $(WEBPACK_DEST) ## build webpack files +.PHONY: vite +vite: $(FRONTEND_DEST) ## build vite files -$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml - @$(MAKE) -s node-check node_modules - @rm -rf $(WEBPACK_DEST_ENTRIES) - @echo "Running webpack..." - @BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret - @touch $(WEBPACK_DEST) +$(FRONTEND_DEST): $(FRONTEND_SOURCES) $(FRONTEND_CONFIGS) pnpm-lock.yaml + @$(MAKE) -s node_modules + @rm -rf $(FRONTEND_DEST_ENTRIES) + @echo "Running vite build..." + @pnpm exec vite build + @touch $(FRONTEND_DEST) .PHONY: svg -svg: node-check | node_modules ## build svg files +svg: node_modules ## build svg files rm -rf $(SVG_DEST_DIR) node tools/generate-svg.ts @@ -905,7 +799,7 @@ svg-check: svg .PHONY: lockfile-check lockfile-check: - $(NODE_VARS) pnpm install --frozen-lockfile + pnpm install --frozen-lockfile @diff=$$(git diff --color=always pnpm-lock.yaml); \ if [ -n "$$diff" ]; then \ echo "pnpm-lock.yaml is inconsistent with package.json"; \ @@ -914,16 +808,6 @@ lockfile-check: exit 1; \ fi -.PHONY: update-translations -update-translations: - mkdir -p ./translations - cd ./translations && curl -L https://crowdin.com/download/project/gitea.zip > gitea.zip && unzip gitea.zip - rm ./translations/gitea.zip - $(SED_INPLACE) -e 's/="/=/g' -e 's/"$$//g' ./translations/*.ini - $(SED_INPLACE) -e 's/\\"/"/g' ./translations/*.ini - mv ./translations/*.ini ./options/locale/ - rmdir ./translations - .PHONY: generate-gitignore generate-gitignore: ## update gitignore files $(GO) run build/generate-gitignores.go @@ -945,9 +829,6 @@ docker: docker build --disable-content-trust=false -t $(DOCKER_REF) . # support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" . -# This endif closes the if at the top of the file -endif - # Disable parallel execution because it would break some targets that don't # specify exact dependencies like 'backend' which does currently not depend # on 'frontend' to enable Node.js-less builds from source tarballs. diff --git a/package/gitea/source/README.md b/package/gitea/source/README.md index ed000971..7ebeac97 100644 --- a/package/gitea/source/README.md +++ b/package/gitea/source/README.md @@ -8,7 +8,6 @@ [![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md) diff --git a/package/gitea/source/README.zh-cn.md b/package/gitea/source/README.zh-cn.md index f34b25b9..8ccacc0f 100644 --- a/package/gitea/source/README.zh-cn.md +++ b/package/gitea/source/README.zh-cn.md @@ -8,7 +8,6 @@ [![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [English](./README.md) | [繁體中文](./README.zh-tw.md) @@ -46,7 +45,7 @@ `build` 目标分为两个子目标: - `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。 -- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。 +- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本以及 [pnpm](https://pnpm.io/installation)。 需要互联网连接来下载 go 和 npm 模块。从包含预构建前端文件的官方源代码压缩包构建时,不会触发 `frontend` 目标,因此可以在没有 Node.js 的情况下构建。 diff --git a/package/gitea/source/README.zh-tw.md b/package/gitea/source/README.zh-tw.md index 9de3f85d..4160fd0b 100644 --- a/package/gitea/source/README.zh-tw.md +++ b/package/gitea/source/README.zh-tw.md @@ -8,7 +8,6 @@ [![](https://www.codetriage.com/go-gitea/gitea/badges/users.svg)](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source") [![](https://opencollective.com/gitea/tiers/backers/badge.svg?label=backers&color=brightgreen)](https://opencollective.com/gitea "Become a backer/sponsor of gitea") [![](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT "License: MIT") -[![Contribute with Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod&color=green)](https://gitpod.io/#https://github.com/go-gitea/gitea) [![](https://badges.crowdin.net/gitea/localized.svg)](https://translate.gitea.com "Crowdin") [English](./README.md) | [简体中文](./README.zh-cn.md) @@ -46,7 +45,7 @@ `build` 目標分為兩個子目標: - `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。 -- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。 +- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本以及 [pnpm](https://pnpm.io/installation)。 需要互聯網連接來下載 go 和 npm 模塊。從包含預構建前端文件的官方源代碼壓縮包構建時,不會觸發 `frontend` 目標,因此可以在沒有 Node.js 的情況下構建。 diff --git a/package/gitea/source/assets/emoji.json b/package/gitea/source/assets/emoji.json index 28244caa..2d6329e4 100644 --- a/package/gitea/source/assets/emoji.json +++ b/package/gitea/source/assets/emoji.json @@ -1 +1,11483 @@ -[{"emoji":"👍","aliases":["+1","thumbsup"]},{"emoji":"👎","aliases":["-1","thumbsdown"]},{"emoji":"💯","aliases":["100"]},{"emoji":"🔢","aliases":["1234"]},{"emoji":"🥇","aliases":["1st_place_medal"]},{"emoji":"🥈","aliases":["2nd_place_medal"]},{"emoji":"🥉","aliases":["3rd_place_medal"]},{"emoji":"🎱","aliases":["8ball"]},{"emoji":"🅰️","aliases":["a"]},{"emoji":"🆎","aliases":["ab"]},{"emoji":"🧮","aliases":["abacus"]},{"emoji":"🔤","aliases":["abc"]},{"emoji":"🔡","aliases":["abcd"]},{"emoji":"🉑","aliases":["accept"]},{"emoji":"🪗","aliases":["accordion"]},{"emoji":"🩹","aliases":["adhesive_bandage"]},{"emoji":"🧑","aliases":["adult"]},{"emoji":"🚡","aliases":["aerial_tramway"]},{"emoji":"🇦🇫","aliases":["afghanistan"]},{"emoji":"✈️","aliases":["airplane"]},{"emoji":"🇦🇽","aliases":["aland_islands"]},{"emoji":"⏰","aliases":["alarm_clock"]},{"emoji":"🇦🇱","aliases":["albania"]},{"emoji":"⚗️","aliases":["alembic"]},{"emoji":"🇩🇿","aliases":["algeria"]},{"emoji":"👽","aliases":["alien"]},{"emoji":"🚑","aliases":["ambulance"]},{"emoji":"🇦🇸","aliases":["american_samoa"]},{"emoji":"🏺","aliases":["amphora"]},{"emoji":"🫀","aliases":["anatomical_heart"]},{"emoji":"⚓","aliases":["anchor"]},{"emoji":"🇦🇩","aliases":["andorra"]},{"emoji":"👼","aliases":["angel"]},{"emoji":"💢","aliases":["anger"]},{"emoji":"🇦🇴","aliases":["angola"]},{"emoji":"😠","aliases":["angry"]},{"emoji":"🇦🇮","aliases":["anguilla"]},{"emoji":"😧","aliases":["anguished"]},{"emoji":"🐜","aliases":["ant"]},{"emoji":"🇦🇶","aliases":["antarctica"]},{"emoji":"🇦🇬","aliases":["antigua_barbuda"]},{"emoji":"🍎","aliases":["apple"]},{"emoji":"♒","aliases":["aquarius"]},{"emoji":"🇦🇷","aliases":["argentina"]},{"emoji":"♈","aliases":["aries"]},{"emoji":"🇦🇲","aliases":["armenia"]},{"emoji":"◀️","aliases":["arrow_backward"]},{"emoji":"⏬","aliases":["arrow_double_down"]},{"emoji":"⏫","aliases":["arrow_double_up"]},{"emoji":"⬇️","aliases":["arrow_down"]},{"emoji":"🔽","aliases":["arrow_down_small"]},{"emoji":"▶️","aliases":["arrow_forward"]},{"emoji":"⤵️","aliases":["arrow_heading_down"]},{"emoji":"⤴️","aliases":["arrow_heading_up"]},{"emoji":"⬅️","aliases":["arrow_left"]},{"emoji":"↙️","aliases":["arrow_lower_left"]},{"emoji":"↘️","aliases":["arrow_lower_right"]},{"emoji":"➡️","aliases":["arrow_right"]},{"emoji":"↪️","aliases":["arrow_right_hook"]},{"emoji":"⬆️","aliases":["arrow_up"]},{"emoji":"↕️","aliases":["arrow_up_down"]},{"emoji":"🔼","aliases":["arrow_up_small"]},{"emoji":"↖️","aliases":["arrow_upper_left"]},{"emoji":"↗️","aliases":["arrow_upper_right"]},{"emoji":"🔃","aliases":["arrows_clockwise"]},{"emoji":"🔄","aliases":["arrows_counterclockwise"]},{"emoji":"🎨","aliases":["art"]},{"emoji":"🚛","aliases":["articulated_lorry"]},{"emoji":"🛰️","aliases":["artificial_satellite"]},{"emoji":"🧑‍🎨","aliases":["artist"]},{"emoji":"🇦🇼","aliases":["aruba"]},{"emoji":"🇦🇨","aliases":["ascension_island"]},{"emoji":"*️⃣","aliases":["asterisk"]},{"emoji":"😲","aliases":["astonished"]},{"emoji":"🧑‍🚀","aliases":["astronaut"]},{"emoji":"👟","aliases":["athletic_shoe"]},{"emoji":"🏧","aliases":["atm"]},{"emoji":"⚛️","aliases":["atom_symbol"]},{"emoji":"🇦🇺","aliases":["australia"]},{"emoji":"🇦🇹","aliases":["austria"]},{"emoji":"🛺","aliases":["auto_rickshaw"]},{"emoji":"🥑","aliases":["avocado"]},{"emoji":"🪓","aliases":["axe"]},{"emoji":"🇦🇿","aliases":["azerbaijan"]},{"emoji":"🅱️","aliases":["b"]},{"emoji":"👶","aliases":["baby"]},{"emoji":"🍼","aliases":["baby_bottle"]},{"emoji":"🐤","aliases":["baby_chick"]},{"emoji":"🚼","aliases":["baby_symbol"]},{"emoji":"🔙","aliases":["back"]},{"emoji":"🥓","aliases":["bacon"]},{"emoji":"🦡","aliases":["badger"]},{"emoji":"🏸","aliases":["badminton"]},{"emoji":"🥯","aliases":["bagel"]},{"emoji":"🛄","aliases":["baggage_claim"]},{"emoji":"🥖","aliases":["baguette_bread"]},{"emoji":"🇧🇸","aliases":["bahamas"]},{"emoji":"🇧🇭","aliases":["bahrain"]},{"emoji":"⚖️","aliases":["balance_scale"]},{"emoji":"👨‍🦲","aliases":["bald_man"]},{"emoji":"👩‍🦲","aliases":["bald_woman"]},{"emoji":"🩰","aliases":["ballet_shoes"]},{"emoji":"🎈","aliases":["balloon"]},{"emoji":"🗳️","aliases":["ballot_box"]},{"emoji":"☑️","aliases":["ballot_box_with_check"]},{"emoji":"🎍","aliases":["bamboo"]},{"emoji":"🍌","aliases":["banana"]},{"emoji":"‼️","aliases":["bangbang"]},{"emoji":"🇧🇩","aliases":["bangladesh"]},{"emoji":"🪕","aliases":["banjo"]},{"emoji":"🏦","aliases":["bank"]},{"emoji":"📊","aliases":["bar_chart"]},{"emoji":"🇧🇧","aliases":["barbados"]},{"emoji":"💈","aliases":["barber"]},{"emoji":"⚾","aliases":["baseball"]},{"emoji":"🧺","aliases":["basket"]},{"emoji":"🏀","aliases":["basketball"]},{"emoji":"🦇","aliases":["bat"]},{"emoji":"🛀","aliases":["bath"]},{"emoji":"🛁","aliases":["bathtub"]},{"emoji":"🔋","aliases":["battery"]},{"emoji":"🏖️","aliases":["beach_umbrella"]},{"emoji":"🫘","aliases":["beans"]},{"emoji":"🐻","aliases":["bear"]},{"emoji":"🧔","aliases":["bearded_person"]},{"emoji":"🦫","aliases":["beaver"]},{"emoji":"🛏️","aliases":["bed"]},{"emoji":"🐝","aliases":["bee","honeybee"]},{"emoji":"🍺","aliases":["beer"]},{"emoji":"🍻","aliases":["beers"]},{"emoji":"🪲","aliases":["beetle"]},{"emoji":"🔰","aliases":["beginner"]},{"emoji":"🇧🇾","aliases":["belarus"]},{"emoji":"🇧🇪","aliases":["belgium"]},{"emoji":"🇧🇿","aliases":["belize"]},{"emoji":"🔔","aliases":["bell"]},{"emoji":"🫑","aliases":["bell_pepper"]},{"emoji":"🛎️","aliases":["bellhop_bell"]},{"emoji":"🇧🇯","aliases":["benin"]},{"emoji":"🍱","aliases":["bento"]},{"emoji":"🇧🇲","aliases":["bermuda"]},{"emoji":"🧃","aliases":["beverage_box"]},{"emoji":"🇧🇹","aliases":["bhutan"]},{"emoji":"🚴","aliases":["bicyclist"]},{"emoji":"🚲","aliases":["bike"]},{"emoji":"🚴‍♂️","aliases":["biking_man"]},{"emoji":"🚴‍♀️","aliases":["biking_woman"]},{"emoji":"👙","aliases":["bikini"]},{"emoji":"🧢","aliases":["billed_cap"]},{"emoji":"☣️","aliases":["biohazard"]},{"emoji":"🐦","aliases":["bird"]},{"emoji":"🎂","aliases":["birthday"]},{"emoji":"🦬","aliases":["bison"]},{"emoji":"🫦","aliases":["biting_lip"]},{"emoji":"🐦‍⬛","aliases":["black_bird"]},{"emoji":"🐈‍⬛","aliases":["black_cat"]},{"emoji":"⚫","aliases":["black_circle"]},{"emoji":"🏴","aliases":["black_flag"]},{"emoji":"🖤","aliases":["black_heart"]},{"emoji":"🃏","aliases":["black_joker"]},{"emoji":"⬛","aliases":["black_large_square"]},{"emoji":"◾","aliases":["black_medium_small_square"]},{"emoji":"◼️","aliases":["black_medium_square"]},{"emoji":"✒️","aliases":["black_nib"]},{"emoji":"▪️","aliases":["black_small_square"]},{"emoji":"🔲","aliases":["black_square_button"]},{"emoji":"👱‍♂️","aliases":["blond_haired_man"]},{"emoji":"👱","aliases":["blond_haired_person"]},{"emoji":"👱‍♀️","aliases":["blond_haired_woman","blonde_woman"]},{"emoji":"🌼","aliases":["blossom"]},{"emoji":"🐡","aliases":["blowfish"]},{"emoji":"📘","aliases":["blue_book"]},{"emoji":"🚙","aliases":["blue_car"]},{"emoji":"💙","aliases":["blue_heart"]},{"emoji":"🟦","aliases":["blue_square"]},{"emoji":"🫐","aliases":["blueberries"]},{"emoji":"😊","aliases":["blush"]},{"emoji":"🐗","aliases":["boar"]},{"emoji":"⛵","aliases":["boat","sailboat"]},{"emoji":"🇧🇴","aliases":["bolivia"]},{"emoji":"💣","aliases":["bomb"]},{"emoji":"🦴","aliases":["bone"]},{"emoji":"📖","aliases":["book","open_book"]},{"emoji":"🔖","aliases":["bookmark"]},{"emoji":"📑","aliases":["bookmark_tabs"]},{"emoji":"📚","aliases":["books"]},{"emoji":"💥","aliases":["boom","collision"]},{"emoji":"🪃","aliases":["boomerang"]},{"emoji":"👢","aliases":["boot"]},{"emoji":"🇧🇦","aliases":["bosnia_herzegovina"]},{"emoji":"🇧🇼","aliases":["botswana"]},{"emoji":"⛹️‍♂️","aliases":["bouncing_ball_man","basketball_man"]},{"emoji":"⛹️","aliases":["bouncing_ball_person"]},{"emoji":"⛹️‍♀️","aliases":["bouncing_ball_woman","basketball_woman"]},{"emoji":"💐","aliases":["bouquet"]},{"emoji":"🇧🇻","aliases":["bouvet_island"]},{"emoji":"🙇","aliases":["bow"]},{"emoji":"🏹","aliases":["bow_and_arrow"]},{"emoji":"🙇‍♂️","aliases":["bowing_man"]},{"emoji":"🙇‍♀️","aliases":["bowing_woman"]},{"emoji":"🥣","aliases":["bowl_with_spoon"]},{"emoji":"🎳","aliases":["bowling"]},{"emoji":"🥊","aliases":["boxing_glove"]},{"emoji":"👦","aliases":["boy"]},{"emoji":"🧠","aliases":["brain"]},{"emoji":"🇧🇷","aliases":["brazil"]},{"emoji":"🍞","aliases":["bread"]},{"emoji":"🤱","aliases":["breast_feeding"]},{"emoji":"🧱","aliases":["bricks"]},{"emoji":"🌉","aliases":["bridge_at_night"]},{"emoji":"💼","aliases":["briefcase"]},{"emoji":"🇮🇴","aliases":["british_indian_ocean_territory"]},{"emoji":"🇻🇬","aliases":["british_virgin_islands"]},{"emoji":"🥦","aliases":["broccoli"]},{"emoji":"💔","aliases":["broken_heart"]},{"emoji":"🧹","aliases":["broom"]},{"emoji":"🟤","aliases":["brown_circle"]},{"emoji":"🤎","aliases":["brown_heart"]},{"emoji":"🟫","aliases":["brown_square"]},{"emoji":"🇧🇳","aliases":["brunei"]},{"emoji":"🧋","aliases":["bubble_tea"]},{"emoji":"🫧","aliases":["bubbles"]},{"emoji":"🪣","aliases":["bucket"]},{"emoji":"🐛","aliases":["bug"]},{"emoji":"🏗️","aliases":["building_construction"]},{"emoji":"💡","aliases":["bulb"]},{"emoji":"🇧🇬","aliases":["bulgaria"]},{"emoji":"🚅","aliases":["bullettrain_front"]},{"emoji":"🚄","aliases":["bullettrain_side"]},{"emoji":"🇧🇫","aliases":["burkina_faso"]},{"emoji":"🌯","aliases":["burrito"]},{"emoji":"🇧🇮","aliases":["burundi"]},{"emoji":"🚌","aliases":["bus"]},{"emoji":"🕴️","aliases":["business_suit_levitating"]},{"emoji":"🚏","aliases":["busstop"]},{"emoji":"👤","aliases":["bust_in_silhouette"]},{"emoji":"👥","aliases":["busts_in_silhouette"]},{"emoji":"🧈","aliases":["butter"]},{"emoji":"🦋","aliases":["butterfly"]},{"emoji":"🌵","aliases":["cactus"]},{"emoji":"🍰","aliases":["cake"]},{"emoji":"📆","aliases":["calendar"]},{"emoji":"🤙","aliases":["call_me_hand"]},{"emoji":"📲","aliases":["calling"]},{"emoji":"🇰🇭","aliases":["cambodia"]},{"emoji":"🐫","aliases":["camel"]},{"emoji":"📷","aliases":["camera"]},{"emoji":"📸","aliases":["camera_flash"]},{"emoji":"🇨🇲","aliases":["cameroon"]},{"emoji":"🏕️","aliases":["camping"]},{"emoji":"🇨🇦","aliases":["canada"]},{"emoji":"🇮🇨","aliases":["canary_islands"]},{"emoji":"♋","aliases":["cancer"]},{"emoji":"🕯️","aliases":["candle"]},{"emoji":"🍬","aliases":["candy"]},{"emoji":"🥫","aliases":["canned_food"]},{"emoji":"🛶","aliases":["canoe"]},{"emoji":"🇨🇻","aliases":["cape_verde"]},{"emoji":"🔠","aliases":["capital_abcd"]},{"emoji":"♑","aliases":["capricorn"]},{"emoji":"🚗","aliases":["car","red_car"]},{"emoji":"🗃️","aliases":["card_file_box"]},{"emoji":"📇","aliases":["card_index"]},{"emoji":"🗂️","aliases":["card_index_dividers"]},{"emoji":"🇧🇶","aliases":["caribbean_netherlands"]},{"emoji":"🎠","aliases":["carousel_horse"]},{"emoji":"🪚","aliases":["carpentry_saw"]},{"emoji":"🥕","aliases":["carrot"]},{"emoji":"🤸","aliases":["cartwheeling"]},{"emoji":"🐱","aliases":["cat"]},{"emoji":"🐈","aliases":["cat2"]},{"emoji":"🇰🇾","aliases":["cayman_islands"]},{"emoji":"💿","aliases":["cd"]},{"emoji":"🇨🇫","aliases":["central_african_republic"]},{"emoji":"🇪🇦","aliases":["ceuta_melilla"]},{"emoji":"🇹🇩","aliases":["chad"]},{"emoji":"⛓️","aliases":["chains"]},{"emoji":"🪑","aliases":["chair"]},{"emoji":"🍾","aliases":["champagne"]},{"emoji":"💹","aliases":["chart"]},{"emoji":"📉","aliases":["chart_with_downwards_trend"]},{"emoji":"📈","aliases":["chart_with_upwards_trend"]},{"emoji":"🏁","aliases":["checkered_flag"]},{"emoji":"🧀","aliases":["cheese"]},{"emoji":"🍒","aliases":["cherries"]},{"emoji":"🌸","aliases":["cherry_blossom"]},{"emoji":"♟️","aliases":["chess_pawn"]},{"emoji":"🌰","aliases":["chestnut"]},{"emoji":"🐔","aliases":["chicken"]},{"emoji":"🧒","aliases":["child"]},{"emoji":"🚸","aliases":["children_crossing"]},{"emoji":"🇨🇱","aliases":["chile"]},{"emoji":"🐿️","aliases":["chipmunk"]},{"emoji":"🍫","aliases":["chocolate_bar"]},{"emoji":"🥢","aliases":["chopsticks"]},{"emoji":"🇨🇽","aliases":["christmas_island"]},{"emoji":"🎄","aliases":["christmas_tree"]},{"emoji":"⛪","aliases":["church"]},{"emoji":"🎦","aliases":["cinema"]},{"emoji":"🎪","aliases":["circus_tent"]},{"emoji":"🌇","aliases":["city_sunrise"]},{"emoji":"🌆","aliases":["city_sunset"]},{"emoji":"🏙️","aliases":["cityscape"]},{"emoji":"🆑","aliases":["cl"]},{"emoji":"🗜️","aliases":["clamp"]},{"emoji":"👏","aliases":["clap"]},{"emoji":"🎬","aliases":["clapper"]},{"emoji":"🏛️","aliases":["classical_building"]},{"emoji":"🧗","aliases":["climbing"]},{"emoji":"🧗‍♂️","aliases":["climbing_man"]},{"emoji":"🧗‍♀️","aliases":["climbing_woman"]},{"emoji":"🥂","aliases":["clinking_glasses"]},{"emoji":"📋","aliases":["clipboard"]},{"emoji":"🇨🇵","aliases":["clipperton_island"]},{"emoji":"🕐","aliases":["clock1"]},{"emoji":"🕙","aliases":["clock10"]},{"emoji":"🕥","aliases":["clock1030"]},{"emoji":"🕚","aliases":["clock11"]},{"emoji":"🕦","aliases":["clock1130"]},{"emoji":"🕛","aliases":["clock12"]},{"emoji":"🕧","aliases":["clock1230"]},{"emoji":"🕜","aliases":["clock130"]},{"emoji":"🕑","aliases":["clock2"]},{"emoji":"🕝","aliases":["clock230"]},{"emoji":"🕒","aliases":["clock3"]},{"emoji":"🕞","aliases":["clock330"]},{"emoji":"🕓","aliases":["clock4"]},{"emoji":"🕟","aliases":["clock430"]},{"emoji":"🕔","aliases":["clock5"]},{"emoji":"🕠","aliases":["clock530"]},{"emoji":"🕕","aliases":["clock6"]},{"emoji":"🕡","aliases":["clock630"]},{"emoji":"🕖","aliases":["clock7"]},{"emoji":"🕢","aliases":["clock730"]},{"emoji":"🕗","aliases":["clock8"]},{"emoji":"🕣","aliases":["clock830"]},{"emoji":"🕘","aliases":["clock9"]},{"emoji":"🕤","aliases":["clock930"]},{"emoji":"📕","aliases":["closed_book"]},{"emoji":"🔐","aliases":["closed_lock_with_key"]},{"emoji":"🌂","aliases":["closed_umbrella"]},{"emoji":"☁️","aliases":["cloud"]},{"emoji":"🌩️","aliases":["cloud_with_lightning"]},{"emoji":"⛈️","aliases":["cloud_with_lightning_and_rain"]},{"emoji":"🌧️","aliases":["cloud_with_rain"]},{"emoji":"🌨️","aliases":["cloud_with_snow"]},{"emoji":"🤡","aliases":["clown_face"]},{"emoji":"♣️","aliases":["clubs"]},{"emoji":"🇨🇳","aliases":["cn"]},{"emoji":"🧥","aliases":["coat"]},{"emoji":"🪳","aliases":["cockroach"]},{"emoji":"🍸","aliases":["cocktail"]},{"emoji":"🥥","aliases":["coconut"]},{"emoji":"🇨🇨","aliases":["cocos_islands"]},{"emoji":"☕","aliases":["coffee"]},{"emoji":"⚰️","aliases":["coffin"]},{"emoji":"🪙","aliases":["coin"]},{"emoji":"🥶","aliases":["cold_face"]},{"emoji":"😰","aliases":["cold_sweat"]},{"emoji":"🇨🇴","aliases":["colombia"]},{"emoji":"☄️","aliases":["comet"]},{"emoji":"🇰🇲","aliases":["comoros"]},{"emoji":"🧭","aliases":["compass"]},{"emoji":"💻","aliases":["computer"]},{"emoji":"🖱️","aliases":["computer_mouse"]},{"emoji":"🎊","aliases":["confetti_ball"]},{"emoji":"😖","aliases":["confounded"]},{"emoji":"😕","aliases":["confused"]},{"emoji":"🇨🇬","aliases":["congo_brazzaville"]},{"emoji":"🇨🇩","aliases":["congo_kinshasa"]},{"emoji":"㊗️","aliases":["congratulations"]},{"emoji":"🚧","aliases":["construction"]},{"emoji":"👷","aliases":["construction_worker"]},{"emoji":"👷‍♂️","aliases":["construction_worker_man"]},{"emoji":"👷‍♀️","aliases":["construction_worker_woman"]},{"emoji":"🎛️","aliases":["control_knobs"]},{"emoji":"🏪","aliases":["convenience_store"]},{"emoji":"🧑‍🍳","aliases":["cook"]},{"emoji":"🇨🇰","aliases":["cook_islands"]},{"emoji":"🍪","aliases":["cookie"]},{"emoji":"🆒","aliases":["cool"]},{"emoji":"©️","aliases":["copyright"]},{"emoji":"🪸","aliases":["coral"]},{"emoji":"🌽","aliases":["corn"]},{"emoji":"🇨🇷","aliases":["costa_rica"]},{"emoji":"🇨🇮","aliases":["cote_divoire"]},{"emoji":"🛋️","aliases":["couch_and_lamp"]},{"emoji":"👫","aliases":["couple"]},{"emoji":"💑","aliases":["couple_with_heart"]},{"emoji":"👨‍❤️‍👨","aliases":["couple_with_heart_man_man"]},{"emoji":"👩‍❤️‍👨","aliases":["couple_with_heart_woman_man"]},{"emoji":"👩‍❤️‍👩","aliases":["couple_with_heart_woman_woman"]},{"emoji":"💏","aliases":["couplekiss"]},{"emoji":"👨‍❤️‍💋‍👨","aliases":["couplekiss_man_man"]},{"emoji":"👩‍❤️‍💋‍👨","aliases":["couplekiss_man_woman"]},{"emoji":"👩‍❤️‍💋‍👩","aliases":["couplekiss_woman_woman"]},{"emoji":"🐮","aliases":["cow"]},{"emoji":"🐄","aliases":["cow2"]},{"emoji":"🤠","aliases":["cowboy_hat_face"]},{"emoji":"🦀","aliases":["crab"]},{"emoji":"🖍️","aliases":["crayon"]},{"emoji":"💳","aliases":["credit_card"]},{"emoji":"🌙","aliases":["crescent_moon"]},{"emoji":"🦗","aliases":["cricket"]},{"emoji":"🏏","aliases":["cricket_game"]},{"emoji":"🇭🇷","aliases":["croatia"]},{"emoji":"🐊","aliases":["crocodile"]},{"emoji":"🥐","aliases":["croissant"]},{"emoji":"🤞","aliases":["crossed_fingers"]},{"emoji":"🎌","aliases":["crossed_flags"]},{"emoji":"⚔️","aliases":["crossed_swords"]},{"emoji":"👑","aliases":["crown"]},{"emoji":"🩼","aliases":["crutch"]},{"emoji":"😢","aliases":["cry"]},{"emoji":"😿","aliases":["crying_cat_face"]},{"emoji":"🔮","aliases":["crystal_ball"]},{"emoji":"🇨🇺","aliases":["cuba"]},{"emoji":"🥒","aliases":["cucumber"]},{"emoji":"🥤","aliases":["cup_with_straw"]},{"emoji":"🧁","aliases":["cupcake"]},{"emoji":"💘","aliases":["cupid"]},{"emoji":"🇨🇼","aliases":["curacao"]},{"emoji":"🥌","aliases":["curling_stone"]},{"emoji":"👨‍🦱","aliases":["curly_haired_man"]},{"emoji":"👩‍🦱","aliases":["curly_haired_woman"]},{"emoji":"➰","aliases":["curly_loop"]},{"emoji":"💱","aliases":["currency_exchange"]},{"emoji":"🍛","aliases":["curry"]},{"emoji":"🤬","aliases":["cursing_face"]},{"emoji":"🍮","aliases":["custard"]},{"emoji":"🛃","aliases":["customs"]},{"emoji":"🥩","aliases":["cut_of_meat"]},{"emoji":"🌀","aliases":["cyclone"]},{"emoji":"🇨🇾","aliases":["cyprus"]},{"emoji":"🇨🇿","aliases":["czech_republic"]},{"emoji":"🗡️","aliases":["dagger"]},{"emoji":"👯","aliases":["dancers"]},{"emoji":"👯‍♂️","aliases":["dancing_men"]},{"emoji":"👯‍♀️","aliases":["dancing_women"]},{"emoji":"🍡","aliases":["dango"]},{"emoji":"🕶️","aliases":["dark_sunglasses"]},{"emoji":"🎯","aliases":["dart"]},{"emoji":"💨","aliases":["dash"]},{"emoji":"📅","aliases":["date"]},{"emoji":"🇩🇪","aliases":["de"]},{"emoji":"🧏‍♂️","aliases":["deaf_man"]},{"emoji":"🧏","aliases":["deaf_person"]},{"emoji":"🧏‍♀️","aliases":["deaf_woman"]},{"emoji":"🌳","aliases":["deciduous_tree"]},{"emoji":"🦌","aliases":["deer"]},{"emoji":"🇩🇰","aliases":["denmark"]},{"emoji":"🏬","aliases":["department_store"]},{"emoji":"🏚️","aliases":["derelict_house"]},{"emoji":"🏜️","aliases":["desert"]},{"emoji":"🏝️","aliases":["desert_island"]},{"emoji":"🖥️","aliases":["desktop_computer"]},{"emoji":"🕵️","aliases":["detective"]},{"emoji":"💠","aliases":["diamond_shape_with_a_dot_inside"]},{"emoji":"♦️","aliases":["diamonds"]},{"emoji":"🇩🇬","aliases":["diego_garcia"]},{"emoji":"😞","aliases":["disappointed"]},{"emoji":"😥","aliases":["disappointed_relieved"]},{"emoji":"🥸","aliases":["disguised_face"]},{"emoji":"🤿","aliases":["diving_mask"]},{"emoji":"🪔","aliases":["diya_lamp"]},{"emoji":"💫","aliases":["dizzy"]},{"emoji":"😵","aliases":["dizzy_face"]},{"emoji":"🇩🇯","aliases":["djibouti"]},{"emoji":"🧬","aliases":["dna"]},{"emoji":"🚯","aliases":["do_not_litter"]},{"emoji":"🦤","aliases":["dodo"]},{"emoji":"🐶","aliases":["dog"]},{"emoji":"🐕","aliases":["dog2"]},{"emoji":"💵","aliases":["dollar"]},{"emoji":"🎎","aliases":["dolls"]},{"emoji":"🐬","aliases":["dolphin","flipper"]},{"emoji":"🇩🇲","aliases":["dominica"]},{"emoji":"🇩🇴","aliases":["dominican_republic"]},{"emoji":"🫏","aliases":["donkey"]},{"emoji":"🚪","aliases":["door"]},{"emoji":"🫥","aliases":["dotted_line_face"]},{"emoji":"🍩","aliases":["doughnut"]},{"emoji":"🕊️","aliases":["dove"]},{"emoji":"🐉","aliases":["dragon"]},{"emoji":"🐲","aliases":["dragon_face"]},{"emoji":"👗","aliases":["dress"]},{"emoji":"🐪","aliases":["dromedary_camel"]},{"emoji":"🤤","aliases":["drooling_face"]},{"emoji":"🩸","aliases":["drop_of_blood"]},{"emoji":"💧","aliases":["droplet"]},{"emoji":"🥁","aliases":["drum"]},{"emoji":"🦆","aliases":["duck"]},{"emoji":"🥟","aliases":["dumpling"]},{"emoji":"📀","aliases":["dvd"]},{"emoji":"🦅","aliases":["eagle"]},{"emoji":"👂","aliases":["ear"]},{"emoji":"🌾","aliases":["ear_of_rice"]},{"emoji":"🦻","aliases":["ear_with_hearing_aid"]},{"emoji":"🌍","aliases":["earth_africa"]},{"emoji":"🌎","aliases":["earth_americas"]},{"emoji":"🌏","aliases":["earth_asia"]},{"emoji":"🇪🇨","aliases":["ecuador"]},{"emoji":"🥚","aliases":["egg"]},{"emoji":"🍆","aliases":["eggplant"]},{"emoji":"🇪🇬","aliases":["egypt"]},{"emoji":"8️⃣","aliases":["eight"]},{"emoji":"✴️","aliases":["eight_pointed_black_star"]},{"emoji":"✳️","aliases":["eight_spoked_asterisk"]},{"emoji":"⏏️","aliases":["eject_button"]},{"emoji":"🇸🇻","aliases":["el_salvador"]},{"emoji":"🔌","aliases":["electric_plug"]},{"emoji":"🐘","aliases":["elephant"]},{"emoji":"🛗","aliases":["elevator"]},{"emoji":"🧝","aliases":["elf"]},{"emoji":"🧝‍♂️","aliases":["elf_man"]},{"emoji":"🧝‍♀️","aliases":["elf_woman"]},{"emoji":"📧","aliases":["email","e-mail"]},{"emoji":"🪹","aliases":["empty_nest"]},{"emoji":"🔚","aliases":["end"]},{"emoji":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","aliases":["england"]},{"emoji":"✉️","aliases":["envelope"]},{"emoji":"📩","aliases":["envelope_with_arrow"]},{"emoji":"🇬🇶","aliases":["equatorial_guinea"]},{"emoji":"🇪🇷","aliases":["eritrea"]},{"emoji":"🇪🇸","aliases":["es"]},{"emoji":"🇪🇪","aliases":["estonia"]},{"emoji":"🇪🇹","aliases":["ethiopia"]},{"emoji":"🇪🇺","aliases":["eu","european_union"]},{"emoji":"💶","aliases":["euro"]},{"emoji":"🏰","aliases":["european_castle"]},{"emoji":"🏤","aliases":["european_post_office"]},{"emoji":"🌲","aliases":["evergreen_tree"]},{"emoji":"❗","aliases":["exclamation","heavy_exclamation_mark"]},{"emoji":"🤯","aliases":["exploding_head"]},{"emoji":"😑","aliases":["expressionless"]},{"emoji":"👁️","aliases":["eye"]},{"emoji":"👁️‍🗨️","aliases":["eye_speech_bubble"]},{"emoji":"👓","aliases":["eyeglasses"]},{"emoji":"👀","aliases":["eyes"]},{"emoji":"😮‍💨","aliases":["face_exhaling"]},{"emoji":"🥹","aliases":["face_holding_back_tears"]},{"emoji":"😶‍🌫️","aliases":["face_in_clouds"]},{"emoji":"🫤","aliases":["face_with_diagonal_mouth"]},{"emoji":"🤕","aliases":["face_with_head_bandage"]},{"emoji":"🫢","aliases":["face_with_open_eyes_and_hand_over_mouth"]},{"emoji":"🫣","aliases":["face_with_peeking_eye"]},{"emoji":"😵‍💫","aliases":["face_with_spiral_eyes"]},{"emoji":"🤒","aliases":["face_with_thermometer"]},{"emoji":"🤦","aliases":["facepalm"]},{"emoji":"🏭","aliases":["factory"]},{"emoji":"🧑‍🏭","aliases":["factory_worker"]},{"emoji":"🧚","aliases":["fairy"]},{"emoji":"🧚‍♂️","aliases":["fairy_man"]},{"emoji":"🧚‍♀️","aliases":["fairy_woman"]},{"emoji":"🧆","aliases":["falafel"]},{"emoji":"🇫🇰","aliases":["falkland_islands"]},{"emoji":"🍂","aliases":["fallen_leaf"]},{"emoji":"👪","aliases":["family"]},{"emoji":"👨‍👦","aliases":["family_man_boy"]},{"emoji":"👨‍👦‍👦","aliases":["family_man_boy_boy"]},{"emoji":"👨‍👧","aliases":["family_man_girl"]},{"emoji":"👨‍👧‍👦","aliases":["family_man_girl_boy"]},{"emoji":"👨‍👧‍👧","aliases":["family_man_girl_girl"]},{"emoji":"👨‍👨‍👦","aliases":["family_man_man_boy"]},{"emoji":"👨‍👨‍👦‍👦","aliases":["family_man_man_boy_boy"]},{"emoji":"👨‍👨‍👧","aliases":["family_man_man_girl"]},{"emoji":"👨‍👨‍👧‍👦","aliases":["family_man_man_girl_boy"]},{"emoji":"👨‍👨‍👧‍👧","aliases":["family_man_man_girl_girl"]},{"emoji":"👨‍👩‍👦","aliases":["family_man_woman_boy"]},{"emoji":"👨‍👩‍👦‍👦","aliases":["family_man_woman_boy_boy"]},{"emoji":"👨‍👩‍👧","aliases":["family_man_woman_girl"]},{"emoji":"👨‍👩‍👧‍👦","aliases":["family_man_woman_girl_boy"]},{"emoji":"👨‍👩‍👧‍👧","aliases":["family_man_woman_girl_girl"]},{"emoji":"👩‍👦","aliases":["family_woman_boy"]},{"emoji":"👩‍👦‍👦","aliases":["family_woman_boy_boy"]},{"emoji":"👩‍👧","aliases":["family_woman_girl"]},{"emoji":"👩‍👧‍👦","aliases":["family_woman_girl_boy"]},{"emoji":"👩‍👧‍👧","aliases":["family_woman_girl_girl"]},{"emoji":"👩‍👩‍👦","aliases":["family_woman_woman_boy"]},{"emoji":"👩‍👩‍👦‍👦","aliases":["family_woman_woman_boy_boy"]},{"emoji":"👩‍👩‍👧","aliases":["family_woman_woman_girl"]},{"emoji":"👩‍👩‍👧‍👦","aliases":["family_woman_woman_girl_boy"]},{"emoji":"👩‍👩‍👧‍👧","aliases":["family_woman_woman_girl_girl"]},{"emoji":"🧑‍🌾","aliases":["farmer"]},{"emoji":"🇫🇴","aliases":["faroe_islands"]},{"emoji":"⏩","aliases":["fast_forward"]},{"emoji":"📠","aliases":["fax"]},{"emoji":"😨","aliases":["fearful"]},{"emoji":"🪶","aliases":["feather"]},{"emoji":"🐾","aliases":["feet","paw_prints"]},{"emoji":"🕵️‍♀️","aliases":["female_detective"]},{"emoji":"♀️","aliases":["female_sign"]},{"emoji":"🎡","aliases":["ferris_wheel"]},{"emoji":"⛴️","aliases":["ferry"]},{"emoji":"🏑","aliases":["field_hockey"]},{"emoji":"🇫🇯","aliases":["fiji"]},{"emoji":"🗄️","aliases":["file_cabinet"]},{"emoji":"📁","aliases":["file_folder"]},{"emoji":"📽️","aliases":["film_projector"]},{"emoji":"🎞️","aliases":["film_strip"]},{"emoji":"🇫🇮","aliases":["finland"]},{"emoji":"🔥","aliases":["fire"]},{"emoji":"🚒","aliases":["fire_engine"]},{"emoji":"🧯","aliases":["fire_extinguisher"]},{"emoji":"🧨","aliases":["firecracker"]},{"emoji":"🧑‍🚒","aliases":["firefighter"]},{"emoji":"🎆","aliases":["fireworks"]},{"emoji":"🌓","aliases":["first_quarter_moon"]},{"emoji":"🌛","aliases":["first_quarter_moon_with_face"]},{"emoji":"🐟","aliases":["fish"]},{"emoji":"🍥","aliases":["fish_cake"]},{"emoji":"🎣","aliases":["fishing_pole_and_fish"]},{"emoji":"🤛","aliases":["fist_left"]},{"emoji":"👊","aliases":["fist_oncoming","facepunch","punch"]},{"emoji":"✊","aliases":["fist_raised","fist"]},{"emoji":"🤜","aliases":["fist_right"]},{"emoji":"5️⃣","aliases":["five"]},{"emoji":"🎏","aliases":["flags"]},{"emoji":"🦩","aliases":["flamingo"]},{"emoji":"🔦","aliases":["flashlight"]},{"emoji":"🥿","aliases":["flat_shoe"]},{"emoji":"🫓","aliases":["flatbread"]},{"emoji":"⚜️","aliases":["fleur_de_lis"]},{"emoji":"🛬","aliases":["flight_arrival"]},{"emoji":"🛫","aliases":["flight_departure"]},{"emoji":"💾","aliases":["floppy_disk"]},{"emoji":"🎴","aliases":["flower_playing_cards"]},{"emoji":"😳","aliases":["flushed"]},{"emoji":"🪈","aliases":["flute"]},{"emoji":"🪰","aliases":["fly"]},{"emoji":"🥏","aliases":["flying_disc"]},{"emoji":"🛸","aliases":["flying_saucer"]},{"emoji":"🌫️","aliases":["fog"]},{"emoji":"🌁","aliases":["foggy"]},{"emoji":"🪭","aliases":["folding_hand_fan"]},{"emoji":"🫕","aliases":["fondue"]},{"emoji":"🦶","aliases":["foot"]},{"emoji":"🏈","aliases":["football"]},{"emoji":"👣","aliases":["footprints"]},{"emoji":"🍴","aliases":["fork_and_knife"]},{"emoji":"🥠","aliases":["fortune_cookie"]},{"emoji":"⛲","aliases":["fountain"]},{"emoji":"🖋️","aliases":["fountain_pen"]},{"emoji":"4️⃣","aliases":["four"]},{"emoji":"🍀","aliases":["four_leaf_clover"]},{"emoji":"🦊","aliases":["fox_face"]},{"emoji":"🇫🇷","aliases":["fr"]},{"emoji":"🖼️","aliases":["framed_picture"]},{"emoji":"🆓","aliases":["free"]},{"emoji":"🇬🇫","aliases":["french_guiana"]},{"emoji":"🇵🇫","aliases":["french_polynesia"]},{"emoji":"🇹🇫","aliases":["french_southern_territories"]},{"emoji":"🍳","aliases":["fried_egg"]},{"emoji":"🍤","aliases":["fried_shrimp"]},{"emoji":"🍟","aliases":["fries"]},{"emoji":"🐸","aliases":["frog"]},{"emoji":"😦","aliases":["frowning"]},{"emoji":"☹️","aliases":["frowning_face"]},{"emoji":"🙍‍♂️","aliases":["frowning_man"]},{"emoji":"🙍","aliases":["frowning_person"]},{"emoji":"🙍‍♀️","aliases":["frowning_woman"]},{"emoji":"⛽","aliases":["fuelpump"]},{"emoji":"🌕","aliases":["full_moon"]},{"emoji":"🌝","aliases":["full_moon_with_face"]},{"emoji":"⚱️","aliases":["funeral_urn"]},{"emoji":"🇬🇦","aliases":["gabon"]},{"emoji":"🇬🇲","aliases":["gambia"]},{"emoji":"🎲","aliases":["game_die"]},{"emoji":"🧄","aliases":["garlic"]},{"emoji":"🇬🇧","aliases":["gb","uk"]},{"emoji":"⚙️","aliases":["gear"]},{"emoji":"💎","aliases":["gem"]},{"emoji":"♊","aliases":["gemini"]},{"emoji":"🧞","aliases":["genie"]},{"emoji":"🧞‍♂️","aliases":["genie_man"]},{"emoji":"🧞‍♀️","aliases":["genie_woman"]},{"emoji":"🇬🇪","aliases":["georgia"]},{"emoji":"🇬🇭","aliases":["ghana"]},{"emoji":"👻","aliases":["ghost"]},{"emoji":"🇬🇮","aliases":["gibraltar"]},{"emoji":"🎁","aliases":["gift"]},{"emoji":"💝","aliases":["gift_heart"]},{"emoji":"🫚","aliases":["ginger_root"]},{"emoji":"🦒","aliases":["giraffe"]},{"emoji":"👧","aliases":["girl"]},{"emoji":"🌐","aliases":["globe_with_meridians"]},{"emoji":"🧤","aliases":["gloves"]},{"emoji":"🥅","aliases":["goal_net"]},{"emoji":"🐐","aliases":["goat"]},{"emoji":"🥽","aliases":["goggles"]},{"emoji":"⛳","aliases":["golf"]},{"emoji":"🏌️","aliases":["golfing"]},{"emoji":"🏌️‍♂️","aliases":["golfing_man"]},{"emoji":"🏌️‍♀️","aliases":["golfing_woman"]},{"emoji":"🪿","aliases":["goose"]},{"emoji":"🦍","aliases":["gorilla"]},{"emoji":"🍇","aliases":["grapes"]},{"emoji":"🇬🇷","aliases":["greece"]},{"emoji":"🍏","aliases":["green_apple"]},{"emoji":"📗","aliases":["green_book"]},{"emoji":"🟢","aliases":["green_circle"]},{"emoji":"💚","aliases":["green_heart"]},{"emoji":"🥗","aliases":["green_salad"]},{"emoji":"🟩","aliases":["green_square"]},{"emoji":"🇬🇱","aliases":["greenland"]},{"emoji":"🇬🇩","aliases":["grenada"]},{"emoji":"❕","aliases":["grey_exclamation"]},{"emoji":"🩶","aliases":["grey_heart"]},{"emoji":"❔","aliases":["grey_question"]},{"emoji":"😬","aliases":["grimacing"]},{"emoji":"😁","aliases":["grin"]},{"emoji":"😀","aliases":["grinning"]},{"emoji":"🇬🇵","aliases":["guadeloupe"]},{"emoji":"🇬🇺","aliases":["guam"]},{"emoji":"💂","aliases":["guard"]},{"emoji":"💂‍♂️","aliases":["guardsman"]},{"emoji":"💂‍♀️","aliases":["guardswoman"]},{"emoji":"🇬🇹","aliases":["guatemala"]},{"emoji":"🇬🇬","aliases":["guernsey"]},{"emoji":"🦮","aliases":["guide_dog"]},{"emoji":"🇬🇳","aliases":["guinea"]},{"emoji":"🇬🇼","aliases":["guinea_bissau"]},{"emoji":"🎸","aliases":["guitar"]},{"emoji":"🔫","aliases":["gun"]},{"emoji":"🇬🇾","aliases":["guyana"]},{"emoji":"🪮","aliases":["hair_pick"]},{"emoji":"💇","aliases":["haircut"]},{"emoji":"💇‍♂️","aliases":["haircut_man"]},{"emoji":"💇‍♀️","aliases":["haircut_woman"]},{"emoji":"🇭🇹","aliases":["haiti"]},{"emoji":"🍔","aliases":["hamburger"]},{"emoji":"🔨","aliases":["hammer"]},{"emoji":"⚒️","aliases":["hammer_and_pick"]},{"emoji":"🛠️","aliases":["hammer_and_wrench"]},{"emoji":"🪬","aliases":["hamsa"]},{"emoji":"🐹","aliases":["hamster"]},{"emoji":"✋","aliases":["hand","raised_hand"]},{"emoji":"🤭","aliases":["hand_over_mouth"]},{"emoji":"🫰","aliases":["hand_with_index_finger_and_thumb_crossed"]},{"emoji":"👜","aliases":["handbag"]},{"emoji":"🤾","aliases":["handball_person"]},{"emoji":"🤝","aliases":["handshake"]},{"emoji":"💩","aliases":["hankey","poop","shit"]},{"emoji":"#️⃣","aliases":["hash"]},{"emoji":"🐥","aliases":["hatched_chick"]},{"emoji":"🐣","aliases":["hatching_chick"]},{"emoji":"🎧","aliases":["headphones"]},{"emoji":"🪦","aliases":["headstone"]},{"emoji":"🧑‍⚕️","aliases":["health_worker"]},{"emoji":"🙉","aliases":["hear_no_evil"]},{"emoji":"🇭🇲","aliases":["heard_mcdonald_islands"]},{"emoji":"❤️","aliases":["heart"]},{"emoji":"💟","aliases":["heart_decoration"]},{"emoji":"😍","aliases":["heart_eyes"]},{"emoji":"😻","aliases":["heart_eyes_cat"]},{"emoji":"🫶","aliases":["heart_hands"]},{"emoji":"❤️‍🔥","aliases":["heart_on_fire"]},{"emoji":"💓","aliases":["heartbeat"]},{"emoji":"💗","aliases":["heartpulse"]},{"emoji":"♥️","aliases":["hearts"]},{"emoji":"✔️","aliases":["heavy_check_mark"]},{"emoji":"➗","aliases":["heavy_division_sign"]},{"emoji":"💲","aliases":["heavy_dollar_sign"]},{"emoji":"🟰","aliases":["heavy_equals_sign"]},{"emoji":"❣️","aliases":["heavy_heart_exclamation"]},{"emoji":"➖","aliases":["heavy_minus_sign"]},{"emoji":"✖️","aliases":["heavy_multiplication_x"]},{"emoji":"➕","aliases":["heavy_plus_sign"]},{"emoji":"🦔","aliases":["hedgehog"]},{"emoji":"🚁","aliases":["helicopter"]},{"emoji":"🌿","aliases":["herb"]},{"emoji":"🌺","aliases":["hibiscus"]},{"emoji":"🔆","aliases":["high_brightness"]},{"emoji":"👠","aliases":["high_heel"]},{"emoji":"🥾","aliases":["hiking_boot"]},{"emoji":"🛕","aliases":["hindu_temple"]},{"emoji":"🦛","aliases":["hippopotamus"]},{"emoji":"🔪","aliases":["hocho","knife"]},{"emoji":"🕳️","aliases":["hole"]},{"emoji":"🇭🇳","aliases":["honduras"]},{"emoji":"🍯","aliases":["honey_pot"]},{"emoji":"🇭🇰","aliases":["hong_kong"]},{"emoji":"🪝","aliases":["hook"]},{"emoji":"🐴","aliases":["horse"]},{"emoji":"🏇","aliases":["horse_racing"]},{"emoji":"🏥","aliases":["hospital"]},{"emoji":"🥵","aliases":["hot_face"]},{"emoji":"🌶️","aliases":["hot_pepper"]},{"emoji":"🌭","aliases":["hotdog"]},{"emoji":"🏨","aliases":["hotel"]},{"emoji":"♨️","aliases":["hotsprings"]},{"emoji":"⌛","aliases":["hourglass"]},{"emoji":"⏳","aliases":["hourglass_flowing_sand"]},{"emoji":"🏠","aliases":["house"]},{"emoji":"🏡","aliases":["house_with_garden"]},{"emoji":"🏘️","aliases":["houses"]},{"emoji":"🤗","aliases":["hugs"]},{"emoji":"🇭🇺","aliases":["hungary"]},{"emoji":"😯","aliases":["hushed"]},{"emoji":"🛖","aliases":["hut"]},{"emoji":"🪻","aliases":["hyacinth"]},{"emoji":"🍨","aliases":["ice_cream"]},{"emoji":"🧊","aliases":["ice_cube"]},{"emoji":"🏒","aliases":["ice_hockey"]},{"emoji":"⛸️","aliases":["ice_skate"]},{"emoji":"🍦","aliases":["icecream"]},{"emoji":"🇮🇸","aliases":["iceland"]},{"emoji":"🆔","aliases":["id"]},{"emoji":"🪪","aliases":["identification_card"]},{"emoji":"🉐","aliases":["ideograph_advantage"]},{"emoji":"👿","aliases":["imp"]},{"emoji":"📥","aliases":["inbox_tray"]},{"emoji":"📨","aliases":["incoming_envelope"]},{"emoji":"🫵","aliases":["index_pointing_at_the_viewer"]},{"emoji":"🇮🇳","aliases":["india"]},{"emoji":"🇮🇩","aliases":["indonesia"]},{"emoji":"♾️","aliases":["infinity"]},{"emoji":"ℹ️","aliases":["information_source"]},{"emoji":"😇","aliases":["innocent"]},{"emoji":"⁉️","aliases":["interrobang"]},{"emoji":"📱","aliases":["iphone"]},{"emoji":"🇮🇷","aliases":["iran"]},{"emoji":"🇮🇶","aliases":["iraq"]},{"emoji":"🇮🇪","aliases":["ireland"]},{"emoji":"🇮🇲","aliases":["isle_of_man"]},{"emoji":"🇮🇱","aliases":["israel"]},{"emoji":"🇮🇹","aliases":["it"]},{"emoji":"🏮","aliases":["izakaya_lantern","lantern"]},{"emoji":"🎃","aliases":["jack_o_lantern"]},{"emoji":"🇯🇲","aliases":["jamaica"]},{"emoji":"🗾","aliases":["japan"]},{"emoji":"🏯","aliases":["japanese_castle"]},{"emoji":"👺","aliases":["japanese_goblin"]},{"emoji":"👹","aliases":["japanese_ogre"]},{"emoji":"🫙","aliases":["jar"]},{"emoji":"👖","aliases":["jeans"]},{"emoji":"🪼","aliases":["jellyfish"]},{"emoji":"🇯🇪","aliases":["jersey"]},{"emoji":"🧩","aliases":["jigsaw"]},{"emoji":"🇯🇴","aliases":["jordan"]},{"emoji":"😂","aliases":["joy"]},{"emoji":"😹","aliases":["joy_cat"]},{"emoji":"🕹️","aliases":["joystick"]},{"emoji":"🇯🇵","aliases":["jp"]},{"emoji":"🧑‍⚖️","aliases":["judge"]},{"emoji":"🤹","aliases":["juggling_person"]},{"emoji":"🕋","aliases":["kaaba"]},{"emoji":"🦘","aliases":["kangaroo"]},{"emoji":"🇰🇿","aliases":["kazakhstan"]},{"emoji":"🇰🇪","aliases":["kenya"]},{"emoji":"🔑","aliases":["key"]},{"emoji":"⌨️","aliases":["keyboard"]},{"emoji":"🔟","aliases":["keycap_ten"]},{"emoji":"🪯","aliases":["khanda"]},{"emoji":"🛴","aliases":["kick_scooter"]},{"emoji":"👘","aliases":["kimono"]},{"emoji":"🇰🇮","aliases":["kiribati"]},{"emoji":"💋","aliases":["kiss"]},{"emoji":"😗","aliases":["kissing"]},{"emoji":"😽","aliases":["kissing_cat"]},{"emoji":"😚","aliases":["kissing_closed_eyes"]},{"emoji":"😘","aliases":["kissing_heart"]},{"emoji":"😙","aliases":["kissing_smiling_eyes"]},{"emoji":"🪁","aliases":["kite"]},{"emoji":"🥝","aliases":["kiwi_fruit"]},{"emoji":"🧎‍♂️","aliases":["kneeling_man"]},{"emoji":"🧎","aliases":["kneeling_person"]},{"emoji":"🧎‍♀️","aliases":["kneeling_woman"]},{"emoji":"🪢","aliases":["knot"]},{"emoji":"🐨","aliases":["koala"]},{"emoji":"🈁","aliases":["koko"]},{"emoji":"🇽🇰","aliases":["kosovo"]},{"emoji":"🇰🇷","aliases":["kr"]},{"emoji":"🇰🇼","aliases":["kuwait"]},{"emoji":"🇰🇬","aliases":["kyrgyzstan"]},{"emoji":"🥼","aliases":["lab_coat"]},{"emoji":"🏷️","aliases":["label"]},{"emoji":"🥍","aliases":["lacrosse"]},{"emoji":"🪜","aliases":["ladder"]},{"emoji":"🐞","aliases":["lady_beetle"]},{"emoji":"🇱🇦","aliases":["laos"]},{"emoji":"🔵","aliases":["large_blue_circle"]},{"emoji":"🔷","aliases":["large_blue_diamond"]},{"emoji":"🔶","aliases":["large_orange_diamond"]},{"emoji":"🌗","aliases":["last_quarter_moon"]},{"emoji":"🌜","aliases":["last_quarter_moon_with_face"]},{"emoji":"✝️","aliases":["latin_cross"]},{"emoji":"🇱🇻","aliases":["latvia"]},{"emoji":"😆","aliases":["laughing","satisfied","laugh"]},{"emoji":"🥬","aliases":["leafy_green"]},{"emoji":"🍃","aliases":["leaves"]},{"emoji":"🇱🇧","aliases":["lebanon"]},{"emoji":"📒","aliases":["ledger"]},{"emoji":"🛅","aliases":["left_luggage"]},{"emoji":"↔️","aliases":["left_right_arrow"]},{"emoji":"🗨️","aliases":["left_speech_bubble"]},{"emoji":"↩️","aliases":["leftwards_arrow_with_hook"]},{"emoji":"🫲","aliases":["leftwards_hand"]},{"emoji":"🫷","aliases":["leftwards_pushing_hand"]},{"emoji":"🦵","aliases":["leg"]},{"emoji":"🍋","aliases":["lemon"]},{"emoji":"♌","aliases":["leo"]},{"emoji":"🐆","aliases":["leopard"]},{"emoji":"🇱🇸","aliases":["lesotho"]},{"emoji":"🎚️","aliases":["level_slider"]},{"emoji":"🇱🇷","aliases":["liberia"]},{"emoji":"♎","aliases":["libra"]},{"emoji":"🇱🇾","aliases":["libya"]},{"emoji":"🇱🇮","aliases":["liechtenstein"]},{"emoji":"🩵","aliases":["light_blue_heart"]},{"emoji":"🚈","aliases":["light_rail"]},{"emoji":"🔗","aliases":["link"]},{"emoji":"🦁","aliases":["lion"]},{"emoji":"👄","aliases":["lips"]},{"emoji":"💄","aliases":["lipstick"]},{"emoji":"🇱🇹","aliases":["lithuania"]},{"emoji":"🦎","aliases":["lizard"]},{"emoji":"🦙","aliases":["llama"]},{"emoji":"🦞","aliases":["lobster"]},{"emoji":"🔒","aliases":["lock"]},{"emoji":"🔏","aliases":["lock_with_ink_pen"]},{"emoji":"🍭","aliases":["lollipop"]},{"emoji":"🪘","aliases":["long_drum"]},{"emoji":"➿","aliases":["loop"]},{"emoji":"🧴","aliases":["lotion_bottle"]},{"emoji":"🪷","aliases":["lotus"]},{"emoji":"🧘","aliases":["lotus_position"]},{"emoji":"🧘‍♂️","aliases":["lotus_position_man"]},{"emoji":"🧘‍♀️","aliases":["lotus_position_woman"]},{"emoji":"🔊","aliases":["loud_sound"]},{"emoji":"📢","aliases":["loudspeaker"]},{"emoji":"🏩","aliases":["love_hotel"]},{"emoji":"💌","aliases":["love_letter"]},{"emoji":"🤟","aliases":["love_you_gesture"]},{"emoji":"🪫","aliases":["low_battery"]},{"emoji":"🔅","aliases":["low_brightness"]},{"emoji":"🧳","aliases":["luggage"]},{"emoji":"🫁","aliases":["lungs"]},{"emoji":"🇱🇺","aliases":["luxembourg"]},{"emoji":"🤥","aliases":["lying_face"]},{"emoji":"Ⓜ️","aliases":["m"]},{"emoji":"🇲🇴","aliases":["macau"]},{"emoji":"🇲🇰","aliases":["macedonia"]},{"emoji":"🇲🇬","aliases":["madagascar"]},{"emoji":"🔍","aliases":["mag"]},{"emoji":"🔎","aliases":["mag_right"]},{"emoji":"🧙","aliases":["mage"]},{"emoji":"🧙‍♂️","aliases":["mage_man"]},{"emoji":"🧙‍♀️","aliases":["mage_woman"]},{"emoji":"🪄","aliases":["magic_wand"]},{"emoji":"🧲","aliases":["magnet"]},{"emoji":"🀄","aliases":["mahjong"]},{"emoji":"📫","aliases":["mailbox"]},{"emoji":"📪","aliases":["mailbox_closed"]},{"emoji":"📬","aliases":["mailbox_with_mail"]},{"emoji":"📭","aliases":["mailbox_with_no_mail"]},{"emoji":"🇲🇼","aliases":["malawi"]},{"emoji":"🇲🇾","aliases":["malaysia"]},{"emoji":"🇲🇻","aliases":["maldives"]},{"emoji":"🕵️‍♂️","aliases":["male_detective"]},{"emoji":"♂️","aliases":["male_sign"]},{"emoji":"🇲🇱","aliases":["mali"]},{"emoji":"🇲🇹","aliases":["malta"]},{"emoji":"🦣","aliases":["mammoth"]},{"emoji":"👨","aliases":["man"]},{"emoji":"👨‍🎨","aliases":["man_artist"]},{"emoji":"👨‍🚀","aliases":["man_astronaut"]},{"emoji":"🧔‍♂️","aliases":["man_beard"]},{"emoji":"🤸‍♂️","aliases":["man_cartwheeling"]},{"emoji":"👨‍🍳","aliases":["man_cook"]},{"emoji":"🕺","aliases":["man_dancing"]},{"emoji":"🤦‍♂️","aliases":["man_facepalming"]},{"emoji":"👨‍🏭","aliases":["man_factory_worker"]},{"emoji":"👨‍🌾","aliases":["man_farmer"]},{"emoji":"👨‍🍼","aliases":["man_feeding_baby"]},{"emoji":"👨‍🚒","aliases":["man_firefighter"]},{"emoji":"👨‍⚕️","aliases":["man_health_worker"]},{"emoji":"👨‍🦽","aliases":["man_in_manual_wheelchair"]},{"emoji":"👨‍🦼","aliases":["man_in_motorized_wheelchair"]},{"emoji":"🤵‍♂️","aliases":["man_in_tuxedo"]},{"emoji":"👨‍⚖️","aliases":["man_judge"]},{"emoji":"🤹‍♂️","aliases":["man_juggling"]},{"emoji":"👨‍🔧","aliases":["man_mechanic"]},{"emoji":"👨‍💼","aliases":["man_office_worker"]},{"emoji":"👨‍✈️","aliases":["man_pilot"]},{"emoji":"🤾‍♂️","aliases":["man_playing_handball"]},{"emoji":"🤽‍♂️","aliases":["man_playing_water_polo"]},{"emoji":"👨‍🔬","aliases":["man_scientist"]},{"emoji":"🤷‍♂️","aliases":["man_shrugging"]},{"emoji":"👨‍🎤","aliases":["man_singer"]},{"emoji":"👨‍🎓","aliases":["man_student"]},{"emoji":"👨‍🏫","aliases":["man_teacher"]},{"emoji":"👨‍💻","aliases":["man_technologist"]},{"emoji":"👲","aliases":["man_with_gua_pi_mao"]},{"emoji":"👨‍🦯","aliases":["man_with_probing_cane"]},{"emoji":"👳‍♂️","aliases":["man_with_turban"]},{"emoji":"👰‍♂️","aliases":["man_with_veil"]},{"emoji":"🥭","aliases":["mango"]},{"emoji":"👞","aliases":["mans_shoe","shoe"]},{"emoji":"🕰️","aliases":["mantelpiece_clock"]},{"emoji":"🦽","aliases":["manual_wheelchair"]},{"emoji":"🍁","aliases":["maple_leaf"]},{"emoji":"🪇","aliases":["maracas"]},{"emoji":"🇲🇭","aliases":["marshall_islands"]},{"emoji":"🥋","aliases":["martial_arts_uniform"]},{"emoji":"🇲🇶","aliases":["martinique"]},{"emoji":"😷","aliases":["mask"]},{"emoji":"💆","aliases":["massage"]},{"emoji":"💆‍♂️","aliases":["massage_man"]},{"emoji":"💆‍♀️","aliases":["massage_woman"]},{"emoji":"🧉","aliases":["mate"]},{"emoji":"🇲🇷","aliases":["mauritania"]},{"emoji":"🇲🇺","aliases":["mauritius"]},{"emoji":"🇾🇹","aliases":["mayotte"]},{"emoji":"🍖","aliases":["meat_on_bone"]},{"emoji":"🧑‍🔧","aliases":["mechanic"]},{"emoji":"🦾","aliases":["mechanical_arm"]},{"emoji":"🦿","aliases":["mechanical_leg"]},{"emoji":"🎖️","aliases":["medal_military"]},{"emoji":"🏅","aliases":["medal_sports"]},{"emoji":"⚕️","aliases":["medical_symbol"]},{"emoji":"📣","aliases":["mega"]},{"emoji":"🍈","aliases":["melon"]},{"emoji":"🫠","aliases":["melting_face"]},{"emoji":"📝","aliases":["memo","pencil"]},{"emoji":"🤼‍♂️","aliases":["men_wrestling"]},{"emoji":"❤️‍🩹","aliases":["mending_heart"]},{"emoji":"🕎","aliases":["menorah"]},{"emoji":"🚹","aliases":["mens"]},{"emoji":"🧜‍♀️","aliases":["mermaid"]},{"emoji":"🧜‍♂️","aliases":["merman"]},{"emoji":"🧜","aliases":["merperson"]},{"emoji":"🤘","aliases":["metal"]},{"emoji":"🚇","aliases":["metro"]},{"emoji":"🇲🇽","aliases":["mexico"]},{"emoji":"🦠","aliases":["microbe"]},{"emoji":"🇫🇲","aliases":["micronesia"]},{"emoji":"🎤","aliases":["microphone"]},{"emoji":"🔬","aliases":["microscope"]},{"emoji":"🖕","aliases":["middle_finger","fu"]},{"emoji":"🪖","aliases":["military_helmet"]},{"emoji":"🥛","aliases":["milk_glass"]},{"emoji":"🌌","aliases":["milky_way"]},{"emoji":"🚐","aliases":["minibus"]},{"emoji":"💽","aliases":["minidisc"]},{"emoji":"🪞","aliases":["mirror"]},{"emoji":"🪩","aliases":["mirror_ball"]},{"emoji":"📴","aliases":["mobile_phone_off"]},{"emoji":"🇲🇩","aliases":["moldova"]},{"emoji":"🇲🇨","aliases":["monaco"]},{"emoji":"🤑","aliases":["money_mouth_face"]},{"emoji":"💸","aliases":["money_with_wings"]},{"emoji":"💰","aliases":["moneybag"]},{"emoji":"🇲🇳","aliases":["mongolia"]},{"emoji":"🐒","aliases":["monkey"]},{"emoji":"🐵","aliases":["monkey_face"]},{"emoji":"🧐","aliases":["monocle_face"]},{"emoji":"🚝","aliases":["monorail"]},{"emoji":"🇲🇪","aliases":["montenegro"]},{"emoji":"🇲🇸","aliases":["montserrat"]},{"emoji":"🌔","aliases":["moon","waxing_gibbous_moon"]},{"emoji":"🥮","aliases":["moon_cake"]},{"emoji":"🫎","aliases":["moose"]},{"emoji":"🇲🇦","aliases":["morocco"]},{"emoji":"🎓","aliases":["mortar_board"]},{"emoji":"🕌","aliases":["mosque"]},{"emoji":"🦟","aliases":["mosquito"]},{"emoji":"🛥️","aliases":["motor_boat"]},{"emoji":"🛵","aliases":["motor_scooter"]},{"emoji":"🏍️","aliases":["motorcycle"]},{"emoji":"🦼","aliases":["motorized_wheelchair"]},{"emoji":"🛣️","aliases":["motorway"]},{"emoji":"🗻","aliases":["mount_fuji"]},{"emoji":"⛰️","aliases":["mountain"]},{"emoji":"🚵","aliases":["mountain_bicyclist"]},{"emoji":"🚵‍♂️","aliases":["mountain_biking_man"]},{"emoji":"🚵‍♀️","aliases":["mountain_biking_woman"]},{"emoji":"🚠","aliases":["mountain_cableway"]},{"emoji":"🚞","aliases":["mountain_railway"]},{"emoji":"🏔️","aliases":["mountain_snow"]},{"emoji":"🐭","aliases":["mouse"]},{"emoji":"🐁","aliases":["mouse2"]},{"emoji":"🪤","aliases":["mouse_trap"]},{"emoji":"🎥","aliases":["movie_camera"]},{"emoji":"🗿","aliases":["moyai"]},{"emoji":"🇲🇿","aliases":["mozambique"]},{"emoji":"🤶","aliases":["mrs_claus"]},{"emoji":"💪","aliases":["muscle"]},{"emoji":"🍄","aliases":["mushroom"]},{"emoji":"🎹","aliases":["musical_keyboard"]},{"emoji":"🎵","aliases":["musical_note"]},{"emoji":"🎼","aliases":["musical_score"]},{"emoji":"🔇","aliases":["mute"]},{"emoji":"🧑‍🎄","aliases":["mx_claus"]},{"emoji":"🇲🇲","aliases":["myanmar"]},{"emoji":"💅","aliases":["nail_care"]},{"emoji":"📛","aliases":["name_badge"]},{"emoji":"🇳🇦","aliases":["namibia"]},{"emoji":"🏞️","aliases":["national_park"]},{"emoji":"🇳🇷","aliases":["nauru"]},{"emoji":"🤢","aliases":["nauseated_face"]},{"emoji":"🧿","aliases":["nazar_amulet"]},{"emoji":"👔","aliases":["necktie"]},{"emoji":"❎","aliases":["negative_squared_cross_mark"]},{"emoji":"🇳🇵","aliases":["nepal"]},{"emoji":"🤓","aliases":["nerd_face"]},{"emoji":"🪺","aliases":["nest_with_eggs"]},{"emoji":"🪆","aliases":["nesting_dolls"]},{"emoji":"🇳🇱","aliases":["netherlands"]},{"emoji":"😐","aliases":["neutral_face"]},{"emoji":"🆕","aliases":["new"]},{"emoji":"🇳🇨","aliases":["new_caledonia"]},{"emoji":"🌑","aliases":["new_moon"]},{"emoji":"🌚","aliases":["new_moon_with_face"]},{"emoji":"🇳🇿","aliases":["new_zealand"]},{"emoji":"📰","aliases":["newspaper"]},{"emoji":"🗞️","aliases":["newspaper_roll"]},{"emoji":"⏭️","aliases":["next_track_button"]},{"emoji":"🆖","aliases":["ng"]},{"emoji":"🇳🇮","aliases":["nicaragua"]},{"emoji":"🇳🇪","aliases":["niger"]},{"emoji":"🇳🇬","aliases":["nigeria"]},{"emoji":"🌃","aliases":["night_with_stars"]},{"emoji":"9️⃣","aliases":["nine"]},{"emoji":"🥷","aliases":["ninja"]},{"emoji":"🇳🇺","aliases":["niue"]},{"emoji":"🔕","aliases":["no_bell"]},{"emoji":"🚳","aliases":["no_bicycles"]},{"emoji":"⛔","aliases":["no_entry"]},{"emoji":"🚫","aliases":["no_entry_sign"]},{"emoji":"🙅","aliases":["no_good"]},{"emoji":"🙅‍♂️","aliases":["no_good_man","ng_man"]},{"emoji":"🙅‍♀️","aliases":["no_good_woman","ng_woman"]},{"emoji":"📵","aliases":["no_mobile_phones"]},{"emoji":"😶","aliases":["no_mouth"]},{"emoji":"🚷","aliases":["no_pedestrians"]},{"emoji":"🚭","aliases":["no_smoking"]},{"emoji":"🚱","aliases":["non-potable_water"]},{"emoji":"🇳🇫","aliases":["norfolk_island"]},{"emoji":"🇰🇵","aliases":["north_korea"]},{"emoji":"🇲🇵","aliases":["northern_mariana_islands"]},{"emoji":"🇳🇴","aliases":["norway"]},{"emoji":"👃","aliases":["nose"]},{"emoji":"📓","aliases":["notebook"]},{"emoji":"📔","aliases":["notebook_with_decorative_cover"]},{"emoji":"🎶","aliases":["notes"]},{"emoji":"🔩","aliases":["nut_and_bolt"]},{"emoji":"⭕","aliases":["o"]},{"emoji":"🅾️","aliases":["o2"]},{"emoji":"🌊","aliases":["ocean"]},{"emoji":"🐙","aliases":["octopus"]},{"emoji":"🍢","aliases":["oden"]},{"emoji":"🏢","aliases":["office"]},{"emoji":"🧑‍💼","aliases":["office_worker"]},{"emoji":"🛢️","aliases":["oil_drum"]},{"emoji":"🆗","aliases":["ok"]},{"emoji":"👌","aliases":["ok_hand"]},{"emoji":"🙆‍♂️","aliases":["ok_man"]},{"emoji":"🙆","aliases":["ok_person"]},{"emoji":"🙆‍♀️","aliases":["ok_woman"]},{"emoji":"🗝️","aliases":["old_key"]},{"emoji":"🧓","aliases":["older_adult"]},{"emoji":"👴","aliases":["older_man"]},{"emoji":"👵","aliases":["older_woman"]},{"emoji":"🫒","aliases":["olive"]},{"emoji":"🕉️","aliases":["om"]},{"emoji":"🇴🇲","aliases":["oman"]},{"emoji":"🔛","aliases":["on"]},{"emoji":"🚘","aliases":["oncoming_automobile"]},{"emoji":"🚍","aliases":["oncoming_bus"]},{"emoji":"🚔","aliases":["oncoming_police_car"]},{"emoji":"🚖","aliases":["oncoming_taxi"]},{"emoji":"1️⃣","aliases":["one"]},{"emoji":"🩱","aliases":["one_piece_swimsuit"]},{"emoji":"🧅","aliases":["onion"]},{"emoji":"📂","aliases":["open_file_folder"]},{"emoji":"👐","aliases":["open_hands"]},{"emoji":"😮","aliases":["open_mouth"]},{"emoji":"☂️","aliases":["open_umbrella"]},{"emoji":"⛎","aliases":["ophiuchus"]},{"emoji":"📙","aliases":["orange_book"]},{"emoji":"🟠","aliases":["orange_circle"]},{"emoji":"🧡","aliases":["orange_heart"]},{"emoji":"🟧","aliases":["orange_square"]},{"emoji":"🦧","aliases":["orangutan"]},{"emoji":"☦️","aliases":["orthodox_cross"]},{"emoji":"🦦","aliases":["otter"]},{"emoji":"📤","aliases":["outbox_tray"]},{"emoji":"🦉","aliases":["owl"]},{"emoji":"🐂","aliases":["ox"]},{"emoji":"🦪","aliases":["oyster"]},{"emoji":"📦","aliases":["package"]},{"emoji":"📄","aliases":["page_facing_up"]},{"emoji":"📃","aliases":["page_with_curl"]},{"emoji":"📟","aliases":["pager"]},{"emoji":"🖌️","aliases":["paintbrush"]},{"emoji":"🇵🇰","aliases":["pakistan"]},{"emoji":"🇵🇼","aliases":["palau"]},{"emoji":"🇵🇸","aliases":["palestinian_territories"]},{"emoji":"🫳","aliases":["palm_down_hand"]},{"emoji":"🌴","aliases":["palm_tree"]},{"emoji":"🫴","aliases":["palm_up_hand"]},{"emoji":"🤲","aliases":["palms_up_together"]},{"emoji":"🇵🇦","aliases":["panama"]},{"emoji":"🥞","aliases":["pancakes"]},{"emoji":"🐼","aliases":["panda_face"]},{"emoji":"📎","aliases":["paperclip"]},{"emoji":"🖇️","aliases":["paperclips"]},{"emoji":"🇵🇬","aliases":["papua_new_guinea"]},{"emoji":"🪂","aliases":["parachute"]},{"emoji":"🇵🇾","aliases":["paraguay"]},{"emoji":"⛱️","aliases":["parasol_on_ground"]},{"emoji":"🅿️","aliases":["parking"]},{"emoji":"🦜","aliases":["parrot"]},{"emoji":"〽️","aliases":["part_alternation_mark"]},{"emoji":"⛅","aliases":["partly_sunny"]},{"emoji":"🥳","aliases":["partying_face"]},{"emoji":"🛳️","aliases":["passenger_ship"]},{"emoji":"🛂","aliases":["passport_control"]},{"emoji":"⏸️","aliases":["pause_button"]},{"emoji":"🫛","aliases":["pea_pod"]},{"emoji":"☮️","aliases":["peace_symbol"]},{"emoji":"🍑","aliases":["peach"]},{"emoji":"🦚","aliases":["peacock"]},{"emoji":"🥜","aliases":["peanuts"]},{"emoji":"🍐","aliases":["pear"]},{"emoji":"🖊️","aliases":["pen"]},{"emoji":"✏️","aliases":["pencil2"]},{"emoji":"🐧","aliases":["penguin"]},{"emoji":"😔","aliases":["pensive"]},{"emoji":"🧑‍🤝‍🧑","aliases":["people_holding_hands"]},{"emoji":"🫂","aliases":["people_hugging"]},{"emoji":"🎭","aliases":["performing_arts"]},{"emoji":"😣","aliases":["persevere"]},{"emoji":"🧑‍🦲","aliases":["person_bald"]},{"emoji":"🧑‍🦱","aliases":["person_curly_hair"]},{"emoji":"🧑‍🍼","aliases":["person_feeding_baby"]},{"emoji":"🤺","aliases":["person_fencing"]},{"emoji":"🧑‍🦽","aliases":["person_in_manual_wheelchair"]},{"emoji":"🧑‍🦼","aliases":["person_in_motorized_wheelchair"]},{"emoji":"🤵","aliases":["person_in_tuxedo"]},{"emoji":"🧑‍🦰","aliases":["person_red_hair"]},{"emoji":"🧑‍🦳","aliases":["person_white_hair"]},{"emoji":"🫅","aliases":["person_with_crown"]},{"emoji":"🧑‍🦯","aliases":["person_with_probing_cane"]},{"emoji":"👳","aliases":["person_with_turban"]},{"emoji":"👰","aliases":["person_with_veil"]},{"emoji":"🇵🇪","aliases":["peru"]},{"emoji":"🧫","aliases":["petri_dish"]},{"emoji":"🇵🇭","aliases":["philippines"]},{"emoji":"☎️","aliases":["phone","telephone"]},{"emoji":"⛏️","aliases":["pick"]},{"emoji":"🛻","aliases":["pickup_truck"]},{"emoji":"🥧","aliases":["pie"]},{"emoji":"🐷","aliases":["pig"]},{"emoji":"🐖","aliases":["pig2"]},{"emoji":"🐽","aliases":["pig_nose"]},{"emoji":"💊","aliases":["pill"]},{"emoji":"🧑‍✈️","aliases":["pilot"]},{"emoji":"🪅","aliases":["pinata"]},{"emoji":"🤌","aliases":["pinched_fingers"]},{"emoji":"🤏","aliases":["pinching_hand"]},{"emoji":"🍍","aliases":["pineapple"]},{"emoji":"🏓","aliases":["ping_pong"]},{"emoji":"🩷","aliases":["pink_heart"]},{"emoji":"🏴‍☠️","aliases":["pirate_flag"]},{"emoji":"♓","aliases":["pisces"]},{"emoji":"🇵🇳","aliases":["pitcairn_islands"]},{"emoji":"🍕","aliases":["pizza"]},{"emoji":"🪧","aliases":["placard"]},{"emoji":"🛐","aliases":["place_of_worship"]},{"emoji":"🍽️","aliases":["plate_with_cutlery"]},{"emoji":"⏯️","aliases":["play_or_pause_button"]},{"emoji":"🛝","aliases":["playground_slide"]},{"emoji":"🥺","aliases":["pleading_face"]},{"emoji":"🪠","aliases":["plunger"]},{"emoji":"👇","aliases":["point_down"]},{"emoji":"👈","aliases":["point_left"]},{"emoji":"👉","aliases":["point_right"]},{"emoji":"☝️","aliases":["point_up"]},{"emoji":"👆","aliases":["point_up_2"]},{"emoji":"🇵🇱","aliases":["poland"]},{"emoji":"🐻‍❄️","aliases":["polar_bear"]},{"emoji":"🚓","aliases":["police_car"]},{"emoji":"👮","aliases":["police_officer","cop"]},{"emoji":"👮‍♂️","aliases":["policeman"]},{"emoji":"👮‍♀️","aliases":["policewoman"]},{"emoji":"🐩","aliases":["poodle"]},{"emoji":"🍿","aliases":["popcorn"]},{"emoji":"🇵🇹","aliases":["portugal"]},{"emoji":"🏣","aliases":["post_office"]},{"emoji":"📯","aliases":["postal_horn"]},{"emoji":"📮","aliases":["postbox"]},{"emoji":"🚰","aliases":["potable_water"]},{"emoji":"🥔","aliases":["potato"]},{"emoji":"🪴","aliases":["potted_plant"]},{"emoji":"👝","aliases":["pouch"]},{"emoji":"🍗","aliases":["poultry_leg"]},{"emoji":"💷","aliases":["pound"]},{"emoji":"🫗","aliases":["pouring_liquid"]},{"emoji":"😾","aliases":["pouting_cat"]},{"emoji":"🙎","aliases":["pouting_face"]},{"emoji":"🙎‍♂️","aliases":["pouting_man"]},{"emoji":"🙎‍♀️","aliases":["pouting_woman"]},{"emoji":"🙏","aliases":["pray"]},{"emoji":"📿","aliases":["prayer_beads"]},{"emoji":"🫃","aliases":["pregnant_man"]},{"emoji":"🫄","aliases":["pregnant_person"]},{"emoji":"🤰","aliases":["pregnant_woman"]},{"emoji":"🥨","aliases":["pretzel"]},{"emoji":"⏮️","aliases":["previous_track_button"]},{"emoji":"🤴","aliases":["prince"]},{"emoji":"👸","aliases":["princess"]},{"emoji":"🖨️","aliases":["printer"]},{"emoji":"🦯","aliases":["probing_cane"]},{"emoji":"🇵🇷","aliases":["puerto_rico"]},{"emoji":"🟣","aliases":["purple_circle"]},{"emoji":"💜","aliases":["purple_heart"]},{"emoji":"🟪","aliases":["purple_square"]},{"emoji":"👛","aliases":["purse"]},{"emoji":"📌","aliases":["pushpin"]},{"emoji":"🚮","aliases":["put_litter_in_its_place"]},{"emoji":"🇶🇦","aliases":["qatar"]},{"emoji":"❓","aliases":["question"]},{"emoji":"🐰","aliases":["rabbit"]},{"emoji":"🐇","aliases":["rabbit2"]},{"emoji":"🦝","aliases":["raccoon"]},{"emoji":"🐎","aliases":["racehorse"]},{"emoji":"🏎️","aliases":["racing_car"]},{"emoji":"📻","aliases":["radio"]},{"emoji":"🔘","aliases":["radio_button"]},{"emoji":"☢️","aliases":["radioactive"]},{"emoji":"😡","aliases":["rage","pout"]},{"emoji":"🚃","aliases":["railway_car"]},{"emoji":"🛤️","aliases":["railway_track"]},{"emoji":"🌈","aliases":["rainbow"]},{"emoji":"🏳️‍🌈","aliases":["rainbow_flag"]},{"emoji":"🤚","aliases":["raised_back_of_hand"]},{"emoji":"🤨","aliases":["raised_eyebrow"]},{"emoji":"🖐️","aliases":["raised_hand_with_fingers_splayed"]},{"emoji":"🙌","aliases":["raised_hands"]},{"emoji":"🙋","aliases":["raising_hand"]},{"emoji":"🙋‍♂️","aliases":["raising_hand_man"]},{"emoji":"🙋‍♀️","aliases":["raising_hand_woman"]},{"emoji":"🐏","aliases":["ram"]},{"emoji":"🍜","aliases":["ramen"]},{"emoji":"🐀","aliases":["rat"]},{"emoji":"🪒","aliases":["razor"]},{"emoji":"🧾","aliases":["receipt"]},{"emoji":"⏺️","aliases":["record_button"]},{"emoji":"♻️","aliases":["recycle"]},{"emoji":"🔴","aliases":["red_circle"]},{"emoji":"🧧","aliases":["red_envelope"]},{"emoji":"👨‍🦰","aliases":["red_haired_man"]},{"emoji":"👩‍🦰","aliases":["red_haired_woman"]},{"emoji":"🟥","aliases":["red_square"]},{"emoji":"®️","aliases":["registered"]},{"emoji":"☺️","aliases":["relaxed"]},{"emoji":"😌","aliases":["relieved"]},{"emoji":"🎗️","aliases":["reminder_ribbon"]},{"emoji":"🔁","aliases":["repeat"]},{"emoji":"🔂","aliases":["repeat_one"]},{"emoji":"⛑️","aliases":["rescue_worker_helmet"]},{"emoji":"🚻","aliases":["restroom"]},{"emoji":"🇷🇪","aliases":["reunion"]},{"emoji":"💞","aliases":["revolving_hearts"]},{"emoji":"⏪","aliases":["rewind"]},{"emoji":"🦏","aliases":["rhinoceros"]},{"emoji":"🎀","aliases":["ribbon"]},{"emoji":"🍚","aliases":["rice"]},{"emoji":"🍙","aliases":["rice_ball"]},{"emoji":"🍘","aliases":["rice_cracker"]},{"emoji":"🎑","aliases":["rice_scene"]},{"emoji":"🗯️","aliases":["right_anger_bubble"]},{"emoji":"🫱","aliases":["rightwards_hand"]},{"emoji":"🫸","aliases":["rightwards_pushing_hand"]},{"emoji":"💍","aliases":["ring"]},{"emoji":"🛟","aliases":["ring_buoy"]},{"emoji":"🪐","aliases":["ringed_planet"]},{"emoji":"🤖","aliases":["robot"]},{"emoji":"🪨","aliases":["rock"]},{"emoji":"🚀","aliases":["rocket"]},{"emoji":"🤣","aliases":["rofl"]},{"emoji":"🙄","aliases":["roll_eyes"]},{"emoji":"🧻","aliases":["roll_of_paper"]},{"emoji":"🎢","aliases":["roller_coaster"]},{"emoji":"🛼","aliases":["roller_skate"]},{"emoji":"🇷🇴","aliases":["romania"]},{"emoji":"🐓","aliases":["rooster"]},{"emoji":"🌹","aliases":["rose"]},{"emoji":"🏵️","aliases":["rosette"]},{"emoji":"🚨","aliases":["rotating_light"]},{"emoji":"📍","aliases":["round_pushpin"]},{"emoji":"🚣","aliases":["rowboat"]},{"emoji":"🚣‍♂️","aliases":["rowing_man"]},{"emoji":"🚣‍♀️","aliases":["rowing_woman"]},{"emoji":"🇷🇺","aliases":["ru"]},{"emoji":"🏉","aliases":["rugby_football"]},{"emoji":"🏃","aliases":["runner","running"]},{"emoji":"🏃‍♂️","aliases":["running_man"]},{"emoji":"🎽","aliases":["running_shirt_with_sash"]},{"emoji":"🏃‍♀️","aliases":["running_woman"]},{"emoji":"🇷🇼","aliases":["rwanda"]},{"emoji":"🈂️","aliases":["sa"]},{"emoji":"🧷","aliases":["safety_pin"]},{"emoji":"🦺","aliases":["safety_vest"]},{"emoji":"♐","aliases":["sagittarius"]},{"emoji":"🍶","aliases":["sake"]},{"emoji":"🧂","aliases":["salt"]},{"emoji":"🫡","aliases":["saluting_face"]},{"emoji":"🇼🇸","aliases":["samoa"]},{"emoji":"🇸🇲","aliases":["san_marino"]},{"emoji":"👡","aliases":["sandal"]},{"emoji":"🥪","aliases":["sandwich"]},{"emoji":"🎅","aliases":["santa"]},{"emoji":"🇸🇹","aliases":["sao_tome_principe"]},{"emoji":"🥻","aliases":["sari"]},{"emoji":"📡","aliases":["satellite"]},{"emoji":"🇸🇦","aliases":["saudi_arabia"]},{"emoji":"🧖‍♂️","aliases":["sauna_man"]},{"emoji":"🧖","aliases":["sauna_person"]},{"emoji":"🧖‍♀️","aliases":["sauna_woman"]},{"emoji":"🦕","aliases":["sauropod"]},{"emoji":"🎷","aliases":["saxophone"]},{"emoji":"🧣","aliases":["scarf"]},{"emoji":"🏫","aliases":["school"]},{"emoji":"🎒","aliases":["school_satchel"]},{"emoji":"🧑‍🔬","aliases":["scientist"]},{"emoji":"✂️","aliases":["scissors"]},{"emoji":"🦂","aliases":["scorpion"]},{"emoji":"♏","aliases":["scorpius"]},{"emoji":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","aliases":["scotland"]},{"emoji":"😱","aliases":["scream"]},{"emoji":"🙀","aliases":["scream_cat"]},{"emoji":"🪛","aliases":["screwdriver"]},{"emoji":"📜","aliases":["scroll"]},{"emoji":"🦭","aliases":["seal"]},{"emoji":"💺","aliases":["seat"]},{"emoji":"㊙️","aliases":["secret"]},{"emoji":"🙈","aliases":["see_no_evil"]},{"emoji":"🌱","aliases":["seedling"]},{"emoji":"🤳","aliases":["selfie"]},{"emoji":"🇸🇳","aliases":["senegal"]},{"emoji":"🇷🇸","aliases":["serbia"]},{"emoji":"🐕‍🦺","aliases":["service_dog"]},{"emoji":"7️⃣","aliases":["seven"]},{"emoji":"🪡","aliases":["sewing_needle"]},{"emoji":"🇸🇨","aliases":["seychelles"]},{"emoji":"🫨","aliases":["shaking_face"]},{"emoji":"🥘","aliases":["shallow_pan_of_food"]},{"emoji":"☘️","aliases":["shamrock"]},{"emoji":"🦈","aliases":["shark"]},{"emoji":"🍧","aliases":["shaved_ice"]},{"emoji":"🐑","aliases":["sheep"]},{"emoji":"🐚","aliases":["shell"]},{"emoji":"🛡️","aliases":["shield"]},{"emoji":"⛩️","aliases":["shinto_shrine"]},{"emoji":"🚢","aliases":["ship"]},{"emoji":"👕","aliases":["shirt","tshirt"]},{"emoji":"🛍️","aliases":["shopping"]},{"emoji":"🛒","aliases":["shopping_cart"]},{"emoji":"🩳","aliases":["shorts"]},{"emoji":"🚿","aliases":["shower"]},{"emoji":"🦐","aliases":["shrimp"]},{"emoji":"🤷","aliases":["shrug"]},{"emoji":"🤫","aliases":["shushing_face"]},{"emoji":"🇸🇱","aliases":["sierra_leone"]},{"emoji":"📶","aliases":["signal_strength"]},{"emoji":"🇸🇬","aliases":["singapore"]},{"emoji":"🧑‍🎤","aliases":["singer"]},{"emoji":"🇸🇽","aliases":["sint_maarten"]},{"emoji":"6️⃣","aliases":["six"]},{"emoji":"🔯","aliases":["six_pointed_star"]},{"emoji":"🛹","aliases":["skateboard"]},{"emoji":"🎿","aliases":["ski"]},{"emoji":"⛷️","aliases":["skier"]},{"emoji":"💀","aliases":["skull"]},{"emoji":"☠️","aliases":["skull_and_crossbones"]},{"emoji":"🦨","aliases":["skunk"]},{"emoji":"🛷","aliases":["sled"]},{"emoji":"😴","aliases":["sleeping"]},{"emoji":"🛌","aliases":["sleeping_bed"]},{"emoji":"😪","aliases":["sleepy"]},{"emoji":"🙁","aliases":["slightly_frowning_face"]},{"emoji":"🙂","aliases":["slightly_smiling_face"]},{"emoji":"🎰","aliases":["slot_machine"]},{"emoji":"🦥","aliases":["sloth"]},{"emoji":"🇸🇰","aliases":["slovakia"]},{"emoji":"🇸🇮","aliases":["slovenia"]},{"emoji":"🛩️","aliases":["small_airplane"]},{"emoji":"🔹","aliases":["small_blue_diamond"]},{"emoji":"🔸","aliases":["small_orange_diamond"]},{"emoji":"🔺","aliases":["small_red_triangle"]},{"emoji":"🔻","aliases":["small_red_triangle_down"]},{"emoji":"😄","aliases":["smile"]},{"emoji":"😸","aliases":["smile_cat"]},{"emoji":"😃","aliases":["smiley"]},{"emoji":"😺","aliases":["smiley_cat"]},{"emoji":"🥲","aliases":["smiling_face_with_tear"]},{"emoji":"🥰","aliases":["smiling_face_with_three_hearts"]},{"emoji":"😈","aliases":["smiling_imp"]},{"emoji":"😏","aliases":["smirk"]},{"emoji":"😼","aliases":["smirk_cat"]},{"emoji":"🚬","aliases":["smoking"]},{"emoji":"🐌","aliases":["snail"]},{"emoji":"🐍","aliases":["snake"]},{"emoji":"🤧","aliases":["sneezing_face"]},{"emoji":"🏂","aliases":["snowboarder"]},{"emoji":"❄️","aliases":["snowflake"]},{"emoji":"⛄","aliases":["snowman"]},{"emoji":"☃️","aliases":["snowman_with_snow"]},{"emoji":"🧼","aliases":["soap"]},{"emoji":"😭","aliases":["sob"]},{"emoji":"⚽","aliases":["soccer"]},{"emoji":"🧦","aliases":["socks"]},{"emoji":"🥎","aliases":["softball"]},{"emoji":"🇸🇧","aliases":["solomon_islands"]},{"emoji":"🇸🇴","aliases":["somalia"]},{"emoji":"🔜","aliases":["soon"]},{"emoji":"🆘","aliases":["sos"]},{"emoji":"🔉","aliases":["sound"]},{"emoji":"🇿🇦","aliases":["south_africa"]},{"emoji":"🇬🇸","aliases":["south_georgia_south_sandwich_islands"]},{"emoji":"🇸🇸","aliases":["south_sudan"]},{"emoji":"👾","aliases":["space_invader"]},{"emoji":"♠️","aliases":["spades"]},{"emoji":"🍝","aliases":["spaghetti"]},{"emoji":"❇️","aliases":["sparkle"]},{"emoji":"🎇","aliases":["sparkler"]},{"emoji":"✨","aliases":["sparkles"]},{"emoji":"💖","aliases":["sparkling_heart"]},{"emoji":"🙊","aliases":["speak_no_evil"]},{"emoji":"🔈","aliases":["speaker"]},{"emoji":"🗣️","aliases":["speaking_head"]},{"emoji":"💬","aliases":["speech_balloon"]},{"emoji":"🚤","aliases":["speedboat"]},{"emoji":"🕷️","aliases":["spider"]},{"emoji":"🕸️","aliases":["spider_web"]},{"emoji":"🗓️","aliases":["spiral_calendar"]},{"emoji":"🗒️","aliases":["spiral_notepad"]},{"emoji":"🧽","aliases":["sponge"]},{"emoji":"🥄","aliases":["spoon"]},{"emoji":"🦑","aliases":["squid"]},{"emoji":"🇱🇰","aliases":["sri_lanka"]},{"emoji":"🇧🇱","aliases":["st_barthelemy"]},{"emoji":"🇸🇭","aliases":["st_helena"]},{"emoji":"🇰🇳","aliases":["st_kitts_nevis"]},{"emoji":"🇱🇨","aliases":["st_lucia"]},{"emoji":"🇲🇫","aliases":["st_martin"]},{"emoji":"🇵🇲","aliases":["st_pierre_miquelon"]},{"emoji":"🇻🇨","aliases":["st_vincent_grenadines"]},{"emoji":"🏟️","aliases":["stadium"]},{"emoji":"🧍‍♂️","aliases":["standing_man"]},{"emoji":"🧍","aliases":["standing_person"]},{"emoji":"🧍‍♀️","aliases":["standing_woman"]},{"emoji":"⭐","aliases":["star"]},{"emoji":"🌟","aliases":["star2"]},{"emoji":"☪️","aliases":["star_and_crescent"]},{"emoji":"✡️","aliases":["star_of_david"]},{"emoji":"🤩","aliases":["star_struck"]},{"emoji":"🌠","aliases":["stars"]},{"emoji":"🚉","aliases":["station"]},{"emoji":"🗽","aliases":["statue_of_liberty"]},{"emoji":"🚂","aliases":["steam_locomotive"]},{"emoji":"🩺","aliases":["stethoscope"]},{"emoji":"🍲","aliases":["stew"]},{"emoji":"⏹️","aliases":["stop_button"]},{"emoji":"🛑","aliases":["stop_sign"]},{"emoji":"⏱️","aliases":["stopwatch"]},{"emoji":"📏","aliases":["straight_ruler"]},{"emoji":"🍓","aliases":["strawberry"]},{"emoji":"😛","aliases":["stuck_out_tongue"]},{"emoji":"😝","aliases":["stuck_out_tongue_closed_eyes"]},{"emoji":"😜","aliases":["stuck_out_tongue_winking_eye"]},{"emoji":"🧑‍🎓","aliases":["student"]},{"emoji":"🎙️","aliases":["studio_microphone"]},{"emoji":"🥙","aliases":["stuffed_flatbread"]},{"emoji":"🇸🇩","aliases":["sudan"]},{"emoji":"🌥️","aliases":["sun_behind_large_cloud"]},{"emoji":"🌦️","aliases":["sun_behind_rain_cloud"]},{"emoji":"🌤️","aliases":["sun_behind_small_cloud"]},{"emoji":"🌞","aliases":["sun_with_face"]},{"emoji":"🌻","aliases":["sunflower"]},{"emoji":"😎","aliases":["sunglasses"]},{"emoji":"☀️","aliases":["sunny"]},{"emoji":"🌅","aliases":["sunrise"]},{"emoji":"🌄","aliases":["sunrise_over_mountains"]},{"emoji":"🦸","aliases":["superhero"]},{"emoji":"🦸‍♂️","aliases":["superhero_man"]},{"emoji":"🦸‍♀️","aliases":["superhero_woman"]},{"emoji":"🦹","aliases":["supervillain"]},{"emoji":"🦹‍♂️","aliases":["supervillain_man"]},{"emoji":"🦹‍♀️","aliases":["supervillain_woman"]},{"emoji":"🏄","aliases":["surfer"]},{"emoji":"🏄‍♂️","aliases":["surfing_man"]},{"emoji":"🏄‍♀️","aliases":["surfing_woman"]},{"emoji":"🇸🇷","aliases":["suriname"]},{"emoji":"🍣","aliases":["sushi"]},{"emoji":"🚟","aliases":["suspension_railway"]},{"emoji":"🇸🇯","aliases":["svalbard_jan_mayen"]},{"emoji":"🦢","aliases":["swan"]},{"emoji":"🇸🇿","aliases":["swaziland"]},{"emoji":"😓","aliases":["sweat"]},{"emoji":"💦","aliases":["sweat_drops"]},{"emoji":"😅","aliases":["sweat_smile"]},{"emoji":"🇸🇪","aliases":["sweden"]},{"emoji":"🍠","aliases":["sweet_potato"]},{"emoji":"🩲","aliases":["swim_brief"]},{"emoji":"🏊","aliases":["swimmer"]},{"emoji":"🏊‍♂️","aliases":["swimming_man"]},{"emoji":"🏊‍♀️","aliases":["swimming_woman"]},{"emoji":"🇨🇭","aliases":["switzerland"]},{"emoji":"🔣","aliases":["symbols"]},{"emoji":"🕍","aliases":["synagogue"]},{"emoji":"🇸🇾","aliases":["syria"]},{"emoji":"💉","aliases":["syringe"]},{"emoji":"🦖","aliases":["t-rex"]},{"emoji":"🌮","aliases":["taco"]},{"emoji":"🎉","aliases":["tada","hooray"]},{"emoji":"🇹🇼","aliases":["taiwan"]},{"emoji":"🇹🇯","aliases":["tajikistan"]},{"emoji":"🥡","aliases":["takeout_box"]},{"emoji":"🫔","aliases":["tamale"]},{"emoji":"🎋","aliases":["tanabata_tree"]},{"emoji":"🍊","aliases":["tangerine","orange","mandarin"]},{"emoji":"🇹🇿","aliases":["tanzania"]},{"emoji":"♉","aliases":["taurus"]},{"emoji":"🚕","aliases":["taxi"]},{"emoji":"🍵","aliases":["tea"]},{"emoji":"🧑‍🏫","aliases":["teacher"]},{"emoji":"🫖","aliases":["teapot"]},{"emoji":"🧑‍💻","aliases":["technologist"]},{"emoji":"🧸","aliases":["teddy_bear"]},{"emoji":"📞","aliases":["telephone_receiver"]},{"emoji":"🔭","aliases":["telescope"]},{"emoji":"🎾","aliases":["tennis"]},{"emoji":"⛺","aliases":["tent"]},{"emoji":"🧪","aliases":["test_tube"]},{"emoji":"🇹🇭","aliases":["thailand"]},{"emoji":"🌡️","aliases":["thermometer"]},{"emoji":"🤔","aliases":["thinking"]},{"emoji":"🩴","aliases":["thong_sandal"]},{"emoji":"💭","aliases":["thought_balloon"]},{"emoji":"🧵","aliases":["thread"]},{"emoji":"3️⃣","aliases":["three"]},{"emoji":"🎫","aliases":["ticket"]},{"emoji":"🎟️","aliases":["tickets"]},{"emoji":"🐯","aliases":["tiger"]},{"emoji":"🐅","aliases":["tiger2"]},{"emoji":"⏲️","aliases":["timer_clock"]},{"emoji":"🇹🇱","aliases":["timor_leste"]},{"emoji":"💁‍♂️","aliases":["tipping_hand_man","sassy_man"]},{"emoji":"💁","aliases":["tipping_hand_person","information_desk_person"]},{"emoji":"💁‍♀️","aliases":["tipping_hand_woman","sassy_woman"]},{"emoji":"😫","aliases":["tired_face"]},{"emoji":"™️","aliases":["tm"]},{"emoji":"🇹🇬","aliases":["togo"]},{"emoji":"🚽","aliases":["toilet"]},{"emoji":"🇹🇰","aliases":["tokelau"]},{"emoji":"🗼","aliases":["tokyo_tower"]},{"emoji":"🍅","aliases":["tomato"]},{"emoji":"🇹🇴","aliases":["tonga"]},{"emoji":"👅","aliases":["tongue"]},{"emoji":"🧰","aliases":["toolbox"]},{"emoji":"🦷","aliases":["tooth"]},{"emoji":"🪥","aliases":["toothbrush"]},{"emoji":"🔝","aliases":["top"]},{"emoji":"🎩","aliases":["tophat"]},{"emoji":"🌪️","aliases":["tornado"]},{"emoji":"🇹🇷","aliases":["tr"]},{"emoji":"🖲️","aliases":["trackball"]},{"emoji":"🚜","aliases":["tractor"]},{"emoji":"🚥","aliases":["traffic_light"]},{"emoji":"🚋","aliases":["train"]},{"emoji":"🚆","aliases":["train2"]},{"emoji":"🚊","aliases":["tram"]},{"emoji":"🏳️‍⚧️","aliases":["transgender_flag"]},{"emoji":"⚧️","aliases":["transgender_symbol"]},{"emoji":"🚩","aliases":["triangular_flag_on_post"]},{"emoji":"📐","aliases":["triangular_ruler"]},{"emoji":"🔱","aliases":["trident"]},{"emoji":"🇹🇹","aliases":["trinidad_tobago"]},{"emoji":"🇹🇦","aliases":["tristan_da_cunha"]},{"emoji":"😤","aliases":["triumph"]},{"emoji":"🧌","aliases":["troll"]},{"emoji":"🚎","aliases":["trolleybus"]},{"emoji":"🏆","aliases":["trophy"]},{"emoji":"🍹","aliases":["tropical_drink"]},{"emoji":"🐠","aliases":["tropical_fish"]},{"emoji":"🚚","aliases":["truck"]},{"emoji":"🎺","aliases":["trumpet"]},{"emoji":"🌷","aliases":["tulip"]},{"emoji":"🥃","aliases":["tumbler_glass"]},{"emoji":"🇹🇳","aliases":["tunisia"]},{"emoji":"🦃","aliases":["turkey"]},{"emoji":"🇹🇲","aliases":["turkmenistan"]},{"emoji":"🇹🇨","aliases":["turks_caicos_islands"]},{"emoji":"🐢","aliases":["turtle"]},{"emoji":"🇹🇻","aliases":["tuvalu"]},{"emoji":"📺","aliases":["tv"]},{"emoji":"🔀","aliases":["twisted_rightwards_arrows"]},{"emoji":"2️⃣","aliases":["two"]},{"emoji":"💕","aliases":["two_hearts"]},{"emoji":"👬","aliases":["two_men_holding_hands"]},{"emoji":"👭","aliases":["two_women_holding_hands"]},{"emoji":"🈹","aliases":["u5272"]},{"emoji":"🈴","aliases":["u5408"]},{"emoji":"🈺","aliases":["u55b6"]},{"emoji":"🈯","aliases":["u6307"]},{"emoji":"🈷️","aliases":["u6708"]},{"emoji":"🈶","aliases":["u6709"]},{"emoji":"🈵","aliases":["u6e80"]},{"emoji":"🈚","aliases":["u7121"]},{"emoji":"🈸","aliases":["u7533"]},{"emoji":"🈲","aliases":["u7981"]},{"emoji":"🈳","aliases":["u7a7a"]},{"emoji":"🇺🇬","aliases":["uganda"]},{"emoji":"🇺🇦","aliases":["ukraine"]},{"emoji":"☔","aliases":["umbrella"]},{"emoji":"😒","aliases":["unamused"]},{"emoji":"🔞","aliases":["underage"]},{"emoji":"🦄","aliases":["unicorn"]},{"emoji":"🇦🇪","aliases":["united_arab_emirates"]},{"emoji":"🇺🇳","aliases":["united_nations"]},{"emoji":"🔓","aliases":["unlock"]},{"emoji":"🆙","aliases":["up"]},{"emoji":"🙃","aliases":["upside_down_face"]},{"emoji":"🇺🇾","aliases":["uruguay"]},{"emoji":"🇺🇸","aliases":["us"]},{"emoji":"🇺🇲","aliases":["us_outlying_islands"]},{"emoji":"🇻🇮","aliases":["us_virgin_islands"]},{"emoji":"🇺🇿","aliases":["uzbekistan"]},{"emoji":"✌️","aliases":["v"]},{"emoji":"🧛","aliases":["vampire"]},{"emoji":"🧛‍♂️","aliases":["vampire_man"]},{"emoji":"🧛‍♀️","aliases":["vampire_woman"]},{"emoji":"🇻🇺","aliases":["vanuatu"]},{"emoji":"🇻🇦","aliases":["vatican_city"]},{"emoji":"🇻🇪","aliases":["venezuela"]},{"emoji":"🚦","aliases":["vertical_traffic_light"]},{"emoji":"📼","aliases":["vhs"]},{"emoji":"📳","aliases":["vibration_mode"]},{"emoji":"📹","aliases":["video_camera"]},{"emoji":"🎮","aliases":["video_game"]},{"emoji":"🇻🇳","aliases":["vietnam"]},{"emoji":"🎻","aliases":["violin"]},{"emoji":"♍","aliases":["virgo"]},{"emoji":"🌋","aliases":["volcano"]},{"emoji":"🏐","aliases":["volleyball"]},{"emoji":"🤮","aliases":["vomiting_face"]},{"emoji":"🆚","aliases":["vs"]},{"emoji":"🖖","aliases":["vulcan_salute"]},{"emoji":"🧇","aliases":["waffle"]},{"emoji":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","aliases":["wales"]},{"emoji":"🚶","aliases":["walking"]},{"emoji":"🚶‍♂️","aliases":["walking_man"]},{"emoji":"🚶‍♀️","aliases":["walking_woman"]},{"emoji":"🇼🇫","aliases":["wallis_futuna"]},{"emoji":"🌘","aliases":["waning_crescent_moon"]},{"emoji":"🌖","aliases":["waning_gibbous_moon"]},{"emoji":"⚠️","aliases":["warning"]},{"emoji":"🗑️","aliases":["wastebasket"]},{"emoji":"⌚","aliases":["watch"]},{"emoji":"🐃","aliases":["water_buffalo"]},{"emoji":"🤽","aliases":["water_polo"]},{"emoji":"🍉","aliases":["watermelon"]},{"emoji":"👋","aliases":["wave"]},{"emoji":"〰️","aliases":["wavy_dash"]},{"emoji":"🌒","aliases":["waxing_crescent_moon"]},{"emoji":"🚾","aliases":["wc"]},{"emoji":"😩","aliases":["weary"]},{"emoji":"💒","aliases":["wedding"]},{"emoji":"🏋️","aliases":["weight_lifting"]},{"emoji":"🏋️‍♂️","aliases":["weight_lifting_man"]},{"emoji":"🏋️‍♀️","aliases":["weight_lifting_woman"]},{"emoji":"🇪🇭","aliases":["western_sahara"]},{"emoji":"🐳","aliases":["whale"]},{"emoji":"🐋","aliases":["whale2"]},{"emoji":"🛞","aliases":["wheel"]},{"emoji":"☸️","aliases":["wheel_of_dharma"]},{"emoji":"♿","aliases":["wheelchair"]},{"emoji":"✅","aliases":["white_check_mark"]},{"emoji":"⚪","aliases":["white_circle"]},{"emoji":"🏳️","aliases":["white_flag"]},{"emoji":"💮","aliases":["white_flower"]},{"emoji":"👨‍🦳","aliases":["white_haired_man"]},{"emoji":"👩‍🦳","aliases":["white_haired_woman"]},{"emoji":"🤍","aliases":["white_heart"]},{"emoji":"⬜","aliases":["white_large_square"]},{"emoji":"◽","aliases":["white_medium_small_square"]},{"emoji":"◻️","aliases":["white_medium_square"]},{"emoji":"▫️","aliases":["white_small_square"]},{"emoji":"🔳","aliases":["white_square_button"]},{"emoji":"🥀","aliases":["wilted_flower"]},{"emoji":"🎐","aliases":["wind_chime"]},{"emoji":"🌬️","aliases":["wind_face"]},{"emoji":"🪟","aliases":["window"]},{"emoji":"🍷","aliases":["wine_glass"]},{"emoji":"🪽","aliases":["wing"]},{"emoji":"😉","aliases":["wink"]},{"emoji":"🛜","aliases":["wireless"]},{"emoji":"🐺","aliases":["wolf"]},{"emoji":"👩","aliases":["woman"]},{"emoji":"👩‍🎨","aliases":["woman_artist"]},{"emoji":"👩‍🚀","aliases":["woman_astronaut"]},{"emoji":"🧔‍♀️","aliases":["woman_beard"]},{"emoji":"🤸‍♀️","aliases":["woman_cartwheeling"]},{"emoji":"👩‍🍳","aliases":["woman_cook"]},{"emoji":"💃","aliases":["woman_dancing","dancer"]},{"emoji":"🤦‍♀️","aliases":["woman_facepalming"]},{"emoji":"👩‍🏭","aliases":["woman_factory_worker"]},{"emoji":"👩‍🌾","aliases":["woman_farmer"]},{"emoji":"👩‍🍼","aliases":["woman_feeding_baby"]},{"emoji":"👩‍🚒","aliases":["woman_firefighter"]},{"emoji":"👩‍⚕️","aliases":["woman_health_worker"]},{"emoji":"👩‍🦽","aliases":["woman_in_manual_wheelchair"]},{"emoji":"👩‍🦼","aliases":["woman_in_motorized_wheelchair"]},{"emoji":"🤵‍♀️","aliases":["woman_in_tuxedo"]},{"emoji":"👩‍⚖️","aliases":["woman_judge"]},{"emoji":"🤹‍♀️","aliases":["woman_juggling"]},{"emoji":"👩‍🔧","aliases":["woman_mechanic"]},{"emoji":"👩‍💼","aliases":["woman_office_worker"]},{"emoji":"👩‍✈️","aliases":["woman_pilot"]},{"emoji":"🤾‍♀️","aliases":["woman_playing_handball"]},{"emoji":"🤽‍♀️","aliases":["woman_playing_water_polo"]},{"emoji":"👩‍🔬","aliases":["woman_scientist"]},{"emoji":"🤷‍♀️","aliases":["woman_shrugging"]},{"emoji":"👩‍🎤","aliases":["woman_singer"]},{"emoji":"👩‍🎓","aliases":["woman_student"]},{"emoji":"👩‍🏫","aliases":["woman_teacher"]},{"emoji":"👩‍💻","aliases":["woman_technologist"]},{"emoji":"🧕","aliases":["woman_with_headscarf"]},{"emoji":"👩‍🦯","aliases":["woman_with_probing_cane"]},{"emoji":"👳‍♀️","aliases":["woman_with_turban"]},{"emoji":"👰‍♀️","aliases":["woman_with_veil","bride_with_veil"]},{"emoji":"👚","aliases":["womans_clothes"]},{"emoji":"👒","aliases":["womans_hat"]},{"emoji":"🤼‍♀️","aliases":["women_wrestling"]},{"emoji":"🚺","aliases":["womens"]},{"emoji":"🪵","aliases":["wood"]},{"emoji":"🥴","aliases":["woozy_face"]},{"emoji":"🗺️","aliases":["world_map"]},{"emoji":"🪱","aliases":["worm"]},{"emoji":"😟","aliases":["worried"]},{"emoji":"🔧","aliases":["wrench"]},{"emoji":"🤼","aliases":["wrestling"]},{"emoji":"✍️","aliases":["writing_hand"]},{"emoji":"❌","aliases":["x"]},{"emoji":"🩻","aliases":["x_ray"]},{"emoji":"🧶","aliases":["yarn"]},{"emoji":"🥱","aliases":["yawning_face"]},{"emoji":"🟡","aliases":["yellow_circle"]},{"emoji":"💛","aliases":["yellow_heart"]},{"emoji":"🟨","aliases":["yellow_square"]},{"emoji":"🇾🇪","aliases":["yemen"]},{"emoji":"💴","aliases":["yen"]},{"emoji":"☯️","aliases":["yin_yang"]},{"emoji":"🪀","aliases":["yo_yo"]},{"emoji":"😋","aliases":["yum"]},{"emoji":"🇿🇲","aliases":["zambia"]},{"emoji":"🤪","aliases":["zany_face"]},{"emoji":"⚡","aliases":["zap"]},{"emoji":"🦓","aliases":["zebra"]},{"emoji":"0️⃣","aliases":["zero"]},{"emoji":"🇿🇼","aliases":["zimbabwe"]},{"emoji":"🤐","aliases":["zipper_mouth_face"]},{"emoji":"🧟","aliases":["zombie"]},{"emoji":"🧟‍♂️","aliases":["zombie_man"]},{"emoji":"🧟‍♀️","aliases":["zombie_woman"]},{"emoji":"💤","aliases":["zzz"]}] \ No newline at end of file +[ + { + "emoji": "👍", + "aliases": [ + "+1", + "thumbsup" + ] + }, + { + "emoji": "👎", + "aliases": [ + "-1", + "thumbsdown" + ] + }, + { + "emoji": "💯", + "aliases": [ + "100" + ] + }, + { + "emoji": "🔢", + "aliases": [ + "1234" + ] + }, + { + "emoji": "🥇", + "aliases": [ + "1st_place_medal" + ] + }, + { + "emoji": "🥈", + "aliases": [ + "2nd_place_medal" + ] + }, + { + "emoji": "🥉", + "aliases": [ + "3rd_place_medal" + ] + }, + { + "emoji": "🎱", + "aliases": [ + "8ball" + ] + }, + { + "emoji": "🅰️", + "aliases": [ + "a" + ] + }, + { + "emoji": "🆎", + "aliases": [ + "ab" + ] + }, + { + "emoji": "🧮", + "aliases": [ + "abacus" + ] + }, + { + "emoji": "🔤", + "aliases": [ + "abc" + ] + }, + { + "emoji": "🔡", + "aliases": [ + "abcd" + ] + }, + { + "emoji": "🉑", + "aliases": [ + "accept" + ] + }, + { + "emoji": "🪗", + "aliases": [ + "accordion" + ] + }, + { + "emoji": "🩹", + "aliases": [ + "adhesive_bandage" + ] + }, + { + "emoji": "🧑", + "aliases": [ + "adult" + ] + }, + { + "emoji": "🚡", + "aliases": [ + "aerial_tramway" + ] + }, + { + "emoji": "🇦🇫", + "aliases": [ + "afghanistan" + ] + }, + { + "emoji": "✈️", + "aliases": [ + "airplane" + ] + }, + { + "emoji": "🇦🇽", + "aliases": [ + "aland_islands" + ] + }, + { + "emoji": "⏰", + "aliases": [ + "alarm_clock" + ] + }, + { + "emoji": "🇦🇱", + "aliases": [ + "albania" + ] + }, + { + "emoji": "⚗️", + "aliases": [ + "alembic" + ] + }, + { + "emoji": "🇩🇿", + "aliases": [ + "algeria" + ] + }, + { + "emoji": "👽", + "aliases": [ + "alien" + ] + }, + { + "emoji": "🚑", + "aliases": [ + "ambulance" + ] + }, + { + "emoji": "🇦🇸", + "aliases": [ + "american_samoa" + ] + }, + { + "emoji": "🏺", + "aliases": [ + "amphora" + ] + }, + { + "emoji": "🫀", + "aliases": [ + "anatomical_heart" + ] + }, + { + "emoji": "⚓", + "aliases": [ + "anchor" + ] + }, + { + "emoji": "🇦🇩", + "aliases": [ + "andorra" + ] + }, + { + "emoji": "👼", + "aliases": [ + "angel" + ] + }, + { + "emoji": "💢", + "aliases": [ + "anger" + ] + }, + { + "emoji": "🇦🇴", + "aliases": [ + "angola" + ] + }, + { + "emoji": "😠", + "aliases": [ + "angry" + ] + }, + { + "emoji": "🇦🇮", + "aliases": [ + "anguilla" + ] + }, + { + "emoji": "😧", + "aliases": [ + "anguished" + ] + }, + { + "emoji": "🐜", + "aliases": [ + "ant" + ] + }, + { + "emoji": "🇦🇶", + "aliases": [ + "antarctica" + ] + }, + { + "emoji": "🇦🇬", + "aliases": [ + "antigua_barbuda" + ] + }, + { + "emoji": "🍎", + "aliases": [ + "apple" + ] + }, + { + "emoji": "♒", + "aliases": [ + "aquarius" + ] + }, + { + "emoji": "🇦🇷", + "aliases": [ + "argentina" + ] + }, + { + "emoji": "♈", + "aliases": [ + "aries" + ] + }, + { + "emoji": "🇦🇲", + "aliases": [ + "armenia" + ] + }, + { + "emoji": "◀️", + "aliases": [ + "arrow_backward" + ] + }, + { + "emoji": "⏬", + "aliases": [ + "arrow_double_down" + ] + }, + { + "emoji": "⏫", + "aliases": [ + "arrow_double_up" + ] + }, + { + "emoji": "⬇️", + "aliases": [ + "arrow_down" + ] + }, + { + "emoji": "🔽", + "aliases": [ + "arrow_down_small" + ] + }, + { + "emoji": "▶️", + "aliases": [ + "arrow_forward" + ] + }, + { + "emoji": "⤵️", + "aliases": [ + "arrow_heading_down" + ] + }, + { + "emoji": "⤴️", + "aliases": [ + "arrow_heading_up" + ] + }, + { + "emoji": "⬅️", + "aliases": [ + "arrow_left" + ] + }, + { + "emoji": "↙️", + "aliases": [ + "arrow_lower_left" + ] + }, + { + "emoji": "↘️", + "aliases": [ + "arrow_lower_right" + ] + }, + { + "emoji": "➡️", + "aliases": [ + "arrow_right" + ] + }, + { + "emoji": "↪️", + "aliases": [ + "arrow_right_hook" + ] + }, + { + "emoji": "⬆️", + "aliases": [ + "arrow_up" + ] + }, + { + "emoji": "↕️", + "aliases": [ + "arrow_up_down" + ] + }, + { + "emoji": "🔼", + "aliases": [ + "arrow_up_small" + ] + }, + { + "emoji": "↖️", + "aliases": [ + "arrow_upper_left" + ] + }, + { + "emoji": "↗️", + "aliases": [ + "arrow_upper_right" + ] + }, + { + "emoji": "🔃", + "aliases": [ + "arrows_clockwise" + ] + }, + { + "emoji": "🔄", + "aliases": [ + "arrows_counterclockwise" + ] + }, + { + "emoji": "🎨", + "aliases": [ + "art" + ] + }, + { + "emoji": "🚛", + "aliases": [ + "articulated_lorry" + ] + }, + { + "emoji": "🛰️", + "aliases": [ + "artificial_satellite" + ] + }, + { + "emoji": "🧑‍🎨", + "aliases": [ + "artist" + ] + }, + { + "emoji": "🇦🇼", + "aliases": [ + "aruba" + ] + }, + { + "emoji": "🇦🇨", + "aliases": [ + "ascension_island" + ] + }, + { + "emoji": "*️⃣", + "aliases": [ + "asterisk" + ] + }, + { + "emoji": "😲", + "aliases": [ + "astonished" + ] + }, + { + "emoji": "🧑‍🚀", + "aliases": [ + "astronaut" + ] + }, + { + "emoji": "👟", + "aliases": [ + "athletic_shoe" + ] + }, + { + "emoji": "🏧", + "aliases": [ + "atm" + ] + }, + { + "emoji": "⚛️", + "aliases": [ + "atom_symbol" + ] + }, + { + "emoji": "🇦🇺", + "aliases": [ + "australia" + ] + }, + { + "emoji": "🇦🇹", + "aliases": [ + "austria" + ] + }, + { + "emoji": "🛺", + "aliases": [ + "auto_rickshaw" + ] + }, + { + "emoji": "🥑", + "aliases": [ + "avocado" + ] + }, + { + "emoji": "🪓", + "aliases": [ + "axe" + ] + }, + { + "emoji": "🇦🇿", + "aliases": [ + "azerbaijan" + ] + }, + { + "emoji": "🅱️", + "aliases": [ + "b" + ] + }, + { + "emoji": "👶", + "aliases": [ + "baby" + ] + }, + { + "emoji": "🍼", + "aliases": [ + "baby_bottle" + ] + }, + { + "emoji": "🐤", + "aliases": [ + "baby_chick" + ] + }, + { + "emoji": "🚼", + "aliases": [ + "baby_symbol" + ] + }, + { + "emoji": "🔙", + "aliases": [ + "back" + ] + }, + { + "emoji": "🥓", + "aliases": [ + "bacon" + ] + }, + { + "emoji": "🦡", + "aliases": [ + "badger" + ] + }, + { + "emoji": "🏸", + "aliases": [ + "badminton" + ] + }, + { + "emoji": "🥯", + "aliases": [ + "bagel" + ] + }, + { + "emoji": "🛄", + "aliases": [ + "baggage_claim" + ] + }, + { + "emoji": "🥖", + "aliases": [ + "baguette_bread" + ] + }, + { + "emoji": "🇧🇸", + "aliases": [ + "bahamas" + ] + }, + { + "emoji": "🇧🇭", + "aliases": [ + "bahrain" + ] + }, + { + "emoji": "⚖️", + "aliases": [ + "balance_scale" + ] + }, + { + "emoji": "👨‍🦲", + "aliases": [ + "bald_man" + ] + }, + { + "emoji": "👩‍🦲", + "aliases": [ + "bald_woman" + ] + }, + { + "emoji": "🩰", + "aliases": [ + "ballet_shoes" + ] + }, + { + "emoji": "🎈", + "aliases": [ + "balloon" + ] + }, + { + "emoji": "🗳️", + "aliases": [ + "ballot_box" + ] + }, + { + "emoji": "☑️", + "aliases": [ + "ballot_box_with_check" + ] + }, + { + "emoji": "🎍", + "aliases": [ + "bamboo" + ] + }, + { + "emoji": "🍌", + "aliases": [ + "banana" + ] + }, + { + "emoji": "‼️", + "aliases": [ + "bangbang" + ] + }, + { + "emoji": "🇧🇩", + "aliases": [ + "bangladesh" + ] + }, + { + "emoji": "🪕", + "aliases": [ + "banjo" + ] + }, + { + "emoji": "🏦", + "aliases": [ + "bank" + ] + }, + { + "emoji": "📊", + "aliases": [ + "bar_chart" + ] + }, + { + "emoji": "🇧🇧", + "aliases": [ + "barbados" + ] + }, + { + "emoji": "💈", + "aliases": [ + "barber" + ] + }, + { + "emoji": "⚾", + "aliases": [ + "baseball" + ] + }, + { + "emoji": "🧺", + "aliases": [ + "basket" + ] + }, + { + "emoji": "🏀", + "aliases": [ + "basketball" + ] + }, + { + "emoji": "🦇", + "aliases": [ + "bat" + ] + }, + { + "emoji": "🛀", + "aliases": [ + "bath" + ] + }, + { + "emoji": "🛁", + "aliases": [ + "bathtub" + ] + }, + { + "emoji": "🔋", + "aliases": [ + "battery" + ] + }, + { + "emoji": "🏖️", + "aliases": [ + "beach_umbrella" + ] + }, + { + "emoji": "🫘", + "aliases": [ + "beans" + ] + }, + { + "emoji": "🐻", + "aliases": [ + "bear" + ] + }, + { + "emoji": "🧔", + "aliases": [ + "bearded_person" + ] + }, + { + "emoji": "🦫", + "aliases": [ + "beaver" + ] + }, + { + "emoji": "🛏️", + "aliases": [ + "bed" + ] + }, + { + "emoji": "🐝", + "aliases": [ + "bee", + "honeybee" + ] + }, + { + "emoji": "🍺", + "aliases": [ + "beer" + ] + }, + { + "emoji": "🍻", + "aliases": [ + "beers" + ] + }, + { + "emoji": "🪲", + "aliases": [ + "beetle" + ] + }, + { + "emoji": "🔰", + "aliases": [ + "beginner" + ] + }, + { + "emoji": "🇧🇾", + "aliases": [ + "belarus" + ] + }, + { + "emoji": "🇧🇪", + "aliases": [ + "belgium" + ] + }, + { + "emoji": "🇧🇿", + "aliases": [ + "belize" + ] + }, + { + "emoji": "🔔", + "aliases": [ + "bell" + ] + }, + { + "emoji": "🫑", + "aliases": [ + "bell_pepper" + ] + }, + { + "emoji": "🛎️", + "aliases": [ + "bellhop_bell" + ] + }, + { + "emoji": "🇧🇯", + "aliases": [ + "benin" + ] + }, + { + "emoji": "🍱", + "aliases": [ + "bento" + ] + }, + { + "emoji": "🇧🇲", + "aliases": [ + "bermuda" + ] + }, + { + "emoji": "🧃", + "aliases": [ + "beverage_box" + ] + }, + { + "emoji": "🇧🇹", + "aliases": [ + "bhutan" + ] + }, + { + "emoji": "🚴", + "aliases": [ + "bicyclist" + ] + }, + { + "emoji": "🚲", + "aliases": [ + "bike" + ] + }, + { + "emoji": "🚴‍♂️", + "aliases": [ + "biking_man" + ] + }, + { + "emoji": "🚴‍♀️", + "aliases": [ + "biking_woman" + ] + }, + { + "emoji": "👙", + "aliases": [ + "bikini" + ] + }, + { + "emoji": "🧢", + "aliases": [ + "billed_cap" + ] + }, + { + "emoji": "☣️", + "aliases": [ + "biohazard" + ] + }, + { + "emoji": "🐦", + "aliases": [ + "bird" + ] + }, + { + "emoji": "🎂", + "aliases": [ + "birthday" + ] + }, + { + "emoji": "🦬", + "aliases": [ + "bison" + ] + }, + { + "emoji": "🫦", + "aliases": [ + "biting_lip" + ] + }, + { + "emoji": "🐦‍⬛", + "aliases": [ + "black_bird" + ] + }, + { + "emoji": "🐈‍⬛", + "aliases": [ + "black_cat" + ] + }, + { + "emoji": "⚫", + "aliases": [ + "black_circle" + ] + }, + { + "emoji": "🏴", + "aliases": [ + "black_flag" + ] + }, + { + "emoji": "🖤", + "aliases": [ + "black_heart" + ] + }, + { + "emoji": "🃏", + "aliases": [ + "black_joker" + ] + }, + { + "emoji": "⬛", + "aliases": [ + "black_large_square" + ] + }, + { + "emoji": "◾", + "aliases": [ + "black_medium_small_square" + ] + }, + { + "emoji": "◼️", + "aliases": [ + "black_medium_square" + ] + }, + { + "emoji": "✒️", + "aliases": [ + "black_nib" + ] + }, + { + "emoji": "▪️", + "aliases": [ + "black_small_square" + ] + }, + { + "emoji": "🔲", + "aliases": [ + "black_square_button" + ] + }, + { + "emoji": "👱‍♂️", + "aliases": [ + "blond_haired_man" + ] + }, + { + "emoji": "👱", + "aliases": [ + "blond_haired_person" + ] + }, + { + "emoji": "👱‍♀️", + "aliases": [ + "blond_haired_woman", + "blonde_woman" + ] + }, + { + "emoji": "🌼", + "aliases": [ + "blossom" + ] + }, + { + "emoji": "🐡", + "aliases": [ + "blowfish" + ] + }, + { + "emoji": "📘", + "aliases": [ + "blue_book" + ] + }, + { + "emoji": "🚙", + "aliases": [ + "blue_car" + ] + }, + { + "emoji": "💙", + "aliases": [ + "blue_heart" + ] + }, + { + "emoji": "🟦", + "aliases": [ + "blue_square" + ] + }, + { + "emoji": "🫐", + "aliases": [ + "blueberries" + ] + }, + { + "emoji": "😊", + "aliases": [ + "blush" + ] + }, + { + "emoji": "🐗", + "aliases": [ + "boar" + ] + }, + { + "emoji": "⛵", + "aliases": [ + "boat", + "sailboat" + ] + }, + { + "emoji": "🇧🇴", + "aliases": [ + "bolivia" + ] + }, + { + "emoji": "💣", + "aliases": [ + "bomb" + ] + }, + { + "emoji": "🦴", + "aliases": [ + "bone" + ] + }, + { + "emoji": "📖", + "aliases": [ + "book", + "open_book" + ] + }, + { + "emoji": "🔖", + "aliases": [ + "bookmark" + ] + }, + { + "emoji": "📑", + "aliases": [ + "bookmark_tabs" + ] + }, + { + "emoji": "📚", + "aliases": [ + "books" + ] + }, + { + "emoji": "💥", + "aliases": [ + "boom", + "collision" + ] + }, + { + "emoji": "🪃", + "aliases": [ + "boomerang" + ] + }, + { + "emoji": "👢", + "aliases": [ + "boot" + ] + }, + { + "emoji": "🇧🇦", + "aliases": [ + "bosnia_herzegovina" + ] + }, + { + "emoji": "🇧🇼", + "aliases": [ + "botswana" + ] + }, + { + "emoji": "⛹️‍♂️", + "aliases": [ + "bouncing_ball_man", + "basketball_man" + ] + }, + { + "emoji": "⛹️", + "aliases": [ + "bouncing_ball_person" + ] + }, + { + "emoji": "⛹️‍♀️", + "aliases": [ + "bouncing_ball_woman", + "basketball_woman" + ] + }, + { + "emoji": "💐", + "aliases": [ + "bouquet" + ] + }, + { + "emoji": "🇧🇻", + "aliases": [ + "bouvet_island" + ] + }, + { + "emoji": "🙇", + "aliases": [ + "bow" + ] + }, + { + "emoji": "🏹", + "aliases": [ + "bow_and_arrow" + ] + }, + { + "emoji": "🙇‍♂️", + "aliases": [ + "bowing_man" + ] + }, + { + "emoji": "🙇‍♀️", + "aliases": [ + "bowing_woman" + ] + }, + { + "emoji": "🥣", + "aliases": [ + "bowl_with_spoon" + ] + }, + { + "emoji": "🎳", + "aliases": [ + "bowling" + ] + }, + { + "emoji": "🥊", + "aliases": [ + "boxing_glove" + ] + }, + { + "emoji": "👦", + "aliases": [ + "boy" + ] + }, + { + "emoji": "🧠", + "aliases": [ + "brain" + ] + }, + { + "emoji": "🇧🇷", + "aliases": [ + "brazil" + ] + }, + { + "emoji": "🍞", + "aliases": [ + "bread" + ] + }, + { + "emoji": "🤱", + "aliases": [ + "breast_feeding" + ] + }, + { + "emoji": "🧱", + "aliases": [ + "bricks" + ] + }, + { + "emoji": "🌉", + "aliases": [ + "bridge_at_night" + ] + }, + { + "emoji": "💼", + "aliases": [ + "briefcase" + ] + }, + { + "emoji": "🇮🇴", + "aliases": [ + "british_indian_ocean_territory" + ] + }, + { + "emoji": "🇻🇬", + "aliases": [ + "british_virgin_islands" + ] + }, + { + "emoji": "🥦", + "aliases": [ + "broccoli" + ] + }, + { + "emoji": "⛓️‍💥", + "aliases": [ + "broken_chain" + ] + }, + { + "emoji": "💔", + "aliases": [ + "broken_heart" + ] + }, + { + "emoji": "🧹", + "aliases": [ + "broom" + ] + }, + { + "emoji": "🟤", + "aliases": [ + "brown_circle" + ] + }, + { + "emoji": "🤎", + "aliases": [ + "brown_heart" + ] + }, + { + "emoji": "🍄‍🟫", + "aliases": [ + "brown_mushroom" + ] + }, + { + "emoji": "🟫", + "aliases": [ + "brown_square" + ] + }, + { + "emoji": "🇧🇳", + "aliases": [ + "brunei" + ] + }, + { + "emoji": "🧋", + "aliases": [ + "bubble_tea" + ] + }, + { + "emoji": "🫧", + "aliases": [ + "bubbles" + ] + }, + { + "emoji": "🪣", + "aliases": [ + "bucket" + ] + }, + { + "emoji": "🐛", + "aliases": [ + "bug" + ] + }, + { + "emoji": "🏗️", + "aliases": [ + "building_construction" + ] + }, + { + "emoji": "💡", + "aliases": [ + "bulb" + ] + }, + { + "emoji": "🇧🇬", + "aliases": [ + "bulgaria" + ] + }, + { + "emoji": "🚅", + "aliases": [ + "bullettrain_front" + ] + }, + { + "emoji": "🚄", + "aliases": [ + "bullettrain_side" + ] + }, + { + "emoji": "🇧🇫", + "aliases": [ + "burkina_faso" + ] + }, + { + "emoji": "🌯", + "aliases": [ + "burrito" + ] + }, + { + "emoji": "🇧🇮", + "aliases": [ + "burundi" + ] + }, + { + "emoji": "🚌", + "aliases": [ + "bus" + ] + }, + { + "emoji": "🕴️", + "aliases": [ + "business_suit_levitating" + ] + }, + { + "emoji": "🚏", + "aliases": [ + "busstop" + ] + }, + { + "emoji": "👤", + "aliases": [ + "bust_in_silhouette" + ] + }, + { + "emoji": "👥", + "aliases": [ + "busts_in_silhouette" + ] + }, + { + "emoji": "🧈", + "aliases": [ + "butter" + ] + }, + { + "emoji": "🦋", + "aliases": [ + "butterfly" + ] + }, + { + "emoji": "🌵", + "aliases": [ + "cactus" + ] + }, + { + "emoji": "🍰", + "aliases": [ + "cake" + ] + }, + { + "emoji": "📆", + "aliases": [ + "calendar" + ] + }, + { + "emoji": "🤙", + "aliases": [ + "call_me_hand" + ] + }, + { + "emoji": "📲", + "aliases": [ + "calling" + ] + }, + { + "emoji": "🇰🇭", + "aliases": [ + "cambodia" + ] + }, + { + "emoji": "🐫", + "aliases": [ + "camel" + ] + }, + { + "emoji": "📷", + "aliases": [ + "camera" + ] + }, + { + "emoji": "📸", + "aliases": [ + "camera_flash" + ] + }, + { + "emoji": "🇨🇲", + "aliases": [ + "cameroon" + ] + }, + { + "emoji": "🏕️", + "aliases": [ + "camping" + ] + }, + { + "emoji": "🇨🇦", + "aliases": [ + "canada" + ] + }, + { + "emoji": "🇮🇨", + "aliases": [ + "canary_islands" + ] + }, + { + "emoji": "♋", + "aliases": [ + "cancer" + ] + }, + { + "emoji": "🕯️", + "aliases": [ + "candle" + ] + }, + { + "emoji": "🍬", + "aliases": [ + "candy" + ] + }, + { + "emoji": "🥫", + "aliases": [ + "canned_food" + ] + }, + { + "emoji": "🛶", + "aliases": [ + "canoe" + ] + }, + { + "emoji": "🇨🇻", + "aliases": [ + "cape_verde" + ] + }, + { + "emoji": "🔠", + "aliases": [ + "capital_abcd" + ] + }, + { + "emoji": "♑", + "aliases": [ + "capricorn" + ] + }, + { + "emoji": "🚗", + "aliases": [ + "car", + "red_car" + ] + }, + { + "emoji": "🗃️", + "aliases": [ + "card_file_box" + ] + }, + { + "emoji": "📇", + "aliases": [ + "card_index" + ] + }, + { + "emoji": "🗂️", + "aliases": [ + "card_index_dividers" + ] + }, + { + "emoji": "🇧🇶", + "aliases": [ + "caribbean_netherlands" + ] + }, + { + "emoji": "🎠", + "aliases": [ + "carousel_horse" + ] + }, + { + "emoji": "🪚", + "aliases": [ + "carpentry_saw" + ] + }, + { + "emoji": "🥕", + "aliases": [ + "carrot" + ] + }, + { + "emoji": "🤸", + "aliases": [ + "cartwheeling" + ] + }, + { + "emoji": "🐱", + "aliases": [ + "cat" + ] + }, + { + "emoji": "🐈", + "aliases": [ + "cat2" + ] + }, + { + "emoji": "🇰🇾", + "aliases": [ + "cayman_islands" + ] + }, + { + "emoji": "💿", + "aliases": [ + "cd" + ] + }, + { + "emoji": "🇨🇫", + "aliases": [ + "central_african_republic" + ] + }, + { + "emoji": "🇪🇦", + "aliases": [ + "ceuta_melilla" + ] + }, + { + "emoji": "🇹🇩", + "aliases": [ + "chad" + ] + }, + { + "emoji": "⛓️", + "aliases": [ + "chains" + ] + }, + { + "emoji": "🪑", + "aliases": [ + "chair" + ] + }, + { + "emoji": "🍾", + "aliases": [ + "champagne" + ] + }, + { + "emoji": "💹", + "aliases": [ + "chart" + ] + }, + { + "emoji": "📉", + "aliases": [ + "chart_with_downwards_trend" + ] + }, + { + "emoji": "📈", + "aliases": [ + "chart_with_upwards_trend" + ] + }, + { + "emoji": "🏁", + "aliases": [ + "checkered_flag" + ] + }, + { + "emoji": "🧀", + "aliases": [ + "cheese" + ] + }, + { + "emoji": "🍒", + "aliases": [ + "cherries" + ] + }, + { + "emoji": "🌸", + "aliases": [ + "cherry_blossom" + ] + }, + { + "emoji": "♟️", + "aliases": [ + "chess_pawn" + ] + }, + { + "emoji": "🌰", + "aliases": [ + "chestnut" + ] + }, + { + "emoji": "🐔", + "aliases": [ + "chicken" + ] + }, + { + "emoji": "🧒", + "aliases": [ + "child" + ] + }, + { + "emoji": "🚸", + "aliases": [ + "children_crossing" + ] + }, + { + "emoji": "🇨🇱", + "aliases": [ + "chile" + ] + }, + { + "emoji": "🐿️", + "aliases": [ + "chipmunk" + ] + }, + { + "emoji": "🍫", + "aliases": [ + "chocolate_bar" + ] + }, + { + "emoji": "🥢", + "aliases": [ + "chopsticks" + ] + }, + { + "emoji": "🇨🇽", + "aliases": [ + "christmas_island" + ] + }, + { + "emoji": "🎄", + "aliases": [ + "christmas_tree" + ] + }, + { + "emoji": "⛪", + "aliases": [ + "church" + ] + }, + { + "emoji": "🎦", + "aliases": [ + "cinema" + ] + }, + { + "emoji": "🎪", + "aliases": [ + "circus_tent" + ] + }, + { + "emoji": "🌇", + "aliases": [ + "city_sunrise" + ] + }, + { + "emoji": "🌆", + "aliases": [ + "city_sunset" + ] + }, + { + "emoji": "🏙️", + "aliases": [ + "cityscape" + ] + }, + { + "emoji": "🆑", + "aliases": [ + "cl" + ] + }, + { + "emoji": "🗜️", + "aliases": [ + "clamp" + ] + }, + { + "emoji": "👏", + "aliases": [ + "clap" + ] + }, + { + "emoji": "🎬", + "aliases": [ + "clapper" + ] + }, + { + "emoji": "🏛️", + "aliases": [ + "classical_building" + ] + }, + { + "emoji": "🧗", + "aliases": [ + "climbing" + ] + }, + { + "emoji": "🧗‍♂️", + "aliases": [ + "climbing_man" + ] + }, + { + "emoji": "🧗‍♀️", + "aliases": [ + "climbing_woman" + ] + }, + { + "emoji": "🥂", + "aliases": [ + "clinking_glasses" + ] + }, + { + "emoji": "📋", + "aliases": [ + "clipboard" + ] + }, + { + "emoji": "🇨🇵", + "aliases": [ + "clipperton_island" + ] + }, + { + "emoji": "🕐", + "aliases": [ + "clock1" + ] + }, + { + "emoji": "🕙", + "aliases": [ + "clock10" + ] + }, + { + "emoji": "🕥", + "aliases": [ + "clock1030" + ] + }, + { + "emoji": "🕚", + "aliases": [ + "clock11" + ] + }, + { + "emoji": "🕦", + "aliases": [ + "clock1130" + ] + }, + { + "emoji": "🕛", + "aliases": [ + "clock12" + ] + }, + { + "emoji": "🕧", + "aliases": [ + "clock1230" + ] + }, + { + "emoji": "🕜", + "aliases": [ + "clock130" + ] + }, + { + "emoji": "🕑", + "aliases": [ + "clock2" + ] + }, + { + "emoji": "🕝", + "aliases": [ + "clock230" + ] + }, + { + "emoji": "🕒", + "aliases": [ + "clock3" + ] + }, + { + "emoji": "🕞", + "aliases": [ + "clock330" + ] + }, + { + "emoji": "🕓", + "aliases": [ + "clock4" + ] + }, + { + "emoji": "🕟", + "aliases": [ + "clock430" + ] + }, + { + "emoji": "🕔", + "aliases": [ + "clock5" + ] + }, + { + "emoji": "🕠", + "aliases": [ + "clock530" + ] + }, + { + "emoji": "🕕", + "aliases": [ + "clock6" + ] + }, + { + "emoji": "🕡", + "aliases": [ + "clock630" + ] + }, + { + "emoji": "🕖", + "aliases": [ + "clock7" + ] + }, + { + "emoji": "🕢", + "aliases": [ + "clock730" + ] + }, + { + "emoji": "🕗", + "aliases": [ + "clock8" + ] + }, + { + "emoji": "🕣", + "aliases": [ + "clock830" + ] + }, + { + "emoji": "🕘", + "aliases": [ + "clock9" + ] + }, + { + "emoji": "🕤", + "aliases": [ + "clock930" + ] + }, + { + "emoji": "📕", + "aliases": [ + "closed_book" + ] + }, + { + "emoji": "🔐", + "aliases": [ + "closed_lock_with_key" + ] + }, + { + "emoji": "🌂", + "aliases": [ + "closed_umbrella" + ] + }, + { + "emoji": "☁️", + "aliases": [ + "cloud" + ] + }, + { + "emoji": "🌩️", + "aliases": [ + "cloud_with_lightning" + ] + }, + { + "emoji": "⛈️", + "aliases": [ + "cloud_with_lightning_and_rain" + ] + }, + { + "emoji": "🌧️", + "aliases": [ + "cloud_with_rain" + ] + }, + { + "emoji": "🌨️", + "aliases": [ + "cloud_with_snow" + ] + }, + { + "emoji": "🤡", + "aliases": [ + "clown_face" + ] + }, + { + "emoji": "♣️", + "aliases": [ + "clubs" + ] + }, + { + "emoji": "🇨🇳", + "aliases": [ + "cn" + ] + }, + { + "emoji": "🧥", + "aliases": [ + "coat" + ] + }, + { + "emoji": "🪳", + "aliases": [ + "cockroach" + ] + }, + { + "emoji": "🍸", + "aliases": [ + "cocktail" + ] + }, + { + "emoji": "🥥", + "aliases": [ + "coconut" + ] + }, + { + "emoji": "🇨🇨", + "aliases": [ + "cocos_islands" + ] + }, + { + "emoji": "☕", + "aliases": [ + "coffee" + ] + }, + { + "emoji": "⚰️", + "aliases": [ + "coffin" + ] + }, + { + "emoji": "🪙", + "aliases": [ + "coin" + ] + }, + { + "emoji": "🥶", + "aliases": [ + "cold_face" + ] + }, + { + "emoji": "😰", + "aliases": [ + "cold_sweat" + ] + }, + { + "emoji": "🇨🇴", + "aliases": [ + "colombia" + ] + }, + { + "emoji": "☄️", + "aliases": [ + "comet" + ] + }, + { + "emoji": "🇰🇲", + "aliases": [ + "comoros" + ] + }, + { + "emoji": "🧭", + "aliases": [ + "compass" + ] + }, + { + "emoji": "💻", + "aliases": [ + "computer" + ] + }, + { + "emoji": "🖱️", + "aliases": [ + "computer_mouse" + ] + }, + { + "emoji": "🎊", + "aliases": [ + "confetti_ball" + ] + }, + { + "emoji": "😖", + "aliases": [ + "confounded" + ] + }, + { + "emoji": "😕", + "aliases": [ + "confused" + ] + }, + { + "emoji": "🇨🇬", + "aliases": [ + "congo_brazzaville" + ] + }, + { + "emoji": "🇨🇩", + "aliases": [ + "congo_kinshasa" + ] + }, + { + "emoji": "㊗️", + "aliases": [ + "congratulations" + ] + }, + { + "emoji": "🚧", + "aliases": [ + "construction" + ] + }, + { + "emoji": "👷", + "aliases": [ + "construction_worker" + ] + }, + { + "emoji": "👷‍♂️", + "aliases": [ + "construction_worker_man" + ] + }, + { + "emoji": "👷‍♀️", + "aliases": [ + "construction_worker_woman" + ] + }, + { + "emoji": "🎛️", + "aliases": [ + "control_knobs" + ] + }, + { + "emoji": "🏪", + "aliases": [ + "convenience_store" + ] + }, + { + "emoji": "🧑‍🍳", + "aliases": [ + "cook" + ] + }, + { + "emoji": "🇨🇰", + "aliases": [ + "cook_islands" + ] + }, + { + "emoji": "🍪", + "aliases": [ + "cookie" + ] + }, + { + "emoji": "🆒", + "aliases": [ + "cool" + ] + }, + { + "emoji": "©️", + "aliases": [ + "copyright" + ] + }, + { + "emoji": "🪸", + "aliases": [ + "coral" + ] + }, + { + "emoji": "🌽", + "aliases": [ + "corn" + ] + }, + { + "emoji": "🇨🇷", + "aliases": [ + "costa_rica" + ] + }, + { + "emoji": "🇨🇮", + "aliases": [ + "cote_divoire" + ] + }, + { + "emoji": "🛋️", + "aliases": [ + "couch_and_lamp" + ] + }, + { + "emoji": "👫", + "aliases": [ + "couple" + ] + }, + { + "emoji": "💑", + "aliases": [ + "couple_with_heart" + ] + }, + { + "emoji": "👨‍❤️‍👨", + "aliases": [ + "couple_with_heart_man_man" + ] + }, + { + "emoji": "👩‍❤️‍👨", + "aliases": [ + "couple_with_heart_woman_man" + ] + }, + { + "emoji": "👩‍❤️‍👩", + "aliases": [ + "couple_with_heart_woman_woman" + ] + }, + { + "emoji": "💏", + "aliases": [ + "couplekiss" + ] + }, + { + "emoji": "👨‍❤️‍💋‍👨", + "aliases": [ + "couplekiss_man_man" + ] + }, + { + "emoji": "👩‍❤️‍💋‍👨", + "aliases": [ + "couplekiss_man_woman" + ] + }, + { + "emoji": "👩‍❤️‍💋‍👩", + "aliases": [ + "couplekiss_woman_woman" + ] + }, + { + "emoji": "🐮", + "aliases": [ + "cow" + ] + }, + { + "emoji": "🐄", + "aliases": [ + "cow2" + ] + }, + { + "emoji": "🤠", + "aliases": [ + "cowboy_hat_face" + ] + }, + { + "emoji": "🦀", + "aliases": [ + "crab" + ] + }, + { + "emoji": "🖍️", + "aliases": [ + "crayon" + ] + }, + { + "emoji": "💳", + "aliases": [ + "credit_card" + ] + }, + { + "emoji": "🌙", + "aliases": [ + "crescent_moon" + ] + }, + { + "emoji": "🦗", + "aliases": [ + "cricket" + ] + }, + { + "emoji": "🏏", + "aliases": [ + "cricket_game" + ] + }, + { + "emoji": "🇭🇷", + "aliases": [ + "croatia" + ] + }, + { + "emoji": "🐊", + "aliases": [ + "crocodile" + ] + }, + { + "emoji": "🥐", + "aliases": [ + "croissant" + ] + }, + { + "emoji": "🤞", + "aliases": [ + "crossed_fingers" + ] + }, + { + "emoji": "🎌", + "aliases": [ + "crossed_flags" + ] + }, + { + "emoji": "⚔️", + "aliases": [ + "crossed_swords" + ] + }, + { + "emoji": "👑", + "aliases": [ + "crown" + ] + }, + { + "emoji": "🩼", + "aliases": [ + "crutch" + ] + }, + { + "emoji": "😢", + "aliases": [ + "cry" + ] + }, + { + "emoji": "😿", + "aliases": [ + "crying_cat_face" + ] + }, + { + "emoji": "🔮", + "aliases": [ + "crystal_ball" + ] + }, + { + "emoji": "🇨🇺", + "aliases": [ + "cuba" + ] + }, + { + "emoji": "🥒", + "aliases": [ + "cucumber" + ] + }, + { + "emoji": "🥤", + "aliases": [ + "cup_with_straw" + ] + }, + { + "emoji": "🧁", + "aliases": [ + "cupcake" + ] + }, + { + "emoji": "💘", + "aliases": [ + "cupid" + ] + }, + { + "emoji": "🇨🇼", + "aliases": [ + "curacao" + ] + }, + { + "emoji": "🥌", + "aliases": [ + "curling_stone" + ] + }, + { + "emoji": "👨‍🦱", + "aliases": [ + "curly_haired_man" + ] + }, + { + "emoji": "👩‍🦱", + "aliases": [ + "curly_haired_woman" + ] + }, + { + "emoji": "➰", + "aliases": [ + "curly_loop" + ] + }, + { + "emoji": "💱", + "aliases": [ + "currency_exchange" + ] + }, + { + "emoji": "🍛", + "aliases": [ + "curry" + ] + }, + { + "emoji": "🤬", + "aliases": [ + "cursing_face" + ] + }, + { + "emoji": "🍮", + "aliases": [ + "custard" + ] + }, + { + "emoji": "🛃", + "aliases": [ + "customs" + ] + }, + { + "emoji": "🥩", + "aliases": [ + "cut_of_meat" + ] + }, + { + "emoji": "🌀", + "aliases": [ + "cyclone" + ] + }, + { + "emoji": "🇨🇾", + "aliases": [ + "cyprus" + ] + }, + { + "emoji": "🇨🇿", + "aliases": [ + "czech_republic" + ] + }, + { + "emoji": "🗡️", + "aliases": [ + "dagger" + ] + }, + { + "emoji": "👯", + "aliases": [ + "dancers" + ] + }, + { + "emoji": "👯‍♂️", + "aliases": [ + "dancing_men" + ] + }, + { + "emoji": "👯‍♀️", + "aliases": [ + "dancing_women" + ] + }, + { + "emoji": "🍡", + "aliases": [ + "dango" + ] + }, + { + "emoji": "🕶️", + "aliases": [ + "dark_sunglasses" + ] + }, + { + "emoji": "🎯", + "aliases": [ + "dart" + ] + }, + { + "emoji": "💨", + "aliases": [ + "dash" + ] + }, + { + "emoji": "📅", + "aliases": [ + "date" + ] + }, + { + "emoji": "🇩🇪", + "aliases": [ + "de" + ] + }, + { + "emoji": "🧏‍♂️", + "aliases": [ + "deaf_man" + ] + }, + { + "emoji": "🧏", + "aliases": [ + "deaf_person" + ] + }, + { + "emoji": "🧏‍♀️", + "aliases": [ + "deaf_woman" + ] + }, + { + "emoji": "🌳", + "aliases": [ + "deciduous_tree" + ] + }, + { + "emoji": "🦌", + "aliases": [ + "deer" + ] + }, + { + "emoji": "🇩🇰", + "aliases": [ + "denmark" + ] + }, + { + "emoji": "🏬", + "aliases": [ + "department_store" + ] + }, + { + "emoji": "🏚️", + "aliases": [ + "derelict_house" + ] + }, + { + "emoji": "🏜️", + "aliases": [ + "desert" + ] + }, + { + "emoji": "🏝️", + "aliases": [ + "desert_island" + ] + }, + { + "emoji": "🖥️", + "aliases": [ + "desktop_computer" + ] + }, + { + "emoji": "🕵️", + "aliases": [ + "detective" + ] + }, + { + "emoji": "💠", + "aliases": [ + "diamond_shape_with_a_dot_inside" + ] + }, + { + "emoji": "♦️", + "aliases": [ + "diamonds" + ] + }, + { + "emoji": "🇩🇬", + "aliases": [ + "diego_garcia" + ] + }, + { + "emoji": "😞", + "aliases": [ + "disappointed" + ] + }, + { + "emoji": "😥", + "aliases": [ + "disappointed_relieved" + ] + }, + { + "emoji": "🥸", + "aliases": [ + "disguised_face" + ] + }, + { + "emoji": "🤿", + "aliases": [ + "diving_mask" + ] + }, + { + "emoji": "🪔", + "aliases": [ + "diya_lamp" + ] + }, + { + "emoji": "💫", + "aliases": [ + "dizzy" + ] + }, + { + "emoji": "😵", + "aliases": [ + "dizzy_face" + ] + }, + { + "emoji": "🇩🇯", + "aliases": [ + "djibouti" + ] + }, + { + "emoji": "🧬", + "aliases": [ + "dna" + ] + }, + { + "emoji": "🚯", + "aliases": [ + "do_not_litter" + ] + }, + { + "emoji": "🦤", + "aliases": [ + "dodo" + ] + }, + { + "emoji": "🐶", + "aliases": [ + "dog" + ] + }, + { + "emoji": "🐕", + "aliases": [ + "dog2" + ] + }, + { + "emoji": "💵", + "aliases": [ + "dollar" + ] + }, + { + "emoji": "🎎", + "aliases": [ + "dolls" + ] + }, + { + "emoji": "🐬", + "aliases": [ + "dolphin", + "flipper" + ] + }, + { + "emoji": "🇩🇲", + "aliases": [ + "dominica" + ] + }, + { + "emoji": "🇩🇴", + "aliases": [ + "dominican_republic" + ] + }, + { + "emoji": "🫏", + "aliases": [ + "donkey" + ] + }, + { + "emoji": "🚪", + "aliases": [ + "door" + ] + }, + { + "emoji": "🫥", + "aliases": [ + "dotted_line_face" + ] + }, + { + "emoji": "🍩", + "aliases": [ + "doughnut" + ] + }, + { + "emoji": "🕊️", + "aliases": [ + "dove" + ] + }, + { + "emoji": "🐉", + "aliases": [ + "dragon" + ] + }, + { + "emoji": "🐲", + "aliases": [ + "dragon_face" + ] + }, + { + "emoji": "👗", + "aliases": [ + "dress" + ] + }, + { + "emoji": "🐪", + "aliases": [ + "dromedary_camel" + ] + }, + { + "emoji": "🤤", + "aliases": [ + "drooling_face" + ] + }, + { + "emoji": "🩸", + "aliases": [ + "drop_of_blood" + ] + }, + { + "emoji": "💧", + "aliases": [ + "droplet" + ] + }, + { + "emoji": "🥁", + "aliases": [ + "drum" + ] + }, + { + "emoji": "🦆", + "aliases": [ + "duck" + ] + }, + { + "emoji": "🥟", + "aliases": [ + "dumpling" + ] + }, + { + "emoji": "📀", + "aliases": [ + "dvd" + ] + }, + { + "emoji": "🦅", + "aliases": [ + "eagle" + ] + }, + { + "emoji": "👂", + "aliases": [ + "ear" + ] + }, + { + "emoji": "🌾", + "aliases": [ + "ear_of_rice" + ] + }, + { + "emoji": "🦻", + "aliases": [ + "ear_with_hearing_aid" + ] + }, + { + "emoji": "🌍", + "aliases": [ + "earth_africa" + ] + }, + { + "emoji": "🌎", + "aliases": [ + "earth_americas" + ] + }, + { + "emoji": "🌏", + "aliases": [ + "earth_asia" + ] + }, + { + "emoji": "🇪🇨", + "aliases": [ + "ecuador" + ] + }, + { + "emoji": "🥚", + "aliases": [ + "egg" + ] + }, + { + "emoji": "🍆", + "aliases": [ + "eggplant" + ] + }, + { + "emoji": "🇪🇬", + "aliases": [ + "egypt" + ] + }, + { + "emoji": "8️⃣", + "aliases": [ + "eight" + ] + }, + { + "emoji": "✴️", + "aliases": [ + "eight_pointed_black_star" + ] + }, + { + "emoji": "✳️", + "aliases": [ + "eight_spoked_asterisk" + ] + }, + { + "emoji": "⏏️", + "aliases": [ + "eject_button" + ] + }, + { + "emoji": "🇸🇻", + "aliases": [ + "el_salvador" + ] + }, + { + "emoji": "🔌", + "aliases": [ + "electric_plug" + ] + }, + { + "emoji": "🐘", + "aliases": [ + "elephant" + ] + }, + { + "emoji": "🛗", + "aliases": [ + "elevator" + ] + }, + { + "emoji": "🧝", + "aliases": [ + "elf" + ] + }, + { + "emoji": "🧝‍♂️", + "aliases": [ + "elf_man" + ] + }, + { + "emoji": "🧝‍♀️", + "aliases": [ + "elf_woman" + ] + }, + { + "emoji": "📧", + "aliases": [ + "email", + "e-mail" + ] + }, + { + "emoji": "🪹", + "aliases": [ + "empty_nest" + ] + }, + { + "emoji": "🔚", + "aliases": [ + "end" + ] + }, + { + "emoji": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + "aliases": [ + "england" + ] + }, + { + "emoji": "✉️", + "aliases": [ + "envelope" + ] + }, + { + "emoji": "📩", + "aliases": [ + "envelope_with_arrow" + ] + }, + { + "emoji": "🇬🇶", + "aliases": [ + "equatorial_guinea" + ] + }, + { + "emoji": "🇪🇷", + "aliases": [ + "eritrea" + ] + }, + { + "emoji": "🇪🇸", + "aliases": [ + "es" + ] + }, + { + "emoji": "🇪🇪", + "aliases": [ + "estonia" + ] + }, + { + "emoji": "🇪🇹", + "aliases": [ + "ethiopia" + ] + }, + { + "emoji": "🇪🇺", + "aliases": [ + "eu", + "european_union" + ] + }, + { + "emoji": "💶", + "aliases": [ + "euro" + ] + }, + { + "emoji": "🏰", + "aliases": [ + "european_castle" + ] + }, + { + "emoji": "🏤", + "aliases": [ + "european_post_office" + ] + }, + { + "emoji": "🌲", + "aliases": [ + "evergreen_tree" + ] + }, + { + "emoji": "❗", + "aliases": [ + "exclamation", + "heavy_exclamation_mark" + ] + }, + { + "emoji": "🤯", + "aliases": [ + "exploding_head" + ] + }, + { + "emoji": "😑", + "aliases": [ + "expressionless" + ] + }, + { + "emoji": "👁️", + "aliases": [ + "eye" + ] + }, + { + "emoji": "👁️‍🗨️", + "aliases": [ + "eye_speech_bubble" + ] + }, + { + "emoji": "👓", + "aliases": [ + "eyeglasses" + ] + }, + { + "emoji": "👀", + "aliases": [ + "eyes" + ] + }, + { + "emoji": "😮‍💨", + "aliases": [ + "face_exhaling" + ] + }, + { + "emoji": "🥹", + "aliases": [ + "face_holding_back_tears" + ] + }, + { + "emoji": "😶‍🌫️", + "aliases": [ + "face_in_clouds" + ] + }, + { + "emoji": "🫩", + "aliases": [ + "face_with_bags_under_eyes" + ] + }, + { + "emoji": "🫤", + "aliases": [ + "face_with_diagonal_mouth" + ] + }, + { + "emoji": "🤕", + "aliases": [ + "face_with_head_bandage" + ] + }, + { + "emoji": "🫢", + "aliases": [ + "face_with_open_eyes_and_hand_over_mouth" + ] + }, + { + "emoji": "🫣", + "aliases": [ + "face_with_peeking_eye" + ] + }, + { + "emoji": "😵‍💫", + "aliases": [ + "face_with_spiral_eyes" + ] + }, + { + "emoji": "🤒", + "aliases": [ + "face_with_thermometer" + ] + }, + { + "emoji": "🤦", + "aliases": [ + "facepalm" + ] + }, + { + "emoji": "🏭", + "aliases": [ + "factory" + ] + }, + { + "emoji": "🧑‍🏭", + "aliases": [ + "factory_worker" + ] + }, + { + "emoji": "🧚", + "aliases": [ + "fairy" + ] + }, + { + "emoji": "🧚‍♂️", + "aliases": [ + "fairy_man" + ] + }, + { + "emoji": "🧚‍♀️", + "aliases": [ + "fairy_woman" + ] + }, + { + "emoji": "🧆", + "aliases": [ + "falafel" + ] + }, + { + "emoji": "🇫🇰", + "aliases": [ + "falkland_islands" + ] + }, + { + "emoji": "🍂", + "aliases": [ + "fallen_leaf" + ] + }, + { + "emoji": "👪", + "aliases": [ + "family" + ] + }, + { + "emoji": "🧑‍🧑‍🧒", + "aliases": [ + "family_adult_adult_child" + ] + }, + { + "emoji": "🧑‍🧑‍🧒‍🧒", + "aliases": [ + "family_adult_adult_child_child" + ] + }, + { + "emoji": "🧑‍🧒", + "aliases": [ + "family_adult_child" + ] + }, + { + "emoji": "🧑‍🧒‍🧒", + "aliases": [ + "family_adult_child_child" + ] + }, + { + "emoji": "👨‍👦", + "aliases": [ + "family_man_boy" + ] + }, + { + "emoji": "👨‍👦‍👦", + "aliases": [ + "family_man_boy_boy" + ] + }, + { + "emoji": "👨‍👧", + "aliases": [ + "family_man_girl" + ] + }, + { + "emoji": "👨‍👧‍👦", + "aliases": [ + "family_man_girl_boy" + ] + }, + { + "emoji": "👨‍👧‍👧", + "aliases": [ + "family_man_girl_girl" + ] + }, + { + "emoji": "👨‍👨‍👦", + "aliases": [ + "family_man_man_boy" + ] + }, + { + "emoji": "👨‍👨‍👦‍👦", + "aliases": [ + "family_man_man_boy_boy" + ] + }, + { + "emoji": "👨‍👨‍👧", + "aliases": [ + "family_man_man_girl" + ] + }, + { + "emoji": "👨‍👨‍👧‍👦", + "aliases": [ + "family_man_man_girl_boy" + ] + }, + { + "emoji": "👨‍👨‍👧‍👧", + "aliases": [ + "family_man_man_girl_girl" + ] + }, + { + "emoji": "👨‍👩‍👦", + "aliases": [ + "family_man_woman_boy" + ] + }, + { + "emoji": "👨‍👩‍👦‍👦", + "aliases": [ + "family_man_woman_boy_boy" + ] + }, + { + "emoji": "👨‍👩‍👧", + "aliases": [ + "family_man_woman_girl" + ] + }, + { + "emoji": "👨‍👩‍👧‍👦", + "aliases": [ + "family_man_woman_girl_boy" + ] + }, + { + "emoji": "👨‍👩‍👧‍👧", + "aliases": [ + "family_man_woman_girl_girl" + ] + }, + { + "emoji": "👩‍👦", + "aliases": [ + "family_woman_boy" + ] + }, + { + "emoji": "👩‍👦‍👦", + "aliases": [ + "family_woman_boy_boy" + ] + }, + { + "emoji": "👩‍👧", + "aliases": [ + "family_woman_girl" + ] + }, + { + "emoji": "👩‍👧‍👦", + "aliases": [ + "family_woman_girl_boy" + ] + }, + { + "emoji": "👩‍👧‍👧", + "aliases": [ + "family_woman_girl_girl" + ] + }, + { + "emoji": "👩‍👩‍👦", + "aliases": [ + "family_woman_woman_boy" + ] + }, + { + "emoji": "👩‍👩‍👦‍👦", + "aliases": [ + "family_woman_woman_boy_boy" + ] + }, + { + "emoji": "👩‍👩‍👧", + "aliases": [ + "family_woman_woman_girl" + ] + }, + { + "emoji": "👩‍👩‍👧‍👦", + "aliases": [ + "family_woman_woman_girl_boy" + ] + }, + { + "emoji": "👩‍👩‍👧‍👧", + "aliases": [ + "family_woman_woman_girl_girl" + ] + }, + { + "emoji": "🧑‍🌾", + "aliases": [ + "farmer" + ] + }, + { + "emoji": "🇫🇴", + "aliases": [ + "faroe_islands" + ] + }, + { + "emoji": "⏩", + "aliases": [ + "fast_forward" + ] + }, + { + "emoji": "📠", + "aliases": [ + "fax" + ] + }, + { + "emoji": "😨", + "aliases": [ + "fearful" + ] + }, + { + "emoji": "🪶", + "aliases": [ + "feather" + ] + }, + { + "emoji": "🐾", + "aliases": [ + "feet", + "paw_prints" + ] + }, + { + "emoji": "🕵️‍♀️", + "aliases": [ + "female_detective" + ] + }, + { + "emoji": "♀️", + "aliases": [ + "female_sign" + ] + }, + { + "emoji": "🎡", + "aliases": [ + "ferris_wheel" + ] + }, + { + "emoji": "⛴️", + "aliases": [ + "ferry" + ] + }, + { + "emoji": "🏑", + "aliases": [ + "field_hockey" + ] + }, + { + "emoji": "🇫🇯", + "aliases": [ + "fiji" + ] + }, + { + "emoji": "🗄️", + "aliases": [ + "file_cabinet" + ] + }, + { + "emoji": "📁", + "aliases": [ + "file_folder" + ] + }, + { + "emoji": "📽️", + "aliases": [ + "film_projector" + ] + }, + { + "emoji": "🎞️", + "aliases": [ + "film_strip" + ] + }, + { + "emoji": "🫆", + "aliases": [ + "fingerprint" + ] + }, + { + "emoji": "🇫🇮", + "aliases": [ + "finland" + ] + }, + { + "emoji": "🔥", + "aliases": [ + "fire" + ] + }, + { + "emoji": "🚒", + "aliases": [ + "fire_engine" + ] + }, + { + "emoji": "🧯", + "aliases": [ + "fire_extinguisher" + ] + }, + { + "emoji": "🧨", + "aliases": [ + "firecracker" + ] + }, + { + "emoji": "🧑‍🚒", + "aliases": [ + "firefighter" + ] + }, + { + "emoji": "🎆", + "aliases": [ + "fireworks" + ] + }, + { + "emoji": "🌓", + "aliases": [ + "first_quarter_moon" + ] + }, + { + "emoji": "🌛", + "aliases": [ + "first_quarter_moon_with_face" + ] + }, + { + "emoji": "🐟", + "aliases": [ + "fish" + ] + }, + { + "emoji": "🍥", + "aliases": [ + "fish_cake" + ] + }, + { + "emoji": "🎣", + "aliases": [ + "fishing_pole_and_fish" + ] + }, + { + "emoji": "🤛", + "aliases": [ + "fist_left" + ] + }, + { + "emoji": "👊", + "aliases": [ + "fist_oncoming", + "facepunch", + "punch" + ] + }, + { + "emoji": "✊", + "aliases": [ + "fist_raised", + "fist" + ] + }, + { + "emoji": "🤜", + "aliases": [ + "fist_right" + ] + }, + { + "emoji": "5️⃣", + "aliases": [ + "five" + ] + }, + { + "emoji": "🇨🇶", + "aliases": [ + "flag_sark" + ] + }, + { + "emoji": "🎏", + "aliases": [ + "flags" + ] + }, + { + "emoji": "🦩", + "aliases": [ + "flamingo" + ] + }, + { + "emoji": "🔦", + "aliases": [ + "flashlight" + ] + }, + { + "emoji": "🥿", + "aliases": [ + "flat_shoe" + ] + }, + { + "emoji": "🫓", + "aliases": [ + "flatbread" + ] + }, + { + "emoji": "⚜️", + "aliases": [ + "fleur_de_lis" + ] + }, + { + "emoji": "🛬", + "aliases": [ + "flight_arrival" + ] + }, + { + "emoji": "🛫", + "aliases": [ + "flight_departure" + ] + }, + { + "emoji": "💾", + "aliases": [ + "floppy_disk" + ] + }, + { + "emoji": "🎴", + "aliases": [ + "flower_playing_cards" + ] + }, + { + "emoji": "😳", + "aliases": [ + "flushed" + ] + }, + { + "emoji": "🪈", + "aliases": [ + "flute" + ] + }, + { + "emoji": "🪰", + "aliases": [ + "fly" + ] + }, + { + "emoji": "🥏", + "aliases": [ + "flying_disc" + ] + }, + { + "emoji": "🛸", + "aliases": [ + "flying_saucer" + ] + }, + { + "emoji": "🌫️", + "aliases": [ + "fog" + ] + }, + { + "emoji": "🌁", + "aliases": [ + "foggy" + ] + }, + { + "emoji": "🪭", + "aliases": [ + "folding_hand_fan" + ] + }, + { + "emoji": "🫕", + "aliases": [ + "fondue" + ] + }, + { + "emoji": "🦶", + "aliases": [ + "foot" + ] + }, + { + "emoji": "🏈", + "aliases": [ + "football" + ] + }, + { + "emoji": "👣", + "aliases": [ + "footprints" + ] + }, + { + "emoji": "🍴", + "aliases": [ + "fork_and_knife" + ] + }, + { + "emoji": "🥠", + "aliases": [ + "fortune_cookie" + ] + }, + { + "emoji": "⛲", + "aliases": [ + "fountain" + ] + }, + { + "emoji": "🖋️", + "aliases": [ + "fountain_pen" + ] + }, + { + "emoji": "4️⃣", + "aliases": [ + "four" + ] + }, + { + "emoji": "🍀", + "aliases": [ + "four_leaf_clover" + ] + }, + { + "emoji": "🦊", + "aliases": [ + "fox_face" + ] + }, + { + "emoji": "🇫🇷", + "aliases": [ + "fr" + ] + }, + { + "emoji": "🖼️", + "aliases": [ + "framed_picture" + ] + }, + { + "emoji": "🆓", + "aliases": [ + "free" + ] + }, + { + "emoji": "🇬🇫", + "aliases": [ + "french_guiana" + ] + }, + { + "emoji": "🇵🇫", + "aliases": [ + "french_polynesia" + ] + }, + { + "emoji": "🇹🇫", + "aliases": [ + "french_southern_territories" + ] + }, + { + "emoji": "🍳", + "aliases": [ + "fried_egg" + ] + }, + { + "emoji": "🍤", + "aliases": [ + "fried_shrimp" + ] + }, + { + "emoji": "🍟", + "aliases": [ + "fries" + ] + }, + { + "emoji": "🐸", + "aliases": [ + "frog" + ] + }, + { + "emoji": "😦", + "aliases": [ + "frowning" + ] + }, + { + "emoji": "☹️", + "aliases": [ + "frowning_face" + ] + }, + { + "emoji": "🙍‍♂️", + "aliases": [ + "frowning_man" + ] + }, + { + "emoji": "🙍", + "aliases": [ + "frowning_person" + ] + }, + { + "emoji": "🙍‍♀️", + "aliases": [ + "frowning_woman" + ] + }, + { + "emoji": "⛽", + "aliases": [ + "fuelpump" + ] + }, + { + "emoji": "🌕", + "aliases": [ + "full_moon" + ] + }, + { + "emoji": "🌝", + "aliases": [ + "full_moon_with_face" + ] + }, + { + "emoji": "⚱️", + "aliases": [ + "funeral_urn" + ] + }, + { + "emoji": "🇬🇦", + "aliases": [ + "gabon" + ] + }, + { + "emoji": "🇬🇲", + "aliases": [ + "gambia" + ] + }, + { + "emoji": "🎲", + "aliases": [ + "game_die" + ] + }, + { + "emoji": "🧄", + "aliases": [ + "garlic" + ] + }, + { + "emoji": "🇬🇧", + "aliases": [ + "gb", + "uk" + ] + }, + { + "emoji": "⚙️", + "aliases": [ + "gear" + ] + }, + { + "emoji": "💎", + "aliases": [ + "gem" + ] + }, + { + "emoji": "♊", + "aliases": [ + "gemini" + ] + }, + { + "emoji": "🧞", + "aliases": [ + "genie" + ] + }, + { + "emoji": "🧞‍♂️", + "aliases": [ + "genie_man" + ] + }, + { + "emoji": "🧞‍♀️", + "aliases": [ + "genie_woman" + ] + }, + { + "emoji": "🇬🇪", + "aliases": [ + "georgia" + ] + }, + { + "emoji": "🇬🇭", + "aliases": [ + "ghana" + ] + }, + { + "emoji": "👻", + "aliases": [ + "ghost" + ] + }, + { + "emoji": "🇬🇮", + "aliases": [ + "gibraltar" + ] + }, + { + "emoji": "🎁", + "aliases": [ + "gift" + ] + }, + { + "emoji": "💝", + "aliases": [ + "gift_heart" + ] + }, + { + "emoji": "🫚", + "aliases": [ + "ginger_root" + ] + }, + { + "emoji": "🦒", + "aliases": [ + "giraffe" + ] + }, + { + "emoji": "👧", + "aliases": [ + "girl" + ] + }, + { + "emoji": "🌐", + "aliases": [ + "globe_with_meridians" + ] + }, + { + "emoji": "🧤", + "aliases": [ + "gloves" + ] + }, + { + "emoji": "🥅", + "aliases": [ + "goal_net" + ] + }, + { + "emoji": "🐐", + "aliases": [ + "goat" + ] + }, + { + "emoji": "🥽", + "aliases": [ + "goggles" + ] + }, + { + "emoji": "⛳", + "aliases": [ + "golf" + ] + }, + { + "emoji": "🏌️", + "aliases": [ + "golfing" + ] + }, + { + "emoji": "🏌️‍♂️", + "aliases": [ + "golfing_man" + ] + }, + { + "emoji": "🏌️‍♀️", + "aliases": [ + "golfing_woman" + ] + }, + { + "emoji": "🪿", + "aliases": [ + "goose" + ] + }, + { + "emoji": "🦍", + "aliases": [ + "gorilla" + ] + }, + { + "emoji": "🍇", + "aliases": [ + "grapes" + ] + }, + { + "emoji": "🇬🇷", + "aliases": [ + "greece" + ] + }, + { + "emoji": "🍏", + "aliases": [ + "green_apple" + ] + }, + { + "emoji": "📗", + "aliases": [ + "green_book" + ] + }, + { + "emoji": "🟢", + "aliases": [ + "green_circle" + ] + }, + { + "emoji": "💚", + "aliases": [ + "green_heart" + ] + }, + { + "emoji": "🥗", + "aliases": [ + "green_salad" + ] + }, + { + "emoji": "🟩", + "aliases": [ + "green_square" + ] + }, + { + "emoji": "🇬🇱", + "aliases": [ + "greenland" + ] + }, + { + "emoji": "🇬🇩", + "aliases": [ + "grenada" + ] + }, + { + "emoji": "❕", + "aliases": [ + "grey_exclamation" + ] + }, + { + "emoji": "🩶", + "aliases": [ + "grey_heart" + ] + }, + { + "emoji": "❔", + "aliases": [ + "grey_question" + ] + }, + { + "emoji": "😬", + "aliases": [ + "grimacing" + ] + }, + { + "emoji": "😁", + "aliases": [ + "grin" + ] + }, + { + "emoji": "😀", + "aliases": [ + "grinning" + ] + }, + { + "emoji": "🇬🇵", + "aliases": [ + "guadeloupe" + ] + }, + { + "emoji": "🇬🇺", + "aliases": [ + "guam" + ] + }, + { + "emoji": "💂", + "aliases": [ + "guard" + ] + }, + { + "emoji": "💂‍♂️", + "aliases": [ + "guardsman" + ] + }, + { + "emoji": "💂‍♀️", + "aliases": [ + "guardswoman" + ] + }, + { + "emoji": "🇬🇹", + "aliases": [ + "guatemala" + ] + }, + { + "emoji": "🇬🇬", + "aliases": [ + "guernsey" + ] + }, + { + "emoji": "🦮", + "aliases": [ + "guide_dog" + ] + }, + { + "emoji": "🇬🇳", + "aliases": [ + "guinea" + ] + }, + { + "emoji": "🇬🇼", + "aliases": [ + "guinea_bissau" + ] + }, + { + "emoji": "🎸", + "aliases": [ + "guitar" + ] + }, + { + "emoji": "🔫", + "aliases": [ + "gun" + ] + }, + { + "emoji": "🇬🇾", + "aliases": [ + "guyana" + ] + }, + { + "emoji": "🪮", + "aliases": [ + "hair_pick" + ] + }, + { + "emoji": "💇", + "aliases": [ + "haircut" + ] + }, + { + "emoji": "💇‍♂️", + "aliases": [ + "haircut_man" + ] + }, + { + "emoji": "💇‍♀️", + "aliases": [ + "haircut_woman" + ] + }, + { + "emoji": "🇭🇹", + "aliases": [ + "haiti" + ] + }, + { + "emoji": "🍔", + "aliases": [ + "hamburger" + ] + }, + { + "emoji": "🔨", + "aliases": [ + "hammer" + ] + }, + { + "emoji": "⚒️", + "aliases": [ + "hammer_and_pick" + ] + }, + { + "emoji": "🛠️", + "aliases": [ + "hammer_and_wrench" + ] + }, + { + "emoji": "🪬", + "aliases": [ + "hamsa" + ] + }, + { + "emoji": "🐹", + "aliases": [ + "hamster" + ] + }, + { + "emoji": "✋", + "aliases": [ + "hand", + "raised_hand" + ] + }, + { + "emoji": "🤭", + "aliases": [ + "hand_over_mouth" + ] + }, + { + "emoji": "🫰", + "aliases": [ + "hand_with_index_finger_and_thumb_crossed" + ] + }, + { + "emoji": "👜", + "aliases": [ + "handbag" + ] + }, + { + "emoji": "🤾", + "aliases": [ + "handball_person" + ] + }, + { + "emoji": "🤝", + "aliases": [ + "handshake" + ] + }, + { + "emoji": "💩", + "aliases": [ + "hankey", + "poop", + "shit" + ] + }, + { + "emoji": "🪉", + "aliases": [ + "harp" + ] + }, + { + "emoji": "#️⃣", + "aliases": [ + "hash" + ] + }, + { + "emoji": "🐥", + "aliases": [ + "hatched_chick" + ] + }, + { + "emoji": "🐣", + "aliases": [ + "hatching_chick" + ] + }, + { + "emoji": "🙂‍↔️", + "aliases": [ + "head_shaking_horizontally" + ] + }, + { + "emoji": "🙂‍↕️", + "aliases": [ + "head_shaking_vertically" + ] + }, + { + "emoji": "🎧", + "aliases": [ + "headphones" + ] + }, + { + "emoji": "🪦", + "aliases": [ + "headstone" + ] + }, + { + "emoji": "🧑‍⚕️", + "aliases": [ + "health_worker" + ] + }, + { + "emoji": "🙉", + "aliases": [ + "hear_no_evil" + ] + }, + { + "emoji": "🇭🇲", + "aliases": [ + "heard_mcdonald_islands" + ] + }, + { + "emoji": "❤️", + "aliases": [ + "heart" + ] + }, + { + "emoji": "💟", + "aliases": [ + "heart_decoration" + ] + }, + { + "emoji": "😍", + "aliases": [ + "heart_eyes" + ] + }, + { + "emoji": "😻", + "aliases": [ + "heart_eyes_cat" + ] + }, + { + "emoji": "🫶", + "aliases": [ + "heart_hands" + ] + }, + { + "emoji": "❤️‍🔥", + "aliases": [ + "heart_on_fire" + ] + }, + { + "emoji": "💓", + "aliases": [ + "heartbeat" + ] + }, + { + "emoji": "💗", + "aliases": [ + "heartpulse" + ] + }, + { + "emoji": "♥️", + "aliases": [ + "hearts" + ] + }, + { + "emoji": "✔️", + "aliases": [ + "heavy_check_mark" + ] + }, + { + "emoji": "➗", + "aliases": [ + "heavy_division_sign" + ] + }, + { + "emoji": "💲", + "aliases": [ + "heavy_dollar_sign" + ] + }, + { + "emoji": "🟰", + "aliases": [ + "heavy_equals_sign" + ] + }, + { + "emoji": "❣️", + "aliases": [ + "heavy_heart_exclamation" + ] + }, + { + "emoji": "➖", + "aliases": [ + "heavy_minus_sign" + ] + }, + { + "emoji": "✖️", + "aliases": [ + "heavy_multiplication_x" + ] + }, + { + "emoji": "➕", + "aliases": [ + "heavy_plus_sign" + ] + }, + { + "emoji": "🦔", + "aliases": [ + "hedgehog" + ] + }, + { + "emoji": "🚁", + "aliases": [ + "helicopter" + ] + }, + { + "emoji": "🌿", + "aliases": [ + "herb" + ] + }, + { + "emoji": "🌺", + "aliases": [ + "hibiscus" + ] + }, + { + "emoji": "🔆", + "aliases": [ + "high_brightness" + ] + }, + { + "emoji": "👠", + "aliases": [ + "high_heel" + ] + }, + { + "emoji": "🥾", + "aliases": [ + "hiking_boot" + ] + }, + { + "emoji": "🛕", + "aliases": [ + "hindu_temple" + ] + }, + { + "emoji": "🦛", + "aliases": [ + "hippopotamus" + ] + }, + { + "emoji": "🔪", + "aliases": [ + "hocho", + "knife" + ] + }, + { + "emoji": "🕳️", + "aliases": [ + "hole" + ] + }, + { + "emoji": "🇭🇳", + "aliases": [ + "honduras" + ] + }, + { + "emoji": "🍯", + "aliases": [ + "honey_pot" + ] + }, + { + "emoji": "🇭🇰", + "aliases": [ + "hong_kong" + ] + }, + { + "emoji": "🪝", + "aliases": [ + "hook" + ] + }, + { + "emoji": "🐴", + "aliases": [ + "horse" + ] + }, + { + "emoji": "🏇", + "aliases": [ + "horse_racing" + ] + }, + { + "emoji": "🏥", + "aliases": [ + "hospital" + ] + }, + { + "emoji": "🥵", + "aliases": [ + "hot_face" + ] + }, + { + "emoji": "🌶️", + "aliases": [ + "hot_pepper" + ] + }, + { + "emoji": "🌭", + "aliases": [ + "hotdog" + ] + }, + { + "emoji": "🏨", + "aliases": [ + "hotel" + ] + }, + { + "emoji": "♨️", + "aliases": [ + "hotsprings" + ] + }, + { + "emoji": "⌛", + "aliases": [ + "hourglass" + ] + }, + { + "emoji": "⏳", + "aliases": [ + "hourglass_flowing_sand" + ] + }, + { + "emoji": "🏠", + "aliases": [ + "house" + ] + }, + { + "emoji": "🏡", + "aliases": [ + "house_with_garden" + ] + }, + { + "emoji": "🏘️", + "aliases": [ + "houses" + ] + }, + { + "emoji": "🤗", + "aliases": [ + "hugs" + ] + }, + { + "emoji": "🇭🇺", + "aliases": [ + "hungary" + ] + }, + { + "emoji": "😯", + "aliases": [ + "hushed" + ] + }, + { + "emoji": "🛖", + "aliases": [ + "hut" + ] + }, + { + "emoji": "🪻", + "aliases": [ + "hyacinth" + ] + }, + { + "emoji": "🍨", + "aliases": [ + "ice_cream" + ] + }, + { + "emoji": "🧊", + "aliases": [ + "ice_cube" + ] + }, + { + "emoji": "🏒", + "aliases": [ + "ice_hockey" + ] + }, + { + "emoji": "⛸️", + "aliases": [ + "ice_skate" + ] + }, + { + "emoji": "🍦", + "aliases": [ + "icecream" + ] + }, + { + "emoji": "🇮🇸", + "aliases": [ + "iceland" + ] + }, + { + "emoji": "🆔", + "aliases": [ + "id" + ] + }, + { + "emoji": "🪪", + "aliases": [ + "identification_card" + ] + }, + { + "emoji": "🉐", + "aliases": [ + "ideograph_advantage" + ] + }, + { + "emoji": "👿", + "aliases": [ + "imp" + ] + }, + { + "emoji": "📥", + "aliases": [ + "inbox_tray" + ] + }, + { + "emoji": "📨", + "aliases": [ + "incoming_envelope" + ] + }, + { + "emoji": "🫵", + "aliases": [ + "index_pointing_at_the_viewer" + ] + }, + { + "emoji": "🇮🇳", + "aliases": [ + "india" + ] + }, + { + "emoji": "🇮🇩", + "aliases": [ + "indonesia" + ] + }, + { + "emoji": "♾️", + "aliases": [ + "infinity" + ] + }, + { + "emoji": "ℹ️", + "aliases": [ + "information_source" + ] + }, + { + "emoji": "😇", + "aliases": [ + "innocent" + ] + }, + { + "emoji": "⁉️", + "aliases": [ + "interrobang" + ] + }, + { + "emoji": "📱", + "aliases": [ + "iphone" + ] + }, + { + "emoji": "🇮🇷", + "aliases": [ + "iran" + ] + }, + { + "emoji": "🇮🇶", + "aliases": [ + "iraq" + ] + }, + { + "emoji": "🇮🇪", + "aliases": [ + "ireland" + ] + }, + { + "emoji": "🇮🇲", + "aliases": [ + "isle_of_man" + ] + }, + { + "emoji": "🇮🇱", + "aliases": [ + "israel" + ] + }, + { + "emoji": "🇮🇹", + "aliases": [ + "it" + ] + }, + { + "emoji": "🏮", + "aliases": [ + "izakaya_lantern", + "lantern" + ] + }, + { + "emoji": "🎃", + "aliases": [ + "jack_o_lantern" + ] + }, + { + "emoji": "🇯🇲", + "aliases": [ + "jamaica" + ] + }, + { + "emoji": "🗾", + "aliases": [ + "japan" + ] + }, + { + "emoji": "🏯", + "aliases": [ + "japanese_castle" + ] + }, + { + "emoji": "👺", + "aliases": [ + "japanese_goblin" + ] + }, + { + "emoji": "👹", + "aliases": [ + "japanese_ogre" + ] + }, + { + "emoji": "🫙", + "aliases": [ + "jar" + ] + }, + { + "emoji": "👖", + "aliases": [ + "jeans" + ] + }, + { + "emoji": "🪼", + "aliases": [ + "jellyfish" + ] + }, + { + "emoji": "🇯🇪", + "aliases": [ + "jersey" + ] + }, + { + "emoji": "🧩", + "aliases": [ + "jigsaw" + ] + }, + { + "emoji": "🇯🇴", + "aliases": [ + "jordan" + ] + }, + { + "emoji": "😂", + "aliases": [ + "joy" + ] + }, + { + "emoji": "😹", + "aliases": [ + "joy_cat" + ] + }, + { + "emoji": "🕹️", + "aliases": [ + "joystick" + ] + }, + { + "emoji": "🇯🇵", + "aliases": [ + "jp" + ] + }, + { + "emoji": "🧑‍⚖️", + "aliases": [ + "judge" + ] + }, + { + "emoji": "🤹", + "aliases": [ + "juggling_person" + ] + }, + { + "emoji": "🕋", + "aliases": [ + "kaaba" + ] + }, + { + "emoji": "🦘", + "aliases": [ + "kangaroo" + ] + }, + { + "emoji": "🇰🇿", + "aliases": [ + "kazakhstan" + ] + }, + { + "emoji": "🇰🇪", + "aliases": [ + "kenya" + ] + }, + { + "emoji": "🔑", + "aliases": [ + "key" + ] + }, + { + "emoji": "⌨️", + "aliases": [ + "keyboard" + ] + }, + { + "emoji": "🔟", + "aliases": [ + "keycap_ten" + ] + }, + { + "emoji": "🪯", + "aliases": [ + "khanda" + ] + }, + { + "emoji": "🛴", + "aliases": [ + "kick_scooter" + ] + }, + { + "emoji": "👘", + "aliases": [ + "kimono" + ] + }, + { + "emoji": "🇰🇮", + "aliases": [ + "kiribati" + ] + }, + { + "emoji": "💋", + "aliases": [ + "kiss" + ] + }, + { + "emoji": "😗", + "aliases": [ + "kissing" + ] + }, + { + "emoji": "😽", + "aliases": [ + "kissing_cat" + ] + }, + { + "emoji": "😚", + "aliases": [ + "kissing_closed_eyes" + ] + }, + { + "emoji": "😘", + "aliases": [ + "kissing_heart" + ] + }, + { + "emoji": "😙", + "aliases": [ + "kissing_smiling_eyes" + ] + }, + { + "emoji": "🪁", + "aliases": [ + "kite" + ] + }, + { + "emoji": "🥝", + "aliases": [ + "kiwi_fruit" + ] + }, + { + "emoji": "🧎‍♂️", + "aliases": [ + "kneeling_man" + ] + }, + { + "emoji": "🧎", + "aliases": [ + "kneeling_person" + ] + }, + { + "emoji": "🧎‍♀️", + "aliases": [ + "kneeling_woman" + ] + }, + { + "emoji": "🪢", + "aliases": [ + "knot" + ] + }, + { + "emoji": "🐨", + "aliases": [ + "koala" + ] + }, + { + "emoji": "🈁", + "aliases": [ + "koko" + ] + }, + { + "emoji": "🇽🇰", + "aliases": [ + "kosovo" + ] + }, + { + "emoji": "🇰🇷", + "aliases": [ + "kr" + ] + }, + { + "emoji": "🇰🇼", + "aliases": [ + "kuwait" + ] + }, + { + "emoji": "🇰🇬", + "aliases": [ + "kyrgyzstan" + ] + }, + { + "emoji": "🥼", + "aliases": [ + "lab_coat" + ] + }, + { + "emoji": "🏷️", + "aliases": [ + "label" + ] + }, + { + "emoji": "🥍", + "aliases": [ + "lacrosse" + ] + }, + { + "emoji": "🪜", + "aliases": [ + "ladder" + ] + }, + { + "emoji": "🐞", + "aliases": [ + "lady_beetle" + ] + }, + { + "emoji": "🇱🇦", + "aliases": [ + "laos" + ] + }, + { + "emoji": "🔵", + "aliases": [ + "large_blue_circle" + ] + }, + { + "emoji": "🔷", + "aliases": [ + "large_blue_diamond" + ] + }, + { + "emoji": "🔶", + "aliases": [ + "large_orange_diamond" + ] + }, + { + "emoji": "🌗", + "aliases": [ + "last_quarter_moon" + ] + }, + { + "emoji": "🌜", + "aliases": [ + "last_quarter_moon_with_face" + ] + }, + { + "emoji": "✝️", + "aliases": [ + "latin_cross" + ] + }, + { + "emoji": "🇱🇻", + "aliases": [ + "latvia" + ] + }, + { + "emoji": "😆", + "aliases": [ + "laughing", + "satisfied", + "laugh" + ] + }, + { + "emoji": "🪾", + "aliases": [ + "leafless_tree" + ] + }, + { + "emoji": "🥬", + "aliases": [ + "leafy_green" + ] + }, + { + "emoji": "🍃", + "aliases": [ + "leaves" + ] + }, + { + "emoji": "🇱🇧", + "aliases": [ + "lebanon" + ] + }, + { + "emoji": "📒", + "aliases": [ + "ledger" + ] + }, + { + "emoji": "🛅", + "aliases": [ + "left_luggage" + ] + }, + { + "emoji": "↔️", + "aliases": [ + "left_right_arrow" + ] + }, + { + "emoji": "🗨️", + "aliases": [ + "left_speech_bubble" + ] + }, + { + "emoji": "↩️", + "aliases": [ + "leftwards_arrow_with_hook" + ] + }, + { + "emoji": "🫲", + "aliases": [ + "leftwards_hand" + ] + }, + { + "emoji": "🫷", + "aliases": [ + "leftwards_pushing_hand" + ] + }, + { + "emoji": "🦵", + "aliases": [ + "leg" + ] + }, + { + "emoji": "🍋", + "aliases": [ + "lemon" + ] + }, + { + "emoji": "♌", + "aliases": [ + "leo" + ] + }, + { + "emoji": "🐆", + "aliases": [ + "leopard" + ] + }, + { + "emoji": "🇱🇸", + "aliases": [ + "lesotho" + ] + }, + { + "emoji": "🎚️", + "aliases": [ + "level_slider" + ] + }, + { + "emoji": "🇱🇷", + "aliases": [ + "liberia" + ] + }, + { + "emoji": "♎", + "aliases": [ + "libra" + ] + }, + { + "emoji": "🇱🇾", + "aliases": [ + "libya" + ] + }, + { + "emoji": "🇱🇮", + "aliases": [ + "liechtenstein" + ] + }, + { + "emoji": "🩵", + "aliases": [ + "light_blue_heart" + ] + }, + { + "emoji": "🚈", + "aliases": [ + "light_rail" + ] + }, + { + "emoji": "🍋‍🟩", + "aliases": [ + "lime" + ] + }, + { + "emoji": "🔗", + "aliases": [ + "link" + ] + }, + { + "emoji": "🦁", + "aliases": [ + "lion" + ] + }, + { + "emoji": "👄", + "aliases": [ + "lips" + ] + }, + { + "emoji": "💄", + "aliases": [ + "lipstick" + ] + }, + { + "emoji": "🇱🇹", + "aliases": [ + "lithuania" + ] + }, + { + "emoji": "🦎", + "aliases": [ + "lizard" + ] + }, + { + "emoji": "🦙", + "aliases": [ + "llama" + ] + }, + { + "emoji": "🦞", + "aliases": [ + "lobster" + ] + }, + { + "emoji": "🔒", + "aliases": [ + "lock" + ] + }, + { + "emoji": "🔏", + "aliases": [ + "lock_with_ink_pen" + ] + }, + { + "emoji": "🍭", + "aliases": [ + "lollipop" + ] + }, + { + "emoji": "🪘", + "aliases": [ + "long_drum" + ] + }, + { + "emoji": "➿", + "aliases": [ + "loop" + ] + }, + { + "emoji": "🧴", + "aliases": [ + "lotion_bottle" + ] + }, + { + "emoji": "🪷", + "aliases": [ + "lotus" + ] + }, + { + "emoji": "🧘", + "aliases": [ + "lotus_position" + ] + }, + { + "emoji": "🧘‍♂️", + "aliases": [ + "lotus_position_man" + ] + }, + { + "emoji": "🧘‍♀️", + "aliases": [ + "lotus_position_woman" + ] + }, + { + "emoji": "🔊", + "aliases": [ + "loud_sound" + ] + }, + { + "emoji": "📢", + "aliases": [ + "loudspeaker" + ] + }, + { + "emoji": "🏩", + "aliases": [ + "love_hotel" + ] + }, + { + "emoji": "💌", + "aliases": [ + "love_letter" + ] + }, + { + "emoji": "🤟", + "aliases": [ + "love_you_gesture" + ] + }, + { + "emoji": "🪫", + "aliases": [ + "low_battery" + ] + }, + { + "emoji": "🔅", + "aliases": [ + "low_brightness" + ] + }, + { + "emoji": "🧳", + "aliases": [ + "luggage" + ] + }, + { + "emoji": "🫁", + "aliases": [ + "lungs" + ] + }, + { + "emoji": "🇱🇺", + "aliases": [ + "luxembourg" + ] + }, + { + "emoji": "🤥", + "aliases": [ + "lying_face" + ] + }, + { + "emoji": "Ⓜ️", + "aliases": [ + "m" + ] + }, + { + "emoji": "🇲🇴", + "aliases": [ + "macau" + ] + }, + { + "emoji": "🇲🇰", + "aliases": [ + "macedonia" + ] + }, + { + "emoji": "🇲🇬", + "aliases": [ + "madagascar" + ] + }, + { + "emoji": "🔍", + "aliases": [ + "mag" + ] + }, + { + "emoji": "🔎", + "aliases": [ + "mag_right" + ] + }, + { + "emoji": "🧙", + "aliases": [ + "mage" + ] + }, + { + "emoji": "🧙‍♂️", + "aliases": [ + "mage_man" + ] + }, + { + "emoji": "🧙‍♀️", + "aliases": [ + "mage_woman" + ] + }, + { + "emoji": "🪄", + "aliases": [ + "magic_wand" + ] + }, + { + "emoji": "🧲", + "aliases": [ + "magnet" + ] + }, + { + "emoji": "🀄", + "aliases": [ + "mahjong" + ] + }, + { + "emoji": "📫", + "aliases": [ + "mailbox" + ] + }, + { + "emoji": "📪", + "aliases": [ + "mailbox_closed" + ] + }, + { + "emoji": "📬", + "aliases": [ + "mailbox_with_mail" + ] + }, + { + "emoji": "📭", + "aliases": [ + "mailbox_with_no_mail" + ] + }, + { + "emoji": "🇲🇼", + "aliases": [ + "malawi" + ] + }, + { + "emoji": "🇲🇾", + "aliases": [ + "malaysia" + ] + }, + { + "emoji": "🇲🇻", + "aliases": [ + "maldives" + ] + }, + { + "emoji": "🕵️‍♂️", + "aliases": [ + "male_detective" + ] + }, + { + "emoji": "♂️", + "aliases": [ + "male_sign" + ] + }, + { + "emoji": "🇲🇱", + "aliases": [ + "mali" + ] + }, + { + "emoji": "🇲🇹", + "aliases": [ + "malta" + ] + }, + { + "emoji": "🦣", + "aliases": [ + "mammoth" + ] + }, + { + "emoji": "👨", + "aliases": [ + "man" + ] + }, + { + "emoji": "👨‍🎨", + "aliases": [ + "man_artist" + ] + }, + { + "emoji": "👨‍🚀", + "aliases": [ + "man_astronaut" + ] + }, + { + "emoji": "🧔‍♂️", + "aliases": [ + "man_beard" + ] + }, + { + "emoji": "🤸‍♂️", + "aliases": [ + "man_cartwheeling" + ] + }, + { + "emoji": "👨‍🍳", + "aliases": [ + "man_cook" + ] + }, + { + "emoji": "🕺", + "aliases": [ + "man_dancing" + ] + }, + { + "emoji": "🤦‍♂️", + "aliases": [ + "man_facepalming" + ] + }, + { + "emoji": "👨‍🏭", + "aliases": [ + "man_factory_worker" + ] + }, + { + "emoji": "👨‍🌾", + "aliases": [ + "man_farmer" + ] + }, + { + "emoji": "👨‍🍼", + "aliases": [ + "man_feeding_baby" + ] + }, + { + "emoji": "👨‍🚒", + "aliases": [ + "man_firefighter" + ] + }, + { + "emoji": "👨‍⚕️", + "aliases": [ + "man_health_worker" + ] + }, + { + "emoji": "👨‍🦽", + "aliases": [ + "man_in_manual_wheelchair" + ] + }, + { + "emoji": "👨‍🦽‍➡️", + "aliases": [ + "man_in_manual_wheelchair_facing_right" + ] + }, + { + "emoji": "👨‍🦼", + "aliases": [ + "man_in_motorized_wheelchair" + ] + }, + { + "emoji": "👨‍🦼‍➡️", + "aliases": [ + "man_in_motorized_wheelchair_facing_right" + ] + }, + { + "emoji": "🤵‍♂️", + "aliases": [ + "man_in_tuxedo" + ] + }, + { + "emoji": "👨‍⚖️", + "aliases": [ + "man_judge" + ] + }, + { + "emoji": "🤹‍♂️", + "aliases": [ + "man_juggling" + ] + }, + { + "emoji": "🧎‍♂️‍➡️", + "aliases": [ + "man_kneeling_facing_right" + ] + }, + { + "emoji": "👨‍🔧", + "aliases": [ + "man_mechanic" + ] + }, + { + "emoji": "👨‍💼", + "aliases": [ + "man_office_worker" + ] + }, + { + "emoji": "👨‍✈️", + "aliases": [ + "man_pilot" + ] + }, + { + "emoji": "🤾‍♂️", + "aliases": [ + "man_playing_handball" + ] + }, + { + "emoji": "🤽‍♂️", + "aliases": [ + "man_playing_water_polo" + ] + }, + { + "emoji": "🏃‍♂️‍➡️", + "aliases": [ + "man_running_facing_right" + ] + }, + { + "emoji": "👨‍🔬", + "aliases": [ + "man_scientist" + ] + }, + { + "emoji": "🤷‍♂️", + "aliases": [ + "man_shrugging" + ] + }, + { + "emoji": "👨‍🎤", + "aliases": [ + "man_singer" + ] + }, + { + "emoji": "👨‍🎓", + "aliases": [ + "man_student" + ] + }, + { + "emoji": "👨‍🏫", + "aliases": [ + "man_teacher" + ] + }, + { + "emoji": "👨‍💻", + "aliases": [ + "man_technologist" + ] + }, + { + "emoji": "🚶‍♂️‍➡️", + "aliases": [ + "man_walking_facing_right" + ] + }, + { + "emoji": "👲", + "aliases": [ + "man_with_gua_pi_mao" + ] + }, + { + "emoji": "👨‍🦯", + "aliases": [ + "man_with_probing_cane" + ] + }, + { + "emoji": "👳‍♂️", + "aliases": [ + "man_with_turban" + ] + }, + { + "emoji": "👰‍♂️", + "aliases": [ + "man_with_veil" + ] + }, + { + "emoji": "👨‍🦯‍➡️", + "aliases": [ + "man_with_white_cane_facing_right" + ] + }, + { + "emoji": "🥭", + "aliases": [ + "mango" + ] + }, + { + "emoji": "👞", + "aliases": [ + "mans_shoe", + "shoe" + ] + }, + { + "emoji": "🕰️", + "aliases": [ + "mantelpiece_clock" + ] + }, + { + "emoji": "🦽", + "aliases": [ + "manual_wheelchair" + ] + }, + { + "emoji": "🍁", + "aliases": [ + "maple_leaf" + ] + }, + { + "emoji": "🪇", + "aliases": [ + "maracas" + ] + }, + { + "emoji": "🇲🇭", + "aliases": [ + "marshall_islands" + ] + }, + { + "emoji": "🥋", + "aliases": [ + "martial_arts_uniform" + ] + }, + { + "emoji": "🇲🇶", + "aliases": [ + "martinique" + ] + }, + { + "emoji": "😷", + "aliases": [ + "mask" + ] + }, + { + "emoji": "💆", + "aliases": [ + "massage" + ] + }, + { + "emoji": "💆‍♂️", + "aliases": [ + "massage_man" + ] + }, + { + "emoji": "💆‍♀️", + "aliases": [ + "massage_woman" + ] + }, + { + "emoji": "🧉", + "aliases": [ + "mate" + ] + }, + { + "emoji": "🇲🇷", + "aliases": [ + "mauritania" + ] + }, + { + "emoji": "🇲🇺", + "aliases": [ + "mauritius" + ] + }, + { + "emoji": "🇾🇹", + "aliases": [ + "mayotte" + ] + }, + { + "emoji": "🍖", + "aliases": [ + "meat_on_bone" + ] + }, + { + "emoji": "🧑‍🔧", + "aliases": [ + "mechanic" + ] + }, + { + "emoji": "🦾", + "aliases": [ + "mechanical_arm" + ] + }, + { + "emoji": "🦿", + "aliases": [ + "mechanical_leg" + ] + }, + { + "emoji": "🎖️", + "aliases": [ + "medal_military" + ] + }, + { + "emoji": "🏅", + "aliases": [ + "medal_sports" + ] + }, + { + "emoji": "⚕️", + "aliases": [ + "medical_symbol" + ] + }, + { + "emoji": "📣", + "aliases": [ + "mega" + ] + }, + { + "emoji": "🍈", + "aliases": [ + "melon" + ] + }, + { + "emoji": "🫠", + "aliases": [ + "melting_face" + ] + }, + { + "emoji": "📝", + "aliases": [ + "memo", + "pencil" + ] + }, + { + "emoji": "🤼‍♂️", + "aliases": [ + "men_wrestling" + ] + }, + { + "emoji": "❤️‍🩹", + "aliases": [ + "mending_heart" + ] + }, + { + "emoji": "🕎", + "aliases": [ + "menorah" + ] + }, + { + "emoji": "🚹", + "aliases": [ + "mens" + ] + }, + { + "emoji": "🧜‍♀️", + "aliases": [ + "mermaid" + ] + }, + { + "emoji": "🧜‍♂️", + "aliases": [ + "merman" + ] + }, + { + "emoji": "🧜", + "aliases": [ + "merperson" + ] + }, + { + "emoji": "🤘", + "aliases": [ + "metal" + ] + }, + { + "emoji": "🚇", + "aliases": [ + "metro" + ] + }, + { + "emoji": "🇲🇽", + "aliases": [ + "mexico" + ] + }, + { + "emoji": "🦠", + "aliases": [ + "microbe" + ] + }, + { + "emoji": "🇫🇲", + "aliases": [ + "micronesia" + ] + }, + { + "emoji": "🎤", + "aliases": [ + "microphone" + ] + }, + { + "emoji": "🔬", + "aliases": [ + "microscope" + ] + }, + { + "emoji": "🖕", + "aliases": [ + "middle_finger", + "fu" + ] + }, + { + "emoji": "🪖", + "aliases": [ + "military_helmet" + ] + }, + { + "emoji": "🥛", + "aliases": [ + "milk_glass" + ] + }, + { + "emoji": "🌌", + "aliases": [ + "milky_way" + ] + }, + { + "emoji": "🚐", + "aliases": [ + "minibus" + ] + }, + { + "emoji": "💽", + "aliases": [ + "minidisc" + ] + }, + { + "emoji": "🪞", + "aliases": [ + "mirror" + ] + }, + { + "emoji": "🪩", + "aliases": [ + "mirror_ball" + ] + }, + { + "emoji": "📴", + "aliases": [ + "mobile_phone_off" + ] + }, + { + "emoji": "🇲🇩", + "aliases": [ + "moldova" + ] + }, + { + "emoji": "🇲🇨", + "aliases": [ + "monaco" + ] + }, + { + "emoji": "🤑", + "aliases": [ + "money_mouth_face" + ] + }, + { + "emoji": "💸", + "aliases": [ + "money_with_wings" + ] + }, + { + "emoji": "💰", + "aliases": [ + "moneybag" + ] + }, + { + "emoji": "🇲🇳", + "aliases": [ + "mongolia" + ] + }, + { + "emoji": "🐒", + "aliases": [ + "monkey" + ] + }, + { + "emoji": "🐵", + "aliases": [ + "monkey_face" + ] + }, + { + "emoji": "🧐", + "aliases": [ + "monocle_face" + ] + }, + { + "emoji": "🚝", + "aliases": [ + "monorail" + ] + }, + { + "emoji": "🇲🇪", + "aliases": [ + "montenegro" + ] + }, + { + "emoji": "🇲🇸", + "aliases": [ + "montserrat" + ] + }, + { + "emoji": "🌔", + "aliases": [ + "moon", + "waxing_gibbous_moon" + ] + }, + { + "emoji": "🥮", + "aliases": [ + "moon_cake" + ] + }, + { + "emoji": "🫎", + "aliases": [ + "moose" + ] + }, + { + "emoji": "🇲🇦", + "aliases": [ + "morocco" + ] + }, + { + "emoji": "🎓", + "aliases": [ + "mortar_board" + ] + }, + { + "emoji": "🕌", + "aliases": [ + "mosque" + ] + }, + { + "emoji": "🦟", + "aliases": [ + "mosquito" + ] + }, + { + "emoji": "🛥️", + "aliases": [ + "motor_boat" + ] + }, + { + "emoji": "🛵", + "aliases": [ + "motor_scooter" + ] + }, + { + "emoji": "🏍️", + "aliases": [ + "motorcycle" + ] + }, + { + "emoji": "🦼", + "aliases": [ + "motorized_wheelchair" + ] + }, + { + "emoji": "🛣️", + "aliases": [ + "motorway" + ] + }, + { + "emoji": "🗻", + "aliases": [ + "mount_fuji" + ] + }, + { + "emoji": "⛰️", + "aliases": [ + "mountain" + ] + }, + { + "emoji": "🚵", + "aliases": [ + "mountain_bicyclist" + ] + }, + { + "emoji": "🚵‍♂️", + "aliases": [ + "mountain_biking_man" + ] + }, + { + "emoji": "🚵‍♀️", + "aliases": [ + "mountain_biking_woman" + ] + }, + { + "emoji": "🚠", + "aliases": [ + "mountain_cableway" + ] + }, + { + "emoji": "🚞", + "aliases": [ + "mountain_railway" + ] + }, + { + "emoji": "🏔️", + "aliases": [ + "mountain_snow" + ] + }, + { + "emoji": "🐭", + "aliases": [ + "mouse" + ] + }, + { + "emoji": "🐁", + "aliases": [ + "mouse2" + ] + }, + { + "emoji": "🪤", + "aliases": [ + "mouse_trap" + ] + }, + { + "emoji": "🎥", + "aliases": [ + "movie_camera" + ] + }, + { + "emoji": "🗿", + "aliases": [ + "moyai" + ] + }, + { + "emoji": "🇲🇿", + "aliases": [ + "mozambique" + ] + }, + { + "emoji": "🤶", + "aliases": [ + "mrs_claus" + ] + }, + { + "emoji": "💪", + "aliases": [ + "muscle" + ] + }, + { + "emoji": "🍄", + "aliases": [ + "mushroom" + ] + }, + { + "emoji": "🎹", + "aliases": [ + "musical_keyboard" + ] + }, + { + "emoji": "🎵", + "aliases": [ + "musical_note" + ] + }, + { + "emoji": "🎼", + "aliases": [ + "musical_score" + ] + }, + { + "emoji": "🔇", + "aliases": [ + "mute" + ] + }, + { + "emoji": "🧑‍🎄", + "aliases": [ + "mx_claus" + ] + }, + { + "emoji": "🇲🇲", + "aliases": [ + "myanmar" + ] + }, + { + "emoji": "💅", + "aliases": [ + "nail_care" + ] + }, + { + "emoji": "📛", + "aliases": [ + "name_badge" + ] + }, + { + "emoji": "🇳🇦", + "aliases": [ + "namibia" + ] + }, + { + "emoji": "🏞️", + "aliases": [ + "national_park" + ] + }, + { + "emoji": "🇳🇷", + "aliases": [ + "nauru" + ] + }, + { + "emoji": "🤢", + "aliases": [ + "nauseated_face" + ] + }, + { + "emoji": "🧿", + "aliases": [ + "nazar_amulet" + ] + }, + { + "emoji": "👔", + "aliases": [ + "necktie" + ] + }, + { + "emoji": "❎", + "aliases": [ + "negative_squared_cross_mark" + ] + }, + { + "emoji": "🇳🇵", + "aliases": [ + "nepal" + ] + }, + { + "emoji": "🤓", + "aliases": [ + "nerd_face" + ] + }, + { + "emoji": "🪺", + "aliases": [ + "nest_with_eggs" + ] + }, + { + "emoji": "🪆", + "aliases": [ + "nesting_dolls" + ] + }, + { + "emoji": "🇳🇱", + "aliases": [ + "netherlands" + ] + }, + { + "emoji": "😐", + "aliases": [ + "neutral_face" + ] + }, + { + "emoji": "🆕", + "aliases": [ + "new" + ] + }, + { + "emoji": "🇳🇨", + "aliases": [ + "new_caledonia" + ] + }, + { + "emoji": "🌑", + "aliases": [ + "new_moon" + ] + }, + { + "emoji": "🌚", + "aliases": [ + "new_moon_with_face" + ] + }, + { + "emoji": "🇳🇿", + "aliases": [ + "new_zealand" + ] + }, + { + "emoji": "📰", + "aliases": [ + "newspaper" + ] + }, + { + "emoji": "🗞️", + "aliases": [ + "newspaper_roll" + ] + }, + { + "emoji": "⏭️", + "aliases": [ + "next_track_button" + ] + }, + { + "emoji": "🆖", + "aliases": [ + "ng" + ] + }, + { + "emoji": "🇳🇮", + "aliases": [ + "nicaragua" + ] + }, + { + "emoji": "🇳🇪", + "aliases": [ + "niger" + ] + }, + { + "emoji": "🇳🇬", + "aliases": [ + "nigeria" + ] + }, + { + "emoji": "🌃", + "aliases": [ + "night_with_stars" + ] + }, + { + "emoji": "9️⃣", + "aliases": [ + "nine" + ] + }, + { + "emoji": "🥷", + "aliases": [ + "ninja" + ] + }, + { + "emoji": "🇳🇺", + "aliases": [ + "niue" + ] + }, + { + "emoji": "🔕", + "aliases": [ + "no_bell" + ] + }, + { + "emoji": "🚳", + "aliases": [ + "no_bicycles" + ] + }, + { + "emoji": "⛔", + "aliases": [ + "no_entry" + ] + }, + { + "emoji": "🚫", + "aliases": [ + "no_entry_sign" + ] + }, + { + "emoji": "🙅", + "aliases": [ + "no_good" + ] + }, + { + "emoji": "🙅‍♂️", + "aliases": [ + "no_good_man", + "ng_man" + ] + }, + { + "emoji": "🙅‍♀️", + "aliases": [ + "no_good_woman", + "ng_woman" + ] + }, + { + "emoji": "📵", + "aliases": [ + "no_mobile_phones" + ] + }, + { + "emoji": "😶", + "aliases": [ + "no_mouth" + ] + }, + { + "emoji": "🚷", + "aliases": [ + "no_pedestrians" + ] + }, + { + "emoji": "🚭", + "aliases": [ + "no_smoking" + ] + }, + { + "emoji": "🚱", + "aliases": [ + "non-potable_water" + ] + }, + { + "emoji": "🇳🇫", + "aliases": [ + "norfolk_island" + ] + }, + { + "emoji": "🇰🇵", + "aliases": [ + "north_korea" + ] + }, + { + "emoji": "🇲🇵", + "aliases": [ + "northern_mariana_islands" + ] + }, + { + "emoji": "🇳🇴", + "aliases": [ + "norway" + ] + }, + { + "emoji": "👃", + "aliases": [ + "nose" + ] + }, + { + "emoji": "📓", + "aliases": [ + "notebook" + ] + }, + { + "emoji": "📔", + "aliases": [ + "notebook_with_decorative_cover" + ] + }, + { + "emoji": "🎶", + "aliases": [ + "notes" + ] + }, + { + "emoji": "🔩", + "aliases": [ + "nut_and_bolt" + ] + }, + { + "emoji": "⭕", + "aliases": [ + "o" + ] + }, + { + "emoji": "🅾️", + "aliases": [ + "o2" + ] + }, + { + "emoji": "🌊", + "aliases": [ + "ocean" + ] + }, + { + "emoji": "🐙", + "aliases": [ + "octopus" + ] + }, + { + "emoji": "🍢", + "aliases": [ + "oden" + ] + }, + { + "emoji": "🏢", + "aliases": [ + "office" + ] + }, + { + "emoji": "🧑‍💼", + "aliases": [ + "office_worker" + ] + }, + { + "emoji": "🛢️", + "aliases": [ + "oil_drum" + ] + }, + { + "emoji": "🆗", + "aliases": [ + "ok" + ] + }, + { + "emoji": "👌", + "aliases": [ + "ok_hand" + ] + }, + { + "emoji": "🙆‍♂️", + "aliases": [ + "ok_man" + ] + }, + { + "emoji": "🙆", + "aliases": [ + "ok_person" + ] + }, + { + "emoji": "🙆‍♀️", + "aliases": [ + "ok_woman" + ] + }, + { + "emoji": "🗝️", + "aliases": [ + "old_key" + ] + }, + { + "emoji": "🧓", + "aliases": [ + "older_adult" + ] + }, + { + "emoji": "👴", + "aliases": [ + "older_man" + ] + }, + { + "emoji": "👵", + "aliases": [ + "older_woman" + ] + }, + { + "emoji": "🫒", + "aliases": [ + "olive" + ] + }, + { + "emoji": "🕉️", + "aliases": [ + "om" + ] + }, + { + "emoji": "🇴🇲", + "aliases": [ + "oman" + ] + }, + { + "emoji": "🔛", + "aliases": [ + "on" + ] + }, + { + "emoji": "🚘", + "aliases": [ + "oncoming_automobile" + ] + }, + { + "emoji": "🚍", + "aliases": [ + "oncoming_bus" + ] + }, + { + "emoji": "🚔", + "aliases": [ + "oncoming_police_car" + ] + }, + { + "emoji": "🚖", + "aliases": [ + "oncoming_taxi" + ] + }, + { + "emoji": "1️⃣", + "aliases": [ + "one" + ] + }, + { + "emoji": "🩱", + "aliases": [ + "one_piece_swimsuit" + ] + }, + { + "emoji": "🧅", + "aliases": [ + "onion" + ] + }, + { + "emoji": "📂", + "aliases": [ + "open_file_folder" + ] + }, + { + "emoji": "👐", + "aliases": [ + "open_hands" + ] + }, + { + "emoji": "😮", + "aliases": [ + "open_mouth" + ] + }, + { + "emoji": "☂️", + "aliases": [ + "open_umbrella" + ] + }, + { + "emoji": "⛎", + "aliases": [ + "ophiuchus" + ] + }, + { + "emoji": "📙", + "aliases": [ + "orange_book" + ] + }, + { + "emoji": "🟠", + "aliases": [ + "orange_circle" + ] + }, + { + "emoji": "🧡", + "aliases": [ + "orange_heart" + ] + }, + { + "emoji": "🟧", + "aliases": [ + "orange_square" + ] + }, + { + "emoji": "🦧", + "aliases": [ + "orangutan" + ] + }, + { + "emoji": "☦️", + "aliases": [ + "orthodox_cross" + ] + }, + { + "emoji": "🦦", + "aliases": [ + "otter" + ] + }, + { + "emoji": "📤", + "aliases": [ + "outbox_tray" + ] + }, + { + "emoji": "🦉", + "aliases": [ + "owl" + ] + }, + { + "emoji": "🐂", + "aliases": [ + "ox" + ] + }, + { + "emoji": "🦪", + "aliases": [ + "oyster" + ] + }, + { + "emoji": "📦", + "aliases": [ + "package" + ] + }, + { + "emoji": "📄", + "aliases": [ + "page_facing_up" + ] + }, + { + "emoji": "📃", + "aliases": [ + "page_with_curl" + ] + }, + { + "emoji": "📟", + "aliases": [ + "pager" + ] + }, + { + "emoji": "🖌️", + "aliases": [ + "paintbrush" + ] + }, + { + "emoji": "🇵🇰", + "aliases": [ + "pakistan" + ] + }, + { + "emoji": "🇵🇼", + "aliases": [ + "palau" + ] + }, + { + "emoji": "🇵🇸", + "aliases": [ + "palestinian_territories" + ] + }, + { + "emoji": "🫳", + "aliases": [ + "palm_down_hand" + ] + }, + { + "emoji": "🌴", + "aliases": [ + "palm_tree" + ] + }, + { + "emoji": "🫴", + "aliases": [ + "palm_up_hand" + ] + }, + { + "emoji": "🤲", + "aliases": [ + "palms_up_together" + ] + }, + { + "emoji": "🇵🇦", + "aliases": [ + "panama" + ] + }, + { + "emoji": "🥞", + "aliases": [ + "pancakes" + ] + }, + { + "emoji": "🐼", + "aliases": [ + "panda_face" + ] + }, + { + "emoji": "📎", + "aliases": [ + "paperclip" + ] + }, + { + "emoji": "🖇️", + "aliases": [ + "paperclips" + ] + }, + { + "emoji": "🇵🇬", + "aliases": [ + "papua_new_guinea" + ] + }, + { + "emoji": "🪂", + "aliases": [ + "parachute" + ] + }, + { + "emoji": "🇵🇾", + "aliases": [ + "paraguay" + ] + }, + { + "emoji": "⛱️", + "aliases": [ + "parasol_on_ground" + ] + }, + { + "emoji": "🅿️", + "aliases": [ + "parking" + ] + }, + { + "emoji": "🦜", + "aliases": [ + "parrot" + ] + }, + { + "emoji": "〽️", + "aliases": [ + "part_alternation_mark" + ] + }, + { + "emoji": "⛅", + "aliases": [ + "partly_sunny" + ] + }, + { + "emoji": "🥳", + "aliases": [ + "partying_face" + ] + }, + { + "emoji": "🛳️", + "aliases": [ + "passenger_ship" + ] + }, + { + "emoji": "🛂", + "aliases": [ + "passport_control" + ] + }, + { + "emoji": "⏸️", + "aliases": [ + "pause_button" + ] + }, + { + "emoji": "🫛", + "aliases": [ + "pea_pod" + ] + }, + { + "emoji": "☮️", + "aliases": [ + "peace_symbol" + ] + }, + { + "emoji": "🍑", + "aliases": [ + "peach" + ] + }, + { + "emoji": "🦚", + "aliases": [ + "peacock" + ] + }, + { + "emoji": "🥜", + "aliases": [ + "peanuts" + ] + }, + { + "emoji": "🍐", + "aliases": [ + "pear" + ] + }, + { + "emoji": "🖊️", + "aliases": [ + "pen" + ] + }, + { + "emoji": "✏️", + "aliases": [ + "pencil2" + ] + }, + { + "emoji": "🐧", + "aliases": [ + "penguin" + ] + }, + { + "emoji": "😔", + "aliases": [ + "pensive" + ] + }, + { + "emoji": "🧑‍🤝‍🧑", + "aliases": [ + "people_holding_hands" + ] + }, + { + "emoji": "🫂", + "aliases": [ + "people_hugging" + ] + }, + { + "emoji": "🎭", + "aliases": [ + "performing_arts" + ] + }, + { + "emoji": "😣", + "aliases": [ + "persevere" + ] + }, + { + "emoji": "🧑‍🦲", + "aliases": [ + "person_bald" + ] + }, + { + "emoji": "🧑‍🦱", + "aliases": [ + "person_curly_hair" + ] + }, + { + "emoji": "🧑‍🍼", + "aliases": [ + "person_feeding_baby" + ] + }, + { + "emoji": "🤺", + "aliases": [ + "person_fencing" + ] + }, + { + "emoji": "🧑‍🦽", + "aliases": [ + "person_in_manual_wheelchair" + ] + }, + { + "emoji": "🧑‍🦽‍➡️", + "aliases": [ + "person_in_manual_wheelchair_facing_right" + ] + }, + { + "emoji": "🧑‍🦼", + "aliases": [ + "person_in_motorized_wheelchair" + ] + }, + { + "emoji": "🧑‍🦼‍➡️", + "aliases": [ + "person_in_motorized_wheelchair_facing_right" + ] + }, + { + "emoji": "🤵", + "aliases": [ + "person_in_tuxedo" + ] + }, + { + "emoji": "🧎‍➡️", + "aliases": [ + "person_kneeling_facing_right" + ] + }, + { + "emoji": "🧑‍🦰", + "aliases": [ + "person_red_hair" + ] + }, + { + "emoji": "🏃‍➡️", + "aliases": [ + "person_running_facing_right" + ] + }, + { + "emoji": "🚶‍➡️", + "aliases": [ + "person_walking_facing_right" + ] + }, + { + "emoji": "🧑‍🦳", + "aliases": [ + "person_white_hair" + ] + }, + { + "emoji": "🫅", + "aliases": [ + "person_with_crown" + ] + }, + { + "emoji": "🧑‍🦯", + "aliases": [ + "person_with_probing_cane" + ] + }, + { + "emoji": "👳", + "aliases": [ + "person_with_turban" + ] + }, + { + "emoji": "👰", + "aliases": [ + "person_with_veil" + ] + }, + { + "emoji": "🧑‍🦯‍➡️", + "aliases": [ + "person_with_white_cane_facing_right" + ] + }, + { + "emoji": "🇵🇪", + "aliases": [ + "peru" + ] + }, + { + "emoji": "🧫", + "aliases": [ + "petri_dish" + ] + }, + { + "emoji": "🇵🇭", + "aliases": [ + "philippines" + ] + }, + { + "emoji": "🐦‍🔥", + "aliases": [ + "phoenix" + ] + }, + { + "emoji": "☎️", + "aliases": [ + "phone", + "telephone" + ] + }, + { + "emoji": "⛏️", + "aliases": [ + "pick" + ] + }, + { + "emoji": "🛻", + "aliases": [ + "pickup_truck" + ] + }, + { + "emoji": "🥧", + "aliases": [ + "pie" + ] + }, + { + "emoji": "🐷", + "aliases": [ + "pig" + ] + }, + { + "emoji": "🐖", + "aliases": [ + "pig2" + ] + }, + { + "emoji": "🐽", + "aliases": [ + "pig_nose" + ] + }, + { + "emoji": "💊", + "aliases": [ + "pill" + ] + }, + { + "emoji": "🧑‍✈️", + "aliases": [ + "pilot" + ] + }, + { + "emoji": "🪅", + "aliases": [ + "pinata" + ] + }, + { + "emoji": "🤌", + "aliases": [ + "pinched_fingers" + ] + }, + { + "emoji": "🤏", + "aliases": [ + "pinching_hand" + ] + }, + { + "emoji": "🍍", + "aliases": [ + "pineapple" + ] + }, + { + "emoji": "🏓", + "aliases": [ + "ping_pong" + ] + }, + { + "emoji": "🩷", + "aliases": [ + "pink_heart" + ] + }, + { + "emoji": "🏴‍☠️", + "aliases": [ + "pirate_flag" + ] + }, + { + "emoji": "♓", + "aliases": [ + "pisces" + ] + }, + { + "emoji": "🇵🇳", + "aliases": [ + "pitcairn_islands" + ] + }, + { + "emoji": "🍕", + "aliases": [ + "pizza" + ] + }, + { + "emoji": "🪧", + "aliases": [ + "placard" + ] + }, + { + "emoji": "🛐", + "aliases": [ + "place_of_worship" + ] + }, + { + "emoji": "🍽️", + "aliases": [ + "plate_with_cutlery" + ] + }, + { + "emoji": "⏯️", + "aliases": [ + "play_or_pause_button" + ] + }, + { + "emoji": "🛝", + "aliases": [ + "playground_slide" + ] + }, + { + "emoji": "🥺", + "aliases": [ + "pleading_face" + ] + }, + { + "emoji": "🪠", + "aliases": [ + "plunger" + ] + }, + { + "emoji": "👇", + "aliases": [ + "point_down" + ] + }, + { + "emoji": "👈", + "aliases": [ + "point_left" + ] + }, + { + "emoji": "👉", + "aliases": [ + "point_right" + ] + }, + { + "emoji": "☝️", + "aliases": [ + "point_up" + ] + }, + { + "emoji": "👆", + "aliases": [ + "point_up_2" + ] + }, + { + "emoji": "🇵🇱", + "aliases": [ + "poland" + ] + }, + { + "emoji": "🐻‍❄️", + "aliases": [ + "polar_bear" + ] + }, + { + "emoji": "🚓", + "aliases": [ + "police_car" + ] + }, + { + "emoji": "👮", + "aliases": [ + "police_officer", + "cop" + ] + }, + { + "emoji": "👮‍♂️", + "aliases": [ + "policeman" + ] + }, + { + "emoji": "👮‍♀️", + "aliases": [ + "policewoman" + ] + }, + { + "emoji": "🐩", + "aliases": [ + "poodle" + ] + }, + { + "emoji": "🍿", + "aliases": [ + "popcorn" + ] + }, + { + "emoji": "🇵🇹", + "aliases": [ + "portugal" + ] + }, + { + "emoji": "🏣", + "aliases": [ + "post_office" + ] + }, + { + "emoji": "📯", + "aliases": [ + "postal_horn" + ] + }, + { + "emoji": "📮", + "aliases": [ + "postbox" + ] + }, + { + "emoji": "🚰", + "aliases": [ + "potable_water" + ] + }, + { + "emoji": "🥔", + "aliases": [ + "potato" + ] + }, + { + "emoji": "🪴", + "aliases": [ + "potted_plant" + ] + }, + { + "emoji": "👝", + "aliases": [ + "pouch" + ] + }, + { + "emoji": "🍗", + "aliases": [ + "poultry_leg" + ] + }, + { + "emoji": "💷", + "aliases": [ + "pound" + ] + }, + { + "emoji": "🫗", + "aliases": [ + "pouring_liquid" + ] + }, + { + "emoji": "😾", + "aliases": [ + "pouting_cat" + ] + }, + { + "emoji": "🙎", + "aliases": [ + "pouting_face" + ] + }, + { + "emoji": "🙎‍♂️", + "aliases": [ + "pouting_man" + ] + }, + { + "emoji": "🙎‍♀️", + "aliases": [ + "pouting_woman" + ] + }, + { + "emoji": "🙏", + "aliases": [ + "pray" + ] + }, + { + "emoji": "📿", + "aliases": [ + "prayer_beads" + ] + }, + { + "emoji": "🫃", + "aliases": [ + "pregnant_man" + ] + }, + { + "emoji": "🫄", + "aliases": [ + "pregnant_person" + ] + }, + { + "emoji": "🤰", + "aliases": [ + "pregnant_woman" + ] + }, + { + "emoji": "🥨", + "aliases": [ + "pretzel" + ] + }, + { + "emoji": "⏮️", + "aliases": [ + "previous_track_button" + ] + }, + { + "emoji": "🤴", + "aliases": [ + "prince" + ] + }, + { + "emoji": "👸", + "aliases": [ + "princess" + ] + }, + { + "emoji": "🖨️", + "aliases": [ + "printer" + ] + }, + { + "emoji": "🦯", + "aliases": [ + "probing_cane" + ] + }, + { + "emoji": "🇵🇷", + "aliases": [ + "puerto_rico" + ] + }, + { + "emoji": "🟣", + "aliases": [ + "purple_circle" + ] + }, + { + "emoji": "💜", + "aliases": [ + "purple_heart" + ] + }, + { + "emoji": "🟪", + "aliases": [ + "purple_square" + ] + }, + { + "emoji": "👛", + "aliases": [ + "purse" + ] + }, + { + "emoji": "📌", + "aliases": [ + "pushpin" + ] + }, + { + "emoji": "🚮", + "aliases": [ + "put_litter_in_its_place" + ] + }, + { + "emoji": "🇶🇦", + "aliases": [ + "qatar" + ] + }, + { + "emoji": "❓", + "aliases": [ + "question" + ] + }, + { + "emoji": "🐰", + "aliases": [ + "rabbit" + ] + }, + { + "emoji": "🐇", + "aliases": [ + "rabbit2" + ] + }, + { + "emoji": "🦝", + "aliases": [ + "raccoon" + ] + }, + { + "emoji": "🐎", + "aliases": [ + "racehorse" + ] + }, + { + "emoji": "🏎️", + "aliases": [ + "racing_car" + ] + }, + { + "emoji": "📻", + "aliases": [ + "radio" + ] + }, + { + "emoji": "🔘", + "aliases": [ + "radio_button" + ] + }, + { + "emoji": "☢️", + "aliases": [ + "radioactive" + ] + }, + { + "emoji": "😡", + "aliases": [ + "rage", + "pout" + ] + }, + { + "emoji": "🚃", + "aliases": [ + "railway_car" + ] + }, + { + "emoji": "🛤️", + "aliases": [ + "railway_track" + ] + }, + { + "emoji": "🌈", + "aliases": [ + "rainbow" + ] + }, + { + "emoji": "🏳️‍🌈", + "aliases": [ + "rainbow_flag" + ] + }, + { + "emoji": "🤚", + "aliases": [ + "raised_back_of_hand" + ] + }, + { + "emoji": "🤨", + "aliases": [ + "raised_eyebrow" + ] + }, + { + "emoji": "🖐️", + "aliases": [ + "raised_hand_with_fingers_splayed" + ] + }, + { + "emoji": "🙌", + "aliases": [ + "raised_hands" + ] + }, + { + "emoji": "🙋", + "aliases": [ + "raising_hand" + ] + }, + { + "emoji": "🙋‍♂️", + "aliases": [ + "raising_hand_man" + ] + }, + { + "emoji": "🙋‍♀️", + "aliases": [ + "raising_hand_woman" + ] + }, + { + "emoji": "🐏", + "aliases": [ + "ram" + ] + }, + { + "emoji": "🍜", + "aliases": [ + "ramen" + ] + }, + { + "emoji": "🐀", + "aliases": [ + "rat" + ] + }, + { + "emoji": "🪒", + "aliases": [ + "razor" + ] + }, + { + "emoji": "🧾", + "aliases": [ + "receipt" + ] + }, + { + "emoji": "⏺️", + "aliases": [ + "record_button" + ] + }, + { + "emoji": "♻️", + "aliases": [ + "recycle" + ] + }, + { + "emoji": "🔴", + "aliases": [ + "red_circle" + ] + }, + { + "emoji": "🧧", + "aliases": [ + "red_envelope" + ] + }, + { + "emoji": "👨‍🦰", + "aliases": [ + "red_haired_man" + ] + }, + { + "emoji": "👩‍🦰", + "aliases": [ + "red_haired_woman" + ] + }, + { + "emoji": "🟥", + "aliases": [ + "red_square" + ] + }, + { + "emoji": "®️", + "aliases": [ + "registered" + ] + }, + { + "emoji": "☺️", + "aliases": [ + "relaxed" + ] + }, + { + "emoji": "😌", + "aliases": [ + "relieved" + ] + }, + { + "emoji": "🎗️", + "aliases": [ + "reminder_ribbon" + ] + }, + { + "emoji": "🔁", + "aliases": [ + "repeat" + ] + }, + { + "emoji": "🔂", + "aliases": [ + "repeat_one" + ] + }, + { + "emoji": "⛑️", + "aliases": [ + "rescue_worker_helmet" + ] + }, + { + "emoji": "🚻", + "aliases": [ + "restroom" + ] + }, + { + "emoji": "🇷🇪", + "aliases": [ + "reunion" + ] + }, + { + "emoji": "💞", + "aliases": [ + "revolving_hearts" + ] + }, + { + "emoji": "⏪", + "aliases": [ + "rewind" + ] + }, + { + "emoji": "🦏", + "aliases": [ + "rhinoceros" + ] + }, + { + "emoji": "🎀", + "aliases": [ + "ribbon" + ] + }, + { + "emoji": "🍚", + "aliases": [ + "rice" + ] + }, + { + "emoji": "🍙", + "aliases": [ + "rice_ball" + ] + }, + { + "emoji": "🍘", + "aliases": [ + "rice_cracker" + ] + }, + { + "emoji": "🎑", + "aliases": [ + "rice_scene" + ] + }, + { + "emoji": "🗯️", + "aliases": [ + "right_anger_bubble" + ] + }, + { + "emoji": "🫱", + "aliases": [ + "rightwards_hand" + ] + }, + { + "emoji": "🫸", + "aliases": [ + "rightwards_pushing_hand" + ] + }, + { + "emoji": "💍", + "aliases": [ + "ring" + ] + }, + { + "emoji": "🛟", + "aliases": [ + "ring_buoy" + ] + }, + { + "emoji": "🪐", + "aliases": [ + "ringed_planet" + ] + }, + { + "emoji": "🤖", + "aliases": [ + "robot" + ] + }, + { + "emoji": "🪨", + "aliases": [ + "rock" + ] + }, + { + "emoji": "🚀", + "aliases": [ + "rocket" + ] + }, + { + "emoji": "🤣", + "aliases": [ + "rofl" + ] + }, + { + "emoji": "🙄", + "aliases": [ + "roll_eyes" + ] + }, + { + "emoji": "🧻", + "aliases": [ + "roll_of_paper" + ] + }, + { + "emoji": "🎢", + "aliases": [ + "roller_coaster" + ] + }, + { + "emoji": "🛼", + "aliases": [ + "roller_skate" + ] + }, + { + "emoji": "🇷🇴", + "aliases": [ + "romania" + ] + }, + { + "emoji": "🐓", + "aliases": [ + "rooster" + ] + }, + { + "emoji": "🫜", + "aliases": [ + "root_vegetable" + ] + }, + { + "emoji": "🌹", + "aliases": [ + "rose" + ] + }, + { + "emoji": "🏵️", + "aliases": [ + "rosette" + ] + }, + { + "emoji": "🚨", + "aliases": [ + "rotating_light" + ] + }, + { + "emoji": "📍", + "aliases": [ + "round_pushpin" + ] + }, + { + "emoji": "🚣", + "aliases": [ + "rowboat" + ] + }, + { + "emoji": "🚣‍♂️", + "aliases": [ + "rowing_man" + ] + }, + { + "emoji": "🚣‍♀️", + "aliases": [ + "rowing_woman" + ] + }, + { + "emoji": "🇷🇺", + "aliases": [ + "ru" + ] + }, + { + "emoji": "🏉", + "aliases": [ + "rugby_football" + ] + }, + { + "emoji": "🏃", + "aliases": [ + "runner", + "running" + ] + }, + { + "emoji": "🏃‍♂️", + "aliases": [ + "running_man" + ] + }, + { + "emoji": "🎽", + "aliases": [ + "running_shirt_with_sash" + ] + }, + { + "emoji": "🏃‍♀️", + "aliases": [ + "running_woman" + ] + }, + { + "emoji": "🇷🇼", + "aliases": [ + "rwanda" + ] + }, + { + "emoji": "🈂️", + "aliases": [ + "sa" + ] + }, + { + "emoji": "🧷", + "aliases": [ + "safety_pin" + ] + }, + { + "emoji": "🦺", + "aliases": [ + "safety_vest" + ] + }, + { + "emoji": "♐", + "aliases": [ + "sagittarius" + ] + }, + { + "emoji": "🍶", + "aliases": [ + "sake" + ] + }, + { + "emoji": "🧂", + "aliases": [ + "salt" + ] + }, + { + "emoji": "🫡", + "aliases": [ + "saluting_face" + ] + }, + { + "emoji": "🇼🇸", + "aliases": [ + "samoa" + ] + }, + { + "emoji": "🇸🇲", + "aliases": [ + "san_marino" + ] + }, + { + "emoji": "👡", + "aliases": [ + "sandal" + ] + }, + { + "emoji": "🥪", + "aliases": [ + "sandwich" + ] + }, + { + "emoji": "🎅", + "aliases": [ + "santa" + ] + }, + { + "emoji": "🇸🇹", + "aliases": [ + "sao_tome_principe" + ] + }, + { + "emoji": "🥻", + "aliases": [ + "sari" + ] + }, + { + "emoji": "📡", + "aliases": [ + "satellite" + ] + }, + { + "emoji": "🇸🇦", + "aliases": [ + "saudi_arabia" + ] + }, + { + "emoji": "🧖‍♂️", + "aliases": [ + "sauna_man" + ] + }, + { + "emoji": "🧖", + "aliases": [ + "sauna_person" + ] + }, + { + "emoji": "🧖‍♀️", + "aliases": [ + "sauna_woman" + ] + }, + { + "emoji": "🦕", + "aliases": [ + "sauropod" + ] + }, + { + "emoji": "🎷", + "aliases": [ + "saxophone" + ] + }, + { + "emoji": "🧣", + "aliases": [ + "scarf" + ] + }, + { + "emoji": "🏫", + "aliases": [ + "school" + ] + }, + { + "emoji": "🎒", + "aliases": [ + "school_satchel" + ] + }, + { + "emoji": "🧑‍🔬", + "aliases": [ + "scientist" + ] + }, + { + "emoji": "✂️", + "aliases": [ + "scissors" + ] + }, + { + "emoji": "🦂", + "aliases": [ + "scorpion" + ] + }, + { + "emoji": "♏", + "aliases": [ + "scorpius" + ] + }, + { + "emoji": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "aliases": [ + "scotland" + ] + }, + { + "emoji": "😱", + "aliases": [ + "scream" + ] + }, + { + "emoji": "🙀", + "aliases": [ + "scream_cat" + ] + }, + { + "emoji": "🪛", + "aliases": [ + "screwdriver" + ] + }, + { + "emoji": "📜", + "aliases": [ + "scroll" + ] + }, + { + "emoji": "🦭", + "aliases": [ + "seal" + ] + }, + { + "emoji": "💺", + "aliases": [ + "seat" + ] + }, + { + "emoji": "㊙️", + "aliases": [ + "secret" + ] + }, + { + "emoji": "🙈", + "aliases": [ + "see_no_evil" + ] + }, + { + "emoji": "🌱", + "aliases": [ + "seedling" + ] + }, + { + "emoji": "🤳", + "aliases": [ + "selfie" + ] + }, + { + "emoji": "🇸🇳", + "aliases": [ + "senegal" + ] + }, + { + "emoji": "🇷🇸", + "aliases": [ + "serbia" + ] + }, + { + "emoji": "🐕‍🦺", + "aliases": [ + "service_dog" + ] + }, + { + "emoji": "7️⃣", + "aliases": [ + "seven" + ] + }, + { + "emoji": "🪡", + "aliases": [ + "sewing_needle" + ] + }, + { + "emoji": "🇸🇨", + "aliases": [ + "seychelles" + ] + }, + { + "emoji": "🫨", + "aliases": [ + "shaking_face" + ] + }, + { + "emoji": "🥘", + "aliases": [ + "shallow_pan_of_food" + ] + }, + { + "emoji": "☘️", + "aliases": [ + "shamrock" + ] + }, + { + "emoji": "🦈", + "aliases": [ + "shark" + ] + }, + { + "emoji": "🍧", + "aliases": [ + "shaved_ice" + ] + }, + { + "emoji": "🐑", + "aliases": [ + "sheep" + ] + }, + { + "emoji": "🐚", + "aliases": [ + "shell" + ] + }, + { + "emoji": "🛡️", + "aliases": [ + "shield" + ] + }, + { + "emoji": "⛩️", + "aliases": [ + "shinto_shrine" + ] + }, + { + "emoji": "🚢", + "aliases": [ + "ship" + ] + }, + { + "emoji": "👕", + "aliases": [ + "shirt", + "tshirt" + ] + }, + { + "emoji": "🛍️", + "aliases": [ + "shopping" + ] + }, + { + "emoji": "🛒", + "aliases": [ + "shopping_cart" + ] + }, + { + "emoji": "🩳", + "aliases": [ + "shorts" + ] + }, + { + "emoji": "🪏", + "aliases": [ + "shovel" + ] + }, + { + "emoji": "🚿", + "aliases": [ + "shower" + ] + }, + { + "emoji": "🦐", + "aliases": [ + "shrimp" + ] + }, + { + "emoji": "🤷", + "aliases": [ + "shrug" + ] + }, + { + "emoji": "🤫", + "aliases": [ + "shushing_face" + ] + }, + { + "emoji": "🇸🇱", + "aliases": [ + "sierra_leone" + ] + }, + { + "emoji": "📶", + "aliases": [ + "signal_strength" + ] + }, + { + "emoji": "🇸🇬", + "aliases": [ + "singapore" + ] + }, + { + "emoji": "🧑‍🎤", + "aliases": [ + "singer" + ] + }, + { + "emoji": "🇸🇽", + "aliases": [ + "sint_maarten" + ] + }, + { + "emoji": "6️⃣", + "aliases": [ + "six" + ] + }, + { + "emoji": "🔯", + "aliases": [ + "six_pointed_star" + ] + }, + { + "emoji": "🛹", + "aliases": [ + "skateboard" + ] + }, + { + "emoji": "🎿", + "aliases": [ + "ski" + ] + }, + { + "emoji": "⛷️", + "aliases": [ + "skier" + ] + }, + { + "emoji": "💀", + "aliases": [ + "skull" + ] + }, + { + "emoji": "☠️", + "aliases": [ + "skull_and_crossbones" + ] + }, + { + "emoji": "🦨", + "aliases": [ + "skunk" + ] + }, + { + "emoji": "🛷", + "aliases": [ + "sled" + ] + }, + { + "emoji": "😴", + "aliases": [ + "sleeping" + ] + }, + { + "emoji": "🛌", + "aliases": [ + "sleeping_bed" + ] + }, + { + "emoji": "😪", + "aliases": [ + "sleepy" + ] + }, + { + "emoji": "🙁", + "aliases": [ + "slightly_frowning_face" + ] + }, + { + "emoji": "🙂", + "aliases": [ + "slightly_smiling_face" + ] + }, + { + "emoji": "🎰", + "aliases": [ + "slot_machine" + ] + }, + { + "emoji": "🦥", + "aliases": [ + "sloth" + ] + }, + { + "emoji": "🇸🇰", + "aliases": [ + "slovakia" + ] + }, + { + "emoji": "🇸🇮", + "aliases": [ + "slovenia" + ] + }, + { + "emoji": "🛩️", + "aliases": [ + "small_airplane" + ] + }, + { + "emoji": "🔹", + "aliases": [ + "small_blue_diamond" + ] + }, + { + "emoji": "🔸", + "aliases": [ + "small_orange_diamond" + ] + }, + { + "emoji": "🔺", + "aliases": [ + "small_red_triangle" + ] + }, + { + "emoji": "🔻", + "aliases": [ + "small_red_triangle_down" + ] + }, + { + "emoji": "😄", + "aliases": [ + "smile" + ] + }, + { + "emoji": "😸", + "aliases": [ + "smile_cat" + ] + }, + { + "emoji": "😃", + "aliases": [ + "smiley" + ] + }, + { + "emoji": "😺", + "aliases": [ + "smiley_cat" + ] + }, + { + "emoji": "🥲", + "aliases": [ + "smiling_face_with_tear" + ] + }, + { + "emoji": "🥰", + "aliases": [ + "smiling_face_with_three_hearts" + ] + }, + { + "emoji": "😈", + "aliases": [ + "smiling_imp" + ] + }, + { + "emoji": "😏", + "aliases": [ + "smirk" + ] + }, + { + "emoji": "😼", + "aliases": [ + "smirk_cat" + ] + }, + { + "emoji": "🚬", + "aliases": [ + "smoking" + ] + }, + { + "emoji": "🐌", + "aliases": [ + "snail" + ] + }, + { + "emoji": "🐍", + "aliases": [ + "snake" + ] + }, + { + "emoji": "🤧", + "aliases": [ + "sneezing_face" + ] + }, + { + "emoji": "🏂", + "aliases": [ + "snowboarder" + ] + }, + { + "emoji": "❄️", + "aliases": [ + "snowflake" + ] + }, + { + "emoji": "⛄", + "aliases": [ + "snowman" + ] + }, + { + "emoji": "☃️", + "aliases": [ + "snowman_with_snow" + ] + }, + { + "emoji": "🧼", + "aliases": [ + "soap" + ] + }, + { + "emoji": "😭", + "aliases": [ + "sob" + ] + }, + { + "emoji": "⚽", + "aliases": [ + "soccer" + ] + }, + { + "emoji": "🧦", + "aliases": [ + "socks" + ] + }, + { + "emoji": "🥎", + "aliases": [ + "softball" + ] + }, + { + "emoji": "🇸🇧", + "aliases": [ + "solomon_islands" + ] + }, + { + "emoji": "🇸🇴", + "aliases": [ + "somalia" + ] + }, + { + "emoji": "🔜", + "aliases": [ + "soon" + ] + }, + { + "emoji": "🆘", + "aliases": [ + "sos" + ] + }, + { + "emoji": "🔉", + "aliases": [ + "sound" + ] + }, + { + "emoji": "🇿🇦", + "aliases": [ + "south_africa" + ] + }, + { + "emoji": "🇬🇸", + "aliases": [ + "south_georgia_south_sandwich_islands" + ] + }, + { + "emoji": "🇸🇸", + "aliases": [ + "south_sudan" + ] + }, + { + "emoji": "👾", + "aliases": [ + "space_invader" + ] + }, + { + "emoji": "♠️", + "aliases": [ + "spades" + ] + }, + { + "emoji": "🍝", + "aliases": [ + "spaghetti" + ] + }, + { + "emoji": "❇️", + "aliases": [ + "sparkle" + ] + }, + { + "emoji": "🎇", + "aliases": [ + "sparkler" + ] + }, + { + "emoji": "✨", + "aliases": [ + "sparkles" + ] + }, + { + "emoji": "💖", + "aliases": [ + "sparkling_heart" + ] + }, + { + "emoji": "🙊", + "aliases": [ + "speak_no_evil" + ] + }, + { + "emoji": "🔈", + "aliases": [ + "speaker" + ] + }, + { + "emoji": "🗣️", + "aliases": [ + "speaking_head" + ] + }, + { + "emoji": "💬", + "aliases": [ + "speech_balloon" + ] + }, + { + "emoji": "🚤", + "aliases": [ + "speedboat" + ] + }, + { + "emoji": "🕷️", + "aliases": [ + "spider" + ] + }, + { + "emoji": "🕸️", + "aliases": [ + "spider_web" + ] + }, + { + "emoji": "🗓️", + "aliases": [ + "spiral_calendar" + ] + }, + { + "emoji": "🗒️", + "aliases": [ + "spiral_notepad" + ] + }, + { + "emoji": "🫟", + "aliases": [ + "splatter" + ] + }, + { + "emoji": "🧽", + "aliases": [ + "sponge" + ] + }, + { + "emoji": "🥄", + "aliases": [ + "spoon" + ] + }, + { + "emoji": "🦑", + "aliases": [ + "squid" + ] + }, + { + "emoji": "🇱🇰", + "aliases": [ + "sri_lanka" + ] + }, + { + "emoji": "🇧🇱", + "aliases": [ + "st_barthelemy" + ] + }, + { + "emoji": "🇸🇭", + "aliases": [ + "st_helena" + ] + }, + { + "emoji": "🇰🇳", + "aliases": [ + "st_kitts_nevis" + ] + }, + { + "emoji": "🇱🇨", + "aliases": [ + "st_lucia" + ] + }, + { + "emoji": "🇲🇫", + "aliases": [ + "st_martin" + ] + }, + { + "emoji": "🇵🇲", + "aliases": [ + "st_pierre_miquelon" + ] + }, + { + "emoji": "🇻🇨", + "aliases": [ + "st_vincent_grenadines" + ] + }, + { + "emoji": "🏟️", + "aliases": [ + "stadium" + ] + }, + { + "emoji": "🧍‍♂️", + "aliases": [ + "standing_man" + ] + }, + { + "emoji": "🧍", + "aliases": [ + "standing_person" + ] + }, + { + "emoji": "🧍‍♀️", + "aliases": [ + "standing_woman" + ] + }, + { + "emoji": "⭐", + "aliases": [ + "star" + ] + }, + { + "emoji": "🌟", + "aliases": [ + "star2" + ] + }, + { + "emoji": "☪️", + "aliases": [ + "star_and_crescent" + ] + }, + { + "emoji": "✡️", + "aliases": [ + "star_of_david" + ] + }, + { + "emoji": "🤩", + "aliases": [ + "star_struck" + ] + }, + { + "emoji": "🌠", + "aliases": [ + "stars" + ] + }, + { + "emoji": "🚉", + "aliases": [ + "station" + ] + }, + { + "emoji": "🗽", + "aliases": [ + "statue_of_liberty" + ] + }, + { + "emoji": "🚂", + "aliases": [ + "steam_locomotive" + ] + }, + { + "emoji": "🩺", + "aliases": [ + "stethoscope" + ] + }, + { + "emoji": "🍲", + "aliases": [ + "stew" + ] + }, + { + "emoji": "⏹️", + "aliases": [ + "stop_button" + ] + }, + { + "emoji": "🛑", + "aliases": [ + "stop_sign" + ] + }, + { + "emoji": "⏱️", + "aliases": [ + "stopwatch" + ] + }, + { + "emoji": "📏", + "aliases": [ + "straight_ruler" + ] + }, + { + "emoji": "🍓", + "aliases": [ + "strawberry" + ] + }, + { + "emoji": "😛", + "aliases": [ + "stuck_out_tongue" + ] + }, + { + "emoji": "😝", + "aliases": [ + "stuck_out_tongue_closed_eyes" + ] + }, + { + "emoji": "😜", + "aliases": [ + "stuck_out_tongue_winking_eye" + ] + }, + { + "emoji": "🧑‍🎓", + "aliases": [ + "student" + ] + }, + { + "emoji": "🎙️", + "aliases": [ + "studio_microphone" + ] + }, + { + "emoji": "🥙", + "aliases": [ + "stuffed_flatbread" + ] + }, + { + "emoji": "🇸🇩", + "aliases": [ + "sudan" + ] + }, + { + "emoji": "🌥️", + "aliases": [ + "sun_behind_large_cloud" + ] + }, + { + "emoji": "🌦️", + "aliases": [ + "sun_behind_rain_cloud" + ] + }, + { + "emoji": "🌤️", + "aliases": [ + "sun_behind_small_cloud" + ] + }, + { + "emoji": "🌞", + "aliases": [ + "sun_with_face" + ] + }, + { + "emoji": "🌻", + "aliases": [ + "sunflower" + ] + }, + { + "emoji": "😎", + "aliases": [ + "sunglasses" + ] + }, + { + "emoji": "☀️", + "aliases": [ + "sunny" + ] + }, + { + "emoji": "🌅", + "aliases": [ + "sunrise" + ] + }, + { + "emoji": "🌄", + "aliases": [ + "sunrise_over_mountains" + ] + }, + { + "emoji": "🦸", + "aliases": [ + "superhero" + ] + }, + { + "emoji": "🦸‍♂️", + "aliases": [ + "superhero_man" + ] + }, + { + "emoji": "🦸‍♀️", + "aliases": [ + "superhero_woman" + ] + }, + { + "emoji": "🦹", + "aliases": [ + "supervillain" + ] + }, + { + "emoji": "🦹‍♂️", + "aliases": [ + "supervillain_man" + ] + }, + { + "emoji": "🦹‍♀️", + "aliases": [ + "supervillain_woman" + ] + }, + { + "emoji": "🏄", + "aliases": [ + "surfer" + ] + }, + { + "emoji": "🏄‍♂️", + "aliases": [ + "surfing_man" + ] + }, + { + "emoji": "🏄‍♀️", + "aliases": [ + "surfing_woman" + ] + }, + { + "emoji": "🇸🇷", + "aliases": [ + "suriname" + ] + }, + { + "emoji": "🍣", + "aliases": [ + "sushi" + ] + }, + { + "emoji": "🚟", + "aliases": [ + "suspension_railway" + ] + }, + { + "emoji": "🇸🇯", + "aliases": [ + "svalbard_jan_mayen" + ] + }, + { + "emoji": "🦢", + "aliases": [ + "swan" + ] + }, + { + "emoji": "🇸🇿", + "aliases": [ + "swaziland" + ] + }, + { + "emoji": "😓", + "aliases": [ + "sweat" + ] + }, + { + "emoji": "💦", + "aliases": [ + "sweat_drops" + ] + }, + { + "emoji": "😅", + "aliases": [ + "sweat_smile" + ] + }, + { + "emoji": "🇸🇪", + "aliases": [ + "sweden" + ] + }, + { + "emoji": "🍠", + "aliases": [ + "sweet_potato" + ] + }, + { + "emoji": "🩲", + "aliases": [ + "swim_brief" + ] + }, + { + "emoji": "🏊", + "aliases": [ + "swimmer" + ] + }, + { + "emoji": "🏊‍♂️", + "aliases": [ + "swimming_man" + ] + }, + { + "emoji": "🏊‍♀️", + "aliases": [ + "swimming_woman" + ] + }, + { + "emoji": "🇨🇭", + "aliases": [ + "switzerland" + ] + }, + { + "emoji": "🔣", + "aliases": [ + "symbols" + ] + }, + { + "emoji": "🕍", + "aliases": [ + "synagogue" + ] + }, + { + "emoji": "🇸🇾", + "aliases": [ + "syria" + ] + }, + { + "emoji": "💉", + "aliases": [ + "syringe" + ] + }, + { + "emoji": "🦖", + "aliases": [ + "t-rex" + ] + }, + { + "emoji": "🌮", + "aliases": [ + "taco" + ] + }, + { + "emoji": "🎉", + "aliases": [ + "tada", + "hooray" + ] + }, + { + "emoji": "🇹🇼", + "aliases": [ + "taiwan" + ] + }, + { + "emoji": "🇹🇯", + "aliases": [ + "tajikistan" + ] + }, + { + "emoji": "🥡", + "aliases": [ + "takeout_box" + ] + }, + { + "emoji": "🫔", + "aliases": [ + "tamale" + ] + }, + { + "emoji": "🎋", + "aliases": [ + "tanabata_tree" + ] + }, + { + "emoji": "🍊", + "aliases": [ + "tangerine", + "orange", + "mandarin" + ] + }, + { + "emoji": "🇹🇿", + "aliases": [ + "tanzania" + ] + }, + { + "emoji": "♉", + "aliases": [ + "taurus" + ] + }, + { + "emoji": "🚕", + "aliases": [ + "taxi" + ] + }, + { + "emoji": "🍵", + "aliases": [ + "tea" + ] + }, + { + "emoji": "🧑‍🏫", + "aliases": [ + "teacher" + ] + }, + { + "emoji": "🫖", + "aliases": [ + "teapot" + ] + }, + { + "emoji": "🧑‍💻", + "aliases": [ + "technologist" + ] + }, + { + "emoji": "🧸", + "aliases": [ + "teddy_bear" + ] + }, + { + "emoji": "📞", + "aliases": [ + "telephone_receiver" + ] + }, + { + "emoji": "🔭", + "aliases": [ + "telescope" + ] + }, + { + "emoji": "🎾", + "aliases": [ + "tennis" + ] + }, + { + "emoji": "⛺", + "aliases": [ + "tent" + ] + }, + { + "emoji": "🧪", + "aliases": [ + "test_tube" + ] + }, + { + "emoji": "🇹🇭", + "aliases": [ + "thailand" + ] + }, + { + "emoji": "🌡️", + "aliases": [ + "thermometer" + ] + }, + { + "emoji": "🤔", + "aliases": [ + "thinking" + ] + }, + { + "emoji": "🩴", + "aliases": [ + "thong_sandal" + ] + }, + { + "emoji": "💭", + "aliases": [ + "thought_balloon" + ] + }, + { + "emoji": "🧵", + "aliases": [ + "thread" + ] + }, + { + "emoji": "3️⃣", + "aliases": [ + "three" + ] + }, + { + "emoji": "🎫", + "aliases": [ + "ticket" + ] + }, + { + "emoji": "🎟️", + "aliases": [ + "tickets" + ] + }, + { + "emoji": "🐯", + "aliases": [ + "tiger" + ] + }, + { + "emoji": "🐅", + "aliases": [ + "tiger2" + ] + }, + { + "emoji": "⏲️", + "aliases": [ + "timer_clock" + ] + }, + { + "emoji": "🇹🇱", + "aliases": [ + "timor_leste" + ] + }, + { + "emoji": "💁‍♂️", + "aliases": [ + "tipping_hand_man", + "sassy_man" + ] + }, + { + "emoji": "💁", + "aliases": [ + "tipping_hand_person", + "information_desk_person" + ] + }, + { + "emoji": "💁‍♀️", + "aliases": [ + "tipping_hand_woman", + "sassy_woman" + ] + }, + { + "emoji": "😫", + "aliases": [ + "tired_face" + ] + }, + { + "emoji": "™️", + "aliases": [ + "tm" + ] + }, + { + "emoji": "🇹🇬", + "aliases": [ + "togo" + ] + }, + { + "emoji": "🚽", + "aliases": [ + "toilet" + ] + }, + { + "emoji": "🇹🇰", + "aliases": [ + "tokelau" + ] + }, + { + "emoji": "🗼", + "aliases": [ + "tokyo_tower" + ] + }, + { + "emoji": "🍅", + "aliases": [ + "tomato" + ] + }, + { + "emoji": "🇹🇴", + "aliases": [ + "tonga" + ] + }, + { + "emoji": "👅", + "aliases": [ + "tongue" + ] + }, + { + "emoji": "🧰", + "aliases": [ + "toolbox" + ] + }, + { + "emoji": "🦷", + "aliases": [ + "tooth" + ] + }, + { + "emoji": "🪥", + "aliases": [ + "toothbrush" + ] + }, + { + "emoji": "🔝", + "aliases": [ + "top" + ] + }, + { + "emoji": "🎩", + "aliases": [ + "tophat" + ] + }, + { + "emoji": "🌪️", + "aliases": [ + "tornado" + ] + }, + { + "emoji": "🇹🇷", + "aliases": [ + "tr" + ] + }, + { + "emoji": "🖲️", + "aliases": [ + "trackball" + ] + }, + { + "emoji": "🚜", + "aliases": [ + "tractor" + ] + }, + { + "emoji": "🚥", + "aliases": [ + "traffic_light" + ] + }, + { + "emoji": "🚋", + "aliases": [ + "train" + ] + }, + { + "emoji": "🚆", + "aliases": [ + "train2" + ] + }, + { + "emoji": "🚊", + "aliases": [ + "tram" + ] + }, + { + "emoji": "🏳️‍⚧️", + "aliases": [ + "transgender_flag" + ] + }, + { + "emoji": "⚧️", + "aliases": [ + "transgender_symbol" + ] + }, + { + "emoji": "🚩", + "aliases": [ + "triangular_flag_on_post" + ] + }, + { + "emoji": "📐", + "aliases": [ + "triangular_ruler" + ] + }, + { + "emoji": "🔱", + "aliases": [ + "trident" + ] + }, + { + "emoji": "🇹🇹", + "aliases": [ + "trinidad_tobago" + ] + }, + { + "emoji": "🇹🇦", + "aliases": [ + "tristan_da_cunha" + ] + }, + { + "emoji": "😤", + "aliases": [ + "triumph" + ] + }, + { + "emoji": "🧌", + "aliases": [ + "troll" + ] + }, + { + "emoji": "🚎", + "aliases": [ + "trolleybus" + ] + }, + { + "emoji": "🏆", + "aliases": [ + "trophy" + ] + }, + { + "emoji": "🍹", + "aliases": [ + "tropical_drink" + ] + }, + { + "emoji": "🐠", + "aliases": [ + "tropical_fish" + ] + }, + { + "emoji": "🚚", + "aliases": [ + "truck" + ] + }, + { + "emoji": "🎺", + "aliases": [ + "trumpet" + ] + }, + { + "emoji": "🌷", + "aliases": [ + "tulip" + ] + }, + { + "emoji": "🥃", + "aliases": [ + "tumbler_glass" + ] + }, + { + "emoji": "🇹🇳", + "aliases": [ + "tunisia" + ] + }, + { + "emoji": "🦃", + "aliases": [ + "turkey" + ] + }, + { + "emoji": "🇹🇲", + "aliases": [ + "turkmenistan" + ] + }, + { + "emoji": "🇹🇨", + "aliases": [ + "turks_caicos_islands" + ] + }, + { + "emoji": "🐢", + "aliases": [ + "turtle" + ] + }, + { + "emoji": "🇹🇻", + "aliases": [ + "tuvalu" + ] + }, + { + "emoji": "📺", + "aliases": [ + "tv" + ] + }, + { + "emoji": "🔀", + "aliases": [ + "twisted_rightwards_arrows" + ] + }, + { + "emoji": "2️⃣", + "aliases": [ + "two" + ] + }, + { + "emoji": "💕", + "aliases": [ + "two_hearts" + ] + }, + { + "emoji": "👬", + "aliases": [ + "two_men_holding_hands" + ] + }, + { + "emoji": "👭", + "aliases": [ + "two_women_holding_hands" + ] + }, + { + "emoji": "🈹", + "aliases": [ + "u5272" + ] + }, + { + "emoji": "🈴", + "aliases": [ + "u5408" + ] + }, + { + "emoji": "🈺", + "aliases": [ + "u55b6" + ] + }, + { + "emoji": "🈯", + "aliases": [ + "u6307" + ] + }, + { + "emoji": "🈷️", + "aliases": [ + "u6708" + ] + }, + { + "emoji": "🈶", + "aliases": [ + "u6709" + ] + }, + { + "emoji": "🈵", + "aliases": [ + "u6e80" + ] + }, + { + "emoji": "🈚", + "aliases": [ + "u7121" + ] + }, + { + "emoji": "🈸", + "aliases": [ + "u7533" + ] + }, + { + "emoji": "🈲", + "aliases": [ + "u7981" + ] + }, + { + "emoji": "🈳", + "aliases": [ + "u7a7a" + ] + }, + { + "emoji": "🇺🇬", + "aliases": [ + "uganda" + ] + }, + { + "emoji": "🇺🇦", + "aliases": [ + "ukraine" + ] + }, + { + "emoji": "☔", + "aliases": [ + "umbrella" + ] + }, + { + "emoji": "😒", + "aliases": [ + "unamused" + ] + }, + { + "emoji": "🔞", + "aliases": [ + "underage" + ] + }, + { + "emoji": "🦄", + "aliases": [ + "unicorn" + ] + }, + { + "emoji": "🇦🇪", + "aliases": [ + "united_arab_emirates" + ] + }, + { + "emoji": "🇺🇳", + "aliases": [ + "united_nations" + ] + }, + { + "emoji": "🔓", + "aliases": [ + "unlock" + ] + }, + { + "emoji": "🆙", + "aliases": [ + "up" + ] + }, + { + "emoji": "🙃", + "aliases": [ + "upside_down_face" + ] + }, + { + "emoji": "🇺🇾", + "aliases": [ + "uruguay" + ] + }, + { + "emoji": "🇺🇸", + "aliases": [ + "us" + ] + }, + { + "emoji": "🇺🇲", + "aliases": [ + "us_outlying_islands" + ] + }, + { + "emoji": "🇻🇮", + "aliases": [ + "us_virgin_islands" + ] + }, + { + "emoji": "🇺🇿", + "aliases": [ + "uzbekistan" + ] + }, + { + "emoji": "✌️", + "aliases": [ + "v" + ] + }, + { + "emoji": "🧛", + "aliases": [ + "vampire" + ] + }, + { + "emoji": "🧛‍♂️", + "aliases": [ + "vampire_man" + ] + }, + { + "emoji": "🧛‍♀️", + "aliases": [ + "vampire_woman" + ] + }, + { + "emoji": "🇻🇺", + "aliases": [ + "vanuatu" + ] + }, + { + "emoji": "🇻🇦", + "aliases": [ + "vatican_city" + ] + }, + { + "emoji": "🇻🇪", + "aliases": [ + "venezuela" + ] + }, + { + "emoji": "🚦", + "aliases": [ + "vertical_traffic_light" + ] + }, + { + "emoji": "📼", + "aliases": [ + "vhs" + ] + }, + { + "emoji": "📳", + "aliases": [ + "vibration_mode" + ] + }, + { + "emoji": "📹", + "aliases": [ + "video_camera" + ] + }, + { + "emoji": "🎮", + "aliases": [ + "video_game" + ] + }, + { + "emoji": "🇻🇳", + "aliases": [ + "vietnam" + ] + }, + { + "emoji": "🎻", + "aliases": [ + "violin" + ] + }, + { + "emoji": "♍", + "aliases": [ + "virgo" + ] + }, + { + "emoji": "🌋", + "aliases": [ + "volcano" + ] + }, + { + "emoji": "🏐", + "aliases": [ + "volleyball" + ] + }, + { + "emoji": "🤮", + "aliases": [ + "vomiting_face" + ] + }, + { + "emoji": "🆚", + "aliases": [ + "vs" + ] + }, + { + "emoji": "🖖", + "aliases": [ + "vulcan_salute" + ] + }, + { + "emoji": "🧇", + "aliases": [ + "waffle" + ] + }, + { + "emoji": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + "aliases": [ + "wales" + ] + }, + { + "emoji": "🚶", + "aliases": [ + "walking" + ] + }, + { + "emoji": "🚶‍♂️", + "aliases": [ + "walking_man" + ] + }, + { + "emoji": "🚶‍♀️", + "aliases": [ + "walking_woman" + ] + }, + { + "emoji": "🇼🇫", + "aliases": [ + "wallis_futuna" + ] + }, + { + "emoji": "🌘", + "aliases": [ + "waning_crescent_moon" + ] + }, + { + "emoji": "🌖", + "aliases": [ + "waning_gibbous_moon" + ] + }, + { + "emoji": "⚠️", + "aliases": [ + "warning" + ] + }, + { + "emoji": "🗑️", + "aliases": [ + "wastebasket" + ] + }, + { + "emoji": "⌚", + "aliases": [ + "watch" + ] + }, + { + "emoji": "🐃", + "aliases": [ + "water_buffalo" + ] + }, + { + "emoji": "🤽", + "aliases": [ + "water_polo" + ] + }, + { + "emoji": "🍉", + "aliases": [ + "watermelon" + ] + }, + { + "emoji": "👋", + "aliases": [ + "wave" + ] + }, + { + "emoji": "〰️", + "aliases": [ + "wavy_dash" + ] + }, + { + "emoji": "🌒", + "aliases": [ + "waxing_crescent_moon" + ] + }, + { + "emoji": "🚾", + "aliases": [ + "wc" + ] + }, + { + "emoji": "😩", + "aliases": [ + "weary" + ] + }, + { + "emoji": "💒", + "aliases": [ + "wedding" + ] + }, + { + "emoji": "🏋️", + "aliases": [ + "weight_lifting" + ] + }, + { + "emoji": "🏋️‍♂️", + "aliases": [ + "weight_lifting_man" + ] + }, + { + "emoji": "🏋️‍♀️", + "aliases": [ + "weight_lifting_woman" + ] + }, + { + "emoji": "🇪🇭", + "aliases": [ + "western_sahara" + ] + }, + { + "emoji": "🐳", + "aliases": [ + "whale" + ] + }, + { + "emoji": "🐋", + "aliases": [ + "whale2" + ] + }, + { + "emoji": "🛞", + "aliases": [ + "wheel" + ] + }, + { + "emoji": "☸️", + "aliases": [ + "wheel_of_dharma" + ] + }, + { + "emoji": "♿", + "aliases": [ + "wheelchair" + ] + }, + { + "emoji": "✅", + "aliases": [ + "white_check_mark" + ] + }, + { + "emoji": "⚪", + "aliases": [ + "white_circle" + ] + }, + { + "emoji": "🏳️", + "aliases": [ + "white_flag" + ] + }, + { + "emoji": "💮", + "aliases": [ + "white_flower" + ] + }, + { + "emoji": "👨‍🦳", + "aliases": [ + "white_haired_man" + ] + }, + { + "emoji": "👩‍🦳", + "aliases": [ + "white_haired_woman" + ] + }, + { + "emoji": "🤍", + "aliases": [ + "white_heart" + ] + }, + { + "emoji": "⬜", + "aliases": [ + "white_large_square" + ] + }, + { + "emoji": "◽", + "aliases": [ + "white_medium_small_square" + ] + }, + { + "emoji": "◻️", + "aliases": [ + "white_medium_square" + ] + }, + { + "emoji": "▫️", + "aliases": [ + "white_small_square" + ] + }, + { + "emoji": "🔳", + "aliases": [ + "white_square_button" + ] + }, + { + "emoji": "🥀", + "aliases": [ + "wilted_flower" + ] + }, + { + "emoji": "🎐", + "aliases": [ + "wind_chime" + ] + }, + { + "emoji": "🌬️", + "aliases": [ + "wind_face" + ] + }, + { + "emoji": "🪟", + "aliases": [ + "window" + ] + }, + { + "emoji": "🍷", + "aliases": [ + "wine_glass" + ] + }, + { + "emoji": "🪽", + "aliases": [ + "wing" + ] + }, + { + "emoji": "😉", + "aliases": [ + "wink" + ] + }, + { + "emoji": "🛜", + "aliases": [ + "wireless" + ] + }, + { + "emoji": "🐺", + "aliases": [ + "wolf" + ] + }, + { + "emoji": "👩", + "aliases": [ + "woman" + ] + }, + { + "emoji": "👩‍🎨", + "aliases": [ + "woman_artist" + ] + }, + { + "emoji": "👩‍🚀", + "aliases": [ + "woman_astronaut" + ] + }, + { + "emoji": "🧔‍♀️", + "aliases": [ + "woman_beard" + ] + }, + { + "emoji": "🤸‍♀️", + "aliases": [ + "woman_cartwheeling" + ] + }, + { + "emoji": "👩‍🍳", + "aliases": [ + "woman_cook" + ] + }, + { + "emoji": "💃", + "aliases": [ + "woman_dancing", + "dancer" + ] + }, + { + "emoji": "🤦‍♀️", + "aliases": [ + "woman_facepalming" + ] + }, + { + "emoji": "👩‍🏭", + "aliases": [ + "woman_factory_worker" + ] + }, + { + "emoji": "👩‍🌾", + "aliases": [ + "woman_farmer" + ] + }, + { + "emoji": "👩‍🍼", + "aliases": [ + "woman_feeding_baby" + ] + }, + { + "emoji": "👩‍🚒", + "aliases": [ + "woman_firefighter" + ] + }, + { + "emoji": "👩‍⚕️", + "aliases": [ + "woman_health_worker" + ] + }, + { + "emoji": "👩‍🦽", + "aliases": [ + "woman_in_manual_wheelchair" + ] + }, + { + "emoji": "👩‍🦽‍➡️", + "aliases": [ + "woman_in_manual_wheelchair_facing_right" + ] + }, + { + "emoji": "👩‍🦼", + "aliases": [ + "woman_in_motorized_wheelchair" + ] + }, + { + "emoji": "👩‍🦼‍➡️", + "aliases": [ + "woman_in_motorized_wheelchair_facing_right" + ] + }, + { + "emoji": "🤵‍♀️", + "aliases": [ + "woman_in_tuxedo" + ] + }, + { + "emoji": "👩‍⚖️", + "aliases": [ + "woman_judge" + ] + }, + { + "emoji": "🤹‍♀️", + "aliases": [ + "woman_juggling" + ] + }, + { + "emoji": "🧎‍♀️‍➡️", + "aliases": [ + "woman_kneeling_facing_right" + ] + }, + { + "emoji": "👩‍🔧", + "aliases": [ + "woman_mechanic" + ] + }, + { + "emoji": "👩‍💼", + "aliases": [ + "woman_office_worker" + ] + }, + { + "emoji": "👩‍✈️", + "aliases": [ + "woman_pilot" + ] + }, + { + "emoji": "🤾‍♀️", + "aliases": [ + "woman_playing_handball" + ] + }, + { + "emoji": "🤽‍♀️", + "aliases": [ + "woman_playing_water_polo" + ] + }, + { + "emoji": "🏃‍♀️‍➡️", + "aliases": [ + "woman_running_facing_right" + ] + }, + { + "emoji": "👩‍🔬", + "aliases": [ + "woman_scientist" + ] + }, + { + "emoji": "🤷‍♀️", + "aliases": [ + "woman_shrugging" + ] + }, + { + "emoji": "👩‍🎤", + "aliases": [ + "woman_singer" + ] + }, + { + "emoji": "👩‍🎓", + "aliases": [ + "woman_student" + ] + }, + { + "emoji": "👩‍🏫", + "aliases": [ + "woman_teacher" + ] + }, + { + "emoji": "👩‍💻", + "aliases": [ + "woman_technologist" + ] + }, + { + "emoji": "🚶‍♀️‍➡️", + "aliases": [ + "woman_walking_facing_right" + ] + }, + { + "emoji": "🧕", + "aliases": [ + "woman_with_headscarf" + ] + }, + { + "emoji": "👩‍🦯", + "aliases": [ + "woman_with_probing_cane" + ] + }, + { + "emoji": "👳‍♀️", + "aliases": [ + "woman_with_turban" + ] + }, + { + "emoji": "👰‍♀️", + "aliases": [ + "woman_with_veil", + "bride_with_veil" + ] + }, + { + "emoji": "👩‍🦯‍➡️", + "aliases": [ + "woman_with_white_cane_facing_right" + ] + }, + { + "emoji": "👚", + "aliases": [ + "womans_clothes" + ] + }, + { + "emoji": "👒", + "aliases": [ + "womans_hat" + ] + }, + { + "emoji": "🤼‍♀️", + "aliases": [ + "women_wrestling" + ] + }, + { + "emoji": "🚺", + "aliases": [ + "womens" + ] + }, + { + "emoji": "🪵", + "aliases": [ + "wood" + ] + }, + { + "emoji": "🥴", + "aliases": [ + "woozy_face" + ] + }, + { + "emoji": "🗺️", + "aliases": [ + "world_map" + ] + }, + { + "emoji": "🪱", + "aliases": [ + "worm" + ] + }, + { + "emoji": "😟", + "aliases": [ + "worried" + ] + }, + { + "emoji": "🔧", + "aliases": [ + "wrench" + ] + }, + { + "emoji": "🤼", + "aliases": [ + "wrestling" + ] + }, + { + "emoji": "✍️", + "aliases": [ + "writing_hand" + ] + }, + { + "emoji": "❌", + "aliases": [ + "x" + ] + }, + { + "emoji": "🩻", + "aliases": [ + "x_ray" + ] + }, + { + "emoji": "🧶", + "aliases": [ + "yarn" + ] + }, + { + "emoji": "🥱", + "aliases": [ + "yawning_face" + ] + }, + { + "emoji": "🟡", + "aliases": [ + "yellow_circle" + ] + }, + { + "emoji": "💛", + "aliases": [ + "yellow_heart" + ] + }, + { + "emoji": "🟨", + "aliases": [ + "yellow_square" + ] + }, + { + "emoji": "🇾🇪", + "aliases": [ + "yemen" + ] + }, + { + "emoji": "💴", + "aliases": [ + "yen" + ] + }, + { + "emoji": "☯️", + "aliases": [ + "yin_yang" + ] + }, + { + "emoji": "🪀", + "aliases": [ + "yo_yo" + ] + }, + { + "emoji": "😋", + "aliases": [ + "yum" + ] + }, + { + "emoji": "🇿🇲", + "aliases": [ + "zambia" + ] + }, + { + "emoji": "🤪", + "aliases": [ + "zany_face" + ] + }, + { + "emoji": "⚡", + "aliases": [ + "zap" + ] + }, + { + "emoji": "🦓", + "aliases": [ + "zebra" + ] + }, + { + "emoji": "0️⃣", + "aliases": [ + "zero" + ] + }, + { + "emoji": "🇿🇼", + "aliases": [ + "zimbabwe" + ] + }, + { + "emoji": "🤐", + "aliases": [ + "zipper_mouth_face" + ] + }, + { + "emoji": "🧟", + "aliases": [ + "zombie" + ] + }, + { + "emoji": "🧟‍♂️", + "aliases": [ + "zombie_man" + ] + }, + { + "emoji": "🧟‍♀️", + "aliases": [ + "zombie_woman" + ] + }, + { + "emoji": "💤", + "aliases": [ + "zzz" + ] + } +] diff --git a/package/gitea/source/assets/go-licenses.json b/package/gitea/source/assets/go-licenses.json index b1057576..61bf7670 100644 --- a/package/gitea/source/assets/go-licenses.json +++ b/package/gitea/source/assets/go-licenses.json @@ -27,7 +27,7 @@ { "name": "connectrpc.com/connect", "path": "connectrpc.com/connect/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 2021-2024 The Connect Authors\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" + "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 2021-2025 The Connect Authors\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": "dario.cat/mergo", @@ -39,11 +39,6 @@ "path": "filippo.io/edwards25519/LICENSE", "licenseText": "Copyright (c) 2009 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": "git.sr.ht/~mariusor/go-xsd-duration", - "path": "git.sr.ht/~mariusor/go-xsd-duration/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2019 Go xsd:duration\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": "gitea.com/go-chi/binding", "path": "gitea.com/go-chi/binding/LICENSE", @@ -114,6 +109,11 @@ "path": "github.com/ProtonMail/go-crypto/LICENSE", "licenseText": "Copyright (c) 2009 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/PuerkitoBio/goquery", + "path": "github.com/PuerkitoBio/goquery/LICENSE", + "licenseText": "Copyright (c) 2012-2021, Martin Angers \u0026 Contributors\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n* 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.\n\n* Neither the name of the author nor the names of its contributors may be used to endorse or promote products 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, 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 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.\n" + }, { "name": "github.com/RoaringBitmap/roaring/v2", "path": "github.com/RoaringBitmap/roaring/v2/LICENSE", @@ -139,6 +139,16 @@ "path": "github.com/andybalholm/brotli/LICENSE", "licenseText": "Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors.\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/andybalholm/brotli/flate", + "path": "github.com/andybalholm/brotli/flate/LICENSE", + "licenseText": "Copyright (c) 2009 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/andybalholm/cascadia", + "path": "github.com/andybalholm/cascadia/LICENSE", + "licenseText": "Copyright (c) 2011 Andy Balholm. 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\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/anmitsu/go-shlex", "path": "github.com/anmitsu/go-shlex/LICENSE", @@ -190,8 +200,8 @@ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Aymerick JEHANNE\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/beorn7/perks/quantile", - "path": "github.com/beorn7/perks/quantile/LICENSE", + "name": "github.com/beorn7/perks", + "path": "github.com/beorn7/perks/LICENSE", "licenseText": "Copyright (C) 2013 Blake Mizerany\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" }, { @@ -325,8 +335,8 @@ "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Florian Sundermann\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/bradfitz/gomemcache/memcache", - "path": "github.com/bradfitz/gomemcache/memcache/LICENSE", + "name": "github.com/bradfitz/gomemcache", + "path": "github.com/bradfitz/gomemcache/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.\n" }, { @@ -355,8 +365,8 @@ "licenseText": "Copyright (c) 2016 Caleb Spare\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/charmbracelet/git-lfs-transfer/transfer", - "path": "github.com/charmbracelet/git-lfs-transfer/transfer/LICENSE", + "name": "github.com/charmbracelet/git-lfs-transfer", + "path": "github.com/charmbracelet/git-lfs-transfer/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2022-2023 Charmbracelet, 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 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" }, { @@ -364,6 +374,16 @@ "path": "github.com/chi-middleware/proxy/LICENSE", "licenseText": "Copyright (c) 2020 Lauris BH\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/clipperhouse/displaywidth", + "path": "github.com/clipperhouse/displaywidth/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2025 Matt Sherman\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/clipperhouse/uax29/v2", + "path": "github.com/clipperhouse/uax29/v2/LICENSE", + "licenseText": "MIT License\n\nCopyright (c) 2020 Matt Sherman\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/cloudflare/circl", "path": "github.com/cloudflare/circl/LICENSE", @@ -381,22 +401,22 @@ }, { "name": "github.com/couchbase/goutils", - "path": "github.com/couchbase/goutils/LICENSE.md", - "licenseText": "Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n" + "path": "github.com/couchbase/goutils/LICENSE.txt", + "licenseText": "Source code in this repository is licensed under various licenses. The\nBusiness Source License 1.1 (BSL) is one such license. Each file indicates in\na section at the beginning of the file the name of the license that applies to\nit. All licenses used in this repository can be found in the top-level\nlicenses directory.\n" }, { - "name": "github.com/cpuguy83/go-md2man/v2/md2man", - "path": "github.com/cpuguy83/go-md2man/v2/md2man/LICENSE.md", + "name": "github.com/cpuguy83/go-md2man/v2", + "path": "github.com/cpuguy83/go-md2man/v2/LICENSE.md", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2014 Brian Goff\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/cyphar/filepath-securejoin", - "path": "github.com/cyphar/filepath-securejoin/LICENSE", - "licenseText": "Copyright (C) 2014-2015 Docker Inc \u0026 Go Authors. All rights reserved.\nCopyright (C) 2017-2024 SUSE LLC. 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" + "path": "github.com/cyphar/filepath-securejoin/COPYING.md", + "licenseText": "## COPYING ##\n\n`SPDX-License-Identifier: BSD-3-Clause AND MPL-2.0`\n\nThis project is made up of code licensed under different licenses. Which code\nyou use will have an impact on whether only one or both licenses apply to your\nusage of this library.\n\nNote that **each file** in this project individually has a code comment at the\nstart describing the license of that particular file -- this is the most\naccurate license information of this project; in case there is any conflict\nbetween this document and the comment at the start of a file, the comment shall\ntake precedence. The only purpose of this document is to work around [a known\ntechnical limitation of pkg.go.dev's license checking tool when dealing with\nnon-trivial project licenses][go75067].\n\n[go75067]: https://go.dev/issue/75067\n\n### `BSD-3-Clause` ###\n\nAt time of writing, the following files and directories are licensed under the\nBSD-3-Clause license:\n\n * `doc.go`\n * `join*.go`\n * `vfs.go`\n * `internal/consts/*.go`\n * `pathrs-lite/internal/gocompat/*.go`\n * `pathrs-lite/internal/kernelversion/*.go`\n\nThe text of the BSD-3-Clause license used by this project is the following (the\ntext is also available from the [`LICENSE.BSD`](./LICENSE.BSD) file):\n\n```\nCopyright (C) 2014-2015 Docker Inc \u0026 Go Authors. All rights reserved.\nCopyright (C) 2017-2024 SUSE LLC. 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```\n\n### `MPL-2.0` ###\n\nAll other files (unless otherwise marked) are licensed under the Mozilla Public\nLicense (version 2.0).\n\nThe text of the Mozilla Public License (version 2.0) is the following (the text\nis also available from the [`LICENSE.MPL-2.0`](./LICENSE.MPL-2.0) file):\n\n```\nMozilla 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 https://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```\n" }, { - "name": "github.com/davecgh/go-spew/spew", - "path": "github.com/davecgh/go-spew/spew/LICENSE", + "name": "github.com/davecgh/go-spew", + "path": "github.com/davecgh/go-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" }, { @@ -409,16 +429,6 @@ "path": "github.com/dimiro1/reply/LICENSE", "licenseText": "MIT License\n\nCopyright (c) Discourse\nCopyright (c) Claudemiro\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/djherbis/buffer", - "path": "github.com/djherbis/buffer/LICENSE.txt", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Dustin H\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/djherbis/nio/v3", - "path": "github.com/djherbis/nio/v3/LICENSE.txt", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Dustin H\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/dlclark/regexp2", "path": "github.com/dlclark/regexp2/LICENSE", @@ -489,21 +499,6 @@ "path": "github.com/gliderlabs/ssh/LICENSE", "licenseText": "Copyright (c) 2016 Glider Labs. 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 Glider Labs 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-ap/activitypub", - "path": "github.com/go-ap/activitypub/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2017 Golang ActitvityPub\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/go-ap/errors", - "path": "github.com/go-ap/errors/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2019 Golang ActitvityPub\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/go-ap/jsonld", - "path": "github.com/go-ap/jsonld/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2017 Marius Orcsik\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/go-asn1-ber/asn1-ber", "path": "github.com/go-asn1-ber/asn1-ber/LICENSE", @@ -520,8 +515,8 @@ "licenseText": "Copyright (c) 2014 Olivier Poitrey \u003crs@dailymotion.com\u003e\nCopyright (c) 2016-Present https://github.com/go-chi authors\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/go-co-op/gocron", - "path": "github.com/go-co-op/gocron/LICENSE", + "name": "github.com/go-co-op/gocron/v2", + "path": "github.com/go-co-op/gocron/v2/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2014, 辣椒面\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" }, { @@ -569,10 +564,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", + "path": "github.com/go-webauthn/x/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", @@ -594,11 +599,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", @@ -615,15 +615,10 @@ "licenseText": "Copyright (c) 2017 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/golang/groupcache/lru", - "path": "github.com/golang/groupcache/lru/LICENSE", + "name": "github.com/golang/groupcache", + "path": "github.com/golang/groupcache/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 [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/golang/protobuf/proto", - "path": "github.com/golang/protobuf/proto/LICENSE", - "licenseText": "Copyright 2010 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\n" - }, { "name": "github.com/golang/snappy", "path": "github.com/golang/snappy/LICENSE", @@ -635,18 +630,18 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/google/flatbuffers/go", - "path": "github.com/google/flatbuffers/go/LICENSE", + "name": "github.com/google/flatbuffers", + "path": "github.com/google/flatbuffers/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.\n" }, { - "name": "github.com/google/go-github/v74/github", - "path": "github.com/google/go-github/v74/github/LICENSE", + "name": "github.com/google/go-github/v84", + "path": "github.com/google/go-github/v84/LICENSE", "licenseText": "Copyright (c) 2013 The go-github AUTHORS. All rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n" }, { - "name": "github.com/google/go-querystring/query", - "path": "github.com/google/go-querystring/query/LICENSE", + "name": "github.com/google/go-querystring", + "path": "github.com/google/go-querystring/LICENSE", "licenseText": "Copyright (c) 2013 Google. 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" }, { @@ -660,8 +655,8 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/google/pprof/profile", - "path": "github.com/google/pprof/profile/LICENSE", + "name": "github.com/google/pprof", + "path": "github.com/google/pprof/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.\n" }, { @@ -670,8 +665,8 @@ "licenseText": "Copyright (c) 2009,2014 Google Inc. 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/gorilla/css/scanner", - "path": "github.com/gorilla/css/scanner/LICENSE", + "name": "github.com/gorilla/css", + "path": "github.com/gorilla/css/LICENSE", "licenseText": "Copyright (c) 2023 The Gorilla 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\t * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n\t * 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\t * 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\n" }, { @@ -717,7 +712,7 @@ { "name": "github.com/hashicorp/go-version", "path": "github.com/hashicorp/go-version/LICENSE", - "licenseText": "Mozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of version\n 1.1 or earlier of the License, but not also under the terms of a\n Secondary License.\n\n1.6. “Executable Form”\n\n means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n means a work that combines Covered Software with other material, in a separate\n file or files, that is not Covered Software.\n\n1.8. “License”\n\n means this document.\n\n1.9. “Licensable”\n\n means having the right to grant, to the maximum extent possible, whether at the\n time of the initial grant or subsequently, any and all of the rights conveyed by\n this License.\n\n1.10. “Modifications”\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to, deletion\n from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n means any patent claim(s), including without limitation, method, process,\n and apparatus claims, in any patent Licensable by such Contributor that\n would be infringed, but for the grant of the License, by the making,\n using, selling, offering for sale, having made, import, or transfer of\n either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, “You” includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, “control” means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-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 as\n part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its Contributions\n or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution become\n effective for each Contribution on the date the Contributor first distributes\n such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under this\n License. No additional rights or licenses will be implied from the distribution\n or licensing of Covered Software under this License. Notwithstanding Section\n 2.1(b) above, no patent license is granted by a Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; 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 its\n Contributions.\n\n This License does not grant any rights in the trademarks, service marks, or\n logos of any Contributor (except as may be necessary to comply with the\n notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this License\n (see Section 10.2) or under the terms of a Secondary License (if permitted\n under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its Contributions\n are its original creation(s) or it has sufficient rights to grant the\n rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under applicable\n copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under the\n terms of this License. You must inform recipients that the Source Code Form\n of the Covered Software is governed by the terms of this License, and how\n they can obtain a copy of this License. You may not attempt to alter or\n restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this License,\n or sublicense it under different terms, provided that the license for\n the Executable Form does not attempt to limit or alter the recipients’\n rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for the\n Covered Software. If the Larger Work is a combination of Covered Software\n with a work governed by one or more Secondary Licenses, and the Covered\n Software is not Incompatible With Secondary Licenses, this License permits\n You to additionally distribute such Covered Software under the terms of\n such Secondary License(s), so that the recipient of the Larger Work may, at\n their option, further distribute the Covered Software under the terms of\n either this License or such Secondary License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices (including\n copyright notices, patent notices, disclaimers of warranty, or limitations\n of liability) contained within the Source Code Form of the Covered\n Software, except that You may alter any license notices to the extent\n required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on behalf\n of any Contributor. You must make it absolutely clear that any such\n warranty, support, indemnity, or liability obligation is offered by You\n alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute, judicial\n order, or regulation then You must: (a) comply with the terms of this License\n to the maximum extent possible; and (b) describe the limitations and the code\n they affect. Such description must be placed in a text file included with all\n distributions of the Covered Software under this License. Except to the\n extent prohibited by statute or regulation, such description must be\n sufficiently detailed for a recipient of ordinary skill to be able to\n understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n if such Contributor fails to notify You of the non-compliance by some\n reasonable means prior to 60 days after You have come back into compliance.\n Moreover, Your grants from a particular Contributor are reinstated on an\n ongoing basis if such Contributor notifies You of the non-compliance by\n some reasonable means, this is the first time You have received notice of\n non-compliance with this License from such Contributor, and You become\n compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions, counter-claims,\n and cross-claims) alleging that a Contributor Version directly or\n indirectly infringes any patent, then the rights granted to You by any and\n all Contributors for the Covered Software under Section 2.1 of this License\n shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an “as is” basis, without\n warranty of any kind, either expressed, implied, or statutory, including,\n without limitation, warranties that the Covered Software is free of defects,\n merchantable, fit for a particular purpose or non-infringing. The entire\n risk as to the quality and performance of the Covered Software is with You.\n Should any Covered Software prove defective in any respect, You (not any\n Contributor) assume the cost of any necessary servicing, repair, or\n correction. This disclaimer of warranty constitutes an essential part of this\n License. No use of any Covered Software is authorized under this License\n except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from such\n party’s negligence to the extent applicable law prohibits such limitation.\n Some jurisdictions do not allow the exclusion or limitation of incidental or\n consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts of\n a jurisdiction where the defendant maintains its principal place of business\n and such litigation shall be governed by laws of that jurisdiction, without\n reference to its conflict-of-law provisions. Nothing in this Section shall\n prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject matter\n hereof. If any provision of this License is held to be unenforceable, such\n provision shall be reformed only to the extent necessary to make it\n enforceable. Any law or regulation which provides that the language of a\n contract shall be construed against the drafter shall not be used to construe\n this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version of\n the License under which You originally received the Covered Software, or\n under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a modified\n version of this License if you rename the license and remove any\n references to the name of the license steward (except to note that such\n modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n If You choose to distribute Source Code Form that is Incompatible With\n Secondary Licenses under the terms of this version of the License, the\n notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n This Source Code Form is “Incompatible\n With Secondary Licenses”, as defined by\n the Mozilla Public License, v. 2.0.\n\n" + "licenseText": "Copyright IBM Corp. 2014, 2025\n\nMozilla Public License, version 2.0\n\n1. Definitions\n\n1.1. “Contributor”\n\n means each individual or legal entity that creates, contributes to the\n creation of, or owns Covered Software.\n\n1.2. “Contributor Version”\n\n means the combination of the Contributions of others (if any) used by a\n Contributor and that particular Contributor’s Contribution.\n\n1.3. “Contribution”\n\n means Covered Software of a particular Contributor.\n\n1.4. “Covered Software”\n\n means Source Code Form to which the initial Contributor has attached the\n notice in Exhibit A, the Executable Form of such Source Code Form, and\n Modifications of such Source Code Form, in each case including portions\n thereof.\n\n1.5. “Incompatible With Secondary Licenses”\n means\n\n a. that the initial Contributor has attached the notice described in\n Exhibit B to the Covered Software; or\n\n b. that the Covered Software was made available under the terms of version\n 1.1 or earlier of the License, but not also under the terms of a\n Secondary License.\n\n1.6. “Executable Form”\n\n means any form of the work other than Source Code Form.\n\n1.7. “Larger Work”\n\n means a work that combines Covered Software with other material, in a separate\n file or files, that is not Covered Software.\n\n1.8. “License”\n\n means this document.\n\n1.9. “Licensable”\n\n means having the right to grant, to the maximum extent possible, whether at the\n time of the initial grant or subsequently, any and all of the rights conveyed by\n this License.\n\n1.10. “Modifications”\n\n means any of the following:\n\n a. any file in Source Code Form that results from an addition to, deletion\n from, or modification of the contents of Covered Software; or\n\n b. any new file in Source Code Form that contains any Covered Software.\n\n1.11. “Patent Claims” of a Contributor\n\n means any patent claim(s), including without limitation, method, process,\n and apparatus claims, in any patent Licensable by such Contributor that\n would be infringed, but for the grant of the License, by the making,\n using, selling, offering for sale, having made, import, or transfer of\n either its Contributions or its Contributor Version.\n\n1.12. “Secondary License”\n\n means either the GNU General Public License, Version 2.0, the GNU Lesser\n General Public License, Version 2.1, the GNU Affero General Public\n License, Version 3.0, or any later versions of those licenses.\n\n1.13. “Source Code Form”\n\n means the form of the work preferred for making modifications.\n\n1.14. “You” (or “Your”)\n\n means an individual or a legal entity exercising rights under this\n License. For legal entities, “You” includes any entity that controls, is\n controlled by, or is under common control with You. For purposes of this\n definition, “control” means (a) the power, direct or indirect, to cause\n the direction or management of such entity, whether by contract or\n otherwise, or (b) ownership of more than fifty percent (50%) of the\n outstanding shares or beneficial ownership of such entity.\n\n\n2. License Grants and Conditions\n\n2.1. Grants\n\n Each Contributor hereby grants You a world-wide, royalty-free,\n non-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 as\n part of a Larger Work; and\n\n b. under Patent Claims of such Contributor to make, use, sell, offer for\n sale, have made, import, and otherwise transfer either its Contributions\n or its Contributor Version.\n\n2.2. Effective Date\n\n The licenses granted in Section 2.1 with respect to any Contribution become\n effective for each Contribution on the date the Contributor first distributes\n such Contribution.\n\n2.3. Limitations on Grant Scope\n\n The licenses granted in this Section 2 are the only rights granted under this\n License. No additional rights or licenses will be implied from the distribution\n or licensing of Covered Software under this License. Notwithstanding Section\n 2.1(b) above, no patent license is granted by a Contributor:\n\n a. for any code that a Contributor has removed from Covered Software; 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 its\n Contributions.\n\n This License does not grant any rights in the trademarks, service marks, or\n logos of any Contributor (except as may be necessary to comply with the\n notice requirements in Section 3.4).\n\n2.4. Subsequent Licenses\n\n No Contributor makes additional grants as a result of Your choice to\n distribute the Covered Software under a subsequent version of this License\n (see Section 10.2) or under the terms of a Secondary License (if permitted\n under the terms of Section 3.3).\n\n2.5. Representation\n\n Each Contributor represents that the Contributor believes its Contributions\n are its original creation(s) or it has sufficient rights to grant the\n rights to its Contributions conveyed by this License.\n\n2.6. Fair Use\n\n This License is not intended to limit any rights You have under applicable\n copyright doctrines of fair use, fair dealing, or other equivalents.\n\n2.7. Conditions\n\n Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in\n Section 2.1.\n\n\n3. Responsibilities\n\n3.1. Distribution of Source Form\n\n All distribution of Covered Software in Source Code Form, including any\n Modifications that You create or to which You contribute, must be under the\n terms of this License. You must inform recipients that the Source Code Form\n of the Covered Software is governed by the terms of this License, and how\n they can obtain a copy of this License. You may not attempt to alter or\n restrict the recipients’ rights in the Source Code Form.\n\n3.2. Distribution of Executable Form\n\n If You distribute Covered Software in Executable Form then:\n\n a. such Covered Software must also be made available in Source Code Form,\n as described in Section 3.1, and You must inform recipients of the\n Executable Form how they can obtain a copy of such Source Code Form by\n reasonable means in a timely manner, at a charge no more than the cost\n of distribution to the recipient; and\n\n b. You may distribute such Executable Form under the terms of this License,\n or sublicense it under different terms, provided that the license for\n the Executable Form does not attempt to limit or alter the recipients’\n rights in the Source Code Form under this License.\n\n3.3. Distribution of a Larger Work\n\n You may create and distribute a Larger Work under terms of Your choice,\n provided that You also comply with the requirements of this License for the\n Covered Software. If the Larger Work is a combination of Covered Software\n with a work governed by one or more Secondary Licenses, and the Covered\n Software is not Incompatible With Secondary Licenses, this License permits\n You to additionally distribute such Covered Software under the terms of\n such Secondary License(s), so that the recipient of the Larger Work may, at\n their option, further distribute the Covered Software under the terms of\n either this License or such Secondary License(s).\n\n3.4. Notices\n\n You may not remove or alter the substance of any license notices (including\n copyright notices, patent notices, disclaimers of warranty, or limitations\n of liability) contained within the Source Code Form of the Covered\n Software, except that You may alter any license notices to the extent\n required to remedy known factual inaccuracies.\n\n3.5. Application of Additional Terms\n\n You may choose to offer, and to charge a fee for, warranty, support,\n indemnity or liability obligations to one or more recipients of Covered\n Software. However, You may do so only on Your own behalf, and not on behalf\n of any Contributor. You must make it absolutely clear that any such\n warranty, support, indemnity, or liability obligation is offered by You\n alone, and You hereby agree to indemnify every Contributor for any\n liability incurred by such Contributor as a result of warranty, support,\n indemnity or liability terms You offer. You may include additional\n disclaimers of warranty and limitations of liability specific to any\n jurisdiction.\n\n4. Inability to Comply Due to Statute or Regulation\n\n If it is impossible for You to comply with any of the terms of this License\n with respect to some or all of the Covered Software due to statute, judicial\n order, or regulation then You must: (a) comply with the terms of this License\n to the maximum extent possible; and (b) describe the limitations and the code\n they affect. Such description must be placed in a text file included with all\n distributions of the Covered Software under this License. Except to the\n extent prohibited by statute or regulation, such description must be\n sufficiently detailed for a recipient of ordinary skill to be able to\n understand it.\n\n5. Termination\n\n5.1. The rights granted under this License will terminate automatically if You\n fail to comply with any of its terms. However, if You become compliant,\n then the rights granted under this License from a particular Contributor\n are reinstated (a) provisionally, unless and until such Contributor\n explicitly and finally terminates Your grants, and (b) on an ongoing basis,\n if such Contributor fails to notify You of the non-compliance by some\n reasonable means prior to 60 days after You have come back into compliance.\n Moreover, Your grants from a particular Contributor are reinstated on an\n ongoing basis if such Contributor notifies You of the non-compliance by\n some reasonable means, this is the first time You have received notice of\n non-compliance with this License from such Contributor, and You become\n compliant prior to 30 days after Your receipt of the notice.\n\n5.2. If You initiate litigation against any entity by asserting a patent\n infringement claim (excluding declaratory judgment actions, counter-claims,\n and cross-claims) alleging that a Contributor Version directly or\n indirectly infringes any patent, then the rights granted to You by any and\n all Contributors for the Covered Software under Section 2.1 of this License\n shall terminate.\n\n5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user\n license agreements (excluding distributors and resellers) which have been\n validly granted by You or Your distributors under this License prior to\n termination shall survive termination.\n\n6. Disclaimer of Warranty\n\n Covered Software is provided under this License on an “as is” basis, without\n warranty of any kind, either expressed, implied, or statutory, including,\n without limitation, warranties that the Covered Software is free of defects,\n merchantable, fit for a particular purpose or non-infringing. The entire\n risk as to the quality and performance of the Covered Software is with You.\n Should any Covered Software prove defective in any respect, You (not any\n Contributor) assume the cost of any necessary servicing, repair, or\n correction. This disclaimer of warranty constitutes an essential part of this\n License. No use of any Covered Software is authorized under this License\n except under this disclaimer.\n\n7. Limitation of Liability\n\n Under no circumstances and under no legal theory, whether tort (including\n negligence), contract, or otherwise, shall any Contributor, or anyone who\n distributes Covered Software as permitted above, be liable to You for any\n direct, indirect, special, incidental, or consequential damages of any\n character including, without limitation, damages for lost profits, loss of\n goodwill, work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses, even if such party shall have been\n informed of the possibility of such damages. This limitation of liability\n shall not apply to liability for death or personal injury resulting from such\n party’s negligence to the extent applicable law prohibits such limitation.\n Some jurisdictions do not allow the exclusion or limitation of incidental or\n consequential damages, so this exclusion and limitation may not apply to You.\n\n8. Litigation\n\n Any litigation relating to this License may be brought only in the courts of\n a jurisdiction where the defendant maintains its principal place of business\n and such litigation shall be governed by laws of that jurisdiction, without\n reference to its conflict-of-law provisions. Nothing in this Section shall\n prevent a party’s ability to bring cross-claims or counter-claims.\n\n9. Miscellaneous\n\n This License represents the complete agreement concerning the subject matter\n hereof. If any provision of this License is held to be unenforceable, such\n provision shall be reformed only to the extent necessary to make it\n enforceable. Any law or regulation which provides that the language of a\n contract shall be construed against the drafter shall not be used to construe\n this License against a Contributor.\n\n\n10. Versions of the License\n\n10.1. New Versions\n\n Mozilla Foundation is the license steward. Except as provided in Section\n 10.3, no one other than the license steward has the right to modify or\n publish new versions of this License. Each version will be given a\n distinguishing version number.\n\n10.2. Effect of New Versions\n\n You may distribute the Covered Software under the terms of the version of\n the License under which You originally received the Covered Software, or\n under the terms of any subsequent version published by the license\n steward.\n\n10.3. Modified Versions\n\n If you create software not governed by this License, and you want to\n create a new license for such software, you may create and use a modified\n version of this License if you rename the license and remove any\n references to the name of the license steward (except to note that such\n modified license differs from this License).\n\n10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses\n If You choose to distribute Source Code Form that is Incompatible With\n Secondary Licenses under the terms of this version of the License, the\n notice described in Exhibit B of this License must be attached.\n\nExhibit A - Source Code Form License Notice\n\n This Source Code Form is subject to the\n terms of the Mozilla Public License, v.\n 2.0. If a copy of the MPL was not\n distributed with this file, You can\n obtain one at\n http://mozilla.org/MPL/2.0/.\n\nIf it is not possible or desirable to put the notice in a particular file, then\nYou may include the notice in a location (such as a LICENSE file in a relevant\ndirectory) where a recipient would be likely to look for such a notice.\n\nYou may add additional accurate notices of copyright ownership.\n\nExhibit B - “Incompatible With Secondary Licenses” Notice\n\n This Source Code Form is “Incompatible\n With Secondary Licenses”, as defined by\n the Mozilla Public License, v. 2.0.\n\n" }, { "name": "github.com/hashicorp/golang-lru/v2", @@ -734,21 +729,31 @@ "path": "github.com/huandu/xstrings/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Huan Du\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/inbucket/html2text", + "path": "github.com/inbucket/html2text/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Jay Taylor\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/jaytaylor/html2text", "path": "github.com/jaytaylor/html2text/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Jay Taylor\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/jbenet/go-context/io", - "path": "github.com/jbenet/go-context/io/LICENSE", + "name": "github.com/jbenet/go-context", + "path": "github.com/jbenet/go-context/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", - "path": "github.com/jhillyerd/enmime/LICENSE", + "name": "github.com/jhillyerd/enmime/v2", + "path": "github.com/jhillyerd/enmime/v2/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2012-2016 James Hillyerd, All Rights Reserved\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/jonboulle/clockwork", + "path": "github.com/jonboulle/clockwork/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/josharian/intern", "path": "github.com/josharian/intern/license.md", @@ -799,6 +804,11 @@ "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", @@ -806,8 +816,8 @@ }, { "name": "github.com/lib/pq", - "path": "github.com/lib/pq/LICENSE.md", - "licenseText": "Copyright (c) 2011-2013, 'pq' Contributors\nPortions Copyright (C) 2011 Blake Mizerany\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" + "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", @@ -820,8 +830,8 @@ "licenseText": "Copyright (c) 2016 Mail.Ru Group\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" }, { - "name": "github.com/markbates/going/defaults", - "path": "github.com/markbates/going/defaults/LICENSE.txt", + "name": "github.com/markbates/going", + "path": "github.com/markbates/going/LICENSE.txt", "licenseText": "Copyright (c) 2014 Mark Bates\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" }, { @@ -875,8 +885,8 @@ "licenseText": "Copyright (c) 2012 The Go Authors. All rights reserved.\nCopyright (c) Microsoft Corporation.\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/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted/pkg", - "path": "github.com/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted/pkg/LICENSE.txt", + "name": "github.com/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted", + "path": "github.com/microsoft/go-mssqldb/internal/github.com/swisscom/mssql-always-encrypted/LICENSE.txt", "licenseText": "Copyright (c) 2021 Swisscom (Switzerland) Ltd\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\n\n" }, { @@ -909,11 +919,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", @@ -924,19 +929,24 @@ "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", "licenseText": "Copyright (c) 2011, Open Knowledge Foundation Ltd.\nAll 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\n notice, this list of conditions and the following disclaimer.\n\n Redistributions in binary form must reproduce the above copyright\n notice, this list of conditions and the following disclaimer in\n the documentation and/or other materials provided with the\n distribution.\n\n Neither the name of the Open Knowledge Foundation Ltd. nor the\n names of its contributors may be used to endorse or promote\n products derived from this software without specific prior written\n 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\nHOLDER 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/nektos/act/pkg", - "path": "github.com/nektos/act/pkg/LICENSE", + "name": "github.com/nektos/act", + "path": "github.com/nektos/act/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2022 The Gitea Authors\nCopyright (c) 2019\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/niklasfasching/go-org/org", - "path": "github.com/niklasfasching/go-org/org/LICENSE", + "name": "github.com/niklasfasching/go-org", + "path": "github.com/niklasfasching/go-org/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2018 Niklas Fasching\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" }, { @@ -980,8 +990,8 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n https://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 Copyright 2019, 2020 OCI Contributors\n Copyright 2016 Docker, Inc.\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 https://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/opencontainers/image-spec/specs-go", - "path": "github.com/opencontainers/image-spec/specs-go/LICENSE", + "name": "github.com/opencontainers/image-spec", + "path": "github.com/opencontainers/image-spec/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 Copyright 2016 The Linux Foundation.\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" }, { @@ -1005,8 +1015,8 @@ "licenseText": "Copyright (c) 2015, Dave Cheney \u003cdave@cheney.net\u003e\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\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/pmezard/go-difflib/difflib", - "path": "github.com/pmezard/go-difflib/difflib/LICENSE", + "name": "github.com/pmezard/go-difflib", + "path": "github.com/pmezard/go-difflib/LICENSE", "licenseText": "Copyright (c) 2013, Patrick Mezard\nAll 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 copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\n The names of its contributors may not be used to endorse or promote\nproducts derived from this software without specific prior written\npermission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS\nIS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED\nTO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A\nPARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nHOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED\nTO, PROCUREMENT 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" }, { @@ -1015,18 +1025,18 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil", - "path": "github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil/LICENSE", - "licenseText": "Copyright (c) 2013 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/prometheus/client_golang/prometheus", - "path": "github.com/prometheus/client_golang/prometheus/LICENSE", + "name": "github.com/prometheus/client_golang", + "path": "github.com/prometheus/client_golang/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/prometheus/client_model/go", - "path": "github.com/prometheus/client_model/go/LICENSE", + "name": "github.com/prometheus/client_golang/internal/github.com/golang/gddo", + "path": "github.com/prometheus/client_golang/internal/github.com/golang/gddo/LICENSE", + "licenseText": "Copyright (c) 2013 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/prometheus/client_model", + "path": "github.com/prometheus/client_model/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" }, { @@ -1049,11 +1059,6 @@ "path": "github.com/rhysd/actionlint/LICENSE.txt", "licenseText": "the MIT License\n\nCopyright (c) 2021 rhysd\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 copies\nof the 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 IMPLIED,\nINCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR\nPURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR\nTHE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n" }, - { - "name": "github.com/rivo/uniseg", - "path": "github.com/rivo/uniseg/LICENSE.txt", - "licenseText": "MIT License\n\nCopyright (c) 2019 Oliver Kuederle\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/robfig/cron/v3", "path": "github.com/robfig/cron/v3/LICENSE", @@ -1070,8 +1075,8 @@ "licenseText": "Blackfriday is distributed under the Simplified BSD License:\n\n\u003e Copyright © 2011 Russ Ross\n\u003e All rights reserved.\n\u003e\n\u003e Redistribution and use in source and binary forms, with or without\n\u003e modification, are permitted provided that the following conditions\n\u003e are met:\n\u003e\n\u003e 1. Redistributions of source code must retain the above copyright\n\u003e notice, this list of conditions and the following disclaimer.\n\u003e\n\u003e 2. Redistributions in binary form must reproduce the above\n\u003e copyright notice, this list of conditions and the following\n\u003e disclaimer in the documentation and/or other materials provided with\n\u003e the distribution.\n\u003e\n\u003e THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\u003e \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n\u003e LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\n\u003e FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n\u003e COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,\n\u003e INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,\n\u003e BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n\u003e LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n\u003e CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n\u003e LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\n\u003e ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n\u003e POSSIBILITY OF SUCH DAMAGE.\n" }, { - "name": "github.com/santhosh-tekuri/jsonschema/v5", - "path": "github.com/santhosh-tekuri/jsonschema/v5/LICENSE", + "name": "github.com/santhosh-tekuri/jsonschema/v6", + "path": "github.com/santhosh-tekuri/jsonschema/v6/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." }, { @@ -1080,10 +1085,15 @@ "licenseText": "\n Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"[]\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright [yyyy] [name of copyright owner]\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n" }, { - "name": "github.com/sergi/go-diff/diffmatchpatch", - "path": "github.com/sergi/go-diff/diffmatchpatch/LICENSE", + "name": "github.com/sergi/go-diff", + "path": "github.com/sergi/go-diff/LICENSE", "licenseText": "Copyright (c) 2012-2016 The go-diff Authors. All rights reserved.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of this software and associated documentation files (the \"Software\"),\nto deal in the Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, 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\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE.\n\n" }, + { + "name": "github.com/shopspring/decimal", + "path": "github.com/shopspring/decimal/LICENSE", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Spring, 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\n- Based on https://github.com/oguzbilgic/fpd, which has the following license:\n\"\"\"\nThe MIT License (MIT)\n\nCopyright (c) 2013 Oguz Bilgic\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\"\"\"\n" + }, { "name": "github.com/sirupsen/logrus", "path": "github.com/sirupsen/logrus/LICENSE", @@ -1115,13 +1125,13 @@ "licenseText": "MIT License\n\nCopyright (c) 2012-2020 Mat Ryer, Tyler Bunnell and 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/syndtr/goleveldb/leveldb", - "path": "github.com/syndtr/goleveldb/leveldb/LICENSE", + "name": "github.com/syndtr/goleveldb", + "path": "github.com/syndtr/goleveldb/LICENSE", "licenseText": "Copyright 2012 Suryandaru Triandana \u003csyndtr@gmail.com\u003e\nAll 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 copyright\nnotice, this list of conditions and the following disclaimer in the\ndocumentation and/or other materials provided with the distribution.\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\nHOLDER 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/tinylib/msgp/msgp", - "path": "github.com/tinylib/msgp/msgp/LICENSE", + "name": "github.com/tinylib/msgp", + "path": "github.com/tinylib/msgp/LICENSE", "licenseText": "Copyright (c) 2014 Philip Hofer\nPortions Copyright (c) 2009 The Go Authors (license at http://golang.org) where indicated\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." }, { @@ -1149,11 +1159,6 @@ "path": "github.com/urfave/cli/v3/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2023 urfave/cli maintainers\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n" }, - { - "name": "github.com/valyala/fastjson", - "path": "github.com/valyala/fastjson/LICENSE", - "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2018 Aliaksandr Valialkin\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/wneessen/go-mail", "path": "github.com/wneessen/go-mail/LICENSE", @@ -1174,6 +1179,11 @@ "path": "github.com/xanzy/ssh-agent/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n" }, + { + "name": "github.com/xi2/xz", + "path": "github.com/xi2/xz/LICENSE", + "licenseText": "Licensing of github.com/xi2/xz\n==============================\n\n This Go package is a modified version of\n\n XZ Embedded \u003chttp://tukaani.org/xz/embedded.html\u003e\n\n The contents of the testdata directory are modified versions of\n the test files from\n\n XZ Utils \u003chttp://tukaani.org/xz/\u003e\n\n All the files in this package have been written by Michael Cross,\n Lasse Collin and/or Igor PavLov. All these files have been put\n into the public domain. You can do whatever you want with these\n files.\n\n This software is provided \"as is\", without any warranty.\n" + }, { "name": "github.com/yohcop/openid-go", "path": "github.com/yohcop/openid-go/LICENSE", @@ -1184,11 +1194,6 @@ "path": "github.com/yuin/goldmark-highlighting/v2/LICENSE", "licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\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/yuin/goldmark-meta", - "path": "github.com/yuin/goldmark-meta/LICENSE", - "licenseText": "MIT License\n\nCopyright (c) 2019 Yusuke Inuzuka\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/yuin/goldmark", "path": "github.com/yuin/goldmark/LICENSE", @@ -1222,16 +1227,31 @@ { "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" - }, - { - "name": "go.uber.org/zap/exp/zapslog", - "path": "go.uber.org/zap/exp/zapslog/LICENSE", "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": "go4.org/readerutil", - "path": "go4.org/readerutil/LICENSE", + "name": "go.uber.org/zap/exp", + "path": "go.uber.org/zap/exp/LICENSE", + "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.yaml.in/yaml/v2", + "path": "go.yaml.in/yaml/v2/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": "go.yaml.in/yaml/v3", + "path": "go.yaml.in/yaml/v3/LICENSE", + "licenseText": "\nThis project is covered by two different licenses: MIT and Apache.\n\n#### MIT License ####\n\nThe following files were ported to Go from C files of libyaml, and thus\nare still covered by their original MIT license, with the additional\ncopyright staring in 2011 when the project was ported over:\n\n apic.go emitterc.go parserc.go readerc.go scannerc.go\n writerc.go yamlh.go yamlprivateh.go\n\nCopyright (c) 2006-2010 Kirill Simonov\nCopyright (c) 2006-2011 Kirill Simonov\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\nof the Software, and to permit persons to whom the Software is furnished to do\nso, 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### Apache License ###\n\nAll the remaining project files are covered by the Apache license:\n\nCopyright (c) 2011-2019 Canonical Ltd\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": "go.yaml.in/yaml/v4", + "path": "go.yaml.in/yaml/v4/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 2025 - The go-yaml Project Contributors\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": "go4.org", + "path": "go4.org/LICENSE", "licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute copies of the\n Work or Derivative Works thereof in any medium, with or without\n modifications, and in Source or Object form, provided that You\n meet the following conditions:\n\n (a) You must give any other recipients of the Work or\n Derivative Works a copy of this License; and\n\n (b) You must cause any modified files to carry prominent notices\n stating that You changed the files; and\n\n (c) You must retain, in the Source form of any Derivative Works\n that You distribute, all copyright, patent, trademark, and\n attribution notices from the Source form of the Work,\n excluding those notices that do not pertain to any part of\n the Derivative Works; and\n\n (d) If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must\n include a readable copy of the attribution notices contained\n within such NOTICE file, excluding those notices that do not\n pertain to any part of the Derivative Works, in at least one\n of the following places: within a NOTICE text file distributed\n as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or,\n within a display generated by the Derivative Works, if and\n wherever such third-party notices normally appear. The contents\n of the NOTICE file are for informational purposes only and\n do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside\n or as an addendum to the NOTICE text from the Work, provided\n that such additional attribution notices cannot be construed\n as modifying the License.\n\n You may add Your own copyright statement to Your modifications and\n may provide additional or different license terms and conditions\n for use, reproduction, or distribution of Your modifications, or\n for any such Derivative Works as a whole, provided Your use,\n reproduction, and distribution of the Work otherwise complies with\n the conditions stated in this License.\n\n 5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work\n by You to the Licensor shall be under the terms and conditions of\n this License, without any additional terms or conditions.\n Notwithstanding the above, nothing herein shall supersede or modify\n the terms of any separate license agreement you may have executed\n with Licensor regarding such Contributions.\n\n 6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor,\n except as required for reasonable and customary use in describing the\n origin of the Work and reproducing the content of the NOTICE file.\n\n 7. Disclaimer of Warranty. Unless required by applicable law or\n agreed to in writing, Licensor provides the Work (and each\n Contributor provides its Contributions) on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n implied, including, without limitation, any warranties or conditions\n of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any\n risks associated with Your exercise of permissions under this License.\n\n 8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise,\n unless required by applicable law (such as deliberate and grossly\n negligent acts) or agreed to in writing, shall any Contributor be\n liable to You for damages, including any direct, indirect, special,\n incidental, or consequential damages of any character arising as a\n result of this License or out of the use or inability to use the\n Work (including but not limited to damages for loss of goodwill,\n work stoppage, computer failure or malfunction, or any and all\n other commercial damages or losses), even if such Contributor\n has been advised of the possibility of such damages.\n\n 9. Accepting Warranty or Additional Liability. While redistributing\n the Work or Derivative Works thereof, You may choose to offer,\n and charge a fee for, acceptance of support, warranty, indemnity,\n or other liability obligations and/or rights consistent with this\n License. However, in accepting such obligations, You may act only\n on Your own behalf and on Your sole responsibility, not on behalf\n of any other Contributor, and only if You agree to indemnify,\n defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason\n of your accepting any such warranty or additional liability.\n\n END OF TERMS AND CONDITIONS\n\n APPENDIX: How to apply the Apache License to your work.\n\n To apply the Apache License to your work, attach the following\n boilerplate notice, with the fields enclosed by brackets \"{}\"\n replaced with your own identifying information. (Don't include\n the brackets!) The text should be enclosed in the appropriate\n comment syntax for the file format. We also recommend that a\n file or class name and description of purpose be included on the\n same \"printed page\" as the copyright notice for easier\n identification within third-party archives.\n\n Copyright {yyyy} {name of copyright owner}\n\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this file except in compliance with the License.\n You may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\n Unless required by applicable law or agreed to in writing, software\n distributed under the License is distributed on an \"AS IS\" BASIS,\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n See the License for the specific language governing permissions and\n limitations under the License.\n\n" }, { @@ -1245,8 +1265,8 @@ "licenseText": "Copyright 2009 The Go Authors.\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 LLC 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": "golang.org/x/mod/semver", - "path": "golang.org/x/mod/semver/LICENSE", + "name": "golang.org/x/mod", + "path": "golang.org/x/mod/LICENSE", "licenseText": "Copyright 2009 The Go Authors.\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 LLC 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" }, { @@ -1275,13 +1295,13 @@ "licenseText": "Copyright 2009 The Go Authors.\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 LLC 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": "golang.org/x/time/rate", - "path": "golang.org/x/time/rate/LICENSE", + "name": "golang.org/x/time", + "path": "golang.org/x/time/LICENSE", "licenseText": "Copyright 2009 The Go Authors.\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 LLC 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": "google.golang.org/genproto/googleapis/rpc/status", - "path": "google.golang.org/genproto/googleapis/rpc/status/LICENSE", + "name": "google.golang.org/genproto/googleapis/rpc", + "path": "google.golang.org/genproto/googleapis/rpc/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.\n" }, { @@ -1304,11 +1324,6 @@ "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", - "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": "gopkg.in/yaml.v3", "path": "gopkg.in/yaml.v3/LICENSE", diff --git a/package/gitea/source/tools/misspellings.csv b/package/gitea/source/assets/misspellings.csv similarity index 100% rename from package/gitea/source/tools/misspellings.csv rename to package/gitea/source/assets/misspellings.csv diff --git a/package/gitea/source/build.go b/package/gitea/source/build.go deleted file mode 100644 index e81ba546..00000000 --- a/package/gitea/source/build.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build vendor - -package main - -// Libraries that are included to vendor utilities used during Makefile build. -// These libraries will not be included in a normal compilation. - -import ( - // for vet - _ "code.gitea.io/gitea-vet" -) diff --git a/package/gitea/source/build/backport-locales.go b/package/gitea/source/build/backport-locales.go deleted file mode 100644 index d112dd72..00000000 --- a/package/gitea/source/build/backport-locales.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build ignore - -package main - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/setting" -) - -func main() { - if len(os.Args) != 2 { - println("usage: backport-locales ") - println("eg: backport-locales release/v1.19") - os.Exit(1) - } - - mustNoErr := func(err error) { - if err != nil { - panic(err) - } - } - collectInis := func(ref string) map[string]setting.ConfigProvider { - inis := map[string]setting.ConfigProvider{} - err := filepath.WalkDir("options/locale", func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() || !strings.HasSuffix(d.Name(), ".ini") { - return nil - } - cfg, err := setting.NewConfigProviderForLocale(path) - mustNoErr(err) - inis[path] = cfg - fmt.Printf("collecting: %s @ %s\n", path, ref) - return nil - }) - mustNoErr(err) - return inis - } - - // collect new locales from current working directory - inisNew := collectInis("HEAD") - - // switch to the target ref, and collect the old locales - cmd := exec.Command("git", "checkout", os.Args[1]) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - mustNoErr(cmd.Run()) - inisOld := collectInis(os.Args[1]) - - // use old en-US as the base, and copy the new translations to the old locales - enUsOld := inisOld["options/locale/locale_en-US.ini"] - brokenWarned := make(container.Set[string]) - for path, iniOld := range inisOld { - if iniOld == enUsOld { - continue - } - iniNew := inisNew[path] - if iniNew == nil { - continue - } - for _, secEnUS := range enUsOld.Sections() { - secOld := iniOld.Section(secEnUS.Name()) - secNew := iniNew.Section(secEnUS.Name()) - for _, keyEnUs := range secEnUS.Keys() { - if secNew.HasKey(keyEnUs.Name()) { - oldStr := secOld.Key(keyEnUs.Name()).String() - newStr := secNew.Key(keyEnUs.Name()).String() - broken := oldStr != "" && strings.Count(oldStr, "%") != strings.Count(newStr, "%") - broken = broken || strings.Contains(oldStr, "\n") || strings.Contains(oldStr, "\n") - if broken { - brokenWarned.Add(secOld.Name() + "." + keyEnUs.Name()) - fmt.Println("----") - fmt.Printf("WARNING: skip broken locale: %s , [%s] %s\n", path, secEnUS.Name(), keyEnUs.Name()) - fmt.Printf("\told: %s\n", strings.ReplaceAll(oldStr, "\n", "\\n")) - fmt.Printf("\tnew: %s\n", strings.ReplaceAll(newStr, "\n", "\\n")) - continue - } - secOld.Key(keyEnUs.Name()).SetValue(newStr) - } - } - } - mustNoErr(iniOld.SaveTo(path)) - } - - fmt.Println("========") - - for path, iniNew := range inisNew { - for _, sec := range iniNew.Sections() { - for _, key := range sec.Keys() { - str := sec.Key(key.Name()).String() - broken := strings.Contains(str, "\n") - broken = broken || strings.HasPrefix(str, "`") != strings.HasSuffix(str, "`") - broken = broken || strings.HasPrefix(str, "\"`") - broken = broken || strings.HasPrefix(str, "`\"") - broken = broken || strings.Count(str, `"`)%2 == 1 - broken = broken || strings.Count(str, "`")%2 == 1 - if broken && !brokenWarned.Contains(sec.Name()+"."+key.Name()) { - fmt.Printf("WARNING: found broken locale: %s , [%s] %s\n", path, sec.Name(), key.Name()) - fmt.Printf("\tstr: %s\n", strings.ReplaceAll(str, "\n", "\\n")) - fmt.Println("----") - } - } - } - } -} diff --git a/package/gitea/source/build/generate-emoji.go b/package/gitea/source/build/generate-emoji.go index 446ab5f4..cbc1be21 100644 --- a/package/gitea/source/build/generate-emoji.go +++ b/package/gitea/source/build/generate-emoji.go @@ -24,8 +24,8 @@ import ( ) const ( - gemojiURL = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json" - maxUnicodeVersion = 15 + gemojiURL = "https://raw.githubusercontent.com/rhysd/gemoji/537ff2d7e0496e9964824f7f73ec7ece88c9765a/db/emoji.json" + maxUnicodeVersion = 16 ) var flagOut = flag.String("o", "modules/emoji/emoji_data.go", "out") @@ -149,8 +149,8 @@ func generate() ([]byte, error) { } // write a JSON file to use with tribute (write before adding skin tones since we can't support them there yet) - file, _ := json.Marshal(data) - _ = os.WriteFile("assets/emoji.json", file, 0o644) + file, _ := json.MarshalIndent(data, "", " ") + _ = os.WriteFile("assets/emoji.json", append(file, '\n'), 0o644) // Add skin tones to emoji that support it var ( diff --git a/package/gitea/source/build/generate-go-licenses.go b/package/gitea/source/build/generate-go-licenses.go index 84ba3902..057e6a6e 100644 --- a/package/gitea/source/build/generate-go-licenses.go +++ b/package/gitea/source/build/generate-go-licenses.go @@ -8,99 +8,220 @@ package main import ( "encoding/json" "fmt" - "io/fs" "os" - "path" + "os/exec" "path/filepath" "regexp" + "slices" "sort" "strings" - - "code.gitea.io/gitea/modules/container" ) // regexp is based on go-license, excluding README and NOTICE // https://github.com/google/go-licenses/blob/master/licenses/find.go +// also defined in vite.config.ts var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`) +// primaryLicenseRe matches exact primary license filenames without suffixes. +// When a directory has both primary and variant files (e.g. LICENSE and +// LICENSE.docs), only the primary files are kept. +var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`) + +// ignoredNames are LicenseEntry.Name values to exclude from the output. +var ignoredNames = map[string]bool{ + "code.gitea.io/gitea": true, + "code.gitea.io/gitea/options/license": true, +} + +var excludedExt = map[string]bool{ + ".gitignore": true, + ".go": true, + ".mod": true, + ".sum": true, + ".toml": true, + ".yaml": true, + ".yml": true, +} + +type ModuleInfo struct { + Path string + Dir string + PkgDirs []string // directories of packages imported from this module +} + type LicenseEntry struct { Name string `json:"name"` Path string `json:"path"` LicenseText string `json:"licenseText"` } -func main() { - if len(os.Args) != 3 { - fmt.Println("usage: go run generate-go-licenses.go ") +// getModules returns all dependency modules with their local directory paths +// and the package directories used from each module. +func getModules(goCmd string) []ModuleInfo { + cmd := exec.Command(goCmd, "list", "-deps", "-f", + "{{if .Module}}{{.Module.Path}}\t{{.Module.Dir}}\t{{.Dir}}{{end}}", "./...") + cmd.Stderr = os.Stderr + // Use GOOS=linux with CGO to ensure we capture all platform-specific + // dependencies, matching the CI environment. + cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=1") + output, err := cmd.Output() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to run 'go list -deps': %v\n", err) os.Exit(1) } - base, out := os.Args[1], os.Args[2] - - // Add ext for excluded files because license_test.go will be included for some reason. - // And there are more files that should be excluded, check with: - // - // go run github.com/google/go-licenses@v1.6.0 save . --force --save_path=.go-licenses 2>/dev/null - // find .go-licenses -type f | while read FILE; do echo "${$(basename $FILE)##*.}"; done | sort -u - // AUTHORS - // COPYING - // LICENSE - // Makefile - // NOTICE - // gitignore - // go - // md - // mod - // sum - // toml - // txt - // yml - // - // It could be removed once we have a better regex. - excludedExt := container.SetOf(".gitignore", ".go", ".mod", ".sum", ".toml", ".yml") - - var paths []string - err := filepath.WalkDir(base, func(path string, entry fs.DirEntry, err error) error { - if err != nil { - return err + var modules []ModuleInfo + seen := make(map[string]int) // module path -> index in modules + for _, line := range strings.Split(string(output), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue } - if entry.IsDir() || !licenseRe.MatchString(entry.Name()) || excludedExt.Contains(filepath.Ext(entry.Name())) { - return nil + parts := strings.Split(line, "\t") + if len(parts) != 3 { + continue } - paths = append(paths, path) - return nil - }) - if err != nil { - panic(err) + modPath, modDir, pkgDir := parts[0], parts[1], parts[2] + if idx, ok := seen[modPath]; ok { + modules[idx].PkgDirs = append(modules[idx].PkgDirs, pkgDir) + } else { + seen[modPath] = len(modules) + modules = append(modules, ModuleInfo{ + Path: modPath, + Dir: modDir, + PkgDirs: []string{pkgDir}, + }) + } + } + return modules +} + +// findLicenseFiles scans a module's root directory and its used package +// directories for license files. It also walks up from each package directory +// to the module root, scanning intermediate directories. Subdirectory licenses +// are only included if their text differs from the root license(s). +func findLicenseFiles(mod ModuleInfo) []LicenseEntry { + var entries []LicenseEntry + seenTexts := make(map[string]bool) + + // First, collect root-level license files. + entries = append(entries, scanDirForLicenses(mod.Dir, mod.Path, "")...) + for _, e := range entries { + seenTexts[e.LicenseText] = true } - sort.Strings(paths) + // Then check each package directory and all intermediate parent directories + // up to the module root for license files with unique text. + seenDirs := map[string]bool{mod.Dir: true} + for _, pkgDir := range mod.PkgDirs { + for dir := pkgDir; dir != mod.Dir && strings.HasPrefix(dir, mod.Dir); dir = filepath.Dir(dir) { + if seenDirs[dir] { + continue + } + seenDirs[dir] = true + for _, e := range scanDirForLicenses(dir, mod.Path, mod.Dir) { + if !seenTexts[e.LicenseText] { + seenTexts[e.LicenseText] = true + entries = append(entries, e) + } + } + } + } + return entries +} + +// scanDirForLicenses reads a single directory for license files and returns entries. +// If moduleRoot is non-empty, paths are made relative to it. +func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry { + dirEntries, err := os.ReadDir(dir) + if err != nil { + return nil + } var entries []LicenseEntry - for _, filePath := range paths { - licenseText, err := os.ReadFile(filePath) - if err != nil { - panic(err) + for _, entry := range dirEntries { + if entry.IsDir() { + continue } - - pkgPath := filepath.ToSlash(filePath) - pkgPath = strings.TrimPrefix(pkgPath, base+"/") - pkgName := path.Dir(pkgPath) - - // There might be a bug somewhere in go-licenses that sometimes interprets the - // root package as "." and sometimes as "code.gitea.io/gitea". Workaround by - // removing both of them for the sake of stable output. - if pkgName == "." || pkgName == "code.gitea.io/gitea" { + name := entry.Name() + if !licenseRe.MatchString(name) { + continue + } + if excludedExt[strings.ToLower(filepath.Ext(name))] { continue } + content, err := os.ReadFile(filepath.Join(dir, name)) + if err != nil { + continue + } + + entryName := modulePath + entryPath := modulePath + "/" + name + if moduleRoot != "" { + rel, _ := filepath.Rel(moduleRoot, dir) + if rel != "." { + relSlash := filepath.ToSlash(rel) + entryName = modulePath + "/" + relSlash + entryPath = modulePath + "/" + relSlash + "/" + name + } + } + entries = append(entries, LicenseEntry{ - Name: pkgName, - Path: pkgPath, - LicenseText: string(licenseText), + Name: entryName, + Path: entryPath, + LicenseText: string(content), }) } + // When multiple license files exist, prefer primary files (e.g. LICENSE) + // over variants with suffixes (e.g. LICENSE.docs, LICENSE-2.0.txt). + // If no primary file exists, keep only the first variant. + if len(entries) > 1 { + var primary []LicenseEntry + for _, e := range entries { + fileName := e.Path[strings.LastIndex(e.Path, "/")+1:] + if primaryLicenseRe.MatchString(fileName) { + primary = append(primary, e) + } + } + if len(primary) > 0 { + return primary + } + return entries[:1] + } + + return entries +} + +func main() { + if len(os.Args) != 2 { + fmt.Println("usage: go run generate-go-licenses.go ") + os.Exit(1) + } + + out := os.Args[1] + + goCmd := "go" + if env := os.Getenv("GO"); env != "" { + goCmd = env + } + + modules := getModules(goCmd) + + var entries []LicenseEntry + for _, mod := range modules { + entries = append(entries, findLicenseFiles(mod)...) + } + + entries = slices.DeleteFunc(entries, func(e LicenseEntry) bool { + return ignoredNames[e.Name] + }) + + sort.Slice(entries, func(i, j int) bool { + return entries[i].Path < entries[j].Path + }) + jsonBytes, err := json.MarshalIndent(entries, "", " ") if err != nil { panic(err) diff --git a/package/gitea/source/build/update-locales.sh b/package/gitea/source/build/update-locales.sh index 6f9ee334..5316746f 100755 --- a/package/gitea/source/build/update-locales.sh +++ b/package/gitea/source/build/update-locales.sh @@ -1,52 +1,22 @@ #!/bin/sh # this script runs in alpine image which only has `sh` shell - -set +e -if sed --version 2>/dev/null | grep -q GNU; then - SED_INPLACE="sed -i" -else - SED_INPLACE="sed -i ''" -fi -set -e - -if [ ! -f ./options/locale/locale_en-US.ini ]; then +if [ ! -f ./options/locale/locale_en-US.json ]; then echo "please run this script in the root directory of the project" exit 1 fi -mv ./options/locale/locale_en-US.ini ./options/ - -# the "ini" library for locale has many quirks, its behavior is different from Crowdin. -# see i18n_test.go for more details - -# this script helps to unquote the Crowdin outputs for the quirky ini library -# * find all `key="...\"..."` lines -# * remove the leading quote -# * remove the trailing quote -# * unescape the quotes -# * eg: key="...\"..." => key=..."... -$SED_INPLACE -r -e '/^[-.A-Za-z0-9_]+[ ]*=[ ]*".*"$/ { - s/^([-.A-Za-z0-9_]+)[ ]*=[ ]*"/\1=/ - s/"$// - s/\\"/"/g - }' ./options/locale/*.ini - -# * if the escaped line is incomplete like `key="...` or `key=..."`, quote it with backticks -# * eg: key="... => key=`"...` -# * eg: key=..." => key=`..."` -$SED_INPLACE -r -e 's/^([-.A-Za-z0-9_]+)[ ]*=[ ]*(".*[^"])$/\1=`\2`/' ./options/locale/*.ini -$SED_INPLACE -r -e 's/^([-.A-Za-z0-9_]+)[ ]*=[ ]*([^"].*")$/\1=`\2`/' ./options/locale/*.ini +mv ./options/locale/locale_en-US.json ./options/ # Remove translation under 25% of en_us -baselines=$(wc -l "./options/locale_en-US.ini" | cut -d" " -f1) +baselines=$(cat "./options/locale_en-US.json" | wc -l) baselines=$((baselines / 4)) -for filename in ./options/locale/*.ini; do - lines=$(wc -l "$filename" | cut -d" " -f1) - if [ $lines -lt $baselines ]; then +for filename in ./options/locale/*.json; do + lines=$(cat "$filename" | wc -l) + if [ "$lines" -lt "$baselines" ]; then echo "Removing $filename: $lines/$baselines" rm "$filename" fi done -mv ./options/locale_en-US.ini ./options/locale/ +mv ./options/locale_en-US.json ./options/locale/ diff --git a/package/gitea/source/cmd/actions.go b/package/gitea/source/cmd/actions.go index 2c51c6a1..44b6b7b5 100644 --- a/package/gitea/source/cmd/actions.go +++ b/package/gitea/source/cmd/actions.go @@ -13,17 +13,18 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdActions represents the available actions sub-commands. - CmdActions = &cli.Command{ +func newActionsCommand() *cli.Command { + return &cli.Command{ Name: "actions", Usage: "Manage Gitea Actions", Commands: []*cli.Command{ - subcmdActionsGenRunnerToken, + newActionsGenerateRunnerTokenCommand(), }, } +} - subcmdActionsGenRunnerToken = &cli.Command{ +func newActionsGenerateRunnerTokenCommand() *cli.Command { + return &cli.Command{ Name: "generate-runner-token", Usage: "Generate a new token for a runner to use to register with the server", Action: runGenerateActionsRunnerToken, @@ -37,7 +38,7 @@ var ( }, }, } -) +} func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error { setting.MustInstalled() diff --git a/package/gitea/source/cmd/admin.go b/package/gitea/source/cmd/admin.go index 5c58a40c..c0e21731 100644 --- a/package/gitea/source/cmd/admin.go +++ b/package/gitea/source/cmd/admin.go @@ -18,36 +18,41 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdAdmin represents the available admin sub-command. - CmdAdmin = &cli.Command{ +func newAdminCommand() *cli.Command { + return &cli.Command{ Name: "admin", Usage: "Perform common administrative operations", Commands: []*cli.Command{ - subcmdUser, - subcmdRepoSyncReleases, - subcmdRegenerate, - subcmdAuth, - subcmdSendMail, + newUserCommand(), + newRepoSyncReleasesCommand(), + newRegenerateCommand(), + newAuthCommand(), + newSendMailCommand(), }, } +} - subcmdRepoSyncReleases = &cli.Command{ +func newRepoSyncReleasesCommand() *cli.Command { + return &cli.Command{ Name: "repo-sync-releases", Usage: "Synchronize repository releases with tags", Action: runRepoSyncReleases, } +} - subcmdRegenerate = &cli.Command{ +func newRegenerateCommand() *cli.Command { + return &cli.Command{ Name: "regenerate", Usage: "Regenerate specific files", Commands: []*cli.Command{ - microcmdRegenHooks, - microcmdRegenKeys, + newRegenerateHooksCommand(), + newRegenerateKeysCommand(), }, } +} - subcmdAuth = &cli.Command{ +func newAuthCommand() *cli.Command { + return &cli.Command{ Name: "auth", Usage: "Modify external auth providers", Commands: []*cli.Command{ @@ -59,12 +64,14 @@ var ( microcmdAuthUpdateLdapSimpleAuth(), microcmdAuthAddSMTP(), microcmdAuthUpdateSMTP(), - microcmdAuthList, - microcmdAuthDelete, + newAuthListCommand(), + newAuthDeleteCommand(), }, } +} - subcmdSendMail = &cli.Command{ +func newSendMailCommand() *cli.Command { + return &cli.Command{ Name: "sendmail", Usage: "Send a message to all users", Action: runSendMail, @@ -86,7 +93,7 @@ var ( }, }, } -) +} func idFlag() *cli.Int64Flag { return &cli.Int64Flag{ @@ -121,7 +128,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error { } log.Trace("Processing next %d repos of %d", len(repos), count) for _, repo := range repos { - log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RepoPath()) + log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RelativePath()) gitRepo, err := gitrepo.OpenRepository(ctx, repo) if err != nil { log.Warn("OpenRepository: %v", err) @@ -134,7 +141,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error { } log.Trace(" currentNumReleases is %d, running SyncReleasesWithTags", oldnum) - if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { + if _, err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil { log.Warn(" SyncReleasesWithTags: %v", err) gitRepo.Close() continue @@ -147,7 +154,7 @@ func runRepoSyncReleases(ctx context.Context, _ *cli.Command) error { continue } - log.Trace(" repo %s releases synchronized to tags: from %d to %d", + log.Trace("repo %s releases synchronized to tags: from %d to %d", repo.FullName(), oldnum, count) gitRepo.Close() } diff --git a/package/gitea/source/cmd/admin_auth.go b/package/gitea/source/cmd/admin_auth.go index 1a093667..b55bb148 100644 --- a/package/gitea/source/cmd/admin_auth.go +++ b/package/gitea/source/cmd/admin_auth.go @@ -17,14 +17,17 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdAuthDelete = &cli.Command{ +func newAuthDeleteCommand() *cli.Command { + return &cli.Command{ Name: "delete", Usage: "Delete specific auth source", Flags: []cli.Flag{idFlag()}, Action: runDeleteAuth, } - microcmdAuthList = &cli.Command{ +} + +func newAuthListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List auth sources", Action: runListAuth, @@ -55,7 +58,7 @@ var ( }, }, } -) +} func runListAuth(ctx context.Context, c *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/package/gitea/source/cmd/admin_auth_ldap.go b/package/gitea/source/cmd/admin_auth_ldap.go index 069ad660..c9be5abb 100644 --- a/package/gitea/source/cmd/admin_auth_ldap.go +++ b/package/gitea/source/cmd/admin_auth_ldap.go @@ -94,6 +94,10 @@ func commonLdapCLIFlags() []cli.Flag { Name: "public-ssh-key-attribute", Usage: "The attribute of the user’s LDAP record containing the user’s public ssh key.", }, + &cli.BoolFlag{ + Name: "ssh-keys-are-verified", + Usage: "Set to true to automatically flag SSH keys in LDAP as verified.", + }, &cli.BoolFlag{ Name: "skip-local-2fa", Usage: "Set to true to skip local 2fa for users authenticated by this source", @@ -294,6 +298,9 @@ func parseLdapConfig(c *cli.Command, config *ldap.Source) error { if c.IsSet("public-ssh-key-attribute") { config.AttributeSSHPublicKey = c.String("public-ssh-key-attribute") } + if c.IsSet("ssh-keys-are-verified") { + config.SSHKeysAreVerified = c.Bool("ssh-keys-are-verified") + } if c.IsSet("avatar-attribute") { config.AttributeAvatar = c.String("avatar-attribute") } diff --git a/package/gitea/source/cmd/admin_auth_ldap_test.go b/package/gitea/source/cmd/admin_auth_ldap_test.go index 2da7ebc5..0e3e465e 100644 --- a/package/gitea/source/cmd/admin_auth_ldap_test.go +++ b/package/gitea/source/cmd/admin_auth_ldap_test.go @@ -233,7 +233,7 @@ func TestAddLdapBindDn(t *testing.T) { }, getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) { assert.FailNow(t, "getAuthSourceByID called", "case %d: should not call getAuthSourceByID", n) - return nil, nil + return nil, nil //nolint:nilnil // mock function covering improper behavior }, } @@ -463,7 +463,7 @@ func TestAddLdapSimpleAuth(t *testing.T) { }, getAuthSourceByID: func(ctx context.Context, id int64) (*auth.Source, error) { assert.FailNow(t, "getAuthSourceById called", "case %d: should not call getAuthSourceByID", n) - return nil, nil + return nil, nil //nolint:nilnil // mock function covering improper behavior }, } diff --git a/package/gitea/source/cmd/admin_regenerate.go b/package/gitea/source/cmd/admin_regenerate.go index a5f1bd51..aa235441 100644 --- a/package/gitea/source/cmd/admin_regenerate.go +++ b/package/gitea/source/cmd/admin_regenerate.go @@ -13,19 +13,21 @@ import ( "github.com/urfave/cli/v3" ) -var ( - microcmdRegenHooks = &cli.Command{ +func newRegenerateHooksCommand() *cli.Command { + return &cli.Command{ Name: "hooks", Usage: "Regenerate git-hooks", Action: runRegenerateHooks, } +} - microcmdRegenKeys = &cli.Command{ +func newRegenerateKeysCommand() *cli.Command { + return &cli.Command{ Name: "keys", Usage: "Regenerate authorized_keys file", Action: runRegenerateKeys, } -) +} func runRegenerateHooks(ctx context.Context, _ *cli.Command) error { if err := initDB(ctx); err != nil { diff --git a/package/gitea/source/cmd/admin_user.go b/package/gitea/source/cmd/admin_user.go index 3a24c3e5..8dd8bb4e 100644 --- a/package/gitea/source/cmd/admin_user.go +++ b/package/gitea/source/cmd/admin_user.go @@ -7,15 +7,17 @@ import ( "github.com/urfave/cli/v3" ) -var subcmdUser = &cli.Command{ - Name: "user", - Usage: "Modify users", - Commands: []*cli.Command{ - microcmdUserCreate(), - microcmdUserList, - microcmdUserChangePassword(), - microcmdUserDelete(), - microcmdUserGenerateAccessToken, - microcmdUserMustChangePassword(), - }, +func newUserCommand() *cli.Command { + return &cli.Command{ + Name: "user", + Usage: "Modify users", + Commands: []*cli.Command{ + microcmdUserCreate(), + newUserListCommand(), + microcmdUserChangePassword(), + microcmdUserDelete(), + newUserGenerateAccessTokenCommand(), + microcmdUserMustChangePassword(), + }, + } } diff --git a/package/gitea/source/cmd/admin_user_create.go b/package/gitea/source/cmd/admin_user_create.go index cbdb5f90..7e5675cf 100644 --- a/package/gitea/source/cmd/admin_user_create.go +++ b/package/gitea/source/cmd/admin_user_create.go @@ -151,6 +151,7 @@ func runCreateUser(ctx context.Context, c *cli.Command) error { if err != nil { return err } + // codeql[disable-next-line=go/clear-text-logging] fmt.Printf("generated random password is '%s'\n", password) } else if userType == user_model.UserTypeIndividual { return errors.New("must set either password or random-password flag") diff --git a/package/gitea/source/cmd/admin_user_generate_access_token.go b/package/gitea/source/cmd/admin_user_generate_access_token.go index 61064fde..7f8330ff 100644 --- a/package/gitea/source/cmd/admin_user_generate_access_token.go +++ b/package/gitea/source/cmd/admin_user_generate_access_token.go @@ -14,32 +14,34 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserGenerateAccessToken = &cli.Command{ - Name: "generate-access-token", - Usage: "Generate an access token for a specific user", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Usage: "Username", +func newUserGenerateAccessTokenCommand() *cli.Command { + return &cli.Command{ + Name: "generate-access-token", + Usage: "Generate an access token for a specific user", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Usage: "Username", + }, + &cli.StringFlag{ + Name: "token-name", + Aliases: []string{"t"}, + Usage: "Token name", + Value: "gitea-admin", + }, + &cli.BoolFlag{ + Name: "raw", + Usage: "Display only the token value", + }, + &cli.StringFlag{ + Name: "scopes", + Value: "all", + Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, + }, }, - &cli.StringFlag{ - Name: "token-name", - Aliases: []string{"t"}, - Usage: "Token name", - Value: "gitea-admin", - }, - &cli.BoolFlag{ - Name: "raw", - Usage: "Display only the token value", - }, - &cli.StringFlag{ - Name: "scopes", - Value: "all", - Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`, - }, - }, - Action: runGenerateAccessToken, + Action: runGenerateAccessToken, + } } func runGenerateAccessToken(ctx context.Context, c *cli.Command) error { diff --git a/package/gitea/source/cmd/admin_user_list.go b/package/gitea/source/cmd/admin_user_list.go index e3d345e2..2958fe0c 100644 --- a/package/gitea/source/cmd/admin_user_list.go +++ b/package/gitea/source/cmd/admin_user_list.go @@ -14,16 +14,18 @@ import ( "github.com/urfave/cli/v3" ) -var microcmdUserList = &cli.Command{ - Name: "list", - Usage: "List users", - Action: runListUsers, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "admin", - Usage: "List only admin users", +func newUserListCommand() *cli.Command { + return &cli.Command{ + Name: "list", + Usage: "List users", + Action: runListUsers, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "admin", + Usage: "List only admin users", + }, }, - }, + } } func runListUsers(ctx context.Context, c *cli.Command) error { diff --git a/package/gitea/source/cmd/admin_user_must_change_password.go b/package/gitea/source/cmd/admin_user_must_change_password.go index 8521853d..468d462b 100644 --- a/package/gitea/source/cmd/admin_user_must_change_password.go +++ b/package/gitea/source/cmd/admin_user_must_change_password.go @@ -58,6 +58,7 @@ func runMustChangePassword(ctx context.Context, c *cli.Command) error { return err } + // codeql[disable-next-line=go/clear-text-logging] fmt.Printf("Updated %d users setting MustChangePassword to %t\n", n, mustChangePassword) return nil } diff --git a/package/gitea/source/cmd/config.go b/package/gitea/source/cmd/config.go new file mode 100644 index 00000000..5303b0e1 --- /dev/null +++ b/package/gitea/source/cmd/config.go @@ -0,0 +1,156 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + + "code.gitea.io/gitea/modules/setting" + + "github.com/urfave/cli/v3" +) + +func cmdConfig() *cli.Command { + subcmdConfigEditIni := &cli.Command{ + Name: "edit-ini", + Usage: "Load an existing INI file, apply environment variables, keep specified keys, and output to a new INI file.", + Description: ` +Help users to edit the Gitea configuration INI file. + +# Keep Specified Keys + +If you need to re-create the configuration file with only a subset of keys, +you can provide an INI template file for the kept keys and use the "--config-keep-keys" flag. +For example, if a helm chart needs to reset the settings and only keep SECRET_KEY, +it can use a template file (only keys take effect, values are ignored): + + [security] + SECRET_KEY= + +$ ./gitea config edit-ini --config app-old.ini --config-keep-keys app-keys.ini --out app-new.ini + +# Map Environment Variables to INI Configuration + +Environment variables of the form "GITEA__section_name__KEY_NAME" +will be mapped to the ini section "[section_name]" and the key +"KEY_NAME" with the value as provided. + +Environment variables of the form "GITEA__section_name__KEY_NAME__FILE" +will be mapped to the ini section "[section_name]" and the key +"KEY_NAME" with the value loaded from the specified file. + +Environment variable keys can only contain characters "0-9A-Z_", +if a section or key name contains dot ".", it needs to be escaped as _0x2E_. +For example, to apply this config: + + [git.config] + foo.bar=val + +$ export GITEA__git_0x2E_config__foo_0x2E_bar=val + +# Put All Together + +$ ./gitea config edit-ini --config app.ini --config-keep-keys app-keys.ini --apply-env {--in-place|--out app-new.ini} +`, + Flags: []cli.Flag{ + // "--config" flag is provided by global flags, and this flag is also used by "environment-to-ini" script wrapper + // "--in-place" is also used by "environment-to-ini" script wrapper for its old behavior: always overwrite the existing config file + &cli.BoolFlag{ + Name: "in-place", + Usage: "Output to the same config file as input. This flag will be ignored if --out is set.", + }, + &cli.StringFlag{ + Name: "config-keep-keys", + Usage: "An INI template file containing keys for keeping. Only the keys defined in the INI template will be kept from old config. If not set, all keys will be kept.", + }, + &cli.BoolFlag{ + Name: "apply-env", + Usage: "Apply all GITEA__* variables from the environment to the config.", + }, + &cli.StringFlag{ + Name: "out", + Usage: "Destination config file to write to.", + }, + }, + Action: runConfigEditIni, + } + + return &cli.Command{ + Name: "config", + Usage: "Manage Gitea configuration", + Commands: []*cli.Command{ + subcmdConfigEditIni, + }, + } +} + +func runConfigEditIni(_ context.Context, c *cli.Command) error { + // the config system may change the environment variables, so get a copy first, to be used later + env := append([]string{}, os.Environ()...) + + // don't use the guessed setting.CustomConf, instead, require the user to provide --config explicitly + if !c.IsSet("config") { + return errors.New("flag is required but not set: --config") + } + configFileIn := c.String("config") + + cfgIn, err := setting.NewConfigProviderFromFile(configFileIn) + if err != nil { + return fmt.Errorf("failed to load config file %q: %v", configFileIn, err) + } + + // determine output config file: use "--out" flag or use "--in-place" flag to overwrite input file + inPlace := c.Bool("in-place") + configFileOut := c.String("out") + if configFileOut == "" { + if !inPlace { + return errors.New("either --in-place or --out must be specified") + } + configFileOut = configFileIn // in-place edit + } + + needWriteOut := configFileOut != configFileIn + + cfgOut := cfgIn + configKeepKeys := c.String("config-keep-keys") + if configKeepKeys != "" { + needWriteOut = true + cfgOut, err = setting.NewConfigProviderFromFile(configKeepKeys) + if err != nil { + return fmt.Errorf("failed to load config-keep-keys template file %q: %v", configKeepKeys, err) + } + + for _, secOut := range cfgOut.Sections() { + for _, keyOut := range secOut.Keys() { + secIn := cfgIn.Section(secOut.Name()) + keyIn := setting.ConfigSectionKey(secIn, keyOut.Name()) + if keyIn != nil { + keyOut.SetValue(keyIn.String()) + } else { + secOut.DeleteKey(keyOut.Name()) + } + } + if len(secOut.Keys()) == 0 { + cfgOut.DeleteSection(secOut.Name()) + } + } + } + + if c.Bool("apply-env") { + if setting.EnvironmentToConfig(cfgOut, env) { + needWriteOut = true + } + } + + if needWriteOut { + err = cfgOut.SaveTo(configFileOut) + if err != nil { + return err + } + } + return nil +} diff --git a/package/gitea/source/cmd/config_test.go b/package/gitea/source/cmd/config_test.go new file mode 100644 index 00000000..d123daa6 --- /dev/null +++ b/package/gitea/source/cmd/config_test.go @@ -0,0 +1,85 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package cmd + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConfigEdit(t *testing.T) { + tmpDir := t.TempDir() + configOld := tmpDir + "/app-old.ini" + configTemplate := tmpDir + "/app-template.ini" + _ = os.WriteFile(configOld, []byte(` +[sec] +k1=v1 +k2=v2 +`), os.ModePerm) + + _ = os.WriteFile(configTemplate, []byte(` +[sec] +k1=in-template + +[sec2] +k3=v3 +`), os.ModePerm) + + t.Setenv("GITEA__EnV__KeY", "val") + + t.Run("OutputToNewWithEnv", func(t *testing.T) { + configNew := tmpDir + "/app-new.ini" + err := NewMainApp(AppVersion{}).Run(t.Context(), []string{ + "./gitea", "--config", configOld, + "config", "edit-ini", + "--apply-env", + "--config-keep-keys", configTemplate, + "--out", configNew, + }) + require.NoError(t, err) + + // "k1" old value is kept because its key is in the template + // "k2" is removed because it isn't in the template + // "k3" isn't in new config because it isn't in the old config + // [env] is applied from environment variable + data, _ := os.ReadFile(configNew) + require.Equal(t, `[sec] +k1 = v1 + +[env] +KeY = val +`, string(data)) + }) + + t.Run("OutputToExisting(environment-to-ini)", func(t *testing.T) { + // the legacy "environment-to-ini" (now a wrapper script) behavior: + // if no "--out", then "--in-place" must be used to overwrite the existing "--config" file + err := NewMainApp(AppVersion{}).Run(t.Context(), []string{ + "./gitea", "config", "edit-ini", + "--apply-env", + "--config", configOld, + }) + require.ErrorContains(t, err, "either --in-place or --out must be specified") + + // simulate the "environment-to-ini" behavior with "--in-place" + err = NewMainApp(AppVersion{}).Run(t.Context(), []string{ + "./gitea", "config", "edit-ini", + "--in-place", + "--apply-env", + "--config", configOld, + }) + require.NoError(t, err) + + data, _ := os.ReadFile(configOld) + require.Equal(t, `[sec] +k1 = v1 +k2 = v2 + +[env] +KeY = val +`, string(data)) + }) +} diff --git a/package/gitea/source/cmd/docs.go b/package/gitea/source/cmd/docs.go index 098c0e9a..8e0f9428 100644 --- a/package/gitea/source/cmd/docs.go +++ b/package/gitea/source/cmd/docs.go @@ -13,23 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDocs represents the available docs sub-command. -var CmdDocs = &cli.Command{ - Name: "docs", - Usage: "Output CLI documentation", - Description: "A command to output Gitea's CLI documentation, optionally to a file.", - Action: runDocs, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "man", - Usage: "Output man pages instead", +func newDocsCommand() *cli.Command { + return &cli.Command{ + Name: "docs", + Usage: "Output CLI documentation", + Description: "A command to output Gitea's CLI documentation, optionally to a file.", + Action: runDocs, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "man", + Usage: "Output man pages instead", + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Usage: "Path to output to instead of stdout (will overwrite if exists)", + }, }, - &cli.StringFlag{ - Name: "output", - Aliases: []string{"o"}, - Usage: "Path to output to instead of stdout (will overwrite if exists)", - }, - }, + } } func runDocs(_ context.Context, cmd *cli.Command) error { diff --git a/package/gitea/source/cmd/doctor.go b/package/gitea/source/cmd/doctor.go index 596dd611..188740db 100644 --- a/package/gitea/source/cmd/doctor.go +++ b/package/gitea/source/cmd/doctor.go @@ -24,73 +24,77 @@ import ( "xorm.io/xorm" ) -// CmdDoctor represents the available doctor sub-command. -var CmdDoctor = &cli.Command{ - Name: "doctor", - Usage: "Diagnose and optionally fix problems, convert or re-create database tables", - Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - - Commands: []*cli.Command{ - cmdDoctorCheck, - cmdRecreateTable, - cmdDoctorConvert, - }, +func newDoctorCommand() *cli.Command { + return &cli.Command{ + Name: "doctor", + Usage: "Diagnose and optionally fix problems, convert or re-create database tables", + Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", + Commands: []*cli.Command{ + newDoctorCheckCommand(), + newRecreateTableCommand(), + newDoctorConvertCommand(), + }, + } } -var cmdDoctorCheck = &cli.Command{ - Name: "check", - Usage: "Diagnose and optionally fix problems", - Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", - Action: runDoctorCheck, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "list", - Usage: "List the available checks", +func newDoctorCheckCommand() *cli.Command { + return &cli.Command{ + Name: "check", + Usage: "Diagnose and optionally fix problems", + Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.", + Action: runDoctorCheck, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "list", + Usage: "List the available checks", + }, + &cli.BoolFlag{ + Name: "default", + Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", + }, + &cli.StringSliceFlag{ + Name: "run", + Usage: "Run the provided checks - (if --default is set, the default checks will also run)", + }, + &cli.BoolFlag{ + Name: "all", + Usage: "Run all the available checks", + }, + &cli.BoolFlag{ + Name: "fix", + Usage: "Automatically fix what we can", + }, + &cli.StringFlag{ + Name: "log-file", + Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, + }, + &cli.BoolFlag{ + Name: "color", + Aliases: []string{"H"}, + Usage: "Use color for outputted information", + }, }, - &cli.BoolFlag{ - Name: "default", - Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)", - }, - &cli.StringSliceFlag{ - Name: "run", - Usage: "Run the provided checks - (if --default is set, the default checks will also run)", - }, - &cli.BoolFlag{ - Name: "all", - Usage: "Run all the available checks", - }, - &cli.BoolFlag{ - Name: "fix", - Usage: "Automatically fix what we can", - }, - &cli.StringFlag{ - Name: "log-file", - Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`, - }, - &cli.BoolFlag{ - Name: "color", - Aliases: []string{"H"}, - Usage: "Use color for outputted information", - }, - }, + } } -var cmdRecreateTable = &cli.Command{ - Name: "recreate-table", - Usage: "Recreate tables from XORM definitions and copy the data.", - ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "debug", - Usage: "Print SQL commands sent", +func newRecreateTableCommand() *cli.Command { + return &cli.Command{ + Name: "recreate-table", + Usage: "Recreate tables from XORM definitions and copy the data.", + ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Usage: "Print SQL commands sent", + }, }, - }, - Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. + Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns. This command will cause Xorm to recreate tables, copying over the data and deleting the old table. You should back-up your database before doing this and ensure that your database is up-to-date first.`, - Action: runRecreateTable, + Action: runRecreateTable, + } } func runRecreateTable(ctx context.Context, cmd *cli.Command) error { diff --git a/package/gitea/source/cmd/doctor_convert.go b/package/gitea/source/cmd/doctor_convert.go index 8cb718d3..f4867912 100644 --- a/package/gitea/source/cmd/doctor_convert.go +++ b/package/gitea/source/cmd/doctor_convert.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// cmdDoctorConvert represents the available convert sub-command. -var cmdDoctorConvert = &cli.Command{ - Name: "convert", - Usage: "Convert the database", - Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", - Action: runDoctorConvert, +func newDoctorConvertCommand() *cli.Command { + return &cli.Command{ + Name: "convert", + Usage: "Convert the database", + Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar", + Action: runDoctorConvert, + } } func runDoctorConvert(ctx context.Context, cmd *cli.Command) error { diff --git a/package/gitea/source/cmd/doctor_test.go b/package/gitea/source/cmd/doctor_test.go index da942b38..7d2f3589 100644 --- a/package/gitea/source/cmd/doctor_test.go +++ b/package/gitea/source/cmd/doctor_test.go @@ -23,7 +23,7 @@ func TestDoctorRun(t *testing.T) { SkipDatabaseInitialization: true, }) app := &cli.Command{ - Commands: []*cli.Command{cmdDoctorCheck}, + Commands: []*cli.Command{newDoctorCheckCommand()}, } err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"}) assert.NoError(t, err) diff --git a/package/gitea/source/cmd/dump.go b/package/gitea/source/cmd/dump.go index 7f0b23ed..49f4d9e8 100644 --- a/package/gitea/source/cmd/dump.go +++ b/package/gitea/source/cmd/dump.go @@ -23,78 +23,79 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDump represents the available dump sub-command. -var CmdDump = &cli.Command{ - Name: "dump", - Usage: "Dump Gitea files and database", - Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, - Action: runDump, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "file", - Aliases: []string{"f"}, - Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, +func newDumpCommand() *cli.Command { + return &cli.Command{ + Name: "dump", + Usage: "Dump Gitea files and database", + Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`, + Action: runDump, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "file", + Aliases: []string{"f"}, + Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`, + }, + &cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"V"}, + Usage: "Show process details", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display warnings and errors", + }, + &cli.StringFlag{ + Name: "tempdir", + Aliases: []string{"t"}, + Value: os.TempDir(), + Usage: "Temporary dir path", + }, + &cli.StringFlag{ + Name: "database", + Aliases: []string{"d"}, + Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", + }, + &cli.BoolFlag{ + Name: "skip-repository", + Aliases: []string{"R"}, + Usage: "Skip the repository dumping", + }, + &cli.BoolFlag{ + Name: "skip-log", + Aliases: []string{"L"}, + Usage: "Skip the log dumping", + }, + &cli.BoolFlag{ + Name: "skip-custom-dir", + Usage: "Skip custom directory", + }, + &cli.BoolFlag{ + Name: "skip-lfs-data", + Usage: "Skip LFS data", + }, + &cli.BoolFlag{ + Name: "skip-attachment-data", + Usage: "Skip attachment data", + }, + &cli.BoolFlag{ + Name: "skip-package-data", + Usage: "Skip package data", + }, + &cli.BoolFlag{ + Name: "skip-index", + Usage: "Skip bleve index data", + }, + &cli.BoolFlag{ + Name: "skip-db", + Usage: "Skip database", + }, + &cli.StringFlag{ + Name: "type", + Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), + }, }, - &cli.BoolFlag{ - Name: "verbose", - Aliases: []string{"V"}, - Usage: "Show process details", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display warnings and errors", - }, - &cli.StringFlag{ - Name: "tempdir", - Aliases: []string{"t"}, - Value: os.TempDir(), - Usage: "Temporary dir path", - }, - &cli.StringFlag{ - Name: "database", - Aliases: []string{"d"}, - Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres", - }, - &cli.BoolFlag{ - Name: "skip-repository", - Aliases: []string{"R"}, - Usage: "Skip the repository dumping", - }, - &cli.BoolFlag{ - Name: "skip-log", - Aliases: []string{"L"}, - Usage: "Skip the log dumping", - }, - &cli.BoolFlag{ - Name: "skip-custom-dir", - Usage: "Skip custom directory", - }, - &cli.BoolFlag{ - Name: "skip-lfs-data", - Usage: "Skip LFS data", - }, - &cli.BoolFlag{ - Name: "skip-attachment-data", - Usage: "Skip attachment data", - }, - &cli.BoolFlag{ - Name: "skip-package-data", - Usage: "Skip package data", - }, - &cli.BoolFlag{ - Name: "skip-index", - Usage: "Skip bleve index data", - }, - &cli.BoolFlag{ - Name: "skip-db", - Usage: "Skip database", - }, - &cli.StringFlag{ - Name: "type", - Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "), - }, - }, + } } func fatal(format string, args ...any) { diff --git a/package/gitea/source/cmd/dump_repo.go b/package/gitea/source/cmd/dump_repo.go index beda305c..36745436 100644 --- a/package/gitea/source/cmd/dump_repo.go +++ b/package/gitea/source/cmd/dump_repo.go @@ -22,61 +22,62 @@ import ( "github.com/urfave/cli/v3" ) -// CmdDumpRepository represents the available dump repository sub-command. -var CmdDumpRepository = &cli.Command{ - Name: "dump-repo", - Usage: "Dump the repository from git/github/gitea/gitlab", - Description: "This is a command for dumping the repository data.", - Action: runDumpRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "git_service", - Value: "", - Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", - }, - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to store the data", - }, - &cli.StringFlag{ - Name: "clone_addr", - Value: "", - Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", - }, - &cli.StringFlag{ - Name: "auth_username", - Value: "", - Usage: "The username to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_password", - Value: "", - Usage: "The password to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "auth_token", - Value: "", - Usage: "The personal token to visit the clone_addr", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "The data will be stored on a directory with owner name if not empty", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "The data will be stored on a directory with repository name if not empty", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be migrated, one or more units should be separated as comma. +func newDumpRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "dump-repo", + Usage: "Dump the repository from git/github/gitea/gitlab", + Description: "This is a command for dumping the repository data.", + Action: runDumpRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "git_service", + Value: "", + Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.", + }, + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to store the data", + }, + &cli.StringFlag{ + Name: "clone_addr", + Value: "", + Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL", + }, + &cli.StringFlag{ + Name: "auth_username", + Value: "", + Usage: "The username to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_password", + Value: "", + Usage: "The password to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "auth_token", + Value: "", + Usage: "The personal token to visit the clone_addr", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "The data will be stored on a directory with owner name if not empty", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "The data will be stored on a directory with repository name if not empty", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be migrated, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, }, - }, + } } func runDumpRepository(ctx context.Context, cmd *cli.Command) error { diff --git a/package/gitea/source/cmd/embedded.go b/package/gitea/source/cmd/embedded.go index 9180407f..e2110756 100644 --- a/package/gitea/source/cmd/embedded.go +++ b/package/gitea/source/cmd/embedded.go @@ -23,20 +23,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdEmbedded represents the available extract sub-command. -var ( - CmdEmbedded = &cli.Command{ +var matchedAssetFiles []assetFile + +func newEmbeddedCommand() *cli.Command { + return &cli.Command{ Name: "embedded", Usage: "Extract embedded resources", Description: "A command for extracting embedded resources, like templates and images", Commands: []*cli.Command{ - subcmdList, - subcmdView, - subcmdExtract, + newEmbeddedListCommand(), + newEmbeddedViewCommand(), + newEmbeddedExtractCommand(), }, } +} - subcmdList = &cli.Command{ +func newEmbeddedListCommand() *cli.Command { + return &cli.Command{ Name: "list", Usage: "List files matching the given pattern", Action: runList, @@ -48,8 +51,10 @@ var ( }, }, } +} - subcmdView = &cli.Command{ +func newEmbeddedViewCommand() *cli.Command { + return &cli.Command{ Name: "view", Usage: "View a file matching the given pattern", Action: runView, @@ -61,8 +66,10 @@ var ( }, }, } +} - subcmdExtract = &cli.Command{ +func newEmbeddedExtractCommand() *cli.Command { + return &cli.Command{ Name: "extract", Usage: "Extract resources", Action: runExtract, @@ -91,9 +98,7 @@ var ( }, }, } - - matchedAssetFiles []assetFile -) +} type assetFile struct { fs *assetfs.LayeredFS diff --git a/package/gitea/source/cmd/generate.go b/package/gitea/source/cmd/generate.go index cf491604..21f8b42b 100644 --- a/package/gitea/source/cmd/generate.go +++ b/package/gitea/source/cmd/generate.go @@ -15,45 +15,52 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdGenerate represents the available generate sub-command. - CmdGenerate = &cli.Command{ +func newGenerateCommand() *cli.Command { + return &cli.Command{ Name: "generate", Usage: "Generate Gitea's secrets/keys/tokens", Commands: []*cli.Command{ - subcmdSecret, + newGenerateSecretCommand(), }, } +} - subcmdSecret = &cli.Command{ +func newGenerateSecretCommand() *cli.Command { + return &cli.Command{ Name: "secret", Usage: "Generate a secret token", Commands: []*cli.Command{ - microcmdGenerateInternalToken, - microcmdGenerateLfsJwtSecret, - microcmdGenerateSecretKey, + newGenerateInternalTokenCommand(), + newGenerateLfsJWTSecretCommand(), + newGenerateSecretKeyCommand(), }, } +} - microcmdGenerateInternalToken = &cli.Command{ +func newGenerateInternalTokenCommand() *cli.Command { + return &cli.Command{ Name: "INTERNAL_TOKEN", Usage: "Generate a new INTERNAL_TOKEN", Action: runGenerateInternalToken, } +} - microcmdGenerateLfsJwtSecret = &cli.Command{ +func newGenerateLfsJWTSecretCommand() *cli.Command { + return &cli.Command{ Name: "JWT_SECRET", Aliases: []string{"LFS_JWT_SECRET"}, Usage: "Generate a new JWT_SECRET", Action: runGenerateLfsJwtSecret, } +} - microcmdGenerateSecretKey = &cli.Command{ +func newGenerateSecretKeyCommand() *cli.Command { + return &cli.Command{ Name: "SECRET_KEY", Usage: "Generate a new SECRET_KEY", Action: runGenerateSecretKey, } -) +} func runGenerateInternalToken(_ context.Context, c *cli.Command) error { internalToken, err := generate.NewInternalToken() @@ -71,11 +78,7 @@ func runGenerateInternalToken(_ context.Context, c *cli.Command) error { } func runGenerateLfsJwtSecret(_ context.Context, c *cli.Command) error { - _, jwtSecretBase64, err := generate.NewJwtSecretWithBase64() - if err != nil { - return err - } - + _, jwtSecretBase64 := generate.NewJwtSecretWithBase64() fmt.Printf("%s", jwtSecretBase64) if isatty.IsTerminal(os.Stdout.Fd()) { @@ -91,6 +94,7 @@ func runGenerateSecretKey(_ context.Context, c *cli.Command) error { return err } + // codeql[disable-next-line=go/clear-text-logging] fmt.Printf("%s", secretKey) if isatty.IsTerminal(os.Stdout.Fd()) { diff --git a/package/gitea/source/cmd/hook.go b/package/gitea/source/cmd/hook.go index 37fdb927..4a6c7c29 100644 --- a/package/gitea/source/cmd/hook.go +++ b/package/gitea/source/cmd/hook.go @@ -28,23 +28,24 @@ const ( hookBatchSize = 500 ) -var ( - // CmdHook represents the available hooks sub-command. - CmdHook = &cli.Command{ +func newHookCommand() *cli.Command { + return &cli.Command{ Name: "hook", Usage: "(internal) Should only be called by Git", Hidden: true, // internal commands shouldn't be visible Description: "Delegate commands to corresponding Git hooks", Before: PrepareConsoleLoggerLevel(log.FATAL), Commands: []*cli.Command{ - subcmdHookPreReceive, - subcmdHookUpdate, - subcmdHookPostReceive, - subcmdHookProcReceive, + newHookPreReceiveCommand(), + newHookUpdateCommand(), + newHookPostReceiveCommand(), + newHookProcReceiveCommand(), }, } +} - subcmdHookPreReceive = &cli.Command{ +func newHookPreReceiveCommand() *cli.Command { + return &cli.Command{ Name: "pre-receive", Usage: "Delegate pre-receive Git hook", Description: "This command should only be called by Git", @@ -55,7 +56,10 @@ var ( }, }, } - subcmdHookUpdate = &cli.Command{ +} + +func newHookUpdateCommand() *cli.Command { + return &cli.Command{ Name: "update", Usage: "Delegate update Git hook", Description: "This command should only be called by Git", @@ -66,7 +70,10 @@ var ( }, }, } - subcmdHookPostReceive = &cli.Command{ +} + +func newHookPostReceiveCommand() *cli.Command { + return &cli.Command{ Name: "post-receive", Usage: "Delegate post-receive Git hook", Description: "This command should only be called by Git", @@ -77,8 +84,11 @@ var ( }, }, } - // Note: new hook since git 2.29 - subcmdHookProcReceive = &cli.Command{ +} + +// Note: new hook since git 2.29 +func newHookProcReceiveCommand() *cli.Command { + return &cli.Command{ Name: "proc-receive", Usage: "Delegate proc-receive Git hook", Description: "This command should only be called by Git", @@ -89,7 +99,7 @@ var ( }, }, } -) +} type delayWriter struct { internal io.Writer @@ -194,7 +204,7 @@ Gitea or set your environment appropriately.`, "") userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64) - actionPerm, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionPerm), 10, 64) + actionsTaskID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionsTaskID), 10, 64) hookOptions := private.HookOptions{ UserID: userID, @@ -204,7 +214,8 @@ Gitea or set your environment appropriately.`, "") GitPushOptions: pushOptions(), PullRequestID: prID, DeployKeyID: deployKeyID, - ActionPerm: int(actionPerm), + ActionsTaskID: actionsTaskID, + IsWiki: isWiki, } scanner := bufio.NewScanner(os.Stdin) @@ -275,6 +286,9 @@ Gitea or set your environment appropriately.`, "") lastline = 0 } } + if err := scanner.Err(); err != nil { + return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err) + } if count > 0 { hookOptions.OldCommitIDs = oldCommitIDs[:count] @@ -318,7 +332,7 @@ func runHookPostReceive(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) // First of all run update-server-info no matter what - if _, _, err := gitcmd.NewCommand("update-server-info").RunStdString(ctx, nil); err != nil { + if err := gitcmd.NewCommand("update-server-info").RunWithStderr(ctx); err != nil { return fmt.Errorf("failed to call 'git update-server-info': %w", err) } @@ -366,6 +380,7 @@ Gitea or set your environment appropriately.`, "") GitPushOptions: pushOptions(), PullRequestID: prID, PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)), + IsWiki: isWiki, } oldCommitIDs := make([]string, hookBatchSize) newCommitIDs := make([]string, hookBatchSize) @@ -413,6 +428,11 @@ Gitea or set your environment appropriately.`, "") count = 0 } } + if err := scanner.Err(); err != nil { + _ = dWriter.Close() + hookPrintResults(results) + return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err) + } if count == 0 { if wasEmpty && masterPushed { @@ -513,6 +533,7 @@ Gitea or set your environment appropriately.`, "") reader := bufio.NewReader(os.Stdin) repoUser := os.Getenv(repo_module.EnvRepoUsername) + isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki)) repoName := os.Getenv(repo_module.EnvRepoName) pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) pusherName := os.Getenv(repo_module.EnvPusherName) @@ -590,14 +611,15 @@ Gitea or set your environment appropriately.`, "") UserName: pusherName, UserID: pusherID, GitPushOptions: make(map[string]string), + IsWiki: isWiki, } hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize) hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize) hookOptions.RefFullNames = make([]git.RefName, 0, hookBatchSize) for { - // note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed - rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) + // note: pktLineTypeUnknown means pktLineTypeFlush and pktLineTypeData all allowed + rs, err = readPktLine(ctx, reader, pktLineTypeUnknown) if err != nil { return err } @@ -616,7 +638,7 @@ Gitea or set your environment appropriately.`, "") if hasPushOptions { for { - rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) + rs, err = readPktLine(ctx, reader, pktLineTypeUnknown) if err != nil { return err } @@ -713,8 +735,8 @@ Gitea or set your environment appropriately.`, "") type pktLineType int64 const ( - // UnKnow type - pktLineTypeUnknow pktLineType = 0 + // Unknown type + pktLineTypeUnknown pktLineType = 0 // flush-pkt "0000" pktLineTypeFlush pktLineType = iota // data line diff --git a/package/gitea/source/cmd/keys.go b/package/gitea/source/cmd/keys.go index 035d39bf..912cf250 100644 --- a/package/gitea/source/cmd/keys.go +++ b/package/gitea/source/cmd/keys.go @@ -15,40 +15,42 @@ import ( "github.com/urfave/cli/v3" ) -// CmdKeys represents the available keys sub-command -var CmdKeys = &cli.Command{ - Name: "keys", - Usage: "(internal) Should only be called by SSH server", - Hidden: true, // internal commands shouldn't be visible - Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runKeys, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "expected", - Aliases: []string{"e"}, - Value: "git", - Usage: "Expected user for whom provide key commands", +// NewKeysCommand returns the internal SSH key lookup sub-command. +func NewKeysCommand() *cli.Command { + return &cli.Command{ + Name: "keys", + Usage: "(internal) Should only be called by SSH server", + Hidden: true, // internal commands shouldn't be visible + Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint", + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runKeys, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "expected", + Aliases: []string{"e"}, + Value: "git", + Usage: "Expected user for whom provide key commands", + }, + &cli.StringFlag{ + Name: "username", + Aliases: []string{"u"}, + Value: "", + Usage: "Username trying to log in by SSH", + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", + }, + &cli.StringFlag{ + Name: "content", + Aliases: []string{"k"}, + Value: "", + Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", + }, }, - &cli.StringFlag{ - Name: "username", - Aliases: []string{"u"}, - Value: "", - Usage: "Username trying to log in by SSH", - }, - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)", - }, - &cli.StringFlag{ - Name: "content", - Aliases: []string{"k"}, - Value: "", - Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)", - }, - }, + } } func runKeys(ctx context.Context, c *cli.Command) error { diff --git a/package/gitea/source/cmd/mailer.go b/package/gitea/source/cmd/mailer.go index 72bd8e56..a7dbd68e 100644 --- a/package/gitea/source/cmd/mailer.go +++ b/package/gitea/source/cmd/mailer.go @@ -17,10 +17,10 @@ func runSendMail(ctx context.Context, c *cli.Command) error { setting.MustInstalled() 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/package/gitea/source/cmd/main.go b/package/gitea/source/cmd/main.go index da979ee5..a6b89a6f 100644 --- a/package/gitea/source/cmd/main.go +++ b/package/gitea/source/cmd/main.go @@ -112,34 +112,36 @@ func NewMainApp(appVer AppVersion) *cli.Command { Usage: "Set custom path (defaults to '{WorkPath}/custom')", }, } + webCmd := newWebCommand() // these sub-commands need to use a config file subCmdWithConfig := []*cli.Command{ - CmdWeb, - CmdServ, - CmdHook, - CmdKeys, - CmdDump, - CmdAdmin, - CmdMigrate, - CmdDoctor, - CmdManager, - CmdEmbedded, - CmdMigrateStorage, - CmdDumpRepository, - CmdRestoreRepository, - CmdActions, + webCmd, + newServCommand(), + newHookCommand(), + NewKeysCommand(), + newDumpCommand(), + newAdminCommand(), + newMigrateCommand(), + newDoctorCommand(), + newManagerCommand(), + newEmbeddedCommand(), + newMigrateStorageCommand(), + newDumpRepositoryCommand(), + newRestoreRepositoryCommand(), + newActionsCommand(), } // these sub-commands do not need the config file, and they do not depend on any path or environment variable. subCmdStandalone := []*cli.Command{ + cmdConfig(), cmdCert(), - CmdGenerate, - CmdDocs, + newGenerateCommand(), + newDocsCommand(), } // TODO: we should eventually drop the default command, // but not sure whether it would break Windows users who used to double-click the EXE to run. - app.DefaultCommand = CmdWeb.Name + app.DefaultCommand = webCmd.Name app.Before = PrepareConsoleLoggerLevel(log.INFO) for i := range subCmdWithConfig { @@ -148,7 +150,7 @@ func NewMainApp(appVer AppVersion) *cli.Command { app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) - setting.InitGiteaEnvVars() + setting.UnsetUnnecessaryEnvVars() return app } diff --git a/package/gitea/source/cmd/main_test.go b/package/gitea/source/cmd/main_test.go index 69ea1237..b1f6bb3b 100644 --- a/package/gitea/source/cmd/main_test.go +++ b/package/gitea/source/cmd/main_test.go @@ -157,6 +157,7 @@ func TestCliCmd(t *testing.T) { for _, c := range cases { t.Run(c.cmd, func(t *testing.T) { + defer test.MockVariableValue(&setting.InstallLock, false)() app := newTestApp(cli.Command{ Action: func(ctx context.Context, cmd *cli.Command) error { _, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) @@ -170,7 +171,10 @@ func TestCliCmd(t *testing.T) { r, err := runTestApp(app, args...) assert.NoError(t, err, c.cmd) assert.NotEmpty(t, c.exp, c.cmd) - assert.Contains(t, r.Stdout, c.exp, c.cmd) + if !assert.Contains(t, r.Stdout, c.exp, c.cmd) { + t.Log("Full output:\n" + r.Stdout) + t.Log("Expected:\n" + c.exp) + } }) } } diff --git a/package/gitea/source/cmd/manager.go b/package/gitea/source/cmd/manager.go index f0935ea0..586c6599 100644 --- a/package/gitea/source/cmd/manager.go +++ b/package/gitea/source/cmd/manager.go @@ -13,22 +13,24 @@ import ( "github.com/urfave/cli/v3" ) -var ( - // CmdManager represents the manager command - CmdManager = &cli.Command{ +func newManagerCommand() *cli.Command { + return &cli.Command{ Name: "manager", Usage: "Manage the running gitea process", Description: "This is a command for managing the running gitea process", Commands: []*cli.Command{ - subcmdShutdown, - subcmdRestart, - subcmdReloadTemplates, - subcmdFlushQueues, - subcmdLogging, - subCmdProcesses, + newShutdownCommand(), + newRestartCommand(), + newReloadTemplatesCommand(), + newFlushQueuesCommand(), + newLoggingCommand(), + newProcessesCommand(), }, } - subcmdShutdown = &cli.Command{ +} + +func newShutdownCommand() *cli.Command { + return &cli.Command{ Name: "shutdown", Usage: "Gracefully shutdown the running process", Flags: []cli.Flag{ @@ -38,7 +40,10 @@ var ( }, Action: runShutdown, } - subcmdRestart = &cli.Command{ +} + +func newRestartCommand() *cli.Command { + return &cli.Command{ Name: "restart", Usage: "Gracefully restart the running process - (not implemented for windows servers)", Flags: []cli.Flag{ @@ -48,7 +53,10 @@ var ( }, Action: runRestart, } - subcmdReloadTemplates = &cli.Command{ +} + +func newReloadTemplatesCommand() *cli.Command { + return &cli.Command{ Name: "reload-templates", Usage: "Reload template files in the running process", Flags: []cli.Flag{ @@ -58,7 +66,10 @@ var ( }, Action: runReloadTemplates, } - subcmdFlushQueues = &cli.Command{ +} + +func newFlushQueuesCommand() *cli.Command { + return &cli.Command{ Name: "flush-queues", Usage: "Flush queues in the running process", Action: runFlushQueues, @@ -77,7 +88,10 @@ var ( }, }, } - subCmdProcesses = &cli.Command{ +} + +func newProcessesCommand() *cli.Command { + return &cli.Command{ Name: "processes", Usage: "Display running processes within the current process", Action: runProcesses, @@ -107,7 +121,7 @@ var ( }, }, } -) +} func runShutdown(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/package/gitea/source/cmd/manager_logging.go b/package/gitea/source/cmd/manager_logging.go index ac29e7d3..5812e707 100644 --- a/package/gitea/source/cmd/manager_logging.go +++ b/package/gitea/source/cmd/manager_logging.go @@ -15,8 +15,8 @@ import ( "github.com/urfave/cli/v3" ) -var ( - defaultLoggingFlags = []cli.Flag{ +func defaultLoggingFlags() []cli.Flag { + return []cli.Flag{ &cli.StringFlag{ Name: "logger", Usage: `Logger name - will default to "default"`, @@ -57,8 +57,10 @@ var ( Name: "debug", }, } +} - subcmdLogging = &cli.Command{ +func newLoggingCommand() *cli.Command { + return &cli.Command{ Name: "logging", Usage: "Adjust logging commands", Commands: []*cli.Command{ @@ -109,7 +111,7 @@ var ( { Name: "file", Usage: "Add a file logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.StringFlag{ Name: "filename", Aliases: []string{"f"}, @@ -150,7 +152,7 @@ var ( }, { Name: "conn", Usage: "Add a net conn logger", - Flags: append(defaultLoggingFlags, []cli.Flag{ + Flags: append(defaultLoggingFlags(), []cli.Flag{ &cli.BoolFlag{ Name: "reconnect-on-message", Aliases: []string{"R"}, @@ -191,7 +193,7 @@ var ( }, }, } -) +} func runRemoveLogger(ctx context.Context, c *cli.Command) error { setup(ctx, c.Bool("debug")) diff --git a/package/gitea/source/cmd/migrate.go b/package/gitea/source/cmd/migrate.go index e24dc9e5..016f8a0d 100644 --- a/package/gitea/source/cmd/migrate.go +++ b/package/gitea/source/cmd/migrate.go @@ -14,12 +14,13 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrate represents the available migrate sub-command. -var CmdMigrate = &cli.Command{ - Name: "migrate", - Usage: "Migrate the database", - Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, - Action: runMigrate, +func newMigrateCommand() *cli.Command { + return &cli.Command{ + Name: "migrate", + Usage: "Migrate the database", + Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`, + Action: runMigrate, + } } func runMigrate(ctx context.Context, c *cli.Command) error { diff --git a/package/gitea/source/cmd/migrate_storage.go b/package/gitea/source/cmd/migrate_storage.go index 2c63e15f..c9b82055 100644 --- a/package/gitea/source/cmd/migrate_storage.go +++ b/package/gitea/source/cmd/migrate_storage.go @@ -25,107 +25,108 @@ import ( "github.com/urfave/cli/v3" ) -// CmdMigrateStorage represents the available migrate storage sub-command. -var CmdMigrateStorage = &cli.Command{ - Name: "migrate-storage", - Usage: "Migrate the storage", - Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", - Action: runMigrateStorage, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "type", - Aliases: []string{"t"}, - Value: "", - Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts", +func newMigrateStorageCommand() *cli.Command { + return &cli.Command{ + Name: "migrate-storage", + Usage: "Migrate the storage", + Description: "Copies stored files from storage configured in app.ini to parameter-configured storage", + Action: runMigrateStorage, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "", + Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'", + }, + &cli.StringFlag{ + Name: "storage", + Aliases: []string{"s"}, + Value: "", + Usage: "New storage type: local (default), minio or azureblob", + }, + &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Value: "", + Usage: "New storage placement if store is local (leave blank for default)", + }, + // Minio Storage special configurations + &cli.StringFlag{ + Name: "minio-endpoint", + Value: "", + Usage: "Minio storage endpoint", + }, + &cli.StringFlag{ + Name: "minio-access-key-id", + Value: "", + Usage: "Minio storage accessKeyID", + }, + &cli.StringFlag{ + Name: "minio-secret-access-key", + Value: "", + Usage: "Minio storage secretAccessKey", + }, + &cli.StringFlag{ + Name: "minio-bucket", + Value: "", + Usage: "Minio storage bucket", + }, + &cli.StringFlag{ + Name: "minio-location", + Value: "", + Usage: "Minio storage location to create bucket", + }, + &cli.StringFlag{ + Name: "minio-base-path", + Value: "", + Usage: "Minio storage base path on the bucket", + }, + &cli.BoolFlag{ + Name: "minio-use-ssl", + Usage: "Enable SSL for minio", + }, + &cli.BoolFlag{ + Name: "minio-insecure-skip-verify", + Usage: "Skip SSL verification", + }, + &cli.StringFlag{ + Name: "minio-checksum-algorithm", + Value: "", + Usage: "Minio checksum algorithm (default/md5)", + }, + &cli.StringFlag{ + Name: "minio-bucket-lookup-type", + Value: "", + Usage: "Minio bucket lookup type", + }, + // Azure Blob Storage special configurations + &cli.StringFlag{ + Name: "azureblob-endpoint", + Value: "", + Usage: "Azure Blob storage endpoint", + }, + &cli.StringFlag{ + Name: "azureblob-account-name", + Value: "", + Usage: "Azure Blob storage account name", + }, + &cli.StringFlag{ + Name: "azureblob-account-key", + Value: "", + Usage: "Azure Blob storage account key", + }, + &cli.StringFlag{ + Name: "azureblob-container", + Value: "", + Usage: "Azure Blob storage container", + }, + &cli.StringFlag{ + Name: "azureblob-base-path", + Value: "", + Usage: "Azure Blob storage base path", + }, }, - &cli.StringFlag{ - Name: "storage", - Aliases: []string{"s"}, - Value: "", - Usage: "New storage type: local (default), minio or azureblob", - }, - &cli.StringFlag{ - Name: "path", - Aliases: []string{"p"}, - Value: "", - Usage: "New storage placement if store is local (leave blank for default)", - }, - // Minio Storage special configurations - &cli.StringFlag{ - Name: "minio-endpoint", - Value: "", - Usage: "Minio storage endpoint", - }, - &cli.StringFlag{ - Name: "minio-access-key-id", - Value: "", - Usage: "Minio storage accessKeyID", - }, - &cli.StringFlag{ - Name: "minio-secret-access-key", - Value: "", - Usage: "Minio storage secretAccessKey", - }, - &cli.StringFlag{ - Name: "minio-bucket", - Value: "", - Usage: "Minio storage bucket", - }, - &cli.StringFlag{ - Name: "minio-location", - Value: "", - Usage: "Minio storage location to create bucket", - }, - &cli.StringFlag{ - Name: "minio-base-path", - Value: "", - Usage: "Minio storage base path on the bucket", - }, - &cli.BoolFlag{ - Name: "minio-use-ssl", - Usage: "Enable SSL for minio", - }, - &cli.BoolFlag{ - Name: "minio-insecure-skip-verify", - Usage: "Skip SSL verification", - }, - &cli.StringFlag{ - Name: "minio-checksum-algorithm", - Value: "", - Usage: "Minio checksum algorithm (default/md5)", - }, - &cli.StringFlag{ - Name: "minio-bucket-lookup-type", - Value: "", - Usage: "Minio bucket lookup type", - }, - // Azure Blob Storage special configurations - &cli.StringFlag{ - Name: "azureblob-endpoint", - Value: "", - Usage: "Azure Blob storage endpoint", - }, - &cli.StringFlag{ - Name: "azureblob-account-name", - Value: "", - Usage: "Azure Blob storage account name", - }, - &cli.StringFlag{ - Name: "azureblob-account-key", - Value: "", - Usage: "Azure Blob storage account key", - }, - &cli.StringFlag{ - Name: "azureblob-container", - Value: "", - Usage: "Azure Blob storage container", - }, - &cli.StringFlag{ - Name: "azureblob-base-path", - Value: "", - Usage: "Azure Blob storage base path", - }, - }, + } } func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error { diff --git a/package/gitea/source/cmd/restore_repo.go b/package/gitea/source/cmd/restore_repo.go index c61f5a58..26b4682f 100644 --- a/package/gitea/source/cmd/restore_repo.go +++ b/package/gitea/source/cmd/restore_repo.go @@ -13,40 +13,41 @@ import ( "github.com/urfave/cli/v3" ) -// CmdRestoreRepository represents the available restore a repository sub-command. -var CmdRestoreRepository = &cli.Command{ - Name: "restore-repo", - Usage: "Restore the repository from disk", - Description: "This is a command for restoring the repository data.", - Action: runRestoreRepository, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "repo_dir", - Aliases: []string{"r"}, - Value: "./data", - Usage: "Repository dir path to restore from", - }, - &cli.StringFlag{ - Name: "owner_name", - Value: "", - Usage: "Restore destination owner name", - }, - &cli.StringFlag{ - Name: "repo_name", - Value: "", - Usage: "Restore destination repository name", - }, - &cli.StringFlag{ - Name: "units", - Value: "", - Usage: `Which items will be restored, one or more units should be separated as comma. +func newRestoreRepositoryCommand() *cli.Command { + return &cli.Command{ + Name: "restore-repo", + Usage: "Restore the repository from disk", + Description: "This is a command for restoring the repository data.", + Action: runRestoreRepository, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "repo_dir", + Aliases: []string{"r"}, + Value: "./data", + Usage: "Repository dir path to restore from", + }, + &cli.StringFlag{ + Name: "owner_name", + Value: "", + Usage: "Restore destination owner name", + }, + &cli.StringFlag{ + Name: "repo_name", + Value: "", + Usage: "Restore destination repository name", + }, + &cli.StringFlag{ + Name: "units", + Value: "", + Usage: `Which items will be restored, one or more units should be separated as comma. wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`, + }, + &cli.BoolFlag{ + Name: "validation", + Usage: "Sanity check the content of the files before trying to load them", + }, }, - &cli.BoolFlag{ - Name: "validation", - Usage: "Sanity check the content of the files before trying to load them", - }, - }, + } } func runRestoreRepository(ctx context.Context, c *cli.Command) error { diff --git a/package/gitea/source/cmd/serv.go b/package/gitea/source/cmd/serv.go index 4110fda0..a35d476c 100644 --- a/package/gitea/source/cmd/serv.go +++ b/package/gitea/source/cmd/serv.go @@ -35,22 +35,23 @@ import ( "github.com/urfave/cli/v3" ) -// CmdServ represents the available serv sub-command. -var CmdServ = &cli.Command{ - Name: "serv", - Usage: "(internal) Should only be called by SSH shell", - Description: "Serv provides access auth for repositories", - Hidden: true, // Internal commands shouldn't be visible in help - Before: PrepareConsoleLoggerLevel(log.FATAL), - Action: runServ, - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "enable-pprof", +func newServCommand() *cli.Command { + return &cli.Command{ + Name: "serv", + Usage: "(internal) Should only be called by SSH shell", + Description: "Serv provides access auth for repositories", + Hidden: true, // Internal commands shouldn't be visible in help + Before: PrepareConsoleLoggerLevel(log.FATAL), + Action: runServ, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "enable-pprof", + }, + &cli.BoolFlag{ + Name: "debug", + }, }, - &cli.BoolFlag{ - Name: "debug", - }, - }, + } } func setup(ctx context.Context, debug bool) { diff --git a/package/gitea/source/cmd/web.go b/package/gitea/source/cmd/web.go index 4723ddbb..994c481f 100644 --- a/package/gitea/source/cmd/web.go +++ b/package/gitea/source/cmd/web.go @@ -8,14 +8,13 @@ import ( "fmt" "net" "net/http" + "net/http/pprof" "os" "path/filepath" "strconv" "strings" "time" - _ "net/http/pprof" // Used for debugging if enabled and a web server is running - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/gtprof" @@ -23,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/install" @@ -34,42 +34,43 @@ import ( // PIDFile could be set from build tag var PIDFile = "/run/gitea.pid" -// CmdWeb represents the available web sub-command. -var CmdWeb = &cli.Command{ - Name: "web", - Usage: "Start Gitea web server", - Description: `Gitea web server is the only thing you need to run, +func newWebCommand() *cli.Command { + return &cli.Command{ + Name: "web", + Usage: "Start Gitea web server", + Description: `Gitea web server is the only thing you need to run, and it takes care of all the other things for you`, - Before: PrepareConsoleLoggerLevel(log.INFO), - Action: runWeb, - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "port", - Aliases: []string{"p"}, - Value: "3000", - Usage: "Temporary port number to prevent conflict", + Before: PrepareConsoleLoggerLevel(log.INFO), + Action: runWeb, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "port", + Aliases: []string{"p"}, + Value: "3000", + Usage: "Temporary port number to prevent conflict", + }, + &cli.StringFlag{ + Name: "install-port", + Value: "3000", + Usage: "Temporary port number to run the install page on to prevent conflict", + }, + &cli.StringFlag{ + Name: "pid", + Aliases: []string{"P"}, + Value: PIDFile, + Usage: "Custom pid file path", + }, + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display Fatal logging errors until logging is set-up", + }, + &cli.BoolFlag{ + Name: "verbose", + Usage: "Set initial logging to TRACE level until logging is properly set-up", + }, }, - &cli.StringFlag{ - Name: "install-port", - Value: "3000", - Usage: "Temporary port number to run the install page on to prevent conflict", - }, - &cli.StringFlag{ - Name: "pid", - Aliases: []string{"P"}, - Value: PIDFile, - Usage: "Custom pid file path", - }, - &cli.BoolFlag{ - Name: "quiet", - Aliases: []string{"q"}, - Usage: "Only display Fatal logging errors until logging is set-up", - }, - &cli.BoolFlag{ - Name: "verbose", - Usage: "Set initial logging to TRACE level until logging is properly set-up", - }, - }, + } } func runHTTPRedirector() { @@ -156,7 +157,6 @@ func serveInstall(cmd *cli.Command) error { case <-graceful.GetManager().IsShutdown(): <-graceful.GetManager().Done() log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() return err default: } @@ -164,8 +164,6 @@ func serveInstall(cmd *cli.Command) error { } func serveInstalled(c *cli.Command) error { - setting.InitCfgProvider(setting.CustomConf) - setting.LoadCommonSettings() setting.MustInstalled() showWebStartupMessage("Prepare to run web server") @@ -231,27 +229,26 @@ func serveInstalled(c *cli.Command) error { err := listen(webRoutes, true) <-graceful.GetManager().Done() log.Info("PID: %d Gitea Web Finished", os.Getpid()) - log.GetManager().Close() return err } func servePprof() { - // FIXME: it shouldn't use the global DefaultServeMux, and it should use a proper context - http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler()) + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + mux.Handle("/debug/fgprof", fgprof.Handler()) + // FIXME: it should use a proper context _, _, finished := process.GetManager().AddTypedContext(context.TODO(), "Web: PProf Server", process.SystemProcessType, true) // The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment, it's not worth introducing a configurable option for it. log.Info("Starting pprof server on localhost:6060") - log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil)) + log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", mux)) finished() } func runWeb(ctx context.Context, cmd *cli.Command) error { - defer func() { - if panicked := recover(); panicked != nil { - log.Fatal("PANIC: %v\n%s", panicked, log.Stack(2)) - } - }() - if subCmdName, valid := isValidDefaultSubCommand(cmd); !valid { return fmt.Errorf("unknown command: %s", subCmdName) } @@ -271,6 +268,10 @@ func runWeb(ctx context.Context, cmd *cli.Command) error { createPIDFile(cmd.String("pid")) } + // init the HTML renderer and load templates, if error happens, it will report the error immediately and exit with error log + // in dev mode, it won't exit, but watch the template files for changes + _ = templates.PageRenderer() + if !setting.InstallLock { if err := serveInstall(cmd); err != nil { return err diff --git a/package/gitea/source/cmd/web_acme.go b/package/gitea/source/cmd/web_acme.go index 5f7a3083..a2e14638 100644 --- a/package/gitea/source/cmd/web_acme.go +++ b/package/gitea/source/cmd/web_acme.go @@ -7,6 +7,7 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "net" "net/http" "os" "strconv" @@ -124,8 +125,8 @@ func runACME(listenAddr string, m http.Handler) error { defer finished() log.Info("Running Let's Encrypt handler on %s", setting.HTTPAddr+":"+setting.PortToRedirect) - // all traffic coming into HTTP will be redirect to HTTPS automatically (LE HTTP-01 validation happens here) - err := runHTTP("tcp", setting.HTTPAddr+":"+setting.PortToRedirect, "Let's Encrypt HTTP Challenge", myACME.HTTPChallengeHandler(http.HandlerFunc(runLetsEncryptFallbackHandler)), setting.RedirectorUseProxyProtocol) + // all traffic coming into HTTP will be redirected to HTTPS automatically (LE HTTP-01 validation happens here) + err := runHTTP("tcp", net.JoinHostPort(setting.HTTPAddr, setting.PortToRedirect), "Let's Encrypt HTTP Challenge", myACME.HTTPChallengeHandler(http.HandlerFunc(runLetsEncryptFallbackHandler)), setting.RedirectorUseProxyProtocol) if err != nil { log.Fatal("Failed to start the Let's Encrypt handler on port %s: %v", setting.PortToRedirect, err) } diff --git a/package/gitea/source/contrib/backport/backport.go b/package/gitea/source/contrib/backport/backport.go index 5811291b..2af980e5 100644 --- a/package/gitea/source/contrib/backport/backport.go +++ b/package/gitea/source/contrib/backport/backport.go @@ -16,7 +16,7 @@ import ( "strconv" "strings" - "github.com/google/go-github/v74/github" + "github.com/google/go-github/v84/github" "github.com/urfave/cli/v3" "gopkg.in/yaml.v3" ) diff --git a/package/gitea/source/contrib/environment-to-ini/README b/package/gitea/source/contrib/environment-to-ini/README deleted file mode 100644 index f1d3f2ae..00000000 --- a/package/gitea/source/contrib/environment-to-ini/README +++ /dev/null @@ -1,47 +0,0 @@ -Environment To Ini -================== - -Multiple docker users have requested that the Gitea docker is changed -to permit arbitrary configuration via environment variables. - -Gitea needs to use an ini file for configuration because the running -environment that starts the docker may not be the same as that used -by the hooks. An ini file also gives a good default and means that -users do not have to completely provide a full environment. - -With those caveats above, this command provides a generic way of -converting suitably structured environment variables into any ini -value. - -To use the command is very simple just run it and the default gitea -app.ini will be rewritten to take account of the variables provided, -however there are various options to give slightly different -behavior and these can be interrogated with the `-h` option. - -The environment variables should be of the form: - - GITEA__SECTION_NAME__KEY_NAME - -Note, SECTION_NAME in the notation above is case-insensitive. - -Environment variables are usually restricted to a reduced character -set "0-9A-Z_" - in order to allow the setting of sections with -characters outside of that set, they should be escaped as following: -"_0X2E_" for "." and "_0X2D_" for "-". The entire section and key names -can be escaped as a UTF8 byte string if necessary. E.g. to configure: - - """ - ... - [log.console] - COLORIZE=false - STDERR=true - ... - """ - -You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false" -and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found -on the configuration cheat sheet. - -To build locally, run: - - go build contrib/environment-to-ini/environment-to-ini.go diff --git a/package/gitea/source/contrib/environment-to-ini/environment-to-ini.go b/package/gitea/source/contrib/environment-to-ini/environment-to-ini.go deleted file mode 100644 index 5eb576c6..00000000 --- a/package/gitea/source/contrib/environment-to-ini/environment-to-ini.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package main - -import ( - "context" - "os" - - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" - - "github.com/urfave/cli/v3" -) - -func main() { - app := cli.Command{} - app.Name = "environment-to-ini" - app.Usage = "Use provided environment to update configuration ini" - app.Description = `As a helper to allow docker users to update the gitea configuration - through the environment, this command allows environment variables to - be mapped to values in the ini. - - Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME" - will be mapped to the ini section "[section_name]" and the key - "KEY_NAME" with the value as provided. - - Environment variables of the form "GITEA__SECTION_NAME__KEY_NAME__FILE" - will be mapped to the ini section "[section_name]" and the key - "KEY_NAME" with the value loaded from the specified file. - - Environment variables are usually restricted to a reduced character - set "0-9A-Z_" - in order to allow the setting of sections with - characters outside of that set, they should be escaped as following: - "_0X2E_" for ".". The entire section and key names can be escaped as - a UTF8 byte string if necessary. E.g. to configure: - - """ - ... - [log.console] - COLORIZE=false - STDERR=true - ... - """ - - You would set the environment variables: "GITEA__LOG_0x2E_CONSOLE__COLORIZE=false" - and "GITEA__LOG_0x2E_CONSOLE__STDERR=false". Other examples can be found - on the configuration cheat sheet.` - app.Flags = []cli.Flag{ - &cli.StringFlag{ - Name: "custom-path", - Aliases: []string{"C"}, - Value: setting.CustomPath, - Usage: "Custom path file path", - }, - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Value: setting.CustomConf, - Usage: "Custom configuration file path", - }, - &cli.StringFlag{ - Name: "work-path", - Aliases: []string{"w"}, - Value: setting.AppWorkPath, - Usage: "Set the gitea working path", - }, - &cli.StringFlag{ - Name: "out", - Aliases: []string{"o"}, - Value: "", - Usage: "Destination file to write to", - }, - } - app.Action = runEnvironmentToIni - err := app.Run(context.Background(), os.Args) - if err != nil { - log.Fatal("Failed to run app with %s: %v", os.Args, err) - } -} - -func runEnvironmentToIni(_ context.Context, c *cli.Command) error { - // the config system may change the environment variables, so get a copy first, to be used later - env := append([]string{}, os.Environ()...) - setting.InitWorkPathAndCfgProvider(os.Getenv, setting.ArgWorkPathAndCustomConf{ - WorkPath: c.String("work-path"), - CustomPath: c.String("custom-path"), - CustomConf: c.String("config"), - }) - - cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf) - if err != nil { - log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) - } - - changed := setting.EnvironmentToConfig(cfg, env) - - // try to save the config file - destination := c.String("out") - if len(destination) == 0 { - destination = setting.CustomConf - } - if destination != setting.CustomConf || changed { - log.Info("Settings saved to: %q", destination) - err = cfg.SaveTo(destination) - if err != nil { - return err - } - } - - return nil -} diff --git a/package/gitea/source/contrib/upgrade.sh b/package/gitea/source/contrib/upgrade.sh index e5e296ea..2593d245 100755 --- a/package/gitea/source/contrib/upgrade.sh +++ b/package/gitea/source/contrib/upgrade.sh @@ -108,7 +108,9 @@ curl --connect-timeout 10 --silent --show-error --fail --location -O "$binurl{,. sha256sum -c "${binname}.xz.sha256" if [[ -z "${ignore_gpg:-}" ]]; then require gpg - gpg --keyserver keys.openpgp.org --recv 7C9E68152594688862D62AF62D9AE806EC1592E2 + # try to use curl first, it uses standard tcp 443 port and works better behind strict firewall rules + curl -fsSL --connect-timeout 10 "https://keys.openpgp.org/vks/v1/by-fingerprint/7C9E68152594688862D62AF62D9AE806EC1592E2" | gpg --import \ + || gpg --keyserver keys.openpgp.org --recv 7C9E68152594688862D62AF62D9AE806EC1592E2 gpg --verify "${binname}.xz.asc" "${binname}.xz" || { echo 'Signature does not match'; exit 1; } fi rm "${binname}".xz.{sha256,asc} @@ -127,6 +129,8 @@ echo "Creating backup in $giteahome" giteacmd dump $backupopts echo "Updating binary at $giteabin" cp -f "$giteabin" "$giteabin.bak" && mv -f "$binname" "$giteabin" +# Restore SELinux context if applicable (e.g. RHEL/Fedora) +command -v restorecon &>/dev/null && restorecon -v "$giteabin" || true $service_start $service_status diff --git a/package/gitea/source/crowdin.yml b/package/gitea/source/crowdin.yml index 35a38d76..d14d484c 100644 --- a/package/gitea/source/crowdin.yml +++ b/package/gitea/source/crowdin.yml @@ -4,9 +4,9 @@ base_path: "." base_url: "https://api.crowdin.com" preserve_hierarchy: true files: - - source: "/options/locale/locale_en-US.ini" - translation: "/options/locale/locale_%locale%.ini" - type: "ini" + - source: "/options/locale/locale_en-US.json" + translation: "/options/locale/locale_%locale%.json" + type: "json" skip_untranslated_strings: true export_only_approved: true update_option: "update_as_unapproved" diff --git a/package/gitea/source/docker/root/etc/s6/openssh/finish b/package/gitea/source/docker/root/etc/s6/openssh/finish index 06bd9865..8e89b35c 100755 --- a/package/gitea/source/docker/root/etc/s6/openssh/finish +++ b/package/gitea/source/docker/root/etc/s6/openssh/finish @@ -1,2 +1,8 @@ #!/bin/bash +# $1 = exit code of the run script, $2 = signal +if [ "$1" -ne 0 ]; then + # avoid immediately restarting the sshd service, which may cause CPU 100% if the error (permission, configuration) is not fixed + echo "openssh failed with exit code $1 - waiting a short delay before attempting a restart" + sleep 3 +fi exit 0 diff --git a/package/gitea/source/docker/root/usr/local/bin/environment-to-ini b/package/gitea/source/docker/root/usr/local/bin/environment-to-ini new file mode 100644 index 00000000..bb0c5406 --- /dev/null +++ b/package/gitea/source/docker/root/usr/local/bin/environment-to-ini @@ -0,0 +1,2 @@ +#!/bin/bash +exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@" diff --git a/package/gitea/source/docker/rootless/usr/local/bin/environment-to-ini b/package/gitea/source/docker/rootless/usr/local/bin/environment-to-ini new file mode 100644 index 00000000..bb0c5406 --- /dev/null +++ b/package/gitea/source/docker/rootless/usr/local/bin/environment-to-ini @@ -0,0 +1,2 @@ +#!/bin/bash +exec /app/gitea/gitea config edit-ini --in-place --apply-env "$@" diff --git a/package/gitea/source/eslint.config.ts b/package/gitea/source/eslint.config.ts index 678a4964..c75f88a5 100644 --- a/package/gitea/source/eslint.config.ts +++ b/package/gitea/source/eslint.config.ts @@ -1,9 +1,9 @@ import arrayFunc from 'eslint-plugin-array-func'; import comments from '@eslint-community/eslint-plugin-eslint-comments'; +import deMorgan from 'eslint-plugin-de-morgan'; import github from 'eslint-plugin-github'; import globals from 'globals'; import importPlugin from 'eslint-plugin-import-x'; -import noUseExtendNative from 'eslint-plugin-no-use-extend-native'; import playwright from 'eslint-plugin-playwright'; import regexp from 'eslint-plugin-regexp'; import sonarjs from 'eslint-plugin-sonarjs'; @@ -16,10 +16,22 @@ import vue from 'eslint-plugin-vue'; import vueScopedCss from 'eslint-plugin-vue-scoped-css'; import wc from 'eslint-plugin-wc'; import {defineConfig, globalIgnores} from 'eslint/config'; +import type {ESLint} from 'eslint'; const jsExts = ['js', 'mjs', 'cjs'] as const; const tsExts = ['ts', 'mts', 'cts'] as const; -const restrictedSyntax = ['WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression']; + +const restrictedGlobals = [ + {name: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'}, + {name: 'fetch', message: 'Use `modules/fetch.ts` instead.'}, +]; + +const restrictedProperties = [ + {object: 'window', property: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'}, + {object: 'globalThis', property: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'}, + {object: 'window', property: 'fetch', message: 'Use `modules/fetch.ts` instead.'}, + {object: 'globalThis', property: 'fetch', message: 'Use `modules/fetch.ts` instead.'}, +]; export default defineConfig([ globalIgnores([ @@ -33,10 +45,6 @@ export default defineConfig([ languageOptions: { ecmaVersion: 'latest', sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - }, parser: typescriptParser, parserOptions: { sourceType: 'module', @@ -49,24 +57,19 @@ export default defineConfig([ }, linterOptions: { reportUnusedDisableDirectives: 2, + reportUnusedInlineConfigs: 2, }, plugins: { '@eslint-community/eslint-comments': comments, - // @ts-expect-error '@stylistic': stylistic, '@typescript-eslint': typescriptPlugin.plugin, 'array-func': arrayFunc, - // @ts-expect-error -- https://github.com/un-ts/eslint-plugin-import-x/issues/203 - 'import-x': importPlugin, - 'no-use-extend-native': noUseExtendNative, - // @ts-expect-error + 'de-morgan': deMorgan, + 'import-x': importPlugin as unknown as ESLint.Plugin, // https://github.com/un-ts/eslint-plugin-import-x/issues/203 regexp, - // @ts-expect-error sonarjs, - // @ts-expect-error unicorn, github, - // @ts-expect-error wc, }, settings: { @@ -75,7 +78,7 @@ export default defineConfig([ 'import-x/resolver': {'typescript': true}, }, rules: { - '@eslint-community/eslint-comments/disable-enable-pair': [2], + '@eslint-community/eslint-comments/disable-enable-pair': [0], '@eslint-community/eslint-comments/no-aggregating-enable': [2], '@eslint-community/eslint-comments/no-duplicate-disable': [2], '@eslint-community/eslint-comments/no-restricted-disable': [0], @@ -155,11 +158,11 @@ export default defineConfig([ '@typescript-eslint/adjacent-overload-signatures': [0], '@typescript-eslint/array-type': [0], '@typescript-eslint/await-thenable': [2], - '@typescript-eslint/ban-ts-comment': [2, {'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}], + '@typescript-eslint/ban-ts-comment': [2, {'ts-expect-error': true, 'ts-ignore': true, 'ts-nocheck': false, 'ts-check': false}], '@typescript-eslint/ban-tslint-comment': [0], '@typescript-eslint/class-literal-property-style': [0], '@typescript-eslint/class-methods-use-this': [0], - '@typescript-eslint/consistent-generic-constructors': [0], + '@typescript-eslint/consistent-generic-constructors': [2, 'constructor'], '@typescript-eslint/consistent-indexed-object-style': [0], '@typescript-eslint/consistent-return': [0], '@typescript-eslint/consistent-type-assertions': [2, {assertionStyle: 'as', objectLiteralTypeAssertions: 'allow'}], @@ -178,7 +181,7 @@ export default defineConfig([ '@typescript-eslint/naming-convention': [0], '@typescript-eslint/no-array-constructor': [2], '@typescript-eslint/no-array-delete': [2], - '@typescript-eslint/no-base-to-string': [0], + '@typescript-eslint/no-base-to-string': [2], '@typescript-eslint/no-confusing-non-null-assertion': [2], '@typescript-eslint/no-confusing-void-expression': [0], '@typescript-eslint/no-deprecated': [2], @@ -210,7 +213,7 @@ export default defineConfig([ '@typescript-eslint/no-non-null-asserted-nullish-coalescing': [0], '@typescript-eslint/no-non-null-asserted-optional-chain': [2], '@typescript-eslint/no-non-null-assertion': [0], - '@typescript-eslint/no-redeclare': [0], + '@typescript-eslint/no-redeclare': [2], '@typescript-eslint/no-redundant-type-constituents': [2], '@typescript-eslint/no-require-imports': [2], '@typescript-eslint/no-restricted-imports': [0], @@ -221,7 +224,7 @@ export default defineConfig([ '@typescript-eslint/no-unnecessary-condition': [0], '@typescript-eslint/no-unnecessary-qualifier': [0], '@typescript-eslint/no-unnecessary-template-expression': [0], - '@typescript-eslint/no-unnecessary-type-arguments': [0], + '@typescript-eslint/no-unnecessary-type-arguments': [2], '@typescript-eslint/no-unnecessary-type-assertion': [2], '@typescript-eslint/no-unnecessary-type-constraint': [2], '@typescript-eslint/no-unnecessary-type-conversion': [2], @@ -234,10 +237,12 @@ export default defineConfig([ '@typescript-eslint/no-unsafe-member-access': [0], '@typescript-eslint/no-unsafe-return': [0], '@typescript-eslint/no-unsafe-unary-minus': [2], - '@typescript-eslint/no-unused-expressions': [0], + '@typescript-eslint/no-unused-expressions': [2], + '@typescript-eslint/no-unused-private-class-members': [2], '@typescript-eslint/no-unused-vars': [2, {vars: 'all', args: 'all', caughtErrors: 'all', ignoreRestSiblings: false, argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_'}], '@typescript-eslint/no-use-before-define': [2, {functions: false, classes: true, variables: true, allowNamedExports: true, typedefs: false, enums: false, ignoreTypeReferences: true}], '@typescript-eslint/no-useless-constructor': [0], + '@typescript-eslint/no-useless-default-assignment': [2], '@typescript-eslint/no-useless-empty-export': [0], '@typescript-eslint/no-wrapper-object-types': [2], '@typescript-eslint/non-nullable-type-assertion-style': [0], @@ -251,10 +256,10 @@ export default defineConfig([ '@typescript-eslint/prefer-function-type': [2], '@typescript-eslint/prefer-includes': [2], '@typescript-eslint/prefer-literal-enum-member': [0], - '@typescript-eslint/prefer-namespace-keyword': [0], + '@typescript-eslint/prefer-namespace-keyword': [2], '@typescript-eslint/prefer-nullish-coalescing': [0], '@typescript-eslint/prefer-optional-chain': [2, {requireNullish: true}], - '@typescript-eslint/prefer-promise-reject-errors': [0], + '@typescript-eslint/prefer-promise-reject-errors': [2], '@typescript-eslint/prefer-readonly': [0], '@typescript-eslint/prefer-readonly-parameter-types': [0], '@typescript-eslint/prefer-reduce-type-parameter': [0], @@ -265,9 +270,10 @@ export default defineConfig([ '@typescript-eslint/require-array-sort-compare': [0], '@typescript-eslint/require-await': [0], '@typescript-eslint/restrict-plus-operands': [2], - '@typescript-eslint/restrict-template-expressions': [0], + '@typescript-eslint/restrict-template-expressions': [2], '@typescript-eslint/return-await': [0], '@typescript-eslint/strict-boolean-expressions': [0], + '@typescript-eslint/strict-void-return': [0], '@typescript-eslint/switch-exhaustiveness-check': [0], '@typescript-eslint/triple-slash-reference': [2], '@typescript-eslint/typedef': [0], @@ -291,6 +297,8 @@ export default defineConfig([ 'consistent-this': [0], 'constructor-super': [2], 'curly': [0], + 'de-morgan/no-negated-conjunction': [2], + 'de-morgan/no-negated-disjunction': [2], 'default-case-last': [2], 'default-case': [0], 'default-param-last': [0], @@ -321,7 +329,7 @@ export default defineConfig([ 'github/no-innerText': [2], 'github/no-then': [2], 'github/no-useless-passive': [2], - 'github/prefer-observers': [2], + 'github/prefer-observers': [0], 'github/require-passive-events': [2], 'github/unescaped-html-literal': [2], 'grouped-accessor-pairs': [2], @@ -338,7 +346,7 @@ export default defineConfig([ 'import-x/first': [2], 'import-x/group-exports': [0], 'import-x/max-dependencies': [0], - 'import-x/named': [2], + 'import-x/named': [0], 'import-x/namespace': [0], 'import-x/newline-after-import': [0], 'import-x/no-absolute-path': [0], @@ -366,7 +374,7 @@ export default defineConfig([ 'import-x/no-self-import': [2], 'import-x/no-unassigned-import': [0], 'import-x/no-unresolved': [2, {commonjs: true, ignore: ['\\?.+$']}], - // 'import-x/no-unused-modules': [2, {unusedExports: true}], // not compatible with eslint 9 + 'import-x/no-unused-modules': [0], // incompatible with eslint 9 'import-x/no-useless-path-segments': [2, {commonjs: true}], 'import-x/no-webpack-loader-syntax': [2], 'import-x/order': [0], @@ -433,7 +441,7 @@ export default defineConfig([ 'no-import-assign': [2], 'no-inline-comments': [0], 'no-inner-declarations': [2], - 'no-invalid-regexp': [2], + 'no-invalid-regexp': [0], // handled by regexp/no-invalid-regexp 'no-invalid-this': [0], 'no-irregular-whitespace': [2], 'no-iterator': [2], @@ -547,7 +555,7 @@ export default defineConfig([ 'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval 'no-new-native-nonconstructor': [2], 'no-new-object': [2], - 'no-new-symbol': [2], + 'no-new-symbol': [0], // handled by no-new-native-nonconstructor 'no-new-wrappers': [2], 'no-new': [0], 'no-nonoctal-decimal-escape': [2], @@ -562,9 +570,14 @@ export default defineConfig([ 'no-redeclare': [0], // must be disabled for typescript overloads 'no-regex-spaces': [2], 'no-restricted-exports': [0], - 'no-restricted-globals': [2, 'addEventListener', 'blur', 'close', 'closed', 'confirm', 'defaultStatus', 'defaultstatus', 'error', 'event', 'external', 'find', 'focus', 'frameElement', 'frames', 'history', 'innerHeight', 'innerWidth', 'isFinite', 'isNaN', 'length', 'locationbar', 'menubar', 'moveBy', 'moveTo', 'name', 'onblur', 'onerror', 'onfocus', 'onload', 'onresize', 'onunload', 'open', 'opener', 'opera', 'outerHeight', 'outerWidth', 'pageXOffset', 'pageYOffset', 'parent', 'print', 'removeEventListener', 'resizeBy', 'resizeTo', 'screen', 'screenLeft', 'screenTop', 'screenX', 'screenY', 'scroll', 'scrollbars', 'scrollBy', 'scrollTo', 'scrollX', 'scrollY', 'status', 'statusbar', 'stop', 'toolbar', 'top'], - 'no-restricted-imports': [0], - 'no-restricted-syntax': [2, ...restrictedSyntax, {selector: 'CallExpression[callee.name="fetch"]', message: 'use modules/fetch.ts instead'}], + 'no-restricted-globals': [2, ...restrictedGlobals], + 'no-restricted-properties': [2, ...restrictedProperties], + 'no-restricted-imports': [2, {paths: [ + {name: 'jquery', message: 'Use the global $ instead', allowTypeImports: true}, + {name: 'htmx.org', message: 'Use the global htmx instead', allowTypeImports: true}, + {name: 'idiomorph/htmx', message: 'Loaded in globals.ts', allowTypeImports: true}, + ]}], + 'no-restricted-syntax': [2, 'WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'], 'no-return-assign': [0], 'no-script-url': [2], 'no-self-assign': [2, {props: true}], @@ -577,10 +590,11 @@ export default defineConfig([ 'no-template-curly-in-string': [2], 'no-ternary': [0], 'no-this-before-super': [2], - 'no-throw-literal': [2], + 'no-throw-literal': [0], // handled by @typescript-eslint/only-throw-error 'no-undef-init': [2], 'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time 'no-undefined': [0], + 'no-unassigned-vars': [2], 'no-underscore-dangle': [0], 'no-unexpected-multiline': [2], 'no-unmodified-loop-condition': [2], @@ -589,13 +603,13 @@ export default defineConfig([ 'no-unreachable': [2], 'no-unsafe-finally': [2], 'no-unsafe-negation': [2], - 'no-unused-expressions': [2], + 'no-unused-expressions': [0], // handled by @typescript-eslint/no-unused-expressions 'no-unused-labels': [2], - 'no-unused-private-class-members': [2], + 'no-unused-private-class-members': [0], // handled by @typescript-eslint/no-unused-private-class-members 'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars 'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define - 'no-use-extend-native/no-use-extend-native': [2], - 'no-useless-backreference': [2], + 'no-useless-assignment': [2], + 'no-useless-backreference': [0], // handled by regexp/no-useless-backreference 'no-useless-call': [2], 'no-useless-catch': [2], 'no-useless-computed-key': [2], @@ -603,7 +617,7 @@ export default defineConfig([ 'no-useless-constructor': [2], 'no-useless-escape': [2], 'no-useless-rename': [2], - 'no-useless-return': [2], + 'no-useless-return': [0], // handled by sonarjs/no-redundant-jump 'no-var': [2], 'no-void': [2], 'no-warning-comments': [0], @@ -612,7 +626,7 @@ export default defineConfig([ 'one-var-declaration-per-line': [0], 'one-var': [0], 'operator-assignment': [2, 'always'], - 'operator-linebreak': [2, 'after'], + 'operator-linebreak': [0], // handled by @stylistic/operator-linebreak 'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}], 'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}], 'prefer-destructuring': [0], @@ -621,19 +635,20 @@ export default defineConfig([ 'prefer-numeric-literals': [2], 'prefer-object-has-own': [2], 'prefer-object-spread': [2], - 'prefer-promise-reject-errors': [2, {allowEmptyReject: false}], + 'prefer-promise-reject-errors': [0], // handled by @typescript-eslint/prefer-promise-reject-errors 'prefer-regex-literals': [2], 'prefer-rest-params': [2], 'prefer-spread': [2], 'prefer-template': [2], - 'radix': [2, 'as-needed'], + 'preserve-caught-error': [0], + 'radix': [0], 'regexp/confusing-quantifier': [2], 'regexp/control-character-escape': [2], 'regexp/hexadecimal-escape': [0], 'regexp/letter-case': [0], 'regexp/match-any': [2], 'regexp/negation': [2], - 'regexp/no-contradiction-with-assertion': [0], + 'regexp/no-contradiction-with-assertion': [2], 'regexp/no-control-character': [0], 'regexp/no-dupe-characters-character-class': [2], 'regexp/no-dupe-disjunctions': [2], @@ -649,8 +664,8 @@ export default defineConfig([ 'regexp/no-invisible-character': [2], 'regexp/no-lazy-ends': [2], 'regexp/no-legacy-features': [2], - 'regexp/no-misleading-capturing-group': [0], - 'regexp/no-misleading-unicode-character': [0], + 'regexp/no-misleading-capturing-group': [2], + 'regexp/no-misleading-unicode-character': [2], 'regexp/no-missing-g-flag': [2], 'regexp/no-non-standard-flag': [2], 'regexp/no-obscure-range': [2], @@ -692,7 +707,7 @@ export default defineConfig([ 'regexp/prefer-question-quantifier': [2], 'regexp/prefer-range': [2], 'regexp/prefer-regexp-exec': [2], - 'regexp/prefer-regexp-test': [2], + 'regexp/prefer-regexp-test': [0], // handled by unicorn/prefer-regexp-test 'regexp/prefer-result-array-groups': [0], 'regexp/prefer-set-operation': [2], 'regexp/prefer-star-quantifier': [2], @@ -722,7 +737,7 @@ export default defineConfig([ 'sonarjs/no-empty-collection': [2], 'sonarjs/no-extra-arguments': [2], 'sonarjs/no-gratuitous-expressions': [2], - 'sonarjs/no-identical-conditions': [2], + 'sonarjs/no-identical-conditions': [0], // handled by no-dupe-else-if 'sonarjs/no-identical-expressions': [2], 'sonarjs/no-identical-functions': [2, 5], 'sonarjs/no-ignored-return': [2], @@ -735,7 +750,7 @@ export default defineConfig([ 'sonarjs/no-small-switch': [0], 'sonarjs/no-unused-collection': [2], 'sonarjs/no-use-of-empty-return-value': [2], - 'sonarjs/no-useless-catch': [2], + 'sonarjs/no-useless-catch': [0], // handled by no-useless-catch 'sonarjs/non-existent-operator': [2], 'sonarjs/prefer-immediate-return': [0], 'sonarjs/prefer-object-literal': [0], @@ -751,6 +766,7 @@ export default defineConfig([ 'unicorn/catch-error-name': [0], 'unicorn/consistent-destructuring': [2], 'unicorn/consistent-empty-array-spread': [2], + 'unicorn/consistent-template-literal-escape': [2], 'unicorn/consistent-existence-index-check': [0], 'unicorn/consistent-function-scoping': [0], 'unicorn/custom-error-definition': [0], @@ -762,6 +778,7 @@ export default defineConfig([ 'unicorn/filename-case': [0], 'unicorn/import-index': [0], 'unicorn/import-style': [0], + 'unicorn/isolated-functions': [2, {functions: []}], 'unicorn/new-for-builtins': [2], 'unicorn/no-abusive-eslint-disable': [0], 'unicorn/no-anonymous-default-export': [0], @@ -777,6 +794,7 @@ export default defineConfig([ 'unicorn/no-empty-file': [2], 'unicorn/no-for-loop': [0], 'unicorn/no-hex-escape': [0], + 'unicorn/no-immediate-mutation': [0], 'unicorn/no-instanceof-array': [0], 'unicorn/no-invalid-fetch-options': [2], 'unicorn/no-invalid-remove-event-listener': [2], @@ -800,9 +818,11 @@ export default defineConfig([ 'unicorn/no-unnecessary-await': [2], 'unicorn/no-unnecessary-polyfills': [2], 'unicorn/no-unreadable-array-destructuring': [0], - 'unicorn/no-unreadable-iife': [2], + 'unicorn/no-unreadable-iife': [0], 'unicorn/no-unused-properties': [2], + 'unicorn/no-useless-collection-argument': [2], 'unicorn/no-useless-fallback-in-spread': [2], + 'unicorn/no-useless-iterator-to-array': [2], 'unicorn/no-useless-length-check': [2], 'unicorn/no-useless-promise-resolve-reject': [2], 'unicorn/no-useless-spread': [2], @@ -812,9 +832,9 @@ export default defineConfig([ 'unicorn/number-literal-case': [0], 'unicorn/numeric-separators-style': [0], 'unicorn/prefer-add-event-listener': [2], - 'unicorn/prefer-array-find': [2], - 'unicorn/prefer-array-flat-map': [2], + 'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find 'unicorn/prefer-array-flat': [2], + 'unicorn/prefer-array-flat-map': [2], 'unicorn/prefer-array-index-of': [2], 'unicorn/prefer-array-some': [2], 'unicorn/prefer-at': [0], @@ -829,7 +849,7 @@ export default defineConfig([ 'unicorn/prefer-event-target': [2], 'unicorn/prefer-export-from': [0], 'unicorn/prefer-global-this': [0], - 'unicorn/prefer-includes': [2], + 'unicorn/prefer-includes': [0], // handled by @typescript-eslint/prefer-includes 'unicorn/prefer-json-parse-buffer': [0], 'unicorn/prefer-keyboard-event-key': [2], 'unicorn/prefer-logical-operator-over-ternary': [2], @@ -849,13 +869,15 @@ export default defineConfig([ 'unicorn/prefer-query-selector': [2], 'unicorn/prefer-reflect-apply': [0], 'unicorn/prefer-regexp-test': [2], + 'unicorn/prefer-response-static-json': [2], 'unicorn/prefer-set-has': [0], 'unicorn/prefer-set-size': [2], + 'unicorn/prefer-simple-condition-first': [0], 'unicorn/prefer-spread': [0], 'unicorn/prefer-string-raw': [0], 'unicorn/prefer-string-replace-all': [0], 'unicorn/prefer-string-slice': [0], - 'unicorn/prefer-string-starts-ends-with': [2], + 'unicorn/prefer-string-starts-ends-with': [0], // handled by @typescript-eslint/prefer-string-starts-ends-with 'unicorn/prefer-string-trim-start-end': [2], 'unicorn/prefer-structured-clone': [2], 'unicorn/prefer-switch': [0], @@ -869,6 +891,7 @@ export default defineConfig([ 'unicorn/require-post-message-target-origin': [0], 'unicorn/string-content': [0], 'unicorn/switch-case-braces': [0], + 'unicorn/switch-case-break-position': [2], 'unicorn/template-indent': [2], 'unicorn/text-encoding-identifier-case': [0], 'unicorn/throw-new-error': [2], @@ -900,12 +923,13 @@ export default defineConfig([ 'yoda': [2, 'never'], }, }, - // @ts-expect-error { ...playwright.configs['flat/recommended'], - files: ['tests/e2e/**'], + files: ['tests/e2e/**/*.test.ts'], + languageOptions: {globals: {...globals.nodeBuiltin, ...globals.browser}}, rules: { ...playwright.configs['flat/recommended'].rules, + 'playwright/expect-expect': [0], }, }, { @@ -916,27 +940,19 @@ export default defineConfig([ }, }, extends: [ - // @ts-expect-error vue.configs['flat/recommended'], - // @ts-expect-error - vueScopedCss.configs['flat/recommended'], + vueScopedCss.configs.recommended as any, ], rules: { 'vue/attributes-order': [0], 'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}], 'vue/max-attributes-per-line': [0], 'vue/singleline-html-element-content-newline': [0], - }, - }, - { - files: ['web_src/js/modules/fetch.ts', 'web_src/js/standalone/**/*'], - rules: { - 'no-restricted-syntax': [2, ...restrictedSyntax], + 'vue/require-typed-ref': [2], }, }, { files: ['**/*.test.ts', 'web_src/js/test/setup.ts'], - // @ts-expect-error - https://github.com/vitest-dev/eslint-plugin-vitest/issues/737 plugins: {vitest}, languageOptions: {globals: globals.vitest}, rules: { @@ -960,6 +976,7 @@ export default defineConfig([ 'vitest/no-interpolation-in-snapshots': [0], 'vitest/no-large-snapshots': [0], 'vitest/no-mocks-import': [0], + 'vitest/no-importing-vitest-globals': [2], 'vitest/no-restricted-matchers': [0], 'vitest/no-restricted-vi-methods': [0], 'vitest/no-standalone-expect': [0], @@ -988,42 +1005,24 @@ export default defineConfig([ 'vitest/require-to-throw-message': [0], 'vitest/require-top-level-describe': [0], 'vitest/valid-describe-callback': [2], - 'vitest/valid-expect': [2], + 'vitest/valid-expect': [2, {maxArgs: 2}], + 'vitest/valid-expect-in-promise': [2], 'vitest/valid-title': [2], }, }, - { - files: ['web_src/js/types.ts'], - rules: { - 'import-x/no-unused-modules': [0], - }, - }, { files: ['**/*.d.ts'], rules: { - 'import-x/no-unused-modules': [0], '@typescript-eslint/consistent-type-definitions': [0], '@typescript-eslint/consistent-type-imports': [0], }, }, { - files: ['*.config.*'], - rules: { - 'import-x/no-unused-modules': [0], - }, - }, - { - files: ['web_src/**/*', 'docs/**/*'], - languageOptions: {globals: globals.browser}, + files: ['*', 'tools/**/*', 'tests/**/*'], + languageOptions: {globals: globals.nodeBuiltin}, }, { files: ['web_src/**/*'], - languageOptions: { - globals: { - ...globals.browser, - __webpack_public_path__: true, - process: false, // https://github.com/webpack/webpack/issues/15833 - }, - }, + languageOptions: {globals: {...globals.browser, ...globals.jquery, htmx: false}}, }, ]); diff --git a/package/gitea/source/eslint.json.config.ts b/package/gitea/source/eslint.json.config.ts new file mode 100644 index 00000000..45696fb7 --- /dev/null +++ b/package/gitea/source/eslint.json.config.ts @@ -0,0 +1,30 @@ +import {defineConfig, globalIgnores} from 'eslint/config'; +import json from '@eslint/json'; + +export default defineConfig([ + globalIgnores([ + '**/.venv', + '**/node_modules', + '**/public', + ]), + { + files: ['**/*.json'], + plugins: {json}, + language: 'json/json', + extends: ['json/recommended'], + }, + { + files: [ + 'tsconfig.json', + '.devcontainer/*.json', + '.vscode/*.json', + 'contrib/ide/vscode/*.json', + ], + plugins: {json}, + language: 'json/jsonc', + languageOptions: { + allowTrailingCommas: true, + }, + extends: ['json/recommended'], + }, +]); diff --git a/package/gitea/source/flake.lock b/package/gitea/source/flake.lock index 16a487ba..25ce7939 100644 --- a/package/gitea/source/flake.lock +++ b/package/gitea/source/flake.lock @@ -1,30 +1,12 @@ { "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1755186698, - "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", + "lastModified": 1775036866, + "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", + "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", "type": "github" }, "original": { @@ -36,24 +18,8 @@ }, "root": { "inputs": { - "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/package/gitea/source/flake.nix b/package/gitea/source/flake.nix index 588f608f..7b9fbb19 100644 --- a/package/gitea/source/flake.nix +++ b/package/gitea/source/flake.nix @@ -1,73 +1,94 @@ { inputs = { nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; }; outputs = - { nixpkgs, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem ( - system: - let - pkgs = nixpkgs.legacyPackages.${system}; - in - { - devShells.default = - with pkgs; + { nixpkgs, ... }: + let + supportedSystems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-darwin" + "x86_64-linux" + ]; + + forEachSupportedSystem = + f: + nixpkgs.lib.genAttrs supportedSystems ( + system: let - # only bump toolchain versions here - go = go_1_25; - nodejs = nodejs_24; - python3 = python312; - pnpm = pnpm_10; - - # Platform-specific dependencies - linuxOnlyInputs = lib.optionals pkgs.stdenv.isLinux [ - glibc.static - ]; - - linuxOnlyEnv = lib.optionalAttrs pkgs.stdenv.isLinux { - CFLAGS = "-I${glibc.static.dev}/include"; - LDFLAGS = "-L ${glibc.static}/lib"; + pkgs = import nixpkgs { + inherit system; }; in - pkgs.mkShell ( - { - buildInputs = [ - # generic - git - git-lfs - gnumake - gnused - gnutar - gzip - zip + f { inherit pkgs; } + ); + in + { + devShells = forEachSupportedSystem ( + { pkgs, ... }: + { + default = + let + inherit (pkgs) lib; - # frontend - nodejs - pnpm - cairo - pixman - pkg-config + # only bump toolchain versions here + go = pkgs.go_1_26; + nodejs = pkgs.nodejs_24; + python3 = pkgs.python314; + pnpm = pkgs.pnpm_10; - # linting - python3 - uv + # Platform-specific dependencies + linuxOnlyInputs = lib.optionals pkgs.stdenv.isLinux [ + pkgs.glibc.static + ]; - # backend - go - gofumpt - sqlite - ] - ++ linuxOnlyInputs; + linuxOnlyEnv = lib.optionalAttrs pkgs.stdenv.isLinux { + CFLAGS = "-I${pkgs.glibc.static.dev}/include"; + LDFLAGS = "-L ${pkgs.glibc.static}/lib"; + }; + in + pkgs.mkShell { + packages = + with pkgs; + [ + # generic + git + git-lfs + gnumake + gnused + gnutar + gzip + zip - GO = "${go}/bin/go"; - GOROOT = "${go}/share/go"; + # frontend + nodejs + pnpm + cairo + pixman + pkg-config - TAGS = "sqlite sqlite_unlock_notify"; - STATIC = "true"; - } - // linuxOnlyEnv - ); - } - ); + # linting + python3 + uv + + # backend + go + gofumpt + sqlite + ] + ++ linuxOnlyInputs; + + env = { + GO = "${go}/bin/go"; + GOROOT = "${go}/share/go"; + + TAGS = "sqlite sqlite_unlock_notify"; + STATIC = "true"; + } + // linuxOnlyEnv; + }; + } + ); + }; } diff --git a/package/gitea/source/go.mod b/package/gitea/source/go.mod index 172f848d..a2b35f48 100644 --- a/package/gitea/source/go.mod +++ b/package/gitea/source/go.mod @@ -1,8 +1,6 @@ module code.gitea.io/gitea -go 1.25.0 - -toolchain go1.25.5 +go 1.26.3 // rfc5280 said: "The serial number is an integer assigned by the CA to each certificate." // But some CAs use negative serial number, just relax the check. related: @@ -11,208 +9,201 @@ godebug x509negativeserial=1 require ( code.gitea.io/actions-proto-go v0.4.1 - code.gitea.io/gitea-vet v0.2.3 - code.gitea.io/sdk/gitea v0.22.0 + code.gitea.io/sdk/gitea v0.24.1 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 - connectrpc.com/connect v1.18.1 - gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed + connectrpc.com/connect v1.19.1 + gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a gitea.com/go-chi/cache v0.2.1 gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 - gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 + gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 - github.com/42wim/httpsig v1.2.3 - github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920 + github.com/42wim/httpsig v1.2.4 + github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 - 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/Azure/go-ntlmssp v0.1.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.20.0 - github.com/aws/aws-sdk-go-v2/credentials v1.18.10 - github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2 + github.com/alecthomas/chroma/v2 v2.23.1 + github.com/aws/aws-sdk-go-v2/credentials v1.19.13 + github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb - github.com/blevesearch/bleve/v2 v2.5.3 + github.com/blevesearch/bleve/v2 v2.5.7 github.com/bohde/codel v0.2.0 github.com/buildkite/terminal-to-html/v3 v3.16.8 - github.com/caddyserver/certmagic v0.24.0 + github.com/caddyserver/certmagic v0.25.2 github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20251013092601-6327009efd21 github.com/chi-middleware/proxy v1.1.1 github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 - github.com/djherbis/buffer v1.2.0 - github.com/djherbis/nio/v3 v3.0.1 + github.com/dlclark/regexp2 v1.11.5 github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 github.com/dustin/go-humanize v1.0.1 - github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 + github.com/editorconfig/editorconfig-core-go/v2 v2.6.4 github.com/emersion/go-imap v1.2.1 github.com/emirpasic/gods v1.18.1 github.com/ethantkoenig/rupture v1.0.1 github.com/felixge/fgprof v0.9.5 github.com/fsnotify/fsnotify v1.9.0 github.com/gliderlabs/ssh v0.3.8 - github.com/go-ap/activitypub v0.0.0-20250810115208-cb73b20a1742 - github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 - github.com/go-chi/chi/v5 v5.2.3 + 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-git/go-billy/v5 v5.6.2 - github.com/go-git/go-git/v5 v5.16.3 - github.com/go-ldap/ldap/v3 v3.4.11 - github.com/go-redsync/redsync/v4 v4.13.0 + github.com/go-co-op/gocron/v2 v2.19.1 + github.com/go-enry/go-enry/v2 v2.9.5 + github.com/go-git/go-billy/v5 v5.9.0 + github.com/go-git/go-git/v5 v5.19.0 + github.com/go-ldap/ldap/v3 v3.4.13 + github.com/go-redsync/redsync/v4 v4.16.0 github.com/go-sql-driver/mysql v1.9.3 - github.com/go-webauthn/webauthn v0.13.4 + github.com/go-webauthn/webauthn v0.16.1 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/google/go-github/v74 v74.0.0 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/go-github/v84 v84.0.0 github.com/google/licenseclassifier/v2 v2.0.0 - github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 + 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.9.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/huandu/xstrings v1.5.0 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 - github.com/jhillyerd/enmime v1.3.0 - github.com/json-iterator/go v1.1.12 + github.com/jhillyerd/enmime/v2 v2.3.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/klauspost/compress v1.18.0 + github.com/klauspost/compress v1.18.5 github.com/klauspost/cpuid/v2 v2.3.0 - github.com/lib/pq v1.10.9 + github.com/lib/pq v1.12.1 github.com/markbates/goth v1.82.0 github.com/mattn/go-isatty v0.0.20 - github.com/mattn/go-sqlite3 v1.14.32 - github.com/meilisearch/meilisearch-go v0.33.2 - github.com/mholt/archives v0.1.5-0.20251009205813-e30ac6010726 + github.com/mattn/go-sqlite3 v1.14.38 + github.com/meilisearch/meilisearch-go v0.36.1 + github.com/mholt/archives v0.1.5 github.com/microcosm-cc/bluemonday v1.0.27 - github.com/microsoft/go-mssqldb v1.9.3 - github.com/minio/minio-go/v7 v7.0.95 - github.com/msteinert/pam v1.2.0 + github.com/microsoft/go-mssqldb v1.9.6 + github.com/minio/minio-go/v7 v7.0.99 + github.com/msteinert/pam/v2 v2.1.0 github.com/nektos/act v0.2.63 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/pkg/errors v0.9.1 github.com/pquerna/otp v1.5.0 - github.com/prometheus/client_golang v1.23.0 + github.com/prometheus/client_golang v1.23.2 github.com/quasoft/websspi v1.1.2 - github.com/redis/go-redis/v9 v9.12.1 + github.com/redis/go-redis/v9 v9.18.0 github.com/robfig/cron/v3 v3.0.1 - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/sassoftware/go-rpmutils v0.4.0 github.com/sergi/go-diff v1.4.0 github.com/stretchr/testify v1.11.1 github.com/syndtr/goleveldb v1.0.0 github.com/tstranex/u2f v1.0.0 github.com/ulikunitz/xz v0.5.15 - github.com/urfave/cli-docs/v3 v3.0.0-alpha6 + github.com/urfave/cli-docs/v3 v3.1.0 github.com/urfave/cli/v3 v3.4.1 github.com/wneessen/go-mail v0.7.2 github.com/xeipuuv/gojsonschema v1.2.0 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 - github.com/yuin/goldmark-meta v1.1.0 - gitlab.com/gitlab-org/api/client-go v0.142.4 - golang.org/x/crypto v0.45.0 - golang.org/x/image v0.30.0 - golang.org/x/net v0.47.0 - golang.org/x/oauth2 v0.30.0 - golang.org/x/sync v0.18.0 - golang.org/x/sys v0.38.0 - golang.org/x/text v0.31.0 - google.golang.org/grpc v1.75.0 - google.golang.org/protobuf v1.36.8 - gopkg.in/ini.v1 v1.67.0 + gitlab.com/gitlab-org/api/client-go v1.46.0 + go.yaml.in/yaml/v4 v4.0.0-rc.3 + golang.org/x/crypto v0.50.0 + golang.org/x/image v0.38.0 + golang.org/x/net v0.53.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.20.0 + golang.org/x/sys v0.44.0 + golang.org/x/text v0.36.0 + google.golang.org/grpc v1.79.3 + google.golang.org/protobuf v1.36.11 + gopkg.in/ini.v1 v1.67.1 gopkg.in/yaml.v3 v3.0.1 mvdan.cc/xurls/v2 v2.6.0 - strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 + strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab xorm.io/builder v0.3.13 - xorm.io/xorm v1.3.10 + xorm.io/xorm v1.3.11 ) require ( - cloud.google.com/go/compute/metadata v0.8.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + code.gitea.io/gitea-vet v0.2.3 // indirect dario.cat/mergo v1.0.2 // indirect - filippo.io/edwards25519 v1.1.0 // indirect - git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/DataDog/zstd v1.5.7 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect + github.com/RoaringBitmap/roaring/v2 v2.16.0 // indirect github.com/STARRY-S/zip v0.2.3 // indirect - github.com/andybalholm/brotli v1.2.0 // indirect + github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect - github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect - github.com/aws/smithy-go v1.23.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/smithy-go v1.24.2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bits-and-blooms/bitset v1.24.0 // indirect - github.com/blevesearch/bleve_index_api v1.2.9 // indirect - github.com/blevesearch/geo v0.2.4 // indirect - github.com/blevesearch/go-faiss v1.0.25 // indirect + github.com/bits-and-blooms/bitset v1.24.4 // indirect + github.com/blevesearch/bleve_index_api v1.3.7 // indirect + github.com/blevesearch/geo v0.2.5 // indirect + github.com/blevesearch/go-faiss v1.0.30 // 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.11 // indirect + github.com/blevesearch/mmap-go v1.2.0 // indirect + github.com/blevesearch/scorch_segment_api/v2 v2.4.5 // 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.4 // indirect - github.com/bmatcuk/doublestar/v4 v4.9.1 // 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.2 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.0 // 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.1.0 // 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.1 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/couchbase/go-couchbase v0.1.1 // indirect - github.com/couchbase/gomemcached v0.3.3 // indirect - github.com/couchbase/goutils v0.1.2 // indirect + github.com/couchbase/gomemcached v0.3.4 // indirect + github.com/couchbase/goutils v0.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // 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/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 // indirect - github.com/go-ap/errors v0.0.0-20250527110557-c8db454e53fd // 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-fed/httpsig v1.1.1-0.20201223112313-55836744818e // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-ini/ini v1.67.0 // indirect - github.com/go-webauthn/x v0.1.24 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/go-webauthn/x v0.2.2 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/flatbuffers v25.2.10+incompatible // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/google/go-tpm v0.9.5 // indirect + github.com/google/flatbuffers v25.12.19+incompatible // indirect + github.com/google/go-querystring v1.2.0 // 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 @@ -220,53 +211,56 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/inbucket/html2text v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/kevinburke/ssh_config v1.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.6.0 // indirect + github.com/klauspost/crc32 v1.3.0 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/libdns/libdns v1.1.1 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/mailru/easyjson v0.9.2 // 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.16 // indirect + github.com/mattn/go-runewidth v0.0.21 // 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.68 // 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.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/minio/minlz v1.1.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 github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nwaples/rardecode/v2 v2.2.0 // indirect - github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 // indirect - github.com/olekukonko/errors v1.1.0 // indirect - github.com/olekukonko/ll v0.1.0 // indirect - github.com/olekukonko/tablewriter v1.0.9 // indirect + github.com/nwaples/rardecode/v2 v2.2.2 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.2.0 // indirect + github.com/olekukonko/ll v0.1.8 // indirect + github.com/olekukonko/tablewriter v1.1.4 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/philhofer/fwd v1.2.0 // indirect - github.com/pierrec/lz4/v4 v4.1.22 // indirect - github.com/pjbgf/sha1cd v0.4.0 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.65.0 // indirect - github.com/prometheus/procfs v0.17.0 // indirect - github.com/rhysd/actionlint v1.7.7 // indirect - github.com/rivo/uniseg v0.4.7 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/rhysd/actionlint v1.7.12 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/skeema/knownhosts v1.3.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/skeema/knownhosts v1.3.2 // 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.4.0 // indirect + github.com/tinylib/msgp v1.6.3 // indirect github.com/unknwon/com v1.0.1 // indirect - github.com/valyala/fastjson v1.6.4 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -277,16 +271,16 @@ require ( 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 - go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/mod v0.29.0 // indirect - golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.38.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + go4.org v0.0.0-20260112195520-a5071408f32f // 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 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) ignore ( @@ -294,18 +288,22 @@ ignore ( ./node_modules ) -replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 +// When doing "go get -u ./...", Golang will try to update all dependencies +// But not all latest versions of dependencies are compatible with other packages or our codebase, so we need to pin some dependencies to specific versions +// Need to regularly maintain this list to try to update them to latest versions, especially the TODO ones -replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 +replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 // jaytaylor/html2text is unmaintained -replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 +replace github.com/nektos/act => gitea.com/gitea/act v0.261.10 // gitea maintains its own package -replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 +replace github.com/urfave/cli/v3 => github.com/urfave/cli/v3 v3.4.1 // v3.6.2 breaks -c flag parsing in help commands -exclude github.com/gofrs/uuid v3.2.0+incompatible +replace go.yaml.in/yaml/v4 => go.yaml.in/yaml/v4 v4.0.0-rc.3 // rc.4 changes block scalar serialization, wait for stable release -exclude github.com/gofrs/uuid v4.0.0+incompatible +replace github.com/Azure/azure-sdk-for-go/sdk/azcore => github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 // v1.21.0+ uses API version unsupported by Azurite in CI -exclude github.com/goccy/go-json v0.4.11 +replace github.com/Azure/azure-sdk-for-go/sdk/storage/azblob => github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 // v1.6.4+ uses API version unsupported by Azurite in CI -exclude github.com/satori/go.uuid v1.2.0 +replace github.com/microsoft/go-mssqldb => github.com/microsoft/go-mssqldb v1.9.7 // downgraded with Azure SDK + +tool code.gitea.io/gitea-vet diff --git a/package/gitea/source/go.sum b/package/gitea/source/go.sum index 11abf6ba..5b228a00 100644 --- a/package/gitea/source/go.sum +++ b/package/gitea/source/go.sum @@ -1,60 +1,41 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= -cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -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= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= code.gitea.io/actions-proto-go v0.4.1 h1:l0EYhjsgpUe/1VABo2eK7zcoNX2W44WOnb0MSLrKfls= code.gitea.io/actions-proto-go v0.4.1/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas= code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= -code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0= -code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= +code.gitea.io/sdk/gitea v0.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8= +code.gitea.io/sdk/gitea v0.24.1/go.mod h1:5/77BL3sHneCMEiZaMT9lfTvnnibsYxyO48mceCF3qA= +code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE= +code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM= 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.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= -connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= +connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c= -gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= -gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4= -gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= -gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= -gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +gitea.com/gitea/act v0.261.10 h1:ndwbtuMXXz1dpYF2iwY1/PkgKNETo4jmPXfinTZt8cs= +gitea.com/gitea/act v0.261.10/go.mod h1:oIkqQHvU0lfuIWwcpqa4FmU+t3prA89tgkuHUTsrI2c= +gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a h1:JHoBrfuTSF9Ke9aNfSYj1XRPBHjKPgCApVprnt2Am0M= +gitea.com/go-chi/binding v0.0.0-20260414111559-654cea7ac60a/go.mod h1:FOsLJIMdpiHzBp3Vby6Wfkdw2ppGscrjgU1IC7E4/zQ= gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g= gitea.com/go-chi/cache v0.2.1/go.mod h1:Qic0HZ8hOHW62ETGbonpwz8WYypj9NieU9659wFUJ8Q= gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098 h1:p2ki+WK0cIeNQuqjR98IP2KZQKRzJJiV7aTeMAFwaWo= gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098/go.mod h1:LjzIOHlRemuUyO7WR12fmm18VZIlCAaOt9L3yKw40pk= -gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96 h1:IFDiMBObsP6CZIRaDLd54SR6zPYAffPXiXck5Xslu0Q= -gitea.com/go-chi/session v0.0.0-20240316035857-16768d98ec96/go.mod h1:0iEpFKnwO5dG0aF98O4eq6FMsAiXkNBaDIlUOlq4BtM= +gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e h1:4bugwPyGMLvblEm3pZ8fZProSPVxE4l0UXF2Kv6IJoY= +gitea.com/go-chi/session v0.0.0-20251124165456-68e0254e989e/go.mod h1:KDvcfMUoXfATPHs2mbMoXFTXT45/FAFAS39waz9tPk0= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfruqu7xJgjOIrKVQGiLUZdpKYCZewJ4clqhw= gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU= 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= -github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= -github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920 h1:mWAVGlovzUfREJBhm0GwJnDNu21yRrL9QH9NIzAU3rg= -github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920/go.mod h1:zWxcT7BIWOe05xVJL0VMvO/PJ6RpoCux10heb77H6Q8= -github.com/6543/go-version v1.3.1 h1:HvOp+Telns7HWJ2Xo/05YXQSB2bE0WmVgbHqwMPZT4U= -github.com/6543/go-version v1.3.1/go.mod h1:oqFAHCwtLVUTLdhQmVZWYvaHXTdsbB4SY85at64SQEo= +github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU= +github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps= +github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y= +github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432/go.mod h1:BLWe6Nol65Xxncvaw07yYMxiyk02We1lBrbRYsMYsjE= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0 h1:ci6Yd6nysBRLEodoziB6ah1+YOzZbZk+NYneoA6q+6E= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= @@ -69,12 +50,11 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuo github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZYLBR2kBz5C8Tg0fw5w5Y7meRXWI= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= +github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 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/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= @@ -83,14 +63,14 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347 h1:3JhDl+JysaO8nhNU1XMaw35VSGjV4IEQAefaG4Lyok4= github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347/go.mod h1:2ErI0aycD43Ufr6CFK5lT/NrHGmoZuVbn1nlPThw69o= -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/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 v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= github.com/RoaringBitmap/roaring v0.7.1/go.mod h1:jdT9ykXwHFNdJbEtxePexlFYH9LXucApeS0/+/g+p1I= -github.com/RoaringBitmap/roaring/v2 v2.10.0 h1:HbJ8Cs71lfCJyvmSptxeMX2PtvOC8yonlU0GQcy2Ak0= -github.com/RoaringBitmap/roaring/v2 v2.10.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= +github.com/RoaringBitmap/roaring/v2 v2.16.0 h1:Kys1UNf49d5W8Tq3bpuAhIr/Z8/yPB+59CO8A6c/BbE= +github.com/RoaringBitmap/roaring/v2 v2.16.0/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.8.0 h1:tgjwQrDH5m6jIYB7kac5IQZmfUzQNseac/e3H4VoCNE= @@ -98,15 +78,15 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R 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/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= -github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +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.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro= +github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= @@ -114,49 +94,48 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk= -github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= -github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg= -github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2 h1:qIySgaSYDLcInLpY0e7HPCi+AVeD/LTsl9EL1b692oA= -github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2/go.mod h1:SobWM1535Mn1WuThoIVLiLa/C1rRbxbbq5PZW2QFCIM= -github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= -github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.13/go.mod h1:yoTXOQKea18nrM69wGF9jBdG4WocSZA1h38A+t/MAsk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12 h1:yv3mfWt/eiDTTry6fkN5hh8wHJfU5ygnw+DJp10C0/c= +github.com/aws/aws-sdk-go-v2/service/codecommit v1.33.12/go.mod h1:voO3LP/dZ4CTERiNWCz3DFLbK/8hbfeC1OJkLW+sang= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 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.1.10/go.mod h1:w0XsmFg8qg6cmpTtJ0z3pKgjTDBMMnI/+I2syrE6XBE= github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= -github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= -github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/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.0.5/go.mod h1:ZjWibgnbRX33c+vBRgla9QhPb4QOjD6fdVJ+R1Bk8LM= -github.com/blevesearch/bleve/v2 v2.5.3 h1:9l1xtKaETv64SZc1jc4Sy0N804laSa/LeMbYddq1YEM= -github.com/blevesearch/bleve/v2 v2.5.3/go.mod h1:Z/e8aWjiq8HeX+nW8qROSxiE0830yQA071dwR3yoMzw= +github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8= +github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA= github.com/blevesearch/bleve_index_api v1.0.0/go.mod h1:fiwKS0xLEm+gBRgv5mumf0dhgFr2mDgZah1pqv1c1M4= -github.com/blevesearch/bleve_index_api v1.2.9 h1:WqD3kvYwnlYLv8sTdH+AF7n/L4v969Cek68+wZnYj4Q= -github.com/blevesearch/bleve_index_api v1.2.9/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.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U= -github.com/blevesearch/go-faiss v1.0.25/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk= +github.com/blevesearch/bleve_index_api v1.3.7 h1:x3C4pl6o3MlSJ3rNKLQpZMRF0uR9+BDvmF3t8LYCOpk= +github.com/blevesearch/bleve_index_api v1.3.7/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.0.30 h1:pWX3/Si4Z7GlwsD2eRXoF3SfVaDkg8plBlPdUKuhGts= +github.com/blevesearch/go-faiss v1.0.30/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.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA= -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/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.0.1/go.mod h1:lq7yK2jQy1yQjtjTfU931aVqz7pYxEudHaDwOt1tXfU= -github.com/blevesearch/scorch_segment_api/v2 v2.3.11 h1:bYuEgsyGqgU/gy0/Vk6g1eCUqGBs2r+3bRCv+Cnq2kc= -github.com/blevesearch/scorch_segment_api/v2 v2.3.11/go.mod h1:aAWoeQ3DdoZ3Z5138jXVSd1T/klGwvg11z0pSxrJSEk= +github.com/blevesearch/scorch_segment_api/v2 v2.4.5 h1:Q7Bzpyk86xS22TgTd4VQfSvzZAybDEJ90hNOGqyNqlI= +github.com/blevesearch/scorch_segment_api/v2 v2.4.5/go.mod h1:xWYn3EwRM7zBFAPt/J136OugUNzftpYLvPBBx31IpCw= github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ= 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= @@ -167,27 +146,27 @@ github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMG github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ= github.com/blevesearch/vellum v1.0.3/go.mod h1:2u5ax02KeDuNWu4/C+hVQMD6uLN4txH1JbtpaDNLJRo= github.com/blevesearch/vellum v1.0.4/go.mod h1:cMhywHI0de50f7Nj42YgvyD6bFJ2WkNRvNBlNMrEVgY= -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/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0= +github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k= github.com/blevesearch/zapx/v11 v11.2.0/go.mod h1:gN/a0alGw1FZt/YGTo1G6Z6XpDkeOfujX5exY9sCQQM= -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/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.2.0/go.mod h1:fdjwvCwWWwJW/EYTYGtAp3gBA0geCYGLcVTtJEZnY6A= -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/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.2.0/go.mod h1:o5rAy/lRS5JpAbITdrOHBS/TugWYbkcYZTz6VfEinAQ= -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/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.2.0/go.mod h1:GNgZusc1p4ot040cBQMRGEZobvwjCquiEKYh1xLFK9g= -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/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.2.0/go.mod h1:MmQceLpWfME4n1WrBFIwplhWmaQbQqLQARpaKUEOs/A= -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.4 h1:tGgfvleXTAkwsD5mEzgM3zCS/7pgocTCnO1oyAUjlww= -github.com/blevesearch/zapx/v16 v16.2.4/go.mod h1:Rti/REtuuMmzwsI8/C/qIzRaEoSK/wiFYw5e5ctUKKs= -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/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.2 h1:R8alnAeYxzKO4JjQY4FBqJduJwcPE3YjcgevWgI+XT0= +github.com/blevesearch/zapx/v16 v16.3.2/go.mod h1:zCFjv7McXWm1C8rROL+3mUoD5WYe2RKsZP3ufqcYpLY= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= @@ -208,11 +187,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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc= +github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg= +github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE= +github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= 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= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -224,32 +202,32 @@ github.com/chi-middleware/proxy v1.1.1/go.mod h1:jQwMEJct2tz9VmtCELxvnXoMfa+SOdi github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k= github.com/couchbase/go-couchbase v0.1.1 h1:ClFXELcKj/ojyoTYbsY34QUrrYCBi/1G749sXSCkdhk= github.com/couchbase/go-couchbase v0.1.1/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= -github.com/couchbase/gomemcached v0.3.3 h1:D7qqXLO8wNa4pn5oE65lT3pA3IeStn4joT7/JgGXzKc= -github.com/couchbase/gomemcached v0.3.3/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68= -github.com/couchbase/goutils v0.1.2 h1:gWr8B6XNWPIhfalHNog3qQKfGiYyh4K4VhO3P2o9BCs= +github.com/couchbase/gomemcached v0.3.4 h1:VGdrZUJbt5lLyI/MXnyVCZKHKYXg/vaud08lJIAeZps= +github.com/couchbase/gomemcached v0.3.4/go.mod h1:pISAjweI42vljCumsJIo7CVhqIMIIP9g3Wfhl1JJw68= github.com/couchbase/goutils v0.1.2/go.mod h1:h89Ek/tiOxxqjz30nPPlwZdQbdB8BwgnuBxeoUe/ViE= +github.com/couchbase/goutils v0.3.0 h1:rsv72B6BDjW9jmwlfiDUrdu3EpNvPuo5WLULHzQ0DLE= +github.com/couchbase/goutils v0.3.0/go.mod h1:7Gm+D3vXfV4HS+hQWvKfy6e6ILCptGXNqBKvQXhplhk= github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -260,11 +238,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21 h1:PdsjTl0Cg+ZJgOx/CFV5NNgO1ThTreqdgKYiDCMHJwA= github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21/go.mod h1:xJvkyD6Y2rZapGvPJLYo9dyx1s5dxBEDPa8T3YTuOk0= -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= -github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4= -github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= @@ -276,8 +249,8 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvyukov/go-fuzz v0.0.0-20210429054444-fca39067bc72/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 h1:XVUp6qW3BIkmM3/1EkrHpa6bL56APOynfXcZEmIgOhs= -github.com/editorconfig/editorconfig-core-go/v2 v2.6.3/go.mod h1:ThHVc+hqbUsmE1wmK/MASpQEhCleWu1JDJDNhUOMy0c= +github.com/editorconfig/editorconfig-core-go/v2 v2.6.4 h1:CHwUbBVVyKWRX9kt5A/OtwhYUJB32DrFp9xzmjR6cac= +github.com/editorconfig/editorconfig-core-go/v2 v2.6.4/go.mod h1:JWRVKHdVW+dkv6F8p+xGCa6a+TyMrqsFbFkSs/aQkrQ= github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= @@ -290,12 +263,10 @@ github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTe github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ethantkoenig/rupture v1.0.1 h1:6aAXghmvtnngMgQzy7SMGdicMvkV86V4n9fT0meE5E4= github.com/ethantkoenig/rupture v1.0.1/go.mod h1:Sjqo/nbffZp1pVVXNGhpugIjsWmuS9KiIB4GtpEBur4= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -304,97 +275,81 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo 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/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/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1 h1:mtDjlmloH7ytdblogrMz1/8Hqua1y8B4ID+bh3rvod0= github.com/git-lfs/pktline v0.0.0-20230103162542-ca444d533ef1/go.mod h1:fenKRzpXDjNpsIBhuhUzvjCKlDjKam0boRAenTE0Q6A= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= -github.com/go-ap/activitypub v0.0.0-20250810115208-cb73b20a1742 h1:X+SsQlZSgJO0A4d1+nI7+g4axZ8u3iUKPirYb5nB5ic= -github.com/go-ap/activitypub v0.0.0-20250810115208-cb73b20a1742/go.mod h1:0rgUaERG5qjYenwz4oN5OnUjvkdRuHRjb+2c8FRjz+w= -github.com/go-ap/errors v0.0.0-20250527110557-c8db454e53fd h1:fM5mNIWTPoxoOYoTLd6ifkKXSlXa830l5MYXsrt1UmE= -github.com/go-ap/errors v0.0.0-20250527110557-c8db454e53fd/go.mod h1:Vkh+Z3f24K8nMsJKXo1FHn5ebPsXvB/WDH5JRtYqdNo= -github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw= -github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA= 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.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= -github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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-co-op/gocron/v2 v2.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI= +github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= +github.com/go-enry/go-enry/v2 v2.9.5 h1:HPhAQQHYwJgihL2PxBZiUMFWiROsGwOBdB6/D8zCUhY= +github.com/go-enry/go-enry/v2 v2.9.5/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-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/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.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= -github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8= -github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= -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-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc= +github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= -github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= +github.com/go-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.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= +github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA= -github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ= +github.com/go-redsync/redsync/v4 v4.16.0 h1:bNcOzeHH9d3s6pghU9NJFMPrQa41f5Nx3L4YKr3BdEU= +github.com/go-redsync/redsync/v4 v4.16.0/go.mod h1:V4gagqgyASWBZuwx4xGzu72aZNb/6Mo05byUa3mVmKQ= 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-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= -github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-webauthn/webauthn v0.13.4 h1:q68qusWPcqHbg9STSxBLBHnsKaLxNO0RnVKaAqMuAuQ= -github.com/go-webauthn/webauthn v0.13.4/go.mod h1:MglN6OH9ECxvhDqoq1wMoF6P6JRYDiQpC9nc5OomQmI= -github.com/go-webauthn/x v0.1.24 h1:6LaWf2zzWqbyKT8IyQkhje1/1KCGhlEkMz4V1tDnt/A= -github.com/go-webauthn/x v0.1.24/go.mod h1:2o5XKJ+X1AKqYKGgHdKflGnoQFQZ6flJ2IFCBKSbSOw= +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-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.1 h1:x5/SSki5/aIfogaRukqvbg/RXa3Sgxy/9vU7UfFPHKU= +github.com/go-webauthn/webauthn v0.16.1/go.mod h1:RBS+rtQJMkE5VfMQ4diDA2VNrEL8OeUhp4Srz37FHbQ= +github.com/go-webauthn/x v0.2.2 h1:zIiipvMbr48CXi5RG0XdBJR94kd8I5LfzHPb/q+YYmk= +github.com/go-webauthn/x v0.2.2/go.mod h1:IpJ5qyWB9NRhLX3C7gIfjTU7RZLXEP6kzFkoVSE7Fz4= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= 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-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -407,16 +362,13 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8l github.com/golang/snappy v0.0.1/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/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= -github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= -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/gomodule/redigo v1.9.3 h1:dNPSXeXv6HCq2jdyWfjgmhBdqnR6PRO3m/G05nvpPC8= +github.com/gomodule/redigo v1.9.3/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= 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/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= -github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs= +github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 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= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -425,30 +377,24 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM= -github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak= -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-github/v84 v84.0.0 h1:I/0Xn5IuChMe8TdmI2bbim5nyhaRFJ7DEdzmD2w+yVA= +github.com/google/go-github/v84 v84.0.0/go.mod h1:WwYL1z1ajRdlaPszjVu/47x1L0PXukJBn73xsiYrRRQ= +github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= +github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= +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= github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA= github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -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-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= -github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= -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/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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw= github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -468,6 +414,8 @@ github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pw github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/graph-gophers/graphql-go v1.9.0 h1:yu0ucKHLc5qGpRwLYKIWtr9bOoxovkWasuBrPQwlHls= +github.com/graph-gophers/graphql-go v1.9.0/go.mod h1:23olKZ7duEvHlF/2ELEoSZaY1aNPfShjP782SOoNTyM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -481,8 +429,8 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -491,8 +439,9 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 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 v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs= +github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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= @@ -508,35 +457,34 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= -github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw= -github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs= +github.com/jhillyerd/enmime/v2 v2.3.0 h1:Y/pzQanyU8nkSgB2npXX8Dha5OItJE/QwbDJM4sf/kU= +github.com/jhillyerd/enmime/v2 v2.3.0/go.mod h1:mGKXAP45l6pF6HZiaLhgSYsgteJskaSIYmEZXpw6ZpI= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= +github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/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.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/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -546,14 +494,18 @@ 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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +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/lib/pq v1.12.1 h1:x1nbl/338GLqeDJ/FAiILallhAsqubLzEZu/pXtHUow= +github.com/lib/pq v1.12.1/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= 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/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= +github.com/mailru/easyjson v0.9.2/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.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ= @@ -562,38 +514,36 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP 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-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 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.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/meilisearch/meilisearch-go v0.33.2 h1:YgsQSLYhAkRN2ias6I1KNRTjdYCN5w2uHbLUQ+xgrws= -github.com/meilisearch/meilisearch-go v0.33.2/go.mod h1:6eOPcQ+OAuwXvnONlfSgfgvr7TIAWM/6OdhcVHg8cF0= -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/mholt/archives v0.1.5-0.20251009205813-e30ac6010726 h1:WVjGWXBLI1Ggm2kHzNraCGgxFhLoK6gdpPSizCdxnx0= -github.com/mholt/archives v0.1.5-0.20251009205813-e30ac6010726/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= +github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4= +github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/meilisearch/meilisearch-go v0.36.1 h1:mJTCJE5g7tRvaqKco6DfqOuJEjX+rRltDEnkEC02Y0M= +github.com/meilisearch/meilisearch-go v0.36.1/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/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs= -github.com/microsoft/go-mssqldb v1.9.3/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA= -github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= -github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= +github.com/microsoft/go-mssqldb v1.9.7 h1:I+JEk79gYsc6bdVzDHFSSYE9dtNa7dxRwJ0WQbt6i8w= +github.com/microsoft/go-mssqldb v1.9.7/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4= +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.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/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= -github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/minio/minio-go/v7 v7.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE= +github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw= +github.com/minio/minlz v1.1.0 h1:rUOGu3EP4EqJC5k3qCsIwEnZiJULKqtRyDdqbhlvMmQ= +github.com/minio/minlz v1.1.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/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= @@ -604,25 +554,27 @@ github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkF github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= -github.com/msteinert/pam v1.2.0 h1:mYfjlvN2KYs2Pb9G6nb/1f/nPfAttT/Jee5Sq9r3bGE= -github.com/msteinert/pam v1.2.0/go.mod h1:d2n0DCUK8rGecChV3JzvmsDjOY4R7AYbsNxAT+ftQl0= +github.com/msteinert/pam/v2 v2.1.0 h1:er5F9TKV5nGFuTt12ubtqPHEUdeBwReP7vd3wovidGY= +github.com/msteinert/pam/v2 v2.1.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0= github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48= -github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= -github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU= +github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00 h1:ZCnkxe9GgWqqBxAk3cIKlQJuaqgOUF/nUtQs8flVTHM= -github.com/olekukonko/cat v0.0.0-20250817074551-3280053e4e00/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= -github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.0 h1:7nX5bgpvfyxsvI90IJpOIU5zd4MBV6nRkD49e/dEx98= -github.com/olekukonko/ll v0.1.0/go.mod h1:2dJo+hYZcJMLMbKwHEWvxCUbAOLc/CXWS9noET22Mdo= -github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8= -github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= +github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= +github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= +github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -644,13 +596,12 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY= -github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -658,35 +609,30 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= -github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= -github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quasoft/websspi v1.1.2 h1:/mA4w0LxWlE3novvsoEL6BBA1WnjJATbjkh1kFrTidw= github.com/quasoft/websspi v1.1.2/go.mod h1:HmVdl939dQ0WIXZhyik+ARdI03M6bQzaSEKcgpFmewk= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg= -github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= -github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= -github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k= -github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/redis/rueidis v1.0.71 h1:pODtnAR5GAB7j4ekhldZ29HKOxe4Hph0GTDGk1ayEQY= +github.com/redis/rueidis v1.0.71/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ= +github.com/redis/rueidis/rueidiscompat v1.0.71 h1:wNZ//kEjMZgBM0KCk7ncOX8KmAgROU2kDdDNpwheG4w= +github.com/redis/rueidis/rueidiscompat v1.0.71/go.mod h1:esmCLJvaRzZoKlgB82G1bY7Iky5TnO9Rz+NlhbEccFI= +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.12 h1:vQ4GeJN86C0QH+gTUQcs8McmK62OLT3kmakPMtEWYnY= +github.com/rhysd/actionlint v1.7.12/go.mod h1:krOUhujIsJusovkaYzQ/VNH8PFexjNKqU0q5XI/4w+g= github.com/robertkrimen/godocdown v0.0.0-20130622164427-0bfa04905481/go.mod h1:C9WhFzY47SzYBIvzFqSvHIR6ROgDo4TtdTuRaOMjF/s= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -694,20 +640,21 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= -github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= +github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.1 h1:T/YLemO5Yp7KPzS+lVtu+WsHn8yoSwTfItdAd1r3cck= github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= @@ -741,7 +688,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -751,8 +697,8 @@ github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKN github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8= -github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o= +github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s= +github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA= github.com/tstranex/u2f v1.0.0 h1:HhJkSzDDlVSVIVt7pDJwCHQj67k7A5EeBgPmeD+pVsQ= github.com/tstranex/u2f v1.0.0/go.mod h1:eahSLaqAS0zsIEv80+vXT7WanXs7MQQDg3j3wGBSayo= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= @@ -761,12 +707,10 @@ 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/unknwon/com v1.0.1 h1:3d1LTxD+Lnf3soQiD4Cp/0BRB+Rsa/+RTvz8GMMzIXs= github.com/unknwon/com v1.0.1/go.mod h1:tOOxU81rwgoCLoOVVPHb6T/wt8HZygqH5id+GNnlCXM= -github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= -github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU= +github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= +github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= -github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= -github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8= github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= @@ -792,44 +736,45 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de 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/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= -github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 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= -gitlab.com/gitlab-org/api/client-go v0.142.4 h1:tTm+hUPrOcTavmKpM9YIP503IE0EdAkg4TG3t6QGbiw= -gitlab.com/gitlab-org/api/client-go v0.142.4/go.mod h1:Ru5IRauphXt9qwmTzJD7ou1dH7Gc6pnsdFWEiMMpmB0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +gitlab.com/gitlab-org/api/client-go v1.46.0 h1:YxBWFZIFYKcGESCb9fpkwzouo+apyB9pr/XTWzNoL24= +gitlab.com/gitlab-org/api/client-go v1.46.0/go.mod h1:FtgyU6g2HS5+fMhw6nLK96GBEEBx5MzntOiJWfIaiN8= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +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= -go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= -go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= +go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= +go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= +go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= @@ -840,36 +785,13 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf 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.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -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= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -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-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= -golang.org/x/image v0.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.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= -golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c= -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= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +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/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -878,22 +800,11 @@ 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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -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/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -908,19 +819,11 @@ 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -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.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +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= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -932,30 +835,20 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -975,8 +868,8 @@ 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.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.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= @@ -987,12 +880,9 @@ 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.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= 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= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -1002,36 +892,13 @@ 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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +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= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -1039,66 +906,31 @@ 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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +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= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823 h1:YedBIttDguBl/zy2wJauEUm+DZZg4UXseWj0g/3N+yo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401020348-3a24fdc17823/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -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/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= +gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= 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= @@ -1108,47 +940,37 @@ 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= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -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= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= -modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= -modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk= -modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= +modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE= modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= -modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= -modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= pgregory.net/rapid v0.4.2 h1:lsi9jhvZTYvzVpeG93WWgimPRmiJQfGFRNTEZh1dtY0= pgregory.net/rapid v0.4.2/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU= -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= -strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251 h1:mUcz5b3FJbP5Cvdq7Khzn6J9OCUQJaBwgBkCR+MOwSs= -strk.kbt.io/projects/go/libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= +strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab h1:3IZDVyI8uBmZko6pwm39f7mE0aTY7LViNdQHMeH7U60= +strk.kbt.io/projects/go/libravatar v0.0.0-20260301104140-add494e31dab/go.mod h1:FJGmPh3vz9jSos1L/F91iAgnC/aejc0wIIrF2ZwJxdY= xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= -xorm.io/xorm v1.3.10 h1:yR83hTT4mKIPyA/lvWFTzS35xjLwkiYnwdw0Qupeh0o= -xorm.io/xorm v1.3.10/go.mod h1:Lo7hmsFF0F0GbDE7ubX5ZKa+eCf0eCuiJAUG3oI5cxQ= +xorm.io/xorm v1.3.11 h1:i4tlVUASogb0ZZFJHA7dZqoRU2pUpUsutnNdaOlFyMI= +xorm.io/xorm v1.3.11/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q= diff --git a/package/gitea/source/main.go b/package/gitea/source/main.go index 2c25bac4..fcfbb733 100644 --- a/package/gitea/source/main.go +++ b/package/gitea/source/main.go @@ -26,9 +26,8 @@ import ( // these flags will be set by the build flags var ( - Version = "development" // program version for this build - Tags = "" // the Golang build tags - MakeVersion = "" // "make" program version if built with make + Version = "development" // program version for this build + Tags = "" // the Golang build tags ) func init() { @@ -44,14 +43,12 @@ func main() { } app := cmd.NewMainApp(cmd.AppVersion{Version: Version, Extra: formatBuiltWith()}) _ = cmd.RunMainApp(app, os.Args...) // all errors should have been handled by the RunMainApp + // flush the queued logs before exiting, it is a MUST, otherwise there will be log loss log.GetManager().Close() } func formatBuiltWith() string { version := runtime.Version() - if len(MakeVersion) > 0 { - version = MakeVersion + ", " + runtime.Version() - } if len(Tags) == 0 { return " built with " + version } diff --git a/package/gitea/source/models/actions/artifact.go b/package/gitea/source/models/actions/artifact.go index 757bd13a..d61afb2a 100644 --- a/package/gitea/source/models/actions/artifact.go +++ b/package/gitea/source/models/actions/artifact.go @@ -53,6 +53,11 @@ func init() { db.RegisterModel(new(ActionArtifact)) } +const ( + ContentEncodingV3Gzip = "gzip" + ContentTypeZip = "application/zip" +) + // ActionArtifact is a file that is stored in the artifact storage. type ActionArtifact struct { ID int64 `xorm:"pk autoincr"` @@ -61,16 +66,26 @@ type ActionArtifact struct { RepoID int64 `xorm:"index"` OwnerID int64 CommitSHA string - StoragePath string // The path to the artifact in the storage - FileSize int64 // The size of the artifact in bytes - FileCompressedSize int64 // The size of the artifact in bytes after gzip compression - ContentEncoding string // The content encoding of the artifact - ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it - ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it - Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete - CreatedUnix timeutil.TimeStamp `xorm:"created"` - UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` - ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired + StoragePath string // The path to the artifact in the storage + FileSize int64 // The size of the artifact in bytes + FileCompressedSize int64 // The size of the artifact in bytes after gzip compression + + // The content encoding or content type of the artifact + // * empty or null: legacy (v3) uncompressed content + // * magic string "gzip" (ContentEncodingV3Gzip): v3 gzip compressed content + // * requires gzip decoding before storing in a zip for download + // * requires gzip content-encoding header when downloaded single files within a workflow + // * mime type for "Content-Type": + // * "application/zip" (ContentTypeZip), seems to be an abuse, fortunately there is no conflict, and it won't cause problems? + // * "application/pdf", "text/html", etc.: real content type of the artifact + ContentEncodingOrType string `xorm:"content_encoding"` + + ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it + ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it + Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated index"` + ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired } func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) { @@ -156,7 +171,8 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond { } if opts.FinalizedArtifactsV4 { cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired})) - cond = cond.And(builder.Eq{"content_encoding": "application/zip"}) + // see the comment of ActionArtifact.ContentEncodingOrType: "*/*" means the field is a content type + cond = cond.And(builder.Like{"content_encoding", "%/%"}) } return cond @@ -170,10 +186,10 @@ type ActionArtifactMeta struct { } // ListUploadedArtifactsMeta returns all uploaded artifacts meta of a run -func ListUploadedArtifactsMeta(ctx context.Context, runID int64) ([]*ActionArtifactMeta, error) { +func ListUploadedArtifactsMeta(ctx context.Context, repoID, 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("repo_id=? AND run_id=? AND (status=? OR status=?)", repoID, runID, ArtifactStatusUploadConfirmed, ArtifactStatusExpired). GroupBy("artifact_name"). Select("artifact_name, sum(file_size) as file_size, max(status) as status"). Find(&arts) diff --git a/package/gitea/source/models/actions/config.go b/package/gitea/source/models/actions/config.go new file mode 100644 index 00000000..4f5357c5 --- /dev/null +++ b/package/gitea/source/models/actions/config.go @@ -0,0 +1,74 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" + + "xorm.io/xorm/convert" +) + +// OwnerActionsConfig defines the Actions configuration for a user or organization +type OwnerActionsConfig struct { + // TokenPermissionMode defines the default permission mode (permissive, restricted) + TokenPermissionMode repo_model.ActionsTokenPermissionMode `json:"token_permission_mode,omitempty"` + + // MaxTokenPermissions defines the absolute maximum permissions any token can have in this context. + MaxTokenPermissions *repo_model.ActionsTokenPermissions `json:"max_token_permissions,omitempty"` + + // AllowedCrossRepoIDs is a list of specific repo IDs that can be accessed cross-repo + AllowedCrossRepoIDs []int64 `json:"allowed_cross_repo_ids,omitempty"` +} + +var _ convert.ConversionFrom = (*OwnerActionsConfig)(nil) + +func (cfg *OwnerActionsConfig) FromDB(bytes []byte) error { + _ = json.Unmarshal(bytes, cfg) + cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode) + return nil +} + +// GetOwnerActionsConfig loads the OwnerActionsConfig for a user or organization from user settings +// It returns a default config if no setting is found +func GetOwnerActionsConfig(ctx context.Context, userID int64) (ret OwnerActionsConfig, err error) { + return user_model.GetUserSettingJSON(ctx, userID, user_model.SettingsKeyActionsConfig, ret) +} + +// SetOwnerActionsConfig saves the OwnerActionsConfig for a user or organization to user settings +func SetOwnerActionsConfig(ctx context.Context, userID int64, cfg OwnerActionsConfig) error { + return user_model.SetUserSettingJSON(ctx, userID, user_model.SettingsKeyActionsConfig, cfg) +} + +// GetDefaultTokenPermissions returns the default token permissions by its TokenPermissionMode. +func (cfg *OwnerActionsConfig) GetDefaultTokenPermissions() repo_model.ActionsTokenPermissions { + switch cfg.TokenPermissionMode { + case repo_model.ActionsTokenPermissionModeRestricted: + return repo_model.MakeRestrictedPermissions() + case repo_model.ActionsTokenPermissionModePermissive: + return repo_model.MakeActionsTokenPermissions(perm.AccessModeWrite) + default: + return repo_model.MakeActionsTokenPermissions(perm.AccessModeNone) + } +} + +// GetMaxTokenPermissions returns the maximum allowed permissions +func (cfg *OwnerActionsConfig) GetMaxTokenPermissions() repo_model.ActionsTokenPermissions { + if cfg.MaxTokenPermissions != nil { + return *cfg.MaxTokenPermissions + } + // Default max is write for everything + return repo_model.MakeActionsTokenPermissions(perm.AccessModeWrite) +} + +// ClampPermissions ensures that the given permissions don't exceed the maximum +func (cfg *OwnerActionsConfig) ClampPermissions(perms repo_model.ActionsTokenPermissions) repo_model.ActionsTokenPermissions { + maxPerms := cfg.GetMaxTokenPermissions() + return repo_model.ClampActionsTokenPermissions(perms, maxPerms) +} diff --git a/package/gitea/source/models/actions/run.go b/package/gitea/source/models/actions/run.go index dd22901d..033b715f 100644 --- a/package/gitea/source/models/actions/run.go +++ b/package/gitea/source/models/actions/run.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "slices" "strings" "time" @@ -16,13 +15,13 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" - "github.com/nektos/act/pkg/jobparser" "xorm.io/builder" ) @@ -30,7 +29,7 @@ import ( type ActionRun struct { ID int64 Title string - RepoID int64 `xorm:"index unique(repo_index)"` + RepoID int64 `xorm:"unique(repo_index) index(repo_concurrency)"` Repo *repo_model.Repository `xorm:"-"` OwnerID int64 `xorm:"index"` WorkflowID string `xorm:"index"` // the name of workflow file @@ -49,6 +48,9 @@ type ActionRun struct { TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow Status Status `xorm:"index"` Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed + RawConcurrency string // raw concurrency + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // Started and Stopped is used for recording last run time, if rerun happened, they will be reset to 0 Started timeutil.TimeStamp Stopped timeutil.TimeStamp @@ -67,14 +69,14 @@ func (run *ActionRun) HTMLURL() string { if run.Repo == nil { return "" } - return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(), run.Index) + return fmt.Sprintf("%s/actions/runs/%d", run.Repo.HTMLURL(), run.ID) } func (run *ActionRun) Link() string { if run.Repo == nil { return "" } - return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) + return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.ID) } func (run *ActionRun) WorkflowLink() string { @@ -102,8 +104,17 @@ func (run *ActionRun) PrettyRef() string { return refName.ShortName() } +// RefTooltip return a tooltop of run's ref. For pull request, it's the title of the PR, otherwise it's the ShortName. +func (run *ActionRun) RefTooltip() string { + payload, err := run.GetPullRequestEventPayload() + if err == nil && payload != nil && payload.PullRequest != nil { + return payload.PullRequest.Title + } + return git.RefName(run.Ref).ShortName() +} + // LoadAttributes load Repo TriggerUser if not loaded -func (run *ActionRun) LoadAttributes(ctx context.Context) error { +func (run *ActionRun) LoadAttributes(ctx context.Context) (err error) { if run == nil { return nil } @@ -117,11 +128,10 @@ func (run *ActionRun) LoadAttributes(ctx context.Context) error { } if run.TriggerUser == nil { - u, err := user_model.GetPossibleUserByID(ctx, run.TriggerUserID) + run.TriggerUserID, run.TriggerUser, err = user_model.GetPossibleUserByID(ctx, run.TriggerUserID) if err != nil { return err } - run.TriggerUser = u } return nil @@ -141,7 +151,11 @@ func (run *ActionRun) LoadRepo(ctx context.Context) error { } func (run *ActionRun) Duration() time.Duration { - return calculateDuration(run.Started, run.Stopped, run.Status) + run.PreviousDuration + d := calculateDuration(run.Started, run.Stopped, run.Status, run.Updated) + run.PreviousDuration + if d < 0 { + return 0 + } + return d } func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { @@ -156,7 +170,7 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) { } func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { - if run.Event.IsPullRequest() { + if run.Event.IsPullRequest() || run.Event.IsPullRequestReview() { var payload api.PullRequestPayload if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { return nil, err @@ -181,30 +195,35 @@ func (run *ActionRun) IsSchedule() bool { return run.ScheduleID > 0 } -func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) error { - _, err := db.GetEngine(ctx).ID(repo.ID). - NoAutoTime(). - Cols("num_action_runs", "num_closed_action_runs"). - SetExpr("num_action_runs", - builder.Select("count(*)").From("action_run"). - Where(builder.Eq{"repo_id": repo.ID}), - ). - SetExpr("num_closed_action_runs", - builder.Select("count(*)").From("action_run"). - Where(builder.Eq{ - "repo_id": repo.ID, - }.And( - builder.In("status", - StatusSuccess, - StatusFailure, - StatusCancelled, - StatusSkipped, - ), - ), - ), - ). - Update(repo) - return err +// UpdateRepoRunsNumbers updates the number of runs and closed runs of a repository. +// Callers MUST invoke this from outside any transaction that has X-locked action_run rows for the same repo, otherwise, transaction deadlock +func UpdateRepoRunsNumbers(ctx context.Context, repoID int64) { + if db.InTransaction(ctx) { + setting.PanicInDevOrTesting("UpdateRepoRunsNumbers must not be called inside a transaction") + } + + e := db.GetEngine(ctx) + + numActionRuns, err := e.Where("repo_id = ?", repoID).Count(new(ActionRun)) + if err != nil { + log.Error("UpdateRepoRunsNumbers count num_action_runs for repo %d: %v", repoID, err) + return + } + + numClosedActionRuns, err := e.Where("repo_id = ?", repoID). + In("status", StatusSuccess, StatusFailure, StatusCancelled, StatusSkipped). + Count(new(ActionRun)) + if err != nil { + log.Error("UpdateRepoRunsNumbers count num_closed_action_runs for repo %d: %v", repoID, err) + return + } + + if _, err := e.ID(repoID).Cols("num_action_runs", "num_closed_action_runs").NoAutoTime().Update(&repo_model.Repository{ + NumActionRuns: int(numActionRuns), + NumClosedActionRuns: int(numClosedActionRuns), + }); err != nil { + log.Error("UpdateRepoRunsNumbers update repo %d: %v", repoID, err) + } } // CancelPreviousJobs cancels all previous jobs of the same repository, reference, workflow, and event. @@ -239,116 +258,62 @@ func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID strin return cancelledJobs, err } - // Iterate over each job and attempt to cancel it. - for _, job := range jobs { - // Skip jobs that are already in a terminal state (completed, cancelled, etc.). - status := job.Status - if status.IsDone() { - continue - } - - // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. - if job.TaskID == 0 { - job.Status = StatusCancelled - job.Stopped = timeutil.TimeStampNow() - - // Update the job's status and stopped time in the database. - n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") - if err != nil { - return cancelledJobs, err - } - - // If the update affected 0 rows, it means the job has changed in the meantime, so we need to try again. - if n == 0 { - return cancelledJobs, errors.New("job has changed, try again") - } - - cancelledJobs = append(cancelledJobs, job) - // Continue with the next job. - continue - } - - // If the job has an associated task, try to stop the task, effectively cancelling the job. - if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil { - return cancelledJobs, err - } - cancelledJobs = append(cancelledJobs, job) + cjs, err := CancelJobs(ctx, jobs) + if err != nil { + return cancelledJobs, err } + cancelledJobs = append(cancelledJobs, cjs...) } // Return nil to indicate successful cancellation of all running and waiting jobs. return cancelledJobs, nil } -// InsertRun inserts a run -// The title will be cut off at 255 characters if it's longer than 255 characters. -func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWorkflow) error { - return db.WithTx(ctx, func(ctx context.Context) error { - index, err := db.GetNextResourceIndex(ctx, "action_run_index", run.RepoID) - if err != nil { - return err - } - run.Index = index - run.Title = util.EllipsisDisplayString(run.Title, 255) - - if err := db.Insert(ctx, run); err != nil { - return err +func CancelJobs(ctx context.Context, jobs []*ActionRunJob) ([]*ActionRunJob, error) { + cancelledJobs := make([]*ActionRunJob, 0, len(jobs)) + // Iterate over each job and attempt to cancel it. + for _, job := range jobs { + // Skip jobs that are already in a terminal state (completed, cancelled, etc.). + status := job.Status + if status.IsDone() { + continue } - if run.Repo == nil { - repo, err := repo_model.GetRepositoryByID(ctx, run.RepoID) + // If the job has no associated task (probably an error), set its status to 'Cancelled' and stop it. + if job.TaskID == 0 { + job.Status = StatusCancelled + job.Stopped = timeutil.TimeStampNow() + + // Update the job's status and stopped time in the database. + n, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0}, "status", "stopped") if err != nil { - return err + return cancelledJobs, err } - run.Repo = repo + + // If the update affected 0 rows, it means the job has changed in the meantime + if n == 0 { + log.Error("Failed to cancel job %d because it has changed", job.ID) + continue + } + + cancelledJobs = append(cancelledJobs, job) + // Continue with the next job. + continue } - if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { - return err + // If the job has an associated task, try to stop the task, effectively cancelling the job. + if err := StopTask(ctx, job.TaskID, StatusCancelled); err != nil { + return cancelledJobs, err } + updatedJob, err := GetRunJobByRunAndID(ctx, job.RunID, job.ID) + if err != nil { + return cancelledJobs, fmt.Errorf("get job: %w", err) + } + cancelledJobs = append(cancelledJobs, updatedJob) + } - runJobs := make([]*ActionRunJob, 0, len(jobs)) - var hasWaiting bool - for _, v := range jobs { - id, job := v.Job() - needs := job.Needs() - if err := v.SetJob(id, job.EraseNeeds()); err != nil { - return err - } - payload, _ := v.Marshal() - status := StatusWaiting - if len(needs) > 0 || run.NeedApproval { - status = StatusBlocked - } else { - hasWaiting = true - } - job.Name = util.EllipsisDisplayString(job.Name, 255) - runJobs = append(runJobs, &ActionRunJob{ - RunID: run.ID, - RepoID: run.RepoID, - OwnerID: run.OwnerID, - CommitSHA: run.CommitSHA, - IsForkPullRequest: run.IsForkPullRequest, - Name: job.Name, - WorkflowPayload: payload, - JobID: id, - Needs: needs, - RunsOn: job.RunsOn(), - Status: status, - }) - } - if err := db.Insert(ctx, runJobs); err != nil { - return err - } - - // if there is a job in the waiting status, increase tasks version. - if hasWaiting { - if err := IncreaseTaskVersion(ctx, run.OwnerID, run.RepoID); err != nil { - return err - } - } - return nil - }) + // Return nil to indicate successful cancellation of all running and waiting jobs. + return cancelledJobs, nil } func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, error) { @@ -363,16 +328,16 @@ func GetRunByRepoAndID(ctx context.Context, repoID, runID int64) (*ActionRun, er return &run, nil } -func GetRunByIndex(ctx context.Context, repoID, index int64) (*ActionRun, error) { +func GetRunByRepoAndIndex(ctx context.Context, repoID, runIndex int64) (*ActionRun, error) { run := &ActionRun{ RepoID: repoID, - Index: index, + Index: runIndex, } has, err := db.GetEngine(ctx).Get(run) if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf("run with index %d %d: %w", repoID, index, util.ErrNotExist) + return nil, fmt.Errorf("run with repo_id %d and index %d: %w", repoID, runIndex, util.ErrNotExist) } return run, nil @@ -426,19 +391,63 @@ func UpdateRun(ctx context.Context, run *ActionRun, cols ...string) error { // It's impossible that the run is not found, since Gitea never deletes runs. } - if run.Status != 0 || slices.Contains(cols, "status") { - if run.RepoID == 0 { - setting.PanicInDevOrTesting("RepoID should not be 0") - } - if err = run.LoadRepo(ctx); err != nil { - return err - } - if err := updateRepoRunsNumbers(ctx, run.Repo); err != nil { - return err - } - } - return nil } type ActionRunIndex db.ResourceIndex + +func GetConcurrentRunsAndJobs(ctx context.Context, repoID int64, concurrencyGroup string, status []Status) ([]*ActionRun, []*ActionRunJob, error) { + runs, err := db.Find[ActionRun](ctx, &FindRunOptions{ + RepoID: repoID, + ConcurrencyGroup: concurrencyGroup, + Status: status, + }) + if err != nil { + return nil, nil, fmt.Errorf("find runs: %w", err) + } + + jobs, err := db.Find[ActionRunJob](ctx, &FindRunJobOptions{ + RepoID: repoID, + ConcurrencyGroup: concurrencyGroup, + Statuses: status, + }) + if err != nil { + return nil, nil, fmt.Errorf("find jobs: %w", err) + } + + return runs, jobs, nil +} + +func CancelPreviousJobsByRunConcurrency(ctx context.Context, actionRun *ActionRun) ([]*ActionRunJob, error) { + if actionRun.ConcurrencyGroup == "" { + return nil, nil + } + + var jobsToCancel []*ActionRunJob + + statusFindOption := []Status{StatusWaiting, StatusBlocked} + if actionRun.ConcurrencyCancel { + statusFindOption = append(statusFindOption, StatusRunning) + } + runs, jobs, err := GetConcurrentRunsAndJobs(ctx, actionRun.RepoID, actionRun.ConcurrencyGroup, statusFindOption) + if err != nil { + return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) + } + jobsToCancel = append(jobsToCancel, jobs...) + + // cancel runs in the same concurrency group + for _, run := range runs { + if run.ID == actionRun.ID { + continue + } + jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ + RunID: run.ID, + }) + if err != nil { + return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) + } + jobsToCancel = append(jobsToCancel, jobs...) + } + + return CancelJobs(ctx, jobsToCancel) +} diff --git a/package/gitea/source/models/actions/run_job.go b/package/gitea/source/models/actions/run_job.go index e7fa2127..d1e5d1e9 100644 --- a/package/gitea/source/models/actions/run_job.go +++ b/package/gitea/source/models/actions/run_job.go @@ -11,34 +11,60 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/actions/jobparser" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "xorm.io/builder" ) +// MaxJobNumPerRun is the maximum number of jobs in a single run. +// https://docs.github.com/en/actions/reference/limits#existing-system-limits +// TODO: check this limit when creating jobs +const MaxJobNumPerRun = 256 + // ActionRunJob represents a job of a run type ActionRunJob struct { ID int64 RunID int64 `xorm:"index"` Run *ActionRun `xorm:"-"` - RepoID int64 `xorm:"index"` + RepoID int64 `xorm:"index(repo_concurrency)"` Repo *repo_model.Repository `xorm:"-"` OwnerID int64 `xorm:"index"` CommitSHA string `xorm:"index"` IsForkPullRequest bool Name string `xorm:"VARCHAR(255)"` Attempt int64 - WorkflowPayload []byte - JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id - Needs []string `xorm:"JSON TEXT"` - RunsOn []string `xorm:"JSON TEXT"` - TaskID int64 // the latest task of the job - Status Status `xorm:"index"` - Started timeutil.TimeStamp - Stopped timeutil.TimeStamp - Created timeutil.TimeStamp `xorm:"created"` - Updated timeutil.TimeStamp `xorm:"updated index"` + + // WorkflowPayload is act/jobparser.SingleWorkflow for act/jobparser.Parse + // it should contain exactly one job with global workflow fields for this model + WorkflowPayload []byte + + JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id + Needs []string `xorm:"JSON TEXT"` + RunsOn []string `xorm:"JSON TEXT"` + TaskID int64 // the latest task of the job + Status Status `xorm:"index"` + + RawConcurrency string // raw concurrency from job YAML's "concurrency" section + + // IsConcurrencyEvaluated is only valid/needed when this job's RawConcurrency is not empty. + // If RawConcurrency can't be evaluated (e.g. depend on other job's outputs or have errors), this field will be false. + // If RawConcurrency has been successfully evaluated, this field will be true, ConcurrencyGroup and ConcurrencyCancel are also set. + IsConcurrencyEvaluated bool + + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` // evaluated concurrency.group + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` // evaluated concurrency.cancel-in-progress + + // TokenPermissions stores the explicit permissions from workflow/job YAML (no org/repo clamps applied). + // Org/repo clamps are enforced when the token is used at runtime. + // It is JSON-encoded repo_model.ActionsTokenPermissions and may be empty if not specified. + TokenPermissions *repo_model.ActionsTokenPermissions `xorm:"JSON TEXT"` + + Started timeutil.TimeStamp + Stopped timeutil.TimeStamp + Created timeutil.TimeStamp `xorm:"created"` + Updated timeutil.TimeStamp `xorm:"updated index"` } func init() { @@ -46,7 +72,7 @@ func init() { } func (job *ActionRunJob) Duration() time.Duration { - return calculateDuration(job.Started, job.Stopped, job.Status) + return calculateDuration(job.Started, job.Stopped, job.Status, job.Updated) } func (job *ActionRunJob) LoadRun(ctx context.Context) error { @@ -84,13 +110,43 @@ func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { return job.Run.LoadAttributes(ctx) } -func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { +// ParseJob parses the job structure from the ActionRunJob.WorkflowPayload +func (job *ActionRunJob) ParseJob() (*jobparser.Job, error) { + // job.WorkflowPayload is a SingleWorkflow created from an ActionRun's workflow, which exactly contains this job's YAML definition. + // Ideally it shouldn't be called "Workflow", it is just a job with global workflow fields + trigger + parsedWorkflows, err := jobparser.Parse(job.WorkflowPayload) + if err != nil { + return nil, fmt.Errorf("job %d single workflow: unable to parse: %w", job.ID, err) + } else if len(parsedWorkflows) != 1 { + return nil, fmt.Errorf("job %d single workflow: not single workflow", job.ID) + } + _, workflowJob := parsedWorkflows[0].Job() + if workflowJob == nil { + // it shouldn't happen, and since the callers don't check nil, so return an error instead of nil + return nil, util.ErrorWrap(util.ErrNotExist, "job %d single workflow: payload doesn't contain a job", job.ID) + } + return workflowJob, nil +} + +func GetRunJobByRepoAndID(ctx context.Context, repoID, jobID int64) (*ActionRunJob, error) { var job ActionRunJob - has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job) + has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", jobID, repoID).Get(&job) if err != nil { return nil, err } else if !has { - return nil, fmt.Errorf("run job with id %d: %w", id, util.ErrNotExist) + return nil, fmt.Errorf("run job with id %d: %w", jobID, util.ErrNotExist) + } + + return &job, nil +} + +func GetRunJobByRunAndID(ctx context.Context, runID, jobID int64) (*ActionRunJob, error) { + var job ActionRunJob + has, err := db.GetEngine(ctx).Where("id=? AND run_id=?", jobID, runID).Get(&job) + if err != nil { + return nil, err + } else if !has { + return nil, fmt.Errorf("run job with id %d: %w", jobID, util.ErrNotExist) } return &job, nil @@ -125,7 +181,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col return affected, nil } - if affected != 0 && slices.Contains(cols, "status") && job.Status.IsWaiting() { + if slices.Contains(cols, "status") && job.Status.IsWaiting() { // if the status of job changes to waiting again, increase tasks version. if err := IncreaseTaskVersion(ctx, job.OwnerID, job.RepoID); err != nil { return 0, err @@ -134,7 +190,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col if job.RunID == 0 { var err error - if job, err = GetRunJobByID(ctx, job.ID); err != nil { + if job, err = GetRunJobByRepoAndID(ctx, job.RepoID, job.ID); err != nil { return 0, err } } @@ -197,3 +253,39 @@ func AggregateJobStatus(jobs []*ActionRunJob) Status { return StatusUnknown // it shouldn't happen } } + +func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob) (jobsToCancel []*ActionRunJob, _ error) { + if job.RawConcurrency == "" { + return nil, nil + } + if !job.IsConcurrencyEvaluated { + return nil, nil + } + if job.ConcurrencyGroup == "" { + return nil, nil + } + + statusFindOption := []Status{StatusWaiting, StatusBlocked} + if job.ConcurrencyCancel { + statusFindOption = append(statusFindOption, StatusRunning) + } + runs, jobs, err := GetConcurrentRunsAndJobs(ctx, job.RepoID, job.ConcurrencyGroup, statusFindOption) + if err != nil { + return nil, fmt.Errorf("find concurrent runs and jobs: %w", err) + } + jobs = slices.DeleteFunc(jobs, func(j *ActionRunJob) bool { return j.ID == job.ID }) + jobsToCancel = append(jobsToCancel, jobs...) + + // cancel runs in the same concurrency group + for _, run := range runs { + jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{ + RunID: run.ID, + }) + if err != nil { + return nil, fmt.Errorf("find run %d jobs: %w", run.ID, err) + } + jobsToCancel = append(jobsToCancel, jobs...) + } + + return CancelJobs(ctx, jobsToCancel) +} diff --git a/package/gitea/source/models/actions/run_job_list.go b/package/gitea/source/models/actions/run_job_list.go index 5f7bb628..10f76d36 100644 --- a/package/gitea/source/models/actions/run_job_list.go +++ b/package/gitea/source/models/actions/run_job_list.go @@ -69,12 +69,13 @@ func (jobs ActionJobList) LoadAttributes(ctx context.Context, withRepo bool) err type FindRunJobOptions struct { db.ListOptions - RunID int64 - RepoID int64 - OwnerID int64 - CommitSHA string - Statuses []Status - UpdatedBefore timeutil.TimeStamp + RunID int64 + RepoID int64 + OwnerID int64 + CommitSHA string + Statuses []Status + UpdatedBefore timeutil.TimeStamp + ConcurrencyGroup string } func (opts FindRunJobOptions) ToConds() builder.Cond { @@ -94,6 +95,12 @@ func (opts FindRunJobOptions) ToConds() builder.Cond { if opts.UpdatedBefore > 0 { cond = cond.And(builder.Lt{"`action_run_job`.updated": opts.UpdatedBefore}) } + if opts.ConcurrencyGroup != "" { + if opts.RepoID == 0 { + panic("Invalid FindRunJobOptions: repo_id is required") + } + cond = cond.And(builder.Eq{"`action_run_job`.concurrency_group": opts.ConcurrencyGroup}) + } return cond } diff --git a/package/gitea/source/models/actions/run_list.go b/package/gitea/source/models/actions/run_list.go index 12c55e53..2628c471 100644 --- a/package/gitea/source/models/actions/run_list.go +++ b/package/gitea/source/models/actions/run_list.go @@ -64,15 +64,16 @@ func (runs RunList) LoadRepos(ctx context.Context) error { type FindRunOptions struct { db.ListOptions - RepoID int64 - OwnerID int64 - WorkflowID string - Ref string // the commit/tag/… that caused this workflow - TriggerUserID int64 - TriggerEvent webhook_module.HookEventType - Approved bool // not util.OptionalBool, it works only when it's true - Status []Status - CommitSHA string + RepoID int64 + OwnerID int64 + WorkflowID string + Ref string // the commit/tag/… that caused this workflow + TriggerUserID int64 + TriggerEvent webhook_module.HookEventType + Approved bool // not util.OptionalBool, it works only when it's true + Status []Status + ConcurrencyGroup string + CommitSHA string } func (opts FindRunOptions) ToConds() builder.Cond { @@ -101,6 +102,12 @@ func (opts FindRunOptions) ToConds() builder.Cond { if opts.CommitSHA != "" { cond = cond.And(builder.Eq{"`action_run`.commit_sha": opts.CommitSHA}) } + if len(opts.ConcurrencyGroup) > 0 { + if opts.RepoID == 0 { + panic("Invalid FindRunOptions: repo_id is required") + } + cond = cond.And(builder.Eq{"`action_run`.concurrency_group": opts.ConcurrencyGroup}) + } return cond } diff --git a/package/gitea/source/models/actions/run_test.go b/package/gitea/source/models/actions/run_test.go index 6463d6f7..08e99f7d 100644 --- a/package/gitea/source/models/actions/run_test.go +++ b/package/gitea/source/models/actions/run_test.go @@ -5,10 +5,12 @@ package actions import ( "testing" + "time" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" ) @@ -27,9 +29,18 @@ func TestUpdateRepoRunsNumbers(t *testing.T) { assert.Equal(t, 2, repo.NumClosedActionRuns) // now update will correct them, only num_actionr_runs and num_closed_action_runs should be updated - err = updateRepoRunsNumbers(t.Context(), repo) - assert.NoError(t, err) + UpdateRepoRunsNumbers(t.Context(), repo.ID) repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) assert.Equal(t, 5, repo.NumActionRuns) assert.Equal(t, 3, repo.NumClosedActionRuns) } + +func TestActionRun_Duration_NonNegative(t *testing.T) { + run := &ActionRun{ + Started: timeutil.TimeStamp(100), + Stopped: timeutil.TimeStamp(200), + Status: StatusSuccess, + PreviousDuration: -time.Hour, + } + assert.Equal(t, time.Duration(0), run.Duration()) +} diff --git a/package/gitea/source/models/actions/runner.go b/package/gitea/source/models/actions/runner.go index 81d4249a..f5d40ca7 100644 --- a/package/gitea/source/models/actions/runner.go +++ b/package/gitea/source/models/actions/runner.go @@ -14,6 +14,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/shared/types" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -61,6 +62,8 @@ type ActionRunner struct { 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"` + // Store if this runner is disabled and should not pick up new jobs + IsDisabled bool `xorm:"is_disabled NOT NULL DEFAULT false"` Created timeutil.TimeStamp `xorm:"created"` Updated timeutil.TimeStamp `xorm:"updated"` @@ -173,6 +176,13 @@ func (r *ActionRunner) GenerateToken() (err error) { return err } +// CanMatchLabels checks whether the runner's labels can match a job's "runs-on" +// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on +func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool { + runnerLabelSet := container.SetOf(r.AgentLabels...) + return runnerLabelSet.Contains(jobRunsOn...) // match all labels +} + func init() { db.RegisterModel(&ActionRunner{}) } @@ -191,6 +201,7 @@ type FindRunnerOptions struct { Sort string Filter string IsOnline optional.Option[bool] + IsDisabled optional.Option[bool] WithAvailable bool // not only runners belong to, but also runners can be used } @@ -231,6 +242,10 @@ func (opts FindRunnerOptions) ToConds() builder.Cond { cond = cond.And(builder.Lte{"last_online": time.Now().Add(-RunnerOfflineTime).Unix()}) } } + + if opts.IsDisabled.Has() { + cond = cond.And(builder.Eq{"is_disabled": opts.IsDisabled.Value()}) + } return cond } @@ -289,6 +304,20 @@ func UpdateRunner(ctx context.Context, r *ActionRunner, cols ...string) error { return err } +func SetRunnerDisabled(ctx context.Context, runner *ActionRunner, isDisabled bool) error { + if runner.IsDisabled == isDisabled { + return nil + } + + return db.WithTx(ctx, func(ctx context.Context) error { + runner.IsDisabled = isDisabled + if err := UpdateRunner(ctx, runner, "is_disabled"); err != nil { + return err + } + return IncreaseTaskVersion(ctx, runner.OwnerID, runner.RepoID) + }) +} + // DeleteRunner deletes a runner by given ID. func DeleteRunner(ctx context.Context, id int64) error { if _, err := GetRunnerByID(ctx, id); err != nil { diff --git a/package/gitea/source/models/actions/task.go b/package/gitea/source/models/actions/task.go index c1306a84..77139ddc 100644 --- a/package/gitea/source/models/actions/task.go +++ b/package/gitea/source/models/actions/task.go @@ -8,12 +8,13 @@ import ( "crypto/subtle" "errors" "fmt" + "strings" "time" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" - "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/actions/jobparser" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -21,7 +22,6 @@ import ( runnerv1 "code.gitea.io/actions-proto-go/runner/v1" lru "github.com/hashicorp/golang-lru/v2" - "github.com/nektos/act/pkg/jobparser" "google.golang.org/protobuf/types/known/timestamppb" "xorm.io/builder" ) @@ -77,7 +77,7 @@ func init() { } func (task *ActionTask) Duration() time.Duration { - return calculateDuration(task.Started, task.Stopped, task.Status) + return calculateDuration(task.Started, task.Stopped, task.Status, task.Updated) } func (task *ActionTask) IsStopped() bool { @@ -114,7 +114,7 @@ func (task *ActionTask) GetRepoLink() string { func (task *ActionTask) LoadJob(ctx context.Context) error { if task.Job == nil { - job, err := GetRunJobByID(ctx, task.JobID) + job, err := GetRunJobByRepoAndID(ctx, task.RepoID, task.JobID) if err != nil { return err } @@ -216,6 +216,20 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro return nil, errNotExist } +func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) { + if step.Name != "" { + name = step.Name // the step has an explicit name + } else { + // for unnamed step, its "String()" method tries to get a display name by its "name", "uses", + // "run" or "id" (last fallback), we add the "Run " prefix for unnamed steps for better display + // for multi-line "run" scripts, only use the first line to match GitHub's behavior + // https://github.com/actions/runner/blob/66800900843747f37591b077091dd2c8cf2c1796/src/Runner.Worker/Handlers/ScriptHandler.cs#L45-L58 + runStr, _, _ := strings.Cut(strings.TrimSpace(step.Run), "\n") + name = "Run " + util.IfZero(strings.TrimSpace(runStr), step.String()) + } + return util.EllipsisDisplayString(name, limit) // database column has a length limit +} + func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) { ctx, committer, err := db.TxContext(ctx) if err != nil { @@ -246,7 +260,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask var job *ActionRunJob log.Trace("runner labels: %v", runner.AgentLabels) for _, v := range jobs { - if isSubset(runner.AgentLabels, v.RunsOn) { + if runner.CanMatchLabels(v.RunsOn) { job = v break } @@ -278,13 +292,10 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask return nil, false, err } - parsedWorkflows, err := jobparser.Parse(job.WorkflowPayload) + workflowJob, err := job.ParseJob() if err != nil { - return nil, false, fmt.Errorf("parse workflow of job %d: %w", job.ID, err) - } else if len(parsedWorkflows) != 1 { - return nil, false, fmt.Errorf("workflow of job %d: not single workflow", job.ID) + return nil, false, fmt.Errorf("load job %d: %w", job.ID, err) } - _, workflowJob := parsedWorkflows[0].Job() if _, err := e.Insert(task); err != nil { return nil, false, err @@ -298,9 +309,8 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask if len(workflowJob.Steps) > 0 { steps := make([]*ActionTaskStep, len(workflowJob.Steps)) for i, v := range workflowJob.Steps { - name := util.EllipsisDisplayString(v.String(), 255) steps[i] = &ActionTaskStep{ - Name: name, + Name: makeTaskStepDisplayName(v, 255), TaskID: task.ID, Index: int64(i), RepoID: task.RepoID, @@ -378,6 +388,7 @@ func UpdateTaskByState(ctx context.Context, runnerID int64, state *runnerv1.Task } if _, err := UpdateRunJob(ctx, &ActionRunJob{ ID: task.JobID, + RepoID: task.RepoID, Status: task.Status, Stopped: task.Stopped, }, nil); err != nil { @@ -439,6 +450,7 @@ func StopTask(ctx context.Context, taskID int64, status Status) error { task.Stopped = now if _, err := UpdateRunJob(ctx, &ActionRunJob{ ID: task.JobID, + RepoID: task.RepoID, Status: task.Status, Stopped: task.Stopped, }, nil); err != nil { @@ -479,20 +491,6 @@ func FindOldTasksToExpire(ctx context.Context, olderThan timeutil.TimeStamp, lim Find(&tasks) } -func isSubset(set, subset []string) bool { - m := make(container.Set[string], len(set)) - for _, v := range set { - m.Add(v) - } - - for _, v := range subset { - if !m.Contains(v) { - return false - } - } - return true -} - func convertTimestamp(timestamp *timestamppb.Timestamp) timeutil.TimeStamp { if timestamp.GetSeconds() == 0 && timestamp.GetNanos() == 0 { return timeutil.TimeStamp(0) diff --git a/package/gitea/source/models/actions/task_step.go b/package/gitea/source/models/actions/task_step.go index 3af1fe3f..3b477d84 100644 --- a/package/gitea/source/models/actions/task_step.go +++ b/package/gitea/source/models/actions/task_step.go @@ -14,7 +14,7 @@ import ( // ActionTaskStep represents a step of ActionTask type ActionTaskStep struct { ID int64 - Name string `xorm:"VARCHAR(255)"` + Name string `xorm:"VARCHAR(255)"` // the step name, for display purpose only, it will be truncated if it is too long TaskID int64 `xorm:"index unique(task_index)"` Index int64 `xorm:"index unique(task_index)"` RepoID int64 `xorm:"index"` @@ -28,7 +28,7 @@ type ActionTaskStep struct { } func (step *ActionTaskStep) Duration() time.Duration { - return calculateDuration(step.Started, step.Stopped, step.Status) + return calculateDuration(step.Started, step.Stopped, step.Status, step.Updated) } func init() { diff --git a/package/gitea/source/models/actions/task_test.go b/package/gitea/source/models/actions/task_test.go new file mode 100644 index 00000000..cbfc6f76 --- /dev/null +++ b/package/gitea/source/models/actions/task_test.go @@ -0,0 +1,77 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "strings" + "testing" + + "code.gitea.io/gitea/modules/actions/jobparser" + + "github.com/stretchr/testify/assert" +) + +func TestMakeTaskStepDisplayName(t *testing.T) { + tests := []struct { + name string + jobStep *jobparser.Step + expected string + }{ + { + name: "explicit name", + jobStep: &jobparser.Step{ + Name: "Test Step", + }, + expected: "Test Step", + }, + { + name: "uses step", + jobStep: &jobparser.Step{ + Uses: "actions/checkout@v4", + }, + expected: "Run actions/checkout@v4", + }, + { + name: "single-line run", + jobStep: &jobparser.Step{ + Run: "echo hello", + }, + expected: "Run echo hello", + }, + { + name: "multi-line run block scalar", + jobStep: &jobparser.Step{ + Run: "\n echo hello \r\n echo world \n ", + }, + expected: "Run echo hello", + }, + { + name: "fallback to id", + jobStep: &jobparser.Step{ + ID: "step-id", + }, + expected: "Run step-id", + }, + { + name: "very long name truncated", + jobStep: &jobparser.Step{ + Name: strings.Repeat("a", 300), + }, + expected: strings.Repeat("a", 252) + "…", + }, + { + name: "very long run truncated", + jobStep: &jobparser.Step{ + Run: strings.Repeat("a", 300), + }, + expected: "Run " + strings.Repeat("a", 248) + "…", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := makeTaskStepDisplayName(tt.jobStep, 255) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/package/gitea/source/models/actions/token_permissions.go b/package/gitea/source/models/actions/token_permissions.go new file mode 100644 index 00000000..985f6cc9 --- /dev/null +++ b/package/gitea/source/models/actions/token_permissions.go @@ -0,0 +1,60 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "context" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" +) + +// ComputeTaskTokenPermissions computes the effective permissions for a job token against the target repository. +// It uses the job's stored permissions (if any), then applies org/repo clamps and fork/cross-repo restrictions. +// Note: target repository access policy checks are enforced in GetActionsUserRepoPermission; this function only computes the job token's effective permission ceiling. +func ComputeTaskTokenPermissions(ctx context.Context, task *ActionTask, targetRepo *repo_model.Repository) (ret repo_model.ActionsTokenPermissions, err error) { + if err := task.LoadJob(ctx); err != nil { + return ret, err + } + if err := task.Job.LoadRepo(ctx); err != nil { + return ret, err + } + runRepo := task.Job.Repo + + if err := runRepo.LoadOwner(ctx); err != nil { + return ret, err + } + + repoActionsCfg := runRepo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + ownerActionsCfg, err := GetOwnerActionsConfig(ctx, runRepo.OwnerID) + if err != nil { + return ret, err + } + + var jobDeclaredPerms repo_model.ActionsTokenPermissions + if task.Job.TokenPermissions != nil { + jobDeclaredPerms = *task.Job.TokenPermissions + } else if repoActionsCfg.OverrideOwnerConfig { + jobDeclaredPerms = repoActionsCfg.GetDefaultTokenPermissions() + } else { + jobDeclaredPerms = ownerActionsCfg.GetDefaultTokenPermissions() + } + + var effectivePerms repo_model.ActionsTokenPermissions + if repoActionsCfg.OverrideOwnerConfig { + effectivePerms = repoActionsCfg.ClampPermissions(jobDeclaredPerms) + } else { + effectivePerms = ownerActionsCfg.ClampPermissions(jobDeclaredPerms) + } + + // Cross-repository access and fork pull requests are strictly read-only for security. + // This ensures a "task repo" cannot gain write access to other repositories via CrossRepoAccess settings. + isSameRepo := task.Job.RepoID == targetRepo.ID + restrictCrossRepoAccess := task.IsForkPullRequest || !isSameRepo + if restrictCrossRepoAccess { + effectivePerms = repo_model.ClampActionsTokenPermissions(effectivePerms, repo_model.MakeRestrictedPermissions()) + } + + return effectivePerms, nil +} diff --git a/package/gitea/source/models/actions/utils.go b/package/gitea/source/models/actions/utils.go index f6ba661a..1101a36c 100644 --- a/package/gitea/source/models/actions/utils.go +++ b/package/gitea/source/models/actions/utils.go @@ -13,6 +13,7 @@ import ( "time" auth_model "code.gitea.io/gitea/models/auth" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" ) @@ -72,13 +73,25 @@ func (indexes *LogIndexes) ToDB() ([]byte, error) { var timeSince = time.Since -func calculateDuration(started, stopped timeutil.TimeStamp, status Status) time.Duration { +// calculateDuration computes wall time for a run, job, task, or step. When status is terminal +// but stopped is missing or inconsistent with started, fallbackEnd (typically the row Updated +// time) is used so duration still reflects approximate elapsed time instead of 0 or a negative. +func calculateDuration(started, stopped timeutil.TimeStamp, status Status, fallbackEnd timeutil.TimeStamp) time.Duration { if started == 0 { return 0 } s := started.AsTime() if status.IsDone() { - return stopped.AsTime().Sub(s) + end := stopped + if stopped.IsZero() || stopped < started { + if !fallbackEnd.IsZero() && fallbackEnd >= started { + end = fallbackEnd + } else { + log.Trace("actions: invalid duration timestamps (started=%d, stopped=%d, fallbackEnd=%d, status=%s)", started, stopped, fallbackEnd, status) + return 0 + } + } + return end.AsTime().Sub(s) } return timeSince(s).Truncate(time.Second) } diff --git a/package/gitea/source/models/actions/utils_test.go b/package/gitea/source/models/actions/utils_test.go index 98c048d4..2f7e7da3 100644 --- a/package/gitea/source/models/actions/utils_test.go +++ b/package/gitea/source/models/actions/utils_test.go @@ -45,9 +45,10 @@ func Test_calculateDuration(t *testing.T) { return timeutil.TimeStamp(1000).AsTime().Sub(t) } type args struct { - started timeutil.TimeStamp - stopped timeutil.TimeStamp - status Status + started timeutil.TimeStamp + stopped timeutil.TimeStamp + status Status + fallbackEnd timeutil.TimeStamp } tests := []struct { name string @@ -81,10 +82,48 @@ func Test_calculateDuration(t *testing.T) { }, want: 100 * time.Second, }, + { + name: "done_stopped_zero_no_fallback", + args: args{ + started: 500, + stopped: 0, + status: StatusSuccess, + }, + want: 0, + }, + { + name: "done_stopped_zero_uses_fallback", + args: args{ + started: 500, + stopped: 0, + status: StatusSuccess, + fallbackEnd: 600, + }, + want: 100 * time.Second, + }, + { + name: "done_stopped_before_started_no_fallback", + args: args{ + started: 600, + stopped: 550, + status: StatusSuccess, + }, + want: 0, + }, + { + name: "done_stopped_before_started_uses_fallback", + args: args{ + started: 600, + stopped: 550, + status: StatusSuccess, + fallbackEnd: 650, + }, + want: 50 * time.Second, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.Equalf(t, tt.want, calculateDuration(tt.args.started, tt.args.stopped, tt.args.status), "calculateDuration(%v, %v, %v)", tt.args.started, tt.args.stopped, tt.args.status) + assert.Equalf(t, tt.want, calculateDuration(tt.args.started, tt.args.stopped, tt.args.status, tt.args.fallbackEnd), "calculateDuration(%v, %v, %v, %v)", tt.args.started, tt.args.stopped, tt.args.status, tt.args.fallbackEnd) }) } } diff --git a/package/gitea/source/models/activities/action.go b/package/gitea/source/models/activities/action.go index 8e589eda..97388402 100644 --- a/package/gitea/source/models/activities/action.go +++ b/package/gitea/source/models/activities/action.go @@ -186,15 +186,7 @@ func (a *Action) LoadActUser(ctx context.Context) { if a.ActUser != nil { return } - var err error - a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID) - if err == nil { - return - } else if user_model.IsErrUserNotExist(err) { - a.ActUser = user_model.NewGhostUser() - } else { - log.Error("GetUserByID(%d): %v", a.ActUserID, err) - } + a.ActUserID, a.ActUser, _ = user_model.GetPossibleUserByID(ctx, a.ActUserID) } func (a *Action) LoadRepo(ctx context.Context) error { @@ -444,6 +436,12 @@ type GetFeedsOptions struct { DontCount bool // do counting in GetFeeds } +func (opts *GetFeedsOptions) ApplyPublicOnly(publicOnly bool) { + if publicOnly { + opts.IncludePrivate = false + } +} + // ActivityReadable return whether doer can read activities of user func ActivityReadable(user, doer *user_model.User) bool { return !user.KeepActivityPrivate || diff --git a/package/gitea/source/models/activities/action_list.go b/package/gitea/source/models/activities/action_list.go index b52cf7ee..29ff2fdf 100644 --- a/package/gitea/source/models/activities/action_list.go +++ b/package/gitea/source/models/activities/action_list.go @@ -30,7 +30,7 @@ func (actions ActionList) getUserIDs() []int64 { func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) { if len(actions) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // returns nil when there are no actions } userIDs := actions.getUserIDs() diff --git a/package/gitea/source/models/activities/notification_list.go b/package/gitea/source/models/activities/notification_list.go index 6539e14e..d38758bc 100644 --- a/package/gitea/source/models/activities/notification_list.go +++ b/package/gitea/source/models/activities/notification_list.go @@ -113,7 +113,7 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n } toNotify.AddMultiple(issueParticipants...) - // dont notify user who cause notification + // don't notify user who cause notification delete(toNotify, notificationAuthorID) // explicit unwatch on issue issueUnWatches, err := issues_model.GetIssueWatchersIDs(ctx, issueID, false) diff --git a/package/gitea/source/models/activities/user_heatmap.go b/package/gitea/source/models/activities/user_heatmap.go index 8d9730ea..e24d44c5 100644 --- a/package/gitea/source/models/activities/user_heatmap.go +++ b/package/gitea/source/models/activities/user_heatmap.go @@ -5,9 +5,6 @@ package activities import ( "context" - "encoding/json" - "sort" - "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" @@ -22,27 +19,14 @@ type UserHeatmapData struct { Contributions int64 `json:"contributions"` } -type heatmapPushAction struct { - Content string `xorm:"content"` - CreatedUnix timeutil.TimeStamp `xorm:"created_unix"` -} - -type heatmapPushActionContent struct { - Commits []*heatmapPushCommit `json:"Commits"` -} - -type heatmapPushCommit struct { - Timestamp time.Time `json:"Timestamp"` -} - -// GetUserHeatmapDataByUser returns an array of UserHeatmapData +// GetUserHeatmapDataByUser returns an array of UserHeatmapData, it checks whether doer can access user's activity func GetUserHeatmapDataByUser(ctx context.Context, user, doer *user_model.User) ([]*UserHeatmapData, error) { return getUserHeatmapData(ctx, user, nil, doer) } -// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData -func GetUserHeatmapDataByUserTeam(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { - return getUserHeatmapData(ctx, user, team, doer) +// GetUserHeatmapDataByOrgTeam returns an array of UserHeatmapData, it checks whether doer can access org's activity +func GetUserHeatmapDataByOrgTeam(ctx context.Context, org *organization.Organization, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { + return getUserHeatmapData(ctx, org.AsUser(), team, doer) } func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { @@ -78,84 +62,12 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi return nil, err } - cutoff := timeutil.TimeStampNow() - (366+7)*86400 - engine := db.GetEngine(ctx) - - if err := engine. + return hdata, db.GetEngine(ctx). Select(groupBy+" AS timestamp, count(user_id) as contributions"). Table("action"). Where(cond). - And("created_unix > ?", cutoff). // (366+7) days to include the first week for the heatmap - And("op_type != ?", ActionCommitRepo). - And("op_type != ?", ActionMirrorSyncPush). + And("created_unix > ?", timeutil.TimeStampNow()-(366+7)*86400). // (366+7) days to include the first week for the heatmap GroupBy(groupByName). OrderBy("timestamp"). - Find(&hdata); err != nil { - return nil, err - } - - pushActions := make([]*heatmapPushAction, 0) - if err := engine. - Table("action"). - Where(cond). - And("created_unix > ?", cutoff). - And("(op_type = ? OR op_type = ?)", ActionCommitRepo, ActionMirrorSyncPush). - Cols("content", "created_unix"). - Find(&pushActions); err != nil { - return nil, err - } - - byTimestamp := make(map[timeutil.TimeStamp]*UserHeatmapData, len(hdata)) - for _, item := range hdata { - byTimestamp[item.Timestamp] = item - } - - for _, action := range pushActions { - payload := new(heatmapPushActionContent) - if err := json.Unmarshal([]byte(action.Content), payload); err != nil || len(payload.Commits) == 0 { - entry := heatmapEntryForTimestamp(byTimestamp, &hdata, action.CreatedUnix/900*900) - entry.Contributions++ - continue - } - - for _, commit := range payload.Commits { - if commit == nil { - continue - } - - commitUnix := timeutil.TimeStamp(commit.Timestamp.Unix()) - if commitUnix <= cutoff { - continue - } - - entry := heatmapEntryForTimestamp(byTimestamp, &hdata, commitUnix/900*900) - entry.Contributions++ - } - } - - sort.Slice(hdata, func(i, j int) bool { - return hdata[i].Timestamp < hdata[j].Timestamp - }) - - return hdata, nil -} - -func heatmapEntryForTimestamp(byTimestamp map[timeutil.TimeStamp]*UserHeatmapData, hdata *[]*UserHeatmapData, timestamp timeutil.TimeStamp) *UserHeatmapData { - if entry, ok := byTimestamp[timestamp]; ok { - return entry - } - - entry := &UserHeatmapData{Timestamp: timestamp} - byTimestamp[timestamp] = entry - *hdata = append(*hdata, entry) - return entry -} - -// GetTotalContributionsInHeatmap returns the total number of contributions in a heatmap -func GetTotalContributionsInHeatmap(hdata []*UserHeatmapData) int64 { - var total int64 - for _, v := range hdata { - total += v.Contributions - } - return total + Find(&hdata) } diff --git a/package/gitea/source/models/admin/task.go b/package/gitea/source/models/admin/task.go index 5d2b9bbf..7056a835 100644 --- a/package/gitea/source/models/admin/task.go +++ b/package/gitea/source/models/admin/task.go @@ -137,6 +137,11 @@ func (task *Task) MigrateConfig() (*migration.MigrateOptions, error) { log.Error("Unable to decrypt AuthToken, maybe SECRET_KEY is wrong: %v", err) } } + if opts.AWSSecretAccessKeyEncrypted != "" { + if opts.AWSSecretAccessKey, err = secret.DecryptSecret(setting.SecretKey, opts.AWSSecretAccessKeyEncrypted); err != nil { + log.Error("Unable to decrypt AWSSecretAccessKey, maybe SECRET_KEY is wrong: %v", err) + } + } return &opts, nil } @@ -201,6 +206,8 @@ func FinishMigrateTask(ctx context.Context, task *Task) error { conf.AuthPasswordEncrypted = "" conf.AuthTokenEncrypted = "" conf.CloneAddrEncrypted = "" + conf.AWSSecretAccessKey = "" + conf.AWSSecretAccessKeyEncrypted = "" confBytes, err := json.Marshal(conf) if err != nil { return err diff --git a/package/gitea/source/models/asymkey/gpg_key_commit_verification.go b/package/gitea/source/models/asymkey/gpg_key_commit_verification.go index 375b703f..ae5192de 100644 --- a/package/gitea/source/models/asymkey/gpg_key_commit_verification.go +++ b/package/gitea/source/models/asymkey/gpg_key_commit_verification.go @@ -70,7 +70,7 @@ func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, e // We will ignore errors in verification as they don't need to be propagated up err = verifySign(sig, hash, k) if err != nil { - return nil, nil + return nil, nil //nolint:nilnil // verification failed, not an error } return k, nil } @@ -86,7 +86,7 @@ func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) return verified, err } } - return nil, nil + return nil, nil //nolint:nilnil // verification failed, not an error } func HashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification { diff --git a/package/gitea/source/models/asymkey/gpg_key_test.go b/package/gitea/source/models/asymkey/gpg_key_test.go index 4621337f..e6656cb7 100644 --- a/package/gitea/source/models/asymkey/gpg_key_test.go +++ b/package/gitea/source/models/asymkey/gpg_key_test.go @@ -11,7 +11,6 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/packet" @@ -398,7 +397,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}, @@ -419,7 +418,7 @@ func TestParseGPGKey(t *testing.T) { // then revoke the key for _, id := range e.Identities { - id.Revocations = append(id.Revocations, &packet.Signature{RevocationReason: util.ToPointer(packet.KeyCompromised)}) + id.Revocations = append(id.Revocations, &packet.Signature{RevocationReason: new(packet.KeyCompromised)}) } k, err = parseGPGKey(t.Context(), 1, e, true) require.NoError(t, err) diff --git a/package/gitea/source/models/asymkey/ssh_key.go b/package/gitea/source/models/asymkey/ssh_key.go index d77b5d46..1873c308 100644 --- a/package/gitea/source/models/asymkey/ssh_key.go +++ b/package/gitea/source/models/asymkey/ssh_key.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/perm" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -64,7 +65,12 @@ func (key *PublicKey) AfterLoad() { // OmitEmail returns content of public key without email address. func (key *PublicKey) OmitEmail() string { - return strings.Join(strings.Split(key.Content, " ")[:2], " ") + fields := strings.Split(key.Content, " ") // format: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... comment + if len(fields) < 2 { + setting.PanicInDevOrTesting("invalid public key %d content: %s", key.ID, key.Content) + return "" // not a valid public key, it shouldn't really happen, the value is managed internally + } + return strings.Join(fields[:2], " ") } func addKey(ctx context.Context, key *PublicKey) (err error) { @@ -84,7 +90,7 @@ func addKey(ctx context.Context, key *PublicKey) (err error) { } // AddPublicKey adds new public key to database and authorized_keys file. -func AddPublicKey(ctx context.Context, ownerID int64, name, content string, authSourceID int64) (*PublicKey, error) { +func AddPublicKey(ctx context.Context, ownerID int64, name, content string, authSourceID int64, verified bool) (*PublicKey, error) { log.Trace(content) fingerprint, err := CalcFingerprint(content) @@ -115,6 +121,7 @@ func AddPublicKey(ctx context.Context, ownerID int64, name, content string, auth Mode: perm.AccessModeWrite, Type: KeyTypeUser, LoginSourceID: authSourceID, + Verified: verified, } if err = addKey(ctx, key); err != nil { return nil, fmt.Errorf("addKey: %w", err) @@ -298,7 +305,7 @@ func deleteKeysMarkedForDeletion(ctx context.Context, keys []string) (bool, erro } // AddPublicKeysBySource add a users public keys. Returns true if there are changes. -func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool { +func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string, verified bool) bool { var sshKeysNeedUpdate bool for _, sshKey := range sshPublicKeys { var err error @@ -317,7 +324,7 @@ func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.So marshalled = marshalled[:len(marshalled)-1] sshKeyName := fmt.Sprintf("%s-%s", s.Name, ssh.FingerprintSHA256(out)) - if _, err := AddPublicKey(ctx, usr.ID, sshKeyName, marshalled, s.ID); err != nil { + if _, err := AddPublicKey(ctx, usr.ID, sshKeyName, marshalled, s.ID, verified); err != nil { if IsErrKeyAlreadyExist(err) { log.Trace("AddPublicKeysBySource[%s]: Public SSH Key %s already exists for user", sshKeyName, usr.Name) } else { @@ -336,7 +343,7 @@ func AddPublicKeysBySource(ctx context.Context, usr *user_model.User, s *auth.So } // SynchronizePublicKeys updates a user's public keys. Returns true if there are changes. -func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string) bool { +func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.Source, sshPublicKeys []string, verified bool) bool { var sshKeysNeedUpdate bool log.Trace("synchronizePublicKeys[%s]: Handling Public SSH Key synchronization for user %s", s.Name, usr.Name) @@ -381,7 +388,7 @@ func SynchronizePublicKeys(ctx context.Context, usr *user_model.User, s *auth.So newKeys = append(newKeys, key) } } - if AddPublicKeysBySource(ctx, usr, s, newKeys) { + if AddPublicKeysBySource(ctx, usr, s, newKeys, verified) { sshKeysNeedUpdate = true } diff --git a/package/gitea/source/models/asymkey/ssh_key_principals.go b/package/gitea/source/models/asymkey/ssh_key_principals.go index e8b97d30..2d4c7474 100644 --- a/package/gitea/source/models/asymkey/ssh_key_principals.go +++ b/package/gitea/source/models/asymkey/ssh_key_principals.go @@ -40,7 +40,7 @@ func CheckPrincipalKeyString(ctx context.Context, user *user_model.User, content if !email.IsActivated { continue } - if content == email.Email { + if strings.EqualFold(content, email.LowerEmail) { return content, nil } } diff --git a/package/gitea/source/models/auth/oauth2.go b/package/gitea/source/models/auth/oauth2.go index d6648413..83048c11 100644 --- a/package/gitea/source/models/auth/oauth2.go +++ b/package/gitea/source/models/auth/oauth2.go @@ -5,15 +5,15 @@ package auth import ( "context" - "crypto/sha256" + "crypto/subtle" "encoding/base32" - "encoding/base64" "errors" "fmt" "net" "net/url" "slices" "strings" + "time" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/container" @@ -23,10 +23,19 @@ import ( uuid "github.com/google/uuid" "golang.org/x/crypto/bcrypt" + "golang.org/x/oauth2" "xorm.io/builder" "xorm.io/xorm" ) +// Authorization codes should expire within 10 minutes per https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2 +const oauth2AuthorizationCodeValidity = 10 * time.Minute + +var ( + ErrOAuth2AuthorizationCodeInvalidated = errors.New("oauth2 authorization code already invalidated") + ErrOAuth2GrantStaleCounter = errors.New("oauth2 grant state changed during token refresh") +) + // OAuth2Application represents an OAuth2 client (RFC 6749) type OAuth2Application struct { ID int64 `xorm:"pk autoincr"` @@ -145,30 +154,40 @@ func (app *OAuth2Application) ContainsRedirectURI(redirectURI string) bool { // https://www.rfc-editor.org/rfc/rfc6819#section-5.2.3.3 // 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), "/") - for _, u := range app.RedirectURIs { - if strings.TrimSuffix(strings.ToLower(u), "/") == s { + redirectCandidates := []string{redirectURI} + if !app.ConfidentialClient { + loopbackRedirect, ok := normalizePublicClientRedirectURI(redirectURI) + if ok { + redirectCandidates = append(redirectCandidates, loopbackRedirect) + } + } + + for _, candidate := range redirectCandidates { + normalizedCandidate := normalizeRedirectURIForComparison(candidate) + for _, registeredURI := range app.RedirectURIs { + if normalizeRedirectURIForComparison(registeredURI) == normalizedCandidate { return true } } - return false } - if !app.ConfidentialClient { - uri, err := url.Parse(redirectURI) - // ignore port for http loopback uris following https://datatracker.ietf.org/doc/html/rfc8252#section-7.3 - if err == nil && uri.Scheme == "http" && uri.Port() != "" { - ip := net.ParseIP(uri.Hostname()) - if ip != nil && ip.IsLoopback() { - // strip port - uri.Host = uri.Hostname() - if contains(uri.String()) { - return true - } - } - } + + return false +} + +func normalizeRedirectURIForComparison(redirectURI string) string { + return strings.TrimSuffix(util.ToLowerASCII(redirectURI), "/") +} + +func normalizePublicClientRedirectURI(redirectURI string) (string, bool) { + parsedURI, err := url.Parse(redirectURI) + if err != nil || parsedURI.Scheme != "http" || parsedURI.Port() == "" { + return "", false } - return contains(redirectURI) + if ip := net.ParseIP(parsedURI.Hostname()); ip == nil || !ip.IsLoopback() { + return "", false + } + parsedURI.Host = parsedURI.Hostname() + return parsedURI.String(), true } // Base32 characters, but lowercased. @@ -209,7 +228,7 @@ func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64 if has, err := db.GetEngine(ctx).Where("user_id = ? AND application_id = ?", userID, app.ID).Get(grant); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return grant, nil } @@ -386,6 +405,14 @@ func (code *OAuth2AuthorizationCode) TableName() string { return "oauth2_authorization_code" } +// IsExpired reports whether the authorization code is expired. +func (code *OAuth2AuthorizationCode) IsExpired() bool { + if code.ValidUntil.IsZero() { + return true + } + return code.ValidUntil <= timeutil.TimeStampNow() +} + // GenerateRedirectURI generates a redirect URI for a successful authorization request. State will be used if not empty. func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (*url.URL, error) { redirect, err := url.Parse(code.RedirectURI) @@ -403,26 +430,44 @@ func (code *OAuth2AuthorizationCode) GenerateRedirectURI(state string) (*url.URL // Invalidate deletes the auth code from the database to invalidate this code 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 ErrOAuth2AuthorizationCodeInvalidated + } + return nil +} + +func (code *OAuth2AuthorizationCode) requiresCodeVerifier() bool { + return code.CodeChallengeMethod != "" || code.CodeChallenge != "" +} + +func deriveCodeChallenge(method, verifier string) (string, bool) { + switch method { + case "S256": + return oauth2.S256ChallengeFromVerifier(verifier), true + case "plain": + return verifier, true + default: + return "", false + } } // ValidateCodeChallenge validates the given verifier against the saved code challenge. This is part of the PKCE implementation. func (code *OAuth2AuthorizationCode) ValidateCodeChallenge(verifier string) bool { - 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 - case "plain": - return verifier == code.CodeChallenge - case "": + if !code.requiresCodeVerifier() { return true - default: - // unsupported method -> return false + } + if verifier == "" || code.CodeChallengeMethod == "" { return false } + expectedChallenge, ok := deriveCodeChallenge(code.CodeChallengeMethod, verifier) + if !ok { + return false + } + return subtle.ConstantTimeCompare([]byte(expectedChallenge), []byte(code.CodeChallenge)) == 1 } // GetOAuth2AuthorizationByCode returns an authorization by its code @@ -431,13 +476,13 @@ func GetOAuth2AuthorizationByCode(ctx context.Context, code string) (auth *OAuth if has, err := db.GetEngine(ctx).Where("code = ?", code).Get(auth); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } auth.Grant = new(OAuth2Grant) if has, err := db.GetEngine(ctx).ID(auth.GrantID).Get(auth.Grant); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return auth, nil } @@ -472,6 +517,7 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi // for code scanners to grab sensitive tokens. codeSecret := "gta_" + base32Lower.EncodeToString(rBytes) + validUntil := time.Now().Add(oauth2AuthorizationCodeValidity) code = &OAuth2AuthorizationCode{ Grant: grant, GrantID: grant.ID, @@ -479,6 +525,7 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi Code: codeSecret, CodeChallenge: codeChallenge, CodeChallengeMethod: codeChallengeMethod, + ValidUntil: timeutil.TimeStamp(validUntil.Unix()), } if err := db.Insert(ctx, code); err != nil { return nil, err @@ -488,15 +535,18 @@ func (grant *OAuth2Grant) GenerateNewAuthorizationCode(ctx context.Context, redi // IncreaseCounter increases the counter and updates the grant 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 = ?", grant.ID). + And("counter = ?", 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 ErrOAuth2GrantStaleCounter } - grant.Counter = updatedGrant.Counter + grant.Counter++ return nil } @@ -521,7 +571,7 @@ func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err if has, err := db.GetEngine(ctx).ID(id).Get(grant); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return grant, err } @@ -611,7 +661,7 @@ func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source, } if !has { - return nil, fmt.Errorf("oauth2 source not found, name: %q", name) + return nil, util.NewNotExistErrorf("oauth2 source not found, name: %q", name) } return authSource, nil diff --git a/package/gitea/source/models/auth/oauth2_test.go b/package/gitea/source/models/auth/oauth2_test.go index 97f75075..d87ffdaa 100644 --- a/package/gitea/source/models/auth/oauth2_test.go +++ b/package/gitea/source/models/auth/oauth2_test.go @@ -5,13 +5,46 @@ package auth_test import ( "testing" + "time" auth_model "code.gitea.io/gitea/models/auth" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "golang.org/x/oauth2" ) +func TestOAuth2AuthorizationCodeValidity(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("GenerateSetsValidUntil", func(t *testing.T) { + grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1}) + expectedValidUntil := timeutil.TimeStamp(time.Now().Unix() + 600) + code, err := grant.GenerateNewAuthorizationCode(t.Context(), "http://127.0.0.1/", "", "") + assert.NoError(t, err) + assert.Equal(t, expectedValidUntil, code.ValidUntil) + assert.False(t, code.IsExpired()) + assert.NoError(t, code.Invalidate(t.Context())) + }) + + t.Run("Expired", func(t *testing.T) { + defer timeutil.MockSet(time.Unix(2, 0).UTC())() + + code := &auth_model.OAuth2AuthorizationCode{ValidUntil: timeutil.TimeStamp(1)} + assert.True(t, code.IsExpired()) + }) + + t.Run("InvalidateTwice", func(t *testing.T) { + code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode") + assert.NoError(t, err) + if assert.NotNil(t, code) { + assert.NoError(t, code.Invalidate(t.Context())) + assert.ErrorIs(t, code.Invalidate(t.Context()), auth_model.ErrOAuth2AuthorizationCodeInvalidated) + } + }) +} + func TestOAuth2Application_GenerateClientSecret(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) @@ -72,6 +105,47 @@ func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) { assert.False(t, app.ContainsRedirectURI("http://127.0.0.1/other")) } +func TestOAuth2Application_ContainsRedirectURI_ASCIIOnlyNormalization(t *testing.T) { + testCases := []struct { + name string + registered []string + redirectURI string + allowed bool + }{ + { + name: "exact-match", + registered: []string{"https://signin.example.test/callback"}, + redirectURI: "https://signin.example.test/callback", + allowed: true, + }, + { + name: "ascii-case-insensitive", + registered: []string{"https://signin.example.test/callback"}, + redirectURI: "https://signIN.example.test/callback", + allowed: true, + }, + { + name: "non-ascii-not-folded", + registered: []string{"https://signin.example.test/callback"}, + redirectURI: "https://signİn.example.test/callback", + allowed: false, + }, + { + name: "loopback-strips-port", + registered: []string{"http://127.0.0.1/callback"}, + redirectURI: "http://127.0.0.1:12345/callback", + allowed: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + app := &auth_model.OAuth2Application{RedirectURIs: tc.registered} + assert.Equal(t, tc.allowed, app.ContainsRedirectURI(tc.redirectURI)) + }) + } +} + func TestOAuth2Application_ValidateClientSecret(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1}) @@ -149,6 +223,16 @@ func TestOAuth2Grant_IncreaseCounter(t *testing.T) { unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2}) } +func TestOAuth2Grant_IncreaseCounterRejectsStaleCounter(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 1}) + stale := *grant + + assert.NoError(t, grant.IncreaseCounter(t.Context())) + err := stale.IncreaseCounter(t.Context()) + assert.ErrorIs(t, err, auth_model.ErrOAuth2GrantStaleCounter) +} + func TestOAuth2Grant_ScopeContains(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Scope: "openid profile"}) @@ -206,35 +290,38 @@ func TestGetOAuth2AuthorizationByCode(t *testing.T) { } func TestOAuth2AuthorizationCode_ValidateCodeChallenge(t *testing.T) { - // test plain - code := &auth_model.OAuth2AuthorizationCode{ - CodeChallengeMethod: "plain", - CodeChallenge: "test123", - } - assert.True(t, code.ValidateCodeChallenge("test123")) - assert.False(t, code.ValidateCodeChallenge("ierwgjoergjio")) + s256Verifier := "s256-verifier" + s256Challenge := oauth2.S256ChallengeFromVerifier(s256Verifier) + missingVerifierChallenge := oauth2.S256ChallengeFromVerifier("verifier-not-supplied") - // test S256 - code = &auth_model.OAuth2AuthorizationCode{ - CodeChallengeMethod: "S256", - CodeChallenge: "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", + testCases := []struct { + name string + method string + challenge string + verifier string + valid bool + }{ + {"plain-success", "plain", "plain-secret", "plain-secret", true}, + {"plain-failure", "plain", "plain-secret", "ierwgjoergjio", false}, + {"s256-success", "S256", s256Challenge, s256Verifier, true}, + {"s256-failure", "S256", s256Challenge, "wiogjerogorewngoenrgoiuenorg", false}, + {"unsupported-method", "monkey", "foiwgjioriogeiogjerger", "foiwgjioriogeiogjerger", false}, + {"no-pkce-configured", "", "", "", true}, + {"s256-missing-verifier", "S256", missingVerifierChallenge, "", false}, + {"plain-missing-verifier", "plain", "plain-secret", "", false}, + {"missing-method-with-challenge", "", "foierjiogerogerg", "", false}, + {"missing-method-rejects-even-matching-verifier", "", "foierjiogerogerg", "foierjiogerogerg", false}, } - assert.True(t, code.ValidateCodeChallenge("N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt")) - assert.False(t, code.ValidateCodeChallenge("wiogjerogorewngoenrgoiuenorg")) - // test unknown - code = &auth_model.OAuth2AuthorizationCode{ - CodeChallengeMethod: "monkey", - CodeChallenge: "foiwgjioriogeiogjerger", + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + code := &auth_model.OAuth2AuthorizationCode{ + CodeChallengeMethod: tc.method, + CodeChallenge: tc.challenge, + } + assert.Equal(t, tc.valid, code.ValidateCodeChallenge(tc.verifier)) + }) } - assert.False(t, code.ValidateCodeChallenge("foiwgjioriogeiogjerger")) - - // test no code challenge - code = &auth_model.OAuth2AuthorizationCode{ - CodeChallengeMethod: "", - CodeChallenge: "foierjiogerogerg", - } - assert.True(t, code.ValidateCodeChallenge("")) } func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) { diff --git a/package/gitea/source/models/auth/source.go b/package/gitea/source/models/auth/source.go index 08cfc961..7a008f08 100644 --- a/package/gitea/source/models/auth/source.go +++ b/package/gitea/source/models/auth/source.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -117,7 +118,7 @@ func RegisterTypeConfig(typ Type, exemplar Config) { type Source struct { ID int64 `xorm:"pk autoincr"` Type Type - Name string `xorm:"UNIQUE"` + Name string `xorm:"UNIQUE"` // it can be the OIDC's provider name, see services/auth/source/oauth2/source_register.go: RegisterSource IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"` IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"` TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"` @@ -139,7 +140,10 @@ func init() { // BeforeSet is invoked from XORM before setting the value of a field of this object. func (source *Source) BeforeSet(colName string, val xorm.Cell) { if colName == "type" { - typ := Type(db.Cell2Int64(val)) + typ, _, err := db.CellToInt(val, NoType) + if err != nil { + setting.PanicInDevOrTesting("Unable to convert login source (id=%d) type: %v", source.ID, err) + } constructor, ok := registeredConfigs[typ] if !ok { return diff --git a/package/gitea/source/models/auth/source_test.go b/package/gitea/source/models/auth/source_test.go index ebc462c5..fbd663a6 100644 --- a/package/gitea/source/models/auth/source_test.go +++ b/package/gitea/source/models/auth/source_test.go @@ -17,13 +17,9 @@ import ( ) type TestSource struct { - auth_model.ConfigBase + auth_model.ConfigBase `json:"-"` - Provider string - ClientID string - ClientSecret string - OpenIDConnectAutoDiscoveryURL string - IconURL string + TestField string } // FromDB fills up a LDAPConfig from serialized format. @@ -37,27 +33,23 @@ func (source *TestSource) ToDB() ([]byte, error) { } func TestDumpAuthSource(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) + require.NoError(t, unittest.PrepareTestDatabase()) authSourceSchema, err := unittest.GetXORMEngine().TableInfo(new(auth_model.Source)) - assert.NoError(t, err) + require.NoError(t, err) auth_model.RegisterTypeConfig(auth_model.OAuth2, new(TestSource)) + source := &auth_model.Source{ + Type: auth_model.OAuth2, + Name: "TestSource", + Cfg: &TestSource{TestField: "TestValue"}, + } + require.NoError(t, auth_model.CreateSource(t.Context(), source)) - auth_model.CreateSource(t.Context(), &auth_model.Source{ - Type: auth_model.OAuth2, - Name: "TestSource", - IsActive: false, - Cfg: &TestSource{ - Provider: "ConvertibleSourceName", - ClientID: "42", - }, - }) - - sb := new(strings.Builder) - - // TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump - engine := unittest.GetXORMEngine() - require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb)) - assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`) + // intentionally test the "dump" to make sure the dumped JSON is correct: https://github.com/go-gitea/gitea/pull/16847 + sb := &strings.Builder{} + require.NoError(t, unittest.GetXORMEngine().DumpTables([]*schemas.Table{authSourceSchema}, sb)) + // the dumped SQL is something like: + // INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784); + assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`) } diff --git a/package/gitea/source/models/db/collation.go b/package/gitea/source/models/db/collation.go index 79ade873..203f7cbf 100644 --- a/package/gitea/source/models/db/collation.go +++ b/package/gitea/source/models/db/collation.go @@ -98,7 +98,7 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) { return nil, err } } else { - return nil, nil + return nil, nil //nolint:nilnil // return nil for unsupported database types } if res.DatabaseCollation == "" { diff --git a/package/gitea/source/models/db/common.go b/package/gitea/source/models/db/common.go index ea628bf2..b3c43f8b 100644 --- a/package/gitea/source/models/db/common.go +++ b/package/gitea/source/models/db/common.go @@ -12,30 +12,30 @@ import ( "xorm.io/builder" ) -// BuildCaseInsensitiveLike returns a condition to check if the given value is like the given key case-insensitively. -// Handles especially SQLite correctly as UPPER there only transforms ASCII letters. +// BuildCaseInsensitiveLike returns a case-insensitive LIKE condition for the given key and value. +// Cast the search value and the database column value to the same case for case-insensitive matching. +// * SQLite: only cast ASCII chars because it doesn't handle complete Unicode case folding +// * Other databases: use database's string function, assuming that they are able to handle complete Unicode case folding correctly func BuildCaseInsensitiveLike(key, value string) builder.Cond { + // ToLowerASCII is about 7% faster than ToUpperASCII (according to Golang's benchmark) if setting.Database.Type.IsSQLite3() { - return builder.Like{"UPPER(" + key + ")", util.ToUpperASCII(value)} + return builder.Like{"LOWER(" + key + ")", util.ToLowerASCII(value)} } - return builder.Like{"UPPER(" + key + ")", strings.ToUpper(value)} + return builder.Like{"LOWER(" + key + ")", strings.ToLower(value)} } // BuildCaseInsensitiveIn returns a condition to check if the given value is in the given values case-insensitively. -// Handles especially SQLite correctly as UPPER there only transforms ASCII letters. +// See BuildCaseInsensitiveLike for more details func BuildCaseInsensitiveIn(key string, values []string) builder.Cond { - uppers := make([]string, 0, len(values)) + incaseValues := make([]string, len(values)) + caseCast := strings.ToLower if setting.Database.Type.IsSQLite3() { - for _, value := range values { - uppers = append(uppers, util.ToUpperASCII(value)) - } - } else { - for _, value := range values { - uppers = append(uppers, strings.ToUpper(value)) - } + caseCast = util.ToLowerASCII } - - return builder.In("UPPER("+key+")", uppers) + for i, value := range values { + incaseValues[i] = caseCast(value) + } + return builder.In("LOWER("+key+")", incaseValues) } // BuilderDialect returns the xorm.Builder dialect of the engine diff --git a/package/gitea/source/models/db/convert.go b/package/gitea/source/models/db/convert.go index 80b0f7b0..374dbfd8 100644 --- a/package/gitea/source/models/db/convert.go +++ b/package/gitea/source/models/db/convert.go @@ -5,12 +5,11 @@ package db import ( "fmt" - "strconv" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "xorm.io/xorm" + "xorm.io/xorm/convert" "xorm.io/xorm/schemas" ) @@ -74,15 +73,14 @@ WHERE ST.name ='varchar'`) return err } -// Cell2Int64 converts a xorm.Cell type to int64, -// and handles possible irregular cases. -func Cell2Int64(val xorm.Cell) int64 { - switch (*val).(type) { - case []uint8: - log.Trace("Cell2Int64 ([]uint8): %v", *val) - - v, _ := strconv.ParseInt(string((*val).([]uint8)), 10, 64) - return v +// CellToInt converts a xorm.Cell field value to an int value +func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err error) { + if *cell == nil { + return def, false, nil } - return (*val).(int64) + val, err := convert.AsInt64(*cell) + if err != nil { + return def, false, err + } + return T(val), true, err } diff --git a/package/gitea/source/models/db/engine_init.go b/package/gitea/source/models/db/engine_init.go index f26189b8..ef5db3ff 100644 --- a/package/gitea/source/models/db/engine_init.go +++ b/package/gitea/source/models/db/engine_init.go @@ -57,7 +57,7 @@ func InitEngine(ctx context.Context) error { xe, err := newXORMEngine() if err != nil { if strings.Contains(err.Error(), "SQLite3 support") { - return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) + return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) } return fmt.Errorf("failed to connect to database: %w", err) } diff --git a/package/gitea/source/models/dbfs/dbfile.go b/package/gitea/source/models/dbfs/dbfile.go index eaf506fb..a6981cb7 100644 --- a/package/gitea/source/models/dbfs/dbfile.go +++ b/package/gitea/source/models/dbfs/dbfile.go @@ -75,7 +75,7 @@ func (f *file) readAt(fileMeta *dbfsMeta, offset int64, p []byte) (n int, err er } func (f *file) Read(p []byte) (n int, err error) { - if f.metaID == 0 || !f.allowRead { + if !f.allowRead { return 0, os.ErrInvalid } @@ -89,7 +89,7 @@ func (f *file) Read(p []byte) (n int, err error) { } func (f *file) Write(p []byte) (n int, err error) { - if f.metaID == 0 || !f.allowWrite { + if !f.allowWrite { return 0, os.ErrInvalid } @@ -184,10 +184,6 @@ func (f *file) Close() error { } func (f *file) Stat() (os.FileInfo, error) { - if f.metaID == 0 { - return nil, os.ErrInvalid - } - fileMeta, err := findFileMetaByID(f.ctx, f.metaID) if err != nil { return nil, err @@ -232,15 +228,17 @@ func (f *file) open(flag int) (err error) { if f.metaID != 0 { return os.ErrExist } - } else { - // create a new file if none exists. - if f.metaID == 0 { - if err = f.createEmpty(); err != nil { - return err - } + } + // create a new file if not exists. + if f.metaID == 0 { + if err = f.createEmpty(); err != nil { + return err } } } + if f.metaID == 0 { + return os.ErrNotExist + } if flag&os.O_TRUNC != 0 { if err = f.truncate(); err != nil { return err @@ -252,7 +250,7 @@ func (f *file) open(flag int) (err error) { } } return nil - } + } // end if: allowWrite // read only mode if f.metaID == 0 { @@ -322,9 +320,6 @@ func (f *file) delete() error { } func (f *file) size() (int64, error) { - if f.metaID == 0 { - return 0, os.ErrNotExist - } fileMeta, err := findFileMetaByID(f.ctx, f.metaID) if err != nil { return 0, err @@ -339,7 +334,7 @@ func findFileMetaByID(ctx context.Context, metaID int64) (*dbfsMeta, error) { } else if ok { return &fileMeta, nil } - return nil, nil + return nil, os.ErrNotExist } func buildPath(path string) string { diff --git a/package/gitea/source/models/dbfs/dbfs.go b/package/gitea/source/models/dbfs/dbfs.go index f68b4a2b..3f768b53 100644 --- a/package/gitea/source/models/dbfs/dbfs.go +++ b/package/gitea/source/models/dbfs/dbfs.go @@ -40,6 +40,9 @@ The DBFS solution: * In the future, when Gitea action needs to limit the log size (other CI/CD services also do so), it's easier to calculate the log file size. * Even sometimes the UI needs to render the tailing lines, the tailing lines can be found be counting the "\n" from the end of the file by seek. The seeking and finding is not the fastest way, but it's still acceptable and won't affect the performance too much. + +Limitations of the DBFS solution: +* Not fully POSIX-compliant, some behaviors may be different from the real filesystem, especially for concurrent read/write */ type dbfsMeta struct { diff --git a/package/gitea/source/models/dbfs/dbfs_test.go b/package/gitea/source/models/dbfs/dbfs_test.go index e1ecd871..ca57ebe1 100644 --- a/package/gitea/source/models/dbfs/dbfs_test.go +++ b/package/gitea/source/models/dbfs/dbfs_test.go @@ -9,19 +9,14 @@ import ( "os" "testing" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func changeDefaultFileBlockSize(n int64) (restore func()) { - old := defaultFileBlockSize - defaultFileBlockSize = n - return func() { - defaultFileBlockSize = old - } -} - func TestDbfsBasic(t *testing.T) { - defer changeDefaultFileBlockSize(4)() + defer test.MockVariableValue(&defaultFileBlockSize, 4)() // test basic write/read f, err := OpenFile(t.Context(), "test.txt", os.O_RDWR|os.O_CREATE) @@ -122,10 +117,55 @@ func TestDbfsBasic(t *testing.T) { stat, err = f.Stat() assert.NoError(t, err) assert.EqualValues(t, 10, stat.Size()) + + t.Run("NonExisting", func(t *testing.T) { + f, err := OpenFile(t.Context(), "non-existing.txt", os.O_RDONLY) + assert.ErrorIs(t, err, os.ErrNotExist) + assert.Nil(t, f) + + f, err = OpenFile(t.Context(), "non-existing.txt", os.O_WRONLY) + assert.ErrorIs(t, err, os.ErrNotExist) + assert.Nil(t, f) + + f, err = OpenFile(t.Context(), "non-existing.txt", os.O_WRONLY|os.O_APPEND|os.O_TRUNC) + assert.ErrorIs(t, err, os.ErrNotExist) + assert.Nil(t, f) + }) + + t.Run("Existing", func(t *testing.T) { + assertFileContent := func(f File, expected string) { + _, err := f.Seek(0, io.SeekStart) + require.NoError(t, err) + buf, err := io.ReadAll(f) + require.NoError(t, err) + assert.Equal(t, expected, string(buf)) + } + + f, err := OpenFile(t.Context(), "existing.txt", os.O_RDWR|os.O_CREATE) + require.NoError(t, err) + _, _ = f.Write([]byte("test")) + assertFileContent(f, "test") + assert.NoError(t, f.Close()) + + f, err = OpenFile(t.Context(), "existing.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND) + require.NoError(t, err) + _, _ = f.Write([]byte("\nnew")) + assertFileContent(f, "test\nnew") + assert.NoError(t, f.Close()) + + f, err = OpenFile(t.Context(), "existing.txt", os.O_RDWR|os.O_TRUNC) + require.NoError(t, err) + assertFileContent(f, "") + assert.NoError(t, f.Close()) + + f, err = OpenFile(t.Context(), "existing.txt", os.O_RDWR|os.O_CREATE|os.O_EXCL) + assert.ErrorIs(t, err, os.ErrExist) + assert.Nil(t, f) + }) } func TestDbfsReadWrite(t *testing.T) { - defer changeDefaultFileBlockSize(4)() + defer test.MockVariableValue(&defaultFileBlockSize, 4)() f1, err := OpenFile(t.Context(), "test.log", os.O_RDWR|os.O_CREATE) assert.NoError(t, err) @@ -157,30 +197,32 @@ func TestDbfsReadWrite(t *testing.T) { } func TestDbfsSeekWrite(t *testing.T) { - defer changeDefaultFileBlockSize(4)() + defer test.MockVariableValue(&defaultFileBlockSize, 4)() - f, err := OpenFile(t.Context(), "test2.log", os.O_RDWR|os.O_CREATE) - assert.NoError(t, err) - defer f.Close() + // write something + fw, err := OpenFile(t.Context(), "test2.log", os.O_RDWR|os.O_CREATE) + require.NoError(t, err) + defer fw.Close() - n, err := f.Write([]byte("111")) + n, err := fw.Write([]byte("111")) assert.NoError(t, err) - _, err = f.Seek(int64(n), io.SeekStart) + _, err = fw.Seek(int64(n), io.SeekStart) assert.NoError(t, err) - _, err = f.Write([]byte("222")) + _, err = fw.Write([]byte("222")) assert.NoError(t, err) - _, err = f.Seek(int64(n), io.SeekStart) + _, err = fw.Seek(int64(n), io.SeekStart) assert.NoError(t, err) - _, err = f.Write([]byte("333")) + _, err = fw.Write([]byte("333")) assert.NoError(t, err) + // then read it fr, err := OpenFile(t.Context(), "test2.log", os.O_RDONLY) - assert.NoError(t, err) - defer f.Close() + require.NoError(t, err) + defer fr.Close() buf, err := io.ReadAll(fr) assert.NoError(t, err) diff --git a/package/gitea/source/models/fixtures/action_artifact.yml b/package/gitea/source/models/fixtures/action_artifact.yml index ee8ef0d5..a25dfc20 100644 --- a/package/gitea/source/models/fixtures/action_artifact.yml +++ b/package/gitea/source/models/fixtures/action_artifact.yml @@ -141,3 +141,39 @@ created_unix: 1730330775 updated_unix: 1730330775 expired_unix: 1738106775 + +- + id: 26 + run_id: 792 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/pdf" + artifact_path: "report.pdf" + artifact_name: "report.pdf" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 + +- + id: 27 + run_id: 792 + runner_id: 1 + repo_id: 4 + owner_id: 1 + commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 + storage_path: "27/5/1730330775594233150.chunk" + file_size: 1024 + file_compressed_size: 1024 + content_encoding: "application/html" + artifact_path: "report.html" + artifact_name: "report.html" + status: 2 + created_unix: 1730330775 + updated_unix: 1730330775 + expired_unix: 1738106775 diff --git a/package/gitea/source/models/fixtures/action_run.yml b/package/gitea/source/models/fixtures/action_run.yml index 8018ae77..7552bb31 100644 --- a/package/gitea/source/models/fixtures/action_run.yml +++ b/package/gitea/source/models/fixtures/action_run.yml @@ -89,7 +89,7 @@ ref: "refs/heads/test" commit_sha: "c2d72f548424103f01ee1dc02889c1e2bff816b0" event: "push" - trigger_event: "push" + trigger_event: "schedule" is_fork_pull_request: 0 status: 1 started: 1683636528 @@ -141,7 +141,7 @@ approved_by: 0 - - id: 796 + id: 805 title: "update actions" repo_id: 4 owner_id: 1 diff --git a/package/gitea/source/models/fixtures/action_run_job.yml b/package/gitea/source/models/fixtures/action_run_job.yml index 789eb248..04799b73 100644 --- a/package/gitea/source/models/fixtures/action_run_job.yml +++ b/package/gitea/source/models/fixtures/action_run_job.yml @@ -131,8 +131,8 @@ stopped: 1683636626 - - id: 205 - run_id: 796 + id: 206 + run_id: 805 repo_id: 4 owner_id: 1 commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0 @@ -140,7 +140,7 @@ name: job_2 attempt: 1 job_id: job_2 - task_id: 55 + task_id: 56 status: 3 started: 1683636528 stopped: 1683636626 diff --git a/package/gitea/source/models/fixtures/action_task.yml b/package/gitea/source/models/fixtures/action_task.yml index 95d3ee25..e1bc588d 100644 --- a/package/gitea/source/models/fixtures/action_task.yml +++ b/package/gitea/source/models/fixtures/action_task.yml @@ -179,8 +179,7 @@ log_expired: 0 - - id: 55 - job_id: 205 + id: 56 attempt: 1 runner_id: 1 status: 3 # 3 is the status code for "cancelled" diff --git a/package/gitea/source/models/fixtures/attachment.yml b/package/gitea/source/models/fixtures/attachment.yml index 7882d8bf..570d4a27 100644 --- a/package/gitea/source/models/fixtures/attachment.yml +++ b/package/gitea/source/models/fixtures/attachment.yml @@ -153,3 +153,16 @@ download_count: 0 size: 0 created_unix: 946684800 + +- + id: 13 + uuid: a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a23 + repo_id: 1 + issue_id: 0 + release_id: 4 + uploader_id: 2 + comment_id: 0 + name: draft-attach + download_count: 0 + size: 0 + created_unix: 946684800 diff --git a/package/gitea/source/models/fixtures/badge.yml b/package/gitea/source/models/fixtures/badge.yml new file mode 100644 index 00000000..438cd0ca --- /dev/null +++ b/package/gitea/source/models/fixtures/badge.yml @@ -0,0 +1,5 @@ +- + id: 1 + slug: badge1 + description: just a test badge + image_url: badge1.png diff --git a/package/gitea/source/models/fixtures/branch.yml b/package/gitea/source/models/fixtures/branch.yml index 71723014..a1799909 100644 --- a/package/gitea/source/models/fixtures/branch.yml +++ b/package/gitea/source/models/fixtures/branch.yml @@ -225,3 +225,27 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +- + id: 27 + repo_id: 1 + name: 'DefaultBranch' + commit_id: '90c1019714259b24fb81711d4416ac0f18667dfa' + commit_message: 'add license' + commit_time: 1709345946 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 + +- + id: 28 + repo_id: 1 + name: 'sub-home-md-img-check' + commit_id: '4649299398e4d39a5c09eb4f534df6f1e1eb87cc' + commit_message: "Test how READMEs render images when found in a subfolder" + commit_time: 1678403550 + pusher_id: 1 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 diff --git a/package/gitea/source/models/fixtures/repo_unit.yml b/package/gitea/source/models/fixtures/repo_unit.yml index fa89e3f1..4c3e3750 100644 --- a/package/gitea/source/models/fixtures/repo_unit.yml +++ b/package/gitea/source/models/fixtures/repo_unit.yml @@ -736,6 +736,13 @@ - id: 111 + repo_id: 3 + type: 10 + config: "{}" + created_unix: 946684810 + +- + id: 112 repo_id: 4 type: 10 config: "{}" diff --git a/package/gitea/source/models/git/branch.go b/package/gitea/source/models/git/branch.go index 7fef9f5c..698c43f1 100644 --- a/package/gitea/source/models/git/branch.go +++ b/package/gitea/source/models/git/branch.go @@ -247,7 +247,7 @@ func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64 return err } for _, branch := range branches { - if err := AddDeletedBranch(ctx, repoID, branch.Name, doerID); err != nil { + if err := MarkBranchAsDeleted(ctx, repoID, branch.Name, doerID); err != nil { return err } } @@ -268,8 +268,8 @@ func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string }) } -// AddDeletedBranch adds a deleted branch to the database -func AddDeletedBranch(ctx context.Context, repoID int64, branchName string, deletedByID int64) error { +// MarkBranchAsDeleted marks branch as deleted +func MarkBranchAsDeleted(ctx context.Context, repoID int64, branchName string, deletedByID int64) error { branch, err := GetBranch(ctx, repoID, branchName) if err != nil { return err @@ -397,10 +397,16 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str if protectedBranch != nil { // there is a protect rule for this branch - protectedBranch.RuleName = to - if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil { + existingRule, err := GetProtectedBranchRuleByName(ctx, repo.ID, to) + if err != nil { return err } + if existingRule == nil || existingRule.ID == protectedBranch.ID { + protectedBranch.RuleName = to + if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil { + return err + } + } } else { // some glob protect rules may match this branch protected, err := IsBranchProtected(ctx, repo.ID, from) @@ -444,7 +450,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str type FindRecentlyPushedNewBranchesOptions struct { Repo *repo_model.Repository BaseRepo *repo_model.Repository - CommitAfterUnix int64 + PushedAfterUnix int64 MaxCount int } @@ -454,11 +460,11 @@ type RecentlyPushedNewBranch struct { BranchDisplayName string BranchLink string BranchCompareURL string - CommitTime timeutil.TimeStamp + PushedTime timeutil.TimeStamp } // FindRecentlyPushedNewBranches return at most 2 new branches pushed by the user in 2 hours which has no opened PRs created -// if opts.CommitAfterUnix is 0, we will find the branches that were committed to in the last 2 hours +// if opts.PushedAfterUnix is 0, we will find the branches that were pushed in the last 2 hours // if opts.ListOptions is not set, we will only display top 2 latest branches. // Protected branches will be skipped since they are unlikely to be used to create new PRs. func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, opts FindRecentlyPushedNewBranchesOptions) ([]*RecentlyPushedNewBranch, error) { @@ -486,16 +492,29 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o } repoIDs := builder.Select("id").From("repository").Where(repoCond) - if opts.CommitAfterUnix == 0 { - opts.CommitAfterUnix = time.Now().Add(-time.Hour * 2).Unix() + if opts.PushedAfterUnix == 0 { + opts.PushedAfterUnix = time.Now().Add(-time.Hour * 2).Unix() } - baseBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch) + var ignoredCommitIDs []string + baseDefaultBranch, err := GetBranch(ctx, opts.BaseRepo.ID, opts.BaseRepo.DefaultBranch) if err != nil { - return nil, err + log.Warn("GetBranch:DefaultBranch: %v", err) + } else { + ignoredCommitIDs = append(ignoredCommitIDs, baseDefaultBranch.CommitID) } - // find all related branches, these branches may already created PRs, we will check later + baseDefaultTargetBranchName := opts.BaseRepo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig().DefaultTargetBranch + if baseDefaultTargetBranchName != "" && baseDefaultTargetBranchName != opts.BaseRepo.DefaultBranch { + baseDefaultTargetBranch, err := GetBranch(ctx, opts.BaseRepo.ID, baseDefaultTargetBranchName) + if err != nil { + log.Warn("GetBranch:DefaultTargetBranch: %v", err) + } else { + ignoredCommitIDs = append(ignoredCommitIDs, baseDefaultTargetBranch.CommitID) + } + } + + // find all related branches, these branches may already have PRs, we will check later var branches []*Branch if err := db.GetEngine(ctx). Where(builder.And( @@ -503,10 +522,10 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o "pusher_id": doer.ID, "is_deleted": false, }, - builder.Gte{"commit_time": opts.CommitAfterUnix}, + builder.Gte{"updated_unix": opts.PushedAfterUnix}, builder.In("repo_id", repoIDs), // newly created branch have no changes, so skip them - builder.Neq{"commit_id": baseBranch.CommitID}, + builder.NotIn("commit_id", ignoredCommitIDs), )). OrderBy(db.SearchOrderByRecentUpdated.String()). Find(&branches); err != nil { @@ -514,10 +533,8 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o } newBranches := make([]*RecentlyPushedNewBranch, 0, len(branches)) - if opts.MaxCount == 0 { - // by default we display 2 recently pushed new branch - opts.MaxCount = 2 - } + opts.MaxCount = util.IfZero(opts.MaxCount, 2) // by default, we display 2 recently pushed new branch + baseTargetBranchName := opts.BaseRepo.GetPullRequestTargetBranch(ctx) for _, branch := range branches { // whether the branch is protected protected, err := IsBranchProtected(ctx, branch.RepoID, branch.Name) @@ -555,8 +572,8 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o BranchDisplayName: branchDisplayName, BranchName: branch.Name, BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)), - BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name), - CommitTime: branch.CommitTime, + BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, baseTargetBranchName, branch.Name), + PushedTime: branch.UpdatedUnix, }) } if len(newBranches) == opts.MaxCount { @@ -566,3 +583,12 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o return newBranches, nil } + +// CountBranches returns the number of branches in the repository +func CountBranches(ctx context.Context, repoID int64, includeDeleted bool) (int64, error) { + sess := db.GetEngine(ctx).Where("repo_id=?", repoID) + if !includeDeleted { + sess.And("is_deleted=?", false) + } + return sess.Count(new(Branch)) +} diff --git a/package/gitea/source/models/git/branch_test.go b/package/gitea/source/models/git/branch_test.go index 5be43517..3832df93 100644 --- a/package/gitea/source/models/git/branch_test.go +++ b/package/gitea/source/models/git/branch_test.go @@ -6,14 +6,17 @@ package git_test import ( "context" "testing" + "time" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" ) @@ -25,8 +28,8 @@ func TestAddDeletedBranch(t *testing.T) { firstBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{ID: 1}) assert.True(t, firstBranch.IsDeleted) - assert.NoError(t, git_model.AddDeletedBranch(t.Context(), repo.ID, firstBranch.Name, firstBranch.DeletedByID)) - assert.NoError(t, git_model.AddDeletedBranch(t.Context(), repo.ID, "branch2", int64(1))) + assert.NoError(t, git_model.MarkBranchAsDeleted(t.Context(), repo.ID, firstBranch.Name, firstBranch.DeletedByID)) + assert.NoError(t, git_model.MarkBranchAsDeleted(t.Context(), repo.ID, "branch2", int64(1))) secondBranch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo.ID, Name: "branch2"}) assert.True(t, secondBranch.IsDeleted) @@ -63,6 +66,36 @@ func TestGetDeletedBranch(t *testing.T) { assert.NotNil(t, getDeletedBranch(t, firstBranch)) } +func TestFindRecentlyPushedNewBranchesUsesPushTime(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 12}) + branch := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo.ID, Name: "outdated-new-branch"}) + + commitUnix := time.Now().Add(-3 * time.Hour).Unix() + pushUnix := time.Now().Add(-30 * time.Minute).Unix() + _, err := db.GetEngine(t.Context()).Exec( + "UPDATE branch SET commit_time = ?, updated_unix = ? WHERE id = ?", + commitUnix, + pushUnix, + branch.ID, + ) + assert.NoError(t, err) + + branches, err := git_model.FindRecentlyPushedNewBranches(t.Context(), doer, git_model.FindRecentlyPushedNewBranchesOptions{ + Repo: repo, + BaseRepo: repo, + PushedAfterUnix: time.Now().Add(-time.Hour).Unix(), + MaxCount: 1, + }) + assert.NoError(t, err) + if assert.Len(t, branches, 1) { + assert.Equal(t, branch.Name, branches[0].BranchName) + assert.Equal(t, timeutil.TimeStamp(pushUnix), branches[0].PushedTime) + } +} + func TestDeletedBranchLoadUser(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) @@ -114,7 +147,7 @@ func TestFindRenamedBranch(t *testing.T) { assert.True(t, exist) assert.Equal(t, "master", branch.To) - _, exist, err = git_model.FindRenamedBranch(t.Context(), 1, "unknow") + _, exist, err = git_model.FindRenamedBranch(t.Context(), 1, "unknown") assert.NoError(t, err) assert.False(t, exist) } @@ -159,6 +192,53 @@ func TestRenameBranch(t *testing.T) { }) } +func TestRenameBranchProtectedRuleConflict(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + master := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "master"}) + + devBranch := &git_model.Branch{ + RepoID: repo1.ID, + Name: "dev", + CommitID: master.CommitID, + CommitMessage: master.CommitMessage, + CommitTime: master.CommitTime, + PusherID: master.PusherID, + } + assert.NoError(t, db.Insert(t.Context(), devBranch)) + + pbDev := git_model.ProtectedBranch{ + RepoID: repo1.ID, + RuleName: "dev", + CanPush: true, + } + assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbDev, git_model.WhitelistOptions{})) + + pbMain := git_model.ProtectedBranch{ + RepoID: repo1.ID, + RuleName: "main", + CanPush: true, + } + assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbMain, git_model.WhitelistOptions{})) + + assert.NoError(t, git_model.RenameBranch(t.Context(), repo1, "dev", "main", func(ctx context.Context, isDefault bool) error { + return nil + })) + + unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "dev"}) + unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "main"}) + + protectedDev, err := git_model.GetProtectedBranchRuleByName(t.Context(), repo1.ID, "dev") + assert.NoError(t, err) + assert.NotNil(t, protectedDev) + assert.Equal(t, "dev", protectedDev.RuleName) + + protectedMainByID, err := git_model.GetProtectedBranchRuleByID(t.Context(), repo1.ID, pbMain.ID) + assert.NoError(t, err) + assert.NotNil(t, protectedMainByID) + assert.Equal(t, "main", protectedMainByID.RuleName) +} + func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) @@ -183,3 +263,25 @@ func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, deletedBranch) } + +func TestCountBranches(t *testing.T) { + // 1. Setup - Exactly like TestAddDeletedBranch + assert.NoError(t, unittest.PrepareTestDatabase()) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + // 2. Execution - Using t.Context() to match the rest of the file + initialCount, err := git_model.CountBranches(t.Context(), repo.ID, false) + assert.NoError(t, err) + + // 3. Database Action - Using t.Context() + err = db.Insert(t.Context(), &git_model.Branch{ + RepoID: repo.ID, + Name: "test-branch-for-counting", + }) + assert.NoError(t, err) + + // 4. Verification + newCount, err := git_model.CountBranches(t.Context(), repo.ID, false) + assert.NoError(t, err) + assert.Equal(t, initialCount+1, newCount) +} diff --git a/package/gitea/source/models/git/commit_status.go b/package/gitea/source/models/git/commit_status.go index e255bca5..2ae5937a 100644 --- a/package/gitea/source/models/git/commit_status.go +++ b/package/gitea/source/models/git/commit_status.go @@ -30,17 +30,21 @@ import ( // CommitStatus holds a single Status of a single Commit type CommitStatus struct { - ID int64 `xorm:"pk autoincr"` - Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` - RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` - Repo *repo_model.Repository `xorm:"-"` - State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` - SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` - TargetURL string `xorm:"TEXT"` - Description string `xorm:"TEXT"` - ContextHash string `xorm:"VARCHAR(64) index"` - Context string `xorm:"TEXT"` - Creator *user_model.User `xorm:"-"` + ID int64 `xorm:"pk autoincr"` + Index int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` + RepoID int64 `xorm:"INDEX UNIQUE(repo_sha_index)"` + Repo *repo_model.Repository `xorm:"-"` + State commitstatus.CommitStatusState `xorm:"VARCHAR(7) NOT NULL"` + SHA string `xorm:"VARCHAR(64) NOT NULL INDEX UNIQUE(repo_sha_index)"` + + // TargetURL points to the commit status page reported by a CI system + // If Gitea Actions is used, it is a relative link like "{RepoLink}/actions/runs/{RunID}/jobs{JobID}" + TargetURL string `xorm:"TEXT"` + + Description string `xorm:"TEXT"` + ContextHash string `xorm:"VARCHAR(64) index"` + Context string `xorm:"TEXT"` + Creator *user_model.User `xorm:"-"` CreatorID int64 CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` @@ -211,21 +215,45 @@ func (status *CommitStatus) LocaleString(lang translation.Locale) string { // HideActionsURL set `TargetURL` to an empty string if the status comes from Gitea Actions func (status *CommitStatus) HideActionsURL(ctx context.Context) { + if _, ok := status.cutTargetURLGiteaActionsPrefix(ctx); ok { + status.TargetURL = "" + } +} + +func (status *CommitStatus) cutTargetURLGiteaActionsPrefix(ctx context.Context) (string, bool) { if status.RepoID == 0 { - return + return "", false } if status.Repo == nil { if err := status.loadRepository(ctx); err != nil { log.Error("loadRepository: %v", err) - return + return "", false } } prefix := status.Repo.Link() + "/actions" - if strings.HasPrefix(status.TargetURL, prefix) { - status.TargetURL = "" + return strings.CutPrefix(status.TargetURL, prefix) +} + +// ParseGiteaActionsTargetURL parses the commit status target URL as Gitea Actions link +func (status *CommitStatus) ParseGiteaActionsTargetURL(ctx context.Context) (runID, jobID int64, ok bool) { + s, ok := status.cutTargetURLGiteaActionsPrefix(ctx) + if !ok { + return 0, 0, false } + + parts := strings.Split(s, "/") // expect: /runs/{runID}/jobs/{jobID} + if len(parts) < 5 || parts[1] != "runs" || parts[3] != "jobs" { + return 0, 0, false + } + + runID, err1 := strconv.ParseInt(parts[2], 10, 64) + jobID, err2 := strconv.ParseInt(parts[4], 10, 64) + if err1 != nil || err2 != nil { + return 0, 0, false + } + return runID, jobID, true } // CalcCommitStatus returns commit status state via some status, the commit statues should order by id desc diff --git a/package/gitea/source/models/git/commit_status_test.go b/package/gitea/source/models/git/commit_status_test.go index d1b9dfc3..f18d3470 100644 --- a/package/gitea/source/models/git/commit_status_test.go +++ b/package/gitea/source/models/git/commit_status_test.go @@ -139,7 +139,7 @@ func Test_CalcCommitStatus(t *testing.T) { }, }, expected: &git_model.CommitStatus{ - State: commitstatus.CommitStatusPending, + State: commitstatus.CommitStatusFailure, }, }, { @@ -243,7 +243,7 @@ func TestCommitStatusesHideActionsURL(t *testing.T) { statuses := []*git_model.CommitStatus{ { RepoID: repo.ID, - TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), run.Index), + TargetURL: fmt.Sprintf("%s/jobs/%d", run.Link(), run.ID), }, { RepoID: repo.ID, diff --git a/package/gitea/source/models/git/lfs.go b/package/gitea/source/models/git/lfs.go index 8bba060f..1f3d086a 100644 --- a/package/gitea/source/models/git/lfs.go +++ b/package/gitea/source/models/git/lfs.go @@ -8,7 +8,6 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -42,30 +41,6 @@ func (err ErrLFSLockNotExist) Unwrap() error { return util.ErrNotExist } -// ErrLFSUnauthorizedAction represents a "LFSUnauthorizedAction" kind of error. -type ErrLFSUnauthorizedAction struct { - RepoID int64 - UserName string - Mode perm.AccessMode -} - -// IsErrLFSUnauthorizedAction checks if an error is a ErrLFSUnauthorizedAction. -func IsErrLFSUnauthorizedAction(err error) bool { - _, ok := err.(ErrLFSUnauthorizedAction) - return ok -} - -func (err ErrLFSUnauthorizedAction) Error() string { - if err.Mode == perm.AccessModeWrite { - return fmt.Sprintf("User %s doesn't have write access for lfs lock [rid: %d]", err.UserName, err.RepoID) - } - return fmt.Sprintf("User %s doesn't have read access for lfs lock [rid: %d]", err.UserName, err.RepoID) -} - -func (err ErrLFSUnauthorizedAction) Unwrap() error { - return util.ErrPermissionDenied -} - // ErrLFSLockAlreadyExist represents a "LFSLockAlreadyExist" kind of error. type ErrLFSLockAlreadyExist struct { RepoID int64 @@ -93,12 +68,6 @@ type ErrLFSFileLocked struct { UserName string } -// IsErrLFSFileLocked checks if an error is a ErrLFSFileLocked. -func IsErrLFSFileLocked(err error) bool { - _, ok := err.(ErrLFSFileLocked) - return ok -} - func (err ErrLFSFileLocked) Error() string { return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path) } @@ -343,15 +312,12 @@ func IterateRepositoryIDsWithLFSMetaObjects(ctx context.Context, f func(ctx cont // IterateLFSMetaObjectsForRepoOptions provides options for IterateLFSMetaObjectsForRepo type IterateLFSMetaObjectsForRepoOptions struct { - OlderThan timeutil.TimeStamp - UpdatedLessRecentlyThan timeutil.TimeStamp - OrderByUpdated bool - LoopFunctionAlwaysUpdates bool + OlderThan timeutil.TimeStamp + UpdatedLessRecentlyThan timeutil.TimeStamp } // IterateLFSMetaObjectsForRepo provides a iterator for LFSMetaObjects per Repo func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(context.Context, *LFSMetaObject, int64) error, opts *IterateLFSMetaObjectsForRepoOptions) error { - var start int batchSize := setting.Database.IterateBufferSize engine := db.GetEngine(ctx) type CountLFSMetaObject struct { @@ -359,7 +325,7 @@ func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(cont LFSMetaObject `xorm:"extends"` } - id := int64(0) + lastID := int64(0) for { beans := make([]*CountLFSMetaObject, 0, batchSize) @@ -372,29 +338,23 @@ func IterateLFSMetaObjectsForRepo(ctx context.Context, repoID int64, f func(cont if !opts.UpdatedLessRecentlyThan.IsZero() { sess.And("`lfs_meta_object`.updated_unix < ?", opts.UpdatedLessRecentlyThan) } - sess.GroupBy("`lfs_meta_object`.id") - if opts.OrderByUpdated { - sess.OrderBy("`lfs_meta_object`.updated_unix ASC") - } else { - sess.And("`lfs_meta_object`.id > ?", id) - sess.OrderBy("`lfs_meta_object`.id ASC") - } - if err := sess.Limit(batchSize, start).Find(&beans); err != nil { + sess.GroupBy("`lfs_meta_object`.id"). + And("`lfs_meta_object`.id > ?", lastID). + OrderBy("`lfs_meta_object`.id ASC") + + if err := sess.Limit(batchSize).Find(&beans); err != nil { return err } if len(beans) == 0 { return nil } - if !opts.LoopFunctionAlwaysUpdates { - start += len(beans) - } for _, bean := range beans { if err := f(ctx, &bean.LFSMetaObject, bean.Count); err != nil { return err } } - id = beans[len(beans)-1].ID + lastID = beans[len(beans)-1].ID } } diff --git a/package/gitea/source/models/git/lfs_lock.go b/package/gitea/source/models/git/lfs_lock.go index 465b788a..8e63598f 100644 --- a/package/gitea/source/models/git/lfs_lock.go +++ b/package/gitea/source/models/git/lfs_lock.go @@ -11,10 +11,7 @@ import ( "time" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -71,10 +68,6 @@ func (l *LFSLock) LoadOwner(ctx context.Context) error { // CreateLFSLock creates a new lock. func CreateLFSLock(ctx context.Context, repo *repo_model.Repository, lock *LFSLock) (*LFSLock, error) { return db.WithTx2(ctx, func(ctx context.Context) (*LFSLock, error) { - if err := CheckLFSAccessForRepo(ctx, lock.OwnerID, repo, perm.AccessModeWrite); err != nil { - return nil, err - } - lock.Path = util.PathJoinRel(lock.Path) lock.RepoID = repo.ID @@ -137,7 +130,7 @@ func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) ( // GetTreePathLock returns LSF lock for the treePath func GetTreePathLock(ctx context.Context, repoID int64, treePath string) (*LFSLock, error) { if !setting.LFS.StartServer { - return nil, nil + return nil, nil //nolint:nilnil // return nil when LFS is not started } locks, err := GetLFSLockByRepoID(ctx, repoID, 0, 0) @@ -149,7 +142,7 @@ func GetTreePathLock(ctx context.Context, repoID int64, treePath string) (*LFSLo return lock, nil } } - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } // CountLFSLockByRepoID returns a count of all LFSLocks associated with a repository. @@ -165,10 +158,6 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor return nil, err } - if err := CheckLFSAccessForRepo(ctx, u.ID, repo, perm.AccessModeWrite); err != nil { - return nil, err - } - if !force && u.ID != lock.OwnerID { return nil, errors.New("user doesn't own lock and force flag is not set") } @@ -180,22 +169,3 @@ func DeleteLFSLockByID(ctx context.Context, id int64, repo *repo_model.Repositor return lock, nil }) } - -// CheckLFSAccessForRepo check needed access mode base on action -func CheckLFSAccessForRepo(ctx context.Context, ownerID int64, repo *repo_model.Repository, mode perm.AccessMode) error { - if ownerID == 0 { - return ErrLFSUnauthorizedAction{repo.ID, "undefined", mode} - } - u, err := user_model.GetUserByID(ctx, ownerID) - if err != nil { - return err - } - perm, err := access_model.GetUserRepoPermission(ctx, repo, u) - if err != nil { - return err - } - if !perm.CanAccess(mode, unit.TypeCode) { - return ErrLFSUnauthorizedAction{repo.ID, u.DisplayName(), mode} - } - return nil -} diff --git a/package/gitea/source/models/git/lfs_test.go b/package/gitea/source/models/git/lfs_test.go new file mode 100644 index 00000000..4c0242f4 --- /dev/null +++ b/package/gitea/source/models/git/lfs_test.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git_test + +import ( + "bytes" + "context" + "strconv" + "testing" + "time" + + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/lfs" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/assert" +) + +func TestIterateLFSMetaObjectsForRepoUpdatesDoNotSkip(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + ctx := t.Context() + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, "user2", "repo1") + assert.NoError(t, err) + + defer test.MockVariableValue(&setting.Database.IterateBufferSize, 1)() + + created := make([]*git_model.LFSMetaObject, 0, 3) + for i := range 3 { + content := []byte("gitea-lfs-" + strconv.Itoa(i)) + pointer, err := lfs.GeneratePointer(bytes.NewReader(content)) + assert.NoError(t, err) + + meta, err := git_model.NewLFSMetaObject(ctx, repo.ID, pointer) + assert.NoError(t, err) + created = append(created, meta) + } + + iterated := make([]int64, 0, len(created)) + cutoff := time.Now().Add(24 * time.Hour) + iterErr := git_model.IterateLFSMetaObjectsForRepo(ctx, repo.ID, func(ctx context.Context, meta *git_model.LFSMetaObject, count int64) error { + iterated = append(iterated, meta.ID) + _, err := db.GetEngine(ctx).ID(meta.ID).Cols("updated_unix").Update(&git_model.LFSMetaObject{ + UpdatedUnix: timeutil.TimeStamp(time.Now().Unix()), + }) + return err + }, &git_model.IterateLFSMetaObjectsForRepoOptions{ + OlderThan: timeutil.TimeStamp(cutoff.Unix()), + UpdatedLessRecentlyThan: timeutil.TimeStamp(cutoff.Unix()), + }) + assert.NoError(t, iterErr) + + expected := []int64{created[0].ID, created[1].ID, created[2].ID} + assert.Equal(t, expected, iterated) +} diff --git a/package/gitea/source/models/git/protected_branch.go b/package/gitea/source/models/git/protected_branch.go index 1085c14c..f242f94f 100644 --- a/package/gitea/source/models/git/protected_branch.go +++ b/package/gitea/source/models/git/protected_branch.go @@ -318,7 +318,7 @@ func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName st if err != nil { return nil, err } else if !exist { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return rel, nil } @@ -329,7 +329,7 @@ func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*Pro if err != nil { return nil, err } else if !exist { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return rel, nil } @@ -495,9 +495,9 @@ func updateUserWhitelist(ctx context.Context, repo *repo_model.Repository, curre if err != nil { return nil, fmt.Errorf("GetUserByID [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err) } - perm, err := access_model.GetUserRepoPermission(ctx, repo, user) + perm, err := access_model.GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { - return nil, fmt.Errorf("GetUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err) + return nil, fmt.Errorf("GetIndividualUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err) } if !perm.CanWrite(unit.TypeCode) { diff --git a/package/gitea/source/models/git/protected_tag.go b/package/gitea/source/models/git/protected_tag.go index 95642df5..dc38daf9 100644 --- a/package/gitea/source/models/git/protected_tag.go +++ b/package/gitea/source/models/git/protected_tag.go @@ -104,7 +104,7 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) { return nil, err } if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return tag, nil } @@ -117,7 +117,7 @@ func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern str return nil, err } if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return tag, nil } diff --git a/package/gitea/source/models/issues/comment.go b/package/gitea/source/models/issues/comment.go index fd050083..b6676667 100644 --- a/package/gitea/source/models/issues/comment.go +++ b/package/gitea/source/models/issues/comment.go @@ -7,6 +7,7 @@ package issues import ( "context" + "errors" "fmt" "html/template" "slices" @@ -20,6 +21,8 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/references" @@ -233,11 +236,17 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string { return lang.TrString("repo.issues.role." + string(r) + "_helper") } +type SpecialDoerNameType string + +const SpecialDoerNameCodeOwners SpecialDoerNameType = "CODEOWNERS" + // CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database type CommentMetaData struct { ProjectColumnID int64 `json:"project_column_id,omitempty"` ProjectColumnTitle string `json:"project_column_title,omitempty"` ProjectTitle string `json:"project_title,omitempty"` + + SpecialDoerName SpecialDoerNameType `json:"special_doer_name,omitempty"` // e.g. "CODEOWNERS" for CODEOWNERS-triggered review requests } // Comment represents a comment in commit and issue page. @@ -319,21 +328,34 @@ type Comment struct { RefIssue *Issue `xorm:"-"` RefComment *Comment `xorm:"-"` - Commits []*git_model.SignCommitWithStatuses `xorm:"-"` - OldCommit string `xorm:"-"` - NewCommit string `xorm:"-"` - CommitsNum int64 `xorm:"-"` - IsForcePush bool `xorm:"-"` + Commits []*git_model.SignCommitWithStatuses `xorm:"-"` + OldCommit string `xorm:"-"` + NewCommit string `xorm:"-"` + CommitsNum int64 `xorm:"-"` + + // Templates still use it. It is not persisted in database, it is only set when creating or loading + IsForcePush bool `xorm:"-"` } func init() { db.RegisterModel(new(Comment)) } -// PushActionContent is content of push pull comment +// PushActionContent is content of pull request's push comment type PushActionContent struct { - IsForcePush bool `json:"is_force_push"` - CommitIDs []string `json:"commit_ids"` + IsForcePush bool `json:"is_force_push"` + // if IsForcePush=true, CommitIDs contains the commit pair [old head, new head] + // if IsForcePush=false, CommitIDs contains the new commits newly pushed to the head branch + CommitIDs []string `json:"commit_ids"` +} + +func (c *Comment) GetPushActionContent() (*PushActionContent, error) { + if c.Type != CommentTypePullRequestPush { + return nil, errors.New("not a pull request push comment") + } + var data PushActionContent + _ = json.Unmarshal(util.UnsafeStringToBytes(c.Content), &data) + return &data, nil } // LoadIssue loads the issue reference for the comment @@ -377,16 +399,7 @@ func (c *Comment) LoadPoster(ctx context.Context) (err error) { if c.Poster != nil { return nil } - - c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - c.PosterID = user_model.GhostUserID - c.Poster = user_model.NewGhostUser() - } else { - log.Error("getUserByID[%d]: %v", c.ID, err) - } - } + c.PosterID, c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID) return err } @@ -692,7 +705,7 @@ func (c *Comment) LoadTime(ctx context.Context) error { return nil } var err error - c.Time, err = GetTrackedTimeByID(ctx, c.TimeID) + c.Time, err = GetTrackedTimeByID(ctx, c.IssueID, c.TimeID) return err } @@ -764,6 +777,37 @@ func (c *Comment) CodeCommentLink(ctx context.Context) string { return fmt.Sprintf("%s/files#%s", c.Issue.Link(), c.HashTag()) } +func (c *Comment) MetaSpecialDoerTr(locale translation.Locale) template.HTML { + if c.CommentMetaData == nil { + return "" + } + if c.CommentMetaData.SpecialDoerName == SpecialDoerNameCodeOwners { + return locale.Tr("repo.issues.review.codeowners_rules") + } + return htmlutil.HTMLFormat("%s", c.CommentMetaData.SpecialDoerName) +} + +func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdStr template.HTML) template.HTML { + if c.AssigneeID > 0 { + // it guarantees LoadAssigneeUserAndTeam has been called, and c.Assignee is Ghost user but not nil if the user doesn't exist + if c.RemovedAssignee { + if c.PosterID == c.AssigneeID { + return locale.Tr("repo.issues.review.remove_review_request_self", createdStr) + } + return locale.Tr("repo.issues.review.remove_review_request", c.Assignee.GetDisplayName(), createdStr) + } + return locale.Tr("repo.issues.review.add_review_request", c.Assignee.GetDisplayName(), createdStr) + } + teamName := "Ghost Team" + if c.AssigneeTeam != nil { + teamName = c.AssigneeTeam.Name + } + if c.RemovedAssignee { + return locale.Tr("repo.issues.review.remove_review_request", teamName, createdStr) + } + return locale.Tr("repo.issues.review.add_review_request", teamName, createdStr) +} + // CreateComment creates comment with context func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) { return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { @@ -780,6 +824,11 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, ProjectTitle: opts.ProjectTitle, } } + if opts.SpecialDoerName != "" { + commentMetaData = &CommentMetaData{ + SpecialDoerName: opts.SpecialDoerName, + } + } comment := &Comment{ Type: opts.Type, @@ -976,6 +1025,7 @@ type CreateCommentOptions struct { RefIsPull bool IsForcePush bool Invalidated bool + SpecialDoerName SpecialDoerNameType // e.g. "CODEOWNERS" for CODEOWNERS-triggered review requests } // GetCommentByID returns the comment by given ID. @@ -990,6 +1040,20 @@ func GetCommentByID(ctx context.Context, id int64) (*Comment, error) { return c, nil } +func GetCommentWithRepoID(ctx context.Context, repoID, commentID int64) (*Comment, error) { + c, err := GetCommentByID(ctx, commentID) + if err != nil { + return nil, err + } + if err := c.LoadIssue(ctx); err != nil { + return nil, err + } + if c.Issue.RepoID != repoID { + return nil, ErrCommentNotExist{commentID, 0} + } + return c, nil +} + // FindCommentsOptions describes the conditions to Find comments type FindCommentsOptions struct { db.ListOptions diff --git a/package/gitea/source/models/issues/comment_code.go b/package/gitea/source/models/issues/comment_code.go index 55e67a12..8de52f81 100644 --- a/package/gitea/source/models/issues/comment_code.go +++ b/package/gitea/source/models/issues/comment_code.go @@ -102,6 +102,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu continue } comment.Review = re + comment.Issue = issue } comments[n] = comment n++ diff --git a/package/gitea/source/models/issues/issue.go b/package/gitea/source/models/issues/issue.go index 053b96dc..d3e7cb23 100644 --- a/package/gitea/source/models/issues/issue.go +++ b/package/gitea/source/models/issues/issue.go @@ -190,17 +190,10 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool { // LoadPoster loads poster func (issue *Issue) LoadPoster(ctx context.Context) (err error) { - if issue.Poster == nil && issue.PosterID != 0 { - issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID) - if err != nil { - issue.PosterID = user_model.GhostUserID - issue.Poster = user_model.NewGhostUser() - if !user_model.IsErrUserNotExist(err) { - return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err) - } - return nil - } + if issue.Poster != nil { + return nil } + issue.PosterID, issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID) return err } @@ -498,7 +491,7 @@ func (issue *Issue) GetLastComment(ctx context.Context) (*Comment, error) { return nil, err } if !exist { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return &c, nil } @@ -682,7 +675,7 @@ func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, erro } // BlockedByDependencies finds all Dependencies an issue is blocked by -func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) { +func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, total int64, err error) { sess := db.GetEngine(ctx). Table("issue"). Join("INNER", "repository", "repository.id = issue.repo_id"). @@ -693,13 +686,13 @@ func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptio if opts.Page > 0 { sess = db.SetSessionPagination(sess, &opts) } - err = sess.Find(&issueDeps) + total, err = sess.FindAndCount(&issueDeps) for _, depInfo := range issueDeps { depInfo.Issue.Repo = &depInfo.Repository } - return issueDeps, err + return issueDeps, total, err } // BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks diff --git a/package/gitea/source/models/issues/issue_label.go b/package/gitea/source/models/issues/issue_label.go index 151469a9..697448cf 100644 --- a/package/gitea/source/models/issues/issue_label.go +++ b/package/gitea/source/models/issues/issue_label.go @@ -356,7 +356,7 @@ func ClearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User) return err } - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + perm, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer) if err != nil { return err } diff --git a/package/gitea/source/models/issues/issue_project.go b/package/gitea/source/models/issues/issue_project.go index 01852447..3bb09363 100644 --- a/package/gitea/source/models/issues/issue_project.go +++ b/package/gitea/source/models/issues/issue_project.go @@ -64,36 +64,6 @@ func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID i return result, nil } -// LoadIssuesFromColumn load issues assigned to this column -func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) { - issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) { - o.ProjectColumnID = b.ID - o.ProjectID = b.ProjectID - o.SortType = "project-column-sorting" - })) - if err != nil { - return nil, err - } - - if b.Default { - issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) { - o.ProjectColumnID = db.NoConditionID - o.ProjectID = b.ProjectID - o.SortType = "project-column-sorting" - })) - if err != nil { - return nil, err - } - issueList = append(issueList, issues...) - } - - if err := issueList.LoadComments(ctx); err != nil { - return nil, err - } - - return issueList, nil -} - // IssueAssignOrRemoveProject changes the project associated with an issue // If newProjectID is 0, the issue is removed from the project func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error { diff --git a/package/gitea/source/models/issues/issue_update.go b/package/gitea/source/models/issues/issue_update.go index 0a320ffc..01a3eb9a 100644 --- a/package/gitea/source/models/issues/issue_update.go +++ b/package/gitea/source/models/issues/issue_update.go @@ -665,9 +665,9 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u continue } // Normal users must have read access to the referencing issue - perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user) + perm, err := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, user) if err != nil { - return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err) + return nil, fmt.Errorf("GetIndividualUserRepoPermission [%d]: %w", user.ID, err) } if !perm.CanReadIssuesOrPulls(issue.IsPull) { continue diff --git a/package/gitea/source/models/issues/issue_xref.go b/package/gitea/source/models/issues/issue_xref.go index f8495929..41a867c8 100644 --- a/package/gitea/source/models/issues/issue_xref.go +++ b/package/gitea/source/models/issues/issue_xref.go @@ -207,7 +207,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe // Check doer permissions; set action to None if the doer can't change the destination if refIssue.RepoID != ctx.OrigIssue.RepoID || ref.Action != references.XRefActionNone { - perm, err := access_model.GetUserRepoPermission(stdCtx, refIssue.Repo, ctx.Doer) + perm, err := access_model.GetDoerRepoPermission(stdCtx, refIssue.Repo, ctx.Doer) if err != nil { return nil, references.XRefActionNone, err } diff --git a/package/gitea/source/models/issues/label_test.go b/package/gitea/source/models/issues/label_test.go index 50393855..42f774e7 100644 --- a/package/gitea/source/models/issues/label_test.go +++ b/package/gitea/source/models/issues/label_test.go @@ -254,7 +254,7 @@ func TestGetLabelsByIssueID(t *testing.T) { func TestUpdateLabel(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) - // make sure update wont overwrite it + // make sure update won't overwrite it update := &issues_model.Label{ ID: label.ID, Color: "#ffff00", diff --git a/package/gitea/source/models/issues/milestone.go b/package/gitea/source/models/issues/milestone.go index 82a82ac9..1dd86302 100644 --- a/package/gitea/source/models/issues/milestone.go +++ b/package/gitea/source/models/issues/milestone.go @@ -75,6 +75,8 @@ func init() { func (m *Milestone) BeforeUpdate() { if m.NumIssues > 0 { m.Completeness = m.NumClosedIssues * 100 / m.NumIssues + } else if m.IsClosed { + m.Completeness = 100 } else { m.Completeness = 0 } @@ -195,8 +197,8 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error { if err != nil { return err } - _, err = e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?", - id, + _, err = e.Exec("UPDATE `milestone` SET completeness=(CASE WHEN is_closed = ? AND num_issues = 0 THEN 100 ELSE 100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) END) WHERE id=?", + true, id, ) return err } @@ -240,6 +242,11 @@ func changeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) err if count < 1 { return nil } + + if err := UpdateMilestoneCounters(ctx, m.ID); err != nil { + return err + } + return updateRepoMilestoneNum(ctx, m.RepoID) } diff --git a/package/gitea/source/models/issues/milestone_list.go b/package/gitea/source/models/issues/milestone_list.go index 955ab235..021b8beb 100644 --- a/package/gitea/source/models/issues/milestone_list.go +++ b/package/gitea/source/models/issues/milestone_list.go @@ -24,6 +24,18 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 { return ids } +// SplitByOpenClosed splits the milestone list into open and closed milestones +func (milestones MilestoneList) SplitByOpenClosed() (open, closed MilestoneList) { + for _, m := range milestones { + if m.IsClosed { + closed = append(closed, m) + } else { + open = append(open, m) + } + } + return open, closed +} + // FindMilestoneOptions contain options to get milestones type FindMilestoneOptions struct { db.ListOptions diff --git a/package/gitea/source/models/issues/pull.go b/package/gitea/source/models/issues/pull.go index 5669bed6..1b883f29 100644 --- a/package/gitea/source/models/issues/pull.go +++ b/package/gitea/source/models/issues/pull.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "regexp" "strings" "code.gitea.io/gitea/models/db" @@ -24,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" + "github.com/dlclark/regexp2" "xorm.io/builder" ) @@ -417,10 +417,6 @@ func (pr *PullRequest) GetGitHeadRefName() string { return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index) } -func (pr *PullRequest) GetGitHeadBranchRefName() string { - return fmt.Sprintf("%s%s", git.BranchPrefix, pr.HeadBranch) -} - // GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR) func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int { opts := FindCommentsOptions{ @@ -646,9 +642,8 @@ func (pr *PullRequest) UpdateCols(ctx context.Context, cols ...string) error { } // UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged -func (pr *PullRequest) UpdateColsIfNotMerged(ctx context.Context, cols ...string) error { - _, err := db.GetEngine(ctx).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr) - return err +func (pr *PullRequest) UpdateColsIfNotMerged(ctx context.Context, cols ...string) (int64, error) { + return db.GetEngine(ctx).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr) } // IsWorkInProgress determine if the Pull Request is a Work In Progress by its title @@ -663,17 +658,24 @@ func (pr *PullRequest) IsWorkInProgress(ctx context.Context) bool { // HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix func HasWorkInProgressPrefix(title string) bool { - for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { - if strings.HasPrefix(strings.ToUpper(title), strings.ToUpper(prefix)) { - return true - } - } - return false + _, ok := CutWorkInProgressPrefix(title) + return ok } -// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch. +func CutWorkInProgressPrefix(title string) (origTitle string, ok bool) { + for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes { + prefixLen := len(prefix) + if prefixLen <= len(title) && util.AsciiEqualFold(title[:prefixLen], prefix) { + return title[len(prefix):], true + } + } + return title, false +} + +// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch. +// Sometimes a conflict may not list any files func (pr *PullRequest) IsFilesConflicted() bool { - return len(pr.ConflictedFiles) > 0 + return pr.Status == PullRequestStatusConflict } // GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress. @@ -859,7 +861,7 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul } type CodeOwnerRule struct { - Rule *regexp.Regexp + Rule *regexp2.Regexp // it supports negative lookahead, does better for end users Negative bool Users []*user_model.User Teams []*org_model.Team @@ -875,7 +877,13 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule, warnings := make([]string, 0) - rule.Rule, err = regexp.Compile(fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!"))) + // Strip leading "!" for negative rules, then strip leading "/" since + // git returns relative paths (e.g. "docs/foo.md" not "/docs/foo.md") + // and the regex is already anchored with ^...$, so the "/" is redundant. + pattern := strings.TrimPrefix(tokens[0], "!") + pattern = strings.TrimPrefix(pattern, "/") + expr := fmt.Sprintf("^%s$", pattern) + rule.Rule, err = regexp2.Compile(expr, regexp2.None) if err != nil { warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err)) return nil, warnings diff --git a/package/gitea/source/models/issues/pull_list.go b/package/gitea/source/models/issues/pull_list.go index 84f9f616..6a6abca9 100644 --- a/package/gitea/source/models/issues/pull_list.go +++ b/package/gitea/source/models/issues/pull_list.go @@ -14,6 +14,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "xorm.io/builder" @@ -70,38 +71,69 @@ func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch } // CanMaintainerWriteToBranch check whether user is a maintainer and could write to the branch -func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, branch string, user *user_model.User) bool { - if p.CanWrite(unit.TypeCode) { - return true +func CanMaintainerWriteToBranch(ctx context.Context, headPerm access_model.Permission, headBranch string, doer *user_model.User) bool { + can, err := canMaintainerWriteToBranch(ctx, headPerm, headBranch, doer) + if err != nil { + log.Error("CanMaintainerWriteToBranch: %v", err) + return false + } + return can +} + +func canMaintainerWriteToBranch(ctx context.Context, headPerm access_model.Permission, headBranch string, doer *user_model.User) (bool, error) { + if headPerm.CanWrite(unit.TypeCode) { + return true, nil } // the code below depends on units to get the repository ID, not ideal but just keep it for now - firstUnitRepoID := p.GetFirstUnitRepoID() + firstUnitRepoID := headPerm.GetFirstUnitRepoID() if firstUnitRepoID == 0 { - return false + return false, nil } - prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch) + prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, headBranch) if err != nil { - return false + return false, err + } + if _, err := prs.LoadIssues(ctx); err != nil { + return false, err } - for _, pr := range prs { - if pr.AllowMaintainerEdit { - err = pr.LoadBaseRepo(ctx) - if err != nil { - continue - } - prPerm, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, user) - if err != nil { - continue - } - if prPerm.CanWrite(unit.TypeCode) { - return true - } + if !pr.AllowMaintainerEdit { + continue + } + + // check the PR's poster's permissions + // If a "reader" poster created the PR in base repo from head repo, even if it is allowed to be edited by maintainers, + // the maintainers should not be allowed to write, because they don't really have "write" permission in the head repo + if err := pr.Issue.LoadPoster(ctx); err != nil { + return false, err + } + if err := pr.LoadHeadRepo(ctx); err != nil { + return false, err + } + posterHeadPerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.HeadRepo, pr.Issue.Poster) + if err != nil { + return false, err + } + if !posterHeadPerm.CanWrite(unit.TypeCode) { + continue + } + + // check the doer's permission + // Only allow the doer to edit the PR if they have write access to the base repository + if err := pr.LoadBaseRepo(ctx); err != nil { + return false, err + } + doerBasePerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.BaseRepo, doer) + if err != nil { + return false, err + } + if doerBasePerm.CanWrite(unit.TypeCode) { + return true, nil } } - return false + return false, nil } // HasUnmergedPullRequestsByHeadInfo checks if there are open and not merged pull request @@ -324,12 +356,26 @@ func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error) // HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) { - return db.GetEngine(ctx). + return HasMergedPullRequestInRepoBefore(ctx, repoID, posterID, 0, 0) +} + +// HasMergedPullRequestInRepoBefore returns whether the user has a merged PR before a timestamp (0 = no limit) +func HasMergedPullRequestInRepoBefore(ctx context.Context, repoID, posterID int64, beforeUnix timeutil.TimeStamp, excludePullID int64) (bool, error) { + sess := db.GetEngine(ctx). Join("INNER", "pull_request", "pull_request.issue_id = issue.id"). Where("repo_id=?", repoID). And("poster_id=?", posterID). And("is_pull=?", true). - And("pull_request.has_merged=?", true). + And("pull_request.has_merged=?", true) + + if beforeUnix > 0 { + sess.And("pull_request.merged_unix < ?", beforeUnix) + } + if excludePullID > 0 { + sess.And("pull_request.id != ?", excludePullID) + } + + return sess. Select("issue.id"). Limit(1). Get(new(Issue)) diff --git a/package/gitea/source/models/issues/pull_list_test.go b/package/gitea/source/models/issues/pull_list_test.go index 43783070..302b2ca0 100644 --- a/package/gitea/source/models/issues/pull_list_test.go +++ b/package/gitea/source/models/issues/pull_list_test.go @@ -6,15 +6,28 @@ package issues_test import ( "testing" + "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "xorm.io/builder" ) -func TestPullRequestList_LoadAttributes(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestPullRequestList(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + t.Run("LoadAttributes", testPullRequestListLoadAttributes) + t.Run("LoadReviewCommentsCounts", testPullRequestListLoadReviewCommentsCounts) + t.Run("LoadReviews", testPullRequestListLoadReviews) + t.Run("CanMaintainerWriteToBranch", testCanMaintainerWriteToBranch) +} +func testPullRequestListLoadAttributes(t *testing.T) { prs := issues_model.PullRequestList{ unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), @@ -28,9 +41,7 @@ func TestPullRequestList_LoadAttributes(t *testing.T) { assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(t.Context())) } -func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testPullRequestListLoadReviewCommentsCounts(t *testing.T) { prs := issues_model.PullRequestList{ unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), @@ -43,9 +54,7 @@ func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) { } } -func TestPullRequestList_LoadReviews(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testPullRequestListLoadReviews(t *testing.T) { prs := issues_model.PullRequestList{ unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}), unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}), @@ -61,3 +70,73 @@ func TestPullRequestList_LoadReviews(t *testing.T) { assert.EqualValues(t, 10, reviewList[4].ID) assert.EqualValues(t, 22, reviewList[5].ID) } + +func testCanMaintainerWriteToBranch(t *testing.T) { + ctx := t.Context() + baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) + headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11}) + + _ = baseRepo.LoadOwner(ctx) + _ = headRepo.LoadOwner(ctx) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // a PR from header's owner + headOwnerPR := &issues_model.PullRequest{ + Issue: &issues_model.Issue{ + RepoID: baseRepo.ID, + PosterID: headRepo.OwnerID, + }, + HeadRepoID: headRepo.ID, + BaseRepoID: baseRepo.ID, + HeadBranch: "pr-from-head-owner", + BaseBranch: "master", + } + require.NoError(t, issues_model.NewPullRequest(ctx, baseRepo, headOwnerPR.Issue, nil, nil, headOwnerPR)) + + // a PR from a user, they might have or not have "write" permission in the target repo + anyUserPR := &issues_model.PullRequest{ + Issue: &issues_model.Issue{ + RepoID: baseRepo.ID, + PosterID: user.ID, + }, + HeadRepoID: headRepo.ID, + BaseRepoID: baseRepo.ID, + HeadBranch: "pr-from-head-user", + BaseBranch: "master", + } + require.NoError(t, issues_model.NewPullRequest(ctx, baseRepo, anyUserPR.Issue, nil, nil, anyUserPR)) + + doerCanWrite := func(doer *user_model.User, pr *issues_model.PullRequest) bool { + headPerm, _ := access.GetIndividualUserRepoPermission(ctx, headRepo, doer) + return issues_model.CanMaintainerWriteToBranch(ctx, headPerm, pr.HeadBranch, doer) + } + + t.Run("NoAllowMaintainerEdit", func(t *testing.T) { + assert.True(t, doerCanWrite(headRepo.Owner, headOwnerPR)) + assert.False(t, doerCanWrite(baseRepo.Owner, headOwnerPR)) + assert.False(t, doerCanWrite(baseRepo.Owner, anyUserPR)) + assert.False(t, doerCanWrite(user, anyUserPR)) + }) + + t.Run("WithAllowMaintainerEdit-HeadPosterReader", func(t *testing.T) { + _, err := db.GetEngine(ctx).Where(builder.In("id", []int64{headOwnerPR.ID, anyUserPR.ID})). + Cols("allow_maintainer_edit"). + Update(&issues_model.PullRequest{AllowMaintainerEdit: true}) + require.NoError(t, err) + assert.True(t, doerCanWrite(baseRepo.Owner, headOwnerPR)) + assert.False(t, doerCanWrite(baseRepo.Owner, anyUserPR)) // poster doesn't have write permission, so maintainer can't write either + }) + + t.Run("WithAllowMaintainerEdit-HeadPosterWriter", func(t *testing.T) { + _, err := db.GetEngine(ctx).Where(builder.In("id", []int64{headOwnerPR.ID, anyUserPR.ID})). + Cols("allow_maintainer_edit"). + Update(&issues_model.PullRequest{AllowMaintainerEdit: true}) + require.NoError(t, err) + err = db.Insert(ctx, &repo_model.Collaboration{RepoID: headRepo.ID, UserID: user.ID, Mode: perm.AccessModeWrite}) + require.NoError(t, err) + err = db.Insert(ctx, &access.Access{RepoID: headRepo.ID, UserID: user.ID, Mode: perm.AccessModeWrite}) + require.NoError(t, err) + assert.True(t, doerCanWrite(baseRepo.Owner, headOwnerPR)) + assert.True(t, doerCanWrite(baseRepo.Owner, anyUserPR)) // now the poster has the write permission + }) +} diff --git a/package/gitea/source/models/issues/pull_test.go b/package/gitea/source/models/issues/pull_test.go index 7089af25..79d1f8aa 100644 --- a/package/gitea/source/models/issues/pull_test.go +++ b/package/gitea/source/models/issues/pull_test.go @@ -17,16 +17,43 @@ import ( "github.com/stretchr/testify/require" ) -func TestPullRequest_LoadAttributes(t *testing.T) { +func TestPullRequest(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("LoadAttributes", testPullRequestLoadAttributes) + t.Run("LoadIssue", testPullRequestLoadIssue) + t.Run("LoadBaseRepo", testPullRequestLoadBaseRepo) + t.Run("LoadHeadRepo", testPullRequestLoadHeadRepo) + t.Run("PullRequestsNewest", testPullRequestsNewest) + t.Run("PullRequestsOldest", testPullRequestsOldest) + t.Run("GetUnmergedPullRequest", testGetUnmergedPullRequest) + t.Run("HasUnmergedPullRequestsByHeadInfo", testHasUnmergedPullRequestsByHeadInfo) + t.Run("GetUnmergedPullRequestsByHeadInfo", testGetUnmergedPullRequestsByHeadInfo) + t.Run("GetUnmergedPullRequestsByBaseInfo", testGetUnmergedPullRequestsByBaseInfo) + t.Run("GetPullRequestByIndex", testGetPullRequestByIndex) + t.Run("GetPullRequestByID", testGetPullRequestByID) + t.Run("GetPullRequestByIssueID", testGetPullRequestByIssueID) + t.Run("PullRequest_UpdateCols", testPullRequestUpdateCols) + t.Run("PullRequest_IsWorkInProgress", testPullRequestIsWorkInProgress) + t.Run("PullRequest_GetWorkInProgressPrefixWorkInProgress", testPullRequestGetWorkInProgressPrefixWorkInProgress) + t.Run("DeleteOrphanedObjects", testDeleteOrphanedObjects) + t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine) + t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns) + t.Run("GetApprovers", testGetApprovers) + t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit) + t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests) + t.Run("PullRequestsClosedRecentSortType", testPullRequestsClosedRecentSortType) + t.Run("LoadRequestedReviewers", testLoadRequestedReviewers) +} + +func testPullRequestLoadAttributes(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) assert.NoError(t, pr.LoadAttributes(t.Context())) assert.NotNil(t, pr.Merger) assert.Equal(t, pr.MergerID, pr.Merger.ID) } -func TestPullRequest_LoadIssue(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestLoadIssue(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) assert.NoError(t, pr.LoadIssue(t.Context())) assert.NotNil(t, pr.Issue) @@ -36,8 +63,7 @@ func TestPullRequest_LoadIssue(t *testing.T) { assert.Equal(t, int64(2), pr.Issue.ID) } -func TestPullRequest_LoadBaseRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestLoadBaseRepo(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) assert.NoError(t, pr.LoadBaseRepo(t.Context())) assert.NotNil(t, pr.BaseRepo) @@ -47,8 +73,7 @@ func TestPullRequest_LoadBaseRepo(t *testing.T) { assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID) } -func TestPullRequest_LoadHeadRepo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestLoadHeadRepo(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) assert.NoError(t, pr.LoadHeadRepo(t.Context())) assert.NotNil(t, pr.HeadRepo) @@ -59,8 +84,7 @@ func TestPullRequest_LoadHeadRepo(t *testing.T) { // TODO TestNewPullRequest -func TestPullRequestsNewest(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestsNewest(t *testing.T) { prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{ ListOptions: db.ListOptions{ Page: 1, @@ -77,7 +101,7 @@ func TestPullRequestsNewest(t *testing.T) { } } -func TestPullRequests_Closed_RecentSortType(t *testing.T) { +func testPullRequestsClosedRecentSortType(t *testing.T) { // Issue ID | Closed At. | Updated At // 2 | 1707270001 | 1707270001 // 3 | 1707271000 | 1707279999 @@ -90,7 +114,6 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) { {"recentclose", []int64{11, 3, 2}}, } - assert.NoError(t, unittest.PrepareTestDatabase()) _, err := db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2") require.NoError(t, err) _, err = db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3") @@ -118,9 +141,7 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) { } } -func TestLoadRequestedReviewers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testLoadRequestedReviewers(t *testing.T) { pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) assert.NoError(t, pull.LoadIssue(t.Context())) issue := pull.Issue @@ -130,7 +151,7 @@ func TestLoadRequestedReviewers(t *testing.T) { user1, err := user_model.GetUserByID(t.Context(), 1) assert.NoError(t, err) - comment, err := issues_model.AddReviewRequest(t.Context(), issue, user1, &user_model.User{}) + comment, err := issues_model.AddReviewRequest(t.Context(), issue, user1, &user_model.User{}, false) assert.NoError(t, err) assert.NotNil(t, comment) @@ -146,8 +167,7 @@ func TestLoadRequestedReviewers(t *testing.T) { assert.Empty(t, pull.RequestedReviewers) } -func TestPullRequestsOldest(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestsOldest(t *testing.T) { prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{ ListOptions: db.ListOptions{ Page: 1, @@ -164,8 +184,7 @@ func TestPullRequestsOldest(t *testing.T) { } } -func TestGetUnmergedPullRequest(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetUnmergedPullRequest(t *testing.T) { pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub) assert.NoError(t, err) assert.Equal(t, int64(2), pr.ID) @@ -175,9 +194,7 @@ func TestGetUnmergedPullRequest(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testHasUnmergedPullRequestsByHeadInfo(t *testing.T) { exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2") assert.NoError(t, err) assert.True(t, exist) @@ -187,8 +204,7 @@ func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) { assert.False(t, exist) } -func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetUnmergedPullRequestsByHeadInfo(t *testing.T) { prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2") assert.NoError(t, err) assert.Len(t, prs, 1) @@ -198,8 +214,7 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) { } } -func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetUnmergedPullRequestsByBaseInfo(t *testing.T) { prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(t.Context(), 1, "master") assert.NoError(t, err) assert.Len(t, prs, 1) @@ -209,8 +224,7 @@ func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { assert.Equal(t, "master", pr.BaseBranch) } -func TestGetPullRequestByIndex(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetPullRequestByIndex(t *testing.T) { pr, err := issues_model.GetPullRequestByIndex(t.Context(), 1, 2) assert.NoError(t, err) assert.Equal(t, int64(1), pr.BaseRepoID) @@ -225,8 +239,7 @@ func TestGetPullRequestByIndex(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestGetPullRequestByID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetPullRequestByID(t *testing.T) { pr, err := issues_model.GetPullRequestByID(t.Context(), 1) assert.NoError(t, err) assert.Equal(t, int64(1), pr.ID) @@ -237,8 +250,7 @@ func TestGetPullRequestByID(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestGetPullRequestByIssueID(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetPullRequestByIssueID(t *testing.T) { pr, err := issues_model.GetPullRequestByIssueID(t.Context(), 2) assert.NoError(t, err) assert.Equal(t, int64(2), pr.IssueID) @@ -248,8 +260,7 @@ func TestGetPullRequestByIssueID(t *testing.T) { assert.True(t, issues_model.IsErrPullRequestNotExist(err)) } -func TestPullRequest_UpdateCols(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testPullRequestUpdateCols(t *testing.T) { pr := &issues_model.PullRequest{ ID: 1, BaseBranch: "baseBranch", @@ -265,9 +276,7 @@ func TestPullRequest_UpdateCols(t *testing.T) { // TODO TestAddTestPullRequestTask -func TestPullRequest_IsWorkInProgress(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testPullRequestIsWorkInProgress(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) pr.LoadIssue(t.Context()) @@ -280,9 +289,7 @@ func TestPullRequest_IsWorkInProgress(t *testing.T) { assert.True(t, pr.IsWorkInProgress(t.Context())) } -func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testPullRequestGetWorkInProgressPrefixWorkInProgress(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) pr.LoadIssue(t.Context()) @@ -296,9 +303,7 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) { assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix(t.Context())) } -func TestDeleteOrphanedObjects(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testDeleteOrphanedObjects(t *testing.T) { countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{}) assert.NoError(t, err) @@ -317,7 +322,7 @@ func TestDeleteOrphanedObjects(t *testing.T) { assert.Equal(t, countBefore, countAfter) } -func TestParseCodeOwnersLine(t *testing.T) { +func testParseCodeOwnersLine(t *testing.T) { type CodeOwnerTest struct { Line string Tokens []string @@ -331,6 +336,8 @@ func TestParseCodeOwnersLine(t *testing.T) { {Line: `docs/(aws|google|azure)/[^/]*\\.(md|txt) @org3 @org2/team2`, Tokens: []string{`docs/(aws|google|azure)/[^/]*\.(md|txt)`, "@org3", "@org2/team2"}}, {Line: `\#path @org3`, Tokens: []string{`#path`, "@org3"}}, {Line: `path\ with\ spaces/ @org3`, Tokens: []string{`path with spaces/`, "@org3"}}, + {Line: `/docs/.*\\.md @user1`, Tokens: []string{`/docs/.*\.md`, "@user1"}}, + {Line: `!/assets/.*\\.(bin|exe|msi) @user1`, Tokens: []string{`!/assets/.*\.(bin|exe|msi)`, "@user1"}}, } for _, g := range given { @@ -339,8 +346,37 @@ func TestParseCodeOwnersLine(t *testing.T) { } } -func TestGetApprovers(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testCodeOwnerAbsolutePathPatterns(t *testing.T) { + type testCase struct { + content string + file string + expected bool + } + + cases := []testCase{ + // Absolute path pattern should match (leading "/" stripped) + {content: "/README.md @user5\n", file: "README.md", expected: true}, + // Absolute path pattern in subdirectory + {content: "/docs/.* @user5\n", file: "docs/foo.md", expected: true}, + // Absolute path should not match nested paths it shouldn't + {content: "/docs/.* @user5\n", file: "other/docs/foo.md", expected: false}, + // Relative path still works + {content: "README.md @user5\n", file: "README.md", expected: true}, + // Negated absolute path pattern + {content: "!/.* @user5\n", file: "README.md", expected: false}, + } + + for _, c := range cases { + rules, _ := issues_model.GetCodeOwnersFromContent(t.Context(), c.content) + require.NotEmpty(t, rules) + rule := rules[0] + regexpMatched, _ := rule.Rule.MatchString(c.file) + ruleMatched := regexpMatched == !rule.Negative + assert.Equal(t, c.expected, ruleMatched, "pattern %q against file %q", c.content, c.file) + } +} + +func testGetApprovers(t *testing.T) { pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5}) // Official reviews are already deduplicated. Allow unofficial reviews // to assert that there are no duplicated approvers. @@ -350,8 +386,7 @@ func TestGetApprovers(t *testing.T) { assert.Equal(t, expected, approvers) } -func TestGetPullRequestByMergedCommit(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testGetPullRequestByMergedCommit(t *testing.T) { pr, err := issues_model.GetPullRequestByMergedCommit(t.Context(), 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3") assert.NoError(t, err) assert.EqualValues(t, 1, pr.ID) @@ -362,8 +397,7 @@ func TestGetPullRequestByMergedCommit(t *testing.T) { assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{}) } -func TestMigrate_InsertPullRequests(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testMigrateInsertPullRequests(t *testing.T) { reponame := "repo1" repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) diff --git a/package/gitea/source/models/issues/review.go b/package/gitea/source/models/issues/review.go index b758fa5f..ed727f17 100644 --- a/package/gitea/source/models/issues/review.go +++ b/package/gitea/source/models/issues/review.go @@ -176,15 +176,7 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) { if r.ReviewerID == 0 || r.Reviewer != nil { return err } - r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID) - if err != nil { - if !user_model.IsErrUserNotExist(err) { - return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err) - } - r.ReviewerID = user_model.GhostUserID - r.Reviewer = user_model.NewGhostUser() - return nil - } + r.ReviewerID, r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID) return err } @@ -215,25 +207,25 @@ func (r *Review) LoadAttributes(ctx context.Context) (err error) { return err } -// HTMLTypeColorName returns the color used in the ui indicating the review -func (r *Review) HTMLTypeColorName() string { +// HTMLTypeColorClass returns the CSS class used in the ui indicating the review +func (r *Review) HTMLTypeColorClass() string { switch r.Type { case ReviewTypeApprove: if !r.Official { - return "grey" + return "tw-text-text-light" } if r.Stale { - return "yellow" + return "tw-text-yellow" } - return "green" + return "tw-text-green" case ReviewTypeComment: - return "grey" + return "tw-text-text-light" case ReviewTypeReject: - return "red" + return "tw-text-red" case ReviewTypeRequest: - return "yellow" + return "tw-text-yellow" } - return "grey" + return "tw-text-text-light" } // TooltipContent returns the locale string describing the review type @@ -384,7 +376,7 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error // GetCurrentReview returns the current pending review of reviewer for given issue func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) { if reviewer == nil { - return nil, nil + return nil, nil //nolint:nilnil // return nil when reviewer is nil } reviews, err := FindReviews(ctx, FindReviewOptions{ Types: []ReviewType{ReviewTypePending}, @@ -643,7 +635,7 @@ func InsertReviews(ctx context.Context, reviews []*Review) error { } // AddReviewRequest add a review request from one reviewer -func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User) (*Comment, error) { +func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User, isCodeOwners bool) (*Comment, error) { return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { sess := db.GetEngine(ctx) @@ -702,6 +694,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo RemovedAssignee: false, // Use RemovedAssignee as !isRequest AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID ReviewID: review.ID, + SpecialDoerName: util.Iif(isCodeOwners, SpecialDoerNameCodeOwners, ""), }) if err != nil { return nil, err @@ -767,7 +760,7 @@ func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64) } // AddTeamReviewRequest add a review request from one team -func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) { +func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User, isCodeOwners bool) (*Comment, error) { return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) { review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID) if err != nil && !IsErrReviewNotExist(err) { @@ -812,6 +805,7 @@ func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organizat RemovedAssignee: false, // Use RemovedAssignee as !isRequest AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID ReviewID: review.ID, + SpecialDoerName: util.Iif(isCodeOwners, SpecialDoerNameCodeOwners, ""), }) if err != nil { return nil, fmt.Errorf("CreateComment(): %w", err) @@ -917,7 +911,7 @@ func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.Use return false, nil } if doer.ID != issue.PosterID { - p, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + p, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer) if err != nil { return false, err } diff --git a/package/gitea/source/models/issues/review_list.go b/package/gitea/source/models/issues/review_list.go index 86b1a2e7..d0430cd0 100644 --- a/package/gitea/source/models/issues/review_list.go +++ b/package/gitea/source/models/issues/review_list.go @@ -173,9 +173,9 @@ func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, mig reviewersMap := make(map[int64][]*Review) // key is reviewer id originalReviewersMap := make(map[int64][]*Review) // key is original author id reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id - countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, ReviewTypeComment} + countedReviewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, ReviewTypeComment} for _, review := range reviews { - if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed { + if review.ReviewerTeamID == 0 && slices.Contains(countedReviewTypes, review.Type) && !review.Dismissed { if review.OriginalAuthorID != 0 { originalReviewersMap[review.OriginalAuthorID] = append(originalReviewersMap[review.OriginalAuthorID], review) } else { diff --git a/package/gitea/source/models/issues/review_test.go b/package/gitea/source/models/issues/review_test.go index 6795ea8e..092d88d1 100644 --- a/package/gitea/source/models/issues/review_test.go +++ b/package/gitea/source/models/issues/review_test.go @@ -321,14 +321,28 @@ func TestAddReviewRequest(t *testing.T) { pull.HasMerged = false assert.NoError(t, pull.UpdateCols(t.Context(), "has_merged")) issue.IsClosed = true - _, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}) + _, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}, false) assert.Error(t, err) assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) pull.HasMerged = true assert.NoError(t, pull.UpdateCols(t.Context(), "has_merged")) issue.IsClosed = false - _, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}) + _, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}, false) assert.Error(t, err) assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err)) + + // Test CODEOWNERS review request stores metadata correctly + pull2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}) + assert.NoError(t, pull2.LoadIssue(t.Context())) + issue2 := pull2.Issue + assert.NoError(t, issue2.LoadRepo(t.Context())) + reviewer2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 7}) + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + comment, err := issues_model.AddReviewRequest(t.Context(), issue2, reviewer2, doer, true) + assert.NoError(t, err) + assert.NotNil(t, comment) + assert.NotNil(t, comment.CommentMetaData) + assert.Equal(t, issues_model.SpecialDoerNameCodeOwners, comment.CommentMetaData.SpecialDoerName) } diff --git a/package/gitea/source/models/issues/tracked_time.go b/package/gitea/source/models/issues/tracked_time.go index 9c11881e..0b5c341f 100644 --- a/package/gitea/source/models/issues/tracked_time.go +++ b/package/gitea/source/models/issues/tracked_time.go @@ -311,13 +311,13 @@ func deleteTime(ctx context.Context, t *TrackedTime) error { } // GetTrackedTimeByID returns raw TrackedTime without loading attributes by id -func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) { +func GetTrackedTimeByID(ctx context.Context, issueID, trackedTimeID int64) (*TrackedTime, error) { time := new(TrackedTime) - has, err := db.GetEngine(ctx).ID(id).Get(time) + has, err := db.GetEngine(ctx).ID(trackedTimeID).Where("issue_id = ?", issueID).Get(time) if err != nil { return nil, err } else if !has { - return nil, db.ErrNotExist{Resource: "tracked_time", ID: id} + return nil, db.ErrNotExist{Resource: "tracked_time", ID: trackedTimeID} } return time, nil } diff --git a/package/gitea/source/models/migrations/base/db.go b/package/gitea/source/models/migrations/base/db.go index 479a4637..16048c20 100644 --- a/package/gitea/source/models/migrations/base/db.go +++ b/package/gitea/source/models/migrations/base/db.go @@ -5,21 +5,14 @@ package base import ( "context" - "database/sql" "errors" "fmt" - "os" - "path" "reflect" "regexp" "strings" - "time" - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "xorm.io/xorm" "xorm.io/xorm/schemas" @@ -409,7 +402,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin cols += "DROP COLUMN `" + col + "` CASCADE" } if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { - return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) + return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err) } case setting.Database.Type.IsMySQL(): // Drop indexes on columns first @@ -437,7 +430,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin cols += "DROP COLUMN `" + col + "`" } if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil { - return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) + return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err) } case setting.Database.Type.IsMSSQL(): cols := "" @@ -451,27 +444,27 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin tableName, strings.ReplaceAll(cols, "`", "'")) constraints := make([]string, 0) if err := sess.SQL(sql).Find(&constraints); err != nil { - return fmt.Errorf("Find constraints: %v", err) + return fmt.Errorf("find constraints: %w", err) } for _, constraint := range constraints { if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP CONSTRAINT `%s`", tableName, constraint)); err != nil { - return fmt.Errorf("Drop table `%s` default constraint `%s`: %v", tableName, constraint, err) + return fmt.Errorf("drop table `%s` default constraint `%s`: %w", tableName, constraint, err) } } sql = fmt.Sprintf("SELECT DISTINCT Name FROM sys.indexes INNER JOIN sys.index_columns ON indexes.index_id = index_columns.index_id AND indexes.object_id = index_columns.object_id WHERE indexes.object_id = OBJECT_ID('%[1]s') AND index_columns.column_id IN (SELECT column_id FROM sys.columns WHERE LOWER(name) IN (%[2]s) AND object_id = OBJECT_ID('%[1]s'))", tableName, strings.ReplaceAll(cols, "`", "'")) constraints = make([]string, 0) if err := sess.SQL(sql).Find(&constraints); err != nil { - return fmt.Errorf("Find constraints: %v", err) + return fmt.Errorf("find constraints: %w", err) } for _, constraint := range constraints { if _, err := sess.Exec(fmt.Sprintf("DROP INDEX `%[2]s` ON `%[1]s`", tableName, constraint)); err != nil { - return fmt.Errorf("Drop index `%[2]s` on `%[1]s`: %v", tableName, constraint, err) + return fmt.Errorf("drop index `%[2]s` on `%[1]s`: %[3]w", tableName, constraint, err) } } if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP COLUMN %s", tableName, cols)); err != nil { - return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err) + return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err) } default: log.Fatal("Unrecognized DB") @@ -515,114 +508,3 @@ func ModifyColumn(x *xorm.Engine, tableName string, col *schemas.Column) error { } return nil } - -func removeAllWithRetry(dir string) error { - var err error - for range 20 { - err = os.RemoveAll(dir) - if err == nil { - break - } - time.Sleep(100 * time.Millisecond) - } - return err -} - -func newXORMEngine() (*xorm.Engine, error) { - if err := db.InitEngine(context.Background()); err != nil { - return nil, err - } - x := unittest.GetXORMEngine() - return x, nil -} - -func deleteDB() error { - switch { - case setting.Database.Type.IsSQLite3(): - if err := util.Remove(setting.Database.Path); err != nil { - return err - } - return os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) - - case setting.Database.Type.IsMySQL(): - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", - setting.Database.User, setting.Database.Passwd, setting.Database.Host)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { - return err - } - return nil - case setting.Database.Type.IsPostgreSQL(): - db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { - return err - } - db.Close() - - // Check if we need to setup a specific schema - if len(setting.Database.Schema) != 0 { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - if err != nil { - return err - } - defer db.Close() - - schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) - if err != nil { - return err - } - defer schrows.Close() - - if !schrows.Next() { - // Create and setup a DB schema - _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) - if err != nil { - return err - } - } - - // Make the user's default search path the created schema; this will affect new connections - _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) - if err != nil { - return err - } - return nil - } - case setting.Database.Type.IsMSSQL(): - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS [%s]", setting.Database.Name)); err != nil { - return err - } - if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE [%s]", setting.Database.Name)); err != nil { - return err - } - } - - return nil -} diff --git a/package/gitea/source/models/migrations/base/db_test.go b/package/gitea/source/models/migrations/base/db_test.go index 80bf00b2..00635ca7 100644 --- a/package/gitea/source/models/migrations/base/db_test.go +++ b/package/gitea/source/models/migrations/base/db_test.go @@ -11,6 +11,10 @@ import ( "xorm.io/xorm/names" ) +func TestMain(m *testing.M) { + MainTest(m) +} + func Test_DropTableColumns(t *testing.T) { x, deferable := PrepareTestEnv(t, 0) if x == nil || t.Failed() { diff --git a/package/gitea/source/models/migrations/base/main_test.go b/package/gitea/source/models/migrations/base/main_test.go deleted file mode 100644 index c1c78915..00000000 --- a/package/gitea/source/models/migrations/base/main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package base - -import ( - "testing" -) - -func TestMain(m *testing.M) { - MainTest(m) -} diff --git a/package/gitea/source/models/migrations/base/tests.go b/package/gitea/source/models/migrations/base/tests.go index 3b52a5e7..17ea951b 100644 --- a/package/gitea/source/models/migrations/base/tests.go +++ b/package/gitea/source/models/migrations/base/tests.go @@ -4,25 +4,127 @@ package base import ( + "database/sql" "fmt" "os" + "path" "path/filepath" - "runtime" "testing" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/tempdir" - "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/testlogger" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/require" "xorm.io/xorm" + "xorm.io/xorm/schemas" ) // FIXME: this file shouldn't be in a normal package, it should only be compiled for tests +func newXORMEngine(t *testing.T) (*xorm.Engine, error) { + if err := db.InitEngine(t.Context()); err != nil { + return nil, err + } + x := unittest.GetXORMEngine() + return x, nil +} + +func deleteDB() error { + switch { + case setting.Database.Type.IsSQLite3(): + if err := util.Remove(setting.Database.Path); err != nil { + return err + } + return os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) + + case setting.Database.Type.IsMySQL(): + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", + setting.Database.User, setting.Database.Passwd, setting.Database.Host)) + if err != nil { + return err + } + defer db.Close() + + if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { + return err + } + + if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { + return err + } + return nil + case setting.Database.Type.IsPostgreSQL(): + db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", + setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) + if err != nil { + return err + } + defer db.Close() + + if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { + return err + } + + if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { + return err + } + db.Close() + + // Check if we need to setup a specific schema + if len(setting.Database.Schema) != 0 { + db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", + setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) + if err != nil { + return err + } + defer db.Close() + + schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) + if err != nil { + return err + } + defer schrows.Close() + + if !schrows.Next() { + // Create and setup a DB schema + _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) + if err != nil { + return err + } + } + + // Make the user's default search path the created schema; this will affect new connections + _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) + if err != nil { + return err + } + return nil + } + case setting.Database.Type.IsMSSQL(): + host, port := setting.ParseMSSQLHostPort(setting.Database.Host) + db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", + host, port, "master", setting.Database.User, setting.Database.Passwd)) + if err != nil { + return err + } + defer db.Close() + + if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS [%s]", setting.Database.Name)); err != nil { + return err + } + if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE [%s]", setting.Database.Name)); err != nil { + return err + } + } + + return nil +} + // PrepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0. // Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from. // @@ -39,7 +141,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu return nil, deferFn } - x, err := newXORMEngine() + x, err := newXORMEngine(t) require.NoError(t, err) if x != nil { oldDefer := deferFn @@ -88,51 +190,36 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu return x, deferFn } -func MainTest(m *testing.M) { +func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table { + tables, err := x.DBMetas() + require.NoError(t, err) + tableMap := make(map[string]*schemas.Table) + for _, table := range tables { + tableMap[table.Name] = table + } + return tableMap +} + +func mainTest(m *testing.M) int { testlogger.Init() - giteaRoot := test.SetupGiteaRoot() - giteaBinary := "gitea" - if runtime.GOOS == "windows" { - giteaBinary += ".exe" - } - setting.AppPath = filepath.Join(giteaRoot, giteaBinary) - if _, err := os.Stat(setting.AppPath); err != nil { - testlogger.Fatalf("Could not find gitea binary at %s\n", setting.AppPath) - } - - giteaConf := os.Getenv("GITEA_CONF") - if giteaConf == "" { - giteaConf = filepath.Join(filepath.Dir(setting.AppPath), "tests/sqlite.ini") - _, _ = fmt.Fprintf(os.Stderr, "Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) - } - - if !filepath.IsAbs(giteaConf) { - setting.CustomConf = filepath.Join(giteaRoot, giteaConf) - } else { - setting.CustomConf = giteaConf - } - tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data") if err != nil { - testlogger.Fatalf("Unable to create temporary data path %v\n", err) + testlogger.Panicf("Unable to create temporary data path %v\n", err) } defer cleanup() - setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") setting.AppDataPath = tmpDataPath unittest.InitSettingsForTesting() if err = git.InitFull(); err != nil { - testlogger.Fatalf("Unable to InitFull: %v\n", err) + testlogger.Panicf("Unable to InitFull: %v\n", err) } setting.LoadDBSetting() setting.InitLoggersForTest() - - exitStatus := m.Run() - - if err := removeAllWithRetry(setting.RepoRootPath); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) - } - os.Exit(exitStatus) + return m.Run() +} + +func MainTest(m *testing.M) { + os.Exit(mainTest(m)) } diff --git a/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run.yml b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run.yml new file mode 100644 index 00000000..49f71ce3 --- /dev/null +++ b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run.yml @@ -0,0 +1,29 @@ +# type ActionRun struct { +# ID int64 `xorm:"pk autoincr"` +# RepoID int64 `xorm:"index"` +# Index int64 +# CommitSHA string `xorm:"commit_sha"` +# Event string +# TriggerEvent string +# EventPayload string `xorm:"LONGTEXT"` +# } +- + id: 990 + repo_id: 100 + index: 7 + commit_sha: merge-sha + event: pull_request + event_payload: '{"pull_request":{"head":{"sha":"sha-shared"}}}' +- + id: 991 + repo_id: 100 + index: 8 + commit_sha: sha-shared + event: push + event_payload: '{"head_commit":{"id":"sha-shared"}}' +- + id: 1991 + repo_id: 100 + index: 9 + commit_sha: sha-other + event: release diff --git a/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run_job.yml b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run_job.yml new file mode 100644 index 00000000..addf5e06 --- /dev/null +++ b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/action_run_job.yml @@ -0,0 +1,16 @@ +# type ActionRunJob struct { +# ID int64 `xorm:"pk autoincr"` +# RunID int64 `xorm:"index"` +# } +- + id: 997 + run_id: 990 +- + id: 998 + run_id: 990 +- + id: 1997 + run_id: 991 +- + id: 1998 + run_id: 1991 diff --git a/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status.yml b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status.yml new file mode 100644 index 00000000..6be1c7ca --- /dev/null +++ b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status.yml @@ -0,0 +1,51 @@ +# type CommitStatus struct { +# ID int64 `xorm:"pk autoincr"` +# RepoID int64 `xorm:"index"` +# SHA string +# TargetURL string +# } +- + id: 10010 + repo_id: 100 + sha: sha-shared + target_url: /testuser/repo1/actions/runs/7/jobs/0 +- + id: 10011 + repo_id: 100 + sha: sha-shared + target_url: /testuser/repo1/actions/runs/7/jobs/1 +- + id: 10012 + repo_id: 100 + sha: sha-shared + target_url: /testuser/repo1/actions/runs/8/jobs/0 +- + id: 10013 + repo_id: 100 + sha: sha-other + target_url: /testuser/repo1/actions/runs/9/jobs/0 +- + id: 10014 + repo_id: 100 + sha: sha-shared + target_url: /otheruser/badrepo/actions/runs/7/jobs/0 +- + id: 10015 + repo_id: 100 + sha: sha-shared + target_url: /testuser/repo1/actions/runs/10/jobs/0 +- + id: 10016 + repo_id: 100 + sha: sha-shared + target_url: /testuser/repo1/actions/runs/7/jobs/3 +- + id: 10017 + repo_id: 100 + sha: sha-shared + target_url: https://ci.example.com/build/123 +- + id: 10018 + repo_id: 100 + sha: sha-shared + target_url: /testuser/repo1/actions/runs/990/jobs/997 diff --git a/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status_summary.yml b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status_summary.yml new file mode 100644 index 00000000..3dd846bb --- /dev/null +++ b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/commit_status_summary.yml @@ -0,0 +1,19 @@ +# type CommitStatusSummary struct { +# ID int64 `xorm:"pk autoincr"` +# RepoID int64 `xorm:"index"` +# SHA string `xorm:"VARCHAR(64) NOT NULL"` +# State string `xorm:"VARCHAR(7) NOT NULL"` +# TargetURL string +# } +- + id: 10020 + repo_id: 100 + sha: sha-shared + state: pending + target_url: /testuser/repo1/actions/runs/7/jobs/0 +- + id: 10021 + repo_id: 100 + sha: sha-other + state: pending + target_url: /testuser/repo1/actions/runs/9/jobs/0 diff --git a/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/repository.yml b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/repository.yml new file mode 100644 index 00000000..46162e78 --- /dev/null +++ b/package/gitea/source/models/migrations/fixtures/Test_FixCommitStatusTargetURLToUseRunAndJobID/repository.yml @@ -0,0 +1,9 @@ +# type Repository struct { +# ID int64 `xorm:"pk autoincr"` +# OwnerName string +# Name string +# } +- + id: 100 + owner_name: testuser + name: repo1 diff --git a/package/gitea/source/models/migrations/migrations.go b/package/gitea/source/models/migrations/migrations.go index 1b1558f3..db74ff78 100644 --- a/package/gitea/source/models/migrations/migrations.go +++ b/package/gitea/source/models/migrations/migrations.go @@ -25,6 +25,7 @@ import ( "code.gitea.io/gitea/models/migrations/v1_23" "code.gitea.io/gitea/models/migrations/v1_24" "code.gitea.io/gitea/models/migrations/v1_25" + "code.gitea.io/gitea/models/migrations/v1_26" "code.gitea.io/gitea/models/migrations/v1_6" "code.gitea.io/gitea/models/migrations/v1_7" "code.gitea.io/gitea/models/migrations/v1_8" @@ -379,8 +380,8 @@ func prepareMigrationTasks() []*migration { newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices), newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch), newMigration(311, "Add TimeEstimate to Issue table", v1_23.AddTimeEstimateColumnToIssueTable), - // Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312) + newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge), newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin), newMigration(314, "Update OwnerID as zero for repository level action tables", v1_24.UpdateOwnerIDOfRepoLevelActionsTables), @@ -390,10 +391,20 @@ func prepareMigrationTasks() []*migration { newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode), newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable), newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor), + // Gitea 1.24.0 ends at migration ID number 320 (database version 321) - // Gitea 1.24.0 ends at database version 321 newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs), newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength), + // Gitea 1.25.0 ends at migration ID number 322 (database version 323) + + newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency), + newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness), + newMigration(325, "Fix missed repo_id when migrate attachments", v1_26.FixMissedRepoIDWhenMigrateAttachments), + newMigration(326, "Partially migrate commit status target URL to use run ID and job ID", v1_26.FixCommitStatusTargetURLToUseRunAndJobID), + newMigration(327, "Add disabled state to action runners", v1_26.AddDisabledToActionRunner), + newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob), + newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge), + newMigration(330, "Add name column to webhook", v1_26.AddNameToWebhook), } return preparedMigrations } diff --git a/package/gitea/source/models/migrations/v1_12/v128.go b/package/gitea/source/models/migrations/v1_12/v128.go index 34746dcd..ff5b12af 100644 --- a/package/gitea/source/models/migrations/v1_12/v128.go +++ b/package/gitea/source/models/migrations/v1_12/v128.go @@ -84,17 +84,17 @@ func FixMergeBase(ctx context.Context, x *xorm.Engine) error { if !pr.HasMerged { var err error - pr.MergeBase, _, err = gitcmd.NewCommand("merge-base").AddDashesAndList(pr.BaseBranch, gitRefName).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = gitcmd.NewCommand("merge-base").AddDashesAndList(pr.BaseBranch, gitRefName).WithDir(repoPath).RunStdString(ctx) if err != nil { var err2 error - pr.MergeBase, _, err2 = gitcmd.NewCommand("rev-parse").AddDynamicArguments(git.BranchPrefix+pr.BaseBranch).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err2 = gitcmd.NewCommand("rev-parse").AddDynamicArguments(git.BranchPrefix + pr.BaseBranch).WithDir(repoPath).RunStdString(ctx) if err2 != nil { log.Error("Unable to get merge base for PR ID %d, Index %d in %s/%s. Error: %v & %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err, err2) continue } } } else { - parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).WithDir(repoPath).RunStdString(ctx) if err != nil { log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue @@ -108,7 +108,7 @@ func FixMergeBase(ctx context.Context, x *xorm.Engine) error { refs = append(refs, gitRefName) cmd := gitcmd.NewCommand("merge-base").AddDashesAndList(refs...) - pr.MergeBase, _, err = cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue diff --git a/package/gitea/source/models/migrations/v1_12/v134.go b/package/gitea/source/models/migrations/v1_12/v134.go index d31cc3ab..98bb8dbd 100644 --- a/package/gitea/source/models/migrations/v1_12/v134.go +++ b/package/gitea/source/models/migrations/v1_12/v134.go @@ -80,7 +80,7 @@ func RefixMergeBase(ctx context.Context, x *xorm.Engine) error { gitRefName := fmt.Sprintf("refs/pull/%d/head", pr.Index) - parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).WithDir(repoPath).RunStdString(ctx) if err != nil { log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue @@ -95,7 +95,7 @@ func RefixMergeBase(ctx context.Context, x *xorm.Engine) error { refs = append(refs, gitRefName) cmd := gitcmd.NewCommand("merge-base").AddDashesAndList(refs...) - pr.MergeBase, _, err = cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + pr.MergeBase, _, err = cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err) continue diff --git a/package/gitea/source/models/migrations/v1_12/v136.go b/package/gitea/source/models/migrations/v1_12/v136.go index 0f53278b..20b892b6 100644 --- a/package/gitea/source/models/migrations/v1_12/v136.go +++ b/package/gitea/source/models/migrations/v1_12/v136.go @@ -6,11 +6,10 @@ package v1_12 import ( "fmt" "math" - "path/filepath" - "strings" "time" - "code.gitea.io/gitea/modules/git" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" @@ -85,12 +84,9 @@ func AddCommitDivergenceToPulls(x *xorm.Engine) error { log.Error("Missing base repo with id %d for PR ID %d", pr.BaseRepoID, pr.ID) continue } - userPath := filepath.Join(setting.RepoRootPath, strings.ToLower(baseRepo.OwnerName)) - repoPath := filepath.Join(userPath, strings.ToLower(baseRepo.Name)+".git") - + repoStore := repo_model.StorageRepo(repo_model.RelativePath(baseRepo.OwnerName, baseRepo.Name)) gitRefName := fmt.Sprintf("refs/pull/%d/head", pr.Index) - - divergence, err := git.GetDivergingCommits(graceful.GetManager().HammerContext(), repoPath, pr.BaseBranch, gitRefName) + divergence, err := gitrepo.GetDivergingCommits(graceful.GetManager().HammerContext(), repoStore, pr.BaseBranch, gitRefName) if err != nil { log.Warn("Could not recalculate Divergence for pull: %d", pr.ID) pr.CommitsAhead = 0 diff --git a/package/gitea/source/models/migrations/v1_21/v276.go b/package/gitea/source/models/migrations/v1_21/v276.go index 3ab7e22c..be24b319 100644 --- a/package/gitea/source/models/migrations/v1_21/v276.go +++ b/package/gitea/source/models/migrations/v1_21/v276.go @@ -5,14 +5,10 @@ package v1_21 import ( "context" - "fmt" - "path/filepath" - "strings" - "code.gitea.io/gitea/modules/git" - giturl "code.gitea.io/gitea/modules/git/url" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "xorm.io/xorm" ) @@ -163,16 +159,13 @@ func migratePushMirrors(x *xorm.Engine) error { } func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) { - repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git") - if exist, _ := util.IsExist(repoPath); !exist { + ctx := context.Background() + relativePath := repo_model.RelativePath(ownerName, repoName) + if exist, _ := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(relativePath)); !exist { return "", nil } - remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName) - if err != nil { - return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err) - } - u, err := giturl.ParseGitURL(remoteURL) + u, err := gitrepo.GitRemoteGetURL(ctx, repo_model.StorageRepo(relativePath), remoteName) if err != nil { return "", err } diff --git a/package/gitea/source/models/migrations/v1_25/v321_test.go b/package/gitea/source/models/migrations/v1_25/v321_test.go index 4897783f..3ef2c68a 100644 --- a/package/gitea/source/models/migrations/v1_25/v321_test.go +++ b/package/gitea/source/models/migrations/v1_25/v321_test.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/timeutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) { @@ -38,33 +39,26 @@ func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) { type Notice struct { ID int64 `xorm:"pk autoincr"` Type int - Description string `xorm:"LONGTEXT"` + Description string `xorm:"TEXT"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` } // Prepare and load the testing database - x, deferable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice)) - defer deferable() + x, deferrable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice)) + defer deferrable() - assert.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x)) + require.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x)) - tables, err := x.DBMetas() - assert.NoError(t, err) + tables := base.LoadTableSchemasMap(t, x) + table := tables["review_state"] + column := table.GetColumn("updated_files") + assert.Equal(t, "LONGTEXT", column.SQLType.Name) - for _, table := range tables { - switch table.Name { - case "review_state": - column := table.GetColumn("updated_files") - assert.NotNil(t, column) - assert.Equal(t, "LONGTEXT", column.SQLType.Name) - case "package_property": - column := table.GetColumn("value") - assert.NotNil(t, column) - assert.Equal(t, "LONGTEXT", column.SQLType.Name) - case "notice": - column := table.GetColumn("description") - assert.NotNil(t, column) - assert.Equal(t, "LONGTEXT", column.SQLType.Name) - } - } + table = tables["package_property"] + column = table.GetColumn("value") + assert.Equal(t, "LONGTEXT", column.SQLType.Name) + + table = tables["notice"] + column = table.GetColumn("description") + assert.Equal(t, "LONGTEXT", column.SQLType.Name) } diff --git a/package/gitea/source/models/migrations/v1_25/v322_test.go b/package/gitea/source/models/migrations/v1_25/v322_test.go new file mode 100644 index 00000000..78d89070 --- /dev/null +++ b/package/gitea/source/models/migrations/v1_25/v322_test.go @@ -0,0 +1,34 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_ExtendCommentTreePathLength(t *testing.T) { + if setting.Database.Type.IsSQLite3() { + t.Skip("For SQLITE, varchar or char will always be represented as TEXT") + } + + type Comment struct { + ID int64 `xorm:"pk autoincr"` + TreePath string `xorm:"VARCHAR(255)"` + } + + x, deferrable := base.PrepareTestEnv(t, 0, new(Comment)) + defer deferrable() + + require.NoError(t, ExtendCommentTreePathLength(x)) + table := base.LoadTableSchemasMap(t, x)["comment"] + column := table.GetColumn("tree_path") + assert.Contains(t, []string{"NVARCHAR", "VARCHAR"}, column.SQLType.Name) + assert.EqualValues(t, 4000, column.Length) +} diff --git a/package/gitea/source/models/migrations/v1_26/main_test.go b/package/gitea/source/models/migrations/v1_26/main_test.go new file mode 100644 index 00000000..5aa12d55 --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" +) + +func TestMain(m *testing.M) { + base.MainTest(m) +} diff --git a/package/gitea/source/models/migrations/v1_26/v323.go b/package/gitea/source/models/migrations/v1_26/v323.go new file mode 100644 index 00000000..b116f73b --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v323.go @@ -0,0 +1,43 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "xorm.io/xorm" +) + +func AddActionsConcurrency(x *xorm.Engine) error { + type ActionRun struct { + RepoID int64 `xorm:"index(repo_concurrency)"` + RawConcurrency string + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` + } + + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionRun)); err != nil { + return err + } + + if err := x.Sync(new(ActionRun)); err != nil { + return err + } + + type ActionRunJob struct { + RepoID int64 `xorm:"index(repo_concurrency)"` + RawConcurrency string + IsConcurrencyEvaluated bool + ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"` + ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"` + } + + if _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionRunJob)); err != nil { + return err + } + + return nil +} diff --git a/package/gitea/source/models/migrations/v1_26/v324.go b/package/gitea/source/models/migrations/v1_26/v324.go new file mode 100644 index 00000000..5d96bfa3 --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v324.go @@ -0,0 +1,24 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "fmt" + + "xorm.io/xorm" +) + +func FixClosedMilestoneCompleteness(x *xorm.Engine) error { + // Update all milestones to recalculate completeness with the new logic: + // - Closed milestones with 0 issues should show 100% + // - All other milestones should calculate based on closed/total ratio + _, err := x.Exec("UPDATE `milestone` SET completeness=(CASE WHEN is_closed = ? AND num_issues = 0 THEN 100 ELSE 100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) END)", + true, + ) + if err != nil { + return fmt.Errorf("error updating milestone completeness: %w", err) + } + + return nil +} diff --git a/package/gitea/source/models/migrations/v1_26/v325.go b/package/gitea/source/models/migrations/v1_26/v325.go new file mode 100644 index 00000000..d81540f4 --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v325.go @@ -0,0 +1,18 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "xorm.io/xorm" +) + +func FixMissedRepoIDWhenMigrateAttachments(x *xorm.Engine) error { + _, err := x.Exec("UPDATE `attachment` SET `repo_id` = (SELECT `repo_id` FROM `issue` WHERE `issue`.`id` = `attachment`.`issue_id`) WHERE `issue_id` > 0 AND (`repo_id` IS NULL OR `repo_id` = 0);") + if err != nil { + return err + } + + _, err = x.Exec("UPDATE `attachment` SET `repo_id` = (SELECT `repo_id` FROM `release` WHERE `release`.`id` = `attachment`.`release_id`) WHERE `release_id` > 0 AND (`repo_id` IS NULL OR `repo_id` = 0);") + return err +} diff --git a/package/gitea/source/models/migrations/v1_26/v325_test.go b/package/gitea/source/models/migrations/v1_26/v325_test.go new file mode 100644 index 00000000..d4a66fee --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v325_test.go @@ -0,0 +1,45 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/timeutil" + + "github.com/stretchr/testify/require" +) + +func Test_FixMissedRepoIDWhenMigrateAttachments(t *testing.T) { + type Attachment struct { + ID int64 `xorm:"pk autoincr"` + UUID string `xorm:"uuid UNIQUE"` + RepoID int64 `xorm:"INDEX"` // this should not be zero + IssueID int64 `xorm:"INDEX"` // maybe zero when creating + ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating + UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added + CommentID int64 `xorm:"INDEX"` + Name string + DownloadCount int64 `xorm:"DEFAULT 0"` + Size int64 `xorm:"DEFAULT 0"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + } + + type Issue struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + } + + type Release struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"INDEX"` + } + + // Prepare and load the testing database + x, deferrable := base.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release)) + defer deferrable() + + require.NoError(t, FixMissedRepoIDWhenMigrateAttachments(x)) +} diff --git a/package/gitea/source/models/migrations/v1_26/v326.go b/package/gitea/source/models/migrations/v1_26/v326.go new file mode 100644 index 00000000..76532e2f --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v326.go @@ -0,0 +1,315 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + webhook_module "code.gitea.io/gitea/modules/webhook" + + "xorm.io/xorm" +) + +const ( + actionsRunPath = "/actions/runs/" + + // Only commit status target URLs whose resolved run ID is smaller than this threshold are rewritten by this partial migration. + // The fixed value 1000 is a conservative cutoff chosen to cover the smaller legacy run indexes that are most likely to be confused with ID-based URLs at runtime. + // Larger legacy {run} or {job} numbers are usually easier to disambiguate. For example: + // * /actions/runs/1200/jobs/1420 is most likely an ID-based URL, because a run should not contain more than 256 jobs. + // * /actions/runs/1500/jobs/3 is most likely an index-based URL, because a job ID cannot be smaller than its run ID. + // But URLs with small numbers, such as /actions/runs/5/jobs/6, are much harder to distinguish reliably. + // This migration therefore prioritizes rewriting target URLs for runs in that lower range. + legacyURLIDThreshold int64 = 1000 +) + +type migrationRepository struct { + ID int64 + OwnerName string + Name string +} + +type migrationActionRun struct { + ID int64 + RepoID int64 + Index int64 + CommitSHA string `xorm:"commit_sha"` + Event webhook_module.HookEventType + TriggerEvent string + EventPayload string +} + +type migrationActionRunJob struct { + ID int64 + RunID int64 +} + +type migrationCommitStatus struct { + ID int64 + RepoID int64 + TargetURL string +} + +type commitSHAAndRuns struct { + commitSHA string + runs map[int64]*migrationActionRun +} + +// FixCommitStatusTargetURLToUseRunAndJobID partially migrates legacy Actions +// commit status target URLs to the new run/job ID-based form. +// +// Only rows whose resolved run ID is below legacyURLIDThreshold are rewritten. +// This is because smaller legacy run indexes are more likely to collide with run ID URLs during runtime resolution, +// so this migration prioritizes that lower range and leaves the remaining legacy target URLs to the web compatibility logic. +func FixCommitStatusTargetURLToUseRunAndJobID(x *xorm.Engine) error { + jobsByRunIDCache := make(map[int64][]int64) + repoLinkCache := make(map[int64]string) + groups, err := loadLegacyMigrationRunGroups(x) + if err != nil { + return err + } + + for repoID, groupsBySHA := range groups { + for _, group := range groupsBySHA { + if err := migrateCommitStatusTargetURLForGroup(x, "commit_status", repoID, group.commitSHA, group.runs, jobsByRunIDCache, repoLinkCache); err != nil { + return err + } + if err := migrateCommitStatusTargetURLForGroup(x, "commit_status_summary", repoID, group.commitSHA, group.runs, jobsByRunIDCache, repoLinkCache); err != nil { + return err + } + } + } + return nil +} + +func loadLegacyMigrationRunGroups(x *xorm.Engine) (map[int64]map[string]*commitSHAAndRuns, error) { + var runs []migrationActionRun + if err := x.Table("action_run"). + Where("id < ?", legacyURLIDThreshold). + Cols("id", "repo_id", "`index`", "commit_sha", "event", "trigger_event", "event_payload"). + Find(&runs); err != nil { + return nil, fmt.Errorf("query action_run: %w", err) + } + + groups := make(map[int64]map[string]*commitSHAAndRuns) + for i := range runs { + run := runs[i] + commitID, err := getCommitStatusCommitID(&run) + if err != nil { + log.Warn("skip action_run id=%d when resolving commit status commit SHA: %v", run.ID, err) + continue + } + if commitID == "" { + // empty commitID means the run didn't create any commit status records, just skip + continue + } + if groups[run.RepoID] == nil { + groups[run.RepoID] = make(map[string]*commitSHAAndRuns) + } + if groups[run.RepoID][commitID] == nil { + groups[run.RepoID][commitID] = &commitSHAAndRuns{ + commitSHA: commitID, + runs: make(map[int64]*migrationActionRun), + } + } + groups[run.RepoID][commitID].runs[run.Index] = &run + } + return groups, nil +} + +func migrateCommitStatusTargetURLForGroup( + x *xorm.Engine, + table string, + repoID int64, + sha string, + runs map[int64]*migrationActionRun, + jobsByRunIDCache map[int64][]int64, + repoLinkCache map[int64]string, +) error { + var rows []migrationCommitStatus + if err := x.Table(table). + Where("repo_id = ?", repoID). + And("sha = ?", sha). + Cols("id", "repo_id", "target_url"). + Find(&rows); err != nil { + return fmt.Errorf("query %s for repo_id=%d sha=%s: %w", table, repoID, sha, err) + } + + for _, row := range rows { + repoLink, err := getRepoLinkCached(x, repoLinkCache, row.RepoID) + if err != nil || repoLink == "" { + if err != nil { + log.Warn("convert %s id=%d getRepoLinkCached: %v", table, row.ID, err) + } else { + log.Warn("convert %s id=%d: repo=%d not found", table, row.ID, row.RepoID) + } + continue + } + + runNum, jobNum, ok := parseTargetURL(row.TargetURL, repoLink) + if !ok { + continue + } + + run, ok := runs[runNum] + if !ok { + continue + } + + jobID, ok, err := getJobIDByIndexCached(x, jobsByRunIDCache, run.ID, jobNum) + if err != nil || !ok { + if err != nil { + log.Warn("convert %s id=%d getJobIDByIndexCached: %v", table, row.ID, err) + } else { + log.Warn("convert %s id=%d: job not found for run_id=%d job_index=%d", table, row.ID, run.ID, jobNum) + } + continue + } + + oldURL := row.TargetURL + newURL := fmt.Sprintf("%s%s%d/jobs/%d", repoLink, actionsRunPath, run.ID, jobID) + if oldURL == newURL { + continue + } + + if _, err := x.Table(table).ID(row.ID).Cols("target_url").Update(&migrationCommitStatus{TargetURL: newURL}); err != nil { + return fmt.Errorf("update %s id=%d target_url from %s to %s: %w", table, row.ID, oldURL, newURL, err) + } + } + return nil +} + +func getRepoLinkCached(x *xorm.Engine, cache map[int64]string, repoID int64) (string, error) { + if link, ok := cache[repoID]; ok { + return link, nil + } + repo := &migrationRepository{} + has, err := x.Table("repository").Where("id=?", repoID).Get(repo) + if err != nil { + return "", err + } + if !has { + cache[repoID] = "" + return "", nil + } + link := setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name) + cache[repoID] = link + return link, nil +} + +func getJobIDByIndexCached(x *xorm.Engine, cache map[int64][]int64, runID, jobIndex int64) (int64, bool, error) { + jobIDs, ok := cache[runID] + if !ok { + var jobs []migrationActionRunJob + if err := x.Table("action_run_job").Where("run_id=?", runID).Asc("id").Cols("id").Find(&jobs); err != nil { + return 0, false, err + } + jobIDs = make([]int64, 0, len(jobs)) + for _, job := range jobs { + jobIDs = append(jobIDs, job.ID) + } + cache[runID] = jobIDs + } + if jobIndex < 0 || jobIndex >= int64(len(jobIDs)) { + return 0, false, nil + } + return jobIDs[jobIndex], true, nil +} + +func parseTargetURL(targetURL, repoLink string) (runNum, jobNum int64, ok bool) { + prefix := repoLink + actionsRunPath + if !strings.HasPrefix(targetURL, prefix) { + return 0, 0, false + } + rest := targetURL[len(prefix):] + + parts := strings.Split(rest, "/") + if len(parts) == 3 && parts[1] == "jobs" { + runNum, err1 := strconv.ParseInt(parts[0], 10, 64) + jobNum, err2 := strconv.ParseInt(parts[2], 10, 64) + if err1 != nil || err2 != nil { + return 0, 0, false + } + return runNum, jobNum, true + } + + return 0, 0, false +} + +func getCommitStatusCommitID(run *migrationActionRun) (string, error) { + switch run.Event { + case webhook_module.HookEventPush: + payload, err := getPushEventPayload(run) + if err != nil { + return "", fmt.Errorf("getPushEventPayload: %w", err) + } + if payload.HeadCommit == nil { + return "", errors.New("head commit is missing in event payload") + } + return payload.HeadCommit.ID, nil + case webhook_module.HookEventPullRequest, + webhook_module.HookEventPullRequestSync, + webhook_module.HookEventPullRequestAssign, + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: + payload, err := getPullRequestEventPayload(run) + if err != nil { + return "", fmt.Errorf("getPullRequestEventPayload: %w", err) + } + if payload.PullRequest == nil { + return "", errors.New("pull request is missing in event payload") + } else if payload.PullRequest.Head == nil { + return "", errors.New("head of pull request is missing in event payload") + } + return payload.PullRequest.Head.Sha, nil + case webhook_module.HookEventPullRequestReviewApproved, + webhook_module.HookEventPullRequestReviewRejected, + webhook_module.HookEventPullRequestReviewComment: + payload, err := getPullRequestEventPayload(run) + if err != nil { + return "", fmt.Errorf("getPullRequestEventPayload: %w", err) + } + if payload.PullRequest == nil { + return "", errors.New("pull request is missing in event payload") + } else if payload.PullRequest.Head == nil { + return "", errors.New("head of pull request is missing in event payload") + } + return payload.PullRequest.Head.Sha, nil + case webhook_module.HookEventRelease: + return run.CommitSHA, nil + default: + return "", nil + } +} + +func getPushEventPayload(run *migrationActionRun) (*api.PushPayload, error) { + if run.Event != webhook_module.HookEventPush { + return nil, fmt.Errorf("event %s is not a push event", run.Event) + } + var payload api.PushPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return &payload, nil +} + +func getPullRequestEventPayload(run *migrationActionRun) (*api.PullRequestPayload, error) { + if !run.Event.IsPullRequest() && !run.Event.IsPullRequestReview() { + return nil, fmt.Errorf("event %s is not a pull request event", run.Event) + } + var payload api.PullRequestPayload + if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { + return nil, err + } + return &payload, nil +} diff --git a/package/gitea/source/models/migrations/v1_26/v326_test.go b/package/gitea/source/models/migrations/v1_26/v326_test.go new file mode 100644 index 00000000..b92eed35 --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v326_test.go @@ -0,0 +1,113 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + _ "code.gitea.io/gitea/models/actions" + _ "code.gitea.io/gitea/models/git" + _ "code.gitea.io/gitea/models/repo" + + "github.com/stretchr/testify/require" + "xorm.io/xorm" +) + +func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) { + defer test.MockVariableValue(&setting.AppSubURL, "")() + + type Repository struct { + ID int64 `xorm:"pk autoincr"` + OwnerName string + Name string + } + + type ActionRun struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index"` + Index int64 + CommitSHA string `xorm:"commit_sha"` + Event string + TriggerEvent string + EventPayload string `xorm:"LONGTEXT"` + } + + type ActionRunJob struct { + ID int64 `xorm:"pk autoincr"` + RunID int64 `xorm:"index"` + } + + type CommitStatus struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index"` + SHA string + TargetURL string + } + + type CommitStatusSummary struct { + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index"` + SHA string `xorm:"VARCHAR(64) NOT NULL"` + State string `xorm:"VARCHAR(7) NOT NULL"` + TargetURL string + } + + x, deferable := base.PrepareTestEnv(t, 0, + new(Repository), + new(ActionRun), + new(ActionRunJob), + new(CommitStatus), + new(CommitStatusSummary), + ) + defer deferable() + + require.NoError(t, FixCommitStatusTargetURLToUseRunAndJobID(x)) + + cases := []struct { + table string + id int64 + want string + }{ + // Legacy URLs for runs whose resolved run IDs are below the threshold should be rewritten. + {table: "commit_status", id: 10010, want: "/testuser/repo1/actions/runs/990/jobs/997"}, + {table: "commit_status", id: 10011, want: "/testuser/repo1/actions/runs/990/jobs/998"}, + {table: "commit_status", id: 10012, want: "/testuser/repo1/actions/runs/991/jobs/1997"}, + + // Runs whose resolved IDs are above the threshold are intentionally left unchanged. + {table: "commit_status", id: 10013, want: "/testuser/repo1/actions/runs/9/jobs/0"}, + + // URLs that do not resolve cleanly as legacy Actions URLs should remain untouched. + {table: "commit_status", id: 10014, want: "/otheruser/badrepo/actions/runs/7/jobs/0"}, + {table: "commit_status", id: 10015, want: "/testuser/repo1/actions/runs/10/jobs/0"}, + {table: "commit_status", id: 10016, want: "/testuser/repo1/actions/runs/7/jobs/3"}, + {table: "commit_status", id: 10017, want: "https://ci.example.com/build/123"}, + + // Already ID-based URLs are valid inputs and should not be rewritten again. + {table: "commit_status", id: 10018, want: "/testuser/repo1/actions/runs/990/jobs/997"}, + + // The same rewrite rules apply to commit_status_summary rows. + {table: "commit_status_summary", id: 10020, want: "/testuser/repo1/actions/runs/990/jobs/997"}, + {table: "commit_status_summary", id: 10021, want: "/testuser/repo1/actions/runs/9/jobs/0"}, + } + + for _, tc := range cases { + assertTargetURL(t, x, tc.table, tc.id, tc.want) + } +} + +func assertTargetURL(t *testing.T, x *xorm.Engine, table string, id int64, want string) { + t.Helper() + + var row struct { + TargetURL string + } + has, err := x.Table(table).Where("id=?", id).Cols("target_url").Get(&row) + require.NoError(t, err) + require.Truef(t, has, "row not found: table=%s id=%d", table, id) + require.Equal(t, want, row.TargetURL) +} diff --git a/package/gitea/source/models/migrations/v1_26/v327.go b/package/gitea/source/models/migrations/v1_26/v327.go new file mode 100644 index 00000000..51af5676 --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v327.go @@ -0,0 +1,17 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import "xorm.io/xorm" + +func AddDisabledToActionRunner(x *xorm.Engine) error { + type ActionRunner struct { + IsDisabled bool `xorm:"is_disabled NOT NULL DEFAULT false"` + } + + _, err := x.SyncWithOptions(xorm.SyncOptions{ + IgnoreDropIndices: true, + }, new(ActionRunner)) + return err +} diff --git a/package/gitea/source/models/migrations/v1_26/v327_test.go b/package/gitea/source/models/migrations/v1_26/v327_test.go new file mode 100644 index 00000000..971707be --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v327_test.go @@ -0,0 +1,33 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/require" +) + +func Test_AddDisabledToActionRunner(t *testing.T) { + type ActionRunner struct { + ID int64 `xorm:"pk autoincr"` + Name string + } + + x, deferable := base.PrepareTestEnv(t, 0, new(ActionRunner)) + defer deferable() + + _, err := x.Insert(&ActionRunner{Name: "runner"}) + require.NoError(t, err) + + require.NoError(t, AddDisabledToActionRunner(x)) + + var isDisabled bool + has, err := x.SQL("SELECT is_disabled FROM action_runner WHERE id = ?", 1).Get(&isDisabled) + require.NoError(t, err) + require.True(t, has) + require.False(t, isDisabled) +} diff --git a/package/gitea/source/models/migrations/v1_26/v328.go b/package/gitea/source/models/migrations/v1_26/v328.go new file mode 100644 index 00000000..81047305 --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v328.go @@ -0,0 +1,16 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "xorm.io/xorm" +) + +func AddTokenPermissionsToActionRunJob(x *xorm.Engine) error { + type ActionRunJob struct { + TokenPermissions string `xorm:"JSON TEXT"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRunJob)) + return err +} diff --git a/package/gitea/source/models/migrations/v1_26/v329.go b/package/gitea/source/models/migrations/v1_26/v329.go new file mode 100644 index 00000000..1afd155f --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v329.go @@ -0,0 +1,62 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "fmt" + + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +type UserBadge struct { //revive:disable-line:exported + ID int64 `xorm:"pk autoincr"` + BadgeID int64 + UserID int64 +} + +// TableIndices implements xorm's TableIndices interface +func (n *UserBadge) TableIndices() []*schemas.Index { + indices := make([]*schemas.Index, 0, 1) + ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType) + ubUnique.AddColumn("user_id", "badge_id") + indices = append(indices, ubUnique) + return indices +} + +// AddUniqueIndexForUserBadge adds a compound unique indexes for user badge table +// and it replaces an old index on user_id +func AddUniqueIndexForUserBadge(x *xorm.Engine) error { + // remove possible duplicated records in table user_badge + type result struct { + UserID int64 + BadgeID int64 + Cnt int + } + var results []result + if err := x.Select("user_id, badge_id, count(*) as cnt"). + Table("user_badge"). + GroupBy("user_id, badge_id"). + Having("count(*) > 1"). + Find(&results); err != nil { + return err + } + for _, r := range results { + if x.Dialect().URI().DBType == schemas.MSSQL { + if _, err := x.Exec(fmt.Sprintf("delete from user_badge where id in (SELECT top %d id FROM user_badge WHERE user_id = ? and badge_id = ?)", r.Cnt-1), r.UserID, r.BadgeID); err != nil { + return err + } + } else { + var ids []int64 + if err := x.SQL("SELECT id FROM user_badge WHERE user_id = ? and badge_id = ? limit ?", r.UserID, r.BadgeID, r.Cnt-1).Find(&ids); err != nil { + return err + } + if _, err := x.Table("user_badge").In("id", ids).Delete(); err != nil { + return err + } + } + } + + return x.Sync(new(UserBadge)) +} diff --git a/package/gitea/source/models/migrations/v1_26/v329_test.go b/package/gitea/source/models/migrations/v1_26/v329_test.go new file mode 100644 index 00000000..cab8e799 --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v329_test.go @@ -0,0 +1,85 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" +) + +type UserBadgeBefore struct { + ID int64 `xorm:"pk autoincr"` + BadgeID int64 + UserID int64 `xorm:"INDEX"` +} + +func (UserBadgeBefore) TableName() string { + return "user_badge" +} + +func Test_AddUniqueIndexForUserBadge(t *testing.T) { + x, deferable := base.PrepareTestEnv(t, 0, new(UserBadgeBefore)) + defer deferable() + if x == nil || t.Failed() { + return + } + + testData := []*UserBadgeBefore{ + {UserID: 1, BadgeID: 1}, + {UserID: 1, BadgeID: 1}, // duplicate + {UserID: 2, BadgeID: 1}, + {UserID: 1, BadgeID: 2}, + {UserID: 3, BadgeID: 3}, + {UserID: 3, BadgeID: 3}, // duplicate + } + + for _, data := range testData { + _, err := x.Insert(data) + assert.NoError(t, err) + } + + // check that we have duplicates + count, err := x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(2), count) + + count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(2), count) + + totalCount, err := x.Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(6), totalCount) + + // run the migration + if err := AddUniqueIndexForUserBadge(x); err != nil { + assert.NoError(t, err) + return + } + + // verify the duplicates were removed + count, err = x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(1), count) + + count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(1), count) + + // check total count + totalCount, err = x.Count(&UserBadgeBefore{}) + assert.NoError(t, err) + assert.Equal(t, int64(4), totalCount) + + // fail to insert a duplicate + _, err = x.Insert(&UserBadge{UserID: 1, BadgeID: 1}) + assert.Error(t, err) + + // succeed adding a non-duplicate + _, err = x.Insert(&UserBadge{UserID: 4, BadgeID: 1}) + assert.NoError(t, err) +} diff --git a/package/gitea/source/models/migrations/v1_26/v330.go b/package/gitea/source/models/migrations/v1_26/v330.go new file mode 100644 index 00000000..9f773310 --- /dev/null +++ b/package/gitea/source/models/migrations/v1_26/v330.go @@ -0,0 +1,16 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_26 + +import ( + "xorm.io/xorm" +) + +func AddNameToWebhook(x *xorm.Engine) error { + type Webhook struct { + Name string `xorm:"VARCHAR(255) NOT NULL DEFAULT ''"` + } + _, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(Webhook)) + return err +} diff --git a/package/gitea/source/models/migrations/v1_9/v82.go b/package/gitea/source/models/migrations/v1_9/v82.go index f0307bf0..8796b056 100644 --- a/package/gitea/source/models/migrations/v1_9/v82.go +++ b/package/gitea/source/models/migrations/v1_9/v82.go @@ -6,11 +6,10 @@ package v1_9 import ( "context" "fmt" - "path/filepath" - "strings" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/gitrepo" "xorm.io/xorm" ) @@ -34,16 +33,6 @@ func FixReleaseSha1OnReleaseTable(ctx context.Context, x *xorm.Engine) error { Name string } - // UserPath returns the path absolute path of user repositories. - UserPath := func(userName string) string { - return filepath.Join(setting.RepoRootPath, strings.ToLower(userName)) - } - - // RepoPath returns repository path by given user and repository name. - RepoPath := func(userName, repoName string) string { - return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git") - } - // Update release sha1 const batchSize = 100 sess := x.NewSession() @@ -99,7 +88,7 @@ func FixReleaseSha1OnReleaseTable(ctx context.Context, x *xorm.Engine) error { userCache[repo.OwnerID] = user } - gitRepo, err = git.OpenRepository(ctx, RepoPath(user.Name, repo.Name)) + gitRepo, err = gitrepo.OpenRepository(ctx, repo_model.StorageRepo(repo_model.RelativePath(user.Name, repo.Name))) if err != nil { return err } diff --git a/package/gitea/source/models/organization/org.go b/package/gitea/source/models/organization/org.go index b4d28f54..10a2c330 100644 --- a/package/gitea/source/models/organization/org.go +++ b/package/gitea/source/models/organization/org.go @@ -178,7 +178,7 @@ func (org *Organization) HomeLink() string { return org.AsUser().HomeLink() } -// FindOrgMembersOpts represensts find org members conditions +// FindOrgMembersOpts represents find org members conditions type FindOrgMembersOpts struct { db.ListOptions Doer *user_model.User diff --git a/package/gitea/source/models/organization/org_list.go b/package/gitea/source/models/organization/org_list.go index f37961b5..136417d9 100644 --- a/package/gitea/source/models/organization/org_list.go +++ b/package/gitea/source/models/organization/org_list.go @@ -54,6 +54,12 @@ type FindOrgOptions struct { IncludeVisibility structs.VisibleType } +func (opts *FindOrgOptions) ApplyPublicOnly(publicOnly bool) { + if publicOnly { + opts.IncludeVisibility = structs.VisibleTypePublic + } +} + func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder { cond := builder.Eq{"uid": userID} if !includePrivate { diff --git a/package/gitea/source/models/organization/org_user.go b/package/gitea/source/models/organization/org_user.go index 4d7527c1..627c1c2e 100644 --- a/package/gitea/source/models/organization/org_user.go +++ b/package/gitea/source/models/organization/org_user.go @@ -8,10 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/perm" - "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/log" "xorm.io/builder" @@ -129,58 +126,15 @@ func IsUserOrgOwner(ctx context.Context, users user_model.UserList, orgID int64) return results } -// GetOrgAssignees returns all users that have write access and can be assigned to issues -// of the any repository in the organization. -func GetOrgAssignees(ctx context.Context, orgID int64) (_ []*user_model.User, err error) { - e := db.GetEngine(ctx) - userIDs := make([]int64, 0, 10) - if err = e.Table("access"). - Join("INNER", "repository", "`repository`.id = `access`.repo_id"). - Where("`repository`.owner_id = ? AND `access`.mode >= ?", orgID, perm.AccessModeWrite). - Select("user_id"). - Find(&userIDs); err != nil { - return nil, err - } - - additionalUserIDs := make([]int64, 0, 10) - if err = e.Table("team_user"). - Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). - Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). - Join("INNER", "repository", "`repository`.id = `team_repo`.repo_id"). - Where("`repository`.owner_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", - orgID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). - Distinct("`team_user`.uid"). - Select("`team_user`.uid"). - Find(&additionalUserIDs); err != nil { - return nil, err - } - - uniqueUserIDs := make(container.Set[int64]) - uniqueUserIDs.AddMultiple(userIDs...) - uniqueUserIDs.AddMultiple(additionalUserIDs...) - - users := make([]*user_model.User, 0, len(uniqueUserIDs)) - if len(userIDs) > 0 { - if err = e.In("id", uniqueUserIDs.Values()). - Where(builder.Eq{"`user`.is_active": true}). - OrderBy(user_model.GetOrderByName()). - Find(&users); err != nil { - return nil, err - } - } - - return users, nil -} - func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) { if len(users) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil when there are no users } ownerTeam, err := GetOwnerTeam(ctx, orgID) if err != nil { if IsErrTeamNotExist(err) { log.Error("Organization does not have owner team: %d", orgID) - return nil, nil + return nil, nil //nolint:nilnil // return nil when owner team does not exist } return nil, err } diff --git a/package/gitea/source/models/packages/descriptor.go b/package/gitea/source/models/packages/descriptor.go index ea0e0d5e..2ef27051 100644 --- a/package/gitea/source/models/packages/descriptor.go +++ b/package/gitea/source/models/packages/descriptor.go @@ -9,6 +9,7 @@ import ( "fmt" "net/url" + "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/cache" @@ -53,8 +54,11 @@ func (l PackagePropertyList) GetByName(name string) string { // PackageDescriptor describes a package type PackageDescriptor struct { - Package *Package - Owner *user_model.User + // basic package info + Package *Package + Owner *user_model.User + + // package version info Repository *repo_model.Repository Version *PackageVersion SemVer *version.Version @@ -77,6 +81,11 @@ func (pd *PackageDescriptor) PackageWebLink() string { return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) } +// PackageSettingsLink returns the relative package settings link +func (pd *PackageDescriptor) PackageSettingsLink() string { + return fmt.Sprintf("%s/-/packages/settings/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName)) +} + // VersionWebLink returns the relative package version web link func (pd *PackageDescriptor) VersionWebLink() string { return fmt.Sprintf("%s/%s", pd.PackageWebLink(), url.PathEscape(pd.Version.LowerVersion)) @@ -203,6 +212,8 @@ func GetPackageDescriptorWithCache(ctx context.Context, pv *PackageVersion, c *c metadata = &rubygems.Metadata{} case TypeSwift: metadata = &swift.Metadata{} + case TypeTerraformState: + // terraform packages have no metadata case TypeVagrant: metadata = &vagrant.Metadata{} default: @@ -267,6 +278,15 @@ func GetPackageDescriptors(ctx context.Context, pvs []*PackageVersion) ([]*Packa return getPackageDescriptors(ctx, pvs, cache.NewEphemeralCache()) } +// GetAllPackageDescriptors gets all package descriptors for a package +func GetAllPackageDescriptors(ctx context.Context, p *Package) ([]*PackageDescriptor, error) { + pvs := make([]*PackageVersion, 0, 10) + if err := db.GetEngine(ctx).Where("package_id = ?", p.ID).Find(&pvs); err != nil { + return nil, err + } + return getPackageDescriptors(ctx, pvs, cache.NewEphemeralCache()) +} + func getPackageDescriptors(ctx context.Context, pvs []*PackageVersion, c *cache.EphemeralCache) ([]*PackageDescriptor, error) { pds := make([]*PackageDescriptor, 0, len(pvs)) for _, pv := range pvs { diff --git a/package/gitea/source/models/packages/package.go b/package/gitea/source/models/packages/package.go index 38d1cdcf..17e5d4ee 100644 --- a/package/gitea/source/models/packages/package.go +++ b/package/gitea/source/models/packages/package.go @@ -30,28 +30,29 @@ type Type string // List of supported packages const ( - TypeAlpine Type = "alpine" - TypeArch Type = "arch" - TypeCargo Type = "cargo" - TypeChef Type = "chef" - TypeComposer Type = "composer" - TypeConan Type = "conan" - TypeConda Type = "conda" - TypeContainer Type = "container" - TypeCran Type = "cran" - TypeDebian Type = "debian" - TypeGeneric Type = "generic" - TypeGo Type = "go" - TypeHelm Type = "helm" - TypeMaven Type = "maven" - TypeNpm Type = "npm" - TypeNuGet Type = "nuget" - TypePub Type = "pub" - TypePyPI Type = "pypi" - TypeRpm Type = "rpm" - TypeRubyGems Type = "rubygems" - TypeSwift Type = "swift" - TypeVagrant Type = "vagrant" + TypeAlpine Type = "alpine" + TypeArch Type = "arch" + TypeCargo Type = "cargo" + TypeChef Type = "chef" + TypeComposer Type = "composer" + TypeConan Type = "conan" + TypeConda Type = "conda" + TypeContainer Type = "container" + TypeCran Type = "cran" + TypeDebian Type = "debian" + TypeGeneric Type = "generic" + TypeGo Type = "go" + TypeHelm Type = "helm" + TypeMaven Type = "maven" + TypeNpm Type = "npm" + TypeNuGet Type = "nuget" + TypePub Type = "pub" + TypePyPI Type = "pypi" + TypeRpm Type = "rpm" + TypeRubyGems Type = "rubygems" + TypeSwift Type = "swift" + TypeTerraformState Type = "terraform" + TypeVagrant Type = "vagrant" ) var TypeList = []Type{ @@ -76,6 +77,7 @@ var TypeList = []Type{ TypeRpm, TypeRubyGems, TypeSwift, + TypeTerraformState, TypeVagrant, } @@ -124,6 +126,8 @@ func (pt Type) Name() string { return "RubyGems" case TypeSwift: return "Swift" + case TypeTerraformState: + return "Terraform State" case TypeVagrant: return "Vagrant" } @@ -175,6 +179,8 @@ func (pt Type) SVGName() string { return "gitea-rubygems" case TypeSwift: return "gitea-swift" + case TypeTerraformState: + return "gitea-terraform" case TypeVagrant: return "gitea-vagrant" } diff --git a/package/gitea/source/models/packages/package_blob.go b/package/gitea/source/models/packages/package_blob.go index d9c30b65..e765bbf0 100644 --- a/package/gitea/source/models/packages/package_blob.go +++ b/package/gitea/source/models/packages/package_blob.go @@ -43,13 +43,15 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool, existing := &PackageBlob{} - has, err := e.Where(builder.Eq{ + hashCond := builder.Eq{ "size": pb.Size, "hash_md5": pb.HashMD5, "hash_sha1": pb.HashSHA1, "hash_sha256": pb.HashSHA256, "hash_sha512": pb.HashSHA512, - }).Get(existing) + } + + has, err := e.Where(hashCond).Get(existing) if err != nil { return nil, false, err } @@ -57,6 +59,11 @@ func GetOrInsertBlob(ctx context.Context, pb *PackageBlob) (*PackageBlob, bool, return existing, true, nil } if _, err = e.Insert(pb); err != nil { + // Handle race condition: another request may have inserted the same blob + // between our SELECT and INSERT. Retry the SELECT to get the existing blob. + if has, _ = e.Where(hashCond).Get(existing); has { + return existing, true, nil + } return nil, false, err } return pb, false, nil diff --git a/package/gitea/source/models/packages/package_blob_test.go b/package/gitea/source/models/packages/package_blob_test.go new file mode 100644 index 00000000..8b636b4e --- /dev/null +++ b/package/gitea/source/models/packages/package_blob_test.go @@ -0,0 +1,51 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package packages + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" +) + +func TestGetOrInsertBlobConcurrent(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + testBlob := PackageBlob{ + Size: 123, + HashMD5: "md5", + HashSHA1: "sha1", + HashSHA256: "sha256", + HashSHA512: "sha512", + } + + const numGoroutines = 3 + var wg errgroup.Group + results := make([]*PackageBlob, numGoroutines) + existed := make([]bool, numGoroutines) + for idx := range numGoroutines { + wg.Go(func() error { + blob := testBlob // Create a copy of the test blob for each goroutine + var err error + results[idx], existed[idx], err = GetOrInsertBlob(t.Context(), &blob) + return err + }) + } + require.NoError(t, wg.Wait()) + + // then: all GetOrInsertBlob succeeds with the same blob ID, and only one indicates it did not exist before + existedCount := 0 + assert.NotNil(t, results[0]) + for i := range numGoroutines { + assert.Equal(t, results[0].ID, results[i].ID) + if existed[i] { + existedCount++ + } + } + assert.Equal(t, numGoroutines-1, existedCount) +} diff --git a/package/gitea/source/models/packages/package_file.go b/package/gitea/source/models/packages/package_file.go index bf877485..64bd08f0 100644 --- a/package/gitea/source/models/packages/package_file.go +++ b/package/gitea/source/models/packages/package_file.go @@ -68,7 +68,7 @@ func TryInsertFile(ctx context.Context, pf *PackageFile) (*PackageFile, error) { // GetFilesByVersionID gets all files of a version func GetFilesByVersionID(ctx context.Context, versionID int64) ([]*PackageFile, error) { pfs := make([]*PackageFile, 0, 10) - return pfs, db.GetEngine(ctx).Where("version_id = ?", versionID).Find(&pfs) + return pfs, db.GetEngine(ctx).Where("version_id = ?", versionID).OrderBy("lower_name, created_unix, id").Find(&pfs) } // GetFileForVersionByID gets a file of a version by id @@ -115,6 +115,20 @@ func DeleteFileByID(ctx context.Context, fileID int64) error { return err } +// DeleteFilesByPackageID deletes all files of a specific package +// Versions must not be deleted prior to this call +func DeleteFilesByPackageID(ctx context.Context, packageID int64) error { + deleteStmt := builder.Delete(builder.In("version_id", builder.Select("package_version.id").From("package_version").Where(builder.Eq{"package_id": packageID}))).From("package_file") + _, err := db.GetEngine(ctx).Exec(deleteStmt) + return err +} + +// DeleteFilesByVersionID deletes all files of a specific version +func DeleteFilesByVersionID(ctx context.Context, versionID int64) error { + _, err := db.GetEngine(ctx).Where("version_id = ?", versionID).Delete(&PackageFile{}) + return err +} + func UpdateFile(ctx context.Context, pf *PackageFile, cols []string) error { _, err := db.GetEngine(ctx).ID(pf.ID).Cols(cols...).Update(pf) return err diff --git a/package/gitea/source/models/packages/package_property.go b/package/gitea/source/models/packages/package_property.go index acc05d8d..30794ad7 100644 --- a/package/gitea/source/models/packages/package_property.go +++ b/package/gitea/source/models/packages/package_property.go @@ -5,6 +5,7 @@ package packages import ( "context" + "errors" "code.gitea.io/gitea/models/db" @@ -51,13 +52,13 @@ func InsertProperty(ctx context.Context, refType PropertyType, refID int64, name // GetProperties gets all properties func GetProperties(ctx context.Context, refType PropertyType, refID int64) ([]*PackageProperty, error) { pps := make([]*PackageProperty, 0, 10) - return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).Find(&pps) + return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ?", refType, refID).OrderBy("id").Find(&pps) } // GetPropertiesByName gets all properties with a specific name func GetPropertiesByName(ctx context.Context, refType PropertyType, refID int64, name string) ([]*PackageProperty, error) { pps := make([]*PackageProperty, 0, 10) - return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).Find(&pps) + return pps, db.GetEngine(ctx).Where("ref_type = ? AND ref_id = ? AND name = ?", refType, refID, name).OrderBy("id").Find(&pps) } // UpdateProperty updates a property @@ -86,6 +87,46 @@ func DeleteAllProperties(ctx context.Context, refType PropertyType, refID int64) return err } +// DeletePropertiesByPackageID deletes properties of a typed linked to the package +// Use to avoid for loops in mass deletion of properties +func DeletePropertiesByPackageID(ctx context.Context, refType PropertyType, packageID int64) error { + var deleteStmt *builder.Builder + + switch refType { + case PropertyTypeFile: + deleteStmt = builder.Delete( + // Delete all properties that are attached to a file and are in ids from a subquery + // which returns ids from the package_file table joined on package_version to link it with package id + builder.Eq{"ref_type": PropertyTypeFile}, builder.In("ref_id", + builder.Select("package_file.id").From("package_file"). + LeftJoin("package_version", "package_file.version_id = package_version.id"). + Where(builder.Eq{"package_version.package_id": packageID}))).From("package_property") + case PropertyTypeVersion: + // Delete all properties that are attached to a version and are in ids from subquery to the package_version filtered by package id + deleteStmt = builder.Delete( + builder.Eq{"ref_type": PropertyTypeVersion}, builder.In("ref_id", + builder.Select("package_version.id").From("package_version"). + Where(builder.Eq{"package_version.package_id": packageID}))).From("package_property") + case PropertyTypePackage: + // Delete all properties that are attached to a package and their reference links to the given package ID + deleteStmt = builder.Delete( + builder.Eq{"ref_type": PropertyTypePackage}, builder.Eq{"ref_id": packageID}). + From("package_property") + default: + return errors.New("invalid ref type") + } + + _, err := db.GetEngine(ctx).Exec(deleteStmt) + return err +} + +// DeleteFilePropertiesByVersionID deletes all file properties linked to specific version +func DeleteFilePropertiesByVersionID(ctx context.Context, versionID int64) error { + deleteStmt := builder.Delete(builder.Eq{"ref_type": PropertyTypeFile}, builder.In("ref_id", builder.Select("id").From("package_file").Where(builder.Eq{"version_id": versionID}))).From("package_property") + _, err := db.GetEngine(ctx).Exec(deleteStmt) + return err +} + // DeletePropertyByID deletes a property func DeletePropertyByID(ctx context.Context, propertyID int64) error { _, err := db.GetEngine(ctx).ID(propertyID).Delete(&PackageProperty{}) diff --git a/package/gitea/source/models/packages/package_version.go b/package/gitea/source/models/packages/package_version.go index 0a478c03..3e0e1899 100644 --- a/package/gitea/source/models/packages/package_version.go +++ b/package/gitea/source/models/packages/package_version.go @@ -157,6 +157,12 @@ func DeleteVersionByID(ctx context.Context, versionID int64) error { return err } +// DeleteVersionsByPackageID deletes all versions of a specific package +func DeleteVersionsByPackageID(ctx context.Context, packageID int64) error { + _, err := db.GetEngine(ctx).Where(builder.Eq{"package_id": packageID}).Delete(&PackageVersion{}) + return err +} + // HasVersionFileReferences checks if there are associated files func HasVersionFileReferences(ctx context.Context, versionID int64) (bool, error) { return db.GetEngine(ctx).Get(&PackageFile{ diff --git a/package/gitea/source/models/perm/access/access.go b/package/gitea/source/models/perm/access/access.go index 6433c467..acc34c43 100644 --- a/package/gitea/source/models/perm/access/access.go +++ b/package/gitea/source/models/perm/access/access.go @@ -91,43 +91,80 @@ func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mo } } -// FIXME: do cross-comparison so reduce deletions and additions to the minimum? +// refreshAccesses updates the repository's access records in the database by comparing the provided accessMap +// with existing records. It minimizes DB operations by performing selective inserts, updates, and deletes +// instead of removing all existing records and re-adding them. func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) { - minMode := perm.AccessModeRead + minModeToKeep := perm.AccessModeRead if err := repo.LoadOwner(ctx); err != nil { return fmt.Errorf("LoadOwner: %w", err) } - // If the repo isn't private and isn't owned by a organization, + // If the repo isn't private and isn't owned by an organization, // increase the minMode to Write. if !repo.IsPrivate && !repo.Owner.IsOrganization() { - minMode = perm.AccessModeWrite + minModeToKeep = perm.AccessModeWrite } - newAccesses := make([]Access, 0, len(accessMap)) + // Query existing accesses for cross-comparison + var existingAccesses []Access + if err := db.GetEngine(ctx).Where(builder.Eq{"repo_id": repo.ID}).Find(&existingAccesses); err != nil { + return fmt.Errorf("find existing accesses: %w", err) + } + existingMap := make(map[int64]perm.AccessMode, len(existingAccesses)) + for _, a := range existingAccesses { + existingMap[a.UserID] = a.Mode + } + + var toDelete []int64 + var toInsert, toUpdate []Access + + // Determine changes for userID, ua := range accessMap { - if ua.Mode < minMode && !ua.User.IsRestricted { - continue + if ua.Mode < minModeToKeep && !ua.User.IsRestricted { + // No explicit access record needed (handled by default permissions, e.g., public repo access) + if _, exists := existingMap[userID]; exists { + toDelete = append(toDelete, userID) + } + } else { + desiredMode := ua.Mode + if existingMode, exists := existingMap[userID]; exists { + if existingMode != desiredMode { + toUpdate = append(toUpdate, Access{UserID: userID, RepoID: repo.ID, Mode: desiredMode}) + } + } else { + toInsert = append(toInsert, Access{UserID: userID, RepoID: repo.ID, Mode: desiredMode}) + } } - - newAccesses = append(newAccesses, Access{ - UserID: userID, - RepoID: repo.ID, - Mode: ua.Mode, - }) + delete(existingMap, userID) } - // Delete old accesses and insert new ones for repository. - if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil { - return fmt.Errorf("delete old accesses: %w", err) - } - if len(newAccesses) == 0 { - return nil + // Remaining in existingMap should be deleted + for userID := range existingMap { + toDelete = append(toDelete, userID) } - if err = db.Insert(ctx, newAccesses); err != nil { - return fmt.Errorf("insert new accesses: %w", err) + // Execute deletions + if len(toDelete) > 0 { + if _, err = db.GetEngine(ctx).In("user_id", toDelete).And("repo_id = ?", repo.ID).Delete(&Access{}); err != nil { + return fmt.Errorf("delete accesses: %w", err) + } } + + // Execute updates + for _, u := range toUpdate { + if _, err = db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", u.UserID, repo.ID).Cols("mode").Update(&Access{Mode: u.Mode}); err != nil { + return fmt.Errorf("update access for user %d: %w", u.UserID, err) + } + } + + // Execute insertions + if len(toInsert) > 0 { + if err = db.Insert(ctx, toInsert); err != nil { + return fmt.Errorf("insert new accesses: %w", err) + } + } + return nil } diff --git a/package/gitea/source/models/perm/access/access_test.go b/package/gitea/source/models/perm/access/access_test.go index 15d18b36..148a02ef 100644 --- a/package/gitea/source/models/perm/access/access_test.go +++ b/package/gitea/source/models/perm/access/access_test.go @@ -15,11 +15,20 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestAccessLevel(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestAccess(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + t.Run("AccessLevel", testAccessLevel) + t.Run("HasAccess", testHasAccess) + t.Run("RecalculateAccesses", testRecalculateAccesses) + t.Run("RecalculateAccesses2", testRecalculateAccesses2) + t.Run("RecalculateAccessesUpdateMode", testRecalculateAccessesUpdateMode) + t.Run("RecalculateAccessesRemoveAccess", testRecalculateAccessesRemoveAccess) +} +func testAccessLevel(t *testing.T) { user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}) @@ -75,9 +84,7 @@ func TestAccessLevel(t *testing.T) { assert.Equal(t, perm_model.AccessModeRead, level) } -func TestHasAccess(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - +func testHasAccess(t *testing.T) { user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) // A public repository owned by User 2 @@ -101,9 +108,8 @@ func TestHasAccess(t *testing.T) { assert.NoError(t, err) } -func TestRepository_RecalculateAccesses(t *testing.T) { +func testRecalculateAccesses(t *testing.T) { // test with organization repo - assert.NoError(t, unittest.PrepareTestDatabase()) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) assert.NoError(t, repo1.LoadOwner(t.Context())) @@ -118,9 +124,8 @@ func TestRepository_RecalculateAccesses(t *testing.T) { assert.Equal(t, perm_model.AccessModeOwner, access.Mode) } -func TestRepository_RecalculateAccesses2(t *testing.T) { +func testRecalculateAccesses2(t *testing.T) { // test with non-organization repo - assert.NoError(t, unittest.PrepareTestDatabase()) repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) assert.NoError(t, repo1.LoadOwner(t.Context())) @@ -132,3 +137,67 @@ func TestRepository_RecalculateAccesses2(t *testing.T) { assert.NoError(t, err) assert.False(t, has) } + +func testRecalculateAccessesUpdateMode(t *testing.T) { + // Test the update path in refreshAccesses optimization + // Scenario: User's access mode changes from Read to Write + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + assert.NoError(t, repo.LoadOwner(t.Context())) + + // Verify initial access mode + _ = db.Insert(t.Context(), &repo_model.Collaboration{UserID: 4, RepoID: 4, Mode: perm_model.AccessModeWrite}) + _ = db.Insert(t.Context(), &access_model.Access{UserID: 4, RepoID: 4, Mode: perm_model.AccessModeWrite}) + initialAccess := &access_model.Access{UserID: 4, RepoID: 4} + has, err := db.GetEngine(t.Context()).Get(initialAccess) + assert.NoError(t, err) + assert.True(t, has) + initialMode := initialAccess.Mode + + // Change collaboration mode to trigger update path + newMode := perm_model.AccessModeAdmin + assert.NotEqual(t, initialMode, newMode, "New mode should differ from initial mode") + + _, err = db.GetEngine(t.Context()). + Where("user_id = ? AND repo_id = ?", 4, 4). + Cols("mode"). + Update(&repo_model.Collaboration{Mode: newMode}) + assert.NoError(t, err) + + // Recalculate accesses - should UPDATE existing access, not delete+insert + assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo)) + + // Verify access was updated, not deleted and re-inserted + updatedAccess := &access_model.Access{UserID: 4, RepoID: 4} + has, err = db.GetEngine(t.Context()).Get(updatedAccess) + assert.NoError(t, err) + assert.True(t, has, "Access should still exist") + assert.Equal(t, newMode, updatedAccess.Mode, "Access mode should be updated to new collaboration mode") +} + +func testRecalculateAccessesRemoveAccess(t *testing.T) { + // Test the delete path in refreshAccesses optimization + // Scenario: Remove a user's collaboration, access should be deleted + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + assert.NoError(t, repo.LoadOwner(t.Context())) + + // Verify initial access exists + initialAccess := &access_model.Access{UserID: 4, RepoID: 4} + has, err := db.GetEngine(t.Context()).Get(initialAccess) + assert.NoError(t, err) + assert.True(t, has, "Access should exist initially") + + // Remove the collaboration to trigger delete path + _, err = db.GetEngine(t.Context()). + Where("user_id = ? AND repo_id = ?", 4, 4). + Delete(&repo_model.Collaboration{}) + assert.NoError(t, err) + + // Recalculate accesses - should DELETE the access record + assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo)) + + // Verify access was deleted + removedAccess := &access_model.Access{UserID: 4, RepoID: 4} + has, err = db.GetEngine(t.Context()).Get(removedAccess) + assert.NoError(t, err) + assert.False(t, has, "Access should be deleted after removing collaboration") +} diff --git a/package/gitea/source/models/perm/access/actions_repo_permission_test.go b/package/gitea/source/models/perm/access/actions_repo_permission_test.go new file mode 100644 index 00000000..442f6cf2 --- /dev/null +++ b/package/gitea/source/models/perm/access/actions_repo_permission_test.go @@ -0,0 +1,155 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package access + +import ( + "testing" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/models/db" + perm_model "code.gitea.io/gitea/models/perm" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetActionsUserRepoPermission(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + ctx := t.Context() + + // Use fixtures for repos and users + repo4 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) // Public, Owner 5, has Actions unit + repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) // Private, Owner 2, no Actions unit in fixtures + repo15 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 15}) // Private, Owner 2, no Actions unit in fixtures + owner2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + actionsUser := user_model.NewActionsUser() + + // Ensure repo2 and repo15 have Actions units for testing configuration + for _, r := range []*repo_model.Repository{repo2, repo15} { + require.NoError(t, db.Insert(ctx, &repo_model.RepoUnit{ + RepoID: r.ID, + Type: unit.TypeActions, + Config: &repo_model.ActionsConfig{}, + })) + } + + t.Run("SameRepo_Public", func(t *testing.T) { + task47 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47}) + require.Equal(t, repo4.ID, task47.RepoID) + + perm, err := GetActionsUserRepoPermission(ctx, repo4, actionsUser, task47.ID) + require.NoError(t, err) + + // Public repo, bot should have Read access even if not collaborator + assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) + assert.True(t, perm.CanRead(unit.TypeCode)) + }) + + t.Run("SameRepo_Private", func(t *testing.T) { + // Use Task 53 which is already in Repo 2 (Private) + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + require.Equal(t, repo2.ID, task53.RepoID) + + perm, err := GetActionsUserRepoPermission(ctx, repo2, actionsUser, task53.ID) + require.NoError(t, err) + + // Private repo, bot has no base access, but gets Write from effective tokens perms (Permissive by default) + assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) + assert.True(t, perm.CanWrite(unit.TypeCode)) + }) + + t.Run("CrossRepo_Denied_None", func(t *testing.T) { + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + + // Set owner policy to nil allowed repos (None) + cfg := actions_model.OwnerActionsConfig{} + require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, cfg)) + + perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID) + require.NoError(t, err) + + // Should NOT have access to the private repo. + assert.False(t, perm.CanRead(unit.TypeCode)) + }) + + t.Run("ForkPR_NoCrossRepo", func(t *testing.T) { + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + task53.IsForkPullRequest = true + require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request")) + + // Policy contains repo15 + cfg := actions_model.OwnerActionsConfig{ + AllowedCrossRepoIDs: []int64{repo15.ID}, + } + require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, cfg)) + + perm, err := GetActionsUserRepoPermission(ctx, repo15, actionsUser, task53.ID) + require.NoError(t, err) + + // Fork PR never gets cross-repo access to other private repos + assert.False(t, perm.CanRead(unit.TypeCode)) + }) + + t.Run("Inheritance_And_Clamping", func(t *testing.T) { + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + task53.IsForkPullRequest = false + require.NoError(t, actions_model.UpdateTask(ctx, task53, "is_fork_pull_request")) + + // Owner policy: Restricted mode (Read-only Code) + ownerCfg := actions_model.OwnerActionsConfig{ + TokenPermissionMode: repo_model.ActionsTokenPermissionModeRestricted, + MaxTokenPermissions: &repo_model.ActionsTokenPermissions{ + UnitAccessModes: map[unit.Type]perm_model.AccessMode{ + unit.TypeCode: perm_model.AccessModeRead, + }, + }, + } + require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, ownerCfg)) + + // Repo policy: OverrideOwnerConfig = false (should inherit owner's restricted mode) + repo2ActionsUnit := repo2.MustGetUnit(ctx, unit.TypeActions) + repo2ActionsCfg := repo2ActionsUnit.ActionsConfig() + repo2ActionsCfg.OverrideOwnerConfig = false + require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo2ActionsUnit)) + + perm, err := GetActionsUserRepoPermission(ctx, repo2, actionsUser, task53.ID) + require.NoError(t, err) + + // Should be clamped to Read-only + assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeCode)) + assert.False(t, perm.CanWrite(unit.TypeCode)) + }) + + t.Run("RepoOverride_Clamping", func(t *testing.T) { + task53 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53}) + + // Owner policy: Permissive (Write access) + ownerCfg := actions_model.OwnerActionsConfig{ + TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive, + } + require.NoError(t, actions_model.SetOwnerActionsConfig(ctx, owner2.ID, ownerCfg)) + + // Repo policy: OverrideOwnerConfig = true, MaxTokenPermissions = Read + repo2ActionsUnit := repo2.MustGetUnit(ctx, unit.TypeActions) + repo2ActionsCfg := repo2ActionsUnit.ActionsConfig() + repo2ActionsCfg.OverrideOwnerConfig = true + repo2ActionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionModeRestricted + repo2ActionsCfg.MaxTokenPermissions = &repo_model.ActionsTokenPermissions{ + UnitAccessModes: map[unit.Type]perm_model.AccessMode{ + unit.TypeCode: perm_model.AccessModeRead, + }, + } + require.NoError(t, repo_model.UpdateRepoUnitConfig(ctx, repo2ActionsUnit)) + + perm, err := GetActionsUserRepoPermission(ctx, repo2, actionsUser, task53.ID) + require.NoError(t, err) + + // Should be clamped to Read-only + assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeCode)) + }) +} diff --git a/package/gitea/source/models/perm/access/repo_permission.go b/package/gitea/source/models/perm/access/repo_permission.go index b663a6c0..21821f17 100644 --- a/package/gitea/source/models/perm/access/repo_permission.go +++ b/package/gitea/source/models/perm/access/repo_permission.go @@ -5,9 +5,13 @@ package access import ( "context" + "errors" "fmt" + "maps" "slices" + "strings" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" perm_model "code.gitea.io/gitea/models/perm" @@ -167,7 +171,8 @@ func (p *Permission) ReadableUnitTypes() []unit.Type { } func (p *Permission) LogString() string { - format := "") + return fmt.Sprintf(format.String(), args...) } func applyPublicAccessPermission(unitType unit.Type, accessMode perm_model.AccessMode, modeMap *map[unit.Type]perm_model.AccessMode) { @@ -254,8 +259,139 @@ func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) { } } -// GetUserRepoPermission returns the user permissions to the repository -func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { +func checkSameOwnerCrossRepoAccess(ctx context.Context, taskRepo, targetRepo *repo_model.Repository, isForkPR bool) bool { + if isForkPR { + // Fork PRs are never allowed cross-repo access to other private repositories of the owner. + return false + } + if taskRepo.OwnerID != targetRepo.OwnerID { + return false + } + ownerCfg, err := actions_model.GetOwnerActionsConfig(ctx, targetRepo.OwnerID) + if err != nil { + log.Error("GetOwnerActionsConfig: %v", err) + return false + } + + return slices.Contains(ownerCfg.AllowedCrossRepoIDs, targetRepo.ID) +} + +// GetActionsUserRepoPermission returns the actions user permissions to the repository +func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Repository, actionsUser *user_model.User, taskID int64) (perm Permission, err error) { + if actionsUser.ID != user_model.ActionsUserID { + return perm, errors.New("api GetActionsUserRepoPermission can only be called by the actions user") + } + task, err := actions_model.GetTaskByID(ctx, taskID) + if err != nil { + return perm, err + } + + if err := task.LoadJob(ctx); err != nil { + return perm, err + } + + var taskRepo *repo_model.Repository + if task.RepoID != repo.ID { + if err := task.Job.LoadRepo(ctx); err != nil { + return perm, err + } + taskRepo = task.Job.Repo + } else { + taskRepo = repo + } + + // Compute effective permissions for this task against the target repo + effectivePerms, err := actions_model.ComputeTaskTokenPermissions(ctx, task, repo) + if err != nil { + return perm, err + } + if task.RepoID != repo.ID { + // Cross-repo access must also respect the target repo's permission ceiling. + targetRepoActionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig() + if targetRepoActionsCfg.OverrideOwnerConfig { + effectivePerms = targetRepoActionsCfg.ClampPermissions(effectivePerms) + } else { + targetRepoOwnerActionsCfg, err := actions_model.GetOwnerActionsConfig(ctx, repo.OwnerID) + if err != nil { + return perm, err + } + effectivePerms = targetRepoOwnerActionsCfg.ClampPermissions(effectivePerms) + } + } + + if err := repo.LoadUnits(ctx); err != nil { + return perm, err + } + + var maxPerm Permission + + // Set up per-unit access modes based on configured permissions + maxPerm.units = repo.Units + maxPerm.unitsMode = maps.Clone(effectivePerms.UnitAccessModes) + + // Check permission like simple user but limit to read-only (PR #36095) + // Enhanced to also grant read-only access if isSameRepo is true and target repository is public + botPerm, err := GetIndividualUserRepoPermission(ctx, repo, user_model.NewActionsUser()) + if err != nil { + return perm, err + } + if botPerm.AccessMode >= perm_model.AccessModeRead { + // Public repo allows read access, increase permissions to at least read + // Otherwise you cannot access your own repository if your permissions are set to none but the repository is public + for _, u := range repo.Units { + if botPerm.CanRead(u.Type) { + maxPerm.unitsMode[u.Type] = max(maxPerm.unitsMode[u.Type], perm_model.AccessModeRead) + } + } + } + + if task.RepoID == repo.ID { + return maxPerm, nil + } + + if checkSameOwnerCrossRepoAccess(ctx, taskRepo, repo, task.IsForkPullRequest) { + // Access allowed by owner policy (grants access to private repos). + // Note: maxPerm has already been restricted to Read-Only in ComputeTaskTokenPermissions + // because isSameRepo is false. + return maxPerm, nil + } + + // Fall through to allow public repository read access via botPerm check below + + // Check if the repo is public or the Bot has explicit access + if botPerm.AccessMode >= perm_model.AccessModeRead { + return maxPerm, nil + } + + // Check Collaborative Owner and explicit Bot permissions + // We allow access if: + // 1. It's a collaborative owner relationship + // 2. The Actions Bot user has been explicitly granted access and repository is private + // 3. The repository is public (handled by botPerm above) + + if taskRepo.IsPrivate { + actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions) + if actionsUnit.ActionsConfig().IsCollaborativeOwner(taskRepo.OwnerID) { + return maxPerm, nil + } + } + + return perm, nil +} + +// GetDoerRepoPermission returns the repository permission for the current actor, +// dispatching to GetActionsUserRepoPermission when the actor is an Actions token user. +func GetDoerRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) { + if taskID, ok := user_model.GetActionsUserTaskID(user); ok { + return GetActionsUserRepoPermission(ctx, repo, user, taskID) + } + return GetIndividualUserRepoPermission(ctx, repo, user) +} + +// GetIndividualUserRepoPermission returns the permissions for an explicit user identity. +// In most request paths, callers should use GetDoerRepoPermission instead. +// Unlike GetDoerRepoPermission, this helper does not resolve Actions task users. +func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) { defer func() { if err == nil { finalProcessRepoUnitPermission(user, &perm) @@ -414,8 +550,9 @@ func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re // AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the // user does not have access. +// This helper only supports explicit user identities and does not resolve Actions task users. func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint:revive // export stutter - perm, err := GetUserRepoPermission(ctx, repo, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { return perm_model.AccessModeNone, err } @@ -434,7 +571,7 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model. if user.IsOrganization() { return false, fmt.Errorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID) } - perm, err := GetUserRepoPermission(ctx, repo, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { return false, err } @@ -443,6 +580,7 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model. } // HasAnyUnitAccess see the comment of "perm.HasAnyUnitAccess" +// This helper only supports explicit user identities and does not resolve Actions task users. func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) { var user *user_model.User var err error @@ -452,7 +590,7 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi return false, err } } - perm, err := GetUserRepoPermission(ctx, repo, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { return false, err } @@ -500,13 +638,14 @@ func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository, } // CheckRepoUnitUser check whether user could visit the unit of this repository +// This helper only supports explicit user identities and does not resolve Actions task users. func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool { if user != nil && user.IsAdmin { return true } - perm, err := GetUserRepoPermission(ctx, repo, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo, user) if err != nil { - log.Error("GetUserRepoPermission: %w", err) + log.Error("GetIndividualUserRepoPermission: %w", err) return false } diff --git a/package/gitea/source/models/perm/access/repo_permission_test.go b/package/gitea/source/models/perm/access/repo_permission_test.go index a36be213..29a8dac7 100644 --- a/package/gitea/source/models/perm/access/repo_permission_test.go +++ b/package/gitea/source/models/perm/access/repo_permission_test.go @@ -6,6 +6,7 @@ package access import ( "testing" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" perm_model "code.gitea.io/gitea/models/perm" @@ -157,8 +158,13 @@ func TestUnitAccessMode(t *testing.T) { assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map") } -func TestGetUserRepoPermission(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestGetRepoPermission(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + t.Run("GetIndividualUserRepoPermission", testGetIndividualUserRepoPermission) + t.Run("GetDoerRepoPermission", testGetDoerRepoPermission) +} + +func testGetIndividualUserRepoPermission(t *testing.T) { ctx := t.Context() repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo require.NoError(t, repo32.LoadOwner(ctx)) @@ -172,7 +178,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID})) t.Run("DoerInTeamWithNoRepo", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo32, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo32, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo @@ -181,7 +187,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID})) require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone})) t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo32, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo32, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode]) @@ -191,7 +197,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{})) require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite})) t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo32, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo32, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode) assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode]) @@ -204,7 +210,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}, &Access{})) // The user has access set of that repo, remove it, it is useless for our test require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo3.ID})) t.Run("DoerWithNoopTeamOnPrivateRepo", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo3, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo3, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode]) @@ -214,7 +220,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone})) require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeIssues, AccessMode: perm_model.AccessModeRead})) t.Run("DoerWithReadIssueTeamOnPrivateRepo", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo3, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo3, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode) assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode]) @@ -233,7 +239,7 @@ func TestGetUserRepoPermission(t *testing.T) { require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite})) require.NoError(t, db.Insert(ctx, Access{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite})) t.Run("DoerWithReadIssueTeamAndWriteCollaboratorOnPrivateRepo", func(t *testing.T) { - perm, err := GetUserRepoPermission(ctx, repo3, user) + perm, err := GetIndividualUserRepoPermission(ctx, repo3, user) require.NoError(t, err) assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode) assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode]) @@ -245,3 +251,25 @@ func TestGetUserRepoPermission(t *testing.T) { assert.Equal(t, user.ID, users[0].ID) }) } + +func testGetDoerRepoPermission(t *testing.T) { + ctx := t.Context() + + repo4 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + task47 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47}) + actionsDoer := user_model.NewActionsUserWithTaskID(task47.ID) + regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + actionsPerm, err := GetDoerRepoPermission(ctx, repo4, actionsDoer) + require.NoError(t, err) + directPerm, err := GetActionsUserRepoPermission(ctx, repo4, actionsDoer, task47.ID) + require.NoError(t, err) + assert.Equal(t, directPerm, actionsPerm) + + doerPerm, err := GetDoerRepoPermission(ctx, repo1, regularUser) + require.NoError(t, err) + individualPerm, err := GetIndividualUserRepoPermission(ctx, repo1, regularUser) + require.NoError(t, err) + assert.Equal(t, individualPerm, doerPerm) +} diff --git a/package/gitea/source/models/project/column.go b/package/gitea/source/models/project/column.go index 79f6dfe9..7365204f 100644 --- a/package/gitea/source/models/project/column.go +++ b/package/gitea/source/models/project/column.go @@ -257,9 +257,12 @@ func (p *Project) GetColumns(ctx context.Context) (ColumnList, error) { return columns, nil } -// getDefaultColumn return default column and ensure only one exists -func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) { +// getDefaultColumnWithFallback return default column if one exists +// otherwise return the first column by sorting and set it as default column +func (p *Project) getDefaultColumnWithFallback(ctx context.Context) (*Column, error) { var column Column + + // try to find a column "default=true" has, err := db.GetEngine(ctx). Where("project_id=? AND `default` = ?", p.ID, true). Desc("id").Get(&column) @@ -270,23 +273,9 @@ func (p *Project) getDefaultColumn(ctx context.Context) (*Column, error) { if has { return &column, nil } - return nil, ErrProjectColumnNotExist{ColumnID: 0} -} -// MustDefaultColumn returns the default column for a project. -// If one exists, it is returned -// If none exists, the first column will be elevated to the default column of this project -func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) { - c, err := p.getDefaultColumn(ctx) - if err != nil && !IsErrProjectColumnNotExist(err) { - return nil, err - } - if c != nil { - return c, nil - } - - var column Column - has, err := db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column) + // try to find the first column by sorting + has, err = db.GetEngine(ctx).Where("project_id=?", p.ID).OrderBy("sorting, id").Get(&column) if err != nil { return nil, err } @@ -298,8 +287,24 @@ func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) { return &column, nil } + return nil, ErrProjectColumnNotExist{ColumnID: 0} +} + +// MustDefaultColumn returns the default column for a project. +// If one exists, it is returned +// If none exists, the first column will be elevated to the default column of this project +// If there is no column, it creates a default column and returns it +func (p *Project) MustDefaultColumn(ctx context.Context) (*Column, error) { + c, err := p.getDefaultColumnWithFallback(ctx) + if err != nil && !IsErrProjectColumnNotExist(err) { + return nil, err + } + if c != nil { + return c, nil + } + // create a default column if none is found - column = Column{ + column := Column{ ProjectID: p.ID, Default: true, Title: "Uncategorized", diff --git a/package/gitea/source/models/pull/automerge.go b/package/gitea/source/models/pull/automerge.go index 7f940a98..d32dc847 100644 --- a/package/gitea/source/models/pull/automerge.go +++ b/package/gitea/source/models/pull/automerge.go @@ -5,14 +5,12 @@ package pull import ( "context" - "errors" "fmt" "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" ) // AutoMerge represents a pull request scheduled for merging when checks succeed @@ -78,16 +76,8 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe return false, nil, err } - doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) - if errors.Is(err, util.ErrNotExist) { - doer, err = user_model.NewGhostUser(), nil - } - if err != nil { - return false, nil, err - } - - scheduledPRM.Doer = doer - return true, scheduledPRM, nil + scheduledPRM.DoerID, scheduledPRM.Doer, err = user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID) + return true, scheduledPRM, err } // DeleteScheduledAutoMerge delete a scheduled pull request diff --git a/package/gitea/source/models/renderhelper/repo_file.go b/package/gitea/source/models/renderhelper/repo_file.go index e0375ed2..5d0bfd6c 100644 --- a/package/gitea/source/models/renderhelper/repo_file.go +++ b/package/gitea/source/models/renderhelper/repo_file.go @@ -50,8 +50,8 @@ type RepoFileOptions struct { DeprecatedRepoName string // it is only a patch for the non-standard "markup" api DeprecatedOwnerName string // it is only a patch for the non-standard "markup" api - CurrentRefPath string // eg: "branch/main" - CurrentTreePath string // eg: "path/to/file" in the repo + CurrentRefPath string // eg: "branch/main", it is a sub URL path escaped by callers, TODO: rename to CurrentRefSubURL + CurrentTreePath string // eg: "path/to/file" in the repo, it is the tree path without URL path escaping } func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, opts ...RepoFileOptions) *markup.RenderContext { @@ -70,6 +70,10 @@ func NewRenderContextRepoFile(ctx context.Context, repo *repo_model.Repository, "repo": helper.opts.DeprecatedRepoName, }) } - rctx = rctx.WithHelper(helper) + // External render's iframe needs this to generate correct links + // TODO: maybe need to make it access "CurrentRefPath" directly (but impossible at the moment due to cycle-import) + // CurrentRefPath is already path-escaped by callers + rctx.RenderOptions.Metas["RefTypeNameSubURL"] = helper.opts.CurrentRefPath + rctx = rctx.WithHelper(helper).WithEnableHeadingIDGeneration(true) return rctx } diff --git a/package/gitea/source/models/renderhelper/repo_wiki.go b/package/gitea/source/models/renderhelper/repo_wiki.go index b75f1b97..218b1e4a 100644 --- a/package/gitea/source/models/renderhelper/repo_wiki.go +++ b/package/gitea/source/models/renderhelper/repo_wiki.go @@ -71,7 +71,7 @@ func NewRenderContextRepoWiki(ctx context.Context, repo *repo_model.Repository, "markupAllowShortIssuePattern": "true", }) } - rctx = rctx.WithHelper(helper) + rctx = rctx.WithHelper(helper).WithEnableHeadingIDGeneration(true) helper.ctx = rctx return rctx } diff --git a/package/gitea/source/models/repo.go b/package/gitea/source/models/repo.go index 522debb9..c5c5364d 100644 --- a/package/gitea/source/models/repo.go +++ b/package/gitea/source/models/repo.go @@ -7,6 +7,7 @@ package models import ( "context" "strconv" + "strings" _ "image/jpeg" // Needed for jpeg support @@ -86,11 +87,20 @@ func labelStatsCorrectNumClosedIssuesRepo(ctx context.Context, id int64) error { return err } -var milestoneStatsQueryNumIssues = "SELECT `milestone`.id FROM `milestone` WHERE `milestone`.num_closed_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id AND `issue`.is_closed=?) OR `milestone`.num_issues!=(SELECT COUNT(*) FROM `issue` WHERE `issue`.milestone_id=`milestone`.id)" +func milestoneStatsQueryNumIssuesSQL() string { + sql := ` +SELECT "milestone".id FROM "milestone" +WHERE ( + "milestone".num_closed_issues != (SELECT COUNT(*) FROM "issue" WHERE "issue".milestone_id="milestone".id AND "issue".is_closed=?) + OR "milestone".num_issues != (SELECT COUNT(*) FROM "issue" WHERE "issue".milestone_id="milestone".id) +) +` + return strings.TrimSpace(strings.ReplaceAll(sql, "\"", "`")) +} func milestoneStatsCorrectNumIssuesRepo(ctx context.Context, id int64) error { e := db.GetEngine(ctx) - results, err := e.Query(milestoneStatsQueryNumIssues+" AND `milestone`.repo_id = ?", true, id) + results, err := e.Query(milestoneStatsQueryNumIssuesSQL()+" AND `milestone`.repo_id = ?", true, id) if err != nil { return err } @@ -192,7 +202,7 @@ func CheckRepoStats(ctx context.Context) error { }, // Milestone.Num{,Closed}Issues { - statsQuery(milestoneStatsQueryNumIssues, true), + statsQuery(milestoneStatsQueryNumIssuesSQL(), true), issues_model.UpdateMilestoneCounters, "milestone count 'num_closed_issues' and 'num_issues'", }, diff --git a/package/gitea/source/modules/git/repo_archive_test.go b/package/gitea/source/models/repo/archive_test.go similarity index 98% rename from package/gitea/source/modules/git/repo_archive_test.go rename to package/gitea/source/models/repo/archive_test.go index ff7e2dfc..bb6c1bf9 100644 --- a/package/gitea/source/modules/git/repo_archive_test.go +++ b/package/gitea/source/models/repo/archive_test.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package repo import ( "testing" diff --git a/package/gitea/source/models/repo/archiver.go b/package/gitea/source/models/repo/archiver.go index d06e94e5..ca981a17 100644 --- a/package/gitea/source/models/repo/archiver.go +++ b/package/gitea/source/models/repo/archiver.go @@ -11,7 +11,6 @@ import ( "time" "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -27,11 +26,46 @@ const ( ArchiverReady // it's ready ) +// ArchiveType archive types +type ArchiveType int + +const ( + ArchiveUnknown ArchiveType = iota + ArchiveZip // 1 + ArchiveTarGz // 2 + ArchiveBundle // 3 +) + +// String converts an ArchiveType to string: the extension of the archive file without prefix dot +func (a ArchiveType) String() string { + switch a { + case ArchiveZip: + return "zip" + case ArchiveTarGz: + return "tar.gz" + case ArchiveBundle: + return "bundle" + } + return "unknown" +} + +func SplitArchiveNameType(s string) (string, ArchiveType) { + switch { + case strings.HasSuffix(s, ".zip"): + return strings.TrimSuffix(s, ".zip"), ArchiveZip + case strings.HasSuffix(s, ".tar.gz"): + return strings.TrimSuffix(s, ".tar.gz"), ArchiveTarGz + case strings.HasSuffix(s, ".bundle"): + return strings.TrimSuffix(s, ".bundle"), ArchiveBundle + } + return s, ArchiveUnknown +} + // RepoArchiver represents all archivers type RepoArchiver struct { //revive:disable-line:exported - ID int64 `xorm:"pk autoincr"` - RepoID int64 `xorm:"index unique(s)"` - Type git.ArchiveType `xorm:"unique(s)"` + ID int64 `xorm:"pk autoincr"` + RepoID int64 `xorm:"index unique(s)"` + Type ArchiveType `xorm:"unique(s)"` Status ArchiverStatus CommitID string `xorm:"VARCHAR(64) unique(s)"` CreatedUnix timeutil.TimeStamp `xorm:"INDEX NOT NULL created"` @@ -56,15 +90,15 @@ func repoArchiverForRelativePath(relativePath string) (*RepoArchiver, error) { if err != nil { return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid repo id") } - commitID, archiveType := git.SplitArchiveNameType(parts[2]) - if archiveType == git.ArchiveUnknown { + commitID, archiveType := SplitArchiveNameType(parts[2]) + if archiveType == ArchiveUnknown { return nil, util.NewInvalidArgumentErrorf("invalid storage path: invalid archive type") } return &RepoArchiver{RepoID: repoID, CommitID: commitID, Type: archiveType}, nil } // GetRepoArchiver get an archiver -func GetRepoArchiver(ctx context.Context, repoID int64, tp git.ArchiveType, commitID string) (*RepoArchiver, error) { +func GetRepoArchiver(ctx context.Context, repoID int64, tp ArchiveType, commitID string) (*RepoArchiver, error) { var archiver RepoArchiver has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).And("`type`=?", tp).And("commit_id=?", commitID).Get(&archiver) if err != nil { @@ -73,7 +107,7 @@ func GetRepoArchiver(ctx context.Context, repoID int64, tp git.ArchiveType, comm if has { return &archiver, nil } - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } // ExistsRepoArchiverWithStoragePath checks if there is a RepoArchiver for a given storage path diff --git a/package/gitea/source/models/repo/avatar.go b/package/gitea/source/models/repo/avatar.go index eff64bd2..bfb08d99 100644 --- a/package/gitea/source/models/repo/avatar.go +++ b/package/gitea/source/models/repo/avatar.go @@ -41,20 +41,14 @@ func generateRandomAvatar(ctx context.Context, repo *Repository) error { idToString := strconv.FormatInt(repo.ID, 10) seed := idToString - img, err := avatar.RandomImage([]byte(seed)) - if err != nil { - return fmt.Errorf("RandomImage: %w", err) - } + img := avatar.RandomImageDefaultSize([]byte(seed)) repo.Avatar = idToString if err := storage.SaveFrom(storage.RepoAvatars, repo.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, img); err != nil { - log.Error("Encode: %v", err) - } - return err + return png.Encode(w, img) }); err != nil { - return fmt.Errorf("Failed to create dir %s: %w", repo.CustomAvatarRelativePath(), err) + return fmt.Errorf("failed to create dir %s: %w", repo.CustomAvatarRelativePath(), err) } log.Info("New random avatar created for repository: %d", repo.ID) diff --git a/package/gitea/source/models/repo/fork.go b/package/gitea/source/models/repo/fork.go index 1c75e864..80b3e563 100644 --- a/package/gitea/source/models/repo/fork.go +++ b/package/gitea/source/models/repo/fork.go @@ -49,7 +49,7 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error) return nil, err } if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return &forkedRepo, nil } diff --git a/package/gitea/source/models/repo/pull_request_default.go b/package/gitea/source/models/repo/pull_request_default.go new file mode 100644 index 00000000..89f8eb6a --- /dev/null +++ b/package/gitea/source/models/repo/pull_request_default.go @@ -0,0 +1,16 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/util" +) + +func (repo *Repository) GetPullRequestTargetBranch(ctx context.Context) string { + unitPRConfig := repo.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig() + return util.IfZero(unitPRConfig.DefaultTargetBranch, repo.DefaultBranch) +} diff --git a/package/gitea/source/models/repo/pull_request_default_test.go b/package/gitea/source/models/repo/pull_request_default_test.go new file mode 100644 index 00000000..b1653f2f --- /dev/null +++ b/package/gitea/source/models/repo/pull_request_default_test.go @@ -0,0 +1,32 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "testing" + + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultTargetBranchSelection(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + ctx := t.Context() + repo := unittest.AssertExistsAndLoadBean(t, &Repository{ID: 1}) + + assert.Equal(t, repo.DefaultBranch, repo.GetPullRequestTargetBranch(ctx)) + + repo.Units = nil + prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests) + assert.NoError(t, err) + prConfig := prUnit.PullRequestsConfig() + prConfig.DefaultTargetBranch = "branch2" + prUnit.Config = prConfig + assert.NoError(t, UpdateRepoUnitConfig(ctx, prUnit)) + repo.Units = nil + assert.Equal(t, "branch2", repo.GetPullRequestTargetBranch(ctx)) +} diff --git a/package/gitea/source/models/repo/repo.go b/package/gitea/source/models/repo/repo.go index 8237a429..25207cc2 100644 --- a/package/gitea/source/models/repo/repo.go +++ b/package/gitea/source/models/repo/repo.go @@ -229,10 +229,6 @@ func RelativePath(ownerName, repoName string) string { return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".git" } -func RelativeWikiPath(ownerName, repoName string) string { - return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git" -} - // RelativePath should be an unix style path like username/reponame.git func (repo *Repository) RelativePath() string { return RelativePath(repo.OwnerName, repo.Name) @@ -245,12 +241,6 @@ func (sr StorageRepo) RelativePath() string { return string(sr) } -// WikiStorageRepo returns the storage repo for the wiki -// The wiki repository should have the same object format as the code repository -func (repo *Repository) WikiStorageRepo() StorageRepo { - return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name)) -} - // SanitizedOriginalURL returns a sanitized OriginalURL func (repo *Repository) SanitizedOriginalURL() string { if repo.OriginalURL == "" { @@ -291,7 +281,7 @@ func (repo *Repository) SizeDetailsString() string { var str strings.Builder sizeDetails := repo.SizeDetails() for _, detail := range sizeDetails { - str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size))) + fmt.Fprintf(&str, "%s: %s, ", detail.Name, base.FileSize(detail.Size)) } return strings.TrimSuffix(str.String(), ", ") } @@ -432,52 +422,37 @@ func (repo *Repository) UnitEnabled(ctx context.Context, tp unit.Type) bool { return false } -// MustGetUnit always returns a RepoUnit object +// MustGetUnit always returns a RepoUnit object even if the unit doesn't exist (not enabled) func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit { ru, err := repo.GetUnit(ctx, tp) if err == nil { return ru } - + if !errors.Is(err, util.ErrNotExist) { + setting.PanicInDevOrTesting("Failed to get unit %v for repository %d: %v", tp, repo.ID, err) + } + ru = &RepoUnit{RepoID: repo.ID, Type: tp} switch tp { case unit.TypeExternalWiki: - return &RepoUnit{ - Type: tp, - Config: new(ExternalWikiConfig), - } + ru.Config = new(ExternalWikiConfig) case unit.TypeExternalTracker: - return &RepoUnit{ - Type: tp, - Config: new(ExternalTrackerConfig), - } + ru.Config = new(ExternalTrackerConfig) case unit.TypePullRequests: - return &RepoUnit{ - Type: tp, - Config: new(PullRequestsConfig), - } + ru.Config = new(PullRequestsConfig) case unit.TypeIssues: - return &RepoUnit{ - Type: tp, - Config: new(IssuesConfig), - } + ru.Config = new(IssuesConfig) case unit.TypeActions: - return &RepoUnit{ - Type: tp, - Config: new(ActionsConfig), - } + ru.Config = new(ActionsConfig) case unit.TypeProjects: - cfg := new(ProjectsConfig) - cfg.ProjectsMode = ProjectsModeNone - return &RepoUnit{ - Type: tp, - Config: cfg, + ru.Config = new(ProjectsConfig) + default: // other units don't have config + } + if ru.Config != nil { + if err = ru.Config.FromDB(nil); err != nil { + setting.PanicInDevOrTesting("Failed to load default config for unit %v of repository %d: %v", tp, repo.ID, err) } } - - return &RepoUnit{ - Type: tp, - Config: new(UnitConfig), - } + return ru } // GetUnit returns a RepoUnit object @@ -605,7 +580,7 @@ func (repo *Repository) IsGenerated() bool { // RepoPath returns repository path by given user and repository name. func RepoPath(userName, repoName string) string { //revive:disable-line:exported - return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".git") + return filepath.Join(setting.RepoRootPath, filepath.Clean(strings.ToLower(userName)), filepath.Clean(strings.ToLower(repoName)+".git")) } // RepoPath returns the repository path @@ -623,16 +598,13 @@ func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) strin return fmt.Sprintf("%s/%s/compare/%s...%s", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name), util.PathEscapeSegments(oldCommitID), util.PathEscapeSegments(newCommitID)) } -func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, branchName string) string { - if baseRepo == nil { - baseRepo = repo - } +func (repo *Repository) ComposeBranchCompareURL(baseRepo *Repository, baseBranch, branchName string) string { var cmpBranchEscaped string if repo.ID != baseRepo.ID { cmpBranchEscaped = fmt.Sprintf("%s/%s:", url.PathEscape(repo.OwnerName), url.PathEscape(repo.Name)) } cmpBranchEscaped = fmt.Sprintf("%s%s", cmpBranchEscaped, util.PathEscapeSegments(branchName)) - return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseRepo.DefaultBranch), cmpBranchEscaped) + return fmt.Sprintf("%s/compare/%s...%s", baseRepo.Link(), util.PathEscapeSegments(baseBranch), cmpBranchEscaped) } // IsOwnedBy returns true when user owns this repository @@ -879,16 +851,6 @@ func GetRepositoriesMapByIDs(ctx context.Context, ids []int64) (map[int64]*Repos return repos, db.GetEngine(ctx).In("id", ids).Find(&repos) } -// IsRepositoryModelOrDirExist returns true if the repository with given name under user has already existed. -func IsRepositoryModelOrDirExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) { - has, err := IsRepositoryModelExist(ctx, u, repoName) - if err != nil { - return false, err - } - isDir, err := util.IsDir(RepoPath(u.Name, repoName)) - return has || isDir, err -} - func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName string) (bool, error) { return db.GetEngine(ctx).Get(&Repository{ OwnerID: u.ID, @@ -901,7 +863,7 @@ func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName st // non-generated repositories, and TemplateRepo will be left untouched) func GetTemplateRepo(ctx context.Context, repo *Repository) (*Repository, error) { if !repo.IsGenerated() { - return nil, nil + return nil, nil //nolint:nilnil // return nil for non-generated repositories } return GetRepositoryByID(ctx, repo.TemplateID) diff --git a/package/gitea/source/models/repo/repo_list.go b/package/gitea/source/models/repo/repo_list.go index 811f83c9..25dee1bf 100644 --- a/package/gitea/source/models/repo/repo_list.go +++ b/package/gitea/source/models/repo/repo_list.go @@ -212,6 +212,13 @@ type SearchRepoOptions struct { OnlyShowRelevant bool } +func (opts *SearchRepoOptions) ApplyPublicOnly(publicOnly bool) { + if publicOnly { + opts.Private = false + opts.AllLimited = false + } +} + // UserOwnedRepoCond returns user ownered repositories func UserOwnedRepoCond(userID int64) builder.Cond { return builder.Eq{ @@ -778,3 +785,11 @@ func GetUserRepositories(ctx context.Context, opts SearchRepoOptions) (Repositor repos := make(RepositoryList, 0, opts.PageSize) return repos, count, db.SetSessionPagination(sess, &opts).Find(&repos) } + +func GetOwnerRepositoriesByIDs(ctx context.Context, ownerID int64, repoIDs []int64) (RepositoryList, error) { + if len(repoIDs) == 0 { + return RepositoryList{}, nil + } + repos := make(RepositoryList, 0, len(repoIDs)) + return repos, db.GetEngine(ctx).Where(builder.Eq{"owner_id": ownerID}).In("id", repoIDs).Find(&repos) +} diff --git a/package/gitea/source/models/repo/repo_unit.go b/package/gitea/source/models/repo/repo_unit.go index a5207bc2..c32adbbc 100644 --- a/package/gitea/source/models/repo/repo_unit.go +++ b/package/gitea/source/models/repo/repo_unit.go @@ -5,8 +5,6 @@ package repo import ( "context" - "slices" - "strings" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/perm" @@ -131,12 +129,29 @@ type PullRequestsConfig struct { DefaultDeleteBranchAfterMerge bool DefaultMergeStyle MergeStyle DefaultAllowMaintainerEdit bool + DefaultTargetBranch string +} + +func DefaultPullRequestsConfig() *PullRequestsConfig { + cfg := &PullRequestsConfig{ + AllowMerge: true, + AllowRebase: true, + AllowRebaseMerge: true, + AllowSquash: true, + AllowFastForwardOnly: true, + AllowRebaseUpdate: true, + DefaultAllowMaintainerEdit: true, + } + cfg.DefaultDeleteBranchAfterMerge = setting.Repository.PullRequest.DefaultDeleteBranchAfterMerge + cfg.DefaultMergeStyle = MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle) + cfg.DefaultMergeStyle = util.IfZero(cfg.DefaultMergeStyle, MergeStyleMerge) + return cfg } // FromDB fills up a PullRequestsConfig from serialized format. func (cfg *PullRequestsConfig) FromDB(bs []byte) error { - // AllowRebaseUpdate = true as default for existing PullRequestConfig in DB - cfg.AllowRebaseUpdate = true + // set default values for existing PullRequestConfig in DB + *cfg = *DefaultPullRequestsConfig() return json.UnmarshalHandleDoubleEncode(bs, &cfg) } @@ -155,51 +170,8 @@ func (cfg *PullRequestsConfig) IsMergeStyleAllowed(mergeStyle MergeStyle) bool { mergeStyle == MergeStyleManuallyMerged && cfg.AllowManualMerge } -// GetDefaultMergeStyle returns the default merge style for this pull request -func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle { - if len(cfg.DefaultMergeStyle) != 0 { - return cfg.DefaultMergeStyle - } - - if setting.Repository.PullRequest.DefaultMergeStyle != "" { - return MergeStyle(setting.Repository.PullRequest.DefaultMergeStyle) - } - - return MergeStyleMerge -} - -type ActionsConfig struct { - DisabledWorkflows []string -} - -func (cfg *ActionsConfig) EnableWorkflow(file string) { - cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file) -} - -func (cfg *ActionsConfig) ToString() string { - return strings.Join(cfg.DisabledWorkflows, ",") -} - -func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { - return slices.Contains(cfg.DisabledWorkflows, file) -} - -func (cfg *ActionsConfig) DisableWorkflow(file string) { - if slices.Contains(cfg.DisabledWorkflows, file) { - return - } - - cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) -} - -// FromDB fills up a ActionsConfig from serialized format. -func (cfg *ActionsConfig) FromDB(bs []byte) error { - return json.UnmarshalHandleDoubleEncode(bs, &cfg) -} - -// ToDB exports a ActionsConfig to a serialized format. -func (cfg *ActionsConfig) ToDB() ([]byte, error) { - return json.Marshal(cfg) +func DefaultPullRequestsUnit(repoID int64) RepoUnit { + return RepoUnit{RepoID: repoID, Type: unit.TypePullRequests, Config: DefaultPullRequestsConfig()} } // ProjectsMode represents the projects enabled for a repository @@ -223,6 +195,8 @@ type ProjectsConfig struct { // FromDB fills up a ProjectsConfig from serialized format. func (cfg *ProjectsConfig) FromDB(bs []byte) error { + // TODO: remove GetProjectsMode, only use ProjectsMode + cfg.ProjectsMode = ProjectsModeAll return json.UnmarshalHandleDoubleEncode(bs, &cfg) } @@ -253,7 +227,12 @@ func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool { func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { switch colName { case "type": - switch unit.Type(db.Cell2Int64(val)) { + var err error + r.Type, _, err = db.CellToInt(val, unit.TypeInvalid) + if err != nil { + setting.PanicInDevOrTesting("Unable to convert repo unit (id=%d) type: %v", r.ID, err) + } + switch r.Type { case unit.TypeExternalWiki: r.Config = new(ExternalWikiConfig) case unit.TypeExternalTracker: @@ -271,6 +250,11 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { default: r.Config = new(UnitConfig) } + case "config": + if *val == nil { + // XROM doesn't call FromDB if the value is nil, but we need to set default values for the config fields + _ = r.Config.FromDB(nil) + } } } @@ -334,9 +318,9 @@ func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err return units, nil } -// UpdateRepoUnit updates the provided repo unit -func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error { - _, err := db.GetEngine(ctx).ID(unit.ID).Update(unit) +// UpdateRepoUnitConfig updates the config of the provided repo unit +func UpdateRepoUnitConfig(ctx context.Context, unit *RepoUnit) error { + _, err := db.GetEngine(ctx).ID(unit.ID).Cols("config").Update(unit) return err } diff --git a/package/gitea/source/models/repo/repo_unit_actions.go b/package/gitea/source/models/repo/repo_unit_actions.go new file mode 100644 index 00000000..50e29257 --- /dev/null +++ b/package/gitea/source/models/repo/repo_unit_actions.go @@ -0,0 +1,153 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "slices" + + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" +) + +// ActionsTokenPermissionMode defines the default permission mode for Actions tokens +type ActionsTokenPermissionMode string + +const ( + // ActionsTokenPermissionModePermissive - write access by default (current behavior, backwards compatible) + ActionsTokenPermissionModePermissive ActionsTokenPermissionMode = "permissive" + // ActionsTokenPermissionModeRestricted - read access by default + ActionsTokenPermissionModeRestricted ActionsTokenPermissionMode = "restricted" +) + +func (ActionsTokenPermissionMode) EnumValues() []ActionsTokenPermissionMode { + return []ActionsTokenPermissionMode{ActionsTokenPermissionModePermissive /* default */, ActionsTokenPermissionModeRestricted} +} + +// ActionsTokenPermissions defines the permissions for different repository units +type ActionsTokenPermissions struct { + UnitAccessModes map[unit.Type]perm.AccessMode `json:"unit_access_modes,omitempty"` +} + +var ActionsTokenUnitTypes = []unit.Type{ + unit.TypeCode, + unit.TypeIssues, + unit.TypePullRequests, + unit.TypePackages, + unit.TypeActions, + unit.TypeWiki, + unit.TypeReleases, + unit.TypeProjects, +} + +func MakeActionsTokenPermissions(unitAccessMode perm.AccessMode) (ret ActionsTokenPermissions) { + ret.UnitAccessModes = make(map[unit.Type]perm.AccessMode) + for _, u := range ActionsTokenUnitTypes { + ret.UnitAccessModes[u] = unitAccessMode + } + return ret +} + +// ClampActionsTokenPermissions ensures that the given permissions don't exceed the maximum +func ClampActionsTokenPermissions(p1, p2 ActionsTokenPermissions) (ret ActionsTokenPermissions) { + ret.UnitAccessModes = make(map[unit.Type]perm.AccessMode) + for _, ut := range ActionsTokenUnitTypes { + ret.UnitAccessModes[ut] = min(p1.UnitAccessModes[ut], p2.UnitAccessModes[ut]) + } + return ret +} + +// MakeRestrictedPermissions returns the restricted permissions +func MakeRestrictedPermissions() ActionsTokenPermissions { + ret := MakeActionsTokenPermissions(perm.AccessModeNone) + ret.UnitAccessModes[unit.TypeCode] = perm.AccessModeRead + ret.UnitAccessModes[unit.TypePackages] = perm.AccessModeRead + ret.UnitAccessModes[unit.TypeReleases] = perm.AccessModeRead + return ret +} + +type ActionsConfig struct { + DisabledWorkflows []string + // CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos. + // Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions. + CollaborativeOwnerIDs []int64 + // TokenPermissionMode defines the default permission mode (permissive, restricted, or custom) + TokenPermissionMode ActionsTokenPermissionMode `json:"token_permission_mode,omitempty"` + // MaxTokenPermissions defines the absolute maximum permissions any token can have in this context. + // Workflow YAML "permissions" keywords can reduce permissions but never exceed this ceiling. + MaxTokenPermissions *ActionsTokenPermissions `json:"max_token_permissions,omitempty"` + // OverrideOwnerConfig indicates if this repository should override the owner-level configuration (User or Org) + OverrideOwnerConfig bool `json:"override_owner_config,omitempty"` +} + +func (cfg *ActionsConfig) EnableWorkflow(file string) { + cfg.DisabledWorkflows = util.SliceRemoveAll(cfg.DisabledWorkflows, file) +} + +func (cfg *ActionsConfig) IsWorkflowDisabled(file string) bool { + return slices.Contains(cfg.DisabledWorkflows, file) +} + +func (cfg *ActionsConfig) DisableWorkflow(file string) { + if slices.Contains(cfg.DisabledWorkflows, file) { + return + } + + cfg.DisabledWorkflows = append(cfg.DisabledWorkflows, file) +} + +func (cfg *ActionsConfig) AddCollaborativeOwner(ownerID int64) { + if !slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) { + cfg.CollaborativeOwnerIDs = append(cfg.CollaborativeOwnerIDs, ownerID) + } +} + +func (cfg *ActionsConfig) RemoveCollaborativeOwner(ownerID int64) { + cfg.CollaborativeOwnerIDs = util.SliceRemoveAll(cfg.CollaborativeOwnerIDs, ownerID) +} + +func (cfg *ActionsConfig) IsCollaborativeOwner(ownerID int64) bool { + return slices.Contains(cfg.CollaborativeOwnerIDs, ownerID) +} + +// GetDefaultTokenPermissions returns the default token permissions by its TokenPermissionMode. +// It does not apply MaxTokenPermissions; callers must clamp if needed. +func (cfg *ActionsConfig) GetDefaultTokenPermissions() ActionsTokenPermissions { + switch cfg.TokenPermissionMode { + case ActionsTokenPermissionModeRestricted: + return MakeRestrictedPermissions() + case ActionsTokenPermissionModePermissive: + return MakeActionsTokenPermissions(perm.AccessModeWrite) + default: + return ActionsTokenPermissions{} + } +} + +// GetMaxTokenPermissions returns the maximum allowed permissions +func (cfg *ActionsConfig) GetMaxTokenPermissions() ActionsTokenPermissions { + if cfg.MaxTokenPermissions != nil { + return *cfg.MaxTokenPermissions + } + // Default max is write for everything + return MakeActionsTokenPermissions(perm.AccessModeWrite) +} + +// ClampPermissions ensures that the given permissions don't exceed the maximum +func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) ActionsTokenPermissions { + maxPerms := cfg.GetMaxTokenPermissions() + return ClampActionsTokenPermissions(perms, maxPerms) +} + +// FromDB fills up a ActionsConfig from serialized format. +func (cfg *ActionsConfig) FromDB(bs []byte) error { + _ = json.UnmarshalHandleDoubleEncode(bs, &cfg) + cfg.TokenPermissionMode, _ = util.EnumValue(cfg.TokenPermissionMode) + return nil +} + +// ToDB exports a ActionsConfig to a serialized format. +func (cfg *ActionsConfig) ToDB() ([]byte, error) { + return json.Marshal(cfg) +} diff --git a/package/gitea/source/models/repo/repo_unit_test.go b/package/gitea/source/models/repo/repo_unit_test.go index 56dda567..08f9ac2c 100644 --- a/package/gitea/source/models/repo/repo_unit_test.go +++ b/package/gitea/source/models/repo/repo_unit_test.go @@ -4,8 +4,12 @@ package repo import ( + "strings" "testing" + "code.gitea.io/gitea/models/perm" + "code.gitea.io/gitea/models/unit" + "github.com/stretchr/testify/assert" ) @@ -26,5 +30,75 @@ func TestActionsConfig(t *testing.T) { cfg.DisableWorkflow("test1.yaml") cfg.DisableWorkflow("test2.yaml") cfg.DisableWorkflow("test3.yaml") - assert.Equal(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString()) + assert.Equal(t, "test1.yaml,test2.yaml,test3.yaml", strings.Join(cfg.DisabledWorkflows, ",")) +} + +func TestActionsConfigTokenPermissions(t *testing.T) { + t.Run("Default Permission Mode", func(t *testing.T) { + cfg := &ActionsConfig{TokenPermissionMode: "invalid-value"} + _ = cfg.FromDB(nil) + assert.Equal(t, ActionsTokenPermissionModePermissive, cfg.TokenPermissionMode) + assert.Equal(t, perm.AccessModeWrite, cfg.GetDefaultTokenPermissions().UnitAccessModes[unit.TypeCode]) + }) + + t.Run("Explicit Permission Mode", func(t *testing.T) { + cfg := &ActionsConfig{ + TokenPermissionMode: ActionsTokenPermissionModeRestricted, + } + assert.Equal(t, ActionsTokenPermissionModeRestricted, cfg.TokenPermissionMode) + }) + + t.Run("Effective Permissions - Permissive Mode", func(t *testing.T) { + cfg := &ActionsConfig{ + TokenPermissionMode: ActionsTokenPermissionModePermissive, + } + defaultPerms := cfg.GetDefaultTokenPermissions() + perms := cfg.ClampPermissions(defaultPerms) + assert.Equal(t, perm.AccessModeWrite, perms.UnitAccessModes[unit.TypeCode]) + assert.Equal(t, perm.AccessModeWrite, perms.UnitAccessModes[unit.TypeIssues]) + assert.Equal(t, perm.AccessModeWrite, perms.UnitAccessModes[unit.TypePackages]) + }) + + t.Run("Effective Permissions - Restricted Mode", func(t *testing.T) { + cfg := &ActionsConfig{ + TokenPermissionMode: ActionsTokenPermissionModeRestricted, + } + defaultPerms := cfg.GetDefaultTokenPermissions() + perms := cfg.ClampPermissions(defaultPerms) + assert.Equal(t, perm.AccessModeRead, perms.UnitAccessModes[unit.TypeCode]) + assert.Equal(t, perm.AccessModeNone, perms.UnitAccessModes[unit.TypeIssues]) + assert.Equal(t, perm.AccessModeRead, perms.UnitAccessModes[unit.TypePackages]) + }) + + t.Run("Clamp Permissions", func(t *testing.T) { + cfg := &ActionsConfig{ + MaxTokenPermissions: &ActionsTokenPermissions{ + UnitAccessModes: map[unit.Type]perm.AccessMode{ + unit.TypeCode: perm.AccessModeRead, + unit.TypeIssues: perm.AccessModeWrite, + unit.TypePullRequests: perm.AccessModeRead, + unit.TypePackages: perm.AccessModeRead, + unit.TypeActions: perm.AccessModeNone, + unit.TypeWiki: perm.AccessModeWrite, + }, + }, + } + input := ActionsTokenPermissions{ + UnitAccessModes: map[unit.Type]perm.AccessMode{ + unit.TypeCode: perm.AccessModeWrite, // Should be clamped to Read + unit.TypeIssues: perm.AccessModeWrite, // Should stay Write + unit.TypePullRequests: perm.AccessModeWrite, // Should be clamped to Read + unit.TypePackages: perm.AccessModeWrite, // Should be clamped to Read + unit.TypeActions: perm.AccessModeRead, // Should be clamped to None + unit.TypeWiki: perm.AccessModeRead, // Should stay Read + }, + } + clamped := cfg.ClampPermissions(input) + assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypeCode]) + assert.Equal(t, perm.AccessModeWrite, clamped.UnitAccessModes[unit.TypeIssues]) + assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypePullRequests]) + assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypePackages]) + assert.Equal(t, perm.AccessModeNone, clamped.UnitAccessModes[unit.TypeActions]) + assert.Equal(t, perm.AccessModeRead, clamped.UnitAccessModes[unit.TypeWiki]) + }) } diff --git a/package/gitea/source/models/repo/topic.go b/package/gitea/source/models/repo/topic.go index f8f706fc..6d5209d8 100644 --- a/package/gitea/source/models/repo/topic.go +++ b/package/gitea/source/models/repo/topic.go @@ -257,7 +257,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e } if topic == nil { // Repo doesn't have topic, can't be removed - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the topic does not exist } return db.WithTx2(ctx, func(ctx context.Context) (*Topic, error) { diff --git a/package/gitea/source/models/repo/update.go b/package/gitea/source/models/repo/update.go index 3228ae11..bf560cf6 100644 --- a/package/gitea/source/models/repo/update.go +++ b/package/gitea/source/models/repo/update.go @@ -9,8 +9,6 @@ import ( "time" "code.gitea.io/gitea/models/db" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -106,35 +104,6 @@ func (err ErrRepoFilesAlreadyExist) Unwrap() error { return util.ErrAlreadyExist } -// CheckCreateRepository check if doer could create a repository in new owner -func CheckCreateRepository(ctx context.Context, doer, owner *user_model.User, name string, overwriteOrAdopt bool) error { - if !doer.CanCreateRepoIn(owner) { - return ErrReachLimitOfRepo{owner.MaxRepoCreation} - } - - if err := IsUsableRepoName(name); err != nil { - return err - } - - has, err := IsRepositoryModelOrDirExist(ctx, owner, name) - if err != nil { - return fmt.Errorf("IsRepositoryExist: %w", err) - } else if has { - return ErrRepoAlreadyExist{owner.Name, name} - } - - repoPath := RepoPath(owner.Name, name) - isExist, err := util.IsExist(repoPath) - if err != nil { - log.Error("Unable to check if %s exists. Error: %v", repoPath, err) - return err - } - if !overwriteOrAdopt && isExist { - return ErrRepoFilesAlreadyExist{owner.Name, name} - } - return nil -} - // UpdateRepoSize updates the repository size, calculating it using getDirectorySize func UpdateRepoSize(ctx context.Context, repoID, gitSize, lfsSize int64) error { _, err := db.GetEngine(ctx).ID(repoID).Cols("size", "git_size", "lfs_size").NoAutoTime().Update(&Repository{ diff --git a/package/gitea/source/models/repo/user_repo.go b/package/gitea/source/models/repo/user_repo.go index 232087d8..e4b0a1b0 100644 --- a/package/gitea/source/models/repo/user_repo.go +++ b/package/gitea/source/models/repo/user_repo.go @@ -8,6 +8,7 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -23,6 +24,12 @@ type StarredReposOptions struct { IncludePrivate bool } +func (opts *StarredReposOptions) ApplyPublicOnly(publicOnly bool) { + if publicOnly { + opts.IncludePrivate = false + } +} + func (opts *StarredReposOptions) ToConds() builder.Cond { var cond builder.Cond = builder.Eq{ "star.uid": opts.StarrerID, @@ -61,6 +68,12 @@ type WatchedReposOptions struct { IncludePrivate bool } +func (opts *WatchedReposOptions) ApplyPublicOnly(publicOnly bool) { + if publicOnly { + opts.IncludePrivate = false + } +} + func (opts *WatchedReposOptions) ToConds() builder.Cond { var cond builder.Cond = builder.Eq{ "watch.user_id": opts.WatcherID, @@ -94,8 +107,7 @@ func GetWatchedRepos(ctx context.Context, opts *WatchedReposOptions) ([]*Reposit return db.FindAndCount[Repository](ctx, opts) } -// GetRepoAssignees returns all users that have write access and can be assigned to issues -// of the repository, +// GetRepoAssignees returns all users that have write access and can be assigned to issues or pull-requests of the repository, func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) { if err = repo.LoadOwner(ctx); err != nil { return nil, err @@ -114,15 +126,9 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us uniqueUserIDs.AddMultiple(userIDs...) if repo.Owner.IsOrganization() { - additionalUserIDs := make([]int64, 0, 10) - if err = e.Table("team_user"). - Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id"). - Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id"). - Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))", - repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests). - Distinct("`team_user`.uid"). - Select("`team_user`.uid"). - Find(&additionalUserIDs); err != nil { + // issues and pull requests both need "assignee list" + additionalUserIDs, err := organization.GetTeamUserIDsWithAccessToAnyRepoUnit(ctx, repo.OwnerID, repo.ID, perm.AccessModeRead, unit.TypeIssues, unit.TypePullRequests) + if err != nil { return nil, err } uniqueUserIDs.AddMultiple(additionalUserIDs...) @@ -147,19 +153,21 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us } // GetIssuePostersWithSearch returns users with limit of 30 whose username started with prefix that have authored an issue/pull request for the given repository -// If isShowFullName is set to true, also include full name prefix search -func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string, isShowFullName bool) ([]*user_model.User, error) { +// It searches with the "user.name" and "user.full_name" fields case-insensitively. +func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull bool, search string) ([]*user_model.User, error) { users := make([]*user_model.User, 0, 30) - var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"} - if isShowFullName { - prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%")) - } cond := builder.In("`user`.id", builder.Select("poster_id").From("issue").Where( builder.Eq{"repo_id": repo.ID}. And(builder.Eq{"is_pull": isPull}), - ).GroupBy("poster_id")).And(prefixCond) + ).GroupBy("poster_id")) + + if search != "" { + var prefixCond builder.Cond = builder.Like{"lower_name", strings.ToLower(search) + "%"} + prefixCond = prefixCond.Or(db.BuildCaseInsensitiveLike("full_name", "%"+search+"%")) + cond = cond.And(prefixCond) + } return users, db.GetEngine(ctx). Where(cond). diff --git a/package/gitea/source/models/repo/user_repo_test.go b/package/gitea/source/models/repo/user_repo_test.go index a53cf39d..cd45db61 100644 --- a/package/gitea/source/models/repo/user_repo_test.go +++ b/package/gitea/source/models/repo/user_repo_test.go @@ -6,7 +6,12 @@ package repo_test import ( "testing" + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/organization" + perm_model "code.gitea.io/gitea/models/perm" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" @@ -14,9 +19,14 @@ import ( "github.com/stretchr/testify/require" ) -func TestRepoAssignees(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func TestUserRepo(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + t.Run("GetIssuePostersWithSearch", testUserRepoGetIssuePostersWithSearch) + t.Run("Assignees", testUserRepoAssignees) + t.Run("AssigneesNoTeamUnit", testRepoAssigneesNoTeamUnit) +} +func testUserRepoAssignees(t *testing.T) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) users, err := repo_model.GetRepoAssignees(t.Context(), repo2) assert.NoError(t, err) @@ -39,17 +49,37 @@ func TestRepoAssignees(t *testing.T) { } } -func TestGetIssuePostersWithSearch(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) +func testRepoAssigneesNoTeamUnit(t *testing.T) { + ctx := t.Context() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) + require.NoError(t, repo.LoadOwner(ctx)) + require.True(t, repo.Owner.IsOrganization()) + + require.NoError(t, db.TruncateBeans(ctx, &organization.Team{}, &organization.TeamUser{}, &organization.TeamRepo{}, &organization.TeamUnit{}, &access_model.Access{})) + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + team := &organization.Team{OrgID: repo.OwnerID, LowerName: "admin-team", AccessMode: perm_model.AccessModeAdmin} + require.NoError(t, db.Insert(ctx, team)) + require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: repo.OwnerID, TeamID: team.ID, UID: user.ID})) + require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: repo.OwnerID, TeamID: team.ID, RepoID: repo.ID})) + require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: repo.OwnerID, TeamID: team.ID, Type: unit.TypePullRequests, AccessMode: perm_model.AccessModeNone})) + + users, err := repo_model.GetRepoAssignees(ctx, repo) + require.NoError(t, err) + require.Len(t, users, 1) + assert.ElementsMatch(t, []int64{4}, []int64{users[0].ID}) +} + +func testUserRepoGetIssuePostersWithSearch(t *testing.T) { repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) - users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER", false /* full name */) + users, err := repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "USER") require.NoError(t, err) require.Len(t, users, 1) assert.Equal(t, "user2", users[0].Name) - users, err = repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "TW%O", true /* full name */) + users, err = repo_model.GetIssuePostersWithSearch(t.Context(), repo2, false, "TW%O") require.NoError(t, err) require.Len(t, users, 1) assert.Equal(t, "user2", users[0].Name) diff --git a/package/gitea/source/models/repo/wiki.go b/package/gitea/source/models/repo/wiki.go index 9f41445b..47c8fa43 100644 --- a/package/gitea/source/models/repo/wiki.go +++ b/package/gitea/source/models/repo/wiki.go @@ -7,7 +7,6 @@ package repo import ( "context" "fmt" - "path/filepath" "strings" user_model "code.gitea.io/gitea/models/user" @@ -76,12 +75,12 @@ func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User return repo.cloneLink(ctx, doer, repo.Name+".wiki") } -// WikiPath returns wiki data path by given user and repository name. -func WikiPath(userName, repoName string) string { - return filepath.Join(user_model.UserPath(userName), strings.ToLower(repoName)+".wiki.git") +func RelativeWikiPath(ownerName, repoName string) string { + return strings.ToLower(ownerName) + "/" + strings.ToLower(repoName) + ".wiki.git" } -// WikiPath returns wiki data path for given repository. -func (repo *Repository) WikiPath() string { - return WikiPath(repo.OwnerName, repo.Name) +// WikiStorageRepo returns the storage repo for the wiki +// The wiki repository should have the same object format as the code repository +func (repo *Repository) WikiStorageRepo() StorageRepo { + return StorageRepo(RelativeWikiPath(repo.OwnerName, repo.Name)) } diff --git a/package/gitea/source/models/repo/wiki_test.go b/package/gitea/source/models/repo/wiki_test.go index 41e53d93..636c7800 100644 --- a/package/gitea/source/models/repo/wiki_test.go +++ b/package/gitea/source/models/repo/wiki_test.go @@ -4,12 +4,10 @@ package repo_test import ( - "path/filepath" "testing" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" - "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" ) @@ -23,15 +21,10 @@ func TestRepository_WikiCloneLink(t *testing.T) { assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS) } -func TestWikiPath(t *testing.T) { +func TestRepository_RelativeWikiPath(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") - assert.Equal(t, expected, repo_model.WikiPath("user2", "repo1")) -} -func TestRepository_WikiPath(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) - expected := filepath.Join(setting.RepoRootPath, "user2/repo1.wiki.git") - assert.Equal(t, expected, repo.WikiPath()) + assert.Equal(t, "user2/repo1.wiki.git", repo_model.RelativeWikiPath(repo.OwnerName, repo.Name)) + assert.Equal(t, "user2/repo1.wiki.git", repo.WikiStorageRepo().RelativePath()) } diff --git a/package/gitea/source/models/unittest/fixtures_loader.go b/package/gitea/source/models/unittest/fixtures_loader.go index d92b0cdb..5b79cb56 100644 --- a/package/gitea/source/models/unittest/fixtures_loader.go +++ b/package/gitea/source/models/unittest/fixtures_loader.go @@ -169,7 +169,7 @@ func (f *fixturesLoaderInternal) Load() error { func FixturesFileFullPaths(dir string, files []string) (map[string]*FixtureItem, error) { if files != nil && len(files) == 0 { - return nil, nil // load nothing + return nil, nil //nolint:nilnil // load nothing } files = slices.Clone(files) if len(files) == 0 { diff --git a/package/gitea/source/models/unittest/fixtures_test.go b/package/gitea/source/models/unittest/fixtures_test.go index 8a4c5f17..72944ec0 100644 --- a/package/gitea/source/models/unittest/fixtures_test.go +++ b/package/gitea/source/models/unittest/fixtures_test.go @@ -4,19 +4,20 @@ package unittest_test import ( + "os" "path/filepath" "testing" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/require" "xorm.io/xorm" ) var NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) { - return nil, nil + return nil, nil //nolint:nilnil // no vendor fixtures loader configured } /* @@ -58,9 +59,15 @@ func NewFixturesLoaderVendorGoTestfixtures(e *xorm.Engine, opts unittest.Fixture } */ +func TestMain(m *testing.M) { + setting.SetupGiteaTestEnv() + os.Exit(m.Run()) +} + func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions { _ = user_model.User{} - opts := unittest.FixturesOptions{Dir: filepath.Join(test.SetupGiteaRoot(), "models", "fixtures"), Files: []string{ + giteaRoot := setting.GetGiteaTestSourceRoot() + opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{ "user.yml", }} require.NoError(t, unittest.CreateTestEngine(opts)) diff --git a/package/gitea/source/models/unittest/fscopy.go b/package/gitea/source/models/unittest/fscopy.go index 98b01815..cddb7a3f 100644 --- a/package/gitea/source/models/unittest/fscopy.go +++ b/package/gitea/source/models/unittest/fscopy.go @@ -4,10 +4,12 @@ package unittest import ( + "errors" "os" "path/filepath" "strings" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" ) @@ -39,7 +41,20 @@ func SyncFile(srcPath, destPath string) error { // SyncDirs synchronizes files recursively from source to target directory. // It returns error when error occurs in underlying functions. func SyncDirs(srcPath, destPath string) error { - err := os.MkdirAll(destPath, os.ModePerm) + destPath = filepath.Clean(destPath) + destPathAbs, err := filepath.Abs(destPath) + if err != nil { + return err + } + devDataPathAbs, err := filepath.Abs(filepath.Join(setting.GetGiteaTestSourceRoot(), "data")) + if err != nil { + return err + } + if strings.HasPrefix(destPathAbs+string(filepath.Separator), devDataPathAbs+string(filepath.Separator)) { + return errors.New("destination path should not be inside Gitea data directory, otherwise your data for dev mode will be removed") + } + + err = os.MkdirAll(destPath, os.ModePerm) if err != nil { return err } diff --git a/package/gitea/source/models/unittest/testdb.go b/package/gitea/source/models/unittest/testdb.go index 4611a079..63c9a3a9 100644 --- a/package/gitea/source/models/unittest/testdb.go +++ b/package/gitea/source/models/unittest/testdb.go @@ -21,7 +21,7 @@ import ( "code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/tempdir" - "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/testlogger" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -29,16 +29,10 @@ import ( "xorm.io/xorm/names" ) -var giteaRoot string - -func fatalTestError(fmtStr string, args ...any) { - _, _ = fmt.Fprintf(os.Stderr, fmtStr, args...) - os.Exit(1) -} - // InitSettingsForTesting initializes config provider and load common settings for tests func InitSettingsForTesting() { - setting.IsInTesting = true + setting.SetupGiteaTestEnv() + log.OsExiter = func(code int) { if code != 0 { // non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details @@ -50,8 +44,12 @@ func InitSettingsForTesting() { setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") _ = os.Remove(setting.CustomConf) } - setting.InitCfgProvider(setting.CustomConf) - setting.LoadCommonSettings() + + // init paths and config system for testing + getTestEnv := func(key string) string { + return "" + } + setting.InitWorkPathAndCommonConfig(getTestEnv, setting.ArgWorkPathAndCustomConf{CustomConf: setting.CustomConf}) if err := setting.PrepareAppDataPath(); err != nil { log.Fatal("Can not prepare APP_DATA_PATH: %v", err) @@ -60,7 +58,6 @@ func InitSettingsForTesting() { _ = hash.Register("dummy", hash.NewDummyHasher) setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") - setting.InitGiteaEnvVarsForTesting() } // TestOptions represents test options @@ -73,17 +70,18 @@ type TestOptions struct { // MainTest a reusable TestMain(..) function for unit tests that need to use a // test database. Creates the test database, and sets necessary settings. func MainTest(m *testing.M, testOptsArg ...*TestOptions) { - testOpts := util.OptionalArg(testOptsArg, &TestOptions{}) - giteaRoot = test.SetupGiteaRoot() - setting.CustomPath = filepath.Join(giteaRoot, "custom") - InitSettingsForTesting() + os.Exit(mainTest(m, testOptsArg...)) +} +func mainTest(m *testing.M, testOptsArg ...*TestOptions) int { + testOpts := util.OptionalArg(testOptsArg, &TestOptions{}) + InitSettingsForTesting() + giteaRoot := setting.GetGiteaTestSourceRoot() fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles} if err := CreateTestEngine(fixturesOpts); err != nil { - fatalTestError("Error creating test engine: %v\n", err) + testlogger.Panicf("Error creating test engine: %v\n", err) } - setting.IsInTesting = true setting.AppURL = "https://try.gitea.io/" setting.Domain = "try.gitea.io" setting.RunUser = "runuser" @@ -95,20 +93,18 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) { setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos") if err != nil { - fatalTestError("TempDir: %v\n", err) + testlogger.Panicf("TempDir: %v\n", err) } defer cleanup1() setting.RepoRootPath = repoRootPath appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata") if err != nil { - fatalTestError("TempDir: %v\n", err) + testlogger.Panicf("TempDir: %v\n", err) } defer cleanup2() setting.AppDataPath = appDataPath - setting.AppWorkPath = giteaRoot - setting.StaticRootPath = giteaRoot setting.GravatarSource = "https://secure.gravatar.com/avatar/" setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments") @@ -132,22 +128,22 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) { config.SetDynGetter(system.NewDatabaseDynKeyGetter()) if err = cache.Init(); err != nil { - fatalTestError("cache.Init: %v\n", err) + testlogger.Panicf("cache.Init: %v\n", err) } if err = storage.Init(); err != nil { - fatalTestError("storage.Init: %v\n", err) + testlogger.Panicf("storage.Init: %v\n", err) } if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil { - fatalTestError("util.SyncDirs: %v\n", err) + testlogger.Panicf("util.SyncDirs: %v\n", err) } if err = git.InitFull(); err != nil { - fatalTestError("git.Init: %v\n", err) + testlogger.Panicf("git.Init: %v\n", err) } if testOpts.SetUp != nil { if err := testOpts.SetUp(); err != nil { - fatalTestError("set up failed: %v\n", err) + testlogger.Panicf("set up failed: %v\n", err) } } @@ -155,10 +151,10 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) { if testOpts.TearDown != nil { if err := testOpts.TearDown(); err != nil { - fatalTestError("tear down failed: %v\n", err) + testlogger.Panicf("tear down failed: %v\n", err) } } - os.Exit(exitStatus) + return exitStatus } // FixturesOptions fixtures needs to be loaded options @@ -172,7 +168,7 @@ func CreateTestEngine(opts FixturesOptions) error { x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") if err != nil { if strings.Contains(err.Error(), "unknown driver") { - return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) + return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) } return err } @@ -182,7 +178,7 @@ func CreateTestEngine(opts FixturesOptions) error { if err = db.SyncAllTables(); err != nil { return err } - switch os.Getenv("GITEA_UNIT_TESTS_LOG_SQL") { + switch os.Getenv("GITEA_TEST_LOG_SQL") { case "true", "1": x.ShowSQL(true) } @@ -199,7 +195,6 @@ func PrepareTestDatabase() error { // by tests that use the above MainTest(..) function. func PrepareTestEnv(t testing.TB) { assert.NoError(t, PrepareTestDatabase()) - metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta") + metaPath := filepath.Join(setting.GetGiteaTestSourceRoot(), "tests", "gitea-repositories-meta") assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath)) - test.SetupGiteaRoot() // Makes sure GITEA_ROOT is set } diff --git a/package/gitea/source/models/user/avatar.go b/package/gitea/source/models/user/avatar.go index 542bd93b..5811d859 100644 --- a/package/gitea/source/models/user/avatar.go +++ b/package/gitea/source/models/user/avatar.go @@ -30,22 +30,16 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error { seed = u.Name } - img, err := avatar.RandomImage([]byte(seed)) - if err != nil { - return fmt.Errorf("RandomImage: %w", err) - } + img := avatar.RandomImageDefaultSize([]byte(seed)) u.Avatar = avatars.HashEmail(seed) - _, err = storage.Avatars.Stat(u.CustomAvatarRelativePath()) + _, err := storage.Avatars.Stat(u.CustomAvatarRelativePath()) if err != nil { // If unable to Stat the avatar file (usually it means non-existing), then try to save a new one // Don't share the images so that we can delete them easily if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { - if err := png.Encode(w, img); err != nil { - log.Error("Encode: %v", err) - } - return nil + return png.Encode(w, img) }); err != nil { return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err) } @@ -74,7 +68,7 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string { switch { case u.UseCustomAvatar: useLocalAvatar = true - case disableGravatar, setting.OfflineMode: + case disableGravatar: useLocalAvatar = true autoGenerateAvatar = true } diff --git a/package/gitea/source/models/user/badge.go b/package/gitea/source/models/user/badge.go index e475ceb7..a2a5bc38 100644 --- a/package/gitea/source/models/user/badge.go +++ b/package/gitea/source/models/user/badge.go @@ -5,9 +5,12 @@ package user import ( "context" - "fmt" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" + "xorm.io/xorm/schemas" ) // Badge represents a user badge @@ -22,7 +25,16 @@ type Badge struct { type UserBadge struct { //nolint:revive // export stutter ID int64 `xorm:"pk autoincr"` BadgeID int64 - UserID int64 `xorm:"INDEX"` + UserID int64 +} + +// TableIndices implements xorm's TableIndices interface +func (n *UserBadge) TableIndices() []*schemas.Index { + indices := make([]*schemas.Index, 0, 1) + ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType) + ubUnique.AddColumn("user_id", "badge_id") + indices = append(indices, ubUnique) + return indices } func init() { @@ -42,32 +54,85 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) { return badges, count, err } -// CreateBadge creates a new badge. -func CreateBadge(ctx context.Context, badge *Badge) error { - _, err := db.GetEngine(ctx).Insert(badge) - return err +// GetBadgeUsersOptions contains options for getting users with a specific badge +type GetBadgeUsersOptions struct { + db.ListOptions + BadgeSlug string } -// GetBadge returns a badge +// GetBadgeUsers returns the users that have a specific badge with pagination support. +func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) { + sess := db.GetEngine(ctx). + Select("`user`.*"). + Join("INNER", "user_badge", "`user_badge`.user_id=`user`.id"). + Join("INNER", "badge", "`user_badge`.badge_id=badge.id"). + Where("badge.slug=?", opts.BadgeSlug) + + if opts.Page > 0 { + sess = db.SetSessionPagination(sess, opts) + } + + users := make([]*User, 0, opts.PageSize) + count, err := sess.FindAndCount(&users) + return users, count, err +} + +// CreateBadge creates a new badge. +func CreateBadge(ctx context.Context, badge *Badge) error { + exists, err := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge)) + if err != nil { + return err + } + if exists { + return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug) + } + + if _, err := db.GetEngine(ctx).Insert(badge); err != nil { + // Handle race between existence check and insert. + exists, existErr := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge)) + if existErr == nil && exists { + return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug) + } + return err + } + return nil +} + +// GetBadge returns a specific badge func GetBadge(ctx context.Context, slug string) (*Badge, error) { badge := new(Badge) has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge) - if !has { + if err != nil { return nil, err } - return badge, err + if !has { + return nil, util.NewNotExistErrorf("badge does not exist [slug: %s]", slug) + } + return badge, nil } // UpdateBadge updates a badge based on its slug. func UpdateBadge(ctx context.Context, badge *Badge) error { - _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge) + _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Cols("description", "image_url").Update(badge) return err } -// DeleteBadge deletes a badge. +// DeleteBadge deletes a badge and all associated user_badge entries. func DeleteBadge(ctx context.Context, badge *Badge) error { - _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge) - return err + return db.WithTx(ctx, func(ctx context.Context) error { + // First delete all user_badge entries for this badge + if _, err := db.GetEngine(ctx). + Where("badge_id = ?", badge.ID). + Delete(&UserBadge{}); err != nil { + return err + } + + // Then delete the badge itself + if _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge); err != nil { + return err + } + return nil + }) } // AddUserBadge adds a badge to a user. @@ -84,12 +149,25 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error { if err != nil { return err } else if !has { - return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug) + return util.NewNotExistErrorf("badge does not exist [slug: %s]", badge.Slug) } + + exists, err := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge)) + if err != nil { + return err + } + if exists { + return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID) + } + if err := db.Insert(ctx, &UserBadge{ BadgeID: badge.ID, UserID: u.ID, }); err != nil { + exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge)) + if existErr == nil && exists { + return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID) + } return err } } @@ -102,16 +180,33 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error { return RemoveUserBadges(ctx, u, []*Badge{badge}) } -// RemoveUserBadges removes badges from a user. +// RemoveUserBadges removes specific badges from a user. func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error { return db.WithTx(ctx, func(ctx context.Context) error { + if len(badges) == 0 { + return nil + } + + badgeSlugs := make([]string, 0, len(badges)) for _, badge := range badges { - if _, err := db.GetEngine(ctx). - Join("INNER", "badge", "badge.id = `user_badge`.badge_id"). - Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug). - Delete(&UserBadge{}); err != nil { - return err - } + badgeSlugs = append(badgeSlugs, badge.Slug) + } + var userBadges []UserBadge + if err := db.GetEngine(ctx).Table("user_badge"). + Join("INNER", "badge", "badge.id = `user_badge`.badge_id"). + Where("`user_badge`.user_id = ?", u.ID).In("`badge`.slug", badgeSlugs). + Find(&userBadges); err != nil { + return err + } + userBadgeIDs := make([]int64, 0, len(userBadges)) + for _, ub := range userBadges { + userBadgeIDs = append(userBadgeIDs, ub.ID) + } + if len(userBadgeIDs) == 0 { + return nil + } + if _, err := db.GetEngine(ctx).Table("user_badge").In("id", userBadgeIDs).Delete(); err != nil { + return err } return nil }) @@ -122,3 +217,57 @@ func RemoveAllUserBadges(ctx context.Context, u *User) error { _, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{}) return err } + +// SearchBadgeOptions represents the options when finding badges +type SearchBadgeOptions struct { + db.ListOptions + + Keyword string + Slug string + ID int64 + OrderBy db.SearchOrderBy +} + +func (opts *SearchBadgeOptions) ToConds() builder.Cond { + cond := builder.NewCond() + + if opts.Keyword != "" { + keywordCond := builder.Or( + db.BuildCaseInsensitiveLike("badge.slug", opts.Keyword), + db.BuildCaseInsensitiveLike("badge.description", opts.Keyword), + ) + cond = cond.And(keywordCond) + } + + if opts.ID > 0 { + cond = cond.And(builder.Eq{"badge.id": opts.ID}) + } + + if len(opts.Slug) > 0 { + cond = cond.And(builder.Eq{"badge.slug": opts.Slug}) + } + + return cond +} + +func (opts *SearchBadgeOptions) ToOrders() string { + return opts.OrderBy.String() +} + +// SearchBadges returns badges based on the provided SearchBadgeOptions options +func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) { + return db.FindAndCount[Badge](ctx, opts) +} + +// GetBadgeByID returns a specific badge by ID +func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) { + badge := new(Badge) + has, err := db.GetEngine(ctx).ID(id).Get(badge) + if err != nil { + return nil, err + } + if !has { + return nil, util.NewNotExistErrorf("badge does not exist [id: %d]", id) + } + return badge, nil +} diff --git a/package/gitea/source/models/user/badge_test.go b/package/gitea/source/models/user/badge_test.go new file mode 100644 index 00000000..04979a9c --- /dev/null +++ b/package/gitea/source/models/user/badge_test.go @@ -0,0 +1,185 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" +) + +func TestBadge(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + t.Run("GetBadgeNotExist", testGetBadgeNotExist) + t.Run("CreateBadgeAlreadyExists", testCreateBadgeAlreadyExists) + t.Run("GetBadgeUsers", testGetBadgeUsers) + t.Run("AddAndRemoveUserBadges", testAddAndRemoveUserBadges) + t.Run("SearchBadgesOrderingAndKeyword", testSearchBadgesOrderingAndKeyword) +} + +func testGetBadgeNotExist(t *testing.T) { + badge, err := user_model.GetBadge(t.Context(), "does-not-exist") + assert.Nil(t, badge) + assert.Error(t, err) + assert.ErrorIs(t, err, util.ErrNotExist) +} + +func testCreateBadgeAlreadyExists(t *testing.T) { + badge := &user_model.Badge{ + Slug: "duplicate-badge-slug", + Description: "First", + } + assert.NoError(t, user_model.CreateBadge(t.Context(), badge)) + + err := user_model.CreateBadge(t.Context(), &user_model.Badge{ + Slug: "duplicate-badge-slug", + Description: "Second", + }) + assert.Error(t, err) + assert.ErrorIs(t, err, util.ErrAlreadyExist) +} + +func testGetBadgeUsers(t *testing.T) { + // Create a test badge + badge := &user_model.Badge{ + Slug: "test-badge-users", + Description: "Test Badge", + ImageURL: "test.png", + } + assert.NoError(t, user_model.CreateBadge(t.Context(), badge)) + + // Create test users and assign badges + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + assert.NoError(t, user_model.AddUserBadge(t.Context(), user1, badge)) + assert.NoError(t, user_model.AddUserBadge(t.Context(), user2, badge)) + defer func() { + assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge)) + assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user2, badge)) + }() + + // Test getting users with pagination + opts := &user_model.GetBadgeUsersOptions{ + BadgeSlug: badge.Slug, + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 1, + }, + } + + users, count, err := user_model.GetBadgeUsers(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 2, count) + assert.Len(t, users, 1) + + // Test second page + opts.Page = 2 + users, count, err = user_model.GetBadgeUsers(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 2, count) + assert.Len(t, users, 1) + + // Test with non-existent badge + opts.BadgeSlug = "non-existent" + users, count, err = user_model.GetBadgeUsers(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 0, count) + assert.Empty(t, users) +} + +func testAddAndRemoveUserBadges(t *testing.T) { + badge1 := unittest.AssertExistsAndLoadBean(t, &user_model.Badge{ID: 1}) + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + + // Add a badge to user and verify that it is returned in the list + assert.NoError(t, user_model.AddUserBadge(t.Context(), user1, badge1)) + badges, count, err := user_model.GetUserBadges(t.Context(), user1) + assert.Equal(t, int64(1), count) + assert.Equal(t, badge1.Slug, badges[0].Slug) + assert.NoError(t, err) + + // Confirm that it is impossible to duplicate the same badge + err = user_model.AddUserBadge(t.Context(), user1, badge1) + assert.Error(t, err) + assert.ErrorIs(t, err, util.ErrAlreadyExist) + + // Nothing happened to the existing badge + badges, count, err = user_model.GetUserBadges(t.Context(), user1) + assert.Equal(t, int64(1), count) + assert.Equal(t, badge1.Slug, badges[0].Slug) + assert.NoError(t, err) + + // Remove a badge from user and verify that it is no longer in the list + assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge1)) + _, count, err = user_model.GetUserBadges(t.Context(), user1) + assert.Equal(t, int64(0), count) + assert.NoError(t, err) + + // Removing empty or missing badge selections should be a no-op. + assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, nil)) + assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, []*user_model.Badge{{Slug: "does-not-exist"}})) +} + +func testSearchBadgesOrderingAndKeyword(t *testing.T) { + createdBadges := []*user_model.Badge{ + {Slug: "badge-sort-b", Description: "Badge Sort B"}, + {Slug: "badge-sort-c", Description: "Badge Sort C"}, + {Slug: "badge-sort-a", Description: "Badge Sort A"}, + {Slug: "badge-sort-case", Description: "MiXeDCaSeKeyword"}, + } + for _, badge := range createdBadges { + assert.NoError(t, user_model.CreateBadge(t.Context(), badge)) + } + + opts := &user_model.SearchBadgeOptions{ + ListOptions: db.ListOptions{ListAll: true}, + Keyword: "badge-sort-", + OrderBy: db.SearchOrderBy("`badge`.id ASC"), + } + + oldestFirst, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 4, count) + assert.Equal(t, []string{"badge-sort-b", "badge-sort-c", "badge-sort-a", "badge-sort-case"}, collectBadgeSlugs(oldestFirst)) + + opts.OrderBy = db.SearchOrderBy("`badge`.id DESC") + newestFirst, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 4, count) + assert.Equal(t, []string{"badge-sort-case", "badge-sort-a", "badge-sort-c", "badge-sort-b"}, collectBadgeSlugs(newestFirst)) + + opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC") + alpha, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 4, count) + assert.Equal(t, []string{"badge-sort-a", "badge-sort-b", "badge-sort-c", "badge-sort-case"}, collectBadgeSlugs(alpha)) + + opts.OrderBy = db.SearchOrderBy("`badge`.slug DESC") + reverseAlpha, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 4, count) + assert.Equal(t, []string{"badge-sort-case", "badge-sort-c", "badge-sort-b", "badge-sort-a"}, collectBadgeSlugs(reverseAlpha)) + + opts.Keyword = "mixedcasekeyword" + opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC") + caseInsensitive, count, err := user_model.SearchBadges(t.Context(), opts) + assert.NoError(t, err) + assert.EqualValues(t, 1, count) + assert.Equal(t, []string{"badge-sort-case"}, collectBadgeSlugs(caseInsensitive)) +} + +func collectBadgeSlugs(badges []*user_model.Badge) []string { + slugs := make([]string, 0, len(badges)) + for _, badge := range badges { + slugs = append(slugs, badge.Slug) + } + return slugs +} diff --git a/package/gitea/source/models/user/block.go b/package/gitea/source/models/user/block.go index 5f2b65a1..f4afd47d 100644 --- a/package/gitea/source/models/user/block.go +++ b/package/gitea/source/models/user/block.go @@ -90,7 +90,7 @@ func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, er return nil, err } if len(blocks) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return blocks[0], nil } diff --git a/package/gitea/source/models/user/email_address.go b/package/gitea/source/models/user/email_address.go index 67aa1bdd..aa483d5f 100644 --- a/package/gitea/source/models/user/email_address.go +++ b/package/gitea/source/models/user/email_address.go @@ -215,7 +215,7 @@ func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, err if has, err := db.GetEngine(ctx).ID(id).Get(email); err != nil { return nil, err } else if !has { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist } return email, nil } @@ -276,17 +276,22 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e return UpdateUserCols(ctx, user, "rands") } -func MakeActiveEmailPrimary(ctx context.Context, emailID int64) error { - return makeEmailPrimaryInternal(ctx, emailID, true) +func MakeActiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error { + return makeEmailPrimaryInternal(ctx, ownerID, emailID, true) } -func MakeInactiveEmailPrimary(ctx context.Context, emailID int64) error { - return makeEmailPrimaryInternal(ctx, emailID, false) +func MakeInactiveEmailPrimary(ctx context.Context, ownerID, emailID int64) error { + return makeEmailPrimaryInternal(ctx, ownerID, emailID, false) } -func makeEmailPrimaryInternal(ctx context.Context, emailID int64, isActive bool) error { +func makeEmailPrimaryInternal(ctx context.Context, ownerID, emailID int64, isActive bool) error { email := &EmailAddress{} - if has, err := db.GetEngine(ctx).ID(emailID).Where(builder.Eq{"is_activated": isActive}).Get(email); err != nil { + if has, err := db.GetEngine(ctx).ID(emailID). + Where(builder.Eq{ + "uid": ownerID, + "is_activated": isActive, + }). + Get(email); err != nil { return err } else if !has { return ErrEmailAddressNotExist{} @@ -336,7 +341,7 @@ func ChangeInactivePrimaryEmail(ctx context.Context, uid int64, oldEmailAddr, ne if err != nil { return err } - return MakeInactiveEmailPrimary(ctx, newEmail.ID) + return MakeInactiveEmailPrimary(ctx, uid, newEmail.ID) }) } diff --git a/package/gitea/source/models/user/email_address_test.go b/package/gitea/source/models/user/email_address_test.go index 6ef18fb0..4167aaac 100644 --- a/package/gitea/source/models/user/email_address_test.go +++ b/package/gitea/source/models/user/email_address_test.go @@ -46,22 +46,22 @@ func TestIsEmailUsed(t *testing.T) { func TestMakeEmailPrimary(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - err := user_model.MakeActiveEmailPrimary(t.Context(), 9999999) + err := user_model.MakeActiveEmailPrimary(t.Context(), 1, 9999999) assert.Error(t, err) assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) email := unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user11@example.com"}) - err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID) + err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID) assert.Error(t, err) assert.ErrorIs(t, err, user_model.ErrEmailAddressNotExist{}) // inactive email is considered as not exist for "MakeActiveEmailPrimary" email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user9999999@example.com"}) - err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID) + err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID) assert.Error(t, err) assert.True(t, user_model.IsErrUserNotExist(err)) email = unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{Email: "user101@example.com"}) - err = user_model.MakeActiveEmailPrimary(t.Context(), email.ID) + err = user_model.MakeActiveEmailPrimary(t.Context(), email.UID, email.ID) assert.NoError(t, err) user, _ := user_model.GetUserByID(t.Context(), int64(10)) diff --git a/package/gitea/source/models/user/list.go b/package/gitea/source/models/user/list.go index ca589d1e..aaaa7965 100644 --- a/package/gitea/source/models/user/list.go +++ b/package/gitea/source/models/user/list.go @@ -24,7 +24,7 @@ func (users UserList) GetUserIDs() []int64 { return userIDs } -// GetTwoFaStatus return state of 2FA enrollement +// GetTwoFaStatus return state of 2FA enrollment func (users UserList) GetTwoFaStatus(ctx context.Context) map[int64]bool { results := make(map[int64]bool, len(users)) for _, user := range users { @@ -48,7 +48,7 @@ func (users UserList) GetTwoFaStatus(ctx context.Context) map[int64]bool { func (users UserList) loadTwoFactorStatus(ctx context.Context) (map[int64]*auth.TwoFactor, error) { if len(users) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // returns nil when there are no users } userIDs := users.GetUserIDs() diff --git a/package/gitea/source/models/user/search.go b/package/gitea/source/models/user/search.go index cfd0d011..55feef6a 100644 --- a/package/gitea/source/models/user/search.go +++ b/package/gitea/source/models/user/search.go @@ -6,6 +6,7 @@ package user import ( "context" "fmt" + "slices" "strings" "code.gitea.io/gitea/models/db" @@ -17,12 +18,29 @@ import ( "xorm.io/xorm" ) +// AdminUserOrderByMap represents all possible admin user search orders +// This should only be used for admin API endpoints as we should not expose "updated" ordering which could expose recent user activity including logins. +var AdminUserOrderByMap = map[string]map[string]db.SearchOrderBy{ + "asc": { + "name": db.SearchOrderByAlphabetically, + "created": db.SearchOrderByOldest, + "updated": db.SearchOrderByLeastUpdated, + "id": db.SearchOrderByID, + }, + "desc": { + "name": db.SearchOrderByAlphabeticallyReverse, + "created": db.SearchOrderByNewest, + "updated": db.SearchOrderByRecentUpdated, + "id": db.SearchOrderByIDReverse, + }, +} + // SearchUserOptions contains the options for searching type SearchUserOptions struct { db.ListOptions Keyword string - Type UserType + Types []UserType UID int64 LoginName string // this option should be used only for admin user SourceID int64 // this option should be used only for admin user @@ -41,18 +59,24 @@ type SearchUserOptions struct { IncludeReserved bool } +func (opts *SearchUserOptions) ApplyPublicOnly(publicOnly bool) { + if publicOnly { + opts.Visible = []structs.VisibleType{structs.VisibleTypePublic} + } +} + func (opts *SearchUserOptions) toSearchQueryBase(ctx context.Context) *xorm.Session { var cond builder.Cond - cond = builder.Eq{"type": opts.Type} + cond = builder.In("type", opts.Types) if opts.IncludeReserved { - switch opts.Type { - case UserTypeIndividual: + switch { + case slices.Contains(opts.Types, UserTypeIndividual): cond = cond.Or(builder.Eq{"type": UserTypeUserReserved}).Or( builder.Eq{"type": UserTypeBot}, ).Or( builder.Eq{"type": UserTypeRemoteUser}, ) - case UserTypeOrganization: + case slices.Contains(opts.Types, UserTypeOrganization): cond = cond.Or(builder.Eq{"type": UserTypeOrganizationReserved}) } } diff --git a/package/gitea/source/models/user/setting.go b/package/gitea/source/models/user/setting.go index c65afae7..a16fc86e 100644 --- a/package/gitea/source/models/user/setting.go +++ b/package/gitea/source/models/user/setting.go @@ -11,10 +11,12 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/json" setting_module "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "xorm.io/builder" + "xorm.io/xorm/convert" ) // Setting is a key value store of user settings @@ -211,3 +213,44 @@ func upsertUserSettingValue(ctx context.Context, userID int64, key, value string return err }) } + +func GetUserSettingJSON[T any](ctx context.Context, userID int64, key string, def T) (ret T, _ error) { + ret = def + str, err := GetUserSetting(ctx, userID, key) + if err != nil { + return ret, err + } + + conv, ok := any(&ret).(convert.ConversionFrom) + if !ok { + conv, ok = any(ret).(convert.ConversionFrom) + } + if ok { + if err := conv.FromDB(util.UnsafeStringToBytes(str)); err != nil { + return ret, err + } + } else { + if str == "" { + return ret, nil + } + err = json.Unmarshal(util.UnsafeStringToBytes(str), &ret) + } + return ret, err +} + +func SetUserSettingJSON[T any](ctx context.Context, userID int64, key string, val T) (err error) { + conv, ok := any(&val).(convert.ConversionTo) + if !ok { + conv, ok = any(val).(convert.ConversionTo) + } + var bs []byte + if ok { + bs, err = conv.ToDB() + } else { + bs, err = json.Marshal(val) + } + if err != nil { + return err + } + return SetUserSetting(ctx, userID, key, util.UnsafeBytesToString(bs)) +} diff --git a/package/gitea/source/models/user/setting_options.go b/package/gitea/source/models/user/setting_options.go index 6d37ef63..5867b908 100644 --- a/package/gitea/source/models/user/setting_options.go +++ b/package/gitea/source/models/user/setting_options.go @@ -11,10 +11,6 @@ const ( // SettingsKeyShowOutdatedComments is the setting key whether or not to show outdated comments in PRs SettingsKeyShowOutdatedComments = "comment_code.show_outdated" - // UserActivityPubPrivPem is user's private key - UserActivityPubPrivPem = "activitypub.priv_pem" - // UserActivityPubPubPem is user's public key - UserActivityPubPubPem = "activitypub.pub_pem" // SignupIP is the IP address that the user signed up with SignupIP = "signup.ip" // SignupUserAgent is the user agent that the user signed up with @@ -26,4 +22,6 @@ const ( SettingEmailNotificationGiteaActionsAll = "all" SettingEmailNotificationGiteaActionsFailureOnly = "failure-only" // Default for actions email preference SettingEmailNotificationGiteaActionsDisabled = "disabled" + + SettingsKeyActionsConfig = "actions.config" ) diff --git a/package/gitea/source/models/user/user.go b/package/gitea/source/models/user/user.go index b4bd8694..d2dd7990 100644 --- a/package/gitea/source/models/user/user.go +++ b/package/gitea/source/models/user/user.go @@ -7,12 +7,15 @@ package user import ( "context" "encoding/hex" + "errors" "fmt" + "html/template" "mime" "net/mail" "net/url" "path/filepath" "regexp" + "strconv" "strings" "sync" "time" @@ -27,6 +30,7 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" @@ -184,7 +188,7 @@ func (u *User) BeforeUpdate() { } // FIXME: this email doesn't need to be in lowercase, because the emails are mainly managed by the email table with lower_email field - // This trick could be removed in new releases to display the user inputed email as-is. + // This trick could be removed in new releases to display the user inputted email as-is. u.Email = strings.ToLower(u.Email) if !u.IsOrganization() { if len(u.AvatarEmail) == 0 { @@ -212,7 +216,7 @@ func (u *User) SetLastLogin() { // GetPlaceholderEmail returns an noreply email func (u *User) GetPlaceholderEmail() string { - return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress) + return fmt.Sprintf("%d+%s@%s", u.ID, u.LowerName, setting.Service.NoReplyAddress) } // GetEmail returns a noreply email, if the user has set to keep his @@ -249,8 +253,13 @@ func (u *User) MaxCreationLimit() int { } // CanCreateRepoIn checks whether the doer(u) can create a repository in the owner -// NOTE: functions calling this assume a failure due to repository count limit; it ONLY checks the repo number LIMIT, if new checks are added, those functions should be revised +// NOTE: functions calling this assume a failure due to repository count limit, or the owner is not a real user. +// It ONLY checks the repo number LIMIT or whether owner user is real. If new checks are added, those functions should be revised. +// TODO: the callers can only return ErrReachLimitOfRepo, need to fine tune to support other error types in the future. func (u *User) CanCreateRepoIn(owner *User) bool { + if u.ID <= 0 || owner.ID <= 0 { + return false // fake user like Ghost or Actions user + } if u.IsAdmin { return true } @@ -298,6 +307,13 @@ func (u *User) DashboardLink() string { return setting.AppSubURL + "/" } +func (u *User) SettingsLink() string { + if u.IsOrganization() { + return u.OrganisationLink() + "/settings" + } + return setting.AppSubURL + "/user/settings" +} + // HomeLink returns the user or organization home page link. func (u *User) HomeLink() string { return setting.AppSubURL + "/" + url.PathEscape(u.Name) @@ -411,16 +427,6 @@ func (u *User) IsTokenAccessAllowed() bool { return u.Type == UserTypeIndividual || u.Type == UserTypeBot } -// DisplayName returns full name if it's not empty, -// returns username otherwise. -func (u *User) DisplayName() string { - trimmed := strings.TrimSpace(u.FullName) - if len(trimmed) > 0 { - return trimmed - } - return u.Name -} - // EmailTo returns a string suitable to be put into a e-mail `To:` header. func (u *User) EmailTo() string { sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName()) @@ -439,27 +445,45 @@ func (u *User) EmailTo() string { return fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("utf-8", add.Name), add.Address) } -// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set, -// returns username otherwise. +// TODO: DefaultShowFullName causes messy logic, there are already too many methods to display a user's "display name", need to refactor them +// * user.Name / user.FullName: directly used in templates +// * user.DisplayName(): always show FullName if it's not empty, otherwise show Name +// * user.GetDisplayName(): show FullName if it's not empty and DefaultShowFullName is set, otherwise show Name +// * user.ShortName(): used a lot in templates, but it should be removed and let frontend use "ellipsis" styles +// * activity action.ShortActUserName/GetActDisplayName/GetActDisplayNameTitle, etc: duplicate and messy + +// DisplayName returns full name if it's not empty, returns username otherwise. +func (u *User) DisplayName() string { + fullName := strings.TrimSpace(u.FullName) + if fullName != "" { + return fullName + } + return u.Name +} + +// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set, otherwise, username. func (u *User) GetDisplayName() string { if setting.UI.DefaultShowFullName { - trimmed := strings.TrimSpace(u.FullName) - if len(trimmed) > 0 { - return trimmed + fullName := strings.TrimSpace(u.FullName) + if fullName != "" { + return fullName } } return u.Name } -// GetCompleteName returns the full name and username in the form of -// "Full Name (username)" if full name is not empty, otherwise it returns -// "username". -func (u *User) GetCompleteName() string { - trimmedFullName := strings.TrimSpace(u.FullName) - if len(trimmedFullName) > 0 { - return fmt.Sprintf("%s (%s)", trimmedFullName, u.Name) +// ShortName ellipses username to length (still used by many templates), it calls GetDisplayName and respects DEFAULT_SHOW_FULL_NAME +func (u *User) ShortName(length int) string { + return util.EllipsisDisplayString(u.GetDisplayName(), length) +} + +func (u *User) GetShortDisplayNameLinkHTML() template.HTML { + fullName := strings.TrimSpace(u.FullName) + displayName, displayTooltip := u.Name, fullName + if setting.UI.DefaultShowFullName && fullName != "" { + displayName, displayTooltip = fullName, u.Name } - return u.Name + return htmlutil.HTMLFormat(`%s`, u.HomeLink(), displayTooltip, displayName) } func gitSafeName(name string) string { @@ -482,18 +506,10 @@ func (u *User) GitName() string { return fmt.Sprintf("user-%d", u.ID) } -// ShortName ellipses username to length -func (u *User) ShortName(length int) string { - if setting.UI.DefaultShowFullName && len(u.FullName) > 0 { - return util.EllipsisDisplayString(u.FullName, length) - } - return util.EllipsisDisplayString(u.Name, length) -} - -// IsMailable checks if a user is eligible -// to receive emails. +// IsMailable checks if a user is eligible to receive emails. +// System users like Ghost and Gitea Actions are excluded. func (u *User) IsMailable() bool { - return u.IsActive + return u.IsActive && !u.IsGiteaActions() && !u.IsGhost() } // IsUserExist checks if given username exist, @@ -980,7 +996,7 @@ func GetInactiveUsers(ctx context.Context, olderThan time.Duration) ([]*User, er // UserPath returns the path absolute path of user repositories. func UserPath(userName string) string { //revive:disable-line:exported - return filepath.Join(setting.RepoRootPath, strings.ToLower(userName)) + return filepath.Join(setting.RepoRootPath, filepath.Clean(strings.ToLower(userName))) } // GetUserByID returns the user object by given ID if exists. @@ -1008,17 +1024,22 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) { return users, err } -// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0 -func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) { +// GetPossibleUserByID returns the possible user and its ID. If the user doesn't exist, it returns Ghost user +func GetPossibleUserByID(ctx context.Context, id int64) (_ int64, u *User, err error) { if id < 0 { if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok { - return newFunc(), nil + u = newFunc() } - return nil, ErrUserNotExist{UID: id} - } else if id == 0 { - return nil, ErrUserNotExist{} } - return GetUserByID(ctx, id) + if u == nil { + u, err = GetUserByID(ctx, id) + if errors.Is(err, util.ErrNotExist) { + u = NewGhostUser() + } else if err != nil { + return 0, nil, err + } + } + return u.ID, u, nil } // GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0 @@ -1039,13 +1060,13 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) { return users, nil } -// GetUserByName returns user by given name. -func GetUserByName(ctx context.Context, name string) (*User, error) { - if len(name) == 0 { - return nil, ErrUserNotExist{Name: name} +func getUserByNameWithTypes(ctx context.Context, name string, types ...UserType) (*User, error) { + u := &User{} + sess := db.GetEngine(ctx).Where(builder.Eq{"lower_name": strings.ToLower(name)}) + if len(types) > 0 { + sess.In("`type`", types) } - u := &User{LowerName: strings.ToLower(name), Type: UserTypeIndividual} - has, err := db.GetEngine(ctx).Get(u) + has, err := sess.Get(u) if err != nil { return nil, err } else if !has { @@ -1054,6 +1075,15 @@ func GetUserByName(ctx context.Context, name string) (*User, error) { return u, nil } +// GetUserByName returns the user object by given name, any user type. +func GetUserByName(ctx context.Context, name string) (*User, error) { + return getUserByNameWithTypes(ctx, name) +} + +func GetIndividualUserByName(ctx context.Context, name string) (*User, error) { + return getUserByNameWithTypes(ctx, name, UserTypeIndividual) +} + // GetUserEmailsByNames returns a list of e-mails corresponds to names of users // that have their email notifications set to enabled or onmention. func GetUserEmailsByNames(ctx context.Context, names []string) []string { @@ -1096,19 +1126,6 @@ func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([] Find(&ous) } -// GetUserNameByID returns username for the id -func GetUserNameByID(ctx context.Context, id int64) (string, error) { - var name string - has, err := db.GetEngine(ctx).Table("user").Where("id = ?", id).Cols("name").Get(&name) - if err != nil { - return "", err - } - if has { - return name, nil - } - return "", nil -} - // GetUserIDsByNames returns a slice of ids corresponds to names. func GetUserIDsByNames(ctx context.Context, names []string, ignoreNonExistent bool) ([]int64, error) { ids := make([]int64, 0, len(names)) @@ -1187,19 +1204,23 @@ func (eum *EmailUserMap) GetByEmail(email string) *User { func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) { if len(emails) == 0 { - return nil, nil + return nil, nil //nolint:nilnil // return nil when there are no emails to look up } needCheckEmails := make(container.Set[string]) needCheckUserNames := make(container.Set[string]) + needCheckUserIDs := make(container.Set[int64]) noReplyAddressSuffix := "@" + strings.ToLower(setting.Service.NoReplyAddress) for _, email := range emails { emailLower := strings.ToLower(email) - if noReplyUserNameLower, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok { - needCheckUserNames.Add(noReplyUserNameLower) - needCheckEmails.Add(emailLower) - } else { - needCheckEmails.Add(emailLower) + needCheckEmails.Add(emailLower) + if localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix); ok { + name, id := parseLocalPartToNameID(localPart) + if id != 0 { + needCheckUserIDs.Add(id) + } else if name != "" { + needCheckUserNames.Add(name) + } } } @@ -1229,16 +1250,59 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro } } - users := make(map[int64]*User, len(needCheckUserNames)) - if err := db.GetEngine(ctx).In("lower_name", needCheckUserNames.Values()).Find(&users); err != nil { - return nil, err + usersByIDs := make(map[int64]*User) + if len(needCheckUserIDs) > 0 || len(needCheckUserNames) > 0 { + cond := builder.NewCond() + if len(needCheckUserIDs) > 0 { + cond = cond.Or(builder.In("id", needCheckUserIDs.Values())) + } + if len(needCheckUserNames) > 0 { + cond = cond.Or(builder.In("lower_name", needCheckUserNames.Values())) + } + if err := db.GetEngine(ctx).Where(cond).Find(&usersByIDs); err != nil { + return nil, err + } } - for _, user := range users { - results[strings.ToLower(user.GetPlaceholderEmail())] = user + + usersByName := make(map[string]*User) + for _, user := range usersByIDs { + usersByName[user.LowerName] = user } + + for _, email := range emails { + emailLower := strings.ToLower(email) + if _, ok := results[emailLower]; ok { + continue + } + + localPart, ok := strings.CutSuffix(emailLower, noReplyAddressSuffix) + if !ok { + continue + } + name, id := parseLocalPartToNameID(localPart) + if user, ok := usersByIDs[id]; ok { + results[emailLower] = user + } else if user, ok := usersByName[name]; ok { + results[emailLower] = user + } + } + return &EmailUserMap{results}, nil } +// parseLocalPartToNameID attempts to unparse local-part of email that's in format id+user +// returns user and id if possible +func parseLocalPartToNameID(localPart string) (string, int64) { + var id int64 + idstr, name, hasPlus := strings.Cut(localPart, "+") + if hasPlus { + id, _ = strconv.ParseInt(idstr, 10, 64) + } else { + name = idstr + } + return name, id +} + // GetUserByEmail returns the user object by given e-mail if exists. func GetUserByEmail(ctx context.Context, email string) (*User, error) { if len(email) == 0 { @@ -1257,24 +1321,24 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { } // Finally, if email address is the protected email address: - if strings.HasSuffix(email, "@"+setting.Service.NoReplyAddress) { - username := strings.TrimSuffix(email, "@"+setting.Service.NoReplyAddress) - user := &User{} - has, err := db.GetEngine(ctx).Where("lower_name=?", username).Get(user) - if err != nil { - return nil, err - } - if has { - return user, nil + if localPart, ok := strings.CutSuffix(email, strings.ToLower("@"+setting.Service.NoReplyAddress)); ok { + name, id := parseLocalPartToNameID(localPart) + if id != 0 { + return GetUserByID(ctx, id) } + return GetIndividualUserByName(ctx, name) } return nil, ErrUserNotExist{Name: email} } -// GetUser checks if a user already exists -func GetUser(ctx context.Context, user *User) (bool, error) { - return db.GetEngine(ctx).Get(user) +func GetIndividualUser(ctx context.Context, user *User) (bool, error) { + // FIXME: the design is wrong, empty User fields won't apply, this function should be removed in the future + has, err := db.GetEngine(ctx).Get(user) + if has && user.Type != UserTypeIndividual { + has = false + } + return has, err } // GetUserByOpenID returns the user object by given OpenID if exists. @@ -1444,15 +1508,3 @@ func DisabledFeaturesWithLoginType(user *User) *container.Set[string] { } return &setting.Admin.UserDisabledFeatures } - -// GetUserOrOrgByName returns the user or org by name -func GetUserOrOrgByName(ctx context.Context, name string) (*User, error) { - var u User - has, err := db.GetEngine(ctx).Where("lower_name = ?", strings.ToLower(name)).Get(&u) - if err != nil { - return nil, err - } else if !has { - return nil, ErrUserNotExist{Name: name} - } - return &u, nil -} diff --git a/package/gitea/source/models/user/user_system.go b/package/gitea/source/models/user/user_system.go index e07274d2..cf4306df 100644 --- a/package/gitea/source/models/user/user_system.go +++ b/package/gitea/source/models/user/user_system.go @@ -4,6 +4,7 @@ package user import ( + "strconv" "strings" "code.gitea.io/gitea/modules/structs" @@ -23,10 +24,6 @@ func NewGhostUser() *User { } } -func IsGhostUserName(name string) bool { - return strings.EqualFold(name, GhostUserName) -} - // IsGhost check if user is fake user for a deleted account func (u *User) IsGhost() bool { if u == nil { @@ -41,36 +38,52 @@ const ( ActionsUserEmail = "teabot@gitea.io" ) -func IsGiteaActionsUserName(name string) bool { - return strings.EqualFold(name, ActionsUserName) -} - // NewActionsUser creates and returns a fake user for running the actions. func NewActionsUser() *User { return &User{ - ID: ActionsUserID, - Name: ActionsUserName, - LowerName: ActionsUserName, - IsActive: true, - FullName: "Gitea Actions", - Email: ActionsUserEmail, - KeepEmailPrivate: true, - LoginName: ActionsUserName, - Type: UserTypeBot, - AllowCreateOrganization: true, - Visibility: structs.VisibleTypePublic, + ID: ActionsUserID, + Name: ActionsUserName, + LowerName: ActionsUserName, + IsActive: true, + FullName: "Gitea Actions", + Email: ActionsUserEmail, + KeepEmailPrivate: true, + LoginName: ActionsUserName, + Type: UserTypeBot, + Visibility: structs.VisibleTypePublic, } } +func NewActionsUserWithTaskID(id int64) *User { + u := NewActionsUser() + // LoginName is for only internal usage in this case, so it can be moved to other fields in the future + u.LoginSource = -1 + u.LoginName = "@" + ActionsUserName + "/" + strconv.FormatInt(id, 10) + return u +} + +func GetActionsUserTaskID(u *User) (int64, bool) { + if u == nil || u.ID != ActionsUserID { + return 0, false + } + prefix, payload, _ := strings.Cut(u.LoginName, "/") + if prefix != "@"+ActionsUserName { + return 0, false + } else if taskID, err := strconv.ParseInt(payload, 10, 64); err == nil { + return taskID, true + } + return 0, false +} + func (u *User) IsGiteaActions() bool { return u != nil && u.ID == ActionsUserID } func GetSystemUserByName(name string) *User { - if IsGhostUserName(name) { + if strings.EqualFold(name, GhostUserName) { return NewGhostUser() } - if IsGiteaActionsUserName(name) { + if strings.EqualFold(name, ActionsUserName) { return NewActionsUser() } return nil diff --git a/package/gitea/source/models/user/user_system_test.go b/package/gitea/source/models/user/user_system_test.go index 5aa3fa46..3ae9c6e3 100644 --- a/package/gitea/source/models/user/user_system_test.go +++ b/package/gitea/source/models/user/user_system_test.go @@ -11,20 +11,30 @@ import ( ) func TestSystemUser(t *testing.T) { - u, err := GetPossibleUserByID(t.Context(), -1) + uid, u, err := GetPossibleUserByID(t.Context(), -1) require.NoError(t, err) + assert.Equal(t, int64(-1), uid) assert.Equal(t, "Ghost", u.Name) assert.Equal(t, "ghost", u.LowerName) assert.True(t, u.IsGhost()) - assert.True(t, IsGhostUserName("gHost")) - u, err = GetPossibleUserByID(t.Context(), -2) + u = GetSystemUserByName("gHost") + require.NotNil(t, u) + assert.Equal(t, "Ghost", u.Name) + + uid, u, err = GetPossibleUserByID(t.Context(), -2) require.NoError(t, err) + assert.Equal(t, int64(-2), uid) assert.Equal(t, "gitea-actions", u.Name) assert.Equal(t, "gitea-actions", u.LowerName) assert.True(t, u.IsGiteaActions()) - assert.True(t, IsGiteaActionsUserName("Gitea-actionS")) - _, err = GetPossibleUserByID(t.Context(), -3) - require.Error(t, err) + u = GetSystemUserByName("Gitea-actionS") + require.NotNil(t, u) + assert.Equal(t, "Gitea Actions", u.FullName) + + uid, u, err = GetPossibleUserByID(t.Context(), 999999) + require.NoError(t, err) + assert.Equal(t, int64(-1), uid) + assert.Equal(t, "Ghost", u.Name) } diff --git a/package/gitea/source/models/user/user_test.go b/package/gitea/source/models/user/user_test.go index 4201ec48..956eaeaf 100644 --- a/package/gitea/source/models/user/user_test.go +++ b/package/gitea/source/models/user/user_test.go @@ -51,12 +51,27 @@ func TestOAuth2Application_LoadUser(t *testing.T) { func TestUserEmails(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) + defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")() t.Run("GetUserEmailsByNames", func(t *testing.T) { - // ignore none active user email + // ignore not active user email assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user9"})) assert.ElementsMatch(t, []string{"user8@example.com", "user5@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "user5"})) assert.ElementsMatch(t, []string{"user8@example.com"}, user_model.GetUserEmailsByNames(t.Context(), []string{"user8", "org7"})) }) + + cases := []struct { + Email string + UID int64 + }{ + {"UseR1@example.com", 1}, + {"user1-2@example.COM", 1}, + {"USER2@" + setting.Service.NoReplyAddress, 2}, + {"2+user2@" + setting.Service.NoReplyAddress, 2}, + {"2+oldUser2UsernameWhichDoesNotMatterForQuery@" + setting.Service.NoReplyAddress, 2}, + {"99999+badUser@" + setting.Service.NoReplyAddress, 0}, + {"user4@example.com", 4}, + {"no-such", 0}, + } t.Run("GetUsersByEmails", func(t *testing.T) { defer test.MockVariableValue(&setting.Service.NoReplyAddress, "NoReply.gitea.internal")() testGetUserByEmail := func(t *testing.T, email string, uid int64) { @@ -70,15 +85,27 @@ func TestUserEmails(t *testing.T) { require.NotNil(t, user) assert.Equal(t, uid, user.ID) } - cases := []struct { - Email string - UID int64 - }{ - {"UseR1@example.com", 1}, - {"user1-2@example.COM", 1}, - {"USER2@" + setting.Service.NoReplyAddress, 2}, - {"user4@example.com", 4}, - {"no-such", 0}, + for _, c := range cases { + t.Run(c.Email, func(t *testing.T) { + testGetUserByEmail(t, c.Email, c.UID) + }) + } + + t.Run("NoReplyConflict", func(t *testing.T) { + setting.Service.NoReplyAddress = "example.com" + testGetUserByEmail(t, "user1-2@example.COM", 1) + }) + }) + t.Run("GetUserByEmail", func(t *testing.T) { + testGetUserByEmail := func(t *testing.T, email string, uid int64) { + user, err := user_model.GetUserByEmail(t.Context(), email) + if uid == 0 { + require.Error(t, err) + assert.Nil(t, user) + } else { + require.NotNil(t, user) + assert.Equal(t, uid, user.ID) + } } for _, c := range cases { t.Run(c.Email, func(t *testing.T) { @@ -126,7 +153,7 @@ func TestSearchUsers(t *testing.T) { // test orgs testOrgSuccess := func(opts user_model.SearchUserOptions, expectedOrgIDs []int64) { - opts.Type = user_model.UserTypeOrganization + opts.Types = []user_model.UserType{user_model.UserTypeOrganization} testSuccess(opts, expectedOrgIDs) } @@ -150,7 +177,7 @@ func TestSearchUsers(t *testing.T) { // test users testUserSuccess := func(opts user_model.SearchUserOptions, expectedUserIDs []int64) { - opts.Type = user_model.UserTypeIndividual + opts.Types = []user_model.UserType{user_model.UserTypeIndividual} testSuccess(opts, expectedUserIDs) } @@ -648,33 +675,36 @@ func TestGetInactiveUsers(t *testing.T) { func TestCanCreateRepo(t *testing.T) { defer test.MockVariableValue(&setting.Repository.MaxCreationLimit)() const noLimit = -1 - doerNormal := &user_model.User{} - doerAdmin := &user_model.User{IsAdmin: true} + doerActions := user_model.NewActionsUser() + doerNormal := &user_model.User{ID: 2} + doerAdmin := &user_model.User{ID: 1, IsAdmin: true} t.Run("NoGlobalLimit", func(t *testing.T) { setting.Repository.MaxCreationLimit = noLimit - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.False(t, doerActions.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.False(t, doerAdmin.CanCreateRepoIn(doerActions)) }) t.Run("GlobalLimit50", func(t *testing.T) { setting.Repository.MaxCreationLimit = 50 - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit - assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) - assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) // limited by global limit + assert.False(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerNormal.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: noLimit})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 0})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 10, MaxRepoCreation: 100})) - assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{NumRepos: 60, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: noLimit})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: noLimit})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 0})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 10, MaxRepoCreation: 100})) + assert.True(t, doerAdmin.CanCreateRepoIn(&user_model.User{ID: 2, NumRepos: 60, MaxRepoCreation: 100})) }) } diff --git a/package/gitea/source/models/webhook/webhook.go b/package/gitea/source/models/webhook/webhook.go index 7d4b2e22..5144c0ec 100644 --- a/package/gitea/source/models/webhook/webhook.go +++ b/package/gitea/source/models/webhook/webhook.go @@ -126,6 +126,7 @@ type Webhook struct { OwnerID int64 `xorm:"INDEX"` IsSystemWebhook bool URL string `xorm:"url TEXT"` + Name string `xorm:"VARCHAR(255) NOT NULL DEFAULT ''"` HTTPMethod string `xorm:"http_method"` ContentType HookContentType Secret string `xorm:"TEXT"` diff --git a/package/gitea/source/models/webhook/webhook_system.go b/package/gitea/source/models/webhook/webhook_system.go index 58d9d4a5..e8b5040c 100644 --- a/package/gitea/source/models/webhook/webhook_system.go +++ b/package/gitea/source/models/webhook/webhook_system.go @@ -9,19 +9,32 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/optional" + + "xorm.io/builder" ) -// GetSystemOrDefaultWebhooks returns webhooks by given argument or all if argument is missing. -func GetSystemOrDefaultWebhooks(ctx context.Context, isSystemWebhook optional.Option[bool]) ([]*Webhook, error) { - webhooks := make([]*Webhook, 0, 5) - if !isSystemWebhook.Has() { - return webhooks, db.GetEngine(ctx).Where("repo_id=? AND owner_id=?", 0, 0). - Find(&webhooks) - } +// ListSystemWebhookOptions options for listing system or default webhooks +type ListSystemWebhookOptions struct { + db.ListOptions + IsActive optional.Option[bool] + IsSystem optional.Option[bool] +} - return webhooks, db.GetEngine(ctx). - Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, isSystemWebhook.Value()). - Find(&webhooks) +func (opts ListSystemWebhookOptions) ToConds() builder.Cond { + cond := builder.NewCond() + cond = cond.And(builder.Eq{"webhook.repo_id": 0}, builder.Eq{"webhook.owner_id": 0}) + if opts.IsActive.Has() { + cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()}) + } + if opts.IsSystem.Has() { + cond = cond.And(builder.Eq{"is_system_webhook": opts.IsSystem.Value()}) + } + return cond +} + +// GetGlobalWebhooks returns global (default and/or system) webhooks +func GetGlobalWebhooks(ctx context.Context, opts *ListSystemWebhookOptions) ([]*Webhook, int64, error) { + return db.FindAndCount[Webhook](ctx, opts) } // GetDefaultWebhooks returns all admin-default webhooks. diff --git a/package/gitea/source/models/webhook/webhook_system_test.go b/package/gitea/source/models/webhook/webhook_system_test.go index 8aac6939..d0013c68 100644 --- a/package/gitea/source/models/webhook/webhook_system_test.go +++ b/package/gitea/source/models/webhook/webhook_system_test.go @@ -12,23 +12,24 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetSystemOrDefaultWebhooks(t *testing.T) { +func TestListSystemWebhookOptions(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) - - hooks, err := GetSystemOrDefaultWebhooks(t.Context(), optional.None[bool]()) + opts := ListSystemWebhookOptions{IsSystem: optional.None[bool]()} + hooks, _, err := GetGlobalWebhooks(t.Context(), &opts) assert.NoError(t, err) if assert.Len(t, hooks, 2) { assert.Equal(t, int64(5), hooks[0].ID) assert.Equal(t, int64(6), hooks[1].ID) } - - hooks, err = GetSystemOrDefaultWebhooks(t.Context(), optional.Some(true)) + opts.IsSystem = optional.Some(true) + hooks, _, err = GetGlobalWebhooks(t.Context(), &opts) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(5), hooks[0].ID) } - hooks, err = GetSystemOrDefaultWebhooks(t.Context(), optional.Some(false)) + opts.IsSystem = optional.Some(false) + hooks, _, err = GetGlobalWebhooks(t.Context(), &opts) assert.NoError(t, err) if assert.Len(t, hooks, 1) { assert.Equal(t, int64(6), hooks[0].ID) diff --git a/package/gitea/source/modules/actions/artifacts.go b/package/gitea/source/modules/actions/artifacts.go index d28726e8..fcf24dbf 100644 --- a/package/gitea/source/modules/actions/artifacts.go +++ b/package/gitea/source/modules/actions/artifacts.go @@ -4,45 +4,82 @@ package actions import ( + "crypto/hmac" + "crypto/sha256" + "encoding/binary" + "io" "net/http" + "strings" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/services/context" ) -// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend -// The v4 backend ensures ContentEncoding is set to "application/zip", which is not the case for the old backend -func IsArtifactV4(art *actions_model.ActionArtifact) bool { - return art.ArtifactName+".zip" == art.ArtifactPath && art.ContentEncoding == "application/zip" -} +type tagType string -func DownloadArtifactV4ServeDirectOnly(ctx *context.Base, art *actions_model.ActionArtifact) (bool, error) { - if setting.Actions.ArtifactStorage.ServeDirect() { - u, err := storage.ActionsArtifacts.URL(art.StoragePath, art.ArtifactPath, ctx.Req.Method, nil) - if u != nil && err == nil { - ctx.Redirect(u.String(), http.StatusFound) - return true, nil - } +// BuildSignature builds a hmac signature for the input values. +// "tag" is an internal pre-defined static string to distinguish the signatures for different purpose. +func BuildSignature(tag tagType, vals ...string) []byte { + m := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret()) + _, _ = io.WriteString(m, string(tag)) + var buf8 [8]byte + for _, v := range vals { + binary.LittleEndian.PutUint64(buf8[:], uint64(len(v))) + _, _ = m.Write(buf8[:]) + _, _ = io.WriteString(m, v) } - return false, nil + return m.Sum(nil) } -func DownloadArtifactV4Fallback(ctx *context.Base, art *actions_model.ActionArtifact) error { +// IsArtifactV4 detects whether the artifact is likely from v4. +// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash +// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend. +func IsArtifactV4(art *actions_model.ActionArtifact) bool { + return strings.Contains(art.ContentEncodingOrType, "/") +} + +func GetArtifactV4ServeDirectURL(art *actions_model.ActionArtifact, method string) (string, error) { + contentType := art.ContentEncodingOrType + u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, method, &storage.ServeDirectOptions{ContentType: contentType}) + if err != nil { + return "", err + } + return u.String(), nil +} + +func DownloadArtifactV4ServeDirect(ctx *context.Base, art *actions_model.ActionArtifact) bool { + if !setting.Actions.ArtifactStorage.ServeDirect() { + return false + } + u, err := GetArtifactV4ServeDirectURL(art, ctx.Req.Method) + if err != nil { + log.Error("GetArtifactV4ServeDirectURL: %v", err) + return false + } + ctx.Redirect(u, http.StatusFound) + return true +} + +func DownloadArtifactV4ReadStorage(ctx *context.Base, art *actions_model.ActionArtifact) error { f, err := storage.ActionsArtifacts.Open(art.StoragePath) if err != nil { return err } defer f.Close() - http.ServeContent(ctx.Resp, ctx.Req, art.ArtifactName+".zip", art.CreatedUnix.AsLocalTime(), f) + httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, f, httplib.ServeHeaderOptions{ + Filename: art.ArtifactPath, + ContentType: art.ContentEncodingOrType, // v4 guarantees that the field is Content-Type + }) return nil } func DownloadArtifactV4(ctx *context.Base, art *actions_model.ActionArtifact) error { - ok, err := DownloadArtifactV4ServeDirectOnly(ctx, art) - if ok || err != nil { - return err + if DownloadArtifactV4ServeDirect(ctx, art) { + return nil } - return DownloadArtifactV4Fallback(ctx, art) + return DownloadArtifactV4ReadStorage(ctx, art) } diff --git a/package/gitea/source/modules/actions/artifacts_test.go b/package/gitea/source/modules/actions/artifacts_test.go new file mode 100644 index 00000000..4c038436 --- /dev/null +++ b/package/gitea/source/modules/actions/artifacts_test.go @@ -0,0 +1,36 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuildSignature(t *testing.T) { + a := BuildSignature("v0", "x") + b := BuildSignature("v0", "x") + assert.Equal(t, a, b) + + a = BuildSignature("v0", "x", "yz") + b = BuildSignature("v0", "xy", "z") + assert.NotEqual(t, a, b) + + a = BuildSignature("v1", "x") + b = BuildSignature("v2", "x") + assert.NotEqual(t, a, b) + + a = BuildSignature("v0", "x") + b = BuildSignature("v0x") + assert.NotEqual(t, a, b) + + a = BuildSignature("v0", "", "x") + b = BuildSignature("v0", "x", "") + assert.NotEqual(t, a, b) + + a = BuildSignature("v0") + b = BuildSignature("v0") + assert.Equal(t, a, b) +} diff --git a/package/gitea/source/modules/actions/github.go b/package/gitea/source/modules/actions/github.go index 68116ec8..b7f475aa 100644 --- a/package/gitea/source/modules/actions/github.go +++ b/package/gitea/source/modules/actions/github.go @@ -59,6 +59,10 @@ func IsDefaultBranchWorkflow(triggedEvent webhook_module.HookEventType) bool { // Github "issues" event // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issues return true + case webhook_module.HookEventWorkflowRun: + // GitHub "workflow_run" event + // https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run + return true } return false diff --git a/package/gitea/source/modules/actions/jobparser/evaluator.go b/package/gitea/source/modules/actions/jobparser/evaluator.go new file mode 100644 index 00000000..b3200950 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/evaluator.go @@ -0,0 +1,189 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jobparser + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "github.com/nektos/act/pkg/exprparser" + "go.yaml.in/yaml/v4" +) + +// ExpressionEvaluator is copied from runner.expressionEvaluator, +// to avoid unnecessary dependencies +type ExpressionEvaluator struct { + interpreter exprparser.Interpreter +} + +func NewExpressionEvaluator(interpreter exprparser.Interpreter) *ExpressionEvaluator { + return &ExpressionEvaluator{interpreter: interpreter} +} + +func (ee ExpressionEvaluator) evaluate(in string, defaultStatusCheck exprparser.DefaultStatusCheck) (any, error) { + evaluated, err := ee.interpreter.Evaluate(in, defaultStatusCheck) + + return evaluated, err +} + +func (ee ExpressionEvaluator) evaluateScalarYamlNode(node *yaml.Node) error { + var in string + if err := node.Decode(&in); err != nil { + return err + } + if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") { + return nil + } + expr, _ := rewriteSubExpression(in, false) + res, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone) + if err != nil { + return err + } + return node.Encode(res) +} + +func (ee ExpressionEvaluator) evaluateMappingYamlNode(node *yaml.Node) error { + // GitHub has this undocumented feature to merge maps, called insert directive + insertDirective := regexp.MustCompile(`\${{\s*insert\s*}}`) + for i := 0; i < len(node.Content)/2; { + k := node.Content[i*2] + v := node.Content[i*2+1] + if err := ee.EvaluateYamlNode(v); err != nil { + return err + } + var sk string + // Merge the nested map of the insert directive + if k.Decode(&sk) == nil && insertDirective.MatchString(sk) { + node.Content = append(append(node.Content[:i*2], v.Content...), node.Content[(i+1)*2:]...) + i += len(v.Content) / 2 + } else { + if err := ee.EvaluateYamlNode(k); err != nil { + return err + } + i++ + } + } + return nil +} + +func (ee ExpressionEvaluator) evaluateSequenceYamlNode(node *yaml.Node) error { + for i := 0; i < len(node.Content); { + v := node.Content[i] + // Preserve nested sequences + wasseq := v.Kind == yaml.SequenceNode + if err := ee.EvaluateYamlNode(v); err != nil { + return err + } + // GitHub has this undocumented feature to merge sequences / arrays + // We have a nested sequence via evaluation, merge the arrays + if v.Kind == yaml.SequenceNode && !wasseq { + node.Content = append(append(node.Content[:i], v.Content...), node.Content[i+1:]...) + i += len(v.Content) + } else { + i++ + } + } + return nil +} + +func (ee ExpressionEvaluator) EvaluateYamlNode(node *yaml.Node) error { + switch node.Kind { + case yaml.ScalarNode: + return ee.evaluateScalarYamlNode(node) + case yaml.MappingNode: + return ee.evaluateMappingYamlNode(node) + case yaml.SequenceNode: + return ee.evaluateSequenceYamlNode(node) + default: + return nil + } +} + +func (ee ExpressionEvaluator) Interpolate(in string) string { + if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") { + return in + } + + expr, _ := rewriteSubExpression(in, true) + evaluated, err := ee.evaluate(expr, exprparser.DefaultStatusCheckNone) + if err != nil { + return "" + } + + value, ok := evaluated.(string) + if !ok { + panic(fmt.Sprintf("Expression %s did not evaluate to a string", expr)) + } + + return value +} + +func escapeFormatString(in string) string { + return strings.ReplaceAll(strings.ReplaceAll(in, "{", "{{"), "}", "}}") +} + +func rewriteSubExpression(in string, forceFormat bool) (string, error) { + if !strings.Contains(in, "${{") || !strings.Contains(in, "}}") { + return in, nil + } + + strPattern := regexp.MustCompile("(?:''|[^'])*'") + pos := 0 + exprStart := -1 + strStart := -1 + var results []string + var formatOut strings.Builder + for pos < len(in) { + if strStart > -1 { + matches := strPattern.FindStringIndex(in[pos:]) + if matches == nil { + return "", errors.New("unclosed string") + } + + strStart = -1 + pos += matches[1] + } else if exprStart > -1 { + exprEnd := strings.Index(in[pos:], "}}") + strStart = strings.Index(in[pos:], "'") + + if exprEnd > -1 && strStart > -1 { + if exprEnd < strStart { + strStart = -1 + } else { + exprEnd = -1 + } + } + + if exprEnd > -1 { + fmt.Fprintf(&formatOut, "{%d}", len(results)) + results = append(results, strings.TrimSpace(in[exprStart:pos+exprEnd])) + pos += exprEnd + 2 + exprStart = -1 + } else if strStart > -1 { + pos += strStart + 1 + } else { + panic("unclosed expression.") + } + } else { + exprStart = strings.Index(in[pos:], "${{") + if exprStart != -1 { + formatOut.WriteString(escapeFormatString(in[pos : pos+exprStart])) + exprStart = pos + exprStart + 3 + pos = exprStart + } else { + formatOut.WriteString(escapeFormatString(in[pos:])) + pos = len(in) + } + } + } + + if len(results) == 1 && formatOut.String() == "{0}" && !forceFormat { + return in, nil + } + + out := fmt.Sprintf("format('%s', %s)", strings.ReplaceAll(formatOut.String(), "'", "''"), strings.Join(results, ", ")) + return out, nil +} diff --git a/package/gitea/source/modules/actions/jobparser/interpeter.go b/package/gitea/source/modules/actions/jobparser/interpeter.go new file mode 100644 index 00000000..512b6f02 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/interpeter.go @@ -0,0 +1,87 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jobparser + +import ( + "github.com/nektos/act/pkg/exprparser" + "github.com/nektos/act/pkg/model" + "go.yaml.in/yaml/v4" +) + +// NewInterpeter returns an interpeter used in the server, +// need github, needs, strategy, matrix, inputs context only, +// see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability +func NewInterpeter( + jobID string, + job *model.Job, + matrix map[string]any, + gitCtx *model.GithubContext, + results map[string]*JobResult, + vars map[string]string, + inputs map[string]any, +) exprparser.Interpreter { + strategy := make(map[string]any) + if job.Strategy != nil { + strategy["fail-fast"] = job.Strategy.FailFast + strategy["max-parallel"] = job.Strategy.MaxParallel + } + + run := &model.Run{ + Workflow: &model.Workflow{ + Jobs: map[string]*model.Job{}, + }, + JobID: jobID, + } + for id, result := range results { + need := yaml.Node{} + _ = need.Encode(result.Needs) + run.Workflow.Jobs[id] = &model.Job{ + RawNeeds: need, + Result: result.Result, + Outputs: result.Outputs, + } + } + + jobs := run.Workflow.Jobs + jobNeeds := run.Job().Needs() + + using := map[string]exprparser.Needs{} + for _, need := range jobNeeds { + if v, ok := jobs[need]; ok { + using[need] = exprparser.Needs{ + Outputs: v.Outputs, + Result: v.Result, + } + } + } + + ee := &exprparser.EvaluationEnvironment{ + Github: gitCtx, + Env: nil, // no need + Job: nil, // no need + Steps: nil, // no need + Runner: nil, // no need + Secrets: nil, // no need + Strategy: strategy, + Matrix: matrix, + Needs: using, + Inputs: inputs, + Vars: vars, + } + + config := exprparser.Config{ + Run: run, + WorkingDir: "", // WorkingDir is used for the function hashFiles, but it's not needed in the server + Context: "job", + } + + return exprparser.NewInterpeter(ee, config) +} + +// JobResult is the minimum requirement of job results for Interpeter +type JobResult struct { + Needs []string + Result string + Outputs map[string]string +} diff --git a/package/gitea/source/modules/actions/jobparser/jobparser.go b/package/gitea/source/modules/actions/jobparser/jobparser.go new file mode 100644 index 00000000..e3a99b95 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/jobparser.go @@ -0,0 +1,180 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jobparser + +import ( + "bytes" + "fmt" + "sort" + "strings" + + "github.com/nektos/act/pkg/exprparser" + "github.com/nektos/act/pkg/model" + "go.yaml.in/yaml/v4" +) + +func Parse(content []byte, options ...ParseOption) ([]*SingleWorkflow, error) { + origin, err := model.ReadWorkflow(bytes.NewReader(content)) + if err != nil { + return nil, fmt.Errorf("model.ReadWorkflow: %w", err) + } + + workflow := &SingleWorkflow{} + if err := yaml.Unmarshal(content, workflow); err != nil { + return nil, fmt.Errorf("yaml.Unmarshal: %w", err) + } + + pc := &parseContext{} + for _, o := range options { + o(pc) + } + results := map[string]*JobResult{} + for id, job := range origin.Jobs { + if job == nil { + return nil, fmt.Errorf("needed job not found: %q", id) + } + results[id] = &JobResult{ + Needs: job.Needs(), + Result: pc.jobResults[id], + Outputs: nil, // not supported yet + } + } + + var ret []*SingleWorkflow + ids, jobs, err := workflow.jobs() + if err != nil { + return nil, fmt.Errorf("invalid jobs: %w", err) + } + + evaluator := NewExpressionEvaluator(exprparser.NewInterpeter(&exprparser.EvaluationEnvironment{Github: pc.gitContext, Vars: pc.vars, Inputs: pc.inputs}, exprparser.Config{})) + workflow.RunName = evaluator.Interpolate(workflow.RunName) + + for i, id := range ids { + job := jobs[i] + matricxes, err := getMatrixes(origin.GetJob(id)) + if err != nil { + return nil, fmt.Errorf("getMatrixes: %w", err) + } + for _, matrix := range matricxes { + job := job.Clone() + if job.Name == "" { + job.Name = id + } + job.Strategy.RawMatrix = encodeMatrix(matrix) + evaluator := NewExpressionEvaluator(NewInterpeter(id, origin.GetJob(id), matrix, pc.gitContext, results, pc.vars, pc.inputs)) + job.Name = nameWithMatrix(job.Name, matrix, evaluator) + runsOn := origin.GetJob(id).RunsOn() + for i, v := range runsOn { + runsOn[i] = evaluator.Interpolate(v) + } + job.RawRunsOn = encodeRunsOn(runsOn) + swf := &SingleWorkflow{ + Name: workflow.Name, + RawOn: workflow.RawOn, + Env: workflow.Env, + Defaults: workflow.Defaults, + RawPermissions: workflow.RawPermissions, + RunName: workflow.RunName, + } + if err := swf.SetJob(id, job); err != nil { + return nil, fmt.Errorf("SetJob: %w", err) + } + ret = append(ret, swf) + } + } + return ret, nil +} + +func WithJobResults(results map[string]string) ParseOption { + return func(c *parseContext) { + c.jobResults = results + } +} + +func WithGitContext(context *model.GithubContext) ParseOption { + return func(c *parseContext) { + c.gitContext = context + } +} + +func WithVars(vars map[string]string) ParseOption { + return func(c *parseContext) { + c.vars = vars + } +} + +func WithInputs(inputs map[string]any) ParseOption { + return func(c *parseContext) { + c.inputs = inputs + } +} + +type parseContext struct { + jobResults map[string]string + gitContext *model.GithubContext + vars map[string]string + inputs map[string]any +} + +type ParseOption func(c *parseContext) + +func getMatrixes(job *model.Job) ([]map[string]any, error) { + ret, err := job.GetMatrixes() + if err != nil { + return nil, fmt.Errorf("GetMatrixes: %w", err) + } + sort.Slice(ret, func(i, j int) bool { + return matrixName(ret[i]) < matrixName(ret[j]) + }) + return ret, nil +} + +func encodeMatrix(matrix map[string]any) yaml.Node { + if len(matrix) == 0 { + return yaml.Node{} + } + value := map[string][]any{} + for k, v := range matrix { + value[k] = []any{v} + } + node := yaml.Node{} + _ = node.Encode(value) + return node +} + +func encodeRunsOn(runsOn []string) yaml.Node { + node := yaml.Node{} + if len(runsOn) == 1 { + _ = node.Encode(runsOn[0]) + } else { + _ = node.Encode(runsOn) + } + return node +} + +func nameWithMatrix(name string, m map[string]any, evaluator *ExpressionEvaluator) string { + if len(m) == 0 { + return name + } + + if !strings.Contains(name, "${{") || !strings.Contains(name, "}}") { + return name + " " + matrixName(m) + } + + return evaluator.Interpolate(name) +} + +func matrixName(m map[string]any) string { + ks := make([]string, 0, len(m)) + for k := range m { + ks = append(ks, k) + } + sort.Strings(ks) + vs := make([]string, 0, len(m)) + for _, v := range ks { + vs = append(vs, fmt.Sprint(m[v])) + } + + return fmt.Sprintf("(%s)", strings.Join(vs, ", ")) +} diff --git a/package/gitea/source/modules/actions/jobparser/jobparser_test.go b/package/gitea/source/modules/actions/jobparser/jobparser_test.go new file mode 100644 index 00000000..e74f0644 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/jobparser_test.go @@ -0,0 +1,104 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jobparser + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v4" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + options []ParseOption + wantErr bool + }{ + { + name: "multiple_jobs", + options: nil, + wantErr: false, + }, + { + name: "multiple_matrix", + options: nil, + wantErr: false, + }, + { + name: "has_needs", + options: nil, + wantErr: false, + }, + { + name: "has_with", + options: nil, + wantErr: false, + }, + { + name: "has_secrets", + options: nil, + wantErr: false, + }, + { + name: "empty_step", + options: nil, + wantErr: false, + }, + { + name: "job_name_with_matrix", + options: nil, + wantErr: false, + }, + { + name: "prefixed_newline", + options: nil, + wantErr: false, + }, + } + invalidFileTests := []struct { + name string + }{ + {name: "null_job_implicit"}, + {name: "null_job_explicit"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + content := ReadTestdata(t, tt.name+".in.yaml") + want := ReadTestdata(t, tt.name+".out.yaml") + got, err := Parse(content, tt.options...) + if tt.wantErr { + require.Error(t, err) + } + require.NoError(t, err) + + builder := &strings.Builder{} + for _, v := range got { + if builder.Len() > 0 { + builder.WriteString("---\n") + } + encoder := yaml.NewEncoder(builder) + encoder.SetIndent(2) + require.NoError(t, encoder.Encode(v)) + id, job := v.Job() + assert.NotEmpty(t, id) + assert.NotNil(t, job) + } + assert.Equal(t, string(want), builder.String()) + }) + } + + for _, tt := range invalidFileTests { + t.Run(tt.name, func(t *testing.T) { + content := ReadTestdata(t, tt.name+".in.yaml") + require.NotPanics(t, func() { + _, err := Parse(content) + require.Error(t, err) + }) + }) + } +} diff --git a/package/gitea/source/modules/actions/jobparser/model.go b/package/gitea/source/modules/actions/jobparser/model.go new file mode 100644 index 00000000..7132c278 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/model.go @@ -0,0 +1,497 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jobparser + +import ( + "bytes" + "errors" + "fmt" + + "github.com/nektos/act/pkg/model" + "go.yaml.in/yaml/v4" +) + +// SingleWorkflow is a workflow with single job and single matrix +type SingleWorkflow struct { + Name string `yaml:"name,omitempty"` + RawOn yaml.Node `yaml:"on,omitempty"` + Env map[string]string `yaml:"env,omitempty"` + RawJobs yaml.Node `yaml:"jobs,omitempty"` + Defaults Defaults `yaml:"defaults,omitempty"` + RawPermissions yaml.Node `yaml:"permissions,omitempty"` + RunName string `yaml:"run-name,omitempty"` +} + +func (w *SingleWorkflow) Job() (string, *Job) { + ids, jobs, _ := w.jobs() + if len(ids) >= 1 { + return ids[0], jobs[0] + } + return "", nil +} + +func (w *SingleWorkflow) jobs() ([]string, []*Job, error) { + ids, jobs, err := parseMappingNode[*Job](&w.RawJobs) + if err != nil { + return nil, nil, err + } + + for _, job := range jobs { + steps := make([]*Step, 0, len(job.Steps)) + for _, s := range job.Steps { + if s != nil { + steps = append(steps, s) + } + } + job.Steps = steps + } + + return ids, jobs, nil +} + +func (w *SingleWorkflow) SetJob(id string, job *Job) error { + m := map[string]*Job{ + id: job, + } + var buf bytes.Buffer + encoder := yaml.NewEncoder(&buf) + encoder.SetIndent(2) + if err := encoder.Encode(m); err != nil { + return err + } + encoder.Close() + + node := yaml.Node{} + if err := yaml.Unmarshal(buf.Bytes(), &node); err != nil { + return err + } + if len(node.Content) != 1 || node.Content[0].Kind != yaml.MappingNode { + return fmt.Errorf("can not set job: %s", buf.String()) + } + w.RawJobs = *node.Content[0] + return nil +} + +func (w *SingleWorkflow) Marshal() ([]byte, error) { + return yaml.Marshal(w) +} + +type Job struct { + Name string `yaml:"name,omitempty"` + RawNeeds yaml.Node `yaml:"needs,omitempty"` + RawRunsOn yaml.Node `yaml:"runs-on,omitempty"` + Env yaml.Node `yaml:"env,omitempty"` + If yaml.Node `yaml:"if,omitempty"` + Steps []*Step `yaml:"steps,omitempty"` + TimeoutMinutes string `yaml:"timeout-minutes,omitempty"` + Services map[string]*ContainerSpec `yaml:"services,omitempty"` + Strategy Strategy `yaml:"strategy,omitempty"` + RawContainer yaml.Node `yaml:"container,omitempty"` + Defaults Defaults `yaml:"defaults,omitempty"` + Outputs map[string]string `yaml:"outputs,omitempty"` + Uses string `yaml:"uses,omitempty"` + With map[string]any `yaml:"with,omitempty"` + RawSecrets yaml.Node `yaml:"secrets,omitempty"` + RawConcurrency *model.RawConcurrency `yaml:"concurrency,omitempty"` + RawPermissions yaml.Node `yaml:"permissions,omitempty"` +} + +func (j *Job) Clone() *Job { + if j == nil { + return nil + } + return &Job{ + Name: j.Name, + RawNeeds: j.RawNeeds, + RawRunsOn: j.RawRunsOn, + Env: j.Env, + If: j.If, + Steps: j.Steps, + TimeoutMinutes: j.TimeoutMinutes, + Services: j.Services, + Strategy: j.Strategy, + RawContainer: j.RawContainer, + Defaults: j.Defaults, + Outputs: j.Outputs, + Uses: j.Uses, + With: j.With, + RawSecrets: j.RawSecrets, + RawConcurrency: j.RawConcurrency, + RawPermissions: j.RawPermissions, + } +} + +func (j *Job) Needs() []string { + return (&model.Job{RawNeeds: j.RawNeeds}).Needs() +} + +func (j *Job) EraseNeeds() *Job { + j.RawNeeds = yaml.Node{} + return j +} + +func (j *Job) RunsOn() []string { + return (&model.Job{RawRunsOn: j.RawRunsOn}).RunsOn() +} + +type Step struct { + ID string `yaml:"id,omitempty"` + If yaml.Node `yaml:"if,omitempty"` + Name string `yaml:"name,omitempty"` + Uses string `yaml:"uses,omitempty"` + Run string `yaml:"run,omitempty"` + WorkingDirectory string `yaml:"working-directory,omitempty"` + Shell string `yaml:"shell,omitempty"` + Env yaml.Node `yaml:"env,omitempty"` + With map[string]string `yaml:"with,omitempty"` + ContinueOnError bool `yaml:"continue-on-error,omitempty"` + TimeoutMinutes string `yaml:"timeout-minutes,omitempty"` +} + +// String gets the name of step +func (s *Step) String() string { + if s == nil { + return "" + } + return (&model.Step{ + ID: s.ID, + Name: s.Name, + Uses: s.Uses, + Run: s.Run, + }).String() +} + +type ContainerSpec struct { + Image string `yaml:"image,omitempty"` + Env map[string]string `yaml:"env,omitempty"` + Ports []string `yaml:"ports,omitempty"` + Volumes []string `yaml:"volumes,omitempty"` + Options string `yaml:"options,omitempty"` + Credentials map[string]string `yaml:"credentials,omitempty"` + Cmd []string `yaml:"cmd,omitempty"` +} + +type Strategy struct { + FailFastString string `yaml:"fail-fast,omitempty"` + MaxParallelString string `yaml:"max-parallel,omitempty"` + RawMatrix yaml.Node `yaml:"matrix,omitempty"` +} + +type Defaults struct { + Run RunDefaults `yaml:"run,omitempty"` +} + +type RunDefaults struct { + Shell string `yaml:"shell,omitempty"` + WorkingDirectory string `yaml:"working-directory,omitempty"` +} + +type WorkflowDispatchInput struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Required bool `yaml:"required"` + Default string `yaml:"default"` + Type string `yaml:"type"` + Options []string `yaml:"options"` +} + +type Event struct { + Name string + acts map[string][]string + schedules []map[string]string + inputs []WorkflowDispatchInput +} + +func (evt *Event) IsSchedule() bool { + return evt.schedules != nil +} + +func (evt *Event) Acts() map[string][]string { + return evt.acts +} + +func (evt *Event) Schedules() []map[string]string { + return evt.schedules +} + +func (evt *Event) Inputs() []WorkflowDispatchInput { + return evt.inputs +} + +func ReadWorkflowRawConcurrency(content []byte) (*model.RawConcurrency, error) { + w := new(model.Workflow) + err := yaml.NewDecoder(bytes.NewReader(content)).Decode(w) + return w.RawConcurrency, err +} + +func EvaluateConcurrency(rc *model.RawConcurrency, jobID string, job *Job, gitCtx map[string]any, results map[string]*JobResult, vars map[string]string, inputs map[string]any) (string, bool, error) { + actJob := &model.Job{} + if job != nil { + actJob.Strategy = &model.Strategy{ + FailFastString: job.Strategy.FailFastString, + MaxParallelString: job.Strategy.MaxParallelString, + RawMatrix: job.Strategy.RawMatrix, + } + actJob.Strategy.FailFast = actJob.Strategy.GetFailFast() + actJob.Strategy.MaxParallel = actJob.Strategy.GetMaxParallel() + } + + matrix := make(map[string]any) + matrixes, err := actJob.GetMatrixes() + if err != nil { + return "", false, err + } + if len(matrixes) > 0 { + matrix = matrixes[0] + } + + evaluator := NewExpressionEvaluator(NewInterpeter(jobID, actJob, matrix, toGitContext(gitCtx), results, vars, inputs)) + var node yaml.Node + if err := node.Encode(rc); err != nil { + return "", false, fmt.Errorf("failed to encode concurrency: %w", err) + } + if err := evaluator.EvaluateYamlNode(&node); err != nil { + return "", false, fmt.Errorf("failed to evaluate concurrency: %w", err) + } + var evaluated model.RawConcurrency + if err := node.Decode(&evaluated); err != nil { + return "", false, fmt.Errorf("failed to unmarshal evaluated concurrency: %w", err) + } + if evaluated.RawExpression != "" { + return evaluated.RawExpression, false, nil + } + return evaluated.Group, evaluated.CancelInProgress == "true", nil +} + +func toGitContext(input map[string]any) *model.GithubContext { + gitContext := &model.GithubContext{ + EventPath: asString(input["event_path"]), + Workflow: asString(input["workflow"]), + RunID: asString(input["run_id"]), + RunNumber: asString(input["run_number"]), + Actor: asString(input["actor"]), + Repository: asString(input["repository"]), + EventName: asString(input["event_name"]), + Sha: asString(input["sha"]), + Ref: asString(input["ref"]), + RefName: asString(input["ref_name"]), + RefType: asString(input["ref_type"]), + HeadRef: asString(input["head_ref"]), + BaseRef: asString(input["base_ref"]), + Token: asString(input["token"]), + Workspace: asString(input["workspace"]), + Action: asString(input["action"]), + ActionPath: asString(input["action_path"]), + ActionRef: asString(input["action_ref"]), + ActionRepository: asString(input["action_repository"]), + Job: asString(input["job"]), + RepositoryOwner: asString(input["repository_owner"]), + RetentionDays: asString(input["retention_days"]), + } + + event, ok := input["event"].(map[string]any) + if ok { + gitContext.Event = event + } + + return gitContext +} + +func ParseRawOn(rawOn *yaml.Node) ([]*Event, error) { + switch rawOn.Kind { + case yaml.ScalarNode: + var val string + err := rawOn.Decode(&val) + if err != nil { + return nil, err + } + return []*Event{ + {Name: val}, + }, nil + case yaml.SequenceNode: + var val []any + err := rawOn.Decode(&val) + if err != nil { + return nil, err + } + res := make([]*Event, 0, len(val)) + for _, v := range val { + switch t := v.(type) { + case string: + res = append(res, &Event{Name: t}) + default: + return nil, fmt.Errorf("invalid type %T", t) + } + } + return res, nil + case yaml.MappingNode: + events, triggers, err := parseMappingNode[yaml.Node](rawOn) + if err != nil { + return nil, err + } + res := make([]*Event, 0, len(events)) + for i, k := range events { + v := triggers[i] + switch v.Kind { + case yaml.ScalarNode: + res = append(res, &Event{ + Name: k, + }) + case yaml.SequenceNode: + var t []any + err := v.Decode(&t) + if err != nil { + return nil, err + } + schedules := make([]map[string]string, len(t)) + if k == "schedule" { + for i, tt := range t { + vv, ok := tt.(map[string]any) + if !ok { + return nil, fmt.Errorf("unknown on type(schedule): %#v", v) + } + schedules[i] = make(map[string]string, len(vv)) + for k, vvv := range vv { + var ok bool + if schedules[i][k], ok = vvv.(string); !ok { + return nil, fmt.Errorf("unknown on type(schedule): %#v", v) + } + } + } + } + + if len(schedules) == 0 { + schedules = nil + } + res = append(res, &Event{ + Name: k, + schedules: schedules, + }) + case yaml.MappingNode: + acts := make(map[string][]string, len(v.Content)/2) + var inputs []WorkflowDispatchInput + expectedKey := true + var act string + for _, content := range v.Content { + if expectedKey { + if content.Kind != yaml.ScalarNode { + return nil, fmt.Errorf("key type not string: %#v", content) + } + act = "" + err := content.Decode(&act) + if err != nil { + return nil, err + } + } else { + switch content.Kind { + case yaml.SequenceNode: + var t []string + err := content.Decode(&t) + if err != nil { + return nil, err + } + acts[act] = t + case yaml.ScalarNode: + var t string + err := content.Decode(&t) + if err != nil { + return nil, err + } + acts[act] = []string{t} + case yaml.MappingNode: + if k != "workflow_dispatch" || act != "inputs" { + return nil, fmt.Errorf("map should only for workflow_dispatch but %s: %#v", act, content) + } + + var key string + for i, vv := range content.Content { + if i%2 == 0 { + if vv.Kind != yaml.ScalarNode { + return nil, fmt.Errorf("key type not string: %#v", vv) + } + key = "" + if err := vv.Decode(&key); err != nil { + return nil, err + } + } else { + if vv.Kind != yaml.MappingNode { + return nil, fmt.Errorf("key type not map(%s): %#v", key, vv) + } + + input := WorkflowDispatchInput{} + if err := vv.Decode(&input); err != nil { + return nil, err + } + input.Name = key + inputs = append(inputs, input) + } + } + default: + return nil, fmt.Errorf("unknown on type: %#v", content) + } + } + expectedKey = !expectedKey + } + if len(inputs) == 0 { + inputs = nil + } + if len(acts) == 0 { + acts = nil + } + res = append(res, &Event{ + Name: k, + acts: acts, + inputs: inputs, + }) + default: + return nil, fmt.Errorf("unknown on type: %v", v.Kind) + } + } + return res, nil + default: + return nil, fmt.Errorf("unknown on type: %v", rawOn.Kind) + } +} + +// parseMappingNode parse a mapping node and preserve order. +func parseMappingNode[T any](node *yaml.Node) ([]string, []T, error) { + if node.Kind != yaml.MappingNode { + return nil, nil, errors.New("input node is not a mapping node") + } + + var scalars []string + var datas []T + expectKey := true + for _, item := range node.Content { + if expectKey { + if item.Kind != yaml.ScalarNode { + return nil, nil, fmt.Errorf("not a valid scalar node: %v", item.Value) + } + scalars = append(scalars, item.Value) + expectKey = false + } else { + var val T + if err := item.Decode(&val); err != nil { + return nil, nil, err + } + datas = append(datas, val) + expectKey = true + } + } + + if len(scalars) != len(datas) { + return nil, nil, fmt.Errorf("invalid definition of on: %v", node.Value) + } + + return scalars, datas, nil +} + +func asString(v any) string { + if v == nil { + return "" + } else if s, ok := v.(string); ok { + return s + } + return "" +} diff --git a/package/gitea/source/modules/actions/jobparser/model_test.go b/package/gitea/source/modules/actions/jobparser/model_test.go new file mode 100644 index 00000000..d0e82041 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/model_test.go @@ -0,0 +1,373 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jobparser + +import ( + "strings" + "testing" + + "github.com/nektos/act/pkg/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.yaml.in/yaml/v4" +) + +func TestParseRawOn(t *testing.T) { + kases := []struct { + input string + result []*Event + }{ + { + input: "on: issue_comment", + result: []*Event{ + { + Name: "issue_comment", + }, + }, + }, + { + input: "on:\n push", + result: []*Event{ + { + Name: "push", + }, + }, + }, + + { + input: "on:\n - push\n - pull_request", + result: []*Event{ + { + Name: "push", + }, + { + Name: "pull_request", + }, + }, + }, + { + input: "on:\n push:\n branches:\n - master", + result: []*Event{ + { + Name: "push", + acts: map[string][]string{ + "branches": { + "master", + }, + }, + }, + }, + }, + { + input: "on:\n push:\n branches: main", + result: []*Event{ + { + Name: "push", + acts: map[string][]string{ + "branches": { + "main", + }, + }, + }, + }, + }, + { + input: "on:\n branch_protection_rule:\n types: [created, deleted]", + result: []*Event{ + { + Name: "branch_protection_rule", + acts: map[string][]string{ + "types": { + "created", + "deleted", + }, + }, + }, + }, + }, + { + input: "on:\n project:\n types: [created, deleted]\n milestone:\n types: [opened, deleted]", + result: []*Event{ + { + Name: "project", + acts: map[string][]string{ + "types": { + "created", + "deleted", + }, + }, + }, + { + Name: "milestone", + acts: map[string][]string{ + "types": { + "opened", + "deleted", + }, + }, + }, + }, + }, + { + input: "on:\n pull_request:\n types:\n - opened\n branches:\n - 'releases/**'", + result: []*Event{ + { + Name: "pull_request", + acts: map[string][]string{ + "types": { + "opened", + }, + "branches": { + "releases/**", + }, + }, + }, + }, + }, + { + input: "on:\n push:\n branches:\n - main\n pull_request:\n types:\n - opened\n branches:\n - '**'", + result: []*Event{ + { + Name: "push", + acts: map[string][]string{ + "branches": { + "main", + }, + }, + }, + { + Name: "pull_request", + acts: map[string][]string{ + "types": { + "opened", + }, + "branches": { + "**", + }, + }, + }, + }, + }, + { + input: "on:\n push:\n branches:\n - 'main'\n - 'releases/**'", + result: []*Event{ + { + Name: "push", + acts: map[string][]string{ + "branches": { + "main", + "releases/**", + }, + }, + }, + }, + }, + { + input: "on:\n push:\n tags:\n - v1.**", + result: []*Event{ + { + Name: "push", + acts: map[string][]string{ + "tags": { + "v1.**", + }, + }, + }, + }, + }, + { + input: "on: [pull_request, workflow_dispatch]", + result: []*Event{ + { + Name: "pull_request", + }, + { + Name: "workflow_dispatch", + }, + }, + }, + { + input: "on:\n schedule:\n - cron: '20 6 * * *'", + result: []*Event{ + { + Name: "schedule", + schedules: []map[string]string{ + { + "cron": "20 6 * * *", + }, + }, + }, + }, + }, + { + input: `on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + type: choice + options: + - info + - warning + - debug + tags: + description: 'Test scenario tags' + required: false + type: boolean + environment: + description: 'Environment to run tests against' + type: environment + required: true + push: +`, + result: []*Event{ + { + Name: "workflow_dispatch", + inputs: []WorkflowDispatchInput{ + { + Name: "logLevel", + Description: "Log level", + Required: true, + Default: "warning", + Type: "choice", + Options: []string{"info", "warning", "debug"}, + }, + { + Name: "tags", + Description: "Test scenario tags", + Required: false, + Type: "boolean", + }, + { + Name: "environment", + Description: "Environment to run tests against", + Type: "environment", + Required: true, + }, + }, + }, + { + Name: "push", + }, + }, + }, + } + for _, kase := range kases { + t.Run(kase.input, func(t *testing.T) { + origin, err := model.ReadWorkflow(strings.NewReader(kase.input)) + assert.NoError(t, err) + + events, err := ParseRawOn(&origin.RawOn) + assert.NoError(t, err) + assert.Equal(t, kase.result, events, events) + }) + } +} + +func TestSingleWorkflow_SetJob(t *testing.T) { + t.Run("erase needs", func(t *testing.T) { + content := ReadTestdata(t, "erase_needs.in.yaml") + want := ReadTestdata(t, "erase_needs.out.yaml") + swf, err := Parse(content) + require.NoError(t, err) + builder := &strings.Builder{} + for _, v := range swf { + id, job := v.Job() + require.NoError(t, v.SetJob(id, job.EraseNeeds())) + + if builder.Len() > 0 { + builder.WriteString("---\n") + } + encoder := yaml.NewEncoder(builder) + encoder.SetIndent(2) + require.NoError(t, encoder.Encode(v)) + } + assert.Equal(t, string(want), builder.String()) + }) +} + +func TestParseMappingNode(t *testing.T) { + tests := []struct { + input string + scalars []string + datas []any + }{ + { + input: "on:\n push:\n branches:\n - master", + scalars: []string{"push"}, + datas: []any{ + map[string]any{ + "branches": []any{"master"}, + }, + }, + }, + { + input: "on:\n branch_protection_rule:\n types: [created, deleted]", + scalars: []string{"branch_protection_rule"}, + datas: []any{ + map[string]any{ + "types": []any{"created", "deleted"}, + }, + }, + }, + { + input: "on:\n project:\n types: [created, deleted]\n milestone:\n types: [opened, deleted]", + scalars: []string{"project", "milestone"}, + datas: []any{ + map[string]any{ + "types": []any{"created", "deleted"}, + }, + map[string]any{ + "types": []any{"opened", "deleted"}, + }, + }, + }, + { + input: "on:\n pull_request:\n types:\n - opened\n branches:\n - 'releases/**'", + scalars: []string{"pull_request"}, + datas: []any{ + map[string]any{ + "types": []any{"opened"}, + "branches": []any{"releases/**"}, + }, + }, + }, + { + input: "on:\n push:\n branches:\n - main\n pull_request:\n types:\n - opened\n branches:\n - '**'", + scalars: []string{"push", "pull_request"}, + datas: []any{ + map[string]any{ + "branches": []any{"main"}, + }, + map[string]any{ + "types": []any{"opened"}, + "branches": []any{"**"}, + }, + }, + }, + { + input: "on:\n schedule:\n - cron: '20 6 * * *'", + scalars: []string{"schedule"}, + datas: []any{ + []any{map[string]any{ + "cron": "20 6 * * *", + }}, + }, + }, + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + workflow, err := model.ReadWorkflow(strings.NewReader(test.input)) + assert.NoError(t, err) + + scalars, datas, err := parseMappingNode[any](&workflow.RawOn) + assert.NoError(t, err) + assert.Equal(t, test.scalars, scalars, scalars) + assert.Equal(t, test.datas, datas, datas) + }) + } +} diff --git a/package/gitea/source/modules/actions/jobparser/testdata/empty_step.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/empty_step.in.yaml new file mode 100644 index 00000000..737ac0b2 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/empty_step.in.yaml @@ -0,0 +1,8 @@ +name: test +jobs: + job1: + name: job1 + runs-on: linux + steps: + - run: echo job-1 + - diff --git a/package/gitea/source/modules/actions/jobparser/testdata/empty_step.out.yaml b/package/gitea/source/modules/actions/jobparser/testdata/empty_step.out.yaml new file mode 100644 index 00000000..06828e0e --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/empty_step.out.yaml @@ -0,0 +1,7 @@ +name: test +jobs: + job1: + name: job1 + runs-on: linux + steps: + - run: echo job-1 diff --git a/package/gitea/source/modules/actions/jobparser/testdata/erase_needs.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/erase_needs.in.yaml new file mode 100644 index 00000000..a7d1f9b6 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/erase_needs.in.yaml @@ -0,0 +1,16 @@ +name: test +jobs: + job1: + runs-on: linux + steps: + - run: uname -a + job2: + runs-on: linux + steps: + - run: uname -a + needs: job1 + job3: + runs-on: linux + steps: + - run: uname -a + needs: [job1, job2] diff --git a/package/gitea/source/modules/actions/jobparser/testdata/erase_needs.out.yaml b/package/gitea/source/modules/actions/jobparser/testdata/erase_needs.out.yaml new file mode 100644 index 00000000..959960d6 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/erase_needs.out.yaml @@ -0,0 +1,23 @@ +name: test +jobs: + job1: + name: job1 + runs-on: linux + steps: + - run: uname -a +--- +name: test +jobs: + job2: + name: job2 + runs-on: linux + steps: + - run: uname -a +--- +name: test +jobs: + job3: + name: job3 + runs-on: linux + steps: + - run: uname -a diff --git a/package/gitea/source/modules/actions/jobparser/testdata/has_needs.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/has_needs.in.yaml new file mode 100644 index 00000000..a7d1f9b6 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/has_needs.in.yaml @@ -0,0 +1,16 @@ +name: test +jobs: + job1: + runs-on: linux + steps: + - run: uname -a + job2: + runs-on: linux + steps: + - run: uname -a + needs: job1 + job3: + runs-on: linux + steps: + - run: uname -a + needs: [job1, job2] diff --git a/package/gitea/source/modules/actions/jobparser/testdata/has_needs.out.yaml b/package/gitea/source/modules/actions/jobparser/testdata/has_needs.out.yaml new file mode 100644 index 00000000..a544aa22 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/has_needs.out.yaml @@ -0,0 +1,25 @@ +name: test +jobs: + job1: + name: job1 + runs-on: linux + steps: + - run: uname -a +--- +name: test +jobs: + job2: + name: job2 + needs: job1 + runs-on: linux + steps: + - run: uname -a +--- +name: test +jobs: + job3: + name: job3 + needs: [job1, job2] + runs-on: linux + steps: + - run: uname -a diff --git a/package/gitea/source/modules/actions/jobparser/testdata/has_secrets.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/has_secrets.in.yaml new file mode 100644 index 00000000..64b9f69f --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/has_secrets.in.yaml @@ -0,0 +1,14 @@ +name: test +jobs: + job1: + name: job1 + runs-on: linux + uses: .gitea/workflows/build.yml + secrets: + secret: hideme + + job2: + name: job2 + runs-on: linux + uses: .gitea/workflows/build.yml + secrets: inherit diff --git a/package/gitea/source/modules/actions/jobparser/testdata/has_secrets.out.yaml b/package/gitea/source/modules/actions/jobparser/testdata/has_secrets.out.yaml new file mode 100644 index 00000000..23dfb803 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/has_secrets.out.yaml @@ -0,0 +1,16 @@ +name: test +jobs: + job1: + name: job1 + runs-on: linux + uses: .gitea/workflows/build.yml + secrets: + secret: hideme +--- +name: test +jobs: + job2: + name: job2 + runs-on: linux + uses: .gitea/workflows/build.yml + secrets: inherit diff --git a/package/gitea/source/modules/actions/jobparser/testdata/has_with.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/has_with.in.yaml new file mode 100644 index 00000000..4e3dc745 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/has_with.in.yaml @@ -0,0 +1,15 @@ +name: test +jobs: + job1: + name: job1 + runs-on: linux + uses: .gitea/workflows/build.yml + with: + package: service + + job2: + name: job2 + runs-on: linux + uses: .gitea/workflows/build.yml + with: + package: module diff --git a/package/gitea/source/modules/actions/jobparser/testdata/has_with.out.yaml b/package/gitea/source/modules/actions/jobparser/testdata/has_with.out.yaml new file mode 100644 index 00000000..de79b803 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/has_with.out.yaml @@ -0,0 +1,17 @@ +name: test +jobs: + job1: + name: job1 + runs-on: linux + uses: .gitea/workflows/build.yml + with: + package: service +--- +name: test +jobs: + job2: + name: job2 + runs-on: linux + uses: .gitea/workflows/build.yml + with: + package: module diff --git a/package/gitea/source/modules/actions/jobparser/testdata/job_name_with_matrix.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/job_name_with_matrix.in.yaml new file mode 100644 index 00000000..72c700da --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/job_name_with_matrix.in.yaml @@ -0,0 +1,14 @@ +name: test +jobs: + job1: + strategy: + matrix: + os: [ubuntu-22.04, ubuntu-20.04] + version: [1.17, 1.18, 1.19] + runs-on: ${{ matrix.os }} + name: test_version_${{ matrix.version }}_on_${{ matrix.os }} + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version diff --git a/package/gitea/source/modules/actions/jobparser/testdata/job_name_with_matrix.out.yaml b/package/gitea/source/modules/actions/jobparser/testdata/job_name_with_matrix.out.yaml new file mode 100644 index 00000000..081e8d4a --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/job_name_with_matrix.out.yaml @@ -0,0 +1,101 @@ +name: test +jobs: + job1: + name: test_version_1.17_on_ubuntu-20.04 + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-20.04 + version: + - 1.17 +--- +name: test +jobs: + job1: + name: test_version_1.18_on_ubuntu-20.04 + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-20.04 + version: + - 1.18 +--- +name: test +jobs: + job1: + name: test_version_1.19_on_ubuntu-20.04 + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-20.04 + version: + - 1.19 +--- +name: test +jobs: + job1: + name: test_version_1.17_on_ubuntu-22.04 + runs-on: ubuntu-22.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-22.04 + version: + - 1.17 +--- +name: test +jobs: + job1: + name: test_version_1.18_on_ubuntu-22.04 + runs-on: ubuntu-22.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-22.04 + version: + - 1.18 +--- +name: test +jobs: + job1: + name: test_version_1.19_on_ubuntu-22.04 + runs-on: ubuntu-22.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-22.04 + version: + - 1.19 diff --git a/package/gitea/source/modules/actions/jobparser/testdata/multiple_jobs.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/multiple_jobs.in.yaml new file mode 100644 index 00000000..266ede84 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/multiple_jobs.in.yaml @@ -0,0 +1,22 @@ +name: test +jobs: + zzz: + runs-on: linux + steps: + - run: echo zzz + job1: + runs-on: linux + steps: + - run: uname -a && go version + job2: + runs-on: linux + steps: + - run: uname -a && go version + job3: + runs-on: linux + steps: + - run: uname -a && go version + aaa: + runs-on: linux + steps: + - run: uname -a && go version diff --git a/package/gitea/source/modules/actions/jobparser/testdata/multiple_jobs.out.yaml b/package/gitea/source/modules/actions/jobparser/testdata/multiple_jobs.out.yaml new file mode 100644 index 00000000..ea223500 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/multiple_jobs.out.yaml @@ -0,0 +1,39 @@ +name: test +jobs: + zzz: + name: zzz + runs-on: linux + steps: + - run: echo zzz +--- +name: test +jobs: + job1: + name: job1 + runs-on: linux + steps: + - run: uname -a && go version +--- +name: test +jobs: + job2: + name: job2 + runs-on: linux + steps: + - run: uname -a && go version +--- +name: test +jobs: + job3: + name: job3 + runs-on: linux + steps: + - run: uname -a && go version +--- +name: test +jobs: + aaa: + name: aaa + runs-on: linux + steps: + - run: uname -a && go version diff --git a/package/gitea/source/modules/actions/jobparser/testdata/multiple_matrix.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/multiple_matrix.in.yaml new file mode 100644 index 00000000..8b9360c3 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/multiple_matrix.in.yaml @@ -0,0 +1,13 @@ +name: test +jobs: + job1: + strategy: + matrix: + os: [ubuntu-22.04, ubuntu-20.04] + version: [1.17, 1.18, 1.19] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version diff --git a/package/gitea/source/modules/actions/jobparser/testdata/multiple_matrix.out.yaml b/package/gitea/source/modules/actions/jobparser/testdata/multiple_matrix.out.yaml new file mode 100644 index 00000000..e277cdd0 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/multiple_matrix.out.yaml @@ -0,0 +1,101 @@ +name: test +jobs: + job1: + name: job1 (ubuntu-20.04, 1.17) + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-20.04 + version: + - 1.17 +--- +name: test +jobs: + job1: + name: job1 (ubuntu-20.04, 1.18) + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-20.04 + version: + - 1.18 +--- +name: test +jobs: + job1: + name: job1 (ubuntu-20.04, 1.19) + runs-on: ubuntu-20.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-20.04 + version: + - 1.19 +--- +name: test +jobs: + job1: + name: job1 (ubuntu-22.04, 1.17) + runs-on: ubuntu-22.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-22.04 + version: + - 1.17 +--- +name: test +jobs: + job1: + name: job1 (ubuntu-22.04, 1.18) + runs-on: ubuntu-22.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-22.04 + version: + - 1.18 +--- +name: test +jobs: + job1: + name: job1 (ubuntu-22.04, 1.19) + runs-on: ubuntu-22.04 + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.version }} + - run: uname -a && go version + strategy: + matrix: + os: + - ubuntu-22.04 + version: + - 1.19 diff --git a/package/gitea/source/modules/actions/jobparser/testdata/null_job_explicit.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/null_job_explicit.in.yaml new file mode 100644 index 00000000..6731507a --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/null_job_explicit.in.yaml @@ -0,0 +1,9 @@ +# null_job_explicit.in.yaml +on: push +jobs: + empty: null + notempty: + needs: empty + runs-on: ubuntu-latest + steps: + - run: echo ok diff --git a/package/gitea/source/modules/actions/jobparser/testdata/null_job_implicit.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/null_job_implicit.in.yaml new file mode 100644 index 00000000..26591aad --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/null_job_implicit.in.yaml @@ -0,0 +1,9 @@ +# null_job_implicit.in.yaml +on: push +jobs: + empty: + notempty: + needs: empty + runs-on: ubuntu-latest + steps: + - run: echo ok diff --git a/package/gitea/source/modules/actions/jobparser/testdata/prefixed_newline.in.yaml b/package/gitea/source/modules/actions/jobparser/testdata/prefixed_newline.in.yaml new file mode 100644 index 00000000..91f85302 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/prefixed_newline.in.yaml @@ -0,0 +1,14 @@ +name: Step with leading new line + +on: + push: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: "\nExtract tag for variant" + id: extract_tag + run: | + + echo Test diff --git a/package/gitea/source/modules/actions/jobparser/testdata/prefixed_newline.out.yaml b/package/gitea/source/modules/actions/jobparser/testdata/prefixed_newline.out.yaml new file mode 100644 index 00000000..027e9dfa --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata/prefixed_newline.out.yaml @@ -0,0 +1,15 @@ +name: Step with leading new line +"on": + push: +jobs: + test: + name: test + runs-on: ubuntu-latest + steps: + - id: extract_tag + name: |2- + + Extract tag for variant + run: |2 + + echo Test diff --git a/package/gitea/source/modules/actions/jobparser/testdata_test.go b/package/gitea/source/modules/actions/jobparser/testdata_test.go new file mode 100644 index 00000000..872484e2 --- /dev/null +++ b/package/gitea/source/modules/actions/jobparser/testdata_test.go @@ -0,0 +1,21 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package jobparser + +import ( + "embed" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +//go:embed testdata +var testdata embed.FS + +func ReadTestdata(t *testing.T, name string) []byte { + content, err := testdata.ReadFile(filepath.Join("testdata", name)) + require.NoError(t, err) + return content +} diff --git a/package/gitea/source/modules/actions/log.go b/package/gitea/source/modules/actions/log.go index 5a1425e0..3fb56b40 100644 --- a/package/gitea/source/modules/actions/log.go +++ b/package/gitea/source/modules/actions/log.go @@ -33,21 +33,22 @@ const ( // It doesn't respect the file format in the filename like ".zst", since it's difficult to reopen a closed compressed file and append new content. // Why doesn't it store logs in object storage directly? Because it's not efficient to append content to object storage. func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runnerv1.LogRow) ([]int, error) { - flag := os.O_WRONLY + flag, openFileFor := os.O_WRONLY, "write-only" if offset == 0 { - // Create file only if offset is 0, or it could result in content holes if the file doesn't exist. - flag |= os.O_CREATE + // Only allow to create file if offset is 0 (the first write), see #25560. + // Otherwise, it might result in content holes if the file has been deleted after transferred (actions.TransferLogs). + flag, openFileFor = os.O_WRONLY|os.O_CREATE, "write-create" } name := DBFSPrefix + filename f, err := dbfs.OpenFile(ctx, name, flag) if err != nil { - return nil, fmt.Errorf("dbfs OpenFile %q: %w", name, err) + return nil, fmt.Errorf("dbfs.OpenFile %q for %s: %w", name, openFileFor, err) } defer f.Close() stat, err := f.Stat() if err != nil { - return nil, fmt.Errorf("dbfs Stat %q: %w", name, err) + return nil, fmt.Errorf("dbfs.Stat %q: %w", name, err) } if stat.Size() < offset { // If the size is less than offset, refuse to write, or it could result in content holes. @@ -56,7 +57,7 @@ func WriteLogs(ctx context.Context, filename string, offset int64, rows []*runne } if _, err := f.Seek(offset, io.SeekStart); err != nil { - return nil, fmt.Errorf("dbfs Seek %q: %w", name, err) + return nil, fmt.Errorf("dbfs.Seek %q: %w", name, err) } writer := bufio.NewWriterSize(f, defaultBufSize) @@ -121,16 +122,17 @@ const ( // TransferLogs transfers logs from DBFS to object storage. // It happens when the file is complete and no more logs will be appended. // It respects the file format in the filename like ".zst", and compresses the content if needed. +// The task log file must be marked as "log_in_storage=true" after the transfer. func TransferLogs(ctx context.Context, filename string) (func(), error) { name := DBFSPrefix + filename remove := func() { if err := dbfs.Remove(ctx, name); err != nil { - log.Warn("dbfs remove %q: %v", name, err) + log.Warn("dbfs.Remove %q: %v", name, err) } } f, err := dbfs.Open(ctx, name) if err != nil { - return nil, fmt.Errorf("dbfs open %q: %w", name, err) + return nil, fmt.Errorf("dbfs.Open %q: %w", name, err) } defer f.Close() @@ -164,7 +166,7 @@ func RemoveLogs(ctx context.Context, inStorage bool, filename string) error { name := DBFSPrefix + filename err := dbfs.Remove(ctx, name) if err != nil { - return fmt.Errorf("dbfs remove %q: %w", name, err) + return fmt.Errorf("dbfs.Remove %q: %w", name, err) } return nil } @@ -180,7 +182,7 @@ func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeek name := DBFSPrefix + filename f, err := dbfs.Open(ctx, name) if err != nil { - return nil, fmt.Errorf("dbfs open %q: %w", name, err) + return nil, fmt.Errorf("dbfs.Open %q: %w", name, err) } return f, nil } diff --git a/package/gitea/source/modules/actions/workflows.go b/package/gitea/source/modules/actions/workflows.go index 26a6ebc3..ba1aee7d 100644 --- a/package/gitea/source/modules/actions/workflows.go +++ b/package/gitea/source/modules/actions/workflows.go @@ -8,17 +8,18 @@ import ( "slices" "strings" + "code.gitea.io/gitea/modules/actions/jobparser" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" - "github.com/nektos/act/pkg/jobparser" "github.com/nektos/act/pkg/model" "github.com/nektos/act/pkg/workflowpattern" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v4" ) type DetectedWorkflow struct { @@ -41,22 +42,30 @@ func IsWorkflow(path string) bool { return false } - return strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows") + for _, workflowDir := range setting.Actions.WorkflowDirs { + if strings.HasPrefix(path, workflowDir+"/") { + return true + } + } + return false } func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { - rpath := ".gitea/workflows" - tree, err := commit.SubTree(rpath) - if _, ok := err.(git.ErrNotExist); ok { - rpath = ".github/workflows" - tree, err = commit.SubTree(rpath) + var tree *git.Tree + var err error + var workflowDir string + for _, workflowDir = range setting.Actions.WorkflowDirs { + tree, err = commit.SubTree(workflowDir) + if err == nil { + break + } + if !git.IsErrNotExist(err) { + return "", nil, err + } } - if _, ok := err.(git.ErrNotExist); ok { + if tree == nil { return "", nil, nil } - if err != nil { - return "", nil, err - } entries, err := tree.ListEntriesRecursiveFast() if err != nil { @@ -69,7 +78,7 @@ func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { ret = append(ret, entry) } } - return rpath, ret, nil + return workflowDir, ret, nil } func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { @@ -94,10 +103,20 @@ func GetEventsFromContent(content []byte) ([]*jobparser.Event, error) { if err != nil { return nil, err } + if err := ValidateWorkflowContent(content); err != nil { + return nil, err + } return events, nil } +// ValidateWorkflowContent catches structural errors (e.g. blank lines in run: | blocks) +// that model.ReadWorkflow alone does not detect. +func ValidateWorkflowContent(content []byte) error { + _, err := jobparser.Parse(content) + return err +} + func DetectWorkflows( gitRepo *git.Repository, commit *git.Commit, diff --git a/package/gitea/source/modules/actions/workflows_test.go b/package/gitea/source/modules/actions/workflows_test.go index 89620fb6..cda2de13 100644 --- a/package/gitea/source/modules/actions/workflows_test.go +++ b/package/gitea/source/modules/actions/workflows_test.go @@ -7,12 +7,93 @@ import ( "testing" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/test" webhook_module "code.gitea.io/gitea/modules/webhook" "github.com/stretchr/testify/assert" ) +func fullWorkflowContent(part string) []byte { + return []byte(` +name: test +` + part + ` +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo hello +`) +} + +func TestIsWorkflow(t *testing.T) { + defer test.MockVariableValue(&setting.Actions.WorkflowDirs)() + + tests := []struct { + name string + dirs []string + path string + expected bool + }{ + { + name: "default with yml extension", + dirs: []string{".gitea/workflows", ".github/workflows"}, + path: ".gitea/workflows/test.yml", + expected: true, + }, + { + name: "default with yaml extension", + dirs: []string{".gitea/workflows", ".github/workflows"}, + path: ".github/workflows/test.yaml", + expected: true, + }, + { + name: "only gitea configured, github path rejected", + dirs: []string{".gitea/workflows"}, + path: ".github/workflows/test.yml", + expected: false, + }, + { + name: "only github configured, gitea path rejected", + dirs: []string{".github/workflows"}, + path: ".gitea/workflows/test.yml", + expected: false, + }, + { + name: "custom workflow dir", + dirs: []string{".custom/workflows"}, + path: ".custom/workflows/deploy.yml", + expected: true, + }, + { + name: "non-workflow file", + dirs: []string{".gitea/workflows", ".github/workflows"}, + path: ".gitea/workflows/readme.md", + expected: false, + }, + { + name: "directory boundary", + dirs: []string{".gitea/workflows"}, + path: ".gitea/workflows2/test.yml", + expected: false, + }, + { + name: "unrelated path", + dirs: []string{".gitea/workflows", ".github/workflows"}, + path: "src/main.go", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + setting.Actions.WorkflowDirs = tt.dirs + assert.Equal(t, tt.expected, IsWorkflow(tt.path)) + }) + } +} + func TestDetectMatched(t *testing.T) { testCases := []struct { desc string @@ -119,7 +200,7 @@ func TestDetectMatched(t *testing.T) { expected: true, }, { - desc: "HookEventSchedue(schedule) matches GithubEventSchedule(schedule)", + desc: "HookEventSchedule(schedule) matches GithubEventSchedule(schedule)", triggedEvent: webhook_module.HookEventSchedule, payload: nil, yamlOn: "on: schedule", @@ -147,7 +228,7 @@ func TestDetectMatched(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - evts, err := GetEventsFromContent([]byte(tc.yamlOn)) + evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) assert.Equal(t, tc.expected, detectMatched(nil, tc.commit, tc.triggedEvent, tc.payload, evts[0])) @@ -302,7 +383,7 @@ func TestMatchIssuesEvent(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - evts, err := GetEventsFromContent([]byte(tc.yamlOn)) + evts, err := GetEventsFromContent(fullWorkflowContent(tc.yamlOn)) assert.NoError(t, err) assert.Len(t, evts, 1) diff --git a/package/gitea/source/modules/activitypub/client.go b/package/gitea/source/modules/activitypub/client.go deleted file mode 100644 index a9941f9b..00000000 --- a/package/gitea/source/modules/activitypub/client.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package activitypub - -import ( - "bytes" - "context" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" - "net/http" - "strings" - "time" - - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/proxy" - "code.gitea.io/gitea/modules/setting" - - "github.com/42wim/httpsig" -) - -const ( - // ActivityStreamsContentType const - ActivityStreamsContentType = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` - httpsigExpirationTime = 60 -) - -// Gets the current time as an RFC 2616 formatted string -// RFC 2616 requires RFC 1123 dates but with GMT instead of UTC -func CurrentTime() string { - return strings.ReplaceAll(time.Now().UTC().Format(time.RFC1123), "UTC", "GMT") -} - -func containsRequiredHTTPHeaders(method string, headers []string) error { - var hasRequestTarget, hasDate, hasDigest bool - for _, header := range headers { - hasRequestTarget = hasRequestTarget || header == httpsig.RequestTarget - hasDate = hasDate || header == "Date" - hasDigest = hasDigest || header == "Digest" - } - if !hasRequestTarget { - return fmt.Errorf("missing http header for %s: %s", method, httpsig.RequestTarget) - } else if !hasDate { - return fmt.Errorf("missing http header for %s: Date", method) - } else if !hasDigest && method != http.MethodGet { - return fmt.Errorf("missing http header for %s: Digest", method) - } - return nil -} - -// Client struct -type Client struct { - client *http.Client - algs []httpsig.Algorithm - digestAlg httpsig.DigestAlgorithm - getHeaders []string - postHeaders []string - priv *rsa.PrivateKey - pubID string -} - -// NewClient function -func NewClient(ctx context.Context, user *user_model.User, pubID string) (c *Client, err error) { - if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil { - return nil, err - } else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil { - return nil, err - } - - priv, err := GetPrivateKey(ctx, user) - if err != nil { - return nil, err - } - privPem, _ := pem.Decode([]byte(priv)) - privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes) - if err != nil { - return nil, err - } - - c = &Client{ - client: &http.Client{ - Transport: &http.Transport{ - Proxy: proxy.Proxy(), - }, - }, - algs: setting.HttpsigAlgs, - digestAlg: httpsig.DigestAlgorithm(setting.Federation.DigestAlgorithm), - getHeaders: setting.Federation.GetHeaders, - postHeaders: setting.Federation.PostHeaders, - priv: privParsed, - pubID: pubID, - } - return c, err -} - -// NewRequest function -func (c *Client) NewRequest(b []byte, to string) (req *http.Request, err error) { - buf := bytes.NewBuffer(b) - req, err = http.NewRequest(http.MethodPost, to, buf) - if err != nil { - return nil, err - } - req.Header.Add("Content-Type", ActivityStreamsContentType) - req.Header.Add("Date", CurrentTime()) - req.Header.Add("User-Agent", "Gitea/"+setting.AppVer) - signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime) - if err != nil { - return nil, err - } - err = signer.SignRequest(c.priv, c.pubID, req, b) - return req, err -} - -// Post function -func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) { - var req *http.Request - if req, err = c.NewRequest(b, to); err != nil { - return nil, err - } - resp, err = c.client.Do(req) - return resp, err -} diff --git a/package/gitea/source/modules/activitypub/client_test.go b/package/gitea/source/modules/activitypub/client_test.go deleted file mode 100644 index 361270a8..00000000 --- a/package/gitea/source/modules/activitypub/client_test.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package activitypub - -import ( - "fmt" - "io" - "net/http" - "net/http/httptest" - "testing" - - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/setting" - - "github.com/stretchr/testify/assert" -) - -func TestActivityPubSignedPost(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - pubID := "https://example.com/pubID" - c, err := NewClient(t.Context(), user, pubID) - assert.NoError(t, err) - - expected := "BODY" - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Regexp(t, "^"+setting.Federation.DigestAlgorithm, r.Header.Get("Digest")) - assert.Contains(t, r.Header.Get("Signature"), pubID) - assert.Equal(t, ActivityStreamsContentType, r.Header.Get("Content-Type")) - body, err := io.ReadAll(r.Body) - assert.NoError(t, err) - assert.Equal(t, expected, string(body)) - fmt.Fprint(w, expected) - })) - defer srv.Close() - - r, err := c.Post([]byte(expected), srv.URL) - assert.NoError(t, err) - defer r.Body.Close() - body, err := io.ReadAll(r.Body) - assert.NoError(t, err) - assert.Equal(t, expected, string(body)) -} diff --git a/package/gitea/source/modules/activitypub/main_test.go b/package/gitea/source/modules/activitypub/main_test.go deleted file mode 100644 index 4591f1fa..00000000 --- a/package/gitea/source/modules/activitypub/main_test.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package activitypub - -import ( - "testing" - - "code.gitea.io/gitea/models/unittest" - - _ "code.gitea.io/gitea/models" - _ "code.gitea.io/gitea/models/actions" - _ "code.gitea.io/gitea/models/activities" -) - -func TestMain(m *testing.M) { - unittest.MainTest(m) -} diff --git a/package/gitea/source/modules/activitypub/user_settings.go b/package/gitea/source/modules/activitypub/user_settings.go deleted file mode 100644 index 7f939af3..00000000 --- a/package/gitea/source/modules/activitypub/user_settings.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package activitypub - -import ( - "context" - - user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/util" -) - -const rsaBits = 3072 - -// GetKeyPair function returns a user's private and public keys -func GetKeyPair(ctx context.Context, user *user_model.User) (pub, priv string, err error) { - var settings map[string]*user_model.Setting - settings, err = user_model.GetSettings(ctx, user.ID, []string{user_model.UserActivityPubPrivPem, user_model.UserActivityPubPubPem}) - if err != nil { - return pub, priv, err - } else if len(settings) == 0 { - if priv, pub, err = util.GenerateKeyPair(rsaBits); err != nil { - return pub, priv, err - } - if err = user_model.SetUserSetting(ctx, user.ID, user_model.UserActivityPubPrivPem, priv); err != nil { - return pub, priv, err - } - if err = user_model.SetUserSetting(ctx, user.ID, user_model.UserActivityPubPubPem, pub); err != nil { - return pub, priv, err - } - return pub, priv, err - } - priv = settings[user_model.UserActivityPubPrivPem].SettingValue - pub = settings[user_model.UserActivityPubPubPem].SettingValue - return pub, priv, err -} - -// GetPublicKey function returns a user's public key -func GetPublicKey(ctx context.Context, user *user_model.User) (pub string, err error) { - pub, _, err = GetKeyPair(ctx, user) - return pub, err -} - -// GetPrivateKey function returns a user's private key -func GetPrivateKey(ctx context.Context, user *user_model.User) (priv string, err error) { - _, priv, err = GetKeyPair(ctx, user) - return priv, err -} diff --git a/package/gitea/source/modules/activitypub/user_settings_test.go b/package/gitea/source/modules/activitypub/user_settings_test.go deleted file mode 100644 index 105d4aed..00000000 --- a/package/gitea/source/modules/activitypub/user_settings_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package activitypub - -import ( - "testing" - - "code.gitea.io/gitea/models/unittest" - user_model "code.gitea.io/gitea/models/user" - - _ "code.gitea.io/gitea/models" // https://forum.gitea.com/t/testfixtures-could-not-clean-table-access-no-such-table-access/4137/4 - - "github.com/stretchr/testify/assert" -) - -func TestUserSettings(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) - pub, priv, err := GetKeyPair(t.Context(), user1) - assert.NoError(t, err) - pub1, err := GetPublicKey(t.Context(), user1) - assert.NoError(t, err) - assert.Equal(t, pub, pub1) - priv1, err := GetPrivateKey(t.Context(), user1) - assert.NoError(t, err) - assert.Equal(t, priv, priv1) -} diff --git a/package/gitea/source/modules/analyze/code_language.go b/package/gitea/source/modules/analyze/code_language.go index 74e7a06d..d8589861 100644 --- a/package/gitea/source/modules/analyze/code_language.go +++ b/package/gitea/source/modules/analyze/code_language.go @@ -4,12 +4,13 @@ package analyze import ( - "path/filepath" + "path" "github.com/go-enry/go-enry/v2" ) // GetCodeLanguage detects code language based on file name and content +// It can be slow when the content is used for detection func GetCodeLanguage(filename string, content []byte) string { if language, ok := enry.GetLanguageByExtension(filename); ok { return language @@ -23,5 +24,5 @@ func GetCodeLanguage(filename string, content []byte) string { return enry.OtherLanguage } - return enry.GetLanguage(filepath.Base(filename), content) + return enry.GetLanguage(path.Base(filename), content) } diff --git a/package/gitea/source/modules/analyze/vendor.go b/package/gitea/source/modules/analyze/vendor.go index adcca923..f3e75f53 100644 --- a/package/gitea/source/modules/analyze/vendor.go +++ b/package/gitea/source/modules/analyze/vendor.go @@ -4,10 +4,28 @@ package analyze import ( + "path" + "strings" + "github.com/go-enry/go-enry/v2" ) -// IsVendor returns whether or not path is a vendor path. -func IsVendor(path string) bool { - return enry.IsVendor(path) +// IsVendor returns whether the path is a vendor path. +// It uses go-enry's IsVendor function but overrides its detection for certain +// special cases that shouldn't be marked as vendored in the diff view. +func IsVendor(treePath string) bool { + if !enry.IsVendor(treePath) { + return false + } + + // Override detection for single files + basename := path.Base(treePath) + switch basename { + case ".gitignore", ".gitattributes", ".gitmodules": + return false + } + if strings.HasPrefix(treePath, ".github/") || strings.HasPrefix(treePath, ".gitea/") { + return false + } + return true } diff --git a/package/gitea/source/modules/analyze/vendor_test.go b/package/gitea/source/modules/analyze/vendor_test.go index 02a51d4c..6efb825d 100644 --- a/package/gitea/source/modules/analyze/vendor_test.go +++ b/package/gitea/source/modules/analyze/vendor_test.go @@ -14,6 +14,7 @@ func TestIsVendor(t *testing.T) { path string want bool }{ + // Original go-enry test cases {"cache/", true}, {"random/cache/", true}, {"cache", false}, @@ -34,6 +35,14 @@ func TestIsVendor(t *testing.T) { {"a/docs/_build/", true}, {"a/dasdocs/_build-vsdoc.js", true}, {"a/dasdocs/_build-vsdoc.j", false}, + + // Override: Git/GitHub/Gitea-related paths should NOT be detected as vendored + {".gitignore", false}, + {".gitattributes", false}, + {".gitmodules", false}, + {"src/.gitignore", false}, + {".github/workflows/ci.yml", false}, + {".gitea/workflows/ci.yml", false}, } for _, tt := range tests { t.Run(tt.path, func(t *testing.T) { diff --git a/package/gitea/source/modules/assetfs/embed.go b/package/gitea/source/modules/assetfs/embed.go index 95176372..7c90e3ac 100644 --- a/package/gitea/source/modules/assetfs/embed.go +++ b/package/gitea/source/modules/assetfs/embed.go @@ -260,7 +260,7 @@ func (fi *embeddedFileInfo) Mode() fs.FileMode { } func (fi *embeddedFileInfo) ModTime() time.Time { - return getExecutableModTime() + return GetExecutableModTime() } func (fi *embeddedFileInfo) IsDir() bool { @@ -279,9 +279,9 @@ func (fi *embeddedFileInfo) Info() (fs.FileInfo, error) { return fi, nil } -// getExecutableModTime returns the modification time of the executable file. +// GetExecutableModTime returns the modification time of the executable file. // In bindata, we can't use the ModTime of the files because we need to make the build reproducible -var getExecutableModTime = sync.OnceValue(func() (modTime time.Time) { +var GetExecutableModTime = sync.OnceValue(func() (modTime time.Time) { exePath, err := os.Executable() if err != nil { return modTime @@ -365,11 +365,11 @@ func GenerateEmbedBindata(fsRootPath, outputFile string) error { if err = embedFiles(meta.Root, fsRootPath, ""); err != nil { return err } - jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL + jsonBuf, err := json.Marshal(meta) if err != nil { return err } _, _ = output.Write([]byte{'\n'}) - _, err = output.Write(jsonBuf) + _, err = output.Write(bytes.TrimSpace(jsonBuf)) return err } diff --git a/package/gitea/source/modules/assetfs/layered.go b/package/gitea/source/modules/assetfs/layered.go index ce55475b..380c3ac4 100644 --- a/package/gitea/source/modules/assetfs/layered.go +++ b/package/gitea/source/modules/assetfs/layered.go @@ -6,12 +6,12 @@ package assetfs import ( "context" "fmt" - "io" "io/fs" - "net/http" "os" "path/filepath" + "slices" "sort" + "strings" "time" "code.gitea.io/gitea/modules/container" @@ -25,7 +25,7 @@ import ( // Layer represents a layer in a layered asset file-system. It has a name and works like http.FileSystem type Layer struct { name string - fs http.FileSystem + fs fs.FS localPath string } @@ -34,7 +34,7 @@ func (l *Layer) Name() string { } // Open opens the named file. The caller is responsible for closing the file. -func (l *Layer) Open(name string) (http.File, error) { +func (l *Layer) Open(name string) (fs.File, error) { return l.fs.Open(name) } @@ -48,12 +48,12 @@ func Local(name, base string, sub ...string) *Layer { panic(fmt.Sprintf("Unable to get absolute path for %q: %v", base, err)) } root := util.FilePathJoinAbs(base, sub...) - return &Layer{name: name, fs: http.Dir(root), localPath: root} + return &Layer{name: name, fs: os.DirFS(root), localPath: root} } // Bindata returns a new Layer with the given name, it serves files from the given bindata asset. func Bindata(name string, fs fs.FS) *Layer { - return &Layer{name: name, fs: http.FS(fs)} + return &Layer{name: name, fs: fs} } // LayeredFS is a layered asset file-system. It works like http.FileSystem, but it can have multiple layers. @@ -63,13 +63,15 @@ type LayeredFS struct { layers []*Layer } +var _ fs.ReadDirFS = (*LayeredFS)(nil) + // Layered returns a new LayeredFS with the given layers. The first layer is the top layer. func Layered(layers ...*Layer) *LayeredFS { return &LayeredFS{layers: layers} } // Open opens the named file. The caller is responsible for closing the file. -func (l *LayeredFS) Open(name string) (http.File, error) { +func (l *LayeredFS) Open(name string) (fs.File, error) { for _, layer := range l.layers { f, err := layer.Open(name) if err == nil || !os.IsNotExist(err) { @@ -85,44 +87,59 @@ func (l *LayeredFS) ReadFile(elems ...string) ([]byte, error) { return bs, err } +func (l *LayeredFS) ReadDir(name string) (files []fs.DirEntry, _ error) { + filesMap := map[string]fs.DirEntry{} + for _, layer := range l.layers { + entries, err := readDirOptional(layer, name) + if err != nil { + return nil, err + } + for _, entry := range entries { + entryName := entry.Name() + if _, exist := filesMap[entryName]; !exist && shouldInclude(entry) { + filesMap[entryName] = entry + } + } + } + for _, file := range filesMap { + files = append(files, file) + } + slices.SortFunc(files, func(a, b fs.DirEntry) int { return strings.Compare(a.Name(), b.Name()) }) + return files, nil +} + // ReadLayeredFile reads the named file, and returns the layer name. func (l *LayeredFS) ReadLayeredFile(elems ...string) ([]byte, string, error) { name := util.PathJoinRel(elems...) for _, layer := range l.layers { - f, err := layer.Open(name) + bs, err := fs.ReadFile(layer, name) if os.IsNotExist(err) { 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 } -func shouldInclude(info fs.FileInfo, fileMode ...bool) bool { - if util.IsCommonHiddenFileName(info.Name()) { +func shouldInclude(dirEntry fs.DirEntry, fileMode ...bool) bool { + if util.IsCommonHiddenFileName(dirEntry.Name()) { return false } if len(fileMode) == 0 { return true } else if len(fileMode) == 1 { - return fileMode[0] == !info.Mode().IsDir() + return fileMode[0] == !dirEntry.IsDir() } panic("too many arguments for fileMode in shouldInclude") } -func readDir(layer *Layer, name string) ([]fs.FileInfo, error) { - f, err := layer.Open(name) - if os.IsNotExist(err) { +func readDirOptional(layer *Layer, name string) (entries []fs.DirEntry, err error) { + if entries, err = fs.ReadDir(layer, name); os.IsNotExist(err) { return nil, nil - } else if err != nil { - return nil, err } - defer f.Close() - return f.Readdir(-1) + return entries, err } // ListFiles lists files/directories in the given directory. The fileMode controls the returned files. @@ -133,13 +150,13 @@ func readDir(layer *Layer, name string) ([]fs.FileInfo, error) { func (l *LayeredFS) ListFiles(name string, fileMode ...bool) ([]string, error) { fileSet := make(container.Set[string]) for _, layer := range l.layers { - infos, err := readDir(layer, name) + entries, err := readDirOptional(layer, name) if err != nil { return nil, err } - for _, info := range infos { - if shouldInclude(info, fileMode...) { - fileSet.Add(info.Name()) + for _, entry := range entries { + if shouldInclude(entry, fileMode...) { + fileSet.Add(entry.Name()) } } } @@ -163,16 +180,16 @@ func listAllFiles(layers []*Layer, name string, fileMode ...bool) ([]string, err var list func(dir string) error list = func(dir string) error { for _, layer := range layers { - infos, err := readDir(layer, dir) + entries, err := readDirOptional(layer, dir) if err != nil { return err } - for _, info := range infos { - path := util.PathJoinRelX(dir, info.Name()) - if shouldInclude(info, fileMode...) { + for _, entry := range entries { + path := util.PathJoinRelX(dir, entry.Name()) + if shouldInclude(entry, fileMode...) { fileSet.Add(path) } - if info.IsDir() { + if entry.IsDir() { if err = list(path); err != nil { return err } diff --git a/package/gitea/source/modules/auth/pam/pam.go b/package/gitea/source/modules/auth/pam/pam.go index cca1482b..a8b608e6 100644 --- a/package/gitea/source/modules/auth/pam/pam.go +++ b/package/gitea/source/modules/auth/pam/pam.go @@ -8,7 +8,7 @@ package pam import ( "errors" - "github.com/msteinert/pam" + "github.com/msteinert/pam/v2" ) // Supported is true when built with PAM @@ -28,6 +28,7 @@ func Auth(serviceName, userName, passwd string) (string, error) { if err != nil { return "", err } + defer t.End() if err = t.Authenticate(0); err != nil { return "", err diff --git a/package/gitea/source/modules/auth/password/hash/argon2.go b/package/gitea/source/modules/auth/password/hash/argon2.go index 0cd6472f..f4a7497d 100644 --- a/package/gitea/source/modules/auth/password/hash/argon2.go +++ b/package/gitea/source/modules/auth/password/hash/argon2.go @@ -61,17 +61,11 @@ func NewArgon2Hasher(config string) *Argon2Hasher { return nil } - parsed, err := parseUIntParam(vals[0], "time", "argon2", config, nil) - hasher.time = uint32(parsed) - - parsed, err = parseUIntParam(vals[1], "memory", "argon2", config, err) - hasher.memory = uint32(parsed) - - parsed, err = parseUIntParam(vals[2], "threads", "argon2", config, err) - hasher.threads = uint8(parsed) - - parsed, err = parseUIntParam(vals[3], "keyLen", "argon2", config, err) - hasher.keyLen = uint32(parsed) + var err error + hasher.time, err = parseUintParam[uint32](vals[0], "time", "argon2", config, nil) + hasher.memory, err = parseUintParam[uint32](vals[1], "memory", "argon2", config, err) + hasher.threads, err = parseUintParam[uint8](vals[2], "threads", "argon2", config, err) + hasher.keyLen, err = parseUintParam[uint32](vals[3], "keyLen", "argon2", config, err) if err != nil { return nil } diff --git a/package/gitea/source/modules/auth/password/hash/common.go b/package/gitea/source/modules/auth/password/hash/common.go index d5e2c343..1fafc289 100644 --- a/package/gitea/source/modules/auth/password/hash/common.go +++ b/package/gitea/source/modules/auth/password/hash/common.go @@ -7,6 +7,7 @@ import ( "strconv" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) func parseIntParam(value, param, algorithmName, config string, previousErr error) (int, error) { @@ -18,11 +19,12 @@ func parseIntParam(value, param, algorithmName, config string, previousErr error return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed } -func parseUIntParam(value, param, algorithmName, config string, previousErr error) (uint64, error) { //nolint:unparam // algorithmName is always argon2 - parsed, err := strconv.ParseUint(value, 10, 64) +func parseUintParam[T uint32 | uint8](value, param, algorithmName, config string, previousErr error) (ret T, _ error) { + _, isUint32 := any(ret).(uint32) + parsed, err := strconv.ParseUint(value, 10, util.Iif(isUint32, 32, 8)) if err != nil { log.Error("invalid integer for %s representation in %s hash spec %s", param, algorithmName, config) return 0, err } - return parsed, previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed + return T(parsed), previousErr // <- Keep the previous error as this function should still return an error once everything has been checked if any call failed } diff --git a/package/gitea/source/modules/auth/password/hash/setting.go b/package/gitea/source/modules/auth/password/hash/setting.go index f0715f31..e0caed5b 100644 --- a/package/gitea/source/modules/auth/password/hash/setting.go +++ b/package/gitea/source/modules/auth/password/hash/setting.go @@ -14,7 +14,7 @@ const DefaultHashAlgorithmName = "pbkdf2" 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/package/gitea/source/modules/auth/password/pwn/pwn.go b/package/gitea/source/modules/auth/password/pwn/pwn.go index 99a6ca6c..d5ea96c4 100644 --- a/package/gitea/source/modules/auth/password/pwn/pwn.go +++ b/package/gitea/source/modules/auth/password/pwn/pwn.go @@ -72,7 +72,7 @@ func newRequest(ctx context.Context, method, url string, body io.ReadCloser) (*h // Adding padding will make requests more secure, however is also slower // because artificial responses will be added to the response // For more information, see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/ -func (c *Client) CheckPassword(pw string, padding bool) (int, error) { +func (c *Client) CheckPassword(pw string, padding bool) (int64, error) { if pw == "" { return -1, ErrEmptyPassword } @@ -111,7 +111,7 @@ func (c *Client) CheckPassword(pw string, padding bool) (int, error) { if err != nil { return -1, err } - return int(count), nil + return count, nil } } return 0, nil diff --git a/package/gitea/source/modules/auth/password/pwn/pwn_test.go b/package/gitea/source/modules/auth/password/pwn/pwn_test.go index ae03fabc..4b760fdf 100644 --- a/package/gitea/source/modules/auth/password/pwn/pwn_test.go +++ b/package/gitea/source/modules/auth/password/pwn/pwn_test.go @@ -37,25 +37,25 @@ func TestPassword(t *testing.T) { count, err := client.CheckPassword("", false) assert.ErrorIs(t, err, ErrEmptyPassword, "blank input should return ErrEmptyPassword") - assert.Equal(t, -1, count) + assert.EqualValues(t, -1, count) count, err = client.CheckPassword("pwned", false) assert.NoError(t, err) - assert.Equal(t, 1, count) + assert.EqualValues(t, 1, count) count, err = client.CheckPassword("notpwned", false) assert.NoError(t, err) - assert.Equal(t, 0, count) + assert.EqualValues(t, 0, count) count, err = client.CheckPassword("paddedpwned", true) assert.NoError(t, err) - assert.Equal(t, 1, count) + assert.EqualValues(t, 1, count) count, err = client.CheckPassword("paddednotpwned", true) assert.NoError(t, err) - assert.Equal(t, 0, count) + assert.EqualValues(t, 0, count) count, err = client.CheckPassword("paddednotpwnedzero", true) assert.NoError(t, err) - assert.Equal(t, 0, count) + assert.EqualValues(t, 0, count) } diff --git a/package/gitea/source/modules/auth/webauthn/webauthn.go b/package/gitea/source/modules/auth/webauthn/webauthn.go index cbf5279c..86f55c6b 100644 --- a/package/gitea/source/modules/auth/webauthn/webauthn.go +++ b/package/gitea/source/modules/auth/webauthn/webauthn.go @@ -22,7 +22,7 @@ var WebAuthn *webauthn.WebAuthn // Init initializes the WebAuthn instance from the config. func Init() { - gob.Register(&webauthn.SessionData{}) + gob.Register(&webauthn.SessionData{}) // TODO: CHI-SESSION-GOB-REGISTER. appURL, _ := protocol.FullyQualifiedOrigin(setting.AppURL) diff --git a/package/gitea/source/modules/avatar/avatar.go b/package/gitea/source/modules/avatar/avatar.go index 106215ec..3b622b99 100644 --- a/package/gitea/source/modules/avatar/avatar.go +++ b/package/gitea/source/modules/avatar/avatar.go @@ -28,21 +28,18 @@ import ( // than the size after resizing. const DefaultAvatarSize = 256 -// RandomImageSize generates and returns a random avatar image unique to input data +// RandomImageWithSize generates and returns a random avatar image unique to input data // in custom size (height and width). -func RandomImageSize(size int, data []byte) (image.Image, error) { +func RandomImageWithSize(size int, data []byte) image.Image { // we use white as background, and use dark colors to draw blocks - imgMaker, err := identicon.New(size, color.White, identicon.DarkColors...) - if err != nil { - return nil, fmt.Errorf("identicon.New: %w", err) - } - return imgMaker.Make(data), nil + imgMaker := identicon.New(size, color.White, identicon.DarkColors) + return imgMaker.Make(data) } -// RandomImage generates and returns a random avatar image unique to input data +// RandomImageDefaultSize generates and returns a random avatar image unique to input data // in default size (height and width). -func RandomImage(data []byte) (image.Image, error) { - return RandomImageSize(DefaultAvatarSize*setting.Avatar.RenderedSizeFactor, data) +func RandomImageDefaultSize(data []byte) image.Image { + return RandomImageWithSize(DefaultAvatarSize*setting.Avatar.RenderedSizeFactor, data) } // processAvatarImage process the avatar image data, crop and resize it if necessary. diff --git a/package/gitea/source/modules/avatar/avatar_test.go b/package/gitea/source/modules/avatar/avatar_test.go index a5a1a7c1..5414184b 100644 --- a/package/gitea/source/modules/avatar/avatar_test.go +++ b/package/gitea/source/modules/avatar/avatar_test.go @@ -15,19 +15,6 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_RandomImageSize(t *testing.T) { - _, err := RandomImageSize(0, []byte("gitea@local")) - assert.Error(t, err) - - _, err = RandomImageSize(64, []byte("gitea@local")) - assert.NoError(t, err) -} - -func Test_RandomImage(t *testing.T) { - _, err := RandomImage([]byte("gitea@local")) - assert.NoError(t, err) -} - func Test_ProcessAvatarPNG(t *testing.T) { setting.Avatar.MaxWidth = 4096 setting.Avatar.MaxHeight = 4096 @@ -134,3 +121,18 @@ func Test_ProcessAvatarImage(t *testing.T) { _, err = processAvatarImage(origin, 262144) assert.ErrorContains(t, err, "image width is too large: 10 > 5") } + +func BenchmarkRandomImage(b *testing.B) { + b.Run("size-48", func(b *testing.B) { + for b.Loop() { + // BenchmarkRandomImage/size-48-12 49549 22899 ns/op + RandomImageWithSize(48, []byte("test-content")) + } + }) + b.Run("size-96", func(b *testing.B) { + for b.Loop() { + // BenchmarkRandomImage/size-96-12 13816 88187 ns/op + RandomImageWithSize(96, []byte("test-content")) + } + }) +} diff --git a/package/gitea/source/modules/avatar/identicon/identicon.go b/package/gitea/source/modules/avatar/identicon/identicon.go index 19f87da8..a1a0f0d7 100644 --- a/package/gitea/source/modules/avatar/identicon/identicon.go +++ b/package/gitea/source/modules/avatar/identicon/identicon.go @@ -8,13 +8,14 @@ package identicon import ( "crypto/sha256" - "errors" - "fmt" "image" "image/color" ) -const minImageSize = 16 +const ( + minImageSize = 16 + maxImageSize = 2048 +) // Identicon is used to generate pseudo-random avatars type Identicon struct { @@ -24,25 +25,17 @@ type Identicon struct { rect image.Rectangle } -// New returns an Identicon struct with the correct settings -// size image size -// back background color -// fore all possible foreground colors. only one foreground color will be picked randomly for one image -func New(size int, back color.Color, fore ...color.Color) (*Identicon, error) { - if len(fore) == 0 { - return nil, errors.New("foreground is not set") - } - - if size < minImageSize { - return nil, fmt.Errorf("size %d is smaller than min size %d", size, minImageSize) - } - +// New returns an Identicon struct. +// Only one foreground color will be picked randomly for one image. +func New(size int, backColor color.Color, foreColors []color.Color) *Identicon { + size = max(size, minImageSize) + size = min(size, maxImageSize) return &Identicon{ - foreColors: fore, - backColor: back, + foreColors: foreColors, + backColor: backColor, size: size, rect: image.Rect(0, 0, size, size), - }, nil + } } // Make generates an avatar by data diff --git a/package/gitea/source/modules/avatar/identicon/identicon_test.go b/package/gitea/source/modules/avatar/identicon/identicon_test.go index 23bcc73e..6af4b41c 100644 --- a/package/gitea/source/modules/avatar/identicon/identicon_test.go +++ b/package/gitea/source/modules/avatar/identicon/identicon_test.go @@ -23,7 +23,7 @@ func TestGenerate(t *testing.T) { } backColor := color.White - imgMaker, err := New(64, backColor, DarkColors...) + imgMaker, err := New(64, backColor, DarkColors) assert.NoError(t, err) for i := 0; i < 100; i++ { s := strconv.Itoa(i) diff --git a/package/gitea/source/modules/base/natural_sort.go b/package/gitea/source/modules/base/natural_sort.go index acb90022..d1ee7b04 100644 --- a/package/gitea/source/modules/base/natural_sort.go +++ b/package/gitea/source/modules/base/natural_sort.go @@ -41,8 +41,8 @@ func naturalSortAdvance(str string, pos int) (end int, isNumber bool) { return end, isNumber } -// NaturalSortLess compares two strings so that they could be sorted in natural order -func NaturalSortLess(s1, s2 string) bool { +// NaturalSortCompare compares two strings so that they could be sorted in natural order +func NaturalSortCompare(s1, s2 string) int { // There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997 // text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997 // So we need to handle the number parts by ourselves @@ -55,16 +55,16 @@ func NaturalSortLess(s1, s2 string) bool { if isNum1 && isNum2 { if part1 != part2 { if len(part1) != len(part2) { - return len(part1) < len(part2) + return len(part1) - len(part2) } - return part1 < part2 + return c.CompareString(part1, part2) } } else { if cmp := c.CompareString(part1, part2); cmp != 0 { - return cmp < 0 + return cmp } } pos1, pos2 = end1, end2 } - return len(s1) < len(s2) + return len(s1) - len(s2) } diff --git a/package/gitea/source/modules/base/natural_sort_test.go b/package/gitea/source/modules/base/natural_sort_test.go index b001bc4a..451aba66 100644 --- a/package/gitea/source/modules/base/natural_sort_test.go +++ b/package/gitea/source/modules/base/natural_sort_test.go @@ -11,12 +11,10 @@ import ( func TestNaturalSortLess(t *testing.T) { testLess := func(s1, s2 string) { - assert.True(t, NaturalSortLess(s1, s2), "s1 0 { - if table, ok = AmbiguousCharacters[key]; ok { + if table, ok = ambiguousTableMap[key]; ok { break } idx := strings.LastIndexAny(key, "-_") @@ -29,18 +29,18 @@ func AmbiguousTablesForLocale(locale translation.Locale) []*AmbiguousTable { } } if table == nil && (locale.Language() == "zh-CN" || locale.Language() == "zh_CN") { - table = AmbiguousCharacters["zh-hans"] + table = ambiguousTableMap["zh-hans"] } if table == nil && strings.HasPrefix(locale.Language(), "zh") { - table = AmbiguousCharacters["zh-hant"] + table = ambiguousTableMap["zh-hant"] } if table == nil { - table = AmbiguousCharacters["_default"] + table = ambiguousTableMap["_default"] } return []*AmbiguousTable{ table, - AmbiguousCharacters["_common"], + ambiguousTableMap["_common"], } } @@ -52,7 +52,7 @@ func isAmbiguous(r rune, confusableTo *rune, tables ...*AmbiguousTable) bool { i := sort.Search(len(table.Confusable), func(i int) bool { return table.Confusable[i] >= r }) - (*confusableTo) = table.With[i] + *confusableTo = table.With[i] return true } return false diff --git a/package/gitea/source/modules/charset/ambiguous/generate.go b/package/gitea/source/modules/charset/ambiguous/generate.go deleted file mode 100644 index e3fda5be..00000000 --- a/package/gitea/source/modules/charset/ambiguous/generate.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package main - -import ( - "bytes" - "flag" - "fmt" - "go/format" - "os" - "sort" - "text/template" - "unicode" - - "code.gitea.io/gitea/modules/json" - - "golang.org/x/text/unicode/rangetable" -) - -// ambiguous.json provides a one to one mapping of ambiguous characters to other characters -// See https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json - -type AmbiguousTable struct { - Confusable []rune - With []rune - Locale string - RangeTable *unicode.RangeTable -} - -type RunePair struct { - Confusable rune - With rune -} - -var verbose bool - -func main() { - flag.Usage = func() { - fmt.Fprintf(os.Stderr, `%s: Generate AmbiguousCharacter - -Usage: %[1]s [-v] [-o output.go] ambiguous.json -`, os.Args[0]) - flag.PrintDefaults() - } - - output := "" - flag.BoolVar(&verbose, "v", false, "verbose output") - flag.StringVar(&output, "o", "ambiguous_gen.go", "file to output to") - flag.Parse() - input := flag.Arg(0) - if input == "" { - input = "ambiguous.json" - } - - bs, err := os.ReadFile(input) - if err != nil { - fatalf("Unable to read: %s Err: %v", input, err) - } - - var unwrapped string - if err := json.Unmarshal(bs, &unwrapped); err != nil { - fatalf("Unable to unwrap content in: %s Err: %v", input, err) - } - - fromJSON := map[string][]uint32{} - if err := json.Unmarshal([]byte(unwrapped), &fromJSON); err != nil { - fatalf("Unable to unmarshal content in: %s Err: %v", input, err) - } - - tables := make([]*AmbiguousTable, 0, len(fromJSON)) - for locale, chars := range fromJSON { - table := &AmbiguousTable{Locale: locale} - table.Confusable = make([]rune, 0, len(chars)/2) - table.With = make([]rune, 0, len(chars)/2) - pairs := make([]RunePair, len(chars)/2) - for i := 0; i < len(chars); i += 2 { - pairs[i/2].Confusable, pairs[i/2].With = rune(chars[i]), rune(chars[i+1]) - } - sort.Slice(pairs, func(i, j int) bool { - return pairs[i].Confusable < pairs[j].Confusable - }) - for _, pair := range pairs { - table.Confusable = append(table.Confusable, pair.Confusable) - table.With = append(table.With, pair.With) - } - table.RangeTable = rangetable.New(table.Confusable...) - tables = append(tables, table) - } - sort.Slice(tables, func(i, j int) bool { - return tables[i].Locale < tables[j].Locale - }) - data := map[string]any{ - "Tables": tables, - } - - if err := runTemplate(generatorTemplate, output, &data); err != nil { - fatalf("Unable to run template: %v", err) - } -} - -func runTemplate(t *template.Template, filename string, data any) error { - buf := bytes.NewBuffer(nil) - if err := t.Execute(buf, data); err != nil { - return fmt.Errorf("unable to execute template: %w", err) - } - bs, err := format.Source(buf.Bytes()) - if err != nil { - verbosef("Bad source:\n%s", buf.String()) - return fmt.Errorf("unable to format source: %w", err) - } - - old, err := os.ReadFile(filename) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read old file %s because %w", filename, err) - } else if err == nil { - if bytes.Equal(bs, old) { - // files are the same don't rewrite it. - return nil - } - } - - file, err := os.Create(filename) - if err != nil { - return fmt.Errorf("failed to create file %s because %w", filename, err) - } - defer file.Close() - _, err = file.Write(bs) - if err != nil { - return fmt.Errorf("unable to write generated source: %w", err) - } - return nil -} - -var generatorTemplate = template.Must(template.New("ambiguousTemplate").Parse(`// This file is generated by modules/charset/ambiguous/generate.go DO NOT EDIT -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - - -package charset - -import "unicode" - -// This file is generated from https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json - -// AmbiguousTable matches a confusable rune with its partner for the Locale -type AmbiguousTable struct { - Confusable []rune - With []rune - Locale string - RangeTable *unicode.RangeTable -} - -// AmbiguousCharacters provides a map by locale name to the confusable characters in that locale -var AmbiguousCharacters = map[string]*AmbiguousTable{ - {{range .Tables}}{{printf "%q:" .Locale}} { - Confusable: []rune{ {{range .Confusable}}{{.}},{{end}} }, - With: []rune{ {{range .With}}{{.}},{{end}} }, - Locale: {{printf "%q" .Locale}}, - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {{range .RangeTable.R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, - {{end}} }, - R32: []unicode.Range32{ - {{range .RangeTable.R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, - {{end}} }, - LatinOffset: {{.RangeTable.LatinOffset}}, - }, - }, - {{end}} -} - -`)) - -func logf(format string, args ...any) { - fmt.Fprintf(os.Stderr, format+"\n", args...) -} - -func verbosef(format string, args ...any) { - if verbose { - logf(format, args...) - } -} - -func fatalf(format string, args ...any) { - logf("fatal: "+format+"\n", args...) - os.Exit(1) -} diff --git a/package/gitea/source/modules/charset/ambiguous_gen.go b/package/gitea/source/modules/charset/ambiguous_gen.go index c88ffd5a..669a46c9 100644 --- a/package/gitea/source/modules/charset/ambiguous_gen.go +++ b/package/gitea/source/modules/charset/ambiguous_gen.go @@ -1,5 +1,5 @@ -// This file is generated by modules/charset/ambiguous/generate.go DO NOT EDIT -// Copyright 2022 The Gitea Authors. All rights reserved. +// This file is generated by modules/charset/generate/generate.go DO NOT EDIT +// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package charset @@ -16,821 +16,837 @@ type AmbiguousTable struct { RangeTable *unicode.RangeTable } -// AmbiguousCharacters provides a map by locale name to the confusable characters in that locale -var AmbiguousCharacters = map[string]*AmbiguousTable{ - "_common": { - Confusable: []rune{184, 383, 388, 397, 422, 423, 439, 444, 445, 448, 451, 540, 546, 547, 577, 593, 609, 611, 617, 618, 623, 651, 655, 660, 697, 699, 700, 701, 702, 706, 707, 708, 710, 712, 714, 715, 720, 727, 731, 732, 756, 760, 884, 890, 894, 895, 900, 913, 914, 917, 918, 919, 922, 924, 925, 927, 929, 932, 933, 935, 945, 947, 953, 957, 959, 961, 963, 965, 978, 988, 1000, 1010, 1011, 1017, 1018, 1029, 1030, 1032, 1109, 1110, 1112, 1121, 1140, 1141, 1198, 1199, 1211, 1213, 1216, 1231, 1248, 1281, 1292, 1307, 1308, 1309, 1357, 1359, 1365, 1370, 1373, 1377, 1379, 1382, 1392, 1400, 1404, 1405, 1409, 1412, 1413, 1417, 1472, 1475, 1493, 1496, 1497, 1503, 1505, 1523, 1549, 1575, 1607, 1632, 1633, 1637, 1639, 1643, 1645, 1726, 1729, 1748, 1749, 1776, 1777, 1781, 1783, 1793, 1794, 1795, 1796, 1984, 1994, 2036, 2037, 2042, 2307, 2406, 2429, 2534, 2538, 2541, 2662, 2663, 2666, 2691, 2790, 2819, 2848, 2918, 2920, 3046, 3074, 3174, 3202, 3302, 3330, 3360, 3430, 3437, 3458, 3664, 3792, 4125, 4160, 4327, 4351, 4608, 4816, 5024, 5025, 5026, 5029, 5033, 5034, 5035, 5036, 5038, 5043, 5047, 5051, 5053, 5056, 5058, 5059, 5070, 5071, 5074, 5076, 5077, 5081, 5082, 5086, 5087, 5090, 5094, 5095, 5102, 5107, 5108, 5120, 5167, 5171, 5176, 5194, 5196, 5229, 5231, 5234, 5261, 5290, 5311, 5441, 5500, 5501, 5511, 5551, 5556, 5573, 5598, 5610, 5616, 5623, 5741, 5742, 5760, 5810, 5815, 5825, 5836, 5845, 5846, 5868, 5869, 5941, 6147, 6153, 7428, 7439, 7441, 7452, 7456, 7457, 7458, 7462, 7555, 7564, 7837, 7935, 8125, 8126, 8127, 8128, 8175, 8189, 8190, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8208, 8209, 8210, 8218, 8219, 8228, 8232, 8233, 8239, 8242, 8249, 8250, 8257, 8259, 8260, 8270, 8275, 8282, 8287, 8450, 8458, 8459, 8460, 8461, 8462, 8464, 8465, 8466, 8467, 8469, 8473, 8474, 8475, 8476, 8477, 8484, 8488, 8490, 8492, 8493, 8494, 8495, 8496, 8497, 8499, 8500, 8505, 8509, 8517, 8518, 8519, 8520, 8521, 8544, 8548, 8553, 8556, 8557, 8558, 8559, 8560, 8564, 8569, 8572, 8573, 8574, 8722, 8725, 8726, 8727, 8739, 8744, 8746, 8758, 8764, 8868, 8897, 8899, 8959, 9075, 9076, 9082, 9213, 9585, 9587, 10088, 10089, 10094, 10095, 10098, 10099, 10100, 10101, 10133, 10134, 10187, 10189, 10201, 10539, 10540, 10741, 10744, 10745, 10799, 11397, 11406, 11410, 11412, 11416, 11418, 11422, 11423, 11426, 11427, 11428, 11429, 11430, 11432, 11436, 11450, 11462, 11466, 11468, 11472, 11474, 11576, 11577, 11599, 11601, 11604, 11605, 11613, 11840, 12034, 12035, 12295, 12308, 12309, 12339, 12448, 12755, 12756, 20022, 20031, 42192, 42193, 42194, 42195, 42196, 42198, 42199, 42201, 42202, 42204, 42205, 42207, 42208, 42209, 42210, 42211, 42214, 42215, 42218, 42219, 42220, 42222, 42224, 42226, 42227, 42228, 42232, 42233, 42237, 42239, 42510, 42564, 42567, 42719, 42731, 42735, 42801, 42842, 42858, 42862, 42872, 42889, 42892, 42904, 42905, 42911, 42923, 42930, 42931, 42932, 43826, 43829, 43837, 43847, 43848, 43854, 43858, 43866, 43893, 43905, 43907, 43923, 43945, 43946, 43951, 64422, 64423, 64424, 64425, 64426, 64427, 64428, 64429, 64830, 64831, 65072, 65101, 65102, 65103, 65112, 65128, 65165, 65166, 65257, 65258, 65259, 65260, 65282, 65284, 65285, 65286, 65287, 65290, 65291, 65293, 65294, 65295, 65296, 65297, 65298, 65299, 65300, 65301, 65302, 65303, 65304, 65305, 65308, 65309, 65310, 65312, 65313, 65314, 65315, 65316, 65317, 65318, 65319, 65320, 65321, 65322, 65323, 65324, 65325, 65326, 65327, 65328, 65329, 65330, 65331, 65332, 65333, 65334, 65335, 65336, 65337, 65338, 65339, 65340, 65341, 65342, 65343, 65344, 65345, 65346, 65347, 65348, 65349, 65350, 65351, 65352, 65353, 65354, 65355, 65356, 65357, 65358, 65359, 65360, 65361, 65362, 65363, 65364, 65365, 65366, 65367, 65368, 65369, 65370, 65371, 65372, 65373, 65512, 66178, 66182, 66183, 66186, 66192, 66194, 66197, 66198, 66199, 66203, 66208, 66209, 66210, 66213, 66219, 66224, 66225, 66226, 66228, 66255, 66293, 66305, 66306, 66313, 66321, 66325, 66327, 66330, 66335, 66336, 66338, 66564, 66581, 66587, 66592, 66604, 66621, 66632, 66740, 66754, 66766, 66770, 66794, 66806, 66835, 66838, 66840, 66844, 66845, 66853, 66854, 66855, 68176, 70864, 71430, 71434, 71438, 71439, 71840, 71842, 71843, 71844, 71846, 71849, 71852, 71854, 71855, 71858, 71861, 71864, 71867, 71868, 71872, 71873, 71874, 71875, 71876, 71878, 71880, 71882, 71884, 71893, 71894, 71895, 71896, 71900, 71904, 71909, 71910, 71913, 71916, 71919, 71922, 93960, 93962, 93974, 93992, 94005, 94010, 94011, 94015, 94016, 94018, 94019, 94033, 94034, 119060, 119149, 119302, 119309, 119311, 119314, 119315, 119318, 119338, 119350, 119351, 119354, 119355, 119808, 119809, 119810, 119811, 119812, 119813, 119814, 119815, 119816, 119817, 119818, 119819, 119820, 119821, 119822, 119823, 119824, 119825, 119826, 119827, 119828, 119829, 119830, 119831, 119832, 119833, 119834, 119835, 119836, 119837, 119838, 119839, 119840, 119841, 119842, 119843, 119844, 119845, 119847, 119848, 119849, 119850, 119851, 119852, 119853, 119854, 119855, 119856, 119857, 119858, 119859, 119860, 119861, 119862, 119863, 119864, 119865, 119866, 119867, 119868, 119869, 119870, 119871, 119872, 119873, 119874, 119875, 119876, 119877, 119878, 119879, 119880, 119881, 119882, 119883, 119884, 119885, 119886, 119887, 119888, 119889, 119890, 119891, 119892, 119894, 119895, 119896, 119897, 119899, 119900, 119901, 119902, 119903, 119904, 119905, 119906, 119907, 119908, 119909, 119910, 119911, 119912, 119913, 119914, 119915, 119916, 119917, 119918, 119919, 119920, 119921, 119922, 119923, 119924, 119925, 119926, 119927, 119928, 119929, 119930, 119931, 119932, 119933, 119934, 119935, 119936, 119937, 119938, 119939, 119940, 119941, 119942, 119943, 119944, 119945, 119946, 119947, 119948, 119949, 119951, 119952, 119953, 119954, 119955, 119956, 119957, 119958, 119959, 119960, 119961, 119962, 119963, 119964, 119966, 119967, 119970, 119973, 119974, 119977, 119978, 119979, 119980, 119982, 119983, 119984, 119985, 119986, 119987, 119988, 119989, 119990, 119991, 119992, 119993, 119995, 119997, 119998, 119999, 120000, 120001, 120003, 120005, 120006, 120007, 120008, 120009, 120010, 120011, 120012, 120013, 120014, 120015, 120016, 120017, 120018, 120019, 120020, 120021, 120022, 120023, 120024, 120025, 120026, 120027, 120028, 120029, 120030, 120031, 120032, 120033, 120034, 120035, 120036, 120037, 120038, 120039, 120040, 120041, 120042, 120043, 120044, 120045, 120046, 120047, 120048, 120049, 120050, 120051, 120052, 120053, 120055, 120056, 120057, 120058, 120059, 120060, 120061, 120062, 120063, 120064, 120065, 120066, 120067, 120068, 120069, 120071, 120072, 120073, 120074, 120077, 120078, 120079, 120080, 120081, 120082, 120083, 120084, 120086, 120087, 120088, 120089, 120090, 120091, 120092, 120094, 120095, 120096, 120097, 120098, 120099, 120100, 120101, 120102, 120103, 120104, 120105, 120107, 120108, 120109, 120110, 120111, 120112, 120113, 120114, 120115, 120116, 120117, 120118, 120119, 120120, 120121, 120123, 120124, 120125, 120126, 120128, 120129, 120130, 120131, 120132, 120134, 120138, 120139, 120140, 120141, 120142, 120143, 120144, 120146, 120147, 120148, 120149, 120150, 120151, 120152, 120153, 120154, 120155, 120156, 120157, 120159, 120160, 120161, 120162, 120163, 120164, 120165, 120166, 120167, 120168, 120169, 120170, 120171, 120172, 120173, 120174, 120175, 120176, 120177, 120178, 120179, 120180, 120181, 120182, 120183, 120184, 120185, 120186, 120187, 120188, 120189, 120190, 120191, 120192, 120193, 120194, 120195, 120196, 120197, 120198, 120199, 120200, 120201, 120202, 120203, 120204, 120205, 120206, 120207, 120208, 120209, 120211, 120212, 120213, 120214, 120215, 120216, 120217, 120218, 120219, 120220, 120221, 120222, 120223, 120224, 120225, 120226, 120227, 120228, 120229, 120230, 120231, 120232, 120233, 120234, 120235, 120236, 120237, 120238, 120239, 120240, 120241, 120242, 120243, 120244, 120245, 120246, 120247, 120248, 120249, 120250, 120251, 120252, 120253, 120254, 120255, 120256, 120257, 120258, 120259, 120260, 120261, 120263, 120264, 120265, 120266, 120267, 120268, 120269, 120270, 120271, 120272, 120273, 120274, 120275, 120276, 120277, 120278, 120279, 120280, 120281, 120282, 120283, 120284, 120285, 120286, 120287, 120288, 120289, 120290, 120291, 120292, 120293, 120294, 120295, 120296, 120297, 120298, 120299, 120300, 120301, 120302, 120303, 120304, 120305, 120306, 120307, 120308, 120309, 120310, 120311, 120312, 120313, 120315, 120316, 120317, 120318, 120319, 120320, 120321, 120322, 120323, 120324, 120325, 120326, 120327, 120328, 120329, 120330, 120331, 120332, 120333, 120334, 120335, 120336, 120337, 120338, 120339, 120340, 120341, 120342, 120343, 120344, 120345, 120346, 120347, 120348, 120349, 120350, 120351, 120352, 120353, 120354, 120355, 120356, 120357, 120358, 120359, 120360, 120361, 120362, 120363, 120364, 120365, 120367, 120368, 120369, 120370, 120371, 120372, 120373, 120374, 120375, 120376, 120377, 120378, 120379, 120380, 120381, 120382, 120383, 120384, 120385, 120386, 120387, 120388, 120389, 120390, 120391, 120392, 120393, 120394, 120395, 120396, 120397, 120398, 120399, 120400, 120401, 120402, 120403, 120404, 120405, 120406, 120407, 120408, 120409, 120410, 120411, 120412, 120413, 120414, 120415, 120416, 120417, 120419, 120420, 120421, 120422, 120423, 120424, 120425, 120426, 120427, 120428, 120429, 120430, 120431, 120432, 120433, 120434, 120435, 120436, 120437, 120438, 120439, 120440, 120441, 120442, 120443, 120444, 120445, 120446, 120447, 120448, 120449, 120450, 120451, 120452, 120453, 120454, 120455, 120456, 120457, 120458, 120459, 120460, 120461, 120462, 120463, 120464, 120465, 120466, 120467, 120468, 120469, 120471, 120472, 120473, 120474, 120475, 120476, 120477, 120478, 120479, 120480, 120481, 120482, 120483, 120484, 120488, 120489, 120492, 120493, 120494, 120496, 120497, 120499, 120500, 120502, 120504, 120507, 120508, 120510, 120514, 120516, 120522, 120526, 120528, 120530, 120532, 120534, 120544, 120546, 120547, 120550, 120551, 120552, 120554, 120555, 120557, 120558, 120560, 120562, 120565, 120566, 120568, 120572, 120574, 120580, 120584, 120586, 120588, 120590, 120592, 120602, 120604, 120605, 120608, 120609, 120610, 120612, 120613, 120615, 120616, 120618, 120620, 120623, 120624, 120626, 120630, 120632, 120638, 120642, 120644, 120646, 120648, 120650, 120660, 120662, 120663, 120666, 120667, 120668, 120670, 120671, 120673, 120674, 120676, 120678, 120681, 120682, 120684, 120688, 120690, 120696, 120700, 120702, 120704, 120706, 120708, 120718, 120720, 120721, 120724, 120725, 120726, 120728, 120729, 120731, 120732, 120734, 120736, 120739, 120740, 120742, 120746, 120748, 120754, 120758, 120760, 120762, 120764, 120766, 120776, 120778, 120782, 120783, 120784, 120785, 120786, 120787, 120788, 120789, 120790, 120791, 120792, 120793, 120794, 120795, 120796, 120797, 120798, 120799, 120800, 120801, 120802, 120803, 120804, 120805, 120806, 120807, 120808, 120809, 120810, 120811, 120812, 120813, 120814, 120815, 120816, 120817, 120818, 120819, 120820, 120821, 120822, 120823, 120824, 120825, 120826, 120827, 120828, 120829, 120830, 120831, 125127, 125131, 126464, 126500, 126564, 126592, 126596, 128844, 128872, 130032, 130033, 130034, 130035, 130036, 130037, 130038, 130039, 130040, 130041}, - With: []rune{44, 102, 98, 103, 82, 50, 51, 53, 115, 73, 33, 51, 56, 56, 63, 97, 103, 121, 105, 105, 119, 117, 121, 63, 96, 96, 96, 96, 96, 60, 62, 94, 94, 96, 96, 96, 58, 45, 105, 126, 96, 58, 96, 105, 59, 74, 96, 65, 66, 69, 90, 72, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 89, 70, 50, 99, 106, 67, 77, 83, 73, 74, 115, 105, 106, 119, 86, 118, 89, 121, 104, 101, 73, 105, 51, 100, 71, 113, 87, 119, 85, 83, 79, 96, 96, 119, 113, 113, 104, 110, 110, 117, 103, 102, 111, 58, 108, 58, 108, 118, 96, 108, 111, 96, 44, 108, 111, 46, 108, 111, 86, 44, 42, 111, 111, 45, 111, 46, 73, 111, 86, 46, 46, 58, 58, 79, 108, 96, 96, 95, 58, 111, 63, 79, 56, 57, 111, 57, 56, 58, 111, 56, 79, 79, 57, 111, 111, 111, 111, 111, 111, 111, 111, 57, 111, 111, 111, 111, 111, 121, 111, 85, 79, 68, 82, 84, 105, 89, 65, 74, 69, 63, 87, 77, 72, 89, 71, 104, 90, 52, 98, 82, 87, 83, 86, 83, 76, 67, 80, 75, 100, 54, 71, 66, 61, 86, 62, 60, 96, 85, 80, 100, 98, 74, 76, 50, 120, 72, 120, 82, 98, 70, 65, 68, 68, 77, 66, 88, 120, 32, 60, 88, 73, 96, 75, 77, 58, 43, 47, 58, 58, 99, 111, 111, 117, 118, 119, 122, 114, 103, 121, 102, 121, 96, 105, 96, 126, 96, 96, 96, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 45, 45, 45, 44, 96, 46, 32, 32, 32, 96, 60, 62, 47, 45, 47, 42, 126, 58, 32, 67, 103, 72, 72, 72, 104, 73, 73, 76, 108, 78, 80, 81, 82, 82, 82, 90, 90, 75, 66, 67, 101, 101, 69, 70, 77, 111, 105, 121, 68, 100, 101, 105, 106, 73, 86, 88, 76, 67, 68, 77, 105, 118, 120, 73, 99, 100, 45, 47, 92, 42, 73, 118, 85, 58, 126, 84, 118, 85, 69, 105, 112, 97, 73, 47, 88, 40, 41, 60, 62, 40, 41, 123, 125, 43, 45, 47, 92, 84, 120, 120, 92, 47, 92, 120, 114, 72, 73, 75, 77, 78, 79, 111, 80, 112, 67, 99, 84, 89, 88, 45, 47, 57, 51, 76, 54, 86, 69, 73, 33, 79, 81, 88, 61, 92, 47, 79, 40, 41, 47, 61, 47, 92, 92, 47, 66, 80, 100, 68, 84, 71, 75, 74, 67, 90, 70, 77, 78, 76, 83, 82, 86, 72, 87, 88, 89, 65, 69, 73, 79, 85, 46, 44, 58, 61, 46, 50, 105, 86, 63, 50, 115, 50, 51, 57, 38, 58, 96, 70, 102, 117, 51, 74, 88, 66, 101, 102, 111, 114, 114, 117, 117, 121, 105, 114, 119, 122, 118, 115, 99, 111, 111, 111, 111, 111, 111, 111, 111, 40, 41, 58, 95, 95, 95, 45, 92, 108, 108, 111, 111, 111, 111, 34, 36, 37, 38, 96, 42, 43, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 73, 66, 69, 70, 124, 88, 79, 80, 83, 84, 43, 65, 66, 67, 70, 79, 77, 84, 89, 88, 72, 90, 66, 67, 124, 77, 84, 88, 56, 42, 108, 88, 79, 67, 76, 83, 111, 99, 115, 82, 79, 85, 55, 111, 117, 78, 79, 75, 67, 86, 70, 76, 88, 46, 79, 118, 119, 119, 119, 86, 70, 76, 89, 69, 90, 57, 69, 52, 76, 79, 85, 53, 84, 118, 115, 70, 105, 122, 55, 111, 51, 57, 54, 57, 111, 117, 121, 79, 90, 87, 67, 88, 87, 67, 86, 84, 76, 73, 82, 83, 51, 62, 65, 85, 89, 96, 96, 123, 46, 51, 86, 92, 55, 70, 82, 76, 60, 62, 47, 92, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 67, 68, 71, 74, 75, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 102, 104, 105, 106, 107, 108, 110, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 73, 74, 75, 76, 77, 79, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 105, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 70, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 108, 56, 108, 111, 111, 108, 111, 67, 84, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57}, - Locale: "_common", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 184, Hi: 383, Stride: 199}, - {Lo: 388, Hi: 397, Stride: 9}, - {Lo: 422, Hi: 423, Stride: 1}, - {Lo: 439, Hi: 444, Stride: 5}, - {Lo: 445, Hi: 451, Stride: 3}, - {Lo: 540, Hi: 546, Stride: 6}, - {Lo: 547, Hi: 577, Stride: 30}, - {Lo: 593, Hi: 609, Stride: 16}, - {Lo: 611, Hi: 617, Stride: 6}, - {Lo: 618, Hi: 623, Stride: 5}, - {Lo: 651, Hi: 655, Stride: 4}, - {Lo: 660, Hi: 697, Stride: 37}, - {Lo: 699, Hi: 702, Stride: 1}, - {Lo: 706, Hi: 708, Stride: 1}, - {Lo: 710, Hi: 714, Stride: 2}, - {Lo: 715, Hi: 720, Stride: 5}, - {Lo: 727, Hi: 731, Stride: 4}, - {Lo: 732, Hi: 756, Stride: 24}, - {Lo: 760, Hi: 884, Stride: 124}, - {Lo: 890, Hi: 894, Stride: 4}, - {Lo: 895, Hi: 900, Stride: 5}, - {Lo: 913, Hi: 914, Stride: 1}, - {Lo: 917, Hi: 919, Stride: 1}, - {Lo: 922, Hi: 924, Stride: 2}, - {Lo: 925, Hi: 929, Stride: 2}, - {Lo: 932, Hi: 933, Stride: 1}, - {Lo: 935, Hi: 945, Stride: 10}, - {Lo: 947, Hi: 953, Stride: 6}, - {Lo: 957, Hi: 965, Stride: 2}, - {Lo: 978, Hi: 988, Stride: 10}, - {Lo: 1000, Hi: 1010, Stride: 10}, - {Lo: 1011, Hi: 1017, Stride: 6}, - {Lo: 1018, Hi: 1029, Stride: 11}, - {Lo: 1030, Hi: 1032, Stride: 2}, - {Lo: 1109, Hi: 1110, Stride: 1}, - {Lo: 1112, Hi: 1121, Stride: 9}, - {Lo: 1140, Hi: 1141, Stride: 1}, - {Lo: 1198, Hi: 1199, Stride: 1}, - {Lo: 1211, Hi: 1213, Stride: 2}, - {Lo: 1216, Hi: 1231, Stride: 15}, - {Lo: 1248, Hi: 1281, Stride: 33}, - {Lo: 1292, Hi: 1307, Stride: 15}, - {Lo: 1308, Hi: 1309, Stride: 1}, - {Lo: 1357, Hi: 1359, Stride: 2}, - {Lo: 1365, Hi: 1370, Stride: 5}, - {Lo: 1373, Hi: 1377, Stride: 4}, - {Lo: 1379, Hi: 1382, Stride: 3}, - {Lo: 1392, Hi: 1400, Stride: 8}, - {Lo: 1404, Hi: 1405, Stride: 1}, - {Lo: 1409, Hi: 1412, Stride: 3}, - {Lo: 1413, Hi: 1417, Stride: 4}, - {Lo: 1472, Hi: 1475, Stride: 3}, - {Lo: 1493, Hi: 1496, Stride: 3}, - {Lo: 1497, Hi: 1503, Stride: 6}, - {Lo: 1505, Hi: 1523, Stride: 18}, - {Lo: 1549, Hi: 1575, Stride: 26}, - {Lo: 1607, Hi: 1632, Stride: 25}, - {Lo: 1633, Hi: 1637, Stride: 4}, - {Lo: 1639, Hi: 1643, Stride: 4}, - {Lo: 1645, Hi: 1726, Stride: 81}, - {Lo: 1729, Hi: 1748, Stride: 19}, - {Lo: 1749, Hi: 1776, Stride: 27}, - {Lo: 1777, Hi: 1781, Stride: 4}, - {Lo: 1783, Hi: 1793, Stride: 10}, - {Lo: 1794, Hi: 1796, Stride: 1}, - {Lo: 1984, Hi: 1994, Stride: 10}, - {Lo: 2036, Hi: 2037, Stride: 1}, - {Lo: 2042, Hi: 2307, Stride: 265}, - {Lo: 2406, Hi: 2429, Stride: 23}, - {Lo: 2534, Hi: 2538, Stride: 4}, - {Lo: 2541, Hi: 2662, Stride: 121}, - {Lo: 2663, Hi: 2666, Stride: 3}, - {Lo: 2691, Hi: 2790, Stride: 99}, - {Lo: 2819, Hi: 2848, Stride: 29}, - {Lo: 2918, Hi: 2920, Stride: 2}, - {Lo: 3046, Hi: 3074, Stride: 28}, - {Lo: 3174, Hi: 3202, Stride: 28}, - {Lo: 3302, Hi: 3330, Stride: 28}, - {Lo: 3360, Hi: 3430, Stride: 70}, - {Lo: 3437, Hi: 3458, Stride: 21}, - {Lo: 3664, Hi: 3792, Stride: 128}, - {Lo: 4125, Hi: 4160, Stride: 35}, - {Lo: 4327, Hi: 4351, Stride: 24}, - {Lo: 4608, Hi: 5024, Stride: 208}, - {Lo: 5025, Hi: 5026, Stride: 1}, - {Lo: 5029, Hi: 5033, Stride: 4}, - {Lo: 5034, Hi: 5036, Stride: 1}, - {Lo: 5038, Hi: 5043, Stride: 5}, - {Lo: 5047, Hi: 5051, Stride: 4}, - {Lo: 5053, Hi: 5056, Stride: 3}, - {Lo: 5058, Hi: 5059, Stride: 1}, - {Lo: 5070, Hi: 5071, Stride: 1}, - {Lo: 5074, Hi: 5076, Stride: 2}, - {Lo: 5077, Hi: 5081, Stride: 4}, - {Lo: 5082, Hi: 5086, Stride: 4}, - {Lo: 5087, Hi: 5090, Stride: 3}, - {Lo: 5094, Hi: 5095, Stride: 1}, - {Lo: 5102, Hi: 5107, Stride: 5}, - {Lo: 5108, Hi: 5120, Stride: 12}, - {Lo: 5167, Hi: 5171, Stride: 4}, - {Lo: 5176, Hi: 5194, Stride: 18}, - {Lo: 5196, Hi: 5229, Stride: 33}, - {Lo: 5231, Hi: 5234, Stride: 3}, - {Lo: 5261, Hi: 5290, Stride: 29}, - {Lo: 5311, Hi: 5441, Stride: 130}, - {Lo: 5500, Hi: 5501, Stride: 1}, - {Lo: 5511, Hi: 5551, Stride: 40}, - {Lo: 5556, Hi: 5573, Stride: 17}, - {Lo: 5598, Hi: 5610, Stride: 12}, - {Lo: 5616, Hi: 5623, Stride: 7}, - {Lo: 5741, Hi: 5742, Stride: 1}, - {Lo: 5760, Hi: 5810, Stride: 50}, - {Lo: 5815, Hi: 5825, Stride: 10}, - {Lo: 5836, Hi: 5845, Stride: 9}, - {Lo: 5846, Hi: 5868, Stride: 22}, - {Lo: 5869, Hi: 5941, Stride: 72}, - {Lo: 6147, Hi: 6153, Stride: 6}, - {Lo: 7428, Hi: 7439, Stride: 11}, - {Lo: 7441, Hi: 7452, Stride: 11}, - {Lo: 7456, Hi: 7458, Stride: 1}, - {Lo: 7462, Hi: 7555, Stride: 93}, - {Lo: 7564, Hi: 7837, Stride: 273}, - {Lo: 7935, Hi: 8125, Stride: 190}, - {Lo: 8126, Hi: 8128, Stride: 1}, - {Lo: 8175, Hi: 8189, Stride: 14}, - {Lo: 8190, Hi: 8192, Stride: 2}, - {Lo: 8193, Hi: 8202, Stride: 1}, - {Lo: 8208, Hi: 8210, Stride: 1}, - {Lo: 8218, Hi: 8219, Stride: 1}, - {Lo: 8228, Hi: 8232, Stride: 4}, - {Lo: 8233, Hi: 8239, Stride: 6}, - {Lo: 8242, Hi: 8249, Stride: 7}, - {Lo: 8250, Hi: 8257, Stride: 7}, - {Lo: 8259, Hi: 8260, Stride: 1}, - {Lo: 8270, Hi: 8275, Stride: 5}, - {Lo: 8282, Hi: 8287, Stride: 5}, - {Lo: 8450, Hi: 8458, Stride: 8}, - {Lo: 8459, Hi: 8462, Stride: 1}, - {Lo: 8464, Hi: 8467, Stride: 1}, - {Lo: 8469, Hi: 8473, Stride: 4}, - {Lo: 8474, Hi: 8477, Stride: 1}, - {Lo: 8484, Hi: 8488, Stride: 4}, - {Lo: 8490, Hi: 8492, Stride: 2}, - {Lo: 8493, Hi: 8497, Stride: 1}, - {Lo: 8499, Hi: 8500, Stride: 1}, - {Lo: 8505, Hi: 8509, Stride: 4}, - {Lo: 8517, Hi: 8521, Stride: 1}, - {Lo: 8544, Hi: 8548, Stride: 4}, - {Lo: 8553, Hi: 8556, Stride: 3}, - {Lo: 8557, Hi: 8560, Stride: 1}, - {Lo: 8564, Hi: 8569, Stride: 5}, - {Lo: 8572, Hi: 8574, Stride: 1}, - {Lo: 8722, Hi: 8725, Stride: 3}, - {Lo: 8726, Hi: 8727, Stride: 1}, - {Lo: 8739, Hi: 8744, Stride: 5}, - {Lo: 8746, Hi: 8758, Stride: 12}, - {Lo: 8764, Hi: 8868, Stride: 104}, - {Lo: 8897, Hi: 8899, Stride: 2}, - {Lo: 8959, Hi: 9075, Stride: 116}, - {Lo: 9076, Hi: 9082, Stride: 6}, - {Lo: 9213, Hi: 9585, Stride: 372}, - {Lo: 9587, Hi: 10088, Stride: 501}, - {Lo: 10089, Hi: 10094, Stride: 5}, - {Lo: 10095, Hi: 10098, Stride: 3}, - {Lo: 10099, Hi: 10101, Stride: 1}, - {Lo: 10133, Hi: 10134, Stride: 1}, - {Lo: 10187, Hi: 10189, Stride: 2}, - {Lo: 10201, Hi: 10539, Stride: 338}, - {Lo: 10540, Hi: 10741, Stride: 201}, - {Lo: 10744, Hi: 10745, Stride: 1}, - {Lo: 10799, Hi: 11397, Stride: 598}, - {Lo: 11406, Hi: 11410, Stride: 4}, - {Lo: 11412, Hi: 11416, Stride: 4}, - {Lo: 11418, Hi: 11422, Stride: 4}, - {Lo: 11423, Hi: 11426, Stride: 3}, - {Lo: 11427, Hi: 11430, Stride: 1}, - {Lo: 11432, Hi: 11436, Stride: 4}, - {Lo: 11450, Hi: 11462, Stride: 12}, - {Lo: 11466, Hi: 11468, Stride: 2}, - {Lo: 11472, Hi: 11474, Stride: 2}, - {Lo: 11576, Hi: 11577, Stride: 1}, - {Lo: 11599, Hi: 11601, Stride: 2}, - {Lo: 11604, Hi: 11605, Stride: 1}, - {Lo: 11613, Hi: 11840, Stride: 227}, - {Lo: 12034, Hi: 12035, Stride: 1}, - {Lo: 12295, Hi: 12308, Stride: 13}, - {Lo: 12309, Hi: 12339, Stride: 30}, - {Lo: 12448, Hi: 12755, Stride: 307}, - {Lo: 12756, Hi: 20022, Stride: 7266}, - {Lo: 20031, Hi: 42192, Stride: 22161}, - {Lo: 42193, Hi: 42196, Stride: 1}, - {Lo: 42198, Hi: 42199, Stride: 1}, - {Lo: 42201, Hi: 42202, Stride: 1}, - {Lo: 42204, Hi: 42205, Stride: 1}, - {Lo: 42207, Hi: 42211, Stride: 1}, - {Lo: 42214, Hi: 42215, Stride: 1}, - {Lo: 42218, Hi: 42220, Stride: 1}, - {Lo: 42222, Hi: 42226, Stride: 2}, - {Lo: 42227, Hi: 42228, Stride: 1}, - {Lo: 42232, Hi: 42233, Stride: 1}, - {Lo: 42237, Hi: 42239, Stride: 2}, - {Lo: 42510, Hi: 42564, Stride: 54}, - {Lo: 42567, Hi: 42719, Stride: 152}, - {Lo: 42731, Hi: 42735, Stride: 4}, - {Lo: 42801, Hi: 42842, Stride: 41}, - {Lo: 42858, Hi: 42862, Stride: 4}, - {Lo: 42872, Hi: 42889, Stride: 17}, - {Lo: 42892, Hi: 42904, Stride: 12}, - {Lo: 42905, Hi: 42911, Stride: 6}, - {Lo: 42923, Hi: 42930, Stride: 7}, - {Lo: 42931, Hi: 42932, Stride: 1}, - {Lo: 43826, Hi: 43829, Stride: 3}, - {Lo: 43837, Hi: 43847, Stride: 10}, - {Lo: 43848, Hi: 43854, Stride: 6}, - {Lo: 43858, Hi: 43866, Stride: 8}, - {Lo: 43893, Hi: 43905, Stride: 12}, - {Lo: 43907, Hi: 43923, Stride: 16}, - {Lo: 43945, Hi: 43946, Stride: 1}, - {Lo: 43951, Hi: 64422, Stride: 20471}, - {Lo: 64423, Hi: 64429, Stride: 1}, - {Lo: 64830, Hi: 64831, Stride: 1}, - {Lo: 65072, Hi: 65101, Stride: 29}, - {Lo: 65102, Hi: 65103, Stride: 1}, - {Lo: 65112, Hi: 65128, Stride: 16}, - {Lo: 65165, Hi: 65166, Stride: 1}, - {Lo: 65257, Hi: 65260, Stride: 1}, - {Lo: 65282, Hi: 65284, Stride: 2}, - {Lo: 65285, Hi: 65287, Stride: 1}, - {Lo: 65290, Hi: 65291, Stride: 1}, - {Lo: 65293, Hi: 65305, Stride: 1}, - {Lo: 65308, Hi: 65310, Stride: 1}, - {Lo: 65312, Hi: 65373, Stride: 1}, - {Lo: 65512, Hi: 65512, Stride: 1}, +func newAmbiguousTableMap() map[string]*AmbiguousTable { + return map[string]*AmbiguousTable{ + "_common": { + Confusable: []rune{184, 383, 388, 397, 422, 423, 439, 444, 445, 448, 451, 540, 546, 547, 577, 593, 609, 611, 617, 618, 623, 651, 655, 660, 697, 699, 700, 701, 702, 706, 707, 708, 710, 712, 714, 715, 720, 727, 731, 732, 756, 760, 884, 890, 894, 895, 900, 913, 914, 917, 918, 919, 922, 924, 925, 927, 929, 932, 933, 935, 945, 947, 953, 957, 959, 961, 963, 965, 978, 988, 1000, 1010, 1011, 1017, 1018, 1029, 1030, 1032, 1109, 1110, 1112, 1121, 1140, 1141, 1198, 1199, 1211, 1213, 1216, 1231, 1248, 1281, 1292, 1307, 1308, 1309, 1357, 1359, 1365, 1370, 1373, 1377, 1379, 1382, 1392, 1400, 1404, 1405, 1409, 1412, 1413, 1417, 1472, 1475, 1493, 1496, 1497, 1503, 1505, 1523, 1549, 1575, 1607, 1632, 1633, 1637, 1639, 1643, 1645, 1726, 1729, 1748, 1749, 1776, 1777, 1781, 1783, 1793, 1794, 1795, 1796, 1984, 1994, 2036, 2037, 2042, 2307, 2406, 2429, 2534, 2538, 2541, 2662, 2663, 2666, 2691, 2790, 2819, 2848, 2918, 2920, 3046, 3074, 3174, 3202, 3302, 3330, 3360, 3430, 3437, 3458, 3664, 3792, 4125, 4160, 4327, 4351, 4608, 4816, 5024, 5025, 5026, 5029, 5033, 5034, 5035, 5036, 5038, 5043, 5047, 5051, 5053, 5056, 5058, 5059, 5070, 5071, 5074, 5076, 5077, 5081, 5082, 5086, 5087, 5090, 5094, 5095, 5102, 5107, 5108, 5120, 5167, 5171, 5176, 5194, 5196, 5229, 5231, 5234, 5261, 5290, 5311, 5441, 5500, 5501, 5511, 5551, 5556, 5573, 5598, 5610, 5616, 5623, 5741, 5742, 5760, 5810, 5815, 5825, 5836, 5845, 5846, 5868, 5869, 5941, 6147, 6153, 7428, 7439, 7441, 7452, 7456, 7457, 7458, 7462, 7555, 7564, 7837, 7935, 8125, 8126, 8127, 8128, 8175, 8189, 8190, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8208, 8209, 8210, 8218, 8219, 8228, 8232, 8233, 8239, 8242, 8249, 8250, 8257, 8259, 8260, 8270, 8275, 8282, 8287, 8450, 8458, 8459, 8460, 8461, 8462, 8464, 8465, 8466, 8467, 8469, 8473, 8474, 8475, 8476, 8477, 8484, 8488, 8490, 8492, 8493, 8494, 8495, 8496, 8497, 8499, 8500, 8505, 8509, 8517, 8518, 8519, 8520, 8521, 8544, 8548, 8553, 8556, 8557, 8558, 8559, 8560, 8564, 8569, 8572, 8573, 8574, 8722, 8725, 8726, 8727, 8739, 8744, 8746, 8758, 8764, 8868, 8897, 8899, 8959, 9075, 9076, 9082, 9213, 9585, 9587, 10088, 10089, 10094, 10095, 10098, 10099, 10100, 10101, 10133, 10134, 10187, 10189, 10201, 10539, 10540, 10741, 10744, 10745, 10799, 11397, 11406, 11410, 11412, 11416, 11418, 11422, 11423, 11426, 11427, 11428, 11429, 11430, 11432, 11436, 11450, 11462, 11466, 11468, 11472, 11474, 11576, 11577, 11599, 11601, 11604, 11605, 11613, 11840, 12034, 12035, 12295, 12308, 12309, 12339, 12448, 12755, 12756, 20022, 20031, 42192, 42193, 42194, 42195, 42196, 42198, 42199, 42201, 42202, 42204, 42205, 42207, 42208, 42209, 42210, 42211, 42214, 42215, 42218, 42219, 42220, 42222, 42224, 42226, 42227, 42228, 42232, 42233, 42237, 42239, 42510, 42564, 42567, 42719, 42731, 42735, 42801, 42842, 42858, 42862, 42872, 42889, 42892, 42904, 42905, 42911, 42923, 42930, 42931, 42932, 43826, 43829, 43837, 43847, 43848, 43854, 43858, 43866, 43893, 43905, 43907, 43923, 43945, 43946, 43951, 64422, 64423, 64424, 64425, 64426, 64427, 64428, 64429, 64830, 64831, 65072, 65101, 65102, 65103, 65112, 65128, 65165, 65166, 65257, 65258, 65259, 65260, 65282, 65284, 65285, 65286, 65287, 65290, 65291, 65293, 65294, 65295, 65296, 65297, 65298, 65299, 65300, 65301, 65302, 65303, 65304, 65305, 65308, 65309, 65310, 65312, 65313, 65314, 65315, 65316, 65317, 65318, 65319, 65320, 65321, 65322, 65323, 65324, 65325, 65326, 65327, 65328, 65329, 65330, 65331, 65332, 65333, 65334, 65335, 65336, 65337, 65338, 65339, 65340, 65341, 65342, 65343, 65344, 65345, 65346, 65347, 65348, 65349, 65350, 65351, 65352, 65353, 65354, 65355, 65356, 65357, 65358, 65359, 65360, 65361, 65362, 65363, 65364, 65365, 65366, 65367, 65368, 65369, 65370, 65371, 65372, 65373, 65512, 66178, 66182, 66183, 66186, 66192, 66194, 66197, 66198, 66199, 66203, 66208, 66209, 66210, 66213, 66219, 66224, 66225, 66226, 66228, 66255, 66293, 66305, 66306, 66313, 66321, 66325, 66327, 66330, 66335, 66336, 66338, 66564, 66581, 66587, 66592, 66604, 66621, 66632, 66740, 66754, 66766, 66770, 66794, 66806, 66835, 66838, 66840, 66844, 66845, 66853, 66854, 66855, 68176, 70864, 71430, 71434, 71438, 71439, 71840, 71842, 71843, 71844, 71846, 71849, 71852, 71854, 71855, 71858, 71861, 71864, 71867, 71868, 71872, 71873, 71874, 71875, 71876, 71878, 71880, 71882, 71884, 71893, 71894, 71895, 71896, 71900, 71904, 71909, 71910, 71913, 71916, 71919, 71922, 93960, 93962, 93974, 93992, 94005, 94010, 94011, 94015, 94016, 94018, 94019, 94033, 94034, 119060, 119149, 119302, 119309, 119311, 119314, 119315, 119318, 119338, 119350, 119351, 119354, 119355, 119808, 119809, 119810, 119811, 119812, 119813, 119814, 119815, 119816, 119817, 119818, 119819, 119820, 119821, 119822, 119823, 119824, 119825, 119826, 119827, 119828, 119829, 119830, 119831, 119832, 119833, 119834, 119835, 119836, 119837, 119838, 119839, 119840, 119841, 119842, 119843, 119844, 119845, 119847, 119848, 119849, 119850, 119851, 119852, 119853, 119854, 119855, 119856, 119857, 119858, 119859, 119860, 119861, 119862, 119863, 119864, 119865, 119866, 119867, 119868, 119869, 119870, 119871, 119872, 119873, 119874, 119875, 119876, 119877, 119878, 119879, 119880, 119881, 119882, 119883, 119884, 119885, 119886, 119887, 119888, 119889, 119890, 119891, 119892, 119894, 119895, 119896, 119897, 119899, 119900, 119901, 119902, 119903, 119904, 119905, 119906, 119907, 119908, 119909, 119910, 119911, 119912, 119913, 119914, 119915, 119916, 119917, 119918, 119919, 119920, 119921, 119922, 119923, 119924, 119925, 119926, 119927, 119928, 119929, 119930, 119931, 119932, 119933, 119934, 119935, 119936, 119937, 119938, 119939, 119940, 119941, 119942, 119943, 119944, 119945, 119946, 119947, 119948, 119949, 119951, 119952, 119953, 119954, 119955, 119956, 119957, 119958, 119959, 119960, 119961, 119962, 119963, 119964, 119966, 119967, 119970, 119973, 119974, 119977, 119978, 119979, 119980, 119982, 119983, 119984, 119985, 119986, 119987, 119988, 119989, 119990, 119991, 119992, 119993, 119995, 119997, 119998, 119999, 120000, 120001, 120003, 120005, 120006, 120007, 120008, 120009, 120010, 120011, 120012, 120013, 120014, 120015, 120016, 120017, 120018, 120019, 120020, 120021, 120022, 120023, 120024, 120025, 120026, 120027, 120028, 120029, 120030, 120031, 120032, 120033, 120034, 120035, 120036, 120037, 120038, 120039, 120040, 120041, 120042, 120043, 120044, 120045, 120046, 120047, 120048, 120049, 120050, 120051, 120052, 120053, 120055, 120056, 120057, 120058, 120059, 120060, 120061, 120062, 120063, 120064, 120065, 120066, 120067, 120068, 120069, 120071, 120072, 120073, 120074, 120077, 120078, 120079, 120080, 120081, 120082, 120083, 120084, 120086, 120087, 120088, 120089, 120090, 120091, 120092, 120094, 120095, 120096, 120097, 120098, 120099, 120100, 120101, 120102, 120103, 120104, 120105, 120107, 120108, 120109, 120110, 120111, 120112, 120113, 120114, 120115, 120116, 120117, 120118, 120119, 120120, 120121, 120123, 120124, 120125, 120126, 120128, 120129, 120130, 120131, 120132, 120134, 120138, 120139, 120140, 120141, 120142, 120143, 120144, 120146, 120147, 120148, 120149, 120150, 120151, 120152, 120153, 120154, 120155, 120156, 120157, 120159, 120160, 120161, 120162, 120163, 120164, 120165, 120166, 120167, 120168, 120169, 120170, 120171, 120172, 120173, 120174, 120175, 120176, 120177, 120178, 120179, 120180, 120181, 120182, 120183, 120184, 120185, 120186, 120187, 120188, 120189, 120190, 120191, 120192, 120193, 120194, 120195, 120196, 120197, 120198, 120199, 120200, 120201, 120202, 120203, 120204, 120205, 120206, 120207, 120208, 120209, 120211, 120212, 120213, 120214, 120215, 120216, 120217, 120218, 120219, 120220, 120221, 120222, 120223, 120224, 120225, 120226, 120227, 120228, 120229, 120230, 120231, 120232, 120233, 120234, 120235, 120236, 120237, 120238, 120239, 120240, 120241, 120242, 120243, 120244, 120245, 120246, 120247, 120248, 120249, 120250, 120251, 120252, 120253, 120254, 120255, 120256, 120257, 120258, 120259, 120260, 120261, 120263, 120264, 120265, 120266, 120267, 120268, 120269, 120270, 120271, 120272, 120273, 120274, 120275, 120276, 120277, 120278, 120279, 120280, 120281, 120282, 120283, 120284, 120285, 120286, 120287, 120288, 120289, 120290, 120291, 120292, 120293, 120294, 120295, 120296, 120297, 120298, 120299, 120300, 120301, 120302, 120303, 120304, 120305, 120306, 120307, 120308, 120309, 120310, 120311, 120312, 120313, 120315, 120316, 120317, 120318, 120319, 120320, 120321, 120322, 120323, 120324, 120325, 120326, 120327, 120328, 120329, 120330, 120331, 120332, 120333, 120334, 120335, 120336, 120337, 120338, 120339, 120340, 120341, 120342, 120343, 120344, 120345, 120346, 120347, 120348, 120349, 120350, 120351, 120352, 120353, 120354, 120355, 120356, 120357, 120358, 120359, 120360, 120361, 120362, 120363, 120364, 120365, 120367, 120368, 120369, 120370, 120371, 120372, 120373, 120374, 120375, 120376, 120377, 120378, 120379, 120380, 120381, 120382, 120383, 120384, 120385, 120386, 120387, 120388, 120389, 120390, 120391, 120392, 120393, 120394, 120395, 120396, 120397, 120398, 120399, 120400, 120401, 120402, 120403, 120404, 120405, 120406, 120407, 120408, 120409, 120410, 120411, 120412, 120413, 120414, 120415, 120416, 120417, 120419, 120420, 120421, 120422, 120423, 120424, 120425, 120426, 120427, 120428, 120429, 120430, 120431, 120432, 120433, 120434, 120435, 120436, 120437, 120438, 120439, 120440, 120441, 120442, 120443, 120444, 120445, 120446, 120447, 120448, 120449, 120450, 120451, 120452, 120453, 120454, 120455, 120456, 120457, 120458, 120459, 120460, 120461, 120462, 120463, 120464, 120465, 120466, 120467, 120468, 120469, 120471, 120472, 120473, 120474, 120475, 120476, 120477, 120478, 120479, 120480, 120481, 120482, 120483, 120484, 120488, 120489, 120492, 120493, 120494, 120496, 120497, 120499, 120500, 120502, 120504, 120507, 120508, 120510, 120514, 120516, 120522, 120526, 120528, 120530, 120532, 120534, 120544, 120546, 120547, 120550, 120551, 120552, 120554, 120555, 120557, 120558, 120560, 120562, 120565, 120566, 120568, 120572, 120574, 120580, 120584, 120586, 120588, 120590, 120592, 120602, 120604, 120605, 120608, 120609, 120610, 120612, 120613, 120615, 120616, 120618, 120620, 120623, 120624, 120626, 120630, 120632, 120638, 120642, 120644, 120646, 120648, 120650, 120660, 120662, 120663, 120666, 120667, 120668, 120670, 120671, 120673, 120674, 120676, 120678, 120681, 120682, 120684, 120688, 120690, 120696, 120700, 120702, 120704, 120706, 120708, 120718, 120720, 120721, 120724, 120725, 120726, 120728, 120729, 120731, 120732, 120734, 120736, 120739, 120740, 120742, 120746, 120748, 120754, 120758, 120760, 120762, 120764, 120766, 120776, 120778, 120782, 120783, 120784, 120785, 120786, 120787, 120788, 120789, 120790, 120791, 120792, 120793, 120794, 120795, 120796, 120797, 120798, 120799, 120800, 120801, 120802, 120803, 120804, 120805, 120806, 120807, 120808, 120809, 120810, 120811, 120812, 120813, 120814, 120815, 120816, 120817, 120818, 120819, 120820, 120821, 120822, 120823, 120824, 120825, 120826, 120827, 120828, 120829, 120830, 120831, 125127, 125131, 126464, 126500, 126564, 126592, 126596, 128844, 128872, 130032, 130033, 130034, 130035, 130036, 130037, 130038, 130039, 130040, 130041}, + With: []rune{44, 102, 98, 103, 82, 50, 51, 53, 115, 73, 33, 51, 56, 56, 63, 97, 103, 121, 105, 105, 119, 117, 121, 63, 96, 96, 96, 96, 96, 60, 62, 94, 94, 96, 96, 96, 58, 45, 105, 126, 96, 58, 96, 105, 59, 74, 96, 65, 66, 69, 90, 72, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 89, 70, 50, 99, 106, 67, 77, 83, 73, 74, 115, 105, 106, 119, 86, 118, 89, 121, 104, 101, 73, 105, 51, 100, 71, 113, 87, 119, 85, 83, 79, 96, 96, 119, 113, 113, 104, 110, 110, 117, 103, 102, 111, 58, 108, 58, 108, 118, 96, 108, 111, 96, 44, 108, 111, 46, 108, 111, 86, 44, 42, 111, 111, 45, 111, 46, 73, 111, 86, 46, 46, 58, 58, 79, 108, 96, 96, 95, 58, 111, 63, 79, 56, 57, 111, 57, 56, 58, 111, 56, 79, 79, 57, 111, 111, 111, 111, 111, 111, 111, 111, 57, 111, 111, 111, 111, 111, 121, 111, 85, 79, 68, 82, 84, 105, 89, 65, 74, 69, 63, 87, 77, 72, 89, 71, 104, 90, 52, 98, 82, 87, 83, 86, 83, 76, 67, 80, 75, 100, 54, 71, 66, 61, 86, 62, 60, 96, 85, 80, 100, 98, 74, 76, 50, 120, 72, 120, 82, 98, 70, 65, 68, 68, 77, 66, 88, 120, 32, 60, 88, 73, 96, 75, 77, 58, 43, 47, 58, 58, 99, 111, 111, 117, 118, 119, 122, 114, 103, 121, 102, 121, 96, 105, 96, 126, 96, 96, 96, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 45, 45, 45, 44, 96, 46, 32, 32, 32, 96, 60, 62, 47, 45, 47, 42, 126, 58, 32, 67, 103, 72, 72, 72, 104, 73, 73, 76, 108, 78, 80, 81, 82, 82, 82, 90, 90, 75, 66, 67, 101, 101, 69, 70, 77, 111, 105, 121, 68, 100, 101, 105, 106, 73, 86, 88, 76, 67, 68, 77, 105, 118, 120, 73, 99, 100, 45, 47, 92, 42, 73, 118, 85, 58, 126, 84, 118, 85, 69, 105, 112, 97, 73, 47, 88, 40, 41, 60, 62, 40, 41, 123, 125, 43, 45, 47, 92, 84, 120, 120, 92, 47, 92, 120, 114, 72, 73, 75, 77, 78, 79, 111, 80, 112, 67, 99, 84, 89, 88, 45, 47, 57, 51, 76, 54, 86, 69, 73, 33, 79, 81, 88, 61, 92, 47, 79, 40, 41, 47, 61, 47, 92, 92, 47, 66, 80, 100, 68, 84, 71, 75, 74, 67, 90, 70, 77, 78, 76, 83, 82, 86, 72, 87, 88, 89, 65, 69, 73, 79, 85, 46, 44, 58, 61, 46, 50, 105, 86, 63, 50, 115, 50, 51, 57, 38, 58, 96, 70, 102, 117, 51, 74, 88, 66, 101, 102, 111, 114, 114, 117, 117, 121, 105, 114, 119, 122, 118, 115, 99, 111, 111, 111, 111, 111, 111, 111, 111, 40, 41, 58, 95, 95, 95, 45, 92, 108, 108, 111, 111, 111, 111, 34, 36, 37, 38, 96, 42, 43, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 73, 66, 69, 70, 124, 88, 79, 80, 83, 84, 43, 65, 66, 67, 70, 79, 77, 84, 89, 88, 72, 90, 66, 67, 124, 77, 84, 88, 56, 42, 108, 88, 79, 67, 76, 83, 111, 99, 115, 82, 79, 85, 55, 111, 117, 78, 79, 75, 67, 86, 70, 76, 88, 46, 79, 118, 119, 119, 119, 86, 70, 76, 89, 69, 90, 57, 69, 52, 76, 79, 85, 53, 84, 118, 115, 70, 105, 122, 55, 111, 51, 57, 54, 57, 111, 117, 121, 79, 90, 87, 67, 88, 87, 67, 86, 84, 76, 73, 82, 83, 51, 62, 65, 85, 89, 96, 96, 123, 46, 51, 86, 92, 55, 70, 82, 76, 60, 62, 47, 92, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 67, 68, 71, 74, 75, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 102, 104, 105, 106, 107, 108, 110, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 74, 75, 76, 77, 78, 79, 80, 81, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 68, 69, 70, 71, 73, 74, 75, 76, 77, 79, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 73, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 105, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 65, 66, 69, 90, 72, 73, 75, 77, 78, 79, 80, 84, 89, 88, 97, 121, 105, 118, 111, 112, 111, 117, 112, 70, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57, 108, 56, 108, 111, 111, 108, 111, 67, 84, 79, 73, 50, 51, 52, 53, 54, 55, 56, 57}, + Locale: "_common", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 184, Hi: 383, Stride: 199}, + {Lo: 388, Hi: 397, Stride: 9}, + {Lo: 422, Hi: 423, Stride: 1}, + {Lo: 439, Hi: 444, Stride: 5}, + {Lo: 445, Hi: 451, Stride: 3}, + {Lo: 540, Hi: 546, Stride: 6}, + {Lo: 547, Hi: 577, Stride: 30}, + {Lo: 593, Hi: 609, Stride: 16}, + {Lo: 611, Hi: 617, Stride: 6}, + {Lo: 618, Hi: 623, Stride: 5}, + {Lo: 651, Hi: 655, Stride: 4}, + {Lo: 660, Hi: 697, Stride: 37}, + {Lo: 699, Hi: 702, Stride: 1}, + {Lo: 706, Hi: 708, Stride: 1}, + {Lo: 710, Hi: 714, Stride: 2}, + {Lo: 715, Hi: 720, Stride: 5}, + {Lo: 727, Hi: 731, Stride: 4}, + {Lo: 732, Hi: 756, Stride: 24}, + {Lo: 760, Hi: 884, Stride: 124}, + {Lo: 890, Hi: 894, Stride: 4}, + {Lo: 895, Hi: 900, Stride: 5}, + {Lo: 913, Hi: 914, Stride: 1}, + {Lo: 917, Hi: 919, Stride: 1}, + {Lo: 922, Hi: 924, Stride: 2}, + {Lo: 925, Hi: 929, Stride: 2}, + {Lo: 932, Hi: 933, Stride: 1}, + {Lo: 935, Hi: 945, Stride: 10}, + {Lo: 947, Hi: 953, Stride: 6}, + {Lo: 957, Hi: 965, Stride: 2}, + {Lo: 978, Hi: 988, Stride: 10}, + {Lo: 1000, Hi: 1010, Stride: 10}, + {Lo: 1011, Hi: 1017, Stride: 6}, + {Lo: 1018, Hi: 1029, Stride: 11}, + {Lo: 1030, Hi: 1032, Stride: 2}, + {Lo: 1109, Hi: 1110, Stride: 1}, + {Lo: 1112, Hi: 1121, Stride: 9}, + {Lo: 1140, Hi: 1141, Stride: 1}, + {Lo: 1198, Hi: 1199, Stride: 1}, + {Lo: 1211, Hi: 1213, Stride: 2}, + {Lo: 1216, Hi: 1231, Stride: 15}, + {Lo: 1248, Hi: 1281, Stride: 33}, + {Lo: 1292, Hi: 1307, Stride: 15}, + {Lo: 1308, Hi: 1309, Stride: 1}, + {Lo: 1357, Hi: 1359, Stride: 2}, + {Lo: 1365, Hi: 1370, Stride: 5}, + {Lo: 1373, Hi: 1377, Stride: 4}, + {Lo: 1379, Hi: 1382, Stride: 3}, + {Lo: 1392, Hi: 1400, Stride: 8}, + {Lo: 1404, Hi: 1405, Stride: 1}, + {Lo: 1409, Hi: 1412, Stride: 3}, + {Lo: 1413, Hi: 1417, Stride: 4}, + {Lo: 1472, Hi: 1475, Stride: 3}, + {Lo: 1493, Hi: 1496, Stride: 3}, + {Lo: 1497, Hi: 1503, Stride: 6}, + {Lo: 1505, Hi: 1523, Stride: 18}, + {Lo: 1549, Hi: 1575, Stride: 26}, + {Lo: 1607, Hi: 1632, Stride: 25}, + {Lo: 1633, Hi: 1637, Stride: 4}, + {Lo: 1639, Hi: 1643, Stride: 4}, + {Lo: 1645, Hi: 1726, Stride: 81}, + {Lo: 1729, Hi: 1748, Stride: 19}, + {Lo: 1749, Hi: 1776, Stride: 27}, + {Lo: 1777, Hi: 1781, Stride: 4}, + {Lo: 1783, Hi: 1793, Stride: 10}, + {Lo: 1794, Hi: 1796, Stride: 1}, + {Lo: 1984, Hi: 1994, Stride: 10}, + {Lo: 2036, Hi: 2037, Stride: 1}, + {Lo: 2042, Hi: 2307, Stride: 265}, + {Lo: 2406, Hi: 2429, Stride: 23}, + {Lo: 2534, Hi: 2538, Stride: 4}, + {Lo: 2541, Hi: 2662, Stride: 121}, + {Lo: 2663, Hi: 2666, Stride: 3}, + {Lo: 2691, Hi: 2790, Stride: 99}, + {Lo: 2819, Hi: 2848, Stride: 29}, + {Lo: 2918, Hi: 2920, Stride: 2}, + {Lo: 3046, Hi: 3074, Stride: 28}, + {Lo: 3174, Hi: 3202, Stride: 28}, + {Lo: 3302, Hi: 3330, Stride: 28}, + {Lo: 3360, Hi: 3430, Stride: 70}, + {Lo: 3437, Hi: 3458, Stride: 21}, + {Lo: 3664, Hi: 3792, Stride: 128}, + {Lo: 4125, Hi: 4160, Stride: 35}, + {Lo: 4327, Hi: 4351, Stride: 24}, + {Lo: 4608, Hi: 5024, Stride: 208}, + {Lo: 5025, Hi: 5026, Stride: 1}, + {Lo: 5029, Hi: 5033, Stride: 4}, + {Lo: 5034, Hi: 5036, Stride: 1}, + {Lo: 5038, Hi: 5043, Stride: 5}, + {Lo: 5047, Hi: 5051, Stride: 4}, + {Lo: 5053, Hi: 5056, Stride: 3}, + {Lo: 5058, Hi: 5059, Stride: 1}, + {Lo: 5070, Hi: 5071, Stride: 1}, + {Lo: 5074, Hi: 5076, Stride: 2}, + {Lo: 5077, Hi: 5081, Stride: 4}, + {Lo: 5082, Hi: 5086, Stride: 4}, + {Lo: 5087, Hi: 5090, Stride: 3}, + {Lo: 5094, Hi: 5095, Stride: 1}, + {Lo: 5102, Hi: 5107, Stride: 5}, + {Lo: 5108, Hi: 5120, Stride: 12}, + {Lo: 5167, Hi: 5171, Stride: 4}, + {Lo: 5176, Hi: 5194, Stride: 18}, + {Lo: 5196, Hi: 5229, Stride: 33}, + {Lo: 5231, Hi: 5234, Stride: 3}, + {Lo: 5261, Hi: 5290, Stride: 29}, + {Lo: 5311, Hi: 5441, Stride: 130}, + {Lo: 5500, Hi: 5501, Stride: 1}, + {Lo: 5511, Hi: 5551, Stride: 40}, + {Lo: 5556, Hi: 5573, Stride: 17}, + {Lo: 5598, Hi: 5610, Stride: 12}, + {Lo: 5616, Hi: 5623, Stride: 7}, + {Lo: 5741, Hi: 5742, Stride: 1}, + {Lo: 5760, Hi: 5810, Stride: 50}, + {Lo: 5815, Hi: 5825, Stride: 10}, + {Lo: 5836, Hi: 5845, Stride: 9}, + {Lo: 5846, Hi: 5868, Stride: 22}, + {Lo: 5869, Hi: 5941, Stride: 72}, + {Lo: 6147, Hi: 6153, Stride: 6}, + {Lo: 7428, Hi: 7439, Stride: 11}, + {Lo: 7441, Hi: 7452, Stride: 11}, + {Lo: 7456, Hi: 7458, Stride: 1}, + {Lo: 7462, Hi: 7555, Stride: 93}, + {Lo: 7564, Hi: 7837, Stride: 273}, + {Lo: 7935, Hi: 8125, Stride: 190}, + {Lo: 8126, Hi: 8128, Stride: 1}, + {Lo: 8175, Hi: 8189, Stride: 14}, + {Lo: 8190, Hi: 8192, Stride: 2}, + {Lo: 8193, Hi: 8202, Stride: 1}, + {Lo: 8208, Hi: 8210, Stride: 1}, + {Lo: 8218, Hi: 8219, Stride: 1}, + {Lo: 8228, Hi: 8232, Stride: 4}, + {Lo: 8233, Hi: 8239, Stride: 6}, + {Lo: 8242, Hi: 8249, Stride: 7}, + {Lo: 8250, Hi: 8257, Stride: 7}, + {Lo: 8259, Hi: 8260, Stride: 1}, + {Lo: 8270, Hi: 8275, Stride: 5}, + {Lo: 8282, Hi: 8287, Stride: 5}, + {Lo: 8450, Hi: 8458, Stride: 8}, + {Lo: 8459, Hi: 8462, Stride: 1}, + {Lo: 8464, Hi: 8467, Stride: 1}, + {Lo: 8469, Hi: 8473, Stride: 4}, + {Lo: 8474, Hi: 8477, Stride: 1}, + {Lo: 8484, Hi: 8488, Stride: 4}, + {Lo: 8490, Hi: 8492, Stride: 2}, + {Lo: 8493, Hi: 8497, Stride: 1}, + {Lo: 8499, Hi: 8500, Stride: 1}, + {Lo: 8505, Hi: 8509, Stride: 4}, + {Lo: 8517, Hi: 8521, Stride: 1}, + {Lo: 8544, Hi: 8548, Stride: 4}, + {Lo: 8553, Hi: 8556, Stride: 3}, + {Lo: 8557, Hi: 8560, Stride: 1}, + {Lo: 8564, Hi: 8569, Stride: 5}, + {Lo: 8572, Hi: 8574, Stride: 1}, + {Lo: 8722, Hi: 8725, Stride: 3}, + {Lo: 8726, Hi: 8727, Stride: 1}, + {Lo: 8739, Hi: 8744, Stride: 5}, + {Lo: 8746, Hi: 8758, Stride: 12}, + {Lo: 8764, Hi: 8868, Stride: 104}, + {Lo: 8897, Hi: 8899, Stride: 2}, + {Lo: 8959, Hi: 9075, Stride: 116}, + {Lo: 9076, Hi: 9082, Stride: 6}, + {Lo: 9213, Hi: 9585, Stride: 372}, + {Lo: 9587, Hi: 10088, Stride: 501}, + {Lo: 10089, Hi: 10094, Stride: 5}, + {Lo: 10095, Hi: 10098, Stride: 3}, + {Lo: 10099, Hi: 10101, Stride: 1}, + {Lo: 10133, Hi: 10134, Stride: 1}, + {Lo: 10187, Hi: 10189, Stride: 2}, + {Lo: 10201, Hi: 10539, Stride: 338}, + {Lo: 10540, Hi: 10741, Stride: 201}, + {Lo: 10744, Hi: 10745, Stride: 1}, + {Lo: 10799, Hi: 11397, Stride: 598}, + {Lo: 11406, Hi: 11410, Stride: 4}, + {Lo: 11412, Hi: 11416, Stride: 4}, + {Lo: 11418, Hi: 11422, Stride: 4}, + {Lo: 11423, Hi: 11426, Stride: 3}, + {Lo: 11427, Hi: 11430, Stride: 1}, + {Lo: 11432, Hi: 11436, Stride: 4}, + {Lo: 11450, Hi: 11462, Stride: 12}, + {Lo: 11466, Hi: 11468, Stride: 2}, + {Lo: 11472, Hi: 11474, Stride: 2}, + {Lo: 11576, Hi: 11577, Stride: 1}, + {Lo: 11599, Hi: 11601, Stride: 2}, + {Lo: 11604, Hi: 11605, Stride: 1}, + {Lo: 11613, Hi: 11840, Stride: 227}, + {Lo: 12034, Hi: 12035, Stride: 1}, + {Lo: 12295, Hi: 12308, Stride: 13}, + {Lo: 12309, Hi: 12339, Stride: 30}, + {Lo: 12448, Hi: 12755, Stride: 307}, + {Lo: 12756, Hi: 20022, Stride: 7266}, + {Lo: 20031, Hi: 42192, Stride: 22161}, + {Lo: 42193, Hi: 42196, Stride: 1}, + {Lo: 42198, Hi: 42199, Stride: 1}, + {Lo: 42201, Hi: 42202, Stride: 1}, + {Lo: 42204, Hi: 42205, Stride: 1}, + {Lo: 42207, Hi: 42211, Stride: 1}, + {Lo: 42214, Hi: 42215, Stride: 1}, + {Lo: 42218, Hi: 42220, Stride: 1}, + {Lo: 42222, Hi: 42226, Stride: 2}, + {Lo: 42227, Hi: 42228, Stride: 1}, + {Lo: 42232, Hi: 42233, Stride: 1}, + {Lo: 42237, Hi: 42239, Stride: 2}, + {Lo: 42510, Hi: 42564, Stride: 54}, + {Lo: 42567, Hi: 42719, Stride: 152}, + {Lo: 42731, Hi: 42735, Stride: 4}, + {Lo: 42801, Hi: 42842, Stride: 41}, + {Lo: 42858, Hi: 42862, Stride: 4}, + {Lo: 42872, Hi: 42889, Stride: 17}, + {Lo: 42892, Hi: 42904, Stride: 12}, + {Lo: 42905, Hi: 42911, Stride: 6}, + {Lo: 42923, Hi: 42930, Stride: 7}, + {Lo: 42931, Hi: 42932, Stride: 1}, + {Lo: 43826, Hi: 43829, Stride: 3}, + {Lo: 43837, Hi: 43847, Stride: 10}, + {Lo: 43848, Hi: 43854, Stride: 6}, + {Lo: 43858, Hi: 43866, Stride: 8}, + {Lo: 43893, Hi: 43905, Stride: 12}, + {Lo: 43907, Hi: 43923, Stride: 16}, + {Lo: 43945, Hi: 43946, Stride: 1}, + {Lo: 43951, Hi: 64422, Stride: 20471}, + {Lo: 64423, Hi: 64429, Stride: 1}, + {Lo: 64830, Hi: 64831, Stride: 1}, + {Lo: 65072, Hi: 65101, Stride: 29}, + {Lo: 65102, Hi: 65103, Stride: 1}, + {Lo: 65112, Hi: 65128, Stride: 16}, + {Lo: 65165, Hi: 65166, Stride: 1}, + {Lo: 65257, Hi: 65260, Stride: 1}, + {Lo: 65282, Hi: 65284, Stride: 2}, + {Lo: 65285, Hi: 65287, Stride: 1}, + {Lo: 65290, Hi: 65291, Stride: 1}, + {Lo: 65293, Hi: 65305, Stride: 1}, + {Lo: 65308, Hi: 65310, Stride: 1}, + {Lo: 65312, Hi: 65373, Stride: 1}, + {Lo: 65512, Hi: 65512, Stride: 1}, + }, + R32: []unicode.Range32{ + {Lo: 66178, Hi: 66182, Stride: 4}, + {Lo: 66183, Hi: 66186, Stride: 3}, + {Lo: 66192, Hi: 66194, Stride: 2}, + {Lo: 66197, Hi: 66199, Stride: 1}, + {Lo: 66203, Hi: 66208, Stride: 5}, + {Lo: 66209, Hi: 66210, Stride: 1}, + {Lo: 66213, Hi: 66219, Stride: 6}, + {Lo: 66224, Hi: 66226, Stride: 1}, + {Lo: 66228, Hi: 66255, Stride: 27}, + {Lo: 66293, Hi: 66305, Stride: 12}, + {Lo: 66306, Hi: 66313, Stride: 7}, + {Lo: 66321, Hi: 66325, Stride: 4}, + {Lo: 66327, Hi: 66330, Stride: 3}, + {Lo: 66335, Hi: 66336, Stride: 1}, + {Lo: 66338, Hi: 66564, Stride: 226}, + {Lo: 66581, Hi: 66587, Stride: 6}, + {Lo: 66592, Hi: 66604, Stride: 12}, + {Lo: 66621, Hi: 66632, Stride: 11}, + {Lo: 66740, Hi: 66754, Stride: 14}, + {Lo: 66766, Hi: 66770, Stride: 4}, + {Lo: 66794, Hi: 66806, Stride: 12}, + {Lo: 66835, Hi: 66838, Stride: 3}, + {Lo: 66840, Hi: 66844, Stride: 4}, + {Lo: 66845, Hi: 66853, Stride: 8}, + {Lo: 66854, Hi: 66855, Stride: 1}, + {Lo: 68176, Hi: 70864, Stride: 2688}, + {Lo: 71430, Hi: 71438, Stride: 4}, + {Lo: 71439, Hi: 71840, Stride: 401}, + {Lo: 71842, Hi: 71844, Stride: 1}, + {Lo: 71846, Hi: 71852, Stride: 3}, + {Lo: 71854, Hi: 71855, Stride: 1}, + {Lo: 71858, Hi: 71867, Stride: 3}, + {Lo: 71868, Hi: 71872, Stride: 4}, + {Lo: 71873, Hi: 71876, Stride: 1}, + {Lo: 71878, Hi: 71884, Stride: 2}, + {Lo: 71893, Hi: 71896, Stride: 1}, + {Lo: 71900, Hi: 71904, Stride: 4}, + {Lo: 71909, Hi: 71910, Stride: 1}, + {Lo: 71913, Hi: 71922, Stride: 3}, + {Lo: 93960, Hi: 93962, Stride: 2}, + {Lo: 93974, Hi: 93992, Stride: 18}, + {Lo: 94005, Hi: 94010, Stride: 5}, + {Lo: 94011, Hi: 94015, Stride: 4}, + {Lo: 94016, Hi: 94018, Stride: 2}, + {Lo: 94019, Hi: 94033, Stride: 14}, + {Lo: 94034, Hi: 119060, Stride: 25026}, + {Lo: 119149, Hi: 119302, Stride: 153}, + {Lo: 119309, Hi: 119311, Stride: 2}, + {Lo: 119314, Hi: 119315, Stride: 1}, + {Lo: 119318, Hi: 119338, Stride: 20}, + {Lo: 119350, Hi: 119351, Stride: 1}, + {Lo: 119354, Hi: 119355, Stride: 1}, + {Lo: 119808, Hi: 119845, Stride: 1}, + {Lo: 119847, Hi: 119892, Stride: 1}, + {Lo: 119894, Hi: 119897, Stride: 1}, + {Lo: 119899, Hi: 119949, Stride: 1}, + {Lo: 119951, Hi: 119964, Stride: 1}, + {Lo: 119966, Hi: 119967, Stride: 1}, + {Lo: 119970, Hi: 119973, Stride: 3}, + {Lo: 119974, Hi: 119977, Stride: 3}, + {Lo: 119978, Hi: 119980, Stride: 1}, + {Lo: 119982, Hi: 119993, Stride: 1}, + {Lo: 119995, Hi: 119997, Stride: 2}, + {Lo: 119998, Hi: 120001, Stride: 1}, + {Lo: 120003, Hi: 120005, Stride: 2}, + {Lo: 120006, Hi: 120053, Stride: 1}, + {Lo: 120055, Hi: 120069, Stride: 1}, + {Lo: 120071, Hi: 120074, Stride: 1}, + {Lo: 120077, Hi: 120084, Stride: 1}, + {Lo: 120086, Hi: 120092, Stride: 1}, + {Lo: 120094, Hi: 120105, Stride: 1}, + {Lo: 120107, Hi: 120121, Stride: 1}, + {Lo: 120123, Hi: 120126, Stride: 1}, + {Lo: 120128, Hi: 120132, Stride: 1}, + {Lo: 120134, Hi: 120138, Stride: 4}, + {Lo: 120139, Hi: 120144, Stride: 1}, + {Lo: 120146, Hi: 120157, Stride: 1}, + {Lo: 120159, Hi: 120209, Stride: 1}, + {Lo: 120211, Hi: 120261, Stride: 1}, + {Lo: 120263, Hi: 120313, Stride: 1}, + {Lo: 120315, Hi: 120365, Stride: 1}, + {Lo: 120367, Hi: 120417, Stride: 1}, + {Lo: 120419, Hi: 120469, Stride: 1}, + {Lo: 120471, Hi: 120484, Stride: 1}, + {Lo: 120488, Hi: 120489, Stride: 1}, + {Lo: 120492, Hi: 120494, Stride: 1}, + {Lo: 120496, Hi: 120497, Stride: 1}, + {Lo: 120499, Hi: 120500, Stride: 1}, + {Lo: 120502, Hi: 120504, Stride: 2}, + {Lo: 120507, Hi: 120508, Stride: 1}, + {Lo: 120510, Hi: 120514, Stride: 4}, + {Lo: 120516, Hi: 120522, Stride: 6}, + {Lo: 120526, Hi: 120534, Stride: 2}, + {Lo: 120544, Hi: 120546, Stride: 2}, + {Lo: 120547, Hi: 120550, Stride: 3}, + {Lo: 120551, Hi: 120552, Stride: 1}, + {Lo: 120554, Hi: 120555, Stride: 1}, + {Lo: 120557, Hi: 120558, Stride: 1}, + {Lo: 120560, Hi: 120562, Stride: 2}, + {Lo: 120565, Hi: 120566, Stride: 1}, + {Lo: 120568, Hi: 120572, Stride: 4}, + {Lo: 120574, Hi: 120580, Stride: 6}, + {Lo: 120584, Hi: 120592, Stride: 2}, + {Lo: 120602, Hi: 120604, Stride: 2}, + {Lo: 120605, Hi: 120608, Stride: 3}, + {Lo: 120609, Hi: 120610, Stride: 1}, + {Lo: 120612, Hi: 120613, Stride: 1}, + {Lo: 120615, Hi: 120616, Stride: 1}, + {Lo: 120618, Hi: 120620, Stride: 2}, + {Lo: 120623, Hi: 120624, Stride: 1}, + {Lo: 120626, Hi: 120630, Stride: 4}, + {Lo: 120632, Hi: 120638, Stride: 6}, + {Lo: 120642, Hi: 120650, Stride: 2}, + {Lo: 120660, Hi: 120662, Stride: 2}, + {Lo: 120663, Hi: 120666, Stride: 3}, + {Lo: 120667, Hi: 120668, Stride: 1}, + {Lo: 120670, Hi: 120671, Stride: 1}, + {Lo: 120673, Hi: 120674, Stride: 1}, + {Lo: 120676, Hi: 120678, Stride: 2}, + {Lo: 120681, Hi: 120682, Stride: 1}, + {Lo: 120684, Hi: 120688, Stride: 4}, + {Lo: 120690, Hi: 120696, Stride: 6}, + {Lo: 120700, Hi: 120708, Stride: 2}, + {Lo: 120718, Hi: 120720, Stride: 2}, + {Lo: 120721, Hi: 120724, Stride: 3}, + {Lo: 120725, Hi: 120726, Stride: 1}, + {Lo: 120728, Hi: 120729, Stride: 1}, + {Lo: 120731, Hi: 120732, Stride: 1}, + {Lo: 120734, Hi: 120736, Stride: 2}, + {Lo: 120739, Hi: 120740, Stride: 1}, + {Lo: 120742, Hi: 120746, Stride: 4}, + {Lo: 120748, Hi: 120754, Stride: 6}, + {Lo: 120758, Hi: 120766, Stride: 2}, + {Lo: 120776, Hi: 120778, Stride: 2}, + {Lo: 120782, Hi: 120831, Stride: 1}, + {Lo: 125127, Hi: 125131, Stride: 4}, + {Lo: 126464, Hi: 126500, Stride: 36}, + {Lo: 126564, Hi: 126592, Stride: 28}, + {Lo: 126596, Hi: 128844, Stride: 2248}, + {Lo: 128872, Hi: 130032, Stride: 1160}, + {Lo: 130033, Hi: 130041, Stride: 1}, + }, + LatinOffset: 0, }, - R32: []unicode.Range32{ - {Lo: 66178, Hi: 66182, Stride: 4}, - {Lo: 66183, Hi: 66186, Stride: 3}, - {Lo: 66192, Hi: 66194, Stride: 2}, - {Lo: 66197, Hi: 66199, Stride: 1}, - {Lo: 66203, Hi: 66208, Stride: 5}, - {Lo: 66209, Hi: 66210, Stride: 1}, - {Lo: 66213, Hi: 66219, Stride: 6}, - {Lo: 66224, Hi: 66226, Stride: 1}, - {Lo: 66228, Hi: 66255, Stride: 27}, - {Lo: 66293, Hi: 66305, Stride: 12}, - {Lo: 66306, Hi: 66313, Stride: 7}, - {Lo: 66321, Hi: 66325, Stride: 4}, - {Lo: 66327, Hi: 66330, Stride: 3}, - {Lo: 66335, Hi: 66336, Stride: 1}, - {Lo: 66338, Hi: 66564, Stride: 226}, - {Lo: 66581, Hi: 66587, Stride: 6}, - {Lo: 66592, Hi: 66604, Stride: 12}, - {Lo: 66621, Hi: 66632, Stride: 11}, - {Lo: 66740, Hi: 66754, Stride: 14}, - {Lo: 66766, Hi: 66770, Stride: 4}, - {Lo: 66794, Hi: 66806, Stride: 12}, - {Lo: 66835, Hi: 66838, Stride: 3}, - {Lo: 66840, Hi: 66844, Stride: 4}, - {Lo: 66845, Hi: 66853, Stride: 8}, - {Lo: 66854, Hi: 66855, Stride: 1}, - {Lo: 68176, Hi: 70864, Stride: 2688}, - {Lo: 71430, Hi: 71438, Stride: 4}, - {Lo: 71439, Hi: 71840, Stride: 401}, - {Lo: 71842, Hi: 71844, Stride: 1}, - {Lo: 71846, Hi: 71852, Stride: 3}, - {Lo: 71854, Hi: 71855, Stride: 1}, - {Lo: 71858, Hi: 71867, Stride: 3}, - {Lo: 71868, Hi: 71872, Stride: 4}, - {Lo: 71873, Hi: 71876, Stride: 1}, - {Lo: 71878, Hi: 71884, Stride: 2}, - {Lo: 71893, Hi: 71896, Stride: 1}, - {Lo: 71900, Hi: 71904, Stride: 4}, - {Lo: 71909, Hi: 71910, Stride: 1}, - {Lo: 71913, Hi: 71922, Stride: 3}, - {Lo: 93960, Hi: 93962, Stride: 2}, - {Lo: 93974, Hi: 93992, Stride: 18}, - {Lo: 94005, Hi: 94010, Stride: 5}, - {Lo: 94011, Hi: 94015, Stride: 4}, - {Lo: 94016, Hi: 94018, Stride: 2}, - {Lo: 94019, Hi: 94033, Stride: 14}, - {Lo: 94034, Hi: 119060, Stride: 25026}, - {Lo: 119149, Hi: 119302, Stride: 153}, - {Lo: 119309, Hi: 119311, Stride: 2}, - {Lo: 119314, Hi: 119315, Stride: 1}, - {Lo: 119318, Hi: 119338, Stride: 20}, - {Lo: 119350, Hi: 119351, Stride: 1}, - {Lo: 119354, Hi: 119355, Stride: 1}, - {Lo: 119808, Hi: 119845, Stride: 1}, - {Lo: 119847, Hi: 119892, Stride: 1}, - {Lo: 119894, Hi: 119897, Stride: 1}, - {Lo: 119899, Hi: 119949, Stride: 1}, - {Lo: 119951, Hi: 119964, Stride: 1}, - {Lo: 119966, Hi: 119967, Stride: 1}, - {Lo: 119970, Hi: 119973, Stride: 3}, - {Lo: 119974, Hi: 119977, Stride: 3}, - {Lo: 119978, Hi: 119980, Stride: 1}, - {Lo: 119982, Hi: 119993, Stride: 1}, - {Lo: 119995, Hi: 119997, Stride: 2}, - {Lo: 119998, Hi: 120001, Stride: 1}, - {Lo: 120003, Hi: 120005, Stride: 2}, - {Lo: 120006, Hi: 120053, Stride: 1}, - {Lo: 120055, Hi: 120069, Stride: 1}, - {Lo: 120071, Hi: 120074, Stride: 1}, - {Lo: 120077, Hi: 120084, Stride: 1}, - {Lo: 120086, Hi: 120092, Stride: 1}, - {Lo: 120094, Hi: 120105, Stride: 1}, - {Lo: 120107, Hi: 120121, Stride: 1}, - {Lo: 120123, Hi: 120126, Stride: 1}, - {Lo: 120128, Hi: 120132, Stride: 1}, - {Lo: 120134, Hi: 120138, Stride: 4}, - {Lo: 120139, Hi: 120144, Stride: 1}, - {Lo: 120146, Hi: 120157, Stride: 1}, - {Lo: 120159, Hi: 120209, Stride: 1}, - {Lo: 120211, Hi: 120261, Stride: 1}, - {Lo: 120263, Hi: 120313, Stride: 1}, - {Lo: 120315, Hi: 120365, Stride: 1}, - {Lo: 120367, Hi: 120417, Stride: 1}, - {Lo: 120419, Hi: 120469, Stride: 1}, - {Lo: 120471, Hi: 120484, Stride: 1}, - {Lo: 120488, Hi: 120489, Stride: 1}, - {Lo: 120492, Hi: 120494, Stride: 1}, - {Lo: 120496, Hi: 120497, Stride: 1}, - {Lo: 120499, Hi: 120500, Stride: 1}, - {Lo: 120502, Hi: 120504, Stride: 2}, - {Lo: 120507, Hi: 120508, Stride: 1}, - {Lo: 120510, Hi: 120514, Stride: 4}, - {Lo: 120516, Hi: 120522, Stride: 6}, - {Lo: 120526, Hi: 120534, Stride: 2}, - {Lo: 120544, Hi: 120546, Stride: 2}, - {Lo: 120547, Hi: 120550, Stride: 3}, - {Lo: 120551, Hi: 120552, Stride: 1}, - {Lo: 120554, Hi: 120555, Stride: 1}, - {Lo: 120557, Hi: 120558, Stride: 1}, - {Lo: 120560, Hi: 120562, Stride: 2}, - {Lo: 120565, Hi: 120566, Stride: 1}, - {Lo: 120568, Hi: 120572, Stride: 4}, - {Lo: 120574, Hi: 120580, Stride: 6}, - {Lo: 120584, Hi: 120592, Stride: 2}, - {Lo: 120602, Hi: 120604, Stride: 2}, - {Lo: 120605, Hi: 120608, Stride: 3}, - {Lo: 120609, Hi: 120610, Stride: 1}, - {Lo: 120612, Hi: 120613, Stride: 1}, - {Lo: 120615, Hi: 120616, Stride: 1}, - {Lo: 120618, Hi: 120620, Stride: 2}, - {Lo: 120623, Hi: 120624, Stride: 1}, - {Lo: 120626, Hi: 120630, Stride: 4}, - {Lo: 120632, Hi: 120638, Stride: 6}, - {Lo: 120642, Hi: 120650, Stride: 2}, - {Lo: 120660, Hi: 120662, Stride: 2}, - {Lo: 120663, Hi: 120666, Stride: 3}, - {Lo: 120667, Hi: 120668, Stride: 1}, - {Lo: 120670, Hi: 120671, Stride: 1}, - {Lo: 120673, Hi: 120674, Stride: 1}, - {Lo: 120676, Hi: 120678, Stride: 2}, - {Lo: 120681, Hi: 120682, Stride: 1}, - {Lo: 120684, Hi: 120688, Stride: 4}, - {Lo: 120690, Hi: 120696, Stride: 6}, - {Lo: 120700, Hi: 120708, Stride: 2}, - {Lo: 120718, Hi: 120720, Stride: 2}, - {Lo: 120721, Hi: 120724, Stride: 3}, - {Lo: 120725, Hi: 120726, Stride: 1}, - {Lo: 120728, Hi: 120729, Stride: 1}, - {Lo: 120731, Hi: 120732, Stride: 1}, - {Lo: 120734, Hi: 120736, Stride: 2}, - {Lo: 120739, Hi: 120740, Stride: 1}, - {Lo: 120742, Hi: 120746, Stride: 4}, - {Lo: 120748, Hi: 120754, Stride: 6}, - {Lo: 120758, Hi: 120766, Stride: 2}, - {Lo: 120776, Hi: 120778, Stride: 2}, - {Lo: 120782, Hi: 120831, Stride: 1}, - {Lo: 125127, Hi: 125131, Stride: 4}, - {Lo: 126464, Hi: 126500, Stride: 36}, - {Lo: 126564, Hi: 126592, Stride: 28}, - {Lo: 126596, Hi: 128844, Stride: 2248}, - {Lo: 128872, Hi: 130032, Stride: 1160}, - {Lo: 130033, Hi: 130041, Stride: 1}, - }, - LatinOffset: 0, }, - }, - "_default": { - Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "_default", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 160, Hi: 180, Stride: 20}, - {Lo: 215, Hi: 305, Stride: 90}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "_default": { + Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "_default", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 160, Hi: 180, Stride: 20}, + {Lo: 215, Hi: 305, Stride: 90}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8216, Stride: 5}, + {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "cs": { - Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "cs", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 305, Stride: 125}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "cs": { + Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "cs", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 305, Stride: 125}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 0, }, - R32: []unicode.Range32{}, - LatinOffset: 0, }, - }, - "de": { - Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "de", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 305, Stride: 125}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "de": { + Confusable: []rune{180, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "de", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 305, Stride: 125}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 0, }, - R32: []unicode.Range32{}, - LatinOffset: 0, }, - }, - "es": { - Confusable: []rune{180, 215, 305, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "es", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 1009, Stride: 704}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8245, Stride: 34}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "es": { + Confusable: []rune{180, 215, 305, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "es", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 1009, Stride: 704}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8245, Stride: 34}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "fr": { - Confusable: []rune{215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "fr", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 215, Hi: 305, Stride: 90}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8245, Stride: 29}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "fr": { + Confusable: []rune{215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "fr", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 215, Hi: 305, Stride: 90}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8245, Stride: 29}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 0, }, - R32: []unicode.Range32{}, - LatinOffset: 0, }, - }, - "it": { - Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "it", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 160, Hi: 180, Stride: 20}, - {Lo: 215, Hi: 305, Stride: 90}, - {Lo: 921, Hi: 1009, Stride: 88}, - {Lo: 1040, Hi: 1042, Stride: 2}, - {Lo: 1045, Hi: 1047, Stride: 2}, - {Lo: 1050, Hi: 1052, Stride: 2}, - {Lo: 1053, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "it": { + Confusable: []rune{160, 180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "it", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 160, Hi: 180, Stride: 20}, + {Lo: 215, Hi: 305, Stride: 90}, + {Lo: 921, Hi: 1009, Stride: 88}, + {Lo: 1040, Hi: 1042, Stride: 2}, + {Lo: 1045, Hi: 1047, Stride: 2}, + {Lo: 1050, Hi: 1052, Stride: 2}, + {Lo: 1053, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8216, Stride: 5}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "ja": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 65281, 65283, 65292, 65306, 65307}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 33, 35, 44, 58, 59}, - Locale: "ja", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65307, Stride: 1}, + + "ja": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 65281, 65283, 65292, 65306, 65307}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 33, 35, 44, 58, 59}, + Locale: "ja", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8216, Stride: 5}, + {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65307, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "ko": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "ko", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8245, Stride: 34}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "ko": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "ko", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8245, Stride: 34}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "pl": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "pl", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "pl": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "pl", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "pt-BR": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "pt-BR", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8216, Hi: 8217, Stride: 1}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65283, Stride: 2}, - {Lo: 65288, Hi: 65289, Stride: 1}, - {Lo: 65292, Hi: 65306, Stride: 14}, - {Lo: 65307, Hi: 65311, Stride: 4}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "pt-BR": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "pt-BR", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8216, Hi: 8217, Stride: 1}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65283, Stride: 2}, + {Lo: 65288, Hi: 65289, Stride: 1}, + {Lo: 65292, Hi: 65306, Stride: 14}, + {Lo: 65307, Hi: 65311, Stride: 4}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "qps-ploc": { - Confusable: []rune{160, 180, 215, 305, 921, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 105, 73, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "qps-ploc", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 160, Hi: 180, Stride: 20}, - {Lo: 215, Hi: 305, Stride: 90}, - {Lo: 921, Hi: 1040, Stride: 119}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8216, Stride: 5}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "qps-ploc": { + Confusable: []rune{160, 180, 215, 305, 921, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 105, 73, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "qps-ploc", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 160, Hi: 180, Stride: 20}, + {Lo: 215, Hi: 305, Stride: 90}, + {Lo: 921, Hi: 1040, Stride: 119}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8216, Stride: 5}, + {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "ru": { - Confusable: []rune{180, 215, 305, 921, 1009, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{96, 120, 105, 73, 112, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "ru", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 8216, Stride: 7207}, - {Lo: 8217, Hi: 8245, Stride: 28}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "ru": { + Confusable: []rune{180, 215, 305, 921, 1009, 8216, 8217, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{96, 120, 105, 73, 112, 96, 96, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "ru", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 8216, Stride: 7207}, + {Lo: 8217, Hi: 8245, Stride: 28}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "tr": { - Confusable: []rune{160, 180, 215, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, - With: []rune{32, 96, 120, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, - Locale: "tr", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 160, Hi: 180, Stride: 20}, - {Lo: 215, Hi: 921, Stride: 706}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 8245, Stride: 34}, - {Lo: 12494, Hi: 65281, Stride: 52787}, - {Lo: 65283, Hi: 65288, Stride: 5}, - {Lo: 65289, Hi: 65292, Stride: 3}, - {Lo: 65306, Hi: 65307, Stride: 1}, - {Lo: 65311, Hi: 65374, Stride: 63}, + + "tr": { + Confusable: []rune{160, 180, 215, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 8245, 12494, 65281, 65283, 65288, 65289, 65292, 65306, 65307, 65311, 65374}, + With: []rune{32, 96, 120, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 96, 47, 33, 35, 40, 41, 44, 58, 59, 63, 126}, + Locale: "tr", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 160, Hi: 180, Stride: 20}, + {Lo: 215, Hi: 921, Stride: 706}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 8245, Stride: 34}, + {Lo: 12494, Hi: 65281, Stride: 52787}, + {Lo: 65283, Hi: 65288, Stride: 5}, + {Lo: 65289, Hi: 65292, Stride: 3}, + {Lo: 65306, Hi: 65307, Stride: 1}, + {Lo: 65311, Hi: 65374, Stride: 63}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "zh-hans": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8245, 12494, 65281, 65288, 65289, 65306, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 47, 33, 40, 41, 58, 126}, - Locale: "zh-hans", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8245, Hi: 12494, Stride: 4249}, - {Lo: 65281, Hi: 65288, Stride: 7}, - {Lo: 65289, Hi: 65306, Stride: 17}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "zh-hans": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8245, 12494, 65281, 65288, 65289, 65306, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 96, 47, 33, 40, 41, 58, 126}, + Locale: "zh-hans", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8245, Hi: 12494, Stride: 4249}, + {Lo: 65281, Hi: 65288, Stride: 7}, + {Lo: 65289, Hi: 65306, Stride: 17}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, - "zh-hant": { - Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 12494, 65283, 65307, 65374}, - With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 47, 35, 59, 126}, - Locale: "zh-hant", - RangeTable: &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 180, Hi: 215, Stride: 35}, - {Lo: 305, Hi: 921, Stride: 616}, - {Lo: 1009, Hi: 1040, Stride: 31}, - {Lo: 1042, Hi: 1045, Stride: 3}, - {Lo: 1047, Hi: 1050, Stride: 3}, - {Lo: 1052, Hi: 1054, Stride: 1}, - {Lo: 1056, Hi: 1059, Stride: 1}, - {Lo: 1061, Hi: 1068, Stride: 7}, - {Lo: 1072, Hi: 1073, Stride: 1}, - {Lo: 1075, Hi: 1077, Stride: 2}, - {Lo: 1086, Hi: 1088, Stride: 2}, - {Lo: 1089, Hi: 1093, Stride: 2}, - {Lo: 8211, Hi: 12494, Stride: 4283}, - {Lo: 65283, Hi: 65307, Stride: 24}, - {Lo: 65374, Hi: 65374, Stride: 1}, + + "zh-hant": { + Confusable: []rune{180, 215, 305, 921, 1009, 1040, 1042, 1045, 1047, 1050, 1052, 1053, 1054, 1056, 1057, 1058, 1059, 1061, 1068, 1072, 1073, 1075, 1077, 1086, 1088, 1089, 1091, 1093, 8211, 12494, 65283, 65307, 65374}, + With: []rune{96, 120, 105, 73, 112, 65, 66, 69, 51, 75, 77, 72, 79, 80, 67, 84, 89, 88, 98, 97, 54, 114, 101, 111, 112, 99, 121, 120, 45, 47, 35, 59, 126}, + Locale: "zh-hant", + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 180, Hi: 215, Stride: 35}, + {Lo: 305, Hi: 921, Stride: 616}, + {Lo: 1009, Hi: 1040, Stride: 31}, + {Lo: 1042, Hi: 1045, Stride: 3}, + {Lo: 1047, Hi: 1050, Stride: 3}, + {Lo: 1052, Hi: 1054, Stride: 1}, + {Lo: 1056, Hi: 1059, Stride: 1}, + {Lo: 1061, Hi: 1068, Stride: 7}, + {Lo: 1072, Hi: 1073, Stride: 1}, + {Lo: 1075, Hi: 1077, Stride: 2}, + {Lo: 1086, Hi: 1088, Stride: 2}, + {Lo: 1089, Hi: 1093, Stride: 2}, + {Lo: 8211, Hi: 12494, Stride: 4283}, + {Lo: 65283, Hi: 65307, Stride: 24}, + {Lo: 65374, Hi: 65374, Stride: 1}, + }, + R32: []unicode.Range32{}, + LatinOffset: 1, }, - R32: []unicode.Range32{}, - LatinOffset: 1, }, - }, + } } diff --git a/package/gitea/source/modules/charset/ambiguous_gen_test.go b/package/gitea/source/modules/charset/ambiguous_gen_test.go index d3be0b1a..81d2e806 100644 --- a/package/gitea/source/modules/charset/ambiguous_gen_test.go +++ b/package/gitea/source/modules/charset/ambiguous_gen_test.go @@ -8,11 +8,13 @@ import ( "testing" "unicode" + "code.gitea.io/gitea/modules/translation" + "github.com/stretchr/testify/assert" ) func TestAmbiguousCharacters(t *testing.T) { - for locale, ambiguous := range AmbiguousCharacters { + for locale, ambiguous := range globalVars().ambiguousTableMap { assert.Equal(t, locale, ambiguous.Locale) assert.Len(t, ambiguous.With, len(ambiguous.Confusable)) assert.True(t, sort.SliceIsSorted(ambiguous.Confusable, func(i, j int) bool { @@ -28,4 +30,8 @@ func TestAmbiguousCharacters(t *testing.T) { assert.True(t, found, "%c is not in %d", confusable, i) } } + + var confusableTo rune + ret := isAmbiguous('𝐾', &confusableTo, AmbiguousTablesForLocale(&translation.MockLocale{})...) + assert.True(t, ret) } diff --git a/package/gitea/source/modules/charset/breakwriter.go b/package/gitea/source/modules/charset/breakwriter.go deleted file mode 100644 index a87e8464..00000000 --- a/package/gitea/source/modules/charset/breakwriter.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package charset - -import ( - "bytes" - "io" -) - -// BreakWriter wraps an io.Writer to always write '\n' as '
' -type BreakWriter struct { - io.Writer -} - -// Write writes the provided byte slice transparently replacing '\n' with '
' -func (b *BreakWriter) Write(bs []byte) (n int, err error) { - pos := 0 - for pos < len(bs) { - idx := bytes.IndexByte(bs[pos:], '\n') - if idx < 0 { - wn, err := b.Writer.Write(bs[pos:]) - return n + wn, err - } - - if idx > 0 { - wn, err := b.Writer.Write(bs[pos : pos+idx]) - n += wn - if err != nil { - return n, err - } - } - - if _, err = b.Writer.Write([]byte("
")); err != nil { - return n, err - } - pos += idx + 1 - - n++ - } - - return n, err -} diff --git a/package/gitea/source/modules/charset/breakwriter_test.go b/package/gitea/source/modules/charset/breakwriter_test.go deleted file mode 100644 index 5eeeedc4..00000000 --- a/package/gitea/source/modules/charset/breakwriter_test.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package charset - -import ( - "strings" - "testing" -) - -func TestBreakWriter_Write(t *testing.T) { - tests := []struct { - name string - kase string - expect string - wantErr bool - }{ - { - name: "noline", - kase: "abcdefghijklmnopqrstuvwxyz", - expect: "abcdefghijklmnopqrstuvwxyz", - }, - { - name: "endline", - kase: "abcdefghijklmnopqrstuvwxyz\n", - expect: "abcdefghijklmnopqrstuvwxyz
", - }, - { - name: "startline", - kase: "\nabcdefghijklmnopqrstuvwxyz", - expect: "
abcdefghijklmnopqrstuvwxyz", - }, - { - name: "onlyline", - kase: "\n\n\n", - expect: "


", - }, - { - name: "empty", - kase: "", - expect: "", - }, - { - name: "midline", - kase: "\nabc\ndefghijkl\nmnopqrstuvwxy\nz", - expect: "
abc
defghijkl
mnopqrstuvwxy
z", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - buf := &strings.Builder{} - b := &BreakWriter{ - Writer: buf, - } - n, err := b.Write([]byte(tt.kase)) - if (err != nil) != tt.wantErr { - t.Errorf("BreakWriter.Write() error = %v, wantErr %v", err, tt.wantErr) - return - } - if n != len(tt.kase) { - t.Errorf("BreakWriter.Write() = %v, want %v", n, len(tt.kase)) - } - if buf.String() != tt.expect { - t.Errorf("BreakWriter.Write() wrote %q, want %v", buf.String(), tt.expect) - } - }) - } -} diff --git a/package/gitea/source/modules/charset/charset.go b/package/gitea/source/modules/charset/charset.go index 00e0909d..96de1c9f 100644 --- a/package/gitea/source/modules/charset/charset.go +++ b/package/gitea/source/modules/charset/charset.go @@ -5,12 +5,13 @@ package charset import ( "bytes" - "fmt" "io" + "regexp" "strings" + "sync" + "unicode" "unicode/utf8" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -19,11 +20,24 @@ import ( "golang.org/x/text/transform" ) -// UTF8BOM is the utf-8 byte-order marker -var UTF8BOM = []byte{'\xef', '\xbb', '\xbf'} +var globalVars = sync.OnceValue(func() (ret struct { + utf8Bom []byte + + defaultWordRegexp *regexp.Regexp + ambiguousTableMap map[string]*AmbiguousTable + invisibleRangeTable *unicode.RangeTable +}, +) { + ret.utf8Bom = []byte{'\xef', '\xbb', '\xbf'} + ret.ambiguousTableMap = newAmbiguousTableMap() + ret.invisibleRangeTable = newInvisibleRangeTable() + return ret +}) type ConvertOpts struct { - KeepBOM bool + KeepBOM bool + ErrorReplacement []byte + ErrorReturnOrigin bool } var ToUTF8WithFallbackReaderPrefetchSize = 16 * 1024 @@ -33,54 +47,27 @@ func ToUTF8WithFallbackReader(rd io.Reader, opts ConvertOpts) io.Reader { buf := make([]byte, ToUTF8WithFallbackReaderPrefetchSize) n, err := util.ReadAtMost(rd, buf) if err != nil { - return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd) - } - - charsetLabel, err := DetectEncoding(buf[:n]) - if err != nil || charsetLabel == "UTF-8" { - return io.MultiReader(bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), rd) - } - - encoding, _ := charset.Lookup(charsetLabel) - if encoding == nil { - log.Error("Unknown encoding: %s", charsetLabel) + // read error occurs, don't do any processing return io.MultiReader(bytes.NewReader(buf[:n]), rd) } - return transform.NewReader( - io.MultiReader( - bytes.NewReader(MaybeRemoveBOM(buf[:n], opts)), - rd, - ), - encoding.NewDecoder(), - ) -} - -// ToUTF8 converts content to UTF8 encoding -func ToUTF8(content []byte, opts ConvertOpts) ([]byte, error) { - charsetLabel, err := DetectEncoding(content) - if err != nil { - return content, err - } else if charsetLabel == "UTF-8" { - return MaybeRemoveBOM(content, opts), nil + charsetLabel, _ := DetectEncoding(buf[:n]) + if charsetLabel == "UTF-8" { + // is utf-8, try to remove BOM and read it as-is + return io.MultiReader(bytes.NewReader(maybeRemoveBOM(buf[:n], opts)), rd) } encoding, _ := charset.Lookup(charsetLabel) if encoding == nil { - log.Error("Unknown encoding: %s", charsetLabel) - return content, fmt.Errorf("unknown encoding: %s", charsetLabel) + // unknown charset, don't do any processing + return io.MultiReader(bytes.NewReader(buf[:n]), rd) } - // If there is an error, we concatenate the nicely decoded part and the - // original left over. This way we won't lose much data. - result, n, err := transform.Bytes(encoding.NewDecoder(), content) - if err != nil { - result = append(result, content[n:]...) - } - - result = MaybeRemoveBOM(result, opts) - - return result, err + // convert from charset to utf-8 + return transform.NewReader( + io.MultiReader(bytes.NewReader(buf[:n]), rd), + encoding.NewDecoder(), + ) } // ToUTF8WithFallback detects the encoding of content and converts to UTF-8 if possible @@ -89,49 +76,50 @@ func ToUTF8WithFallback(content []byte, opts ConvertOpts) []byte { return bs } -// ToUTF8DropErrors makes sure the return string is valid utf-8; attempts conversion if possible -func ToUTF8DropErrors(content []byte, opts ConvertOpts) []byte { - charsetLabel, err := DetectEncoding(content) - if err != nil || charsetLabel == "UTF-8" { - return MaybeRemoveBOM(content, opts) +func ToUTF8DropErrors(content []byte) []byte { + return ToUTF8(content, ConvertOpts{ErrorReplacement: []byte{' '}}) +} + +func ToUTF8(content []byte, opts ConvertOpts) []byte { + charsetLabel, _ := DetectEncoding(content) + if charsetLabel == "UTF-8" { + return maybeRemoveBOM(content, opts) } encoding, _ := charset.Lookup(charsetLabel) if encoding == nil { - log.Error("Unknown encoding: %s", charsetLabel) + setting.PanicInDevOrTesting("unsupported detected charset %q, it shouldn't happen", charsetLabel) return content } - // We ignore any non-decodable parts from the file. - // Some parts might be lost var decoded []byte decoder := encoding.NewDecoder() idx := 0 - for { + for idx < len(content) { result, n, err := transform.Bytes(decoder, content[idx:]) decoded = append(decoded, result...) if err == nil { break } - decoded = append(decoded, ' ') - idx = idx + n + 1 - if idx >= len(content) { - break + if opts.ErrorReturnOrigin { + return content } + if opts.ErrorReplacement == nil { + decoded = append(decoded, content[idx+n]) + } else { + decoded = append(decoded, opts.ErrorReplacement...) + } + idx += n + 1 } - - return MaybeRemoveBOM(decoded, opts) + return maybeRemoveBOM(decoded, opts) } -// MaybeRemoveBOM removes a UTF-8 BOM from a []byte when opts.KeepBOM is false -func MaybeRemoveBOM(content []byte, opts ConvertOpts) []byte { +// maybeRemoveBOM removes a UTF-8 BOM from a []byte when opts.KeepBOM is false +func maybeRemoveBOM(content []byte, opts ConvertOpts) []byte { if opts.KeepBOM { return content } - if len(content) > 2 && bytes.Equal(content[0:3], UTF8BOM) { - return content[3:] - } - return content + return bytes.TrimPrefix(content, globalVars().utf8Bom) } // DetectEncoding detect the encoding of content diff --git a/package/gitea/source/modules/charset/charset_test.go b/package/gitea/source/modules/charset/charset_test.go index 2fa820da..0314abc3 100644 --- a/package/gitea/source/modules/charset/charset_test.go +++ b/package/gitea/source/modules/charset/charset_test.go @@ -5,6 +5,7 @@ package charset import ( "io" + "os" "strings" "testing" @@ -14,97 +15,78 @@ import ( "github.com/stretchr/testify/assert" ) -func resetDefaultCharsetsOrder() { - defaultDetectedCharsetsOrder := make([]string, 0, len(setting.Repository.DetectedCharsetsOrder)) - for _, charset := range setting.Repository.DetectedCharsetsOrder { - defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder, strings.ToLower(strings.TrimSpace(charset))) - } +func TestMain(m *testing.M) { setting.Repository.DetectedCharsetScore = map[string]int{} - i := 0 - for _, charset := range defaultDetectedCharsetsOrder { - canonicalCharset := strings.ToLower(strings.TrimSpace(charset)) - if _, has := setting.Repository.DetectedCharsetScore[canonicalCharset]; !has { - setting.Repository.DetectedCharsetScore[canonicalCharset] = i - i++ - } + for i, charset := range setting.Repository.DetectedCharsetsOrder { + setting.Repository.DetectedCharsetScore[strings.ToLower(charset)] = i } + os.Exit(m.Run()) } func TestMaybeRemoveBOM(t *testing.T) { - res := MaybeRemoveBOM([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{}) + res := maybeRemoveBOM([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{}) assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res) - res = MaybeRemoveBOM([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{}) + res = maybeRemoveBOM([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{}) assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res) } func TestToUTF8(t *testing.T) { - resetDefaultCharsetsOrder() - // Note: golang compiler seems so behave differently depending on the current // locale, so some conversions might behave differently. For that reason, we don't // depend on particular conversions but in expected behaviors. - res, err := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{}) - assert.NoError(t, err) + res := ToUTF8([]byte{0x41, 0x42, 0x43}, ConvertOpts{}) assert.Equal(t, "ABC", string(res)) // "áéíóú" - res, err = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{}) - assert.NoError(t, err) + res = ToUTF8([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{}) assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res) // "áéíóú" - res, err = ToUTF8([]byte{ + res = ToUTF8([]byte{ 0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba, }, ConvertOpts{}) - assert.NoError(t, err) assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res) - res, err = ToUTF8([]byte{ + res = ToUTF8([]byte{ 0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73, 0x41, 0x41, 0x41, 0x2e, }, ConvertOpts{}) - assert.NoError(t, err) stringMustStartWith(t, "Hola,", res) stringMustEndWith(t, "AAA.", res) - res, err = ToUTF8([]byte{ + res = ToUTF8([]byte{ 0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73, 0x41, 0x41, 0x41, 0x2e, }, ConvertOpts{}) - assert.NoError(t, err) stringMustStartWith(t, "Hola,", res) stringMustEndWith(t, "AAA.", res) - res, err = ToUTF8([]byte{ + res = ToUTF8([]byte{ 0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73, 0x41, 0x41, 0x41, 0x2e, }, ConvertOpts{}) - assert.NoError(t, err) stringMustStartWith(t, "Hola,", res) stringMustEndWith(t, "AAA.", res) // Japanese (Shift-JIS) // 日属秘ぞしちゅ。 - res, err = ToUTF8([]byte{ + res = ToUTF8([]byte{ 0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82, 0xBF, 0x82, 0xE3, 0x81, 0x42, }, ConvertOpts{}) - assert.NoError(t, err) assert.Equal(t, []byte{ 0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3, 0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82, }, res) - res, err = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{}) - assert.NoError(t, err) + res = ToUTF8([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{}) assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res) } func TestToUTF8WithFallback(t *testing.T) { - resetDefaultCharsetsOrder() // "ABC" res := ToUTF8WithFallback([]byte{0x41, 0x42, 0x43}, ConvertOpts{}) assert.Equal(t, []byte{0x41, 0x42, 0x43}, res) @@ -151,54 +133,58 @@ func TestToUTF8WithFallback(t *testing.T) { } func TestToUTF8DropErrors(t *testing.T) { - resetDefaultCharsetsOrder() // "ABC" - res := ToUTF8DropErrors([]byte{0x41, 0x42, 0x43}, ConvertOpts{}) + res := ToUTF8DropErrors([]byte{0x41, 0x42, 0x43}) assert.Equal(t, []byte{0x41, 0x42, 0x43}, res) // "áéíóú" - res = ToUTF8DropErrors([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{}) + res = ToUTF8DropErrors([]byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}) assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res) // UTF8 BOM + "áéíóú" - res = ToUTF8DropErrors([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, ConvertOpts{}) + res = ToUTF8DropErrors([]byte{0xef, 0xbb, 0xbf, 0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}) assert.Equal(t, []byte{0xc3, 0xa1, 0xc3, 0xa9, 0xc3, 0xad, 0xc3, 0xb3, 0xc3, 0xba}, res) // "Hola, así cómo ños" - res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73}, ConvertOpts{}) + res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0xF1, 0x6F, 0x73}) assert.Equal(t, []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73}, res[:8]) assert.Equal(t, []byte{0x73}, res[len(res)-1:]) // "Hola, así cómo " minmatch := []byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xC3, 0xAD, 0x20, 0x63, 0xC3, 0xB3, 0x6D, 0x6F, 0x20} - res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73}, ConvertOpts{}) + res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x07, 0xA4, 0x6F, 0x73}) // Do not fail for differences in invalid cases, as the library might change the conversion criteria for those assert.Equal(t, minmatch, res[0:len(minmatch)]) - res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73}, ConvertOpts{}) + res = ToUTF8DropErrors([]byte{0x48, 0x6F, 0x6C, 0x61, 0x2C, 0x20, 0x61, 0x73, 0xED, 0x20, 0x63, 0xF3, 0x6D, 0x6F, 0x20, 0x81, 0xA4, 0x6F, 0x73}) // Do not fail for differences in invalid cases, as the library might change the conversion criteria for those assert.Equal(t, minmatch, res[0:len(minmatch)]) // Japanese (Shift-JIS) // "日属秘ぞしちゅ。" - res = ToUTF8DropErrors([]byte{0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82, 0xBF, 0x82, 0xE3, 0x81, 0x42}, ConvertOpts{}) + res = ToUTF8DropErrors([]byte{0x93, 0xFA, 0x91, 0xAE, 0x94, 0xE9, 0x82, 0xBC, 0x82, 0xB5, 0x82, 0xBF, 0x82, 0xE3, 0x81, 0x42}) assert.Equal(t, []byte{ 0xE6, 0x97, 0xA5, 0xE5, 0xB1, 0x9E, 0xE7, 0xA7, 0x98, 0xE3, 0x81, 0x9E, 0xE3, 0x81, 0x97, 0xE3, 0x81, 0xA1, 0xE3, 0x82, 0x85, 0xE3, 0x80, 0x82, }, res) - res = ToUTF8DropErrors([]byte{0x00, 0x00, 0x00, 0x00}, ConvertOpts{}) + res = ToUTF8DropErrors([]byte{0x00, 0x00, 0x00, 0x00}) assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res) } func TestDetectEncoding(t *testing.T) { - resetDefaultCharsetsOrder() testSuccess := func(b []byte, expected string) { encoding, err := DetectEncoding(b) assert.NoError(t, err) assert.Equal(t, expected, encoding) } + + // invalid bytes + encoding, err := DetectEncoding([]byte{0xfa}) + assert.Error(t, err) + assert.Equal(t, "UTF-8", encoding) + // utf-8 b := []byte("just some ascii") testSuccess(b, "UTF-8") @@ -213,21 +199,12 @@ func TestDetectEncoding(t *testing.T) { // iso-8859-1: dcor b = []byte{0x44, 0xe9, 0x63, 0x6f, 0x72, 0x0a} - encoding, err := DetectEncoding(b) + encoding, err = DetectEncoding(b) assert.NoError(t, err) assert.Contains(t, encoding, "ISO-8859-1") - old := setting.Repository.AnsiCharset - setting.Repository.AnsiCharset = "placeholder" - defer func() { - setting.Repository.AnsiCharset = old - }() - testSuccess(b, "placeholder") - - // invalid bytes - b = []byte{0xfa} - _, err = DetectEncoding(b) - assert.Error(t, err) + defer test.MockVariableValue(&setting.Repository.AnsiCharset, "MyEncoding")() + testSuccess(b, "MyEncoding") } func stringMustStartWith(t *testing.T, expected string, value []byte) { @@ -239,7 +216,6 @@ func stringMustEndWith(t *testing.T, expected string, value []byte) { } func TestToUTF8WithFallbackReader(t *testing.T) { - resetDefaultCharsetsOrder() test.MockVariableValue(&ToUTF8WithFallbackReaderPrefetchSize) block := "aá啊🤔" diff --git a/package/gitea/source/modules/charset/escape.go b/package/gitea/source/modules/charset/escape.go index 92e417d1..8f25e787 100644 --- a/package/gitea/source/modules/charset/escape.go +++ b/package/gitea/source/modules/charset/escape.go @@ -1,10 +1,6 @@ // Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:generate go run invisible/generate.go -v -o ./invisible_gen.go - -//go:generate go run ambiguous/generate.go -v -o ./ambiguous_gen.go ambiguous/ambiguous.json - package charset import ( @@ -12,33 +8,36 @@ import ( "io" "strings" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" ) -// RuneNBSP is the codepoint for NBSP -const RuneNBSP = 0xa0 +type EscapeOptions struct { + Allowed map[rune]bool +} -// EscapeControlHTML escapes the unicode control sequences in a provided html document -func EscapeControlHTML(html template.HTML, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, output template.HTML) { +func AllowRuneNBSP() map[rune]bool { + return map[rune]bool{0xa0: true} +} + +func EscapeOptionsForView() EscapeOptions { + return EscapeOptions{ + // it's safe to see NBSP in the view, but maybe not in the diff + Allowed: AllowRuneNBSP(), + } +} + +// EscapeControlHTML escapes the Unicode control sequences in a provided html document +func EscapeControlHTML(html template.HTML, locale translation.Locale, opts ...EscapeOptions) (escaped *EscapeStatus, output template.HTML) { + if !setting.UI.AmbiguousUnicodeDetection { + return &EscapeStatus{}, html + } sb := &strings.Builder{} - escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, allowed...) // err has been handled in EscapeControlReader + escaped, _ = EscapeControlReader(strings.NewReader(string(html)), sb, locale, opts...) // err has been handled in EscapeControlReader return escaped, template.HTML(sb.String()) } -// EscapeControlReader escapes the unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus -func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) { - if !setting.UI.AmbiguousUnicodeDetection { - _, err = io.Copy(writer, reader) - return &EscapeStatus{}, err - } - outputStream := &HTMLStreamerWriter{Writer: writer} - streamer := NewEscapeStreamer(locale, outputStream, allowed...).(*escapeStreamer) - - if err = StreamHTML(reader, streamer); err != nil { - streamer.escaped.HasError = true - log.Error("Error whilst escaping: %v", err) - } - return streamer.escaped, err +// EscapeControlReader escapes the Unicode control sequences in a provided reader of HTML content and writer in a locale and returns the findings as an EscapeStatus +func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.Locale, opts ...EscapeOptions) (*EscapeStatus, error) { + return escapeStream(locale, reader, writer, opts...) } diff --git a/package/gitea/source/modules/charset/escape_status.go b/package/gitea/source/modules/charset/escape_status.go index 37b6ad86..fb9ebbb2 100644 --- a/package/gitea/source/modules/charset/escape_status.go +++ b/package/gitea/source/modules/charset/escape_status.go @@ -3,11 +3,9 @@ package charset -// EscapeStatus represents the findings of the unicode escaper +// EscapeStatus represents the findings of the Unicode escaper type EscapeStatus struct { - Escaped bool - HasError bool - HasBadRunes bool + Escaped bool // it means that some characters were escaped, and they can also be unescaped back HasInvisible bool HasAmbiguous bool } @@ -19,8 +17,6 @@ func (status *EscapeStatus) Or(other *EscapeStatus) *EscapeStatus { st = &EscapeStatus{} } st.Escaped = st.Escaped || other.Escaped - st.HasError = st.HasError || other.HasError - st.HasBadRunes = st.HasBadRunes || other.HasBadRunes st.HasAmbiguous = st.HasAmbiguous || other.HasAmbiguous st.HasInvisible = st.HasInvisible || other.HasInvisible return st diff --git a/package/gitea/source/modules/charset/escape_stream.go b/package/gitea/source/modules/charset/escape_stream.go index 29943eb8..11d09083 100644 --- a/package/gitea/source/modules/charset/escape_stream.go +++ b/package/gitea/source/modules/charset/escape_stream.go @@ -4,286 +4,415 @@ package charset import ( + "bytes" "fmt" - "regexp" - "strings" + "html" + "io" "unicode" "unicode/utf8" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/translation" - - "golang.org/x/net/html" ) -// VScode defaultWordRegexp -var defaultWordRegexp = regexp.MustCompile(`(-?\d*\.\d\w*)|([^\` + "`" + `\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s\x00-\x1f]+)`) - -func NewEscapeStreamer(locale translation.Locale, next HTMLStreamer, allowed ...rune) HTMLStreamer { - allowedM := make(map[rune]bool, len(allowed)) - for _, v := range allowed { - allowedM[v] = true - } - return &escapeStreamer{ - escaped: &EscapeStatus{}, - PassthroughHTMLStreamer: *NewPassthroughStreamer(next), - locale: locale, - ambiguousTables: AmbiguousTablesForLocale(locale), - allowed: allowedM, - } +type htmlChunkReader struct { + in io.Reader + readErr error + readBuf []byte + curInTag bool } type escapeStreamer struct { - PassthroughHTMLStreamer + htmlChunkReader + escaped *EscapeStatus locale translation.Locale ambiguousTables []*AmbiguousTable allowed map[rune]bool + + out io.Writer } -func (e *escapeStreamer) EscapeStatus() *EscapeStatus { - return e.escaped +func escapeStream(locale translation.Locale, in io.Reader, out io.Writer, opts ...EscapeOptions) (*EscapeStatus, error) { + es := &escapeStreamer{ + escaped: &EscapeStatus{}, + locale: locale, + ambiguousTables: AmbiguousTablesForLocale(locale), + htmlChunkReader: htmlChunkReader{ + in: in, + readBuf: make([]byte, 0, 32*1024), + }, + out: out, + } + + if len(opts) > 0 { + es.allowed = opts[0].Allowed + } + + readCount := 0 + lastIsTag := false + for { + parts, partInTag, err := es.readRunes() + readCount++ + if err == io.EOF { + return es.escaped, nil + } else if err != nil { + return nil, err + } + for i, part := range parts { + if partInTag[i] { + lastIsTag = true + if _, err := out.Write(part); err != nil { + return nil, err + } + } else { + // if last part is tag, then this part is content begin + // if the content is the first part of the first read, then it's also content begin + isContentBegin := lastIsTag || (readCount == 1 && i == 0) + lastIsTag = false + if isContentBegin { + if part, err = es.trimAndWriteBom(part); err != nil { + return nil, err + } + } + if err = es.detectAndWriteRunes(part); err != nil { + return nil, err + } + } + } + } } -// Text tells the next streamer there is a text -func (e *escapeStreamer) Text(data string) error { - sb := &strings.Builder{} - var until int - var next int +func (e *escapeStreamer) trimAndWriteBom(part []byte) ([]byte, error) { + remaining, ok := bytes.CutPrefix(part, globalVars().utf8Bom) + if ok { + part = remaining + if _, err := e.out.Write(globalVars().utf8Bom); err != nil { + return part, err + } + } + return part, nil +} + +const longSentenceDetectionLimit = 20 + +func (e *escapeStreamer) possibleLongSentence(results []detectResult, pos int) bool { + countBasic := 0 + countNonASCII := 0 + for i := max(pos-longSentenceDetectionLimit, 0); i < min(pos+longSentenceDetectionLimit, len(results)); i++ { + if results[i].runeType == runeTypeBasic && results[i].runeChar != ' ' { + countBasic++ + } + if results[i].runeType == runeTypeNonASCII || results[i].runeType == runeTypeAmbiguous { + countNonASCII++ + } + } + countChar := countBasic + countNonASCII + // many non-ASCII runes around, it seems to be a sentence, + // don't handle the invisible/ambiguous chars in it, otherwise it will be too noisy + return countChar != 0 && countNonASCII*100/countChar >= 50 +} + +func (e *escapeStreamer) analyzeDetectResults(results []detectResult) { + for i := range results { + res := &results[i] + if res.runeType == runeTypeInvisible || res.runeType == runeTypeAmbiguous { + leftIsNonASCII := i > 0 && (results[i-1].runeType == runeTypeNonASCII || results[i-1].runeType == runeTypeAmbiguous) + rightIsNonASCII := i < len(results)-1 && (results[i+1].runeType == runeTypeNonASCII || results[i+1].runeType == runeTypeAmbiguous) + surroundingNonASCII := leftIsNonASCII || rightIsNonASCII + if !surroundingNonASCII { + if len(results) < longSentenceDetectionLimit { + res.needEscape = setting.UI.AmbiguousUnicodeDetection + } else if !e.possibleLongSentence(results, i) { + res.needEscape = setting.UI.AmbiguousUnicodeDetection + } + } + } + } +} + +func (e *escapeStreamer) detectAndWriteRunes(part []byte) error { + results := e.detectRunes(part) + e.analyzeDetectResults(results) + return e.writeDetectResults(part, results) +} + +func (e *htmlChunkReader) readRunes() (parts [][]byte, partInTag []bool, _ error) { + // we have read everything, eof + if e.readErr != nil && len(e.readBuf) == 0 { + return nil, nil, e.readErr + } + + // not eof, and the there is space in the buffer, try to read more data + if e.readErr == nil && len(e.readBuf) <= cap(e.readBuf)*3/4 { + n, err := e.in.Read(e.readBuf[len(e.readBuf):cap(e.readBuf)]) + e.readErr = err + e.readBuf = e.readBuf[:len(e.readBuf)+n] + } + if len(e.readBuf) == 0 { + return nil, nil, e.readErr + } + + // try to exact tag parts and content parts pos := 0 - if len(data) > len(UTF8BOM) && data[:len(UTF8BOM)] == string(UTF8BOM) { - _, _ = sb.WriteString(data[:len(UTF8BOM)]) - pos = len(UTF8BOM) - } - dataBytes := []byte(data) - for pos < len(data) { - nextIdxs := defaultWordRegexp.FindStringIndex(data[pos:]) - if nextIdxs == nil { - until = len(data) - next = until - } else { - until, next = nextIdxs[0]+pos, nextIdxs[1]+pos - } - - // from pos until we know that the runes are not \r\t\n or even ' ' - runes := make([]rune, 0, next-until) - positions := make([]int, 0, next-until+1) - - for pos < until { - r, sz := utf8.DecodeRune(dataBytes[pos:]) - positions = positions[:0] - positions = append(positions, pos, pos+sz) - types, confusables, _ := e.runeTypes(r) - if err := e.handleRunes(dataBytes, []rune{r}, positions, types, confusables, sb); err != nil { - return err - } - pos += sz - } - - for i := pos; i < next; { - r, sz := utf8.DecodeRune(dataBytes[i:]) - runes = append(runes, r) - positions = append(positions, i) - i += sz - } - positions = append(positions, next) - types, confusables, runeCounts := e.runeTypes(runes...) - if runeCounts.needsEscape() { - if err := e.handleRunes(dataBytes, runes, positions, types, confusables, sb); err != nil { - return err + for pos < len(e.readBuf) { + var curPartEnd int + nextInTag := e.curInTag + if e.curInTag { + // if cur part is in tag, try to find the tag close char '>' + idx := bytes.IndexByte(e.readBuf[pos:], '>') + if idx == -1 { + // if no tag close char, then the whole buffer is in tag + curPartEnd = len(e.readBuf) + } else { + // tag part ends, switch to content part + curPartEnd = pos + idx + 1 + nextInTag = !nextInTag } } else { - _, _ = sb.Write(dataBytes[pos:next]) + // if cur part is in content, try to find the tag open char '<' + idx := bytes.IndexByte(e.readBuf[pos:], '<') + if idx == -1 { + // if no tag open char, then the whole buffer is in content + curPartEnd = len(e.readBuf) + } else { + // content part ends, switch to tag part + curPartEnd = pos + idx + nextInTag = !nextInTag + } + } + + curPartLen := curPartEnd - pos + if curPartLen == 0 { + // if cur part is empty, only need to switch the part type + if e.curInTag == nextInTag { + panic("impossible, curPartLen is 0 but the part in tag status is not switched") + } + e.curInTag = nextInTag + continue + } + + // now, curPartLen can't be 0 + curPart := make([]byte, curPartLen) + copy(curPart, e.readBuf[pos:curPartEnd]) + // now we get the curPart bytes, but we can't directly use it, the last rune in it might have been cut + // try to decode the last rune, if it's invalid, then we cut the last byte and try again until we get a valid rune or no byte left + for i := curPartLen - 1; i >= 0; i-- { + last, lastSize := utf8.DecodeRune(curPart[i:]) + if last == utf8.RuneError && lastSize == 1 { + curPartLen-- + } else { + curPartLen += lastSize - 1 + break + } + } + if curPartLen == 0 { + // actually it's impossible that the part doesn't contain any valid rune, + // the only case is that the cap(readBuf) is too small, or the origin contain indeed doesn't contain any valid rune + // * try to leave the last 4 bytes (possible longest utf-8 encoding) to next round + // * at least consume 1 byte to avoid infinite loop + curPartLen = max(len(curPart)-utf8.UTFMax, 1) + } + + // if curPartLen is not the same as curPart, it means we have cut some bytes, + // need to wait for more data if not eof + trailingCorrupted := curPartLen != len(curPart) + + // finally, we get the real part we need + curPart = curPart[:curPartLen] + parts = append(parts, curPart) + partInTag = append(partInTag, e.curInTag) + + pos += curPartLen + e.curInTag = nextInTag + + if trailingCorrupted && e.readErr == nil { + // if the last part is corrupted, and we haven't reach eof, then we need to wait for more data to get the complete part + break } - pos = next } - if sb.Len() > 0 { - if err := e.PassthroughHTMLStreamer.Text(sb.String()); err != nil { + + copy(e.readBuf, e.readBuf[pos:]) + e.readBuf = e.readBuf[:len(e.readBuf)-pos] + return parts, partInTag, nil +} + +func (e *escapeStreamer) writeDetectResults(data []byte, results []detectResult) error { + lastWriteRawIdx := -1 + for idx := range results { + res := &results[idx] + if !res.needEscape { + if lastWriteRawIdx == -1 { + lastWriteRawIdx = idx + } + continue + } + + if lastWriteRawIdx != -1 { + if _, err := e.out.Write(data[results[lastWriteRawIdx].position:res.position]); err != nil { + return err + } + lastWriteRawIdx = -1 + } + switch res.runeType { + case runeTypeBroken: + if err := e.writeBrokenRune(data[res.position : res.position+res.runeSize]); err != nil { + return err + } + case runeTypeAmbiguous: + if err := e.writeAmbiguousRune(res.runeChar, res.confusable); err != nil { + return err + } + case runeTypeInvisible: + if err := e.writeInvisibleRune(res.runeChar); err != nil { + return err + } + case runeTypeControlChar: + if err := e.writeControlRune(res.runeChar); err != nil { + return err + } + default: + panic("unreachable") + } + } + if lastWriteRawIdx != -1 { + lastResult := results[len(results)-1] + if _, err := e.out.Write(data[results[lastWriteRawIdx].position : lastResult.position+lastResult.runeSize]); err != nil { return err } } return nil } -func (e *escapeStreamer) handleRunes(data []byte, runes []rune, positions []int, types []runeType, confusables []rune, sb *strings.Builder) error { - for i, r := range runes { - switch types[i] { - case brokenRuneType: - if sb.Len() > 0 { - if err := e.PassthroughHTMLStreamer.Text(sb.String()); err != nil { - return err - } - sb.Reset() - } - end := positions[i+1] - start := positions[i] - if err := e.brokenRune(data[start:end]); err != nil { - return err - } - case ambiguousRuneType: - if sb.Len() > 0 { - if err := e.PassthroughHTMLStreamer.Text(sb.String()); err != nil { - return err - } - sb.Reset() - } - if err := e.ambiguousRune(r, confusables[0]); err != nil { - return err - } - confusables = confusables[1:] - case invisibleRuneType: - if sb.Len() > 0 { - if err := e.PassthroughHTMLStreamer.Text(sb.String()); err != nil { - return err - } - sb.Reset() - } - if err := e.invisibleRune(r); err != nil { - return err - } - default: - _, _ = sb.WriteRune(r) - } - } - return nil +func (e *escapeStreamer) writeBrokenRune(_ []byte) (err error) { + // Although we'd like to use the original bytes to display (show the real broken content to users), + // however, when this "escape stream" module is applied to the content, the content has already been processed by other modules. + // So the invalid bytes just can't be kept till this step, in most (all) cases, the only thing we see here is utf8.RuneError + _, err = io.WriteString(e.out, ``) + return err } -func (e *escapeStreamer) brokenRune(bs []byte) error { - e.escaped.Escaped = true - e.escaped.HasBadRunes = true - - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "broken-code-point", - }); err != nil { +func (e *escapeStreamer) writeEscapedCharHTML(tag1, attr, tag2, content, tag3 string) (err error) { + _, err = io.WriteString(e.out, tag1) + if err != nil { return err } - if err := e.PassthroughHTMLStreamer.Text(fmt.Sprintf("<%X>", bs)); err != nil { + _, err = io.WriteString(e.out, html.EscapeString(attr)) + if err != nil { return err } - - return e.PassthroughHTMLStreamer.EndTag("span") + _, err = io.WriteString(e.out, tag2) + if err != nil { + return err + } + _, err = io.WriteString(e.out, html.EscapeString(content)) + if err != nil { + return err + } + _, err = io.WriteString(e.out, tag3) + return err } -func (e *escapeStreamer) ambiguousRune(r, c rune) error { +func runeToHex(r rune) string { + return fmt.Sprintf("[U+%04X]", r) +} + +func (e *escapeStreamer) writeAmbiguousRune(r, c rune) (err error) { e.escaped.Escaped = true e.escaped.HasAmbiguous = true - - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "ambiguous-code-point", - }, html.Attribute{ - Key: "data-tooltip-content", - Val: e.locale.TrString("repo.ambiguous_character", r, c), - }); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "char", - }); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.Text(string(r)); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.EndTag("span"); err != nil { - return err - } - - return e.PassthroughHTMLStreamer.EndTag("span") + return e.writeEscapedCharHTML( + ``, + string(r), + ``, + ) } -func (e *escapeStreamer) invisibleRune(r rune) error { +func (e *escapeStreamer) writeInvisibleRune(r rune) error { e.escaped.Escaped = true e.escaped.HasInvisible = true - - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "escaped-code-point", - }, html.Attribute{ - Key: "data-escaped", - Val: fmt.Sprintf("[U+%04X]", r), - }); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.StartTag("span", html.Attribute{ - Key: "class", - Val: "char", - }); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.Text(string(r)); err != nil { - return err - } - if err := e.PassthroughHTMLStreamer.EndTag("span"); err != nil { - return err - } - - return e.PassthroughHTMLStreamer.EndTag("span") + return e.writeEscapedCharHTML( + ``, + string(r), + ``, + ) } -type runeCountType struct { - numBasicRunes int - numNonConfusingNonBasicRunes int - numAmbiguousRunes int - numInvisibleRunes int - numBrokenRunes int +func (e *escapeStreamer) writeControlRune(r rune) error { + var display string + if r >= 0 && r <= 0x1f { + display = string(0x2400 + r) + } else if r == 0x7f { + display = string(rune(0x2421)) + } else { + display = runeToHex(r) + } + return e.writeEscapedCharHTML( + ``, + string(r), + ``, + ) } -func (counts runeCountType) needsEscape() bool { - if counts.numBrokenRunes > 0 { - return true - } - if counts.numBasicRunes == 0 && - counts.numNonConfusingNonBasicRunes > 0 { - return false - } - return counts.numAmbiguousRunes > 0 || counts.numInvisibleRunes > 0 +type detectResult struct { + runeChar rune + runeType int + runeSize int + position int + confusable rune + needEscape bool } -type runeType int - const ( - basicASCIIRuneType runeType = iota // <- This is technically deadcode but its self-documenting so it should stay - brokenRuneType - nonBasicASCIIRuneType - ambiguousRuneType - invisibleRuneType + runeTypeBasic int = iota + runeTypeBroken + runeTypeNonASCII + runeTypeAmbiguous + runeTypeInvisible + runeTypeControlChar ) -func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables []rune, runeCounts runeCountType) { - types = make([]runeType, len(runes)) - for i, r := range runes { - var confusable rune +func (e *escapeStreamer) detectRunes(data []byte) []detectResult { + runeCount := utf8.RuneCount(data) + results := make([]detectResult, runeCount) + invisibleRangeTable := globalVars().invisibleRangeTable + var i int + var confusable rune + for pos := 0; pos < len(data); i++ { + r, runeSize := utf8.DecodeRune(data[pos:]) + results[i].runeChar = r + results[i].runeSize = runeSize + results[i].position = pos + pos += runeSize + switch { case r == utf8.RuneError: - types[i] = brokenRuneType - runeCounts.numBrokenRunes++ - case r == ' ' || r == '\t' || r == '\n': - runeCounts.numBasicRunes++ - case e.allowed[r]: - if r > 0x7e || r < 0x20 { - types[i] = nonBasicASCIIRuneType - runeCounts.numNonConfusingNonBasicRunes++ - } else { - runeCounts.numBasicRunes++ + results[i].runeType = runeTypeBroken + results[i].needEscape = true + case r == ' ' || r == '\t' || r == '\n' || e.allowed[r]: + results[i].runeType = runeTypeBasic + if r >= 0x80 { + results[i].runeType = runeTypeNonASCII } - case unicode.Is(InvisibleRanges, r): - types[i] = invisibleRuneType - runeCounts.numInvisibleRunes++ - case unicode.IsControl(r): - types[i] = invisibleRuneType - runeCounts.numInvisibleRunes++ + case r < 0x20 || r == 0x7f: + results[i].runeType = runeTypeControlChar + results[i].needEscape = true + case unicode.Is(invisibleRangeTable, r): + results[i].runeType = runeTypeInvisible + // not sure about results[i].needEscape, will be detected separately case isAmbiguous(r, &confusable, e.ambiguousTables...): - confusables = append(confusables, confusable) - types[i] = ambiguousRuneType - runeCounts.numAmbiguousRunes++ - case r > 0x7e || r < 0x20: - types[i] = nonBasicASCIIRuneType - runeCounts.numNonConfusingNonBasicRunes++ - default: - runeCounts.numBasicRunes++ + results[i].runeType = runeTypeAmbiguous + results[i].confusable = confusable + // not sure about results[i].needEscape, will be detected separately + case r >= 0x80: + results[i].runeType = runeTypeNonASCII + default: // details to basic runes } } - return types, confusables, runeCounts + return results } diff --git a/package/gitea/source/modules/charset/escape_test.go b/package/gitea/source/modules/charset/escape_test.go index 9d796a0c..4e1ff0fc 100644 --- a/package/gitea/source/modules/charset/escape_test.go +++ b/package/gitea/source/modules/charset/escape_test.go @@ -4,7 +4,6 @@ package charset import ( - "regexp" "strings" "testing" @@ -13,6 +12,7 @@ import ( "code.gitea.io/gitea/modules/translation" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type escapeControlTest struct { @@ -57,24 +57,24 @@ var escapeControlTests = []escapeControlTest{ status: EscapeStatus{}, }, { - name: "hebrew", + name: "hebrew", // old test was wrong, such text shouldn't be escaped text: "עד תקופת יוון העתיקה היה העיסוק במתמטיקה תכליתי בלבד: היא שימשה כאוסף של נוסחאות לחישוב קרקע, אוכלוסין וכו'. פריצת הדרך של היוונים, פרט לתרומותיהם הגדולות לידע המתמטי, הייתה בלימוד המתמטיקה כשלעצמה, מתוקף ערכה הרוחני. יחסם של חלק מהיוונים הקדמונים למתמטיקה היה דתי - למשל, הכת שאסף סביבו פיתגורס האמינה כי המתמטיקה היא הבסיס לכל הדברים. היוונים נחשבים ליוצרי מושג ההוכחה המתמטית, וכן לראשונים שעסקו במתמטיקה לשם עצמה, כלומר כתחום מחקרי עיוני ומופשט ולא רק כעזר שימושי. עם זאת, לצדה", - result: `עד תקופת יוון העתיקה היה העיסוק במתמטיקה תכליתי בלבד: היא שימשה כאוסף של נוסחאות לחישוב קרקע, אוכלוסין וכו'. פריצת הדרך של היוונים, פרט לתרומותיהם הגדולות לידע המתמטי, הייתה בלימוד המתמטיקה כשלעצמה, מתוקף ערכה הרוחני. יחסם של חלק מהיוונים הקדמונים למתמטיקה היה דתי - למשל, הכת שאסף סביבו פיתגורס האמינה כי המתמטיקה היא הבסיס לכל הדברים. היוונים נחשבים ליוצרי מושג ההוכחה המתמטית, וכן לראשונים שעסקו במתמטיקה לשם עצמה, כלומר כתחום מחקרי עיוני ומופשט ולא רק כעזר שימושי. עם זאת, לצדה`, - status: EscapeStatus{Escaped: true, HasAmbiguous: true}, + result: "עד תקופת יוון העתיקה היה העיסוק במתמטיקה תכליתי בלבד: היא שימשה כאוסף של נוסחאות לחישוב קרקע, אוכלוסין וכו'. פריצת הדרך של היוונים, פרט לתרומותיהם הגדולות לידע המתמטי, הייתה בלימוד המתמטיקה כשלעצמה, מתוקף ערכה הרוחני. יחסם של חלק מהיוונים הקדמונים למתמטיקה היה דתי - למשל, הכת שאסף סביבו פיתגורס האמינה כי המתמטיקה היא הבסיס לכל הדברים. היוונים נחשבים ליוצרי מושג ההוכחה המתמטית, וכן לראשונים שעסקו במתמטיקה לשם עצמה, כלומר כתחום מחקרי עיוני ומופשט ולא רק כעזר שימושי. עם זאת, לצדה", + status: EscapeStatus{}, }, { - name: "more hebrew", + name: "more hebrew", // old test was wrong, such text shouldn't be escaped text: `בתקופה מאוחרת יותר, השתמשו היוונים בשיטת סימון מתקדמת יותר, שבה הוצגו המספרים לפי 22 אותיות האלפבית היווני. לסימון המספרים בין 1 ל-9 נקבעו תשע האותיות הראשונות, בתוספת גרש ( ' ) בצד ימין של האות, למעלה; תשע האותיות הבאות ייצגו את העשרות מ-10 עד 90, והבאות את המאות. לסימון הספרות בין 1000 ל-900,000, השתמשו היוונים באותן אותיות, אך הוסיפו לאותיות את הגרש דווקא מצד שמאל של האותיות, למטה. ממיליון ומעלה, כנראה השתמשו היוונים בשני תגים במקום אחד. המתמטיקאי הבולט הראשון ביוון העתיקה, ויש האומרים בתולדות האנושות, הוא תאלס (624 לפנה"ס - 546 לפנה"ס בקירוב).[1] לא יהיה זה משולל יסוד להניח שהוא האדם הראשון שהוכיח משפט מתמטי, ולא רק גילה אותו. תאלס הוכיח שישרים מקבילים חותכים מצד אחד של שוקי זווית קטעים בעלי יחסים שווים (משפט תאלס הראשון), שהזווית המונחת על קוטר במעגל היא זווית ישרה (משפט תאלס השני), שהקוטר מחלק את המעגל לשני חלקים שווים, ושזוויות הבסיס במשולש שווה-שוקיים שוות זו לזו. מיוחסות לו גם שיטות למדידת גובהן של הפירמידות בעזרת מדידת צילן ולקביעת מיקומה של ספינה הנראית מן החוף. בשנים 582 לפנה"ס עד 496 לפנה"ס, בקירוב, חי מתמטיקאי חשוב במיוחד - פיתגורס. המקורות הראשוניים עליו מועטים, וההיסטוריונים מתקשים להפריד את העובדות משכבת המסתורין והאגדות שנקשרו בו. ידוע שסביבו התקבצה האסכולה הפיתגוראית מעין כת פסבדו-מתמטית שהאמינה ש"הכל מספר", או ליתר דיוק הכל ניתן לכימות, וייחסה למספרים משמעויות מיסטיות. ככל הנראה הפיתגוראים ידעו לבנות את הגופים האפלטוניים, הכירו את הממוצע האריתמטי, הממוצע הגאומטרי והממוצע ההרמוני והגיעו להישגים חשובים נוספים. ניתן לומר שהפיתגוראים גילו את היותו של השורש הריבועי של 2, שהוא גם האלכסון בריבוע שאורך צלעותיו 1, אי רציונלי, אך תגליתם הייתה למעשה רק שהקטעים "חסרי מידה משותפת", ומושג המספר האי רציונלי מאוחר יותר.[2] אזכור ראשון לקיומם של קטעים חסרי מידה משותפת מופיע בדיאלוג "תאיטיטוס" של אפלטון, אך רעיון זה היה מוכר עוד קודם לכן, במאה החמישית לפנה"ס להיפאסוס, בן האסכולה הפיתגוראית, ואולי לפיתגורס עצמו.[3]`, - result: `בתקופה מאוחרת יותר, השתמשו היוונים בשיטת סימון מתקדמת יותר, שבה הוצגו המספרים לפי 22 אותיות האלפבית היווני. לסימון המספרים בין 1 ל-9 נקבעו תשע האותיות הראשונות, בתוספת גרש ( ' ) בצד ימין של האות, למעלה; תשע האותיות הבאות ייצגו את העשרות מ-10 עד 90, והבאות את המאות. לסימון הספרות בין 1000 ל-900,000, השתמשו היוונים באותן אותיות, אך הוסיפו לאותיות את הגרש דווקא מצד שמאל של האותיות, למטה. ממיליון ומעלה, כנראה השתמשו היוונים בשני תגים במקום אחד. + result: `בתקופה מאוחרת יותר, השתמשו היוונים בשיטת סימון מתקדמת יותר, שבה הוצגו המספרים לפי 22 אותיות האלפבית היווני. לסימון המספרים בין 1 ל-9 נקבעו תשע האותיות הראשונות, בתוספת גרש ( ' ) בצד ימין של האות, למעלה; תשע האותיות הבאות ייצגו את העשרות מ-10 עד 90, והבאות את המאות. לסימון הספרות בין 1000 ל-900,000, השתמשו היוונים באותן אותיות, אך הוסיפו לאותיות את הגרש דווקא מצד שמאל של האותיות, למטה. ממיליון ומעלה, כנראה השתמשו היוונים בשני תגים במקום אחד. - המתמטיקאי הבולט הראשון ביוון העתיקה, ויש האומרים בתולדות האנושות, הוא תאלס (624 לפנה"ס - 546 לפנה"ס בקירוב).[1] לא יהיה זה משולל יסוד להניח שהוא האדם הראשון שהוכיח משפט מתמטי, ולא רק גילה אותו. תאלס הוכיח שישרים מקבילים חותכים מצד אחד של שוקי זווית קטעים בעלי יחסים שווים (משפט תאלס הראשון), שהזווית המונחת על קוטר במעגל היא זווית ישרה (משפט תאלס השני), שהקוטר מחלק את המעגל לשני חלקים שווים, ושזוויות הבסיס במשולש שווה-שוקיים שוות זו לזו. מיוחסות לו גם שיטות למדידת גובהן של הפירמידות בעזרת מדידת צילן ולקביעת מיקומה של ספינה הנראית מן החוף. + המתמטיקאי הבולט הראשון ביוון העתיקה, ויש האומרים בתולדות האנושות, הוא תאלס (624 לפנה"ס - 546 לפנה"ס בקירוב).[1] לא יהיה זה משולל יסוד להניח שהוא האדם הראשון שהוכיח משפט מתמטי, ולא רק גילה אותו. תאלס הוכיח שישרים מקבילים חותכים מצד אחד של שוקי זווית קטעים בעלי יחסים שווים (משפט תאלס הראשון), שהזווית המונחת על קוטר במעגל היא זווית ישרה (משפט תאלס השני), שהקוטר מחלק את המעגל לשני חלקים שווים, ושזוויות הבסיס במשולש שווה-שוקיים שוות זו לזו. מיוחסות לו גם שיטות למדידת גובהן של הפירמידות בעזרת מדידת צילן ולקביעת מיקומה של ספינה הנראית מן החוף. - בשנים 582 לפנה"ס עד 496 לפנה"ס, בקירוב, חי מתמטיקאי חשוב במיוחד - פיתגורס. המקורות הראשוניים עליו מועטים, וההיסטוריונים מתקשים להפריד את העובדות משכבת המסתורין והאגדות שנקשרו בו. ידוע שסביבו התקבצה האסכולה הפיתגוראית מעין כת פסבדו-מתמטית שהאמינה ש"הכל מספר", או ליתר דיוק הכל ניתן לכימות, וייחסה למספרים משמעויות מיסטיות. ככל הנראה הפיתגוראים ידעו לבנות את הגופים האפלטוניים, הכירו את הממוצע האריתמטי, הממוצע הגאומטרי והממוצע ההרמוני והגיעו להישגים חשובים נוספים. ניתן לומר שהפיתגוראים גילו את היותו של השורש הריבועי של 2, שהוא גם האלכסון בריבוע שאורך צלעותיו 1, אי רציונלי, אך תגליתם הייתה למעשה רק שהקטעים "חסרי מידה משותפת", ומושג המספר האי רציונלי מאוחר יותר.[2] אזכור ראשון לקיומם של קטעים חסרי מידה משותפת מופיע בדיאלוג "תאיטיטוס" של אפלטון, אך רעיון זה היה מוכר עוד קודם לכן, במאה החמישית לפנה"ס להיפאסוס, בן האסכולה הפיתגוראית, ואולי לפיתגורס עצמו.[3]`, - status: EscapeStatus{Escaped: true, HasAmbiguous: true}, + בשנים 582 לפנה"ס עד 496 לפנה"ס, בקירוב, חי מתמטיקאי חשוב במיוחד - פיתגורס. המקורות הראשוניים עליו מועטים, וההיסטוריונים מתקשים להפריד את העובדות משכבת המסתורין והאגדות שנקשרו בו. ידוע שסביבו התקבצה האסכולה הפיתגוראית מעין כת פסבדו-מתמטית שהאמינה ש"הכל מספר", או ליתר דיוק הכל ניתן לכימות, וייחסה למספרים משמעויות מיסטיות. ככל הנראה הפיתגוראים ידעו לבנות את הגופים האפלטוניים, הכירו את הממוצע האריתמטי, הממוצע הגאומטרי והממוצע ההרמוני והגיעו להישגים חשובים נוספים. ניתן לומר שהפיתגוראים גילו את היותו של השורש הריבועי של 2, שהוא גם האלכסון בריבוע שאורך צלעותיו 1, אי רציונלי, אך תגליתם הייתה למעשה רק שהקטעים "חסרי מידה משותפת", ומושג המספר האי רציונלי מאוחר יותר.[2] אזכור ראשון לקיומם של קטעים חסרי מידה משותפת מופיע בדיאלוג "תאיטיטוס" של אפלטון, אך רעיון זה היה מוכר עוד קודם לכן, במאה החמישית לפנה"ס להיפאסוס, בן האסכולה הפיתגוראית, ואולי לפיתגורס עצמו.[3]`, + status: EscapeStatus{}, }, { name: "Mixed RTL+LTR", @@ -111,7 +111,7 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`, { name: "CVE testcase", text: "if access_level != \"user\u202E \u2066// Check if admin\u2069 \u2066\" {", - result: `if access_level != "user` + "\u202e" + ` ` + "\u2066" + `// Check if admin` + "\u2069" + ` ` + "\u2066" + `" {`, + result: `if access_level != "user` + "\u202e" + ` ` + "\u2066" + `// Check if admin` + "\u2069" + ` ` + "\u2066" + `" {`, status: EscapeStatus{Escaped: true, HasInvisible: true}, }, { @@ -123,7 +123,7 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`, result: `Many computer programs fail to display bidirectional text correctly. For example, the Hebrew name Sarah ` + "\u2067" + `שרה` + "\u2066\n" + `sin (ש) (which appears rightmost), then resh (ר), and finally heh (ה) (which should appear leftmost).` + - "\n" + `if access_level != "user` + "\u202e" + ` ` + "\u2066" + `// Check if admin` + "\u2069" + ` ` + "\u2066" + `" {` + "\n", + "\n" + `if access_level != "user` + "\u202e" + ` ` + "\u2066" + `// Check if admin` + "\u2069" + ` ` + "\u2066" + `" {` + "\n", status: EscapeStatus{Escaped: true, HasInvisible: true}, }, { @@ -134,38 +134,22 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`, result: "\xef\xbb\xbftest", status: EscapeStatus{}, }, + { + name: "ambiguous", + text: "O𝐾", + result: `O𝐾`, + status: EscapeStatus{Escaped: true, HasAmbiguous: true}, + }, } func TestEscapeControlReader(t *testing.T) { - // add some control characters to the tests - tests := make([]escapeControlTest, 0, len(escapeControlTests)*3) - copy(tests, escapeControlTests) - - // if there is a BOM, we should keep the BOM - addPrefix := func(prefix, s string) string { - if strings.HasPrefix(s, "\xef\xbb\xbf") { - return s[:3] + prefix + s[3:] - } - return prefix + s - } - for _, test := range escapeControlTests { - test.name += " (+Control)" - test.text = addPrefix("\u001E", test.text) - test.result = addPrefix(``+"\u001e"+``, test.result) - test.status.Escaped = true - test.status.HasInvisible = true - tests = append(tests, test) - } - - re := regexp.MustCompile(`repo.ambiguous_character:\d+,\d+`) // simplify the output for the tests, remove the translation variants - for _, tt := range tests { + for _, tt := range escapeControlTests { t.Run(tt.name, func(t *testing.T) { output := &strings.Builder{} status, err := EscapeControlReader(strings.NewReader(tt.text), output, &translation.MockLocale{}) assert.NoError(t, err) assert.Equal(t, tt.status, *status) outStr := output.String() - outStr = re.ReplaceAllString(outStr, "repo.ambiguous_character") assert.Equal(t, tt.result, outStr) }) } @@ -179,3 +163,50 @@ func TestSettingAmbiguousUnicodeDetection(t *testing.T) { _, out = EscapeControlHTML("a test", &translation.MockLocale{}) assert.EqualValues(t, `a test`, out) } + +func TestHTMLChunkReader(t *testing.T) { + type textPart struct { + text string + isTag bool + } + testReadChunks := func(t *testing.T, chunkSize int, input string, expected []textPart) { + r := &htmlChunkReader{in: strings.NewReader(input), readBuf: make([]byte, 0, chunkSize)} + var results []textPart + for { + parts, partIsTag, err := r.readRunes() + if err != nil { + break + } + for i, part := range parts { + results = append(results, textPart{string(part), partIsTag[i]}) + } + } + assert.Equal(t, expected, results, "chunk size: %d, input: %s", chunkSize, input) + } + + testReadChunks(t, 10, "abcghi", []textPart{ + {text: "abc", isTag: false}, + {text: "", isTag: true}, + {text: "gh", isTag: false}, + // -- chunk + {text: "i", isTag: false}, + }) + + testReadChunks(t, 10, "ghi", []textPart{ + {text: "", isTag: true}, + {text: "", isTag: true}, + // -- chunk + {text: "ghi", isTag: false}, + }) + + rune1, rune2, rune3, rune4 := "A", "é", "啊", "🌞" + require.Len(t, rune1, 1) + require.Len(t, rune2, 2) + require.Len(t, rune3, 3) + require.Len(t, rune4, 4) + input := "<" + rune1 + rune2 + rune3 + rune4 + ">" + rune1 + rune2 + rune3 + rune4 + testReadChunks(t, 4, input, []textPart{{"", true}, {"Aé", false}, {"啊", false}, {"🌞", false}}) + testReadChunks(t, 5, input, []textPart{{"", true}, {"Aé", false}, {"啊", false}, {"🌞", false}}) + testReadChunks(t, 6, input, []textPart{{"", true}, {"A", false}, {"é啊", false}, {"🌞", false}}) + testReadChunks(t, 7, input, []textPart{{"", true}, {"A", false}, {"é啊", false}, {"🌞", false}}) +} diff --git a/package/gitea/source/modules/charset/ambiguous/ambiguous.json b/package/gitea/source/modules/charset/generate/ambiguous.json similarity index 100% rename from package/gitea/source/modules/charset/ambiguous/ambiguous.json rename to package/gitea/source/modules/charset/generate/ambiguous.json diff --git a/package/gitea/source/modules/charset/invisible/generate.go b/package/gitea/source/modules/charset/generate/generate.go similarity index 57% rename from package/gitea/source/modules/charset/invisible/generate.go rename to package/gitea/source/modules/charset/generate/generate.go index bd57dd6c..16ea53fd 100644 --- a/package/gitea/source/modules/charset/invisible/generate.go +++ b/package/gitea/source/modules/charset/generate/generate.go @@ -5,37 +5,86 @@ package main import ( "bytes" - "flag" "fmt" "go/format" + "log" "os" + "sort" "text/template" + "unicode" + + "code.gitea.io/gitea/modules/json" "golang.org/x/text/unicode/rangetable" ) +// ambiguous.json provides a one to one mapping of ambiguous characters to other characters +// See https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json + +type AmbiguousTable struct { + Confusable []rune + With []rune + Locale string + RangeTable *unicode.RangeTable +} + +type RunePair struct { + Confusable rune + With rune +} + // InvisibleRunes these are runes that vscode has assigned to be invisible // See https://github.com/hediet/vscode-unicode-data var InvisibleRunes = []rune{ 9, 10, 11, 12, 13, 32, 127, 160, 173, 847, 1564, 4447, 4448, 6068, 6069, 6155, 6156, 6157, 6158, 7355, 7356, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8203, 8204, 8205, 8206, 8207, 8234, 8235, 8236, 8237, 8238, 8239, 8287, 8288, 8289, 8290, 8291, 8292, 8293, 8294, 8295, 8296, 8297, 8298, 8299, 8300, 8301, 8302, 8303, 10240, 12288, 12644, 65024, 65025, 65026, 65027, 65028, 65029, 65030, 65031, 65032, 65033, 65034, 65035, 65036, 65037, 65038, 65039, 65279, 65440, 65520, 65521, 65522, 65523, 65524, 65525, 65526, 65527, 65528, 65532, 78844, 119155, 119156, 119157, 119158, 119159, 119160, 119161, 119162, 917504, 917505, 917506, 917507, 917508, 917509, 917510, 917511, 917512, 917513, 917514, 917515, 917516, 917517, 917518, 917519, 917520, 917521, 917522, 917523, 917524, 917525, 917526, 917527, 917528, 917529, 917530, 917531, 917532, 917533, 917534, 917535, 917536, 917537, 917538, 917539, 917540, 917541, 917542, 917543, 917544, 917545, 917546, 917547, 917548, 917549, 917550, 917551, 917552, 917553, 917554, 917555, 917556, 917557, 917558, 917559, 917560, 917561, 917562, 917563, 917564, 917565, 917566, 917567, 917568, 917569, 917570, 917571, 917572, 917573, 917574, 917575, 917576, 917577, 917578, 917579, 917580, 917581, 917582, 917583, 917584, 917585, 917586, 917587, 917588, 917589, 917590, 917591, 917592, 917593, 917594, 917595, 917596, 917597, 917598, 917599, 917600, 917601, 917602, 917603, 917604, 917605, 917606, 917607, 917608, 917609, 917610, 917611, 917612, 917613, 917614, 917615, 917616, 917617, 917618, 917619, 917620, 917621, 917622, 917623, 917624, 917625, 917626, 917627, 917628, 917629, 917630, 917631, 917760, 917761, 917762, 917763, 917764, 917765, 917766, 917767, 917768, 917769, 917770, 917771, 917772, 917773, 917774, 917775, 917776, 917777, 917778, 917779, 917780, 917781, 917782, 917783, 917784, 917785, 917786, 917787, 917788, 917789, 917790, 917791, 917792, 917793, 917794, 917795, 917796, 917797, 917798, 917799, 917800, 917801, 917802, 917803, 917804, 917805, 917806, 917807, 917808, 917809, 917810, 917811, 917812, 917813, 917814, 917815, 917816, 917817, 917818, 917819, 917820, 917821, 917822, 917823, 917824, 917825, 917826, 917827, 917828, 917829, 917830, 917831, 917832, 917833, 917834, 917835, 917836, 917837, 917838, 917839, 917840, 917841, 917842, 917843, 917844, 917845, 917846, 917847, 917848, 917849, 917850, 917851, 917852, 917853, 917854, 917855, 917856, 917857, 917858, 917859, 917860, 917861, 917862, 917863, 917864, 917865, 917866, 917867, 917868, 917869, 917870, 917871, 917872, 917873, 917874, 917875, 917876, 917877, 917878, 917879, 917880, 917881, 917882, 917883, 917884, 917885, 917886, 917887, 917888, 917889, 917890, 917891, 917892, 917893, 917894, 917895, 917896, 917897, 917898, 917899, 917900, 917901, 917902, 917903, 917904, 917905, 917906, 917907, 917908, 917909, 917910, 917911, 917912, 917913, 917914, 917915, 917916, 917917, 917918, 917919, 917920, 917921, 917922, 917923, 917924, 917925, 917926, 917927, 917928, 917929, 917930, 917931, 917932, 917933, 917934, 917935, 917936, 917937, 917938, 917939, 917940, 917941, 917942, 917943, 917944, 917945, 917946, 917947, 917948, 917949, 917950, 917951, 917952, 917953, 917954, 917955, 917956, 917957, 917958, 917959, 917960, 917961, 917962, 917963, 917964, 917965, 917966, 917967, 917968, 917969, 917970, 917971, 917972, 917973, 917974, 917975, 917976, 917977, 917978, 917979, 917980, 917981, 917982, 917983, 917984, 917985, 917986, 917987, 917988, 917989, 917990, 917991, 917992, 917993, 917994, 917995, 917996, 917997, 917998, 917999, } -var verbose bool - -func main() { - flag.Usage = func() { - fmt.Fprintf(os.Stderr, `%s: Generate InvisibleRunesRange - -Usage: %[1]s [-v] [-o output.go] -`, os.Args[0]) - flag.PrintDefaults() +func generateAmbiguous() { + bs, err := os.ReadFile("ambiguous.json") + if err != nil { + log.Fatalf("Unable to read, err: %v", err) } - output := "" - flag.BoolVar(&verbose, "v", false, "verbose output") - flag.StringVar(&output, "o", "invisible_gen.go", "file to output to") - flag.Parse() + var unwrapped string + if err := json.Unmarshal(bs, &unwrapped); err != nil { + log.Fatalf("Unable to unwrap content in, err: %v", err) + } + fromJSON := map[string][]uint32{} + if err := json.Unmarshal([]byte(unwrapped), &fromJSON); err != nil { + log.Fatalf("Unable to unmarshal content in, err: %v", err) + } + + tables := make([]*AmbiguousTable, 0, len(fromJSON)) + for locale, chars := range fromJSON { + table := &AmbiguousTable{Locale: locale} + table.Confusable = make([]rune, 0, len(chars)/2) + table.With = make([]rune, 0, len(chars)/2) + pairs := make([]RunePair, len(chars)/2) + for i := 0; i < len(chars); i += 2 { + pairs[i/2].Confusable, pairs[i/2].With = rune(chars[i]), rune(chars[i+1]) + } + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].Confusable < pairs[j].Confusable + }) + for _, pair := range pairs { + table.Confusable = append(table.Confusable, pair.Confusable) + table.With = append(table.With, pair.With) + } + table.RangeTable = rangetable.New(table.Confusable...) + tables = append(tables, table) + } + sort.Slice(tables, func(i, j int) bool { + return tables[i].Locale < tables[j].Locale + }) + data := map[string]any{"Tables": tables} + + if err := runTemplate(templateAmbiguous, "../ambiguous_gen.go", &data); err != nil { + log.Fatalf("Unable to run template: %v", err) + } +} + +func generateInvisible() { // First we filter the runes to remove // filtered := make([]rune, 0, len(InvisibleRunes)) @@ -47,8 +96,8 @@ Usage: %[1]s [-v] [-o output.go] } table := rangetable.New(filtered...) - if err := runTemplate(generatorTemplate, output, table); err != nil { - fatalf("Unable to run template: %v", err) + if err := runTemplate(generatorInvisible, "../invisible_gen.go", table); err != nil { + log.Fatalf("Unable to run template: %v", err) } } @@ -59,7 +108,7 @@ func runTemplate(t *template.Template, filename string, data any) error { } bs, err := format.Source(buf.Bytes()) if err != nil { - verbosef("Bad source:\n%s", buf.String()) + log.Printf("Bad source:\n%s", buf.String()) return fmt.Errorf("unable to format source: %w", err) } @@ -85,37 +134,68 @@ func runTemplate(t *template.Template, filename string, data any) error { return nil } -var generatorTemplate = template.Must(template.New("invisibleTemplate").Parse(`// This file is generated by modules/charset/invisible/generate.go DO NOT EDIT -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT +func main() { + generateAmbiguous() + generateInvisible() +} +var templateAmbiguous = template.Must(template.New("ambiguousTemplate").Parse(`// This file is generated by modules/charset/generate/generate.go DO NOT EDIT +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT package charset import "unicode" -var InvisibleRanges = &unicode.RangeTable{ - R16: []unicode.Range16{ -{{range .R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, -{{end}} }, - R32: []unicode.Range32{ -{{range .R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, -{{end}} }, - LatinOffset: {{.LatinOffset}}, +// This file is generated from https://github.com/hediet/vscode-unicode-data/blob/main/out/ambiguous.json + +// AmbiguousTable matches a confusable rune with its partner for the Locale +type AmbiguousTable struct { + Confusable []rune + With []rune + Locale string + RangeTable *unicode.RangeTable +} + +func newAmbiguousTableMap() map[string]*AmbiguousTable { + return map[string]*AmbiguousTable { + {{- range .Tables}} + {{printf "%q" .Locale}}: { + Confusable: []rune{ {{range .Confusable}}{{.}},{{end}} }, + With: []rune{ {{range .With}}{{.}},{{end}} }, + Locale: {{printf "%q" .Locale}}, + RangeTable: &unicode.RangeTable{ + R16: []unicode.Range16{ + {{range .RangeTable.R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, + {{end}} }, + R32: []unicode.Range32{ + {{range .RangeTable.R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, + {{end}} }, + LatinOffset: {{.RangeTable.LatinOffset}}, + }, + }, + {{end}} + } } `)) -func logf(format string, args ...any) { - fmt.Fprintf(os.Stderr, format+"\n", args...) -} +var generatorInvisible = template.Must(template.New("invisibleTemplate").Parse(`// This file is generated by modules/charset/generate/generate.go DO NOT EDIT +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT -func verbosef(format string, args ...any) { - if verbose { - logf(format, args...) +package charset + +import "unicode" + +func newInvisibleRangeTable() *unicode.RangeTable { + return &unicode.RangeTable{ + R16: []unicode.Range16{ +{{range .R16 }} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, + {{end}}}, + R32: []unicode.Range32{ +{{range .R32}} {Lo:{{.Lo}}, Hi:{{.Hi}}, Stride: {{.Stride}}}, + {{end}}}, + LatinOffset: {{.LatinOffset}}, } } - -func fatalf(format string, args ...any) { - logf("fatal: "+format+"\n", args...) - os.Exit(1) -} +`)) diff --git a/package/gitea/source/modules/charset/htmlstream.go b/package/gitea/source/modules/charset/htmlstream.go deleted file mode 100644 index 61f29120..00000000 --- a/package/gitea/source/modules/charset/htmlstream.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package charset - -import ( - "fmt" - "io" - - "golang.org/x/net/html" -) - -// HTMLStreamer represents a SAX-like interface for HTML -type HTMLStreamer interface { - Error(err error) error - Doctype(data string) error - Comment(data string) error - StartTag(data string, attrs ...html.Attribute) error - SelfClosingTag(data string, attrs ...html.Attribute) error - EndTag(data string) error - Text(data string) error -} - -// PassthroughHTMLStreamer is a passthrough streamer -type PassthroughHTMLStreamer struct { - next HTMLStreamer -} - -func NewPassthroughStreamer(next HTMLStreamer) *PassthroughHTMLStreamer { - return &PassthroughHTMLStreamer{next: next} -} - -var _ (HTMLStreamer) = &PassthroughHTMLStreamer{} - -// Error tells the next streamer in line that there is an error -func (p *PassthroughHTMLStreamer) Error(err error) error { - return p.next.Error(err) -} - -// Doctype tells the next streamer what the doctype is -func (p *PassthroughHTMLStreamer) Doctype(data string) error { - return p.next.Doctype(data) -} - -// Comment tells the next streamer there is a comment -func (p *PassthroughHTMLStreamer) Comment(data string) error { - return p.next.Comment(data) -} - -// StartTag tells the next streamer there is a starting tag -func (p *PassthroughHTMLStreamer) StartTag(data string, attrs ...html.Attribute) error { - return p.next.StartTag(data, attrs...) -} - -// SelfClosingTag tells the next streamer there is a self-closing tag -func (p *PassthroughHTMLStreamer) SelfClosingTag(data string, attrs ...html.Attribute) error { - return p.next.SelfClosingTag(data, attrs...) -} - -// EndTag tells the next streamer there is a end tag -func (p *PassthroughHTMLStreamer) EndTag(data string) error { - return p.next.EndTag(data) -} - -// Text tells the next streamer there is a text -func (p *PassthroughHTMLStreamer) Text(data string) error { - return p.next.Text(data) -} - -// HTMLStreamWriter acts as a writing sink -type HTMLStreamerWriter struct { - io.Writer - err error -} - -// Write implements io.Writer -func (h *HTMLStreamerWriter) Write(data []byte) (int, error) { - if h.err != nil { - return 0, h.err - } - return h.Writer.Write(data) -} - -// Write implements io.StringWriter -func (h *HTMLStreamerWriter) WriteString(data string) (int, error) { - if h.err != nil { - return 0, h.err - } - return h.Writer.Write([]byte(data)) -} - -// Error tells the next streamer in line that there is an error -func (h *HTMLStreamerWriter) Error(err error) error { - if h.err == nil { - h.err = err - } - return h.err -} - -// Doctype tells the next streamer what the doctype is -func (h *HTMLStreamerWriter) Doctype(data string) error { - _, h.err = h.WriteString("") - return h.err -} - -// Comment tells the next streamer there is a comment -func (h *HTMLStreamerWriter) Comment(data string) error { - _, h.err = h.WriteString("") - return h.err -} - -// StartTag tells the next streamer there is a starting tag -func (h *HTMLStreamerWriter) StartTag(data string, attrs ...html.Attribute) error { - return h.startTag(data, attrs, false) -} - -// SelfClosingTag tells the next streamer there is a self-closing tag -func (h *HTMLStreamerWriter) SelfClosingTag(data string, attrs ...html.Attribute) error { - return h.startTag(data, attrs, true) -} - -func (h *HTMLStreamerWriter) startTag(data string, attrs []html.Attribute, selfclosing bool) error { - if _, h.err = h.WriteString("<" + data); h.err != nil { - return h.err - } - for _, attr := range attrs { - if _, h.err = h.WriteString(" " + attr.Key + "=\"" + html.EscapeString(attr.Val) + "\""); h.err != nil { - return h.err - } - } - if selfclosing { - if _, h.err = h.WriteString("/>"); h.err != nil { - return h.err - } - } else { - if _, h.err = h.WriteString(">"); h.err != nil { - return h.err - } - } - return h.err -} - -// EndTag tells the next streamer there is a end tag -func (h *HTMLStreamerWriter) EndTag(data string) error { - _, h.err = h.WriteString("") - return h.err -} - -// Text tells the next streamer there is a text -func (h *HTMLStreamerWriter) Text(data string) error { - _, h.err = h.WriteString(html.EscapeString(data)) - return h.err -} - -// StreamHTML streams an html to a provided streamer -func StreamHTML(source io.Reader, streamer HTMLStreamer) error { - tokenizer := html.NewTokenizer(source) - for { - tt := tokenizer.Next() - switch tt { - case html.ErrorToken: - if tokenizer.Err() != io.EOF { - return tokenizer.Err() - } - return nil - case html.DoctypeToken: - token := tokenizer.Token() - if err := streamer.Doctype(token.Data); err != nil { - return err - } - case html.CommentToken: - token := tokenizer.Token() - if err := streamer.Comment(token.Data); err != nil { - return err - } - case html.StartTagToken: - token := tokenizer.Token() - if err := streamer.StartTag(token.Data, token.Attr...); err != nil { - return err - } - case html.SelfClosingTagToken: - token := tokenizer.Token() - if err := streamer.StartTag(token.Data, token.Attr...); err != nil { - return err - } - case html.EndTagToken: - token := tokenizer.Token() - if err := streamer.EndTag(token.Data); err != nil { - return err - } - case html.TextToken: - token := tokenizer.Token() - if err := streamer.Text(token.Data); err != nil { - return err - } - default: - return fmt.Errorf("unknown type of token: %d", tt) - } - } -} diff --git a/package/gitea/source/modules/charset/invisible_gen.go b/package/gitea/source/modules/charset/invisible_gen.go index 812f0e34..ddda875a 100644 --- a/package/gitea/source/modules/charset/invisible_gen.go +++ b/package/gitea/source/modules/charset/invisible_gen.go @@ -1,36 +1,38 @@ -// This file is generated by modules/charset/invisible/generate.go DO NOT EDIT -// Copyright 2022 The Gitea Authors. All rights reserved. +// This file is generated by modules/charset/generate/generate.go DO NOT EDIT +// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package charset import "unicode" -var InvisibleRanges = &unicode.RangeTable{ - R16: []unicode.Range16{ - {Lo: 11, Hi: 13, Stride: 1}, - {Lo: 127, Hi: 160, Stride: 33}, - {Lo: 173, Hi: 847, Stride: 674}, - {Lo: 1564, Hi: 4447, Stride: 2883}, - {Lo: 4448, Hi: 6068, Stride: 1620}, - {Lo: 6069, Hi: 6155, Stride: 86}, - {Lo: 6156, Hi: 6158, Stride: 1}, - {Lo: 7355, Hi: 7356, Stride: 1}, - {Lo: 8192, Hi: 8207, Stride: 1}, - {Lo: 8234, Hi: 8239, Stride: 1}, - {Lo: 8287, Hi: 8303, Stride: 1}, - {Lo: 10240, Hi: 12288, Stride: 2048}, - {Lo: 12644, Hi: 65024, Stride: 52380}, - {Lo: 65025, Hi: 65039, Stride: 1}, - {Lo: 65279, Hi: 65440, Stride: 161}, - {Lo: 65520, Hi: 65528, Stride: 1}, - {Lo: 65532, Hi: 65532, Stride: 1}, - }, - R32: []unicode.Range32{ - {Lo: 78844, Hi: 119155, Stride: 40311}, - {Lo: 119156, Hi: 119162, Stride: 1}, - {Lo: 917504, Hi: 917631, Stride: 1}, - {Lo: 917760, Hi: 917999, Stride: 1}, - }, - LatinOffset: 2, +func newInvisibleRangeTable() *unicode.RangeTable { + return &unicode.RangeTable{ + R16: []unicode.Range16{ + {Lo: 11, Hi: 13, Stride: 1}, + {Lo: 127, Hi: 160, Stride: 33}, + {Lo: 173, Hi: 847, Stride: 674}, + {Lo: 1564, Hi: 4447, Stride: 2883}, + {Lo: 4448, Hi: 6068, Stride: 1620}, + {Lo: 6069, Hi: 6155, Stride: 86}, + {Lo: 6156, Hi: 6158, Stride: 1}, + {Lo: 7355, Hi: 7356, Stride: 1}, + {Lo: 8192, Hi: 8207, Stride: 1}, + {Lo: 8234, Hi: 8239, Stride: 1}, + {Lo: 8287, Hi: 8303, Stride: 1}, + {Lo: 10240, Hi: 12288, Stride: 2048}, + {Lo: 12644, Hi: 65024, Stride: 52380}, + {Lo: 65025, Hi: 65039, Stride: 1}, + {Lo: 65279, Hi: 65440, Stride: 161}, + {Lo: 65520, Hi: 65528, Stride: 1}, + {Lo: 65532, Hi: 65532, Stride: 1}, + }, + R32: []unicode.Range32{ + {Lo: 78844, Hi: 119155, Stride: 40311}, + {Lo: 119156, Hi: 119162, Stride: 1}, + {Lo: 917504, Hi: 917631, Stride: 1}, + {Lo: 917760, Hi: 917999, Stride: 1}, + }, + LatinOffset: 2, + } } diff --git a/package/gitea/source/modules/commitstatus/commit_status.go b/package/gitea/source/modules/commitstatus/commit_status.go index a0ab4e71..9c699424 100644 --- a/package/gitea/source/modules/commitstatus/commit_status.go +++ b/package/gitea/source/modules/commitstatus/commit_status.go @@ -61,16 +61,17 @@ type CommitStatusStates []CommitStatusState //nolint:revive // export stutter // According to https://docs.github.com/en/rest/commits/statuses?apiVersion=2022-11-28#get-the-combined-status-for-a-specific-reference // > Additionally, a combined state is returned. The state is one of: // > failure if any of the contexts report as error or failure +// > failure if any of the contexts report as warning (Gitea specific behavior) // > pending if there are no statuses or a context is pending // > success if the latest status for all contexts is success func (css CommitStatusStates) Combine() CommitStatusState { successCnt := 0 for _, state := range css { switch { - case state.IsError() || state.IsFailure(): + case state.IsError() || state.IsFailure() || state.IsWarning(): return CommitStatusFailure case state.IsPending(): - case state.IsSuccess() || state.IsWarning() || state.IsSkipped(): + case state.IsSuccess() || state.IsSkipped(): successCnt++ } } diff --git a/package/gitea/source/modules/commitstatus/commit_status_test.go b/package/gitea/source/modules/commitstatus/commit_status_test.go index 10d8f20a..dc8f9ce1 100644 --- a/package/gitea/source/modules/commitstatus/commit_status_test.go +++ b/package/gitea/source/modules/commitstatus/commit_status_test.go @@ -41,7 +41,7 @@ func TestCombine(t *testing.T) { { name: "warning", states: CommitStatusStates{CommitStatusWarning}, - expected: CommitStatusSuccess, + expected: CommitStatusFailure, }, // 2 states { @@ -62,7 +62,7 @@ func TestCombine(t *testing.T) { { name: "pending and warning", states: CommitStatusStates{CommitStatusPending, CommitStatusWarning}, - expected: CommitStatusPending, + expected: CommitStatusFailure, }, { name: "success and error", @@ -77,7 +77,7 @@ func TestCombine(t *testing.T) { { name: "success and warning", states: CommitStatusStates{CommitStatusSuccess, CommitStatusWarning}, - expected: CommitStatusSuccess, + expected: CommitStatusFailure, }, { name: "error and failure", @@ -98,7 +98,7 @@ func TestCombine(t *testing.T) { { name: "pending, success and warning", states: CommitStatusStates{CommitStatusPending, CommitStatusSuccess, CommitStatusWarning}, - expected: CommitStatusPending, + expected: CommitStatusFailure, }, { name: "pending, success and error", @@ -133,7 +133,7 @@ func TestCombine(t *testing.T) { { name: "success, warning and skipped", states: CommitStatusStates{CommitStatusSuccess, CommitStatusWarning, CommitStatusSkipped}, - expected: CommitStatusSuccess, + expected: CommitStatusFailure, }, // All success { @@ -181,12 +181,12 @@ func TestCombine(t *testing.T) { { name: "mixed states with all success", states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusPending, CommitStatusWarning}, - expected: CommitStatusPending, + expected: CommitStatusFailure, }, { name: "all success with warning", states: CommitStatusStates{CommitStatusSuccess, CommitStatusSuccess, CommitStatusSuccess, CommitStatusWarning}, - expected: CommitStatusSuccess, + expected: CommitStatusFailure, }, } diff --git a/package/gitea/source/modules/dump/dumper.go b/package/gitea/source/modules/dump/dumper.go index 02829d6a..2f160707 100644 --- a/package/gitea/source/modules/dump/dumper.go +++ b/package/gitea/source/modules/dump/dumper.go @@ -4,6 +4,7 @@ package dump import ( + "archive/zip" "context" "errors" "fmt" @@ -85,7 +86,7 @@ func NewDumper(ctx context.Context, format string, output io.Writer) (*Dumper, e var comp archives.ArchiverAsync switch format { case "zip": - comp = archives.Zip{} + comp = archives.Zip{Compression: zip.Deflate} case "tar": comp = archives.Tar{} case "tar.sz": diff --git a/package/gitea/source/modules/emoji/emoji_data.go b/package/gitea/source/modules/emoji/emoji_data.go index 8d0ae0a4..ef25e3d9 100644 --- a/package/gitea/source/modules/emoji/emoji_data.go +++ b/package/gitea/source/modules/emoji/emoji_data.go @@ -4,7 +4,7 @@ package emoji // Code generated by build/generate-emoji.go. DO NOT EDIT. -// Sourced from https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json +// Sourced from https://raw.githubusercontent.com/rhysd/gemoji/537ff2d7e0496e9964824f7f73ec7ece88c9765a/db/emoji.json var GemojiData = Gemoji{ {"\U0001f44d", "thumbs up", []string{"+1", "thumbsup"}, "6.0", true}, {"\U0001f44d\U0001f3ff", "thumbs up: Dark Skin Tone", []string{"+1_Dark_Skin_Tone"}, "12.0", false}, @@ -345,10 +345,12 @@ var GemojiData = Gemoji{ {"\U0001f1ee\U0001f1f4", "flag: British Indian Ocean Territory", []string{"british_indian_ocean_territory"}, "6.0", false}, {"\U0001f1fb\U0001f1ec", "flag: British Virgin Islands", []string{"british_virgin_islands"}, "6.0", false}, {"\U0001f966", "broccoli", []string{"broccoli"}, "11.0", false}, + {"\u26d3\ufe0f\u200d\U0001f4a5", "broken chain", []string{"broken_chain"}, "15.1", false}, {"\U0001f494", "broken heart", []string{"broken_heart"}, "6.0", false}, {"\U0001f9f9", "broom", []string{"broom"}, "11.0", false}, {"\U0001f7e4", "brown circle", []string{"brown_circle"}, "12.0", false}, {"\U0001f90e", "brown heart", []string{"brown_heart"}, "12.0", false}, + {"\U0001f344\u200d\U0001f7eb", "brown mushroom", []string{"brown_mushroom"}, "15.1", false}, {"\U0001f7eb", "brown square", []string{"brown_square"}, "12.0", false}, {"\U0001f1e7\U0001f1f3", "flag: Brunei", []string{"brunei"}, "6.0", false}, {"\U0001f9cb", "bubble tea", []string{"bubble_tea"}, "13.0", false}, @@ -838,6 +840,7 @@ var GemojiData = Gemoji{ {"\U0001f62e\u200d\U0001f4a8", "face exhaling", []string{"face_exhaling"}, "13.1", false}, {"\U0001f979", "face holding back tears", []string{"face_holding_back_tears"}, "14.0", false}, {"\U0001f636\u200d\U0001f32b\ufe0f", "face in clouds", []string{"face_in_clouds"}, "13.1", false}, + {"\U0001fae9", "face with bags under eyes", []string{"face_with_bags_under_eyes"}, "16.0", false}, {"\U0001fae4", "face with diagonal mouth", []string{"face_with_diagonal_mouth"}, "14.0", false}, {"\U0001f915", "face with head-bandage", []string{"face_with_head_bandage"}, "8.0", false}, {"\U0001fae2", "face with open eyes and hand over mouth", []string{"face_with_open_eyes_and_hand_over_mouth"}, "14.0", false}, @@ -879,6 +882,10 @@ var GemojiData = Gemoji{ {"\U0001f1eb\U0001f1f0", "flag: Falkland Islands", []string{"falkland_islands"}, "6.0", false}, {"\U0001f342", "fallen leaf", []string{"fallen_leaf"}, "6.0", false}, {"\U0001f46a", "family", []string{"family"}, "6.0", false}, + {"\U0001f9d1\u200d\U0001f9d1\u200d\U0001f9d2", "family: adult, adult, child", []string{"family_adult_adult_child"}, "15.1", false}, + {"\U0001f9d1\u200d\U0001f9d1\u200d\U0001f9d2\u200d\U0001f9d2", "family: adult, adult, child, child", []string{"family_adult_adult_child_child"}, "15.1", false}, + {"\U0001f9d1\u200d\U0001f9d2", "family: adult, child", []string{"family_adult_child"}, "15.1", false}, + {"\U0001f9d1\u200d\U0001f9d2\u200d\U0001f9d2", "family: adult, child, child", []string{"family_adult_child_child"}, "15.1", false}, {"\U0001f468\u200d\U0001f466", "family: man, boy", []string{"family_man_boy"}, "6.0", false}, {"\U0001f468\u200d\U0001f466\u200d\U0001f466", "family: man, boy, boy", []string{"family_man_boy_boy"}, "6.0", false}, {"\U0001f468\u200d\U0001f467", "family: man, girl", []string{"family_man_girl"}, "6.0", false}, @@ -931,6 +938,7 @@ var GemojiData = Gemoji{ {"\U0001f4c1", "file folder", []string{"file_folder"}, "6.0", false}, {"\U0001f4fd\ufe0f", "film projector", []string{"film_projector"}, "7.0", false}, {"\U0001f39e\ufe0f", "film frames", []string{"film_strip"}, "7.0", false}, + {"\U0001fac6", "fingerprint", []string{"fingerprint"}, "16.0", false}, {"\U0001f1eb\U0001f1ee", "flag: Finland", []string{"finland"}, "6.0", false}, {"\U0001f525", "fire", []string{"fire"}, "6.0", false}, {"\U0001f692", "fire engine", []string{"fire_engine"}, "6.0", false}, @@ -973,6 +981,7 @@ var GemojiData = Gemoji{ {"\U0001f91c\U0001f3fc", "right-facing fist: Medium-Light Skin Tone", []string{"fist_right_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f91c\U0001f3fd", "right-facing fist: Medium Skin Tone", []string{"fist_right_Medium_Skin_Tone"}, "12.0", false}, {"5\ufe0f\u20e3", "keycap: 5", []string{"five"}, "", false}, + {"\U0001f1e8\U0001f1f6", "flag: Sark", []string{"flag_sark"}, "16.0", false}, {"\U0001f38f", "carp streamer", []string{"flags"}, "6.0", false}, {"\U0001f9a9", "flamingo", []string{"flamingo"}, "12.0", false}, {"\U0001f526", "flashlight", []string{"flashlight"}, "6.0", false}, @@ -1189,9 +1198,12 @@ var GemojiData = Gemoji{ {"\U0001f91d\U0001f3fc", "handshake: Medium-Light Skin Tone", []string{"handshake_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f91d\U0001f3fd", "handshake: Medium Skin Tone", []string{"handshake_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f4a9", "pile of poo", []string{"hankey", "poop", "shit"}, "6.0", false}, + {"\U0001fa89", "harp", []string{"harp"}, "16.0", false}, {"#\ufe0f\u20e3", "keycap: #", []string{"hash"}, "", false}, {"\U0001f425", "front-facing baby chick", []string{"hatched_chick"}, "6.0", false}, {"\U0001f423", "hatching chick", []string{"hatching_chick"}, "6.0", false}, + {"\U0001f642\u200d\u2194\ufe0f", "head shaking horizontally", []string{"head_shaking_horizontally"}, "15.1", false}, + {"\U0001f642\u200d\u2195\ufe0f", "head shaking vertically", []string{"head_shaking_vertically"}, "15.1", false}, {"\U0001f3a7", "headphone", []string{"headphones"}, "6.0", false}, {"\U0001faa6", "headstone", []string{"headstone"}, "13.0", false}, {"\U0001f9d1\u200d\u2695\ufe0f", "health worker", []string{"health_worker"}, "12.1", true}, @@ -1380,6 +1392,7 @@ var GemojiData = Gemoji{ {"\u271d\ufe0f", "latin cross", []string{"latin_cross"}, "", false}, {"\U0001f1f1\U0001f1fb", "flag: Latvia", []string{"latvia"}, "6.0", false}, {"\U0001f606", "grinning squinting face", []string{"laughing", "satisfied", "laugh"}, "6.0", false}, + {"\U0001fabe", "leafless tree", []string{"leafless_tree"}, "16.0", false}, {"\U0001f96c", "leafy green", []string{"leafy_green"}, "11.0", false}, {"\U0001f343", "leaf fluttering in wind", []string{"leaves"}, "6.0", false}, {"\U0001f1f1\U0001f1e7", "flag: Lebanon", []string{"lebanon"}, "6.0", false}, @@ -1417,6 +1430,7 @@ var GemojiData = Gemoji{ {"\U0001f1f1\U0001f1ee", "flag: Liechtenstein", []string{"liechtenstein"}, "6.0", false}, {"\U0001fa75", "light blue heart", []string{"light_blue_heart"}, "15.0", false}, {"\U0001f688", "light rail", []string{"light_rail"}, "6.0", false}, + {"\U0001f34b\u200d\U0001f7e9", "lime", []string{"lime"}, "15.1", false}, {"\U0001f517", "link", []string{"link"}, "6.0", false}, {"\U0001f981", "lion", []string{"lion"}, "8.0", false}, {"\U0001f444", "mouth", []string{"lips"}, "6.0", false}, @@ -1594,12 +1608,24 @@ var GemojiData = Gemoji{ {"\U0001f468\U0001f3fe\u200d\U0001f9bd", "man in manual wheelchair: Medium-Dark Skin Tone", []string{"man_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f9bd", "man in manual wheelchair: Medium-Light Skin Tone", []string{"man_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f9bd", "man in manual wheelchair: Medium Skin Tone", []string{"man_in_manual_wheelchair_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f468\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right", []string{"man_in_manual_wheelchair_facing_right"}, "15.1", true}, + {"\U0001f468\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Dark Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Light Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Medium-Dark Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Medium-Light Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f", "man in manual wheelchair facing right: Medium Skin Tone", []string{"man_in_manual_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\u200d\U0001f9bc", "man in motorized wheelchair", []string{"man_in_motorized_wheelchair"}, "12.0", true}, {"\U0001f468\U0001f3ff\u200d\U0001f9bc", "man in motorized wheelchair: Dark Skin Tone", []string{"man_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f9bc", "man in motorized wheelchair: Light Skin Tone", []string{"man_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fe\u200d\U0001f9bc", "man in motorized wheelchair: Medium-Dark Skin Tone", []string{"man_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f9bc", "man in motorized wheelchair: Medium-Light Skin Tone", []string{"man_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f9bc", "man in motorized wheelchair: Medium Skin Tone", []string{"man_in_motorized_wheelchair_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f468\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right", []string{"man_in_motorized_wheelchair_facing_right"}, "15.1", true}, + {"\U0001f468\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Dark Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Light Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Medium-Dark Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Medium-Light Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f", "man in motorized wheelchair facing right: Medium Skin Tone", []string{"man_in_motorized_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f935\u200d\u2642\ufe0f", "man in tuxedo", []string{"man_in_tuxedo"}, "13.0", true}, {"\U0001f935\U0001f3ff\u200d\u2642\ufe0f", "man in tuxedo: Dark Skin Tone", []string{"man_in_tuxedo_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fb\u200d\u2642\ufe0f", "man in tuxedo: Light Skin Tone", []string{"man_in_tuxedo_Light_Skin_Tone"}, "12.0", false}, @@ -1618,6 +1644,12 @@ var GemojiData = Gemoji{ {"\U0001f939\U0001f3fe\u200d\u2642\ufe0f", "man juggling: Medium-Dark Skin Tone", []string{"man_juggling_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fc\u200d\u2642\ufe0f", "man juggling: Medium-Light Skin Tone", []string{"man_juggling_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fd\u200d\u2642\ufe0f", "man juggling: Medium Skin Tone", []string{"man_juggling_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right", []string{"man_kneeling_facing_right"}, "15.1", true}, + {"\U0001f9ce\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Dark Skin Tone", []string{"man_kneeling_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Light Skin Tone", []string{"man_kneeling_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Medium-Dark Skin Tone", []string{"man_kneeling_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Medium-Light Skin Tone", []string{"man_kneeling_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man kneeling facing right: Medium Skin Tone", []string{"man_kneeling_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\u200d\U0001f527", "man mechanic", []string{"man_mechanic"}, "", true}, {"\U0001f468\U0001f3ff\u200d\U0001f527", "man mechanic: Dark Skin Tone", []string{"man_mechanic_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f527", "man mechanic: Light Skin Tone", []string{"man_mechanic_Light_Skin_Tone"}, "12.0", false}, @@ -1648,6 +1680,12 @@ var GemojiData = Gemoji{ {"\U0001f93d\U0001f3fe\u200d\u2642\ufe0f", "man playing water polo: Medium-Dark Skin Tone", []string{"man_playing_water_polo_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fc\u200d\u2642\ufe0f", "man playing water polo: Medium-Light Skin Tone", []string{"man_playing_water_polo_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fd\u200d\u2642\ufe0f", "man playing water polo: Medium Skin Tone", []string{"man_playing_water_polo_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right", []string{"man_running_facing_right"}, "15.1", true}, + {"\U0001f3c3\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Dark Skin Tone", []string{"man_running_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Light Skin Tone", []string{"man_running_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Medium-Dark Skin Tone", []string{"man_running_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Medium-Light Skin Tone", []string{"man_running_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man running facing right: Medium Skin Tone", []string{"man_running_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f468\u200d\U0001f52c", "man scientist", []string{"man_scientist"}, "", true}, {"\U0001f468\U0001f3ff\u200d\U0001f52c", "man scientist: Dark Skin Tone", []string{"man_scientist_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fb\u200d\U0001f52c", "man scientist: Light Skin Tone", []string{"man_scientist_Light_Skin_Tone"}, "12.0", false}, @@ -1684,6 +1722,12 @@ var GemojiData = Gemoji{ {"\U0001f468\U0001f3fe\u200d\U0001f4bb", "man technologist: Medium-Dark Skin Tone", []string{"man_technologist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fc\u200d\U0001f4bb", "man technologist: Medium-Light Skin Tone", []string{"man_technologist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f468\U0001f3fd\u200d\U0001f4bb", "man technologist: Medium Skin Tone", []string{"man_technologist_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right", []string{"man_walking_facing_right"}, "15.1", true}, + {"\U0001f6b6\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Dark Skin Tone", []string{"man_walking_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Light Skin Tone", []string{"man_walking_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Medium-Dark Skin Tone", []string{"man_walking_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Medium-Light Skin Tone", []string{"man_walking_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f", "man walking facing right: Medium Skin Tone", []string{"man_walking_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f472", "person with skullcap", []string{"man_with_gua_pi_mao"}, "6.0", true}, {"\U0001f472\U0001f3ff", "person with skullcap: Dark Skin Tone", []string{"man_with_gua_pi_mao_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f472\U0001f3fb", "person with skullcap: Light Skin Tone", []string{"man_with_gua_pi_mao_Light_Skin_Tone"}, "12.0", false}, @@ -1708,6 +1752,12 @@ var GemojiData = Gemoji{ {"\U0001f470\U0001f3fe\u200d\u2642\ufe0f", "man with veil: Medium-Dark Skin Tone", []string{"man_with_veil_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3fc\u200d\u2642\ufe0f", "man with veil: Medium-Light Skin Tone", []string{"man_with_veil_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3fd\u200d\u2642\ufe0f", "man with veil: Medium Skin Tone", []string{"man_with_veil_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f468\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right", []string{"man_with_white_cane_facing_right"}, "15.1", true}, + {"\U0001f468\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Dark Skin Tone", []string{"man_with_white_cane_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Light Skin Tone", []string{"man_with_white_cane_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Medium-Dark Skin Tone", []string{"man_with_white_cane_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Medium-Light Skin Tone", []string{"man_with_white_cane_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f468\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f", "man with white cane facing right: Medium Skin Tone", []string{"man_with_white_cane_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f96d", "mango", []string{"mango"}, "11.0", false}, {"\U0001f45e", "man’s shoe", []string{"mans_shoe", "shoe"}, "6.0", false}, {"\U0001f570\ufe0f", "mantelpiece clock", []string{"mantelpiece_clock"}, "7.0", false}, @@ -1874,12 +1924,12 @@ var GemojiData = Gemoji{ {"\U0001f3b5", "musical note", []string{"musical_note"}, "6.0", false}, {"\U0001f3bc", "musical score", []string{"musical_score"}, "6.0", false}, {"\U0001f507", "muted speaker", []string{"mute"}, "6.0", false}, - {"\U0001f9d1\u200d\U0001f384", "mx claus", []string{"mx_claus"}, "13.0", true}, - {"\U0001f9d1\U0001f3ff\u200d\U0001f384", "mx claus: Dark Skin Tone", []string{"mx_claus_Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fb\u200d\U0001f384", "mx claus: Light Skin Tone", []string{"mx_claus_Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fe\u200d\U0001f384", "mx claus: Medium-Dark Skin Tone", []string{"mx_claus_Medium-Dark_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fc\u200d\U0001f384", "mx claus: Medium-Light Skin Tone", []string{"mx_claus_Medium-Light_Skin_Tone"}, "12.0", false}, - {"\U0001f9d1\U0001f3fd\u200d\U0001f384", "mx claus: Medium Skin Tone", []string{"mx_claus_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\u200d\U0001f384", "Mx Claus", []string{"mx_claus"}, "13.0", true}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f384", "Mx Claus: Dark Skin Tone", []string{"mx_claus_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f384", "Mx Claus: Light Skin Tone", []string{"mx_claus_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f384", "Mx Claus: Medium-Dark Skin Tone", []string{"mx_claus_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f384", "Mx Claus: Medium-Light Skin Tone", []string{"mx_claus_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fd\u200d\U0001f384", "Mx Claus: Medium Skin Tone", []string{"mx_claus_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f1f2\U0001f1f2", "flag: Myanmar (Burma)", []string{"myanmar"}, "6.0", false}, {"\U0001f485", "nail polish", []string{"nail_care"}, "6.0", true}, {"\U0001f485\U0001f3ff", "nail polish: Dark Skin Tone", []string{"nail_care_Dark_Skin_Tone"}, "12.0", false}, @@ -2140,24 +2190,54 @@ var GemojiData = Gemoji{ {"\U0001f9d1\U0001f3fe\u200d\U0001f9bd", "person in manual wheelchair: Medium-Dark Skin Tone", []string{"person_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f9bd", "person in manual wheelchair: Medium-Light Skin Tone", []string{"person_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9bd", "person in manual wheelchair: Medium Skin Tone", []string{"person_in_manual_wheelchair_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right", []string{"person_in_manual_wheelchair_facing_right"}, "15.1", true}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Dark Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Light Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Medium-Dark Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Medium-Light Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f", "person in manual wheelchair facing right: Medium Skin Tone", []string{"person_in_manual_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\u200d\U0001f9bc", "person in motorized wheelchair", []string{"person_in_motorized_wheelchair"}, "12.1", true}, {"\U0001f9d1\U0001f3ff\u200d\U0001f9bc", "person in motorized wheelchair: Dark Skin Tone", []string{"person_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f9bc", "person in motorized wheelchair: Light Skin Tone", []string{"person_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f9bc", "person in motorized wheelchair: Medium-Dark Skin Tone", []string{"person_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f9bc", "person in motorized wheelchair: Medium-Light Skin Tone", []string{"person_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9bc", "person in motorized wheelchair: Medium Skin Tone", []string{"person_in_motorized_wheelchair_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right", []string{"person_in_motorized_wheelchair_facing_right"}, "15.1", true}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Dark Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Light Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Medium-Dark Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Medium-Light Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f", "person in motorized wheelchair facing right: Medium Skin Tone", []string{"person_in_motorized_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f935", "person in tuxedo", []string{"person_in_tuxedo"}, "9.0", true}, {"\U0001f935\U0001f3ff", "person in tuxedo: Dark Skin Tone", []string{"person_in_tuxedo_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fb", "person in tuxedo: Light Skin Tone", []string{"person_in_tuxedo_Light_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fe", "person in tuxedo: Medium-Dark Skin Tone", []string{"person_in_tuxedo_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fc", "person in tuxedo: Medium-Light Skin Tone", []string{"person_in_tuxedo_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fd", "person in tuxedo: Medium Skin Tone", []string{"person_in_tuxedo_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\u200d\u27a1\ufe0f", "person kneeling facing right", []string{"person_kneeling_facing_right"}, "15.1", true}, + {"\U0001f9ce\U0001f3ff\u200d\u27a1\ufe0f", "person kneeling facing right: Dark Skin Tone", []string{"person_kneeling_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fb\u200d\u27a1\ufe0f", "person kneeling facing right: Light Skin Tone", []string{"person_kneeling_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fe\u200d\u27a1\ufe0f", "person kneeling facing right: Medium-Dark Skin Tone", []string{"person_kneeling_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fc\u200d\u27a1\ufe0f", "person kneeling facing right: Medium-Light Skin Tone", []string{"person_kneeling_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fd\u200d\u27a1\ufe0f", "person kneeling facing right: Medium Skin Tone", []string{"person_kneeling_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\u200d\U0001f9b0", "person: red hair", []string{"person_red_hair"}, "12.1", true}, {"\U0001f9d1\U0001f3ff\u200d\U0001f9b0", "person: red hair: Dark Skin Tone", []string{"person_red_hair_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f9b0", "person: red hair: Light Skin Tone", []string{"person_red_hair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fe\u200d\U0001f9b0", "person: red hair: Medium-Dark Skin Tone", []string{"person_red_hair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fc\u200d\U0001f9b0", "person: red hair: Medium-Light Skin Tone", []string{"person_red_hair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fd\u200d\U0001f9b0", "person: red hair: Medium Skin Tone", []string{"person_red_hair_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\u200d\u27a1\ufe0f", "person running facing right", []string{"person_running_facing_right"}, "15.1", true}, + {"\U0001f3c3\U0001f3ff\u200d\u27a1\ufe0f", "person running facing right: Dark Skin Tone", []string{"person_running_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fb\u200d\u27a1\ufe0f", "person running facing right: Light Skin Tone", []string{"person_running_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fe\u200d\u27a1\ufe0f", "person running facing right: Medium-Dark Skin Tone", []string{"person_running_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fc\u200d\u27a1\ufe0f", "person running facing right: Medium-Light Skin Tone", []string{"person_running_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fd\u200d\u27a1\ufe0f", "person running facing right: Medium Skin Tone", []string{"person_running_facing_right_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\u200d\u27a1\ufe0f", "person walking facing right", []string{"person_walking_facing_right"}, "15.1", true}, + {"\U0001f6b6\U0001f3ff\u200d\u27a1\ufe0f", "person walking facing right: Dark Skin Tone", []string{"person_walking_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fb\u200d\u27a1\ufe0f", "person walking facing right: Light Skin Tone", []string{"person_walking_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fe\u200d\u27a1\ufe0f", "person walking facing right: Medium-Dark Skin Tone", []string{"person_walking_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fc\u200d\u27a1\ufe0f", "person walking facing right: Medium-Light Skin Tone", []string{"person_walking_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fd\u200d\u27a1\ufe0f", "person walking facing right: Medium Skin Tone", []string{"person_walking_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\u200d\U0001f9b3", "person: white hair", []string{"person_white_hair"}, "12.1", true}, {"\U0001f9d1\U0001f3ff\u200d\U0001f9b3", "person: white hair: Dark Skin Tone", []string{"person_white_hair_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d1\U0001f3fb\u200d\U0001f9b3", "person: white hair: Light Skin Tone", []string{"person_white_hair_Light_Skin_Tone"}, "12.0", false}, @@ -2188,9 +2268,16 @@ var GemojiData = Gemoji{ {"\U0001f470\U0001f3fe", "person with veil: Medium-Dark Skin Tone", []string{"person_with_veil_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3fc", "person with veil: Medium-Light Skin Tone", []string{"person_with_veil_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3fd", "person with veil: Medium Skin Tone", []string{"person_with_veil_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right", []string{"person_with_white_cane_facing_right"}, "15.1", true}, + {"\U0001f9d1\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Dark Skin Tone", []string{"person_with_white_cane_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Light Skin Tone", []string{"person_with_white_cane_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Medium-Dark Skin Tone", []string{"person_with_white_cane_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Medium-Light Skin Tone", []string{"person_with_white_cane_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9d1\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f", "person with white cane facing right: Medium Skin Tone", []string{"person_with_white_cane_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f1f5\U0001f1ea", "flag: Peru", []string{"peru"}, "6.0", false}, {"\U0001f9eb", "petri dish", []string{"petri_dish"}, "11.0", false}, {"\U0001f1f5\U0001f1ed", "flag: Philippines", []string{"philippines"}, "6.0", false}, + {"\U0001f426\u200d\U0001f525", "phoenix", []string{"phoenix"}, "15.1", false}, {"\u260e\ufe0f", "telephone", []string{"phone", "telephone"}, "", false}, {"\u26cf\ufe0f", "pick", []string{"pick"}, "5.2", false}, {"\U0001f6fb", "pickup truck", []string{"pickup_truck"}, "13.0", false}, @@ -2480,6 +2567,7 @@ var GemojiData = Gemoji{ {"\U0001f6fc", "roller skate", []string{"roller_skate"}, "13.0", false}, {"\U0001f1f7\U0001f1f4", "flag: Romania", []string{"romania"}, "6.0", false}, {"\U0001f413", "rooster", []string{"rooster"}, "6.0", false}, + {"\U0001fadc", "root vegetable", []string{"root_vegetable"}, "16.0", false}, {"\U0001f339", "rose", []string{"rose"}, "6.0", false}, {"\U0001f3f5\ufe0f", "rosette", []string{"rosette"}, "7.0", false}, {"\U0001f6a8", "police car light", []string{"rotating_light"}, "6.0", false}, @@ -2613,6 +2701,7 @@ var GemojiData = Gemoji{ {"\U0001f6cd\ufe0f", "shopping bags", []string{"shopping"}, "7.0", false}, {"\U0001f6d2", "shopping cart", []string{"shopping_cart"}, "9.0", false}, {"\U0001fa73", "shorts", []string{"shorts"}, "12.0", false}, + {"\U0001fa8f", "shovel", []string{"shovel"}, "16.0", false}, {"\U0001f6bf", "shower", []string{"shower"}, "6.0", false}, {"\U0001f990", "shrimp", []string{"shrimp"}, "9.0", false}, {"\U0001f937", "person shrugging", []string{"shrug"}, "11.0", true}, @@ -2711,6 +2800,7 @@ var GemojiData = Gemoji{ {"\U0001f578\ufe0f", "spider web", []string{"spider_web"}, "7.0", false}, {"\U0001f5d3\ufe0f", "spiral calendar", []string{"spiral_calendar"}, "7.0", false}, {"\U0001f5d2\ufe0f", "spiral notepad", []string{"spiral_notepad"}, "7.0", false}, + {"\U0001fadf", "splatter", []string{"splatter"}, "16.0", false}, {"\U0001f9fd", "sponge", []string{"sponge"}, "11.0", false}, {"\U0001f944", "spoon", []string{"spoon"}, "9.0", false}, {"\U0001f991", "squid", []string{"squid"}, "9.0", false}, @@ -2945,7 +3035,7 @@ var GemojiData = Gemoji{ {"\U0001f51d", "TOP arrow", []string{"top"}, "6.0", false}, {"\U0001f3a9", "top hat", []string{"tophat"}, "6.0", false}, {"\U0001f32a\ufe0f", "tornado", []string{"tornado"}, "7.0", false}, - {"\U0001f1f9\U0001f1f7", "flag: Turkey", []string{"tr"}, "8.0", false}, + {"\U0001f1f9\U0001f1f7", "flag: Türkiye", []string{"tr"}, "8.0", false}, {"\U0001f5b2\ufe0f", "trackball", []string{"trackball"}, "7.0", false}, {"\U0001f69c", "tractor", []string{"tractor"}, "6.0", false}, {"\U0001f6a5", "horizontal traffic light", []string{"traffic_light"}, "6.0", false}, @@ -3247,12 +3337,24 @@ var GemojiData = Gemoji{ {"\U0001f469\U0001f3fe\u200d\U0001f9bd", "woman in manual wheelchair: Medium-Dark Skin Tone", []string{"woman_in_manual_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f9bd", "woman in manual wheelchair: Medium-Light Skin Tone", []string{"woman_in_manual_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f9bd", "woman in manual wheelchair: Medium Skin Tone", []string{"woman_in_manual_wheelchair_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f469\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right", []string{"woman_in_manual_wheelchair_facing_right"}, "15.1", true}, + {"\U0001f469\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Dark Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Light Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Medium-Dark Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Medium-Light Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f", "woman in manual wheelchair facing right: Medium Skin Tone", []string{"woman_in_manual_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\u200d\U0001f9bc", "woman in motorized wheelchair", []string{"woman_in_motorized_wheelchair"}, "12.0", true}, {"\U0001f469\U0001f3ff\u200d\U0001f9bc", "woman in motorized wheelchair: Dark Skin Tone", []string{"woman_in_motorized_wheelchair_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f9bc", "woman in motorized wheelchair: Light Skin Tone", []string{"woman_in_motorized_wheelchair_Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fe\u200d\U0001f9bc", "woman in motorized wheelchair: Medium-Dark Skin Tone", []string{"woman_in_motorized_wheelchair_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f9bc", "woman in motorized wheelchair: Medium-Light Skin Tone", []string{"woman_in_motorized_wheelchair_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f9bc", "woman in motorized wheelchair: Medium Skin Tone", []string{"woman_in_motorized_wheelchair_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f469\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right", []string{"woman_in_motorized_wheelchair_facing_right"}, "15.1", true}, + {"\U0001f469\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Dark Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Light Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Medium-Dark Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Medium-Light Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f", "woman in motorized wheelchair facing right: Medium Skin Tone", []string{"woman_in_motorized_wheelchair_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f935\u200d\u2640\ufe0f", "woman in tuxedo", []string{"woman_in_tuxedo"}, "13.0", true}, {"\U0001f935\U0001f3ff\u200d\u2640\ufe0f", "woman in tuxedo: Dark Skin Tone", []string{"woman_in_tuxedo_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f935\U0001f3fb\u200d\u2640\ufe0f", "woman in tuxedo: Light Skin Tone", []string{"woman_in_tuxedo_Light_Skin_Tone"}, "12.0", false}, @@ -3271,6 +3373,12 @@ var GemojiData = Gemoji{ {"\U0001f939\U0001f3fe\u200d\u2640\ufe0f", "woman juggling: Medium-Dark Skin Tone", []string{"woman_juggling_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fc\u200d\u2640\ufe0f", "woman juggling: Medium-Light Skin Tone", []string{"woman_juggling_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f939\U0001f3fd\u200d\u2640\ufe0f", "woman juggling: Medium Skin Tone", []string{"woman_juggling_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right", []string{"woman_kneeling_facing_right"}, "15.1", true}, + {"\U0001f9ce\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Dark Skin Tone", []string{"woman_kneeling_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Light Skin Tone", []string{"woman_kneeling_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Medium-Dark Skin Tone", []string{"woman_kneeling_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Medium-Light Skin Tone", []string{"woman_kneeling_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f9ce\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman kneeling facing right: Medium Skin Tone", []string{"woman_kneeling_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\u200d\U0001f527", "woman mechanic", []string{"woman_mechanic"}, "", true}, {"\U0001f469\U0001f3ff\u200d\U0001f527", "woman mechanic: Dark Skin Tone", []string{"woman_mechanic_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f527", "woman mechanic: Light Skin Tone", []string{"woman_mechanic_Light_Skin_Tone"}, "12.0", false}, @@ -3301,6 +3409,12 @@ var GemojiData = Gemoji{ {"\U0001f93d\U0001f3fe\u200d\u2640\ufe0f", "woman playing water polo: Medium-Dark Skin Tone", []string{"woman_playing_water_polo_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fc\u200d\u2640\ufe0f", "woman playing water polo: Medium-Light Skin Tone", []string{"woman_playing_water_polo_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f93d\U0001f3fd\u200d\u2640\ufe0f", "woman playing water polo: Medium Skin Tone", []string{"woman_playing_water_polo_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right", []string{"woman_running_facing_right"}, "15.1", true}, + {"\U0001f3c3\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Dark Skin Tone", []string{"woman_running_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Light Skin Tone", []string{"woman_running_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Medium-Dark Skin Tone", []string{"woman_running_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Medium-Light Skin Tone", []string{"woman_running_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f3c3\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman running facing right: Medium Skin Tone", []string{"woman_running_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f469\u200d\U0001f52c", "woman scientist", []string{"woman_scientist"}, "", true}, {"\U0001f469\U0001f3ff\u200d\U0001f52c", "woman scientist: Dark Skin Tone", []string{"woman_scientist_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fb\u200d\U0001f52c", "woman scientist: Light Skin Tone", []string{"woman_scientist_Light_Skin_Tone"}, "12.0", false}, @@ -3337,6 +3451,12 @@ var GemojiData = Gemoji{ {"\U0001f469\U0001f3fe\u200d\U0001f4bb", "woman technologist: Medium-Dark Skin Tone", []string{"woman_technologist_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fc\u200d\U0001f4bb", "woman technologist: Medium-Light Skin Tone", []string{"woman_technologist_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f469\U0001f3fd\u200d\U0001f4bb", "woman technologist: Medium Skin Tone", []string{"woman_technologist_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right", []string{"woman_walking_facing_right"}, "15.1", true}, + {"\U0001f6b6\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Dark Skin Tone", []string{"woman_walking_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Light Skin Tone", []string{"woman_walking_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Medium-Dark Skin Tone", []string{"woman_walking_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Medium-Light Skin Tone", []string{"woman_walking_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f6b6\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f", "woman walking facing right: Medium Skin Tone", []string{"woman_walking_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f9d5", "woman with headscarf", []string{"woman_with_headscarf"}, "11.0", true}, {"\U0001f9d5\U0001f3ff", "woman with headscarf: Dark Skin Tone", []string{"woman_with_headscarf_Dark_Skin_Tone"}, "12.0", false}, {"\U0001f9d5\U0001f3fb", "woman with headscarf: Light Skin Tone", []string{"woman_with_headscarf_Light_Skin_Tone"}, "12.0", false}, @@ -3361,6 +3481,12 @@ var GemojiData = Gemoji{ {"\U0001f470\U0001f3fe\u200d\u2640\ufe0f", "woman with veil: Medium-Dark Skin Tone", []string{"woman_with_veil_Medium-Dark_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3fc\u200d\u2640\ufe0f", "woman with veil: Medium-Light Skin Tone", []string{"woman_with_veil_Medium-Light_Skin_Tone"}, "12.0", false}, {"\U0001f470\U0001f3fd\u200d\u2640\ufe0f", "woman with veil: Medium Skin Tone", []string{"woman_with_veil_Medium_Skin_Tone"}, "12.0", false}, + {"\U0001f469\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right", []string{"woman_with_white_cane_facing_right"}, "15.1", true}, + {"\U0001f469\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Dark Skin Tone", []string{"woman_with_white_cane_facing_right_Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Light Skin Tone", []string{"woman_with_white_cane_facing_right_Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Medium-Dark Skin Tone", []string{"woman_with_white_cane_facing_right_Medium-Dark_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Medium-Light Skin Tone", []string{"woman_with_white_cane_facing_right_Medium-Light_Skin_Tone"}, "12.0", false}, + {"\U0001f469\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f", "woman with white cane facing right: Medium Skin Tone", []string{"woman_with_white_cane_facing_right_Medium_Skin_Tone"}, "12.0", false}, {"\U0001f45a", "woman’s clothes", []string{"womans_clothes"}, "6.0", false}, {"\U0001f452", "woman’s hat", []string{"womans_hat"}, "6.0", false}, {"\U0001f93c\u200d\u2640\ufe0f", "women wrestling", []string{"women_wrestling"}, "9.0", false}, diff --git a/package/gitea/source/modules/fileicon/render.go b/package/gitea/source/modules/fileicon/render.go index 6b2fcfa8..5bf2a3a0 100644 --- a/package/gitea/source/modules/fileicon/render.go +++ b/package/gitea/source/modules/fileicon/render.go @@ -34,7 +34,13 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML { } func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML { - if setting.UI.FileIconTheme == "material" { + // Use folder theme for directories and symlinks to directories + theme := setting.UI.FileIconTheme + if entry.EntryMode.IsDir() || (entry.EntryMode.IsLink() && entry.SymlinkToMode.IsDir()) { + theme = setting.UI.FolderIconTheme + } + + if theme == "material" { return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry) } return BasicEntryIconHTML(entry) diff --git a/package/gitea/source/modules/fileicon/render_test.go b/package/gitea/source/modules/fileicon/render_test.go new file mode 100644 index 00000000..d9998f3f --- /dev/null +++ b/package/gitea/source/modules/fileicon/render_test.go @@ -0,0 +1,75 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon_test + +import ( + "testing" + + "code.gitea.io/gitea/modules/fileicon" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestRenderEntryIconHTML_WithDifferentThemes(t *testing.T) { + // Test that folder icons use the folder theme + t.Run("FolderUsesBasicTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")() + + folderEntry := &fileicon.EntryInfo{ + BaseName: "testfolder", + EntryMode: git.EntryModeTree, + } + + html := fileicon.RenderEntryIconHTML(nil, folderEntry) + // Basic theme renders octicon classes + assert.Contains(t, string(html), "octicon-file-directory-fill") + }) + + t.Run("FileUsesMaterialTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")() + + fileEntry := &fileicon.EntryInfo{ + BaseName: "test.js", + EntryMode: git.EntryModeBlob, + } + + html := fileicon.RenderEntryIconHTML(nil, fileEntry) + // Material theme for files renders material icons + assert.Contains(t, string(html), "svg-mfi-") + }) + + t.Run("SymlinkToFolderUsesBasicTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")() + + symlinkEntry := &fileicon.EntryInfo{ + BaseName: "link", + EntryMode: git.EntryModeSymlink, + SymlinkToMode: git.EntryModeTree, + } + + html := fileicon.RenderEntryIconHTML(nil, symlinkEntry) + // Symlinks to folders should use folder theme + assert.Contains(t, string(html), "octicon-file-directory-symlink") + }) + + t.Run("BothMaterialTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "material")() + + folderEntry := &fileicon.EntryInfo{ + BaseName: "testfolder", + EntryMode: git.EntryModeTree, + } + + html := fileicon.RenderEntryIconHTML(nil, folderEntry) + // Material theme for folders renders material folder icons + assert.Contains(t, string(html), "svg-mfi-") + }) +} diff --git a/package/gitea/source/modules/generate/generate.go b/package/gitea/source/modules/generate/generate.go index 2d9a3dd9..ac845044 100644 --- a/package/gitea/source/modules/generate/generate.go +++ b/package/gitea/source/modules/generate/generate.go @@ -54,13 +54,13 @@ func DecodeJwtSecretBase64(src string) ([]byte, error) { } // NewJwtSecretWithBase64 generates a jwt secret with its base64 encoded value intended to be used for saving into config file -func NewJwtSecretWithBase64() ([]byte, string, error) { +func NewJwtSecretWithBase64() ([]byte, string) { bytes := make([]byte, defaultJwtSecretLen) - _, err := io.ReadFull(rand.Reader, bytes) + _, err := rand.Read(bytes) if err != nil { - return nil, "", err + panic(err) // rand.Read never fails } - return bytes, base64.RawURLEncoding.EncodeToString(bytes), nil + return bytes, base64.RawURLEncoding.EncodeToString(bytes) } // NewSecretKey generate a new value intended to be used by SECRET_KEY. diff --git a/package/gitea/source/modules/generate/generate_test.go b/package/gitea/source/modules/generate/generate_test.go index af640a60..f9dd20cc 100644 --- a/package/gitea/source/modules/generate/generate_test.go +++ b/package/gitea/source/modules/generate/generate_test.go @@ -25,10 +25,12 @@ func TestDecodeJwtSecretBase64(t *testing.T) { } func TestNewJwtSecretWithBase64(t *testing.T) { - secret, encoded, err := NewJwtSecretWithBase64() - assert.NoError(t, err) + secret, encoded := NewJwtSecretWithBase64() assert.Len(t, secret, 32) decoded, err := DecodeJwtSecretBase64(encoded) assert.NoError(t, err) assert.Equal(t, secret, decoded) + + secret2, _ := NewJwtSecretWithBase64() + assert.NotEqual(t, secret, secret2) } diff --git a/package/gitea/source/modules/git/attribute/attribute.go b/package/gitea/source/modules/git/attribute/attribute.go index 9c01cb33..8719369d 100644 --- a/package/gitea/source/modules/git/attribute/attribute.go +++ b/package/gitea/source/modules/git/attribute/attribute.go @@ -96,8 +96,8 @@ func (attrs *Attributes) GetGitlabLanguage() optional.Option[string] { // gitlab-language may have additional parameters after the language // ignore them and just use the main language // https://docs.gitlab.com/ee/user/project/highlighting.html#override-syntax-highlighting-for-a-file-type - if idx := strings.IndexByte(raw, '?'); idx >= 0 { - return optional.Some(raw[:idx]) + if before, _, ok := strings.Cut(raw, "?"); ok { + return optional.Some(before) } } return attrStr diff --git a/package/gitea/source/modules/git/attribute/batch.go b/package/gitea/source/modules/git/attribute/batch.go index 9f805d55..b1e6387a 100644 --- a/package/gitea/source/modules/git/attribute/batch.go +++ b/package/gitea/source/modules/git/attribute/batch.go @@ -7,7 +7,7 @@ import ( "bytes" "context" "fmt" - "os" + "io" "path/filepath" "time" @@ -20,7 +20,7 @@ import ( type BatchChecker struct { attributesNum int repo *git.Repository - stdinWriter *os.File + stdinWriter io.WriteCloser stdOut *nulSeparatedAttributeWriter ctx context.Context cancel context.CancelFunc @@ -60,10 +60,7 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string) }, } - stdinReader, stdinWriter, err := os.Pipe() - if err != nil { - return nil, err - } + stdinWriter, stdinWriterClose := cmd.MakeStdinPipe() checker.stdinWriter = stdinWriter lw := new(nulSeparatedAttributeWriter) @@ -71,24 +68,19 @@ func NewBatchChecker(repo *git.Repository, treeish string, attributes []string) lw.closed = make(chan struct{}) checker.stdOut = lw - go func() { - defer func() { - _ = stdinReader.Close() - _ = lw.Close() - }() - stdErr := new(bytes.Buffer) - err := cmd.Run(ctx, &gitcmd.RunOpts{ - Env: envs, - Dir: repo.Path, - Stdin: stdinReader, - Stdout: lw, - Stderr: stdErr, - }) + cmd.WithEnv(envs). + WithDir(repo.Path). + WithStdoutCopy(lw) - if err != nil && !git.IsErrCanceledOrKilled(err) { + go func() { + defer stdinWriterClose() + defer checker.cancel() + defer lw.Close() + + err := cmd.RunWithStderr(ctx) + if err != nil && !gitcmd.IsErrorCanceledOrKilled(err) { log.Error("Attribute checker for commit %s exits with error: %v", treeish, err) } - checker.cancel() }() return checker, nil diff --git a/package/gitea/source/modules/git/attribute/checker.go b/package/gitea/source/modules/git/attribute/checker.go index 4b313adf..3eea31e8 100644 --- a/package/gitea/source/modules/git/attribute/checker.go +++ b/package/gitea/source/modules/git/attribute/checker.go @@ -68,19 +68,14 @@ func CheckAttributes(ctx context.Context, gitRepo *git.Repository, treeish strin } defer cancel() - stdOut := new(bytes.Buffer) - stdErr := new(bytes.Buffer) - - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Env: append(os.Environ(), envs...), - Dir: gitRepo.Path, - Stdout: stdOut, - Stderr: stdErr, - }); err != nil { - return nil, fmt.Errorf("failed to run check-attr: %w\n%s\n%s", err, stdOut.String(), stdErr.String()) + stdout, _, err := cmd.WithEnv(append(os.Environ(), envs...)). + WithDir(gitRepo.Path). + RunStdBytes(ctx) + if err != nil { + return nil, fmt.Errorf("failed to run check-attr: %w", err) } - fields := bytes.Split(stdOut.Bytes(), []byte{'\000'}) + fields := bytes.Split(stdout, []byte{'\000'}) if len(fields)%3 != 1 { return nil, errors.New("wrong number of fields in return from check-attr") } diff --git a/package/gitea/source/modules/git/batch.go b/package/gitea/source/modules/git/batch.go deleted file mode 100644 index f9e1748b..00000000 --- a/package/gitea/source/modules/git/batch.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "bufio" - "context" -) - -type Batch struct { - cancel context.CancelFunc - Reader *bufio.Reader - Writer WriteCloserError -} - -// NewBatch creates a new batch for the given repository, the Close must be invoked before release the batch -func NewBatch(ctx context.Context, repoPath string) (*Batch, error) { - // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! - if err := ensureValidGitRepository(ctx, repoPath); err != nil { - return nil, err - } - - var batch Batch - batch.Writer, batch.Reader, batch.cancel = catFileBatch(ctx, repoPath) - return &batch, nil -} - -func NewBatchCheck(ctx context.Context, repoPath string) (*Batch, error) { - // Now because of some insanity with git cat-file not immediately failing if not run in a valid git directory we need to run git rev-parse first! - if err := ensureValidGitRepository(ctx, repoPath); err != nil { - return nil, err - } - - var check Batch - check.Writer, check.Reader, check.cancel = catFileBatchCheck(ctx, repoPath) - return &check, nil -} - -func (b *Batch) Close() { - if b.cancel != nil { - b.cancel() - b.Reader = nil - b.Writer = nil - b.cancel = nil - } -} diff --git a/package/gitea/source/modules/git/batch_reader.go b/package/gitea/source/modules/git/batch_reader.go deleted file mode 100644 index f09f4144..00000000 --- a/package/gitea/source/modules/git/batch_reader.go +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "bufio" - "bytes" - "context" - "io" - "math" - "strconv" - "strings" - - "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/log" - - "github.com/djherbis/buffer" - "github.com/djherbis/nio/v3" -) - -// WriteCloserError wraps an io.WriteCloser with an additional CloseWithError function -type WriteCloserError interface { - io.WriteCloser - CloseWithError(err error) error -} - -// ensureValidGitRepository runs git rev-parse in the repository path - thus ensuring that the repository is a valid repository. -// Run before opening git cat-file. -// This is needed otherwise the git cat-file will hang for invalid repositories. -func ensureValidGitRepository(ctx context.Context, repoPath string) error { - stderr := strings.Builder{} - err := gitcmd.NewCommand("rev-parse"). - Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stderr: &stderr, - }) - if err != nil { - return gitcmd.ConcatenateError(err, (&stderr).String()) - } - return nil -} - -// catFileBatchCheck opens git cat-file --batch-check in the provided repo and returns a stdin pipe, a stdout reader and cancel function -func catFileBatchCheck(ctx context.Context, repoPath string) (WriteCloserError, *bufio.Reader, func()) { - batchStdinReader, batchStdinWriter := io.Pipe() - batchStdoutReader, batchStdoutWriter := io.Pipe() - ctx, ctxCancel := context.WithCancel(ctx) - closed := make(chan struct{}) - cancel := func() { - ctxCancel() - _ = batchStdoutReader.Close() - _ = batchStdinWriter.Close() - <-closed - } - - // Ensure cancel is called as soon as the provided context is cancelled - go func() { - <-ctx.Done() - cancel() - }() - - go func() { - stderr := strings.Builder{} - err := gitcmd.NewCommand("cat-file", "--batch-check"). - Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdin: batchStdinReader, - Stdout: batchStdoutWriter, - Stderr: &stderr, - - UseContextTimeout: true, - }) - if err != nil { - _ = batchStdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) - _ = batchStdinReader.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) - } else { - _ = batchStdoutWriter.Close() - _ = batchStdinReader.Close() - } - close(closed) - }() - - // For simplicities sake we'll use a buffered reader to read from the cat-file --batch-check - batchReader := bufio.NewReader(batchStdoutReader) - - return batchStdinWriter, batchReader, cancel -} - -// catFileBatch opens git cat-file --batch in the provided repo and returns a stdin pipe, a stdout reader and cancel function -func catFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufio.Reader, func()) { - // We often want to feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary. - // so let's create a batch stdin and stdout - batchStdinReader, batchStdinWriter := io.Pipe() - batchStdoutReader, batchStdoutWriter := nio.Pipe(buffer.New(32 * 1024)) - ctx, ctxCancel := context.WithCancel(ctx) - closed := make(chan struct{}) - cancel := func() { - ctxCancel() - _ = batchStdinWriter.Close() - _ = batchStdoutReader.Close() - <-closed - } - - // Ensure cancel is called as soon as the provided context is cancelled - go func() { - <-ctx.Done() - cancel() - }() - - go func() { - stderr := strings.Builder{} - err := gitcmd.NewCommand("cat-file", "--batch"). - Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdin: batchStdinReader, - Stdout: batchStdoutWriter, - Stderr: &stderr, - - UseContextTimeout: true, - }) - if err != nil { - _ = batchStdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) - _ = batchStdinReader.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) - } else { - _ = batchStdoutWriter.Close() - _ = batchStdinReader.Close() - } - close(closed) - }() - - // For simplicities sake we'll us a buffered reader to read from the cat-file --batch - batchReader := bufio.NewReaderSize(batchStdoutReader, 32*1024) - - return batchStdinWriter, batchReader, cancel -} - -// ReadBatchLine reads the header line from cat-file --batch -// We expect: SP SP LF -// then leaving the rest of the stream " LF" to be read -func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) { - typ, err = rd.ReadString('\n') - if err != nil { - return sha, typ, size, err - } - if len(typ) == 1 { - typ, err = rd.ReadString('\n') - if err != nil { - return sha, typ, size, err - } - } - idx := strings.IndexByte(typ, ' ') - if idx < 0 { - log.Debug("missing space typ: %s", typ) - return sha, typ, size, ErrNotExist{ID: string(sha)} - } - sha = []byte(typ[:idx]) - typ = typ[idx+1:] - - idx = strings.IndexByte(typ, ' ') - if idx < 0 { - return sha, typ, size, ErrNotExist{ID: string(sha)} - } - - sizeStr := typ[idx+1 : len(typ)-1] - typ = typ[:idx] - - size, err = strconv.ParseInt(sizeStr, 10, 64) - return sha, typ, size, err -} - -// ReadTagObjectID reads a tag object ID hash from a cat-file --batch stream, throwing away the rest of the stream. -func ReadTagObjectID(rd *bufio.Reader, size int64) (string, error) { - var id string - var n int64 -headerLoop: - for { - line, err := rd.ReadBytes('\n') - if err != nil { - return "", err - } - n += int64(len(line)) - idx := bytes.Index(line, []byte{' '}) - if idx < 0 { - continue - } - - if string(line[:idx]) == "object" { - id = string(line[idx+1 : len(line)-1]) - break headerLoop - } - } - - // Discard the rest of the tag - return id, DiscardFull(rd, size-n+1) -} - -// ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream. -func ReadTreeID(rd *bufio.Reader, size int64) (string, error) { - var id string - var n int64 -headerLoop: - for { - line, err := rd.ReadBytes('\n') - if err != nil { - return "", err - } - n += int64(len(line)) - idx := bytes.Index(line, []byte{' '}) - if idx < 0 { - continue - } - - if string(line[:idx]) == "tree" { - id = string(line[idx+1 : len(line)-1]) - break headerLoop - } - } - - // Discard the rest of the commit - return id, DiscardFull(rd, size-n+1) -} - -// git tree files are a list: -// SP NUL -// -// Unfortunately this 20-byte notation is somewhat in conflict to all other git tools -// Therefore we need some method to convert these binary hashes to hex hashes - -// constant hextable to help quickly convert between binary and hex representation -const hextable = "0123456789abcdef" - -// BinToHexHeash converts a binary Hash into a hex encoded one. Input and output can be the -// same byte slice to support in place conversion without allocations. -// This is at least 100x quicker that hex.EncodeToString -func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte { - for i := objectFormat.FullLength()/2 - 1; i >= 0; i-- { - v := sha[i] - vhi, vlo := v>>4, v&0x0f - shi, slo := hextable[vhi], hextable[vlo] - out[i*2], out[i*2+1] = shi, slo - } - return out -} - -// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream -// This carefully avoids allocations - except where fnameBuf is too small. -// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations -// -// Each line is composed of: -// SP NUL -// -// We don't attempt to convert the raw HASH to save a lot of time -func ParseCatFileTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { - var readBytes []byte - - // Read the Mode & fname - readBytes, err = rd.ReadSlice('\x00') - if err != nil { - return mode, fname, sha, n, err - } - idx := bytes.IndexByte(readBytes, ' ') - if idx < 0 { - log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes) - return mode, fname, sha, n, &ErrNotExist{} - } - - n += idx + 1 - copy(modeBuf, readBytes[:idx]) - if len(modeBuf) >= idx { - modeBuf = modeBuf[:idx] - } else { - modeBuf = append(modeBuf, readBytes[len(modeBuf):idx]...) - } - mode = modeBuf - - readBytes = readBytes[idx+1:] - - // Deal with the fname - copy(fnameBuf, readBytes) - if len(fnameBuf) > len(readBytes) { - fnameBuf = fnameBuf[:len(readBytes)] - } else { - fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...) - } - for err == bufio.ErrBufferFull { - readBytes, err = rd.ReadSlice('\x00') - fnameBuf = append(fnameBuf, readBytes...) - } - n += len(fnameBuf) - if err != nil { - return mode, fname, sha, n, err - } - fnameBuf = fnameBuf[:len(fnameBuf)-1] - fname = fnameBuf - - // Deal with the binary hash - idx = 0 - length := objectFormat.FullLength() / 2 - for idx < length { - var read int - read, err = rd.Read(shaBuf[idx:length]) - n += read - if err != nil { - return mode, fname, sha, n, err - } - idx += read - } - sha = shaBuf - return mode, fname, sha, n, err -} - -func DiscardFull(rd *bufio.Reader, discard int64) error { - if discard > math.MaxInt32 { - n, err := rd.Discard(math.MaxInt32) - discard -= int64(n) - if err != nil { - return err - } - } - for discard > 0 { - n, err := rd.Discard(int(discard)) - discard -= int64(n) - if err != nil { - return err - } - } - return nil -} diff --git a/package/gitea/source/modules/git/blob_nogogit.go b/package/gitea/source/modules/git/blob_nogogit.go index af3ce376..837b30fd 100644 --- a/package/gitea/source/modules/git/blob_nogogit.go +++ b/package/gitea/source/modules/git/blob_nogogit.go @@ -6,8 +6,6 @@ package git import ( - "bufio" - "bytes" "io" "code.gitea.io/gitea/modules/log" @@ -25,38 +23,28 @@ type Blob struct { // DataAsync gets a ReadCloser for the contents of a blob without reading it all. // Calling the Close function on the result will discard all unread output. -func (b *Blob) DataAsync() (io.ReadCloser, error) { - wr, rd, cancel, err := b.repo.CatFileBatch(b.repo.Ctx) +func (b *Blob) DataAsync() (_ io.ReadCloser, retErr error) { + batch, cancel, err := b.repo.CatFileBatch(b.repo.Ctx) if err != nil { return nil, err } + defer func() { + // if there was an error, cancel the batch right away, + // otherwise let the caller close it + if retErr != nil { + cancel() + } + }() - _, err = wr.Write([]byte(b.ID.String() + "\n")) + info, contentReader, err := batch.QueryContent(b.ID.String()) if err != nil { - cancel() - return nil, err - } - _, _, size, err := ReadBatchLine(rd) - if err != nil { - cancel() return nil, err } b.gotSize = true - b.size = size - - if size < 4096 { - bs, err := io.ReadAll(io.LimitReader(rd, size)) - defer cancel() - if err != nil { - return nil, err - } - _, err = rd.Discard(1) - return io.NopCloser(bytes.NewReader(bs)), err - } - + b.size = info.Size return &blobReader{ - rd: rd, - n: size, + rd: contentReader, + n: info.Size, cancel: cancel, }, nil } @@ -67,30 +55,24 @@ func (b *Blob) Size() int64 { return b.size } - wr, rd, cancel, err := b.repo.CatFileBatchCheck(b.repo.Ctx) + batch, cancel, err := b.repo.CatFileBatch(b.repo.Ctx) if err != nil { log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) return 0 } defer cancel() - _, err = wr.Write([]byte(b.ID.String() + "\n")) + info, err := batch.QueryInfo(b.ID.String()) if err != nil { log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) return 0 } - _, _, b.size, err = ReadBatchLine(rd) - if err != nil { - log.Debug("error whilst reading size for %s in %s. Error: %v", b.ID.String(), b.repo.Path, err) - return 0 - } - b.gotSize = true - + b.size = info.Size return b.size } type blobReader struct { - rd *bufio.Reader + rd BufferedReader n int64 cancel func() } diff --git a/package/gitea/source/modules/git/catfile_batch.go b/package/gitea/source/modules/git/catfile_batch.go new file mode 100644 index 00000000..d13179f3 --- /dev/null +++ b/package/gitea/source/modules/git/catfile_batch.go @@ -0,0 +1,52 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "io" +) + +type BufferedReader interface { + io.Reader + Buffered() int + Peek(n int) ([]byte, error) + Discard(n int) (int, error) + ReadString(sep byte) (string, error) + ReadSlice(sep byte) ([]byte, error) + ReadBytes(sep byte) ([]byte, error) +} + +type CatFileObject struct { + ID string + Type string + Size int64 +} + +type CatFileBatch interface { + // QueryInfo queries the object info from the git repository by its object name using "git cat-file --batch" family commands. + // "git cat-file" accepts "" for the object name, it can be a ref name, object id, etc. https://git-scm.com/docs/gitrevisions + // In Gitea, we only use the simple ref name or object id, no other complex rev syntax like "suffix" or "git describe" although they are supported by git. + QueryInfo(obj string) (*CatFileObject, error) + + // QueryContent is similar to QueryInfo, it queries the object info and additionally returns a reader for its content. + // FIXME: this design still follows the old pattern: the returned BufferedReader is very fragile, + // callers should carefully maintain its lifecycle and discard all unread data. + // TODO: It needs to be refactored to a fully managed Reader stream in the future, don't let callers manually Close or Discard + QueryContent(obj string) (*CatFileObject, BufferedReader, error) +} + +type CatFileBatchCloser interface { + CatFileBatch + Close() +} + +// NewBatch creates a "batch object provider (CatFileBatch)" for the given repository path to retrieve object info and content efficiently. +// The CatFileBatch and the readers create by it should only be used in the same goroutine. +func NewBatch(ctx context.Context, repoPath string) (CatFileBatchCloser, error) { + if DefaultFeatures().SupportCatFileBatchCommand { + return newCatFileBatchCommand(ctx, repoPath) + } + return newCatFileBatchLegacy(ctx, repoPath) +} diff --git a/package/gitea/source/modules/git/catfile_batch_command.go b/package/gitea/source/modules/git/catfile_batch_command.go new file mode 100644 index 00000000..4e18282b --- /dev/null +++ b/package/gitea/source/modules/git/catfile_batch_command.go @@ -0,0 +1,74 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "os" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +// catFileBatchCommand implements the CatFileBatch interface using the "cat-file --batch-command" command +// for git version >= 2.36 +// ref: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt---batch-command +type catFileBatchCommand struct { + ctx context.Context + repoPath string + batch *catFileBatchCommunicator +} + +var _ CatFileBatch = (*catFileBatchCommand)(nil) + +func newCatFileBatchCommand(ctx context.Context, repoPath string) (*catFileBatchCommand, error) { + if _, err := os.Stat(repoPath); err != nil { + return nil, util.NewNotExistErrorf("repo %q doesn't exist", filepath.Base(repoPath)) + } + return &catFileBatchCommand{ctx: ctx, repoPath: repoPath}, nil +} + +func (b *catFileBatchCommand) getBatch() *catFileBatchCommunicator { + if b.batch != nil { + return b.batch + } + b.batch = newCatFileBatch(b.ctx, b.repoPath, gitcmd.NewCommand("cat-file", "--batch-command")) + return b.batch +} + +func (b *catFileBatchCommand) QueryContent(obj string) (*CatFileObject, BufferedReader, error) { + if strings.Contains(obj, "\n") { + setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) + } + _, err := b.getBatch().reqWriter.Write([]byte("contents " + obj + "\n")) + if err != nil { + return nil, nil, err + } + info, err := catFileBatchParseInfoLine(b.getBatch().respReader) + if err != nil { + return nil, nil, err + } + return info, b.getBatch().respReader, nil +} + +func (b *catFileBatchCommand) QueryInfo(obj string) (*CatFileObject, error) { + if strings.Contains(obj, "\n") { + setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) + } + _, err := b.getBatch().reqWriter.Write([]byte("info " + obj + "\n")) + if err != nil { + return nil, err + } + return catFileBatchParseInfoLine(b.getBatch().respReader) +} + +func (b *catFileBatchCommand) Close() { + if b.batch != nil { + b.batch.Close() + b.batch = nil + } +} diff --git a/package/gitea/source/modules/git/catfile_batch_legacy.go b/package/gitea/source/modules/git/catfile_batch_legacy.go new file mode 100644 index 00000000..595043d1 --- /dev/null +++ b/package/gitea/source/modules/git/catfile_batch_legacy.go @@ -0,0 +1,89 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "io" + "os" + "path/filepath" + "strings" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +// catFileBatchLegacy implements the CatFileBatch interface using the "cat-file --batch" command and "cat-file --batch-check" command +// for git version < 2.36 +// to align with "--batch-command", it creates the two commands for querying object contents and object info separately +// ref: https://git-scm.com/docs/git-cat-file#Documentation/git-cat-file.txt---batch +type catFileBatchLegacy struct { + ctx context.Context + repoPath string + batchContent *catFileBatchCommunicator + batchCheck *catFileBatchCommunicator +} + +var _ CatFileBatchCloser = (*catFileBatchLegacy)(nil) + +func newCatFileBatchLegacy(ctx context.Context, repoPath string) (*catFileBatchLegacy, error) { + if _, err := os.Stat(repoPath); err != nil { + return nil, util.NewNotExistErrorf("repo %q doesn't exist", filepath.Base(repoPath)) + } + return &catFileBatchLegacy{ctx: ctx, repoPath: repoPath}, nil +} + +func (b *catFileBatchLegacy) getBatchContent() *catFileBatchCommunicator { + if b.batchContent != nil { + return b.batchContent + } + b.batchContent = newCatFileBatch(b.ctx, b.repoPath, gitcmd.NewCommand("cat-file", "--batch")) + return b.batchContent +} + +func (b *catFileBatchLegacy) getBatchCheck() *catFileBatchCommunicator { + if b.batchCheck != nil { + return b.batchCheck + } + b.batchCheck = newCatFileBatch(b.ctx, b.repoPath, gitcmd.NewCommand("cat-file", "--batch-check")) + return b.batchCheck +} + +func (b *catFileBatchLegacy) QueryContent(obj string) (*CatFileObject, BufferedReader, error) { + if strings.Contains(obj, "\n") { + setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) + } + _, err := io.WriteString(b.getBatchContent().reqWriter, obj+"\n") + if err != nil { + return nil, nil, err + } + info, err := catFileBatchParseInfoLine(b.getBatchContent().respReader) + if err != nil { + return nil, nil, err + } + return info, b.getBatchContent().respReader, nil +} + +func (b *catFileBatchLegacy) QueryInfo(obj string) (*CatFileObject, error) { + if strings.Contains(obj, "\n") { + setting.PanicInDevOrTesting("invalid object name with newline: %q", obj) + } + _, err := io.WriteString(b.getBatchCheck().reqWriter, obj+"\n") + if err != nil { + return nil, err + } + return catFileBatchParseInfoLine(b.getBatchCheck().respReader) +} + +func (b *catFileBatchLegacy) Close() { + if b.batchContent != nil { + b.batchContent.Close() + b.batchContent = nil + } + if b.batchCheck != nil { + b.batchCheck.Close() + b.batchCheck = nil + } +} diff --git a/package/gitea/source/modules/git/catfile_batch_reader.go b/package/gitea/source/modules/git/catfile_batch_reader.go new file mode 100644 index 00000000..5727c4a8 --- /dev/null +++ b/package/gitea/source/modules/git/catfile_batch_reader.go @@ -0,0 +1,264 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bufio" + "bytes" + "context" + "errors" + "io" + "math" + "strconv" + "strings" + "sync/atomic" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" +) + +type catFileBatchCommunicator struct { + closeFunc atomic.Pointer[func(err error)] + reqWriter io.Writer + respReader *bufio.Reader + debugGitCmd *gitcmd.Command +} + +func (b *catFileBatchCommunicator) Close(err ...error) { + if fn := b.closeFunc.Swap(nil); fn != nil { + (*fn)(util.OptionalArg(err)) + } +} + +// newCatFileBatch opens git cat-file --batch/--batch-check/--batch-command command and prepares the stdin/stdout pipes for communication. +func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command) *catFileBatchCommunicator { + ctx, ctxCancel := context.WithCancelCause(ctx) + stdinWriter, stdoutReader, stdPipeClose := cmdCatFile.MakeStdinStdoutPipe() + ret := &catFileBatchCommunicator{ + debugGitCmd: cmdCatFile, + reqWriter: stdinWriter, + respReader: bufio.NewReaderSize(stdoutReader, 32*1024), // use a buffered reader for rich operations + } + ret.closeFunc.Store(new(func(err error) { + ctxCancel(err) + stdPipeClose() + })) + + err := cmdCatFile.WithDir(repoPath).StartWithStderr(ctx) + if err != nil { + log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err) + // ideally here it should return the error, but it would require refactoring all callers + // so just return a dummy communicator that does nothing, almost the same behavior as before, not bad + ret.Close(err) + return ret + } + + go func() { + err := cmdCatFile.WaitWithStderr() + if err != nil && !errors.Is(err, context.Canceled) { + log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err) + } + ret.Close(err) + }() + + return ret +} + +func (b *catFileBatchCommunicator) debugKill() (ret struct { + beforeClose chan struct{} + blockClose chan struct{} + afterClose chan struct{} +}, +) { + ret.beforeClose = make(chan struct{}) + ret.blockClose = make(chan struct{}) + ret.afterClose = make(chan struct{}) + oldCloseFunc := b.closeFunc.Load() + b.closeFunc.Store(new(func(err error) { + b.closeFunc.Store(nil) + close(ret.beforeClose) + <-ret.blockClose + (*oldCloseFunc)(err) + close(ret.afterClose) + })) + b.debugGitCmd.DebugKill() + return ret +} + +// catFileBatchParseInfoLine reads the header line from cat-file --batch +// We expect: SP SP LF +// then leaving the rest of the stream " LF" to be read +func catFileBatchParseInfoLine(rd BufferedReader) (*CatFileObject, error) { + typ, err := rd.ReadString('\n') + if err != nil { + return nil, err + } + if len(typ) == 1 { + typ, err = rd.ReadString('\n') + if err != nil { + return nil, err + } + } + idx := strings.IndexByte(typ, ' ') + if idx < 0 { + return nil, ErrNotExist{} + } + sha := typ[:idx] + typ = typ[idx+1:] + + idx = strings.IndexByte(typ, ' ') + if idx < 0 { + return nil, ErrNotExist{ID: sha} + } + + sizeStr := typ[idx+1 : len(typ)-1] + typ = typ[:idx] + + size, err := strconv.ParseInt(sizeStr, 10, 64) + return &CatFileObject{ID: sha, Type: typ, Size: size}, err +} + +// ReadTagObjectID reads a tag object ID hash from a cat-file --batch stream, throwing away the rest of the stream. +func ReadTagObjectID(rd BufferedReader, size int64) (string, error) { + var id string + var n int64 +headerLoop: + for { + line, err := rd.ReadBytes('\n') + if err != nil { + return "", err + } + n += int64(len(line)) + idx := bytes.Index(line, []byte{' '}) + if idx < 0 { + continue + } + + if string(line[:idx]) == "object" { + id = string(line[idx+1 : len(line)-1]) + break headerLoop + } + } + + // Discard the rest of the tag + return id, DiscardFull(rd, size-n+1) +} + +// ReadTreeID reads a tree ID from a cat-file --batch stream, throwing away the rest of the stream. +func ReadTreeID(rd BufferedReader, size int64) (string, error) { + var id string + var n int64 +headerLoop: + for { + line, err := rd.ReadBytes('\n') + if err != nil { + return "", err + } + n += int64(len(line)) + idx := bytes.Index(line, []byte{' '}) + if idx < 0 { + continue + } + + if string(line[:idx]) == "tree" { + id = string(line[idx+1 : len(line)-1]) + break headerLoop + } + } + + // Discard the rest of the commit + return id, DiscardFull(rd, size-n+1) +} + +// git tree files are a list: +// SP NUL +// +// Unfortunately this 20-byte notation is somewhat in conflict to all other git tools +// Therefore we need some method to convert these binary hashes to hex hashes + +// ParseCatFileTreeLine reads an entry from a tree in a cat-file --batch stream +// This carefully avoids allocations - except where fnameBuf is too small. +// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations +// +// Each line is composed of: +// SP NUL +// +// We don't attempt to convert the raw HASH to save a lot of time +func ParseCatFileTreeLine(objectFormat ObjectFormat, rd BufferedReader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) { + var readBytes []byte + + // Read the Mode & fname + readBytes, err = rd.ReadSlice('\x00') + if err != nil { + return mode, fname, sha, n, err + } + idx := bytes.IndexByte(readBytes, ' ') + if idx < 0 { + log.Debug("missing space in readBytes ParseCatFileTreeLine: %s", readBytes) + return mode, fname, sha, n, &ErrNotExist{} + } + + n += idx + 1 + copy(modeBuf, readBytes[:idx]) + if len(modeBuf) >= idx { + modeBuf = modeBuf[:idx] + } else { + modeBuf = append(modeBuf, readBytes[len(modeBuf):idx]...) + } + mode = modeBuf + + readBytes = readBytes[idx+1:] + + // Deal with the fname + copy(fnameBuf, readBytes) + if len(fnameBuf) > len(readBytes) { + fnameBuf = fnameBuf[:len(readBytes)] + } else { + fnameBuf = append(fnameBuf, readBytes[len(fnameBuf):]...) + } + for err == bufio.ErrBufferFull { + readBytes, err = rd.ReadSlice('\x00') + fnameBuf = append(fnameBuf, readBytes...) + } + n += len(fnameBuf) + if err != nil { + return mode, fname, sha, n, err + } + fnameBuf = fnameBuf[:len(fnameBuf)-1] + fname = fnameBuf + + // Deal with the binary hash + idx = 0 + length := objectFormat.FullLength() / 2 + for idx < length { + var read int + read, err = rd.Read(shaBuf[idx:length]) + n += read + if err != nil { + return mode, fname, sha, n, err + } + idx += read + } + sha = shaBuf + return mode, fname, sha, n, err +} + +func DiscardFull(rd BufferedReader, discard int64) error { + if discard > math.MaxInt32 { + n, err := rd.Discard(math.MaxInt32) + discard -= int64(n) + if err != nil { + return err + } + } + for discard > 0 { + n, err := rd.Discard(int(discard)) + discard -= int64(n) + if err != nil { + return err + } + } + return nil +} diff --git a/package/gitea/source/modules/git/catfile_batch_test.go b/package/gitea/source/modules/git/catfile_batch_test.go new file mode 100644 index 00000000..782d34d2 --- /dev/null +++ b/package/gitea/source/modules/git/catfile_batch_test.go @@ -0,0 +1,105 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "io" + "os" + "path/filepath" + "testing" + + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCatFileBatch(t *testing.T) { + defer test.MockVariableValue(&DefaultFeatures().SupportCatFileBatchCommand)() + DefaultFeatures().SupportCatFileBatchCommand = false + t.Run("LegacyCheck", testCatFileBatch) + DefaultFeatures().SupportCatFileBatchCommand = true + t.Run("BatchCommand", testCatFileBatch) +} + +func testCatFileBatch(t *testing.T) { + t.Run("CorruptedGitRepo", func(t *testing.T) { + tmpDir := t.TempDir() + batch, err := NewBatch(t.Context(), tmpDir) + // as long as the directory exists, no error, because we can't really know whether the git repo is valid until we run commands + require.NoError(t, err) + defer batch.Close() + + _, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") + require.Error(t, err) + _, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") + require.Error(t, err) + }) + + simulateQueryTerminated := func(t *testing.T, errBeforePipeClose, errAfterPipeClose error) { + readError := func(t *testing.T, r io.Reader, expectedErr error) { + if expectedErr == nil { + return // expectedErr == nil means this read should be skipped + } + n, err := r.Read(make([]byte, 100)) + assert.Zero(t, n) + assert.ErrorIs(t, err, expectedErr) + } + + batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + require.NoError(t, err) + defer batch.Close() + _, err = batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") + require.NoError(t, err) + + var c *catFileBatchCommunicator + switch b := batch.(type) { + case *catFileBatchLegacy: + c = b.batchCheck + _, _ = c.reqWriter.Write([]byte("in-complete-line-")) + case *catFileBatchCommand: + c = b.batch + _, _ = c.reqWriter.Write([]byte("info")) + default: + t.FailNow() + } + + require.NotEqual(t, errBeforePipeClose == nil, errAfterPipeClose == nil, "must set exactly one of the expected errors") + + inceptor := c.debugKill() + <-inceptor.beforeClose // wait for the command's Close to be called, the pipe is not closed yet + readError(t, c.respReader, errBeforePipeClose) // then caller will read on an open pipe which will be closed soon + close(inceptor.blockClose) // continue to close the pipe + <-inceptor.afterClose // wait for the pipe to be closed + readError(t, c.respReader, errAfterPipeClose) // then caller will read on a closed pipe + } + t.Run("QueryTerminated", func(t *testing.T) { + simulateQueryTerminated(t, io.EOF, nil) // reader is faster + simulateQueryTerminated(t, nil, os.ErrClosed) // pipes are closed faster + }) + + batch, err := NewBatch(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + require.NoError(t, err) + defer batch.Close() + + t.Run("QueryInfo", func(t *testing.T) { + info, err := batch.QueryInfo("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") + require.NoError(t, err) + assert.Equal(t, "e2129701f1a4d54dc44f03c93bca0a2aec7c5449", info.ID) + assert.Equal(t, "blob", info.Type) + assert.EqualValues(t, 6, info.Size) + }) + + t.Run("QueryContent", func(t *testing.T) { + info, rd, err := batch.QueryContent("e2129701f1a4d54dc44f03c93bca0a2aec7c5449") + require.NoError(t, err) + assert.Equal(t, "e2129701f1a4d54dc44f03c93bca0a2aec7c5449", info.ID) + assert.Equal(t, "blob", info.Type) + assert.EqualValues(t, 6, info.Size) + + content, err := io.ReadAll(io.LimitReader(rd, info.Size)) + require.NoError(t, err) + require.Equal(t, "file1\n", string(content)) + }) +} diff --git a/package/gitea/source/modules/git/commit.go b/package/gitea/source/modules/git/commit.go index a0c5955a..9fb786cb 100644 --- a/package/gitea/source/modules/git/commit.go +++ b/package/gitea/source/modules/git/commit.go @@ -5,17 +5,13 @@ package git import ( - "bufio" - "bytes" "context" "errors" "io" "os/exec" - "strconv" "strings" "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" ) @@ -41,7 +37,7 @@ type CommitSignature struct { // Message returns the commit message. Same as retrieving CommitMessage directly. func (c *Commit) Message() string { - return c.CommitMessage + return strings.ToValidUTF8(c.CommitMessage, "?") } // Summary returns first line of commit message. @@ -86,109 +82,6 @@ func (c *Commit) GetCommitByPath(relpath string) (*Commit, error) { return c.repo.getCommitByPathWithID(c.ID, relpath) } -// AddChanges marks local changes to be ready for commit. -func AddChanges(ctx context.Context, repoPath string, all bool, files ...string) error { - cmd := gitcmd.NewCommand().AddArguments("add") - if all { - cmd.AddArguments("--all") - } - cmd.AddDashesAndList(files...) - _, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) - return err -} - -// CommitChangesOptions the options when a commit created -type CommitChangesOptions struct { - Committer *Signature - Author *Signature - Message string -} - -// CommitChanges commits local changes with given committer, author and message. -// If author is nil, it will be the same as committer. -func CommitChanges(ctx context.Context, repoPath string, opts CommitChangesOptions) error { - cmd := gitcmd.NewCommand() - if opts.Committer != nil { - cmd.AddOptionValues("-c", "user.name="+opts.Committer.Name) - cmd.AddOptionValues("-c", "user.email="+opts.Committer.Email) - } - cmd.AddArguments("commit") - - if opts.Author == nil { - opts.Author = opts.Committer - } - if opts.Author != nil { - cmd.AddOptionFormat("--author='%s <%s>'", opts.Author.Name, opts.Author.Email) - } - cmd.AddOptionFormat("--message=%s", opts.Message) - - _, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) - // No stderr but exit status 1 means nothing to commit. - if err != nil && err.Error() == "exit status 1" { - return nil - } - return err -} - -// AllCommitsCount returns count of all commits in repository -func AllCommitsCount(ctx context.Context, repoPath string, hidePRRefs bool, files ...string) (int64, error) { - cmd := gitcmd.NewCommand("rev-list") - if hidePRRefs { - cmd.AddArguments("--exclude=" + PullPrefix + "*") - } - cmd.AddArguments("--all", "--count") - if len(files) > 0 { - cmd.AddDashesAndList(files...) - } - - stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) - if err != nil { - return 0, err - } - - return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) -} - -// CommitsCountOptions the options when counting commits -type CommitsCountOptions struct { - RepoPath string - Not string - Revision []string - RelPath []string - Since string - Until string -} - -// CommitsCount returns number of total commits of until given revision. -func CommitsCount(ctx context.Context, opts CommitsCountOptions) (int64, error) { - cmd := gitcmd.NewCommand("rev-list", "--count") - - cmd.AddDynamicArguments(opts.Revision...) - - if opts.Not != "" { - cmd.AddOptionValues("--not", opts.Not) - } - - if len(opts.RelPath) > 0 { - cmd.AddDashesAndList(opts.RelPath...) - } - - stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: opts.RepoPath}) - if err != nil { - return 0, err - } - - return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) -} - -// CommitsCount returns number of total commits of until current revision. -func (c *Commit) CommitsCount() (int64, error) { - return CommitsCount(c.repo.Ctx, CommitsCountOptions{ - RepoPath: c.repo.Path, - Revision: []string{c.ID.String()}, - }) -} - // CommitsByRange returns the specific page commits before current revision, every page's number default by CommitsRangeSize func (c *Commit) CommitsByRange(page, pageSize int, not, since, until string) ([]*Commit, error) { return c.repo.commitsByRangeWithTime(c.ID, page, pageSize, not, since, until) @@ -208,7 +101,10 @@ func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) { return false, nil } - _, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor").AddDynamicArguments(that, this).RunStdString(c.repo.Ctx, &gitcmd.RunOpts{Dir: c.repo.Path}) + _, _, err := gitcmd.NewCommand("merge-base", "--is-ancestor"). + AddDynamicArguments(that, this). + WithDir(c.repo.Path). + RunStdString(c.repo.Ctx) if err == nil { return true, nil } @@ -347,110 +243,14 @@ func (c *Commit) GetFileContent(filename string, limit int) (string, error) { return string(bytes), nil } -// GetBranchName gets the closest branch name (as returned by 'git name-rev --name-only') -func (c *Commit) GetBranchName() (string, error) { - cmd := gitcmd.NewCommand("name-rev") - if DefaultFeatures().CheckVersionAtLeast("2.13.0") { - cmd.AddArguments("--exclude", "refs/tags/*") - } - cmd.AddArguments("--name-only", "--no-undefined").AddDynamicArguments(c.ID.String()) - data, _, err := cmd.RunStdString(c.repo.Ctx, &gitcmd.RunOpts{Dir: c.repo.Path}) - if err != nil { - // handle special case where git can not describe commit - if strings.Contains(err.Error(), "cannot describe") { - return "", nil - } - - return "", err - } - - // name-rev commitID output will be "master" or "master~12" - return strings.SplitN(strings.TrimSpace(data), "~", 2)[0], nil -} - -// CommitFileStatus represents status of files in a commit. -type CommitFileStatus struct { - Added []string - Removed []string - Modified []string -} - -// NewCommitFileStatus creates a CommitFileStatus -func NewCommitFileStatus() *CommitFileStatus { - return &CommitFileStatus{ - []string{}, []string{}, []string{}, - } -} - -func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) { - rd := bufio.NewReader(stdout) - peek, err := rd.Peek(1) - if err != nil { - if err != io.EOF { - log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) - } - return - } - if peek[0] == '\n' || peek[0] == '\x00' { - _, _ = rd.Discard(1) - } - for { - modifier, err := rd.ReadString('\x00') - if err != nil { - if err != io.EOF { - log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) - } - return - } - file, err := rd.ReadString('\x00') - if err != nil { - if err != io.EOF { - log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) - } - return - } - file = file[:len(file)-1] - switch modifier[0] { - case 'A': - fileStatus.Added = append(fileStatus.Added, file) - case 'D': - fileStatus.Removed = append(fileStatus.Removed, file) - case 'M': - fileStatus.Modified = append(fileStatus.Modified, file) - } - } -} - -// GetCommitFileStatus returns file status of commit in given repository. -func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*CommitFileStatus, error) { - stdout, w := io.Pipe() - done := make(chan struct{}) - fileStatus := NewCommitFileStatus() - go func() { - parseCommitFileStatus(fileStatus, stdout) - close(done) - }() - - stderr := new(bytes.Buffer) - err := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1").AddDynamicArguments(commitID).Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdout: w, - Stderr: stderr, - }) - w.Close() // Close writer to exit parsing goroutine - if err != nil { - return nil, gitcmd.ConcatenateError(err, stderr.String()) - } - - <-done - return fileStatus, nil -} - // GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository. func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) { - commitID, _, err := gitcmd.NewCommand("rev-parse").AddDynamicArguments(shortID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + commitID, _, err := gitcmd.NewCommand("rev-parse"). + AddDynamicArguments(shortID). + WithDir(repoPath). + RunStdString(ctx) if err != nil { - if strings.Contains(err.Error(), "exit status 128") { + if gitcmd.IsErrorExitCode(err, 128) { return "", ErrNotExist{shortID, ""} } return "", err @@ -458,14 +258,6 @@ func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, err return strings.TrimSpace(commitID), nil } -// GetRepositoryDefaultPublicGPGKey returns the default public key for this commit -func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) { - if c.repo == nil { - return nil, nil - } - return c.repo.GetDefaultPublicGPGKey(forceUpdate) -} - func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool { maxLen := 64 // sha256 if objFmt != nil { diff --git a/package/gitea/source/modules/git/commit_info_test.go b/package/gitea/source/modules/git/commit_info_test.go index 51e1551d..1e1697b0 100644 --- a/package/gitea/source/modules/git/commit_info_test.go +++ b/package/gitea/source/modules/git/commit_info_test.go @@ -30,28 +30,57 @@ func cloneRepo(tb testing.TB, url string) (string, error) { } func testGetCommitsInfo(t *testing.T, repo1 *Repository) { + type expectedEntryInfo struct { + CommitID string + Size int64 + } + // these test case are specific to the repo1 test repo testCases := []struct { CommitID string Path string - ExpectedIDs map[string]string + ExpectedIDs map[string]expectedEntryInfo ExpectedTreeCommit string }{ - {"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", "", map[string]string{ - "file1.txt": "95bb4d39648ee7e325106df01a621c530863a653", - "file2.txt": "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + {"8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", "", map[string]expectedEntryInfo{ + "file1.txt": { + CommitID: "95bb4d39648ee7e325106df01a621c530863a653", + Size: 6, + }, + "file2.txt": { + CommitID: "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + Size: 6, + }, }, "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2"}, - {"2839944139e0de9737a044f78b0e4b40d989a9e3", "", map[string]string{ - "file1.txt": "2839944139e0de9737a044f78b0e4b40d989a9e3", - "branch1.txt": "9c9aef8dd84e02bc7ec12641deb4c930a7c30185", + {"2839944139e0de9737a044f78b0e4b40d989a9e3", "", map[string]expectedEntryInfo{ + "file1.txt": { + CommitID: "2839944139e0de9737a044f78b0e4b40d989a9e3", + Size: 15, + }, + "branch1.txt": { + CommitID: "9c9aef8dd84e02bc7ec12641deb4c930a7c30185", + Size: 8, + }, }, "2839944139e0de9737a044f78b0e4b40d989a9e3"}, - {"5c80b0245c1c6f8343fa418ec374b13b5d4ee658", "branch2", map[string]string{ - "branch2.txt": "5c80b0245c1c6f8343fa418ec374b13b5d4ee658", + {"5c80b0245c1c6f8343fa418ec374b13b5d4ee658", "branch2", map[string]expectedEntryInfo{ + "branch2.txt": { + CommitID: "5c80b0245c1c6f8343fa418ec374b13b5d4ee658", + Size: 8, + }, }, "5c80b0245c1c6f8343fa418ec374b13b5d4ee658"}, - {"feaf4ba6bc635fec442f46ddd4512416ec43c2c2", "", map[string]string{ - "file1.txt": "95bb4d39648ee7e325106df01a621c530863a653", - "file2.txt": "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", - "foo": "37991dec2c8e592043f47155ce4808d4580f9123", + {"feaf4ba6bc635fec442f46ddd4512416ec43c2c2", "", map[string]expectedEntryInfo{ + "file1.txt": { + CommitID: "95bb4d39648ee7e325106df01a621c530863a653", + Size: 6, + }, + "file2.txt": { + CommitID: "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + Size: 6, + }, + "foo": { + CommitID: "37991dec2c8e592043f47155ce4808d4580f9123", + Size: 0, + }, }, "feaf4ba6bc635fec442f46ddd4512416ec43c2c2"}, } for _, testCase := range testCases { @@ -93,11 +122,12 @@ func testGetCommitsInfo(t *testing.T, repo1 *Repository) { for _, commitInfo := range commitsInfo { entry := commitInfo.Entry commit := commitInfo.Commit - expectedID, ok := testCase.ExpectedIDs[entry.Name()] + expectedInfo, ok := testCase.ExpectedIDs[entry.Name()] if !assert.True(t, ok) { continue } - assert.Equal(t, expectedID, commit.ID.String()) + assert.Equal(t, expectedInfo.CommitID, commit.ID.String()) + assert.Equal(t, expectedInfo.Size, entry.Size(), entry.Name()) } } } @@ -173,7 +203,6 @@ func BenchmarkEntries_GetCommitsInfo(b *testing.B) { } else if entries, err = commit.Tree.ListEntries(); err != nil { b.Fatal(err) } - entries.Sort() b.ResetTimer() b.Run(benchmark.name, func(b *testing.B) { for b.Loop() { diff --git a/package/gitea/source/modules/git/commit_sha256_test.go b/package/gitea/source/modules/git/commit_sha256_test.go index 772f5eed..0aefb30c 100644 --- a/package/gitea/source/modules/git/commit_sha256_test.go +++ b/package/gitea/source/modules/git/commit_sha256_test.go @@ -14,33 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestCommitsCountSha256(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") - - commitsCount, err := CommitsCount(t.Context(), - CommitsCountOptions{ - RepoPath: bareRepo1Path, - Revision: []string{"f004f41359117d319dedd0eaab8c5259ee2263da839dcba33637997458627fdc"}, - }) - - assert.NoError(t, err) - assert.Equal(t, int64(3), commitsCount) -} - -func TestCommitsCountWithoutBaseSha256(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") - - commitsCount, err := CommitsCount(t.Context(), - CommitsCountOptions{ - RepoPath: bareRepo1Path, - Not: "main", - Revision: []string{"branch1"}, - }) - - assert.NoError(t, err) - assert.Equal(t, int64(2), commitsCount) -} - func TestGetFullCommitIDSha256(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare_sha256") @@ -157,39 +130,3 @@ func TestHasPreviousCommitSha256(t *testing.T) { assert.NoError(t, err) assert.False(t, selfNot) } - -func TestGetCommitFileStatusMergesSha256(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo6_merge_sha256") - - commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo1Path, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1") - assert.NoError(t, err) - - expected := CommitFileStatus{ - []string{ - "add_file.txt", - }, - []string{}, - []string{ - "to_modify.txt", - }, - } - - assert.Equal(t, expected.Added, commitFileStatus.Added) - assert.Equal(t, expected.Removed, commitFileStatus.Removed) - assert.Equal(t, expected.Modified, commitFileStatus.Modified) - - expected = CommitFileStatus{ - []string{}, - []string{ - "to_remove.txt", - }, - []string{}, - } - - commitFileStatus, err = GetCommitFileStatus(t.Context(), bareRepo1Path, "da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172") - assert.NoError(t, err) - - assert.Equal(t, expected.Added, commitFileStatus.Added) - assert.Equal(t, expected.Removed, commitFileStatus.Removed) - assert.Equal(t, expected.Modified, commitFileStatus.Modified) -} diff --git a/package/gitea/source/modules/git/commit_submodule.go b/package/gitea/source/modules/git/commit_submodule.go index ff253b7e..5e5f90c2 100644 --- a/package/gitea/source/modules/git/commit_submodule.go +++ b/package/gitea/source/modules/git/commit_submodule.go @@ -16,7 +16,7 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) { entry, err := c.GetTreeEntryByPath(".gitmodules") if err != nil { if _, ok := err.(ErrNotExist); ok { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist } return nil, err } @@ -48,5 +48,5 @@ func (c *Commit) GetSubModule(entryName string) (*SubModule, error) { return module, nil } } - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist } diff --git a/package/gitea/source/modules/git/commit_test.go b/package/gitea/source/modules/git/commit_test.go index 688b4e29..d399fb37 100644 --- a/package/gitea/source/modules/git/commit_test.go +++ b/package/gitea/source/modules/git/commit_test.go @@ -13,33 +13,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestCommitsCount(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - - commitsCount, err := CommitsCount(t.Context(), - CommitsCountOptions{ - RepoPath: bareRepo1Path, - Revision: []string{"8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"}, - }) - - assert.NoError(t, err) - assert.Equal(t, int64(3), commitsCount) -} - -func TestCommitsCountWithoutBase(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - - commitsCount, err := CommitsCount(t.Context(), - CommitsCountOptions{ - RepoPath: bareRepo1Path, - Not: "master", - Revision: []string{"branch1"}, - }) - - assert.NoError(t, err) - assert.Equal(t, int64(2), commitsCount) -} - func TestGetFullCommitID(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") @@ -186,6 +159,14 @@ ISO-8859-1`, commitFromReader.Signature.Payload) assert.Equal(t, commitFromReader, commitFromReader2) } +func TestCommitMessageSanitizesInvalidUTF8(t *testing.T) { + commit := &Commit{ + CommitMessage: "title \xff\n\n\nbody \xff\n\n\n", + } + assert.Equal(t, "title ?\n\n\nbody ?\n\n\n", commit.Message()) + assert.Equal(t, "title ?", commit.Summary()) +} + func TestHasPreviousCommit(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") @@ -212,134 +193,6 @@ func TestHasPreviousCommit(t *testing.T) { assert.False(t, selfNot) } -func TestParseCommitFileStatus(t *testing.T) { - type testcase struct { - output string - added []string - removed []string - modified []string - } - - kases := []testcase{ - { - // Merge commit - output: "MM\x00options/locale/locale_en-US.ini\x00", - modified: []string{ - "options/locale/locale_en-US.ini", - }, - added: []string{}, - removed: []string{}, - }, - { - // Spaces commit - output: "D\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00", - removed: []string{ - "b", - "b b/b", - }, - modified: []string{}, - added: []string{ - "b b/b b/b b/b", - "b b/b b/b b/b b/b", - }, - }, - { - // larger commit - output: "M\x00go.mod\x00M\x00go.sum\x00M\x00modules/ssh/ssh.go\x00M\x00vendor/github.com/gliderlabs/ssh/circle.yml\x00M\x00vendor/github.com/gliderlabs/ssh/context.go\x00A\x00vendor/github.com/gliderlabs/ssh/go.mod\x00A\x00vendor/github.com/gliderlabs/ssh/go.sum\x00M\x00vendor/github.com/gliderlabs/ssh/server.go\x00M\x00vendor/github.com/gliderlabs/ssh/session.go\x00M\x00vendor/github.com/gliderlabs/ssh/ssh.go\x00M\x00vendor/golang.org/x/sys/unix/mkerrors.sh\x00M\x00vendor/golang.org/x/sys/unix/syscall_darwin.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_linux.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go\x00M\x00vendor/modules.txt\x00", - modified: []string{ - "go.mod", - "go.sum", - "modules/ssh/ssh.go", - "vendor/github.com/gliderlabs/ssh/circle.yml", - "vendor/github.com/gliderlabs/ssh/context.go", - "vendor/github.com/gliderlabs/ssh/server.go", - "vendor/github.com/gliderlabs/ssh/session.go", - "vendor/github.com/gliderlabs/ssh/ssh.go", - "vendor/golang.org/x/sys/unix/mkerrors.sh", - "vendor/golang.org/x/sys/unix/syscall_darwin.go", - "vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go", - "vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go", - "vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go", - "vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go", - "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go", - "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go", - "vendor/golang.org/x/sys/unix/zerrors_linux.go", - "vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go", - "vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go", - "vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go", - "vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go", - "vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go", - "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go", - "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go", - "vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go", - "vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go", - "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go", - "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go", - "vendor/modules.txt", - }, - added: []string{ - "vendor/github.com/gliderlabs/ssh/go.mod", - "vendor/github.com/gliderlabs/ssh/go.sum", - }, - removed: []string{}, - }, - { - // git 1.7.2 adds an unnecessary \x00 on merge commit - output: "\x00MM\x00options/locale/locale_en-US.ini\x00", - modified: []string{ - "options/locale/locale_en-US.ini", - }, - added: []string{}, - removed: []string{}, - }, - { - // git 1.7.2 adds an unnecessary \n on normal commit - output: "\nD\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00", - removed: []string{ - "b", - "b b/b", - }, - modified: []string{}, - added: []string{ - "b b/b b/b b/b", - "b b/b b/b b/b b/b", - }, - }, - } - - for _, kase := range kases { - fileStatus := NewCommitFileStatus() - parseCommitFileStatus(fileStatus, strings.NewReader(kase.output)) - - assert.Equal(t, kase.added, fileStatus.Added) - assert.Equal(t, kase.removed, fileStatus.Removed) - assert.Equal(t, kase.modified, fileStatus.Modified) - } -} - -func TestGetCommitFileStatusMerges(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo6_merge") - - commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo1Path, "022f4ce6214973e018f02bf363bf8a2e3691f699") - assert.NoError(t, err) - - expected := CommitFileStatus{ - []string{ - "add_file.txt", - }, - []string{ - "to_remove.txt", - }, - []string{ - "to_modify.txt", - }, - } - - assert.Equal(t, expected.Added, commitFileStatus.Added) - assert.Equal(t, expected.Removed, commitFileStatus.Removed) - assert.Equal(t, expected.Modified, commitFileStatus.Modified) -} - func Test_GetCommitBranchStart(t *testing.T) { bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") repo, err := OpenRepository(t.Context(), bareRepo1Path) diff --git a/package/gitea/source/modules/git/config.go b/package/gitea/source/modules/git/config.go index 2eafe971..79aa0535 100644 --- a/package/gitea/source/modules/git/config.go +++ b/package/gitea/source/modules/git/config.go @@ -118,7 +118,9 @@ func syncGitConfig(ctx context.Context) (err error) { } func configSet(ctx context.Context, key, value string) error { - stdout, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx, nil) + stdout, _, err := gitcmd.NewCommand("config", "--global", "--get"). + AddDynamicArguments(key). + RunStdString(ctx) if err != nil && !gitcmd.IsErrorExitCode(err, 1) { return fmt.Errorf("failed to get git config %s, err: %w", key, err) } @@ -128,8 +130,9 @@ func configSet(ctx context.Context, key, value string) error { return nil } - _, _, err = gitcmd.NewCommand("config", "--global").AddDynamicArguments(key, value).RunStdString(ctx, nil) - if err != nil { + if _, _, err = gitcmd.NewCommand("config", "--global"). + AddDynamicArguments(key, value). + RunStdString(ctx); err != nil { return fmt.Errorf("failed to set git global config %s, err: %w", key, err) } @@ -137,14 +140,14 @@ func configSet(ctx context.Context, key, value string) error { } func configSetNonExist(ctx context.Context, key, value string) error { - _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx, nil) + _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx) if err == nil { // already exist return nil } if gitcmd.IsErrorExitCode(err, 1) { // not exist, set new config - _, _, err = gitcmd.NewCommand("config", "--global").AddDynamicArguments(key, value).RunStdString(ctx, nil) + _, _, err = gitcmd.NewCommand("config", "--global").AddDynamicArguments(key, value).RunStdString(ctx) if err != nil { return fmt.Errorf("failed to set git global config %s, err: %w", key, err) } @@ -155,14 +158,14 @@ func configSetNonExist(ctx context.Context, key, value string) error { } func configAddNonExist(ctx context.Context, key, value string) error { - _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx, nil) + _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx) if err == nil { // already exist return nil } if gitcmd.IsErrorExitCode(err, 1) { // not exist, add new config - _, _, err = gitcmd.NewCommand("config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(ctx, nil) + _, _, err = gitcmd.NewCommand("config", "--global", "--add").AddDynamicArguments(key, value).RunStdString(ctx) if err != nil { return fmt.Errorf("failed to add git global config %s, err: %w", key, err) } @@ -172,10 +175,10 @@ func configAddNonExist(ctx context.Context, key, value string) error { } func configUnsetAll(ctx context.Context, key, value string) error { - _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx, nil) + _, _, err := gitcmd.NewCommand("config", "--global", "--get").AddDynamicArguments(key).RunStdString(ctx) if err == nil { // exist, need to remove - _, _, err = gitcmd.NewCommand("config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx, nil) + _, _, err = gitcmd.NewCommand("config", "--global", "--unset-all").AddDynamicArguments(key, regexp.QuoteMeta(value)).RunStdString(ctx) if err != nil { return fmt.Errorf("failed to unset git global config %s, err: %w", key, err) } diff --git a/package/gitea/source/modules/git/diff.go b/package/gitea/source/modules/git/diff.go index d185cc92..d7732eaa 100644 --- a/package/gitea/source/modules/git/diff.go +++ b/package/gitea/source/modules/git/diff.go @@ -5,71 +5,83 @@ package git import ( "bufio" - "bytes" "context" "fmt" "io" - "os" "regexp" "strconv" "strings" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) -// RawDiffType type of a raw diff. +// RawDiffType output format: diff or patch type RawDiffType string -// RawDiffType possible values. const ( RawDiffNormal RawDiffType = "diff" RawDiffPatch RawDiffType = "patch" ) // GetRawDiff dumps diff results of repository in given commit ID to io.Writer. -func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer io.Writer) error { - return GetRepoRawDiffForFile(repo, "", commitID, diffType, "", writer) -} - -// GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer. -func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error { - stderr := new(bytes.Buffer) - cmd := gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R").AddDynamicArguments(commitID) - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdout: writer, - Stderr: stderr, - }); err != nil { - return fmt.Errorf("Run: %w - %s", err, stderr) +func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer io.Writer) (retErr error) { + cmd, err := getRepoRawDiffForFileCmd(repo.Ctx, repo, "", commitID, diffType, "") + if err != nil { + return fmt.Errorf("getRepoRawDiffForFileCmd: %w", err) } - return nil + return cmd.WithStdoutCopy(writer).RunWithStderr(repo.Ctx) } -// GetRepoRawDiffForFile dumps diff results of file in given commit ID to io.Writer according given repository -func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error { +// GetFileDiffCutAroundLine cuts the old or new part of the diff of a file around a specific line number +func GetFileDiffCutAroundLine( + repo *Repository, startCommit, endCommit, treePath string, + line int64, old bool, numbersOfLine int, +) (ret string, retErr error) { + cmd, err := getRepoRawDiffForFileCmd(repo.Ctx, repo, startCommit, endCommit, RawDiffNormal, treePath) + if err != nil { + return "", fmt.Errorf("getRepoRawDiffForFileCmd: %w", err) + } + stdoutReader, stdoutClose := cmd.MakeStdoutPipe() + defer stdoutClose() + cmd.WithPipelineFunc(func(ctx gitcmd.Context) error { + ret, err = CutDiffAroundLine(stdoutReader, line, old, numbersOfLine) + return err + }) + return ret, cmd.RunWithStderr(repo.Ctx) +} + +// getRepoRawDiffForFile returns an io.Reader for the diff results of file in given commit ID +// and a "finish" function to wait for the git command and clean up resources after reading is done. +func getRepoRawDiffForFileCmd(_ context.Context, repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string) (*gitcmd.Command, error) { commit, err := repo.GetCommit(endCommit) if err != nil { - return err + return nil, err } var files []string if len(file) > 0 { files = append(files, file) } - cmd := gitcmd.NewCommand() + cmd := gitcmd.NewCommand().WithDir(repo.Path) switch diffType { case RawDiffNormal: if len(startCommit) != 0 { - cmd.AddArguments("diff", "-M").AddDynamicArguments(startCommit, endCommit).AddDashesAndList(files...) + cmd.AddArguments("diff"). + AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold). + AddDynamicArguments(startCommit, endCommit).AddDashesAndList(files...) } else if commit.ParentCount() == 0 { cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { c, err := commit.Parent(0) if err != nil { - return err + return nil, err } - cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...) + cmd.AddArguments("diff"). + AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold). + AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...) } case RawDiffPatch: if len(startCommit) != 0 { @@ -80,24 +92,15 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } else { c, err := commit.Parent(0) if err != nil { - return err + return nil, err } query := fmt.Sprintf("%s...%s", endCommit, c.ID.String()) cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...) } default: - return fmt.Errorf("invalid diffType: %s", diffType) + return nil, util.NewInvalidArgumentErrorf("invalid diff type: %s", diffType) } - - stderr := new(bytes.Buffer) - if err = cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: writer, - Stderr: stderr, - }); err != nil { - return fmt.Errorf("Run: %w - %s", err, stderr) - } - return nil + return cmd, nil } // ParseDiffHunkString parse the diff hunk content and return @@ -234,7 +237,7 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi } } if err := scanner.Err(); err != nil { - return "", err + return "", fmt.Errorf("CutDiffAroundLine: scan: %w", err) } // No hunk found @@ -300,43 +303,27 @@ func GetAffectedFiles(repo *Repository, branchName, oldCommitID, newCommitID str } oldCommitID = startCommitID } - stdoutReader, stdoutWriter, err := os.Pipe() - if err != nil { - log.Error("Unable to create os.Pipe for %s", repo.Path) - return nil, err - } - defer func() { - _ = stdoutReader.Close() - _ = stdoutWriter.Close() - }() affectedFiles := make([]string, 0, 32) // Run `git diff --name-only` to get the names of the changed files - err = gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID). - Run(repo.Ctx, &gitcmd.RunOpts{ - Env: env, - Dir: repo.Path, - Stdout: stdoutWriter, - PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { - // Close the writer end of the pipe to begin processing - _ = stdoutWriter.Close() - defer func() { - // Close the reader on return to terminate the git command if necessary - _ = stdoutReader.Close() - }() - // Now scan the output from the command - scanner := bufio.NewScanner(stdoutReader) - for scanner.Scan() { - path := strings.TrimSpace(scanner.Text()) - if len(path) == 0 { - continue - } - affectedFiles = append(affectedFiles, path) + cmd := gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(oldCommitID, newCommitID) + stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() + defer stdoutReaderClose() + err := cmd.WithEnv(env).WithDir(repo.Path). + WithPipelineFunc(func(ctx gitcmd.Context) error { + // Now scan the output from the command + scanner := bufio.NewScanner(stdoutReader) + for scanner.Scan() { + path := strings.TrimSpace(scanner.Text()) + if len(path) == 0 { + continue } - return scanner.Err() - }, - }) + affectedFiles = append(affectedFiles, path) + } + return scanner.Err() + }). + Run(repo.Ctx) if err != nil { log.Error("Unable to get affected files for commits from %s to %s in %s: %v", oldCommitID, newCommitID, repo.Path, err) } diff --git a/package/gitea/source/modules/git/error.go b/package/gitea/source/modules/git/error.go index 7d131345..1b7bdca0 100644 --- a/package/gitea/source/modules/git/error.go +++ b/package/gitea/source/modules/git/error.go @@ -4,8 +4,6 @@ package git import ( - "context" - "errors" "fmt" "strings" @@ -98,28 +96,31 @@ func (err *ErrPushRejected) Unwrap() error { // GenerateMessage generates the remote message from the stderr func (err *ErrPushRejected) GenerateMessage() { - messageBuilder := &strings.Builder{} - i := strings.Index(err.StdErr, "remote: ") - if i < 0 { - err.Message = "" + // The stderr is like this: + // + // > remote: error: push is rejected ..... + // > To /work/gitea/tests/integration/gitea-integration-sqlite/gitea-repositories/user2/repo1.git + // > ! [remote rejected] 44e67c77559211d21b630b902cdcc6ab9d4a4f51 -> develop (pre-receive hook declined) + // > error: failed to push some refs to '/work/gitea/tests/integration/gitea-integration-sqlite/gitea-repositories/user2/repo1.git' + // + // The local message contains sensitive information, so we only need the remote message + const prefixRemote = "remote: " + const prefixError = "error: " + pos := strings.Index(err.StdErr, prefixRemote) + if pos < 0 { + err.Message = "push is rejected" return } - for { - if len(err.StdErr) <= i+8 { - break - } - if err.StdErr[i:i+8] != "remote: " { - break - } - i += 8 - nl := strings.IndexByte(err.StdErr[i:], '\n') - if nl >= 0 { - messageBuilder.WriteString(err.StdErr[i : i+nl+1]) - i = i + nl + 1 - } else { - messageBuilder.WriteString(err.StdErr[i:]) - i = len(err.StdErr) + + messageBuilder := &strings.Builder{} + lines := strings.SplitSeq(err.StdErr, "\n") + for line := range lines { + line, ok := strings.CutPrefix(line, prefixRemote) + if !ok { + continue } + line = strings.TrimPrefix(line, prefixError) + messageBuilder.WriteString(strings.TrimSpace(line) + "\n") } err.Message = strings.TrimSpace(messageBuilder.String()) } @@ -140,10 +141,3 @@ func IsErrMoreThanOne(err error) bool { func (err *ErrMoreThanOne) Error() string { return fmt.Sprintf("ErrMoreThanOne Error: %v: %s\n%s", err.Err, err.StdErr, err.StdOut) } - -func IsErrCanceledOrKilled(err error) bool { - // When "cancel()" a git command's context, the returned error of "Run()" could be one of them: - // - context.Canceled - // - *exec.ExitError: "signal: killed" - return err != nil && (errors.Is(err, context.Canceled) || err.Error() == "signal: killed") -} diff --git a/package/gitea/source/modules/git/foreachref/format.go b/package/gitea/source/modules/git/foreachref/format.go index d9573a55..87c1c9a4 100644 --- a/package/gitea/source/modules/git/foreachref/format.go +++ b/package/gitea/source/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 := "" + var escaped strings.Builder for i := range delim { - escaped += "%" + hex.EncodeToString([]byte{delim[i]}) + escaped.WriteString("%" + hex.EncodeToString([]byte{delim[i]})) } - return escaped + return escaped.String() } diff --git a/package/gitea/source/modules/git/foreachref/parser.go b/package/gitea/source/modules/git/foreachref/parser.go index 4e88e943..91868076 100644 --- a/package/gitea/source/modules/git/foreachref/parser.go +++ b/package/gitea/source/modules/git/foreachref/parser.go @@ -100,7 +100,7 @@ func (p *Parser) Err() error { func (p *Parser) parseRef(refBlock string) (map[string]string, error) { if refBlock == "" { // must be at EOF - return nil, nil + return nil, nil //nolint:nilnil // return nil to signal EOF } fieldValues := make(map[string]string) @@ -115,10 +115,10 @@ func (p *Parser) parseRef(refBlock string) (map[string]string, error) { var fieldKey string var fieldVal string - firstSpace := strings.Index(field, " ") - if firstSpace > 0 { - fieldKey = field[:firstSpace] - fieldVal = field[firstSpace+1:] + before, after, ok := strings.Cut(field, " ") + if ok { + fieldKey = before + fieldVal = after } else { // could be the case if the requested field had no value fieldKey = field diff --git a/package/gitea/source/modules/git/git.go b/package/gitea/source/modules/git/git.go index 161fa421..2df83f98 100644 --- a/package/gitea/source/modules/git/git.go +++ b/package/gitea/source/modules/git/git.go @@ -12,25 +12,28 @@ import ( "path/filepath" "runtime" "strings" - "time" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/tempdir" + "code.gitea.io/gitea/modules/testlogger" "github.com/hashicorp/go-version" ) -const RequiredVersion = "2.0.0" // the minimum Git version required +const RequiredVersion = "2.6.0" // the minimum Git version required type Features struct { gitVersion *version.Version - UsingGogit bool - SupportProcReceive bool // >= 2.29 - SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ - SupportedObjectFormats []ObjectFormat // sha1, sha256 - SupportCheckAttrOnBare bool // >= 2.40 + UsingGogit bool + SupportProcReceive bool // >= 2.29 + SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’ + SupportedObjectFormats []ObjectFormat // sha1, sha256 + SupportCheckAttrOnBare bool // >= 2.40 + SupportCatFileBatchCommand bool // >= 2.36, support `git cat-file --batch-command` + SupportGitMergeTree bool // >= 2.40 // we also need "--merge-base" } var defaultFeatures *Features @@ -57,7 +60,7 @@ func DefaultFeatures() *Features { } func loadGitVersionFeatures() (*Features, error) { - stdout, _, runErr := gitcmd.NewCommand("version").RunStdString(context.Background(), nil) + stdout, _, runErr := gitcmd.NewCommand("version").RunStdString(context.Background()) if runErr != nil { return nil, runErr } @@ -75,6 +78,8 @@ func loadGitVersionFeatures() (*Features, error) { features.SupportedObjectFormats = append(features.SupportedObjectFormats, Sha256ObjectFormat) } features.SupportCheckAttrOnBare = features.CheckVersionAtLeast("2.40") + features.SupportCatFileBatchCommand = features.CheckVersionAtLeast("2.36") + features.SupportGitMergeTree = features.CheckVersionAtLeast("2.40") // we also need "--merge-base" return features, nil } @@ -84,12 +89,17 @@ func parseGitVersionLine(s string) (*version.Version, error) { return nil, fmt.Errorf("invalid git version: %q", s) } - // version string is like: "git version 2.29.3" or "git version 2.29.3.windows.1" + // version output is like: "git version {versionString}" + // versionString can be: + // * "2.5.3" + // * "2.29.3.windows.1" + // * "2.28.0.618.gf4bc123cb7": https://github.com/go-gitea/gitea/issues/12731 versionString := fields[2] - if pos := strings.Index(versionString, "windows"); pos >= 1 { - versionString = versionString[:pos-1] + versionFields := strings.Split(versionString, ".") + if len(versionFields) > 3 { + versionFields = versionFields[:3] } - return version.NewVersion(versionString) + return version.NewVersion(strings.Join(versionFields, ".")) } func checkGitVersionCompatibility(gitVer *version.Version) error { @@ -137,10 +147,6 @@ func InitSimple() error { log.Warn("git module has been initialized already, duplicate init may work but it's better to fix it") } - if setting.Git.Timeout.Default > 0 { - gitcmd.SetDefaultCommandExecutionTimeout(time.Duration(setting.Git.Timeout.Default) * time.Second) - } - if err := gitcmd.SetExecutablePath(setting.Git.Path); err != nil { return err } @@ -176,3 +182,23 @@ func InitFull() (err error) { return syncGitConfig(context.Background()) } + +// RunGitTests helps to init the git module and run tests. +// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right, setting.Git.HomePath is initialized in this package but used in gitcmd package +func RunGitTests(m interface{ Run() int }) { + os.Exit(runGitTests(m)) +} + +func runGitTests(m interface{ Run() int }) int { + gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") + if err != nil { + testlogger.Panicf("unable to create temp dir: %s", err.Error()) + } + defer cleanup() + + setting.Git.HomePath = gitHomePath + if err = InitFull(); err != nil { + testlogger.Panicf("failed to call Init: %s", err.Error()) + } + return m.Run() +} diff --git a/package/gitea/source/modules/git/git_test.go b/package/gitea/source/modules/git/git_test.go index 7a8ca74b..e21cbe44 100644 --- a/package/gitea/source/modules/git/git_test.go +++ b/package/gitea/source/modules/git/git_test.go @@ -4,42 +4,14 @@ package git import ( - "fmt" - "os" "testing" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/tempdir" - "github.com/hashicorp/go-version" "github.com/stretchr/testify/assert" ) -func testRun(m *testing.M) error { - gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") - if err != nil { - return fmt.Errorf("unable to create temp dir: %w", err) - } - defer cleanup() - - setting.Git.HomePath = gitHomePath - - if err = InitFull(); err != nil { - return fmt.Errorf("failed to call Init: %w", err) - } - - exitCode := m.Run() - if exitCode != 0 { - return fmt.Errorf("run test failed, ExitCode=%d", exitCode) - } - return nil -} - func TestMain(m *testing.M) { - if err := testRun(m); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err) - os.Exit(1) - } + RunGitTests(m) } func TestParseGitVersion(t *testing.T) { @@ -51,6 +23,10 @@ func TestParseGitVersion(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "2.29.3", v.String()) + v, err = parseGitVersionLine("git version 2.28.0.618.gf4bc123cb7") + assert.NoError(t, err) + assert.Equal(t, "2.28.0", v.String()) + _, err = parseGitVersionLine("git version") assert.Error(t, err) diff --git a/package/gitea/source/modules/git/gitcmd/command.go b/package/gitea/source/modules/git/gitcmd/command.go index ed2f6fb6..ee447dfd 100644 --- a/package/gitea/source/modules/git/gitcmd/command.go +++ b/package/gitea/source/modules/git/gitcmd/command.go @@ -13,7 +13,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strings" "time" @@ -29,34 +28,41 @@ import ( // In most cases, it shouldn't be used. Use AddXxx function instead type TrustedCmdArgs []internal.CmdArg -// defaultCommandExecutionTimeout default command execution timeout duration -var defaultCommandExecutionTimeout = 360 * time.Second - -func SetDefaultCommandExecutionTimeout(timeout time.Duration) { - defaultCommandExecutionTimeout = timeout -} - -// DefaultLocale is the default LC_ALL to run git commands in. -const DefaultLocale = "C" - // Command represents a command with its subcommands or arguments. type Command struct { + callerInfo string prog string args []string - brokenArgs []string - cmd *exec.Cmd // for debug purpose only + preErrors []error configArgs []string + opts runOpts + + cmd *exec.Cmd + + cmdCtx context.Context + cmdCancel process.CancelCauseFunc + cmdFinished process.FinishedFunc + cmdStartTime time.Time + + parentPipeFiles []*os.File + parentPipeReaders []*os.File + childrenPipeFiles []*os.File + + // only os.Pipe and in-memory buffers can work with Stdin safely, see https://github.com/golang/go/issues/77227 if the command would exit unexpectedly + cmdStdin io.Reader + cmdStdout io.Writer + cmdStderr io.Writer + + cmdManagedStderr *bytes.Buffer } func logArgSanitize(arg string) string { - if strings.Contains(arg, "://") && strings.Contains(arg, "@") { - return util.SanitizeCredentialURLs(arg) - } else if filepath.IsAbs(arg) { + if filepath.IsAbs(arg) { base := filepath.Base(arg) dir := filepath.Dir(arg) return ".../" + filepath.Join(filepath.Base(dir), base) } - return arg + return util.SanitizeCredentialURLs(arg) } func (c *Command) LogString() string { @@ -96,6 +102,10 @@ func NewCommand(args ...internal.CmdArg) *Command { } } +func (c *Command) handlePreErrorBrokenCommand(arg string) { + c.preErrors = append(c.preErrors, util.ErrorWrap(ErrBrokenCommand, `broken git command argument %q`, arg)) +} + // isSafeArgumentValue checks if the argument is safe to be used as a value (not an option) func isSafeArgumentValue(s string) bool { return s == "" || s[0] != '-' @@ -123,7 +133,7 @@ func (c *Command) AddArguments(args ...internal.CmdArg) *Command { // The values are treated as dynamic argument values. It equals to: AddArguments("--opt") then AddDynamicArguments(val). func (c *Command) AddOptionValues(opt internal.CmdArg, args ...string) *Command { if !isValidArgumentOption(string(opt)) { - c.brokenArgs = append(c.brokenArgs, string(opt)) + c.handlePreErrorBrokenCommand(string(opt)) return c } c.args = append(c.args, string(opt)) @@ -135,12 +145,12 @@ func (c *Command) AddOptionValues(opt internal.CmdArg, args ...string) *Command // For example: AddOptionFormat("--opt=%s %s", val1, val2) means 1 argument: {"--opt=val1 val2"}. func (c *Command) AddOptionFormat(opt string, args ...any) *Command { if !isValidArgumentOption(opt) { - c.brokenArgs = append(c.brokenArgs, opt) + c.handlePreErrorBrokenCommand(opt) return c } // a quick check to make sure the format string matches the number of arguments, to find low-level mistakes ASAP if strings.Count(strings.ReplaceAll(opt, "%%", ""), "%") != len(args) { - c.brokenArgs = append(c.brokenArgs, opt) + c.handlePreErrorBrokenCommand(opt) return c } s := fmt.Sprintf(opt, args...) @@ -154,10 +164,10 @@ func (c *Command) AddOptionFormat(opt string, args ...any) *Command { func (c *Command) AddDynamicArguments(args ...string) *Command { for _, arg := range args { if !isSafeArgumentValue(arg) { - c.brokenArgs = append(c.brokenArgs, arg) + c.handlePreErrorBrokenCommand(arg) } } - if len(c.brokenArgs) != 0 { + if len(c.preErrors) != 0 { return c } c.args = append(c.args, args...) @@ -177,7 +187,7 @@ func (c *Command) AddDashesAndList(list ...string) *Command { func (c *Command) AddConfig(key, value string) *Command { kv := key + "=" + value if !isSafeArgumentValue(kv) { - c.brokenArgs = append(c.brokenArgs, key) + c.handlePreErrorBrokenCommand(kv) } else { c.configArgs = append(c.configArgs, "-c", kv) } @@ -194,11 +204,9 @@ func ToTrustedCmdArgs(args []string) TrustedCmdArgs { return ret } -// RunOpts represents parameters to run the command. If UseContextTimeout is specified, then Timeout is ignored. -type RunOpts struct { - Env []string - Timeout time.Duration - UseContextTimeout bool +type runOpts struct { + Env []string + Timeout time.Duration // Dir is the working dir for the git command, however: // FIXME: this could be incorrect in many cases, for example: @@ -208,19 +216,7 @@ type RunOpts struct { // The correct approach is to use `--git-dir" global argument Dir string - Stdout, Stderr io.Writer - - // Stdin is used for passing input to the command - // The caller must make sure the Stdin writer is closed properly to finish the Run function. - // Otherwise, the Run function may hang for long time or forever, especially when the Git's context deadline is not the same as the caller's. - // Some common mistakes: - // * `defer stdinWriter.Close()` then call `cmd.Run()`: the Run() would never return if the command is killed by timeout - // * `go { case <- parentContext.Done(): stdinWriter.Close() }` with `cmd.Run(DefaultTimeout)`: the command would have been killed by timeout but the Run doesn't return until stdinWriter.Close() - // * `go { if stdoutReader.Read() err != nil: stdinWriter.Close() }` with `cmd.Run()`: the stdoutReader may never return error if the command is killed by timeout - // In the future, ideally the git module itself should have full control of the stdin, to avoid such problems and make it easier to refactor to a better architecture. - Stdin io.Reader - - PipelineFunc func(context.Context, context.CancelFunc) error + PipelineFunc func(Context) error } func commonBaseEnvs() []string { @@ -251,7 +247,7 @@ func commonBaseEnvs() []string { // CommonGitCmdEnvs returns the common environment variables for a "git" command. func CommonGitCmdEnvs() []string { return append(commonBaseEnvs(), []string{ - "LC_ALL=" + DefaultLocale, + "LC_ALL=C", // ensure git output is in English, error messages are parsed in English "GIT_TERMINAL_PROMPT=0", // avoid prompting for credentials interactively, supported since git v2.3 }...) } @@ -263,185 +259,320 @@ func CommonCmdServEnvs() []string { var ErrBrokenCommand = errors.New("git command is broken") -// Run runs the command with the RunOpts -func (c *Command) Run(ctx context.Context, opts *RunOpts) error { - return c.run(ctx, 1, opts) +func (c *Command) WithDir(dir string) *Command { + c.opts.Dir = dir + return c } -func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error { - if len(c.brokenArgs) != 0 { - log.Error("git command is broken: %s, broken args: %s", c.LogString(), strings.Join(c.brokenArgs, " ")) - return ErrBrokenCommand - } - if opts == nil { - opts = &RunOpts{} - } +func (c *Command) WithEnv(env []string) *Command { + c.opts.Env = env + return c +} - // We must not change the provided options - timeout := opts.Timeout - if timeout <= 0 { - timeout = defaultCommandExecutionTimeout - } +func (c *Command) WithTimeout(timeout time.Duration) *Command { + c.opts.Timeout = timeout + return c +} - cmdLogString := c.LogString() - callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */) +func (c *Command) makeStdoutStderr(w *io.Writer) (PipeReader, func()) { + pr, pw, err := os.Pipe() + if err != nil { + c.preErrors = append(c.preErrors, err) + return &pipeNull{err}, func() {} + } + c.childrenPipeFiles = append(c.childrenPipeFiles, pw) + c.parentPipeFiles = append(c.parentPipeFiles, pr) + c.parentPipeReaders = append(c.parentPipeReaders, pr) + *w /* stdout, stderr */ = pw + return &pipeReader{f: pr}, func() { pr.Close() } +} + +// MakeStdinPipe creates a writer for the command's stdin. +// The returned closer function must be called by the caller to close the pipe. +func (c *Command) MakeStdinPipe() (writer PipeWriter, closer func()) { + pr, pw, err := os.Pipe() + if err != nil { + c.preErrors = append(c.preErrors, err) + return &pipeNull{err}, func() {} + } + c.childrenPipeFiles = append(c.childrenPipeFiles, pr) + c.parentPipeFiles = append(c.parentPipeFiles, pw) + c.cmdStdin = pr + return &pipeWriter{pw}, func() { pw.Close() } +} + +// MakeStdoutPipe creates a reader for the command's stdout. +// The returned closer function must be called by the caller to close the pipe. +// After the pipe reader is closed, the unread data will be discarded. +// +// If the process (git command) still tries to write after the pipe is closed, the Wait error will be "signal: broken pipe". +// WithPipelineFunc + Run won't return "broken pipe" error in this case if the callback returns no error. +// But if you are calling Start / Wait family functions, you should either drain the pipe before close it, or handle the Wait error correctly. +func (c *Command) MakeStdoutPipe() (reader PipeReader, closer func()) { + return c.makeStdoutStderr(&c.cmdStdout) +} + +// MakeStderrPipe is like MakeStdoutPipe, but for stderr. +func (c *Command) MakeStderrPipe() (reader PipeReader, closer func()) { + return c.makeStdoutStderr(&c.cmdStderr) +} + +func (c *Command) MakeStdinStdoutPipe() (stdin PipeWriter, stdout PipeReader, closer func()) { + stdin, stdinClose := c.MakeStdinPipe() + stdout, stdoutClose := c.MakeStdoutPipe() + return stdin, stdout, func() { + stdinClose() + stdoutClose() + } +} + +func (c *Command) WithStdinBytes(stdin []byte) *Command { + c.cmdStdin = bytes.NewReader(stdin) + return c +} + +func (c *Command) WithStdoutBuffer(w PipeBufferWriter) *Command { + c.cmdStdout = w + return c +} + +// WithStdinCopy and WithStdoutCopy are general functions that accept any io.Reader / io.Writer. +// In this case, Golang exec.Cmd will start new internal goroutines to do io.Copy between pipes and provided Reader/Writer. +// If the reader or writer is blocked and never returns, then the io.Copy won't finish, then exec.Cmd.Wait won't return, which may cause deadlocks. +// A typical deadlock example is: +// * `r,w:=io.Pipe(); cmd.Stdin=r; defer w.Close(); cmd.Run()`: the Run() will never return because stdin reader is blocked forever and w.Close() will never be called. +// If the reader/writer won't block forever (for example: read from a file or buffer), then these functions are safe to use. +func (c *Command) WithStdinCopy(w io.Reader) *Command { + c.cmdStdin = w + return c +} + +func (c *Command) WithStdoutCopy(w io.Writer) *Command { + c.cmdStdout = w + return c +} + +// WithPipelineFunc sets the pipeline function for the command. +// The pipeline function will be called in the Run / Wait function after the command is started successfully. +// The function can read/write from/to the command's stdio pipes (if any). +// The pipeline function can cancel (kill) the command by calling ctx.CancelPipeline before the command finishes. +// The returned error of Run / Wait can be joined errors from the pipeline function, context cause, and command exit error. +// Caller can get the pipeline function's error (if any) by UnwrapPipelineError. +func (c *Command) WithPipelineFunc(f func(ctx Context) error) *Command { + c.opts.PipelineFunc = f + return c +} + +// WithParentCallerInfo can be used to set the caller info (usually function name) of the parent function of the caller. +// For most cases, "Run" family functions can get its caller info automatically +// But if you need to call "Run" family functions in a wrapper function: "FeatureFunc -> GeneralWrapperFunc -> RunXxx", +// then you can to call this function in GeneralWrapperFunc to set the caller info of FeatureFunc. +// The caller info can only be set once. +func (c *Command) WithParentCallerInfo(optInfo ...string) *Command { + if c.callerInfo != "" { + return c + } + if len(optInfo) > 0 { + c.callerInfo = optInfo[0] + return c + } + skip := 1 /*parent "wrap/run" functions*/ + 1 /*this function*/ + callerFuncName := util.CallerFuncName(skip) + callerInfo := callerFuncName if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 { callerInfo = callerInfo[pos+1:] } + c.callerInfo = callerInfo + return c +} + +func (c *Command) Start(ctx context.Context) (retErr error) { + if c.cmd != nil { + // this is a programming error, it will cause serious deadlock problems, so it must be fixed. + panic("git command has already been started") + } + + defer func() { + c.closePipeFiles(c.childrenPipeFiles) + if retErr != nil { + // release the pipes to avoid resource leak since the command failed to start + c.closePipeFiles(c.parentPipeFiles) + // if error occurs, we must also finish the task, otherwise, cmdFinished will be called in "Wait" function + if c.cmdFinished != nil { + c.cmdFinished() + } + } + }() + + if len(c.preErrors) != 0 { + // In most cases, such error shouldn't happen. If it happens, log it as error level with more details + err := errors.Join(c.preErrors...) + log.Error("git command: %s, error: %s", c.LogString(), err) + return err + } + + cmdLogString := c.LogString() + if c.callerInfo == "" { + c.WithParentCallerInfo() + } // these logs are for debugging purposes only, so no guarantee of correctness or stability - desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), cmdLogString) + desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", c.callerInfo, logArgSanitize(c.opts.Dir), cmdLogString) log.Debug("git.Command: %s", desc) _, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanGitRun) defer span.End() - span.SetAttributeString(gtprof.TraceAttrFuncCaller, callerInfo) + span.SetAttributeString(gtprof.TraceAttrFuncCaller, c.callerInfo) span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString) - var cancel context.CancelFunc - var finished context.CancelFunc - - if opts.UseContextTimeout { - ctx, cancel, finished = process.GetManager().AddContext(ctx, desc) + if c.opts.Timeout <= 0 { + c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContext(ctx, desc) } else { - ctx, cancel, finished = process.GetManager().AddContextTimeout(ctx, timeout, desc) + c.cmdCtx, c.cmdCancel, c.cmdFinished = process.GetManager().AddContextTimeout(ctx, c.opts.Timeout, desc) } - defer finished() - startTime := time.Now() + c.cmdStartTime = time.Now() - cmd := exec.CommandContext(ctx, c.prog, append(c.configArgs, c.args...)...) - c.cmd = cmd // for debug purpose only - if opts.Env == nil { - cmd.Env = os.Environ() + c.cmd = exec.CommandContext(c.cmdCtx, c.prog, append(c.configArgs, c.args...)...) + if c.opts.Env == nil { + c.cmd.Env = os.Environ() } else { - cmd.Env = opts.Env + c.cmd.Env = c.opts.Env } - process.SetSysProcAttribute(cmd) - cmd.Env = append(cmd.Env, CommonGitCmdEnvs()...) - cmd.Dir = opts.Dir - cmd.Stdout = opts.Stdout - cmd.Stderr = opts.Stderr - cmd.Stdin = opts.Stdin - if err := cmd.Start(); err != nil { - return err - } + process.SetSysProcAttribute(c.cmd) + c.cmd.Env = append(c.cmd.Env, CommonGitCmdEnvs()...) + c.cmd.Dir = c.opts.Dir + c.cmd.Stdout = c.cmdStdout + c.cmd.Stdin = c.cmdStdin + c.cmd.Stderr = c.cmdStderr + return c.cmd.Start() +} - if opts.PipelineFunc != nil { - err := opts.PipelineFunc(ctx, cancel) - if err != nil { - cancel() - _ = cmd.Wait() - return err +func (c *Command) closePipeFiles(files []*os.File) { + for _, f := range files { + _ = f.Close() + } +} + +func (c *Command) discardPipeReaders(files []*os.File) { + for _, f := range files { + _, _ = io.Copy(io.Discard, f) + } +} + +func (c *Command) Wait() error { + defer func() { + // The reader in another goroutine might be still reading the stdout, so we shouldn't close the pipes here + // MakeStdoutPipe returns a closer function to force callers to close the pipe correctly + // Here we only need to mark the command as finished + c.cmdFinished() + }() + + if c.opts.PipelineFunc != nil { + errPipeline := c.opts.PipelineFunc(&cmdContext{Context: c.cmdCtx, cmd: c}) + + if context.Cause(c.cmdCtx) == nil { + // if the context is not canceled explicitly, we need to discard the unread data, + // and wait for the command to exit normally, and then get its exit code + c.discardPipeReaders(c.parentPipeReaders) + } // else: canceled command will be killed, and the exit code is caused by kill + + // after the pipeline function returns, we can safely close the pipes, then wait for the command to exit + c.closePipeFiles(c.parentPipeFiles) + errWait := c.cmd.Wait() + errCause := context.Cause(c.cmdCtx) // in case the cause is set during Wait(), get the final cancel cause + + if unwrapped, ok := UnwrapPipelineError(errCause); ok { + if unwrapped != errPipeline { + panic("unwrapped context pipeline error should be the same one returned by pipeline function") + } + if unwrapped == nil { + // the pipeline function declares that there is no error, and it cancels (kills) the command ahead, + // so we should ignore the errors from "wait" and "cause" + errWait, errCause = nil, nil + } } + + // some legacy code still need to access the error returned by pipeline function by "==" but not "errors.Is" + // so we need to make sure the original error is able to be unwrapped by UnwrapPipelineError + return errors.Join(wrapPipelineError(errPipeline), errCause, errWait) } - err := cmd.Wait() - elapsed := time.Since(startTime) + // there might be other goroutines using the context or pipes, so we just wait for the command to finish + errWait := c.cmd.Wait() + elapsed := time.Since(c.cmdStartTime) if elapsed > time.Second { - log.Debug("slow git.Command.Run: %s (%s)", c, elapsed) + log.Debug("slow git.Command.Run: %s (%s)", c, elapsed) // TODO: no need to log this for long-running commands } - // We need to check if the context is canceled by the program on Windows. - // This is because Windows does not have signal checking when terminating the process. - // It always returns exit code 1, unlike Linux, which has many exit codes for signals. - // `err.Error()` returns "exit status 1" when using the `git check-attr` command after the context is canceled. - if runtime.GOOS == "windows" && - err != nil && - (err.Error() == "" || err.Error() == "exit status 1") && - cmd.ProcessState.ExitCode() == 1 && - ctx.Err() == context.Canceled { - return ctx.Err() - } + // Here the logic is different from "PipelineFunc" case, + // because PipelineFunc can return error if it fails, it knows whether it succeeds or fails. + // But in normal case, the caller just runs the git command, the command's exit code is the source of truth. + // If the caller need to know whether the command error is caused by cancellation, it should check the "err" by itself. + errCause := context.Cause(c.cmdCtx) + return errors.Join(errCause, errWait) +} - if err != nil && ctx.Err() != context.DeadlineExceeded { +func (c *Command) StartWithStderr(ctx context.Context) RunStdError { + if c.cmdStderr != nil { + panic("caller-provided stderr receiver doesn't work with managed stderr buffer") + } + c.cmdManagedStderr = &bytes.Buffer{} + c.cmdStderr = c.cmdManagedStderr + err := c.Start(ctx) + if err != nil { + return &runStdError{err: err} + } + return nil +} + +func (c *Command) WaitWithStderr() RunStdError { + if c.cmdManagedStderr == nil { + panic("managed stderr buffer is not initialized") + } + errWait := c.Wait() + if errWait == nil { + // if no exec error but only stderr output, the stderr output is still saved in "c.cmdManagedStderr" and can be read later + return nil + } + return &runStdError{err: errWait, stderr: util.UnsafeBytesToString(c.cmdManagedStderr.Bytes())} +} + +func (c *Command) RunWithStderr(ctx context.Context) RunStdError { + if err := c.StartWithStderr(ctx); err != nil { + return &runStdError{err: err} + } + return c.WaitWithStderr() +} + +func (c *Command) Run(ctx context.Context) (err error) { + if err = c.Start(ctx); err != nil { return err } - - return ctx.Err() + return c.Wait() } -type RunStdError interface { - error - Unwrap() error - Stderr() string +// RunStdString runs the command and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr). +func (c *Command) RunStdString(ctx context.Context) (stdout, stderr string, runErr RunStdError) { + stdoutBytes, stderrBytes, runErr := c.WithParentCallerInfo().runStdBytes(ctx) + return util.UnsafeBytesToString(stdoutBytes), util.UnsafeBytesToString(stderrBytes), runErr } -type runStdError struct { - err error - stderr string - errMsg string +// RunStdBytes runs the command and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr). +func (c *Command) RunStdBytes(ctx context.Context) (stdout, stderr []byte, runErr RunStdError) { + return c.WithParentCallerInfo().runStdBytes(ctx) } -func (r *runStdError) Error() string { - // the stderr must be in the returned error text, some code only checks `strings.Contains(err.Error(), "git error")` - if r.errMsg == "" { - r.errMsg = ConcatenateError(r.err, r.stderr).Error() - } - return r.errMsg -} - -func (r *runStdError) Unwrap() error { - return r.err -} - -func (r *runStdError) Stderr() string { - return r.stderr -} - -func IsErrorExitCode(err error, code int) bool { - var exitError *exec.ExitError - if errors.As(err, &exitError) { - return exitError.ExitCode() == code - } - return false -} - -// RunStdString runs the command with options and returns stdout/stderr as string. and store stderr to returned error (err combined with stderr). -func (c *Command) RunStdString(ctx context.Context, opts *RunOpts) (stdout, stderr string, runErr RunStdError) { - stdoutBytes, stderrBytes, err := c.runStdBytes(ctx, opts) - stdout = util.UnsafeBytesToString(stdoutBytes) - stderr = util.UnsafeBytesToString(stderrBytes) - if err != nil { - return stdout, stderr, &runStdError{err: err, stderr: stderr} - } - // even if there is no err, there could still be some stderr output, so we just return stdout/stderr as they are - return stdout, stderr, nil -} - -// RunStdBytes runs the command with options and returns stdout/stderr as bytes. and store stderr to returned error (err combined with stderr). -func (c *Command) RunStdBytes(ctx context.Context, opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { - return c.runStdBytes(ctx, opts) -} - -func (c *Command) runStdBytes(ctx context.Context, opts *RunOpts) (stdout, stderr []byte, runErr RunStdError) { - if opts == nil { - opts = &RunOpts{} - } - if opts.Stdout != nil || opts.Stderr != nil { - // we must panic here, otherwise there would be bugs if developers set Stdin/Stderr by mistake, and it would be very difficult to debug +func (c *Command) runStdBytes(ctx context.Context) ([]byte, []byte, RunStdError) { + if c.cmdStdout != nil || c.cmdStderr != nil { + // it must panic here, otherwise there would be bugs if developers set other Stdin/Stderr by mistake, and it would be very difficult to debug panic("stdout and stderr field must be nil when using RunStdBytes") } stdoutBuf := &bytes.Buffer{} - stderrBuf := &bytes.Buffer{} - - // We must not change the provided options as it could break future calls - therefore make a copy. - newOpts := &RunOpts{ - Env: opts.Env, - Timeout: opts.Timeout, - UseContextTimeout: opts.UseContextTimeout, - Dir: opts.Dir, - Stdout: stdoutBuf, - Stderr: stderrBuf, - Stdin: opts.Stdin, - PipelineFunc: opts.PipelineFunc, - } - - err := c.run(ctx, 2, newOpts) - stderr = stderrBuf.Bytes() - if err != nil { - return nil, stderr, &runStdError{err: err, stderr: util.UnsafeBytesToString(stderr)} - } - // even if there is no err, there could still be some stderr output - return stdoutBuf.Bytes(), stderr, nil + err := c.WithParentCallerInfo().WithStdoutBuffer(stdoutBuf).RunWithStderr(ctx) + return stdoutBuf.Bytes(), c.cmdManagedStderr.Bytes(), err +} + +func (c *Command) DebugKill() { + _ = c.cmd.Process.Kill() } diff --git a/package/gitea/source/modules/git/gitcmd/command_race_test.go b/package/gitea/source/modules/git/gitcmd/command_race_test.go deleted file mode 100644 index aee22728..00000000 --- a/package/gitea/source/modules/git/gitcmd/command_race_test.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build race - -package gitcmd - -import ( - "context" - "testing" - "time" -) - -func TestRunWithContextNoTimeout(t *testing.T) { - maxLoops := 10 - - // 'git --version' does not block so it must be finished before the timeout triggered. - cmd := NewCommand("--version") - for i := 0; i < maxLoops; i++ { - if err := cmd.Run(t.Context(), &RunOpts{}); err != nil { - t.Fatal(err) - } - } -} - -func TestRunWithContextTimeout(t *testing.T) { - maxLoops := 10 - - // 'git hash-object --stdin' blocks on stdin so we can have the timeout triggered. - cmd := NewCommand("hash-object", "--stdin") - for i := 0; i < maxLoops; i++ { - if err := cmd.Run(t.Context(), &RunOpts{Timeout: 1 * time.Millisecond}); err != nil { - if err != context.DeadlineExceeded { - t.Fatalf("Testing %d/%d: %v", i, maxLoops, err) - } - } - } -} diff --git a/package/gitea/source/modules/git/gitcmd/command_test.go b/package/gitea/source/modules/git/gitcmd/command_test.go index 544a97f6..1af98284 100644 --- a/package/gitea/source/modules/git/gitcmd/command_test.go +++ b/package/gitea/source/modules/git/gitcmd/command_test.go @@ -4,57 +4,93 @@ package gitcmd import ( + "context" "fmt" "os" + "strings" "testing" + "time" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/tempdir" + "code.gitea.io/gitea/modules/testlogger" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestMain(m *testing.M) { +func testMain(m *testing.M) int { + // FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right. + // "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unable to create temp dir: %v", err) - os.Exit(1) + testlogger.Panicf("failed to create temp dir: %v", err) } defer cleanup() setting.Git.HomePath = gitHomePath + return m.Run() +} + +func TestMain(m *testing.M) { + os.Exit(testMain(m)) } func TestRunWithContextStd(t *testing.T) { - cmd := NewCommand("--version") - stdout, stderr, err := cmd.RunStdString(t.Context(), &RunOpts{}) - assert.NoError(t, err) - assert.Empty(t, stderr) - assert.Contains(t, stdout, "git version") - - cmd = NewCommand("--no-such-arg") - stdout, stderr, err = cmd.RunStdString(t.Context(), &RunOpts{}) - if assert.Error(t, err) { - assert.Equal(t, stderr, err.Stderr()) - assert.Contains(t, err.Stderr(), "unknown option:") - assert.Contains(t, err.Error(), "exit status 129 - unknown option:") - assert.Empty(t, stdout) + { + cmd := NewCommand("--version") + stdout, stderr, err := cmd.RunStdString(t.Context()) + assert.NoError(t, err) + assert.Empty(t, stderr) + assert.Contains(t, stdout, "git version") } - cmd = NewCommand() - cmd.AddDynamicArguments("-test") - assert.ErrorIs(t, cmd.Run(t.Context(), &RunOpts{}), ErrBrokenCommand) + { + cmd := NewCommand("ls-tree", "no-such") + stdout, stderr, err := cmd.RunStdString(t.Context()) + if assert.Error(t, err) { + assert.Equal(t, stderr, err.Stderr()) + stderrLower := strings.ToLower(stderr) // see: IsStdErrorNotValidObjectName + assert.Equal(t, "fatal: not a valid object name no-such\n", stderrLower) + // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message + errLower := strings.ToLower(err.Error()) + assert.Equal(t, "exit status 128 - fatal: not a valid object name no-such", errLower) + assert.Empty(t, stdout) + } + } - cmd = NewCommand() - cmd.AddDynamicArguments("--test") - assert.ErrorIs(t, cmd.Run(t.Context(), &RunOpts{}), ErrBrokenCommand) + { + cmd := NewCommand("ls-tree", "no-such") + stdout, stderr, err := cmd.RunStdBytes(t.Context()) + if assert.Error(t, err) { + assert.Equal(t, string(stderr), err.Stderr()) + stderrLower := strings.ToLower(err.Stderr()) // see: IsStdErrorNotValidObjectName + assert.Equal(t, "fatal: not a valid object name no-such\n", stderrLower) + // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message + errLower := strings.ToLower(err.Error()) + assert.Equal(t, "exit status 128 - fatal: not a valid object name no-such", errLower) + assert.Empty(t, stdout) + } + } - subCmd := "version" - cmd = NewCommand().AddDynamicArguments(subCmd) // for test purpose only, the sub-command should never be dynamic for production - stdout, stderr, err = cmd.RunStdString(t.Context(), &RunOpts{}) - assert.NoError(t, err) - assert.Empty(t, stderr) - assert.Contains(t, stdout, "git version") + { + cmd := NewCommand() + cmd.AddDynamicArguments("-test") + assert.ErrorIs(t, cmd.Run(t.Context()), ErrBrokenCommand) + + cmd = NewCommand() + cmd.AddDynamicArguments("--test") + assert.ErrorIs(t, cmd.Run(t.Context()), ErrBrokenCommand) + } + + { + subCmd := "version" + cmd := NewCommand().AddDynamicArguments(subCmd) // for test purpose only, the sub-command should never be dynamic for production + stdout, stderr, err := cmd.RunStdString(t.Context()) + assert.NoError(t, err) + assert.Empty(t, stderr) + assert.Contains(t, stdout, "git version") + } } func TestGitArgument(t *testing.T) { @@ -73,5 +109,34 @@ func TestCommandString(t *testing.T) { assert.Equal(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString()) cmd = NewCommand("url: https://a:b@c/", "/root/dir-a/dir-b") - assert.Equal(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString()) + assert.Equal(t, cmd.prog+` "url: https://(masked)@c/" .../dir-a/dir-b`, cmd.LogString()) + + cmd = NewCommand("url: a:b@c/", "/root/dir-a/dir-b") + assert.Equal(t, cmd.prog+` "url: (masked)@c/" .../dir-a/dir-b`, cmd.LogString()) +} + +func TestRunStdError(t *testing.T) { + e := &runStdError{stderr: "some error"} + var err RunStdError = e + + var asErr RunStdError + require.ErrorAs(t, err, &asErr) + require.Equal(t, "some error", asErr.Stderr()) + + require.ErrorAs(t, fmt.Errorf("wrapped %w", err), &asErr) +} + +func TestRunWithContextTimeout(t *testing.T) { + t.Run("NoTimeout", func(t *testing.T) { + // 'git --version' does not block so it must be finished before the timeout triggered. + err := NewCommand("--version").Run(t.Context()) + require.NoError(t, err) + }) + t.Run("WithTimeout", func(t *testing.T) { + cmd := NewCommand("hash-object", "--stdin") + _, _, pipeClose := cmd.MakeStdinStdoutPipe() + defer pipeClose() + err := cmd.WithTimeout(1 * time.Millisecond).Run(t.Context()) + require.ErrorIs(t, err, context.DeadlineExceeded) + }) } diff --git a/package/gitea/source/modules/git/gitcmd/context.go b/package/gitea/source/modules/git/gitcmd/context.go new file mode 100644 index 00000000..a32f92ff --- /dev/null +++ b/package/gitea/source/modules/git/gitcmd/context.go @@ -0,0 +1,32 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitcmd + +import ( + "context" +) + +type Context interface { + context.Context + + // CancelPipeline is a helper function to cancel the command context (kill the command) with a specific error cause, + // it returns the same error for convenience to break the PipelineFunc easily + CancelPipeline(err error) error + + // In the future, this interface will be extended to support stdio pipe readers/writers +} + +type cmdContext struct { + context.Context + cmd *Command +} + +func (c *cmdContext) CancelPipeline(err error) error { + // pipelineError is used to distinguish between: + // * context canceled by pipeline caller with/without error (normal cancellation) + // * context canceled by parent context (still context.Canceled error) + // * other causes + c.cmd.cmdCancel(pipelineError{err}) + return err +} diff --git a/package/gitea/source/modules/git/gitcmd/error.go b/package/gitea/source/modules/git/gitcmd/error.go new file mode 100644 index 00000000..b674068c --- /dev/null +++ b/package/gitea/source/modules/git/gitcmd/error.go @@ -0,0 +1,108 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitcmd + +import ( + "context" + "errors" + "fmt" + "os/exec" + "strings" +) + +type RunStdError interface { + error + Unwrap() error + Stderr() string +} + +type runStdError struct { + err error // usually the low-level error like `*exec.ExitError` + stderr string // git command's stderr output + errMsg string // the cached error message for Error() method +} + +func (r *runStdError) Error() string { + // FIXME: GIT-CMD-STDERR: it is a bad design, the stderr should not be put in the error message + // But a lot of code only checks `strings.Contains(err.Error(), "git error")` + if r.errMsg == "" { + r.errMsg = fmt.Sprintf("%s - %s", r.err.Error(), strings.TrimSpace(r.stderr)) + } + return r.errMsg +} + +func (r *runStdError) Unwrap() error { + return r.err +} + +func (r *runStdError) Stderr() string { + return r.stderr +} + +func ErrorAsStderr(err error) (string, bool) { + var runErr RunStdError + if errors.As(err, &runErr) { + return runErr.Stderr(), true + } + return "", false +} + +func StderrHasPrefix(err error, prefix string) bool { + stderr, ok := ErrorAsStderr(err) + if !ok { + return false + } + return strings.HasPrefix(stderr, prefix) +} + +func IsErrorExitCode(err error, code int) bool { + var exitError *exec.ExitError + if errors.As(err, &exitError) { + return exitError.ExitCode() == code + } + return false +} + +func IsErrorSignalKilled(err error) bool { + var exitError *exec.ExitError + return errors.As(err, &exitError) && exitError.String() == "signal: killed" +} + +func IsErrorCanceledOrKilled(err error) bool { + // When "cancel()" a git command's context, the returned error of "Run()" could be one of them: + // - context.Canceled + // - *exec.ExitError: "signal: killed" + // TODO: in the future, we need to use unified error type from gitcmd.Run to check whether it is manually canceled + return errors.Is(err, context.Canceled) || IsErrorSignalKilled(err) +} + +func IsStdErrorNotValidObjectName(err error) bool { + stderr, ok := ErrorAsStderr(err) + // Git is lowercasing the "fatal: Not a valid object name" error message + // ref: https://lore.kernel.org/git/pull.2052.git.1771836302101.gitgitgadget@gmail.com + return ok && strings.Contains(strings.ToLower(stderr), "fatal: not a valid object name") +} + +type pipelineError struct { + error +} + +func (e pipelineError) Unwrap() error { + return e.error +} + +func wrapPipelineError(err error) error { + if err == nil { + return nil + } + return pipelineError{err} +} + +func UnwrapPipelineError(err error) (error, bool) { //nolint:revive // this is for error unwrapping + var pe pipelineError + if errors.As(err, &pe) { + return pe.error, true + } + return nil, false +} diff --git a/package/gitea/source/modules/git/gitcmd/pipe.go b/package/gitea/source/modules/git/gitcmd/pipe.go new file mode 100644 index 00000000..d0ce3e2d --- /dev/null +++ b/package/gitea/source/modules/git/gitcmd/pipe.go @@ -0,0 +1,87 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitcmd + +import ( + "io" + "os" +) + +type PipeBufferReader interface { + // Read should be used in the same goroutine as command's Wait + // When Reader in one goroutine, command's Wait in another goroutine, then the command exits, the pipe will be closed: + // * If the Reader goroutine reads faster, it will read all remaining data and then get io.EOF + // * But this io.EOF doesn't mean the Reader has gotten complete data, the data might still be corrupted + // * If the Reader goroutine reads slower, it will get os.ErrClosed because the os.Pipe is closed ahead when the command exits + // + // When using 2 goroutines, no clear solution to distinguish these two cases or make Reader knows whether the data is complete + // It should avoid using Reader in a different goroutine than the command if the Read error needs to be handled. + Read(p []byte) (n int, err error) + Bytes() []byte +} + +type PipeBufferWriter interface { + Write(p []byte) (n int, err error) + Bytes() []byte +} + +type PipeReader interface { + io.ReadCloser + internalOnly() +} + +type pipeReader struct { + f *os.File +} + +func (r *pipeReader) internalOnly() {} + +func (r *pipeReader) Read(p []byte) (n int, err error) { + return r.f.Read(p) +} + +func (r *pipeReader) Close() error { + return r.f.Close() +} + +type PipeWriter interface { + io.WriteCloser + internalOnly() +} + +type pipeWriter struct { + f *os.File +} + +func (w *pipeWriter) internalOnly() {} + +func (w *pipeWriter) Close() error { + return w.f.Close() +} + +func (w *pipeWriter) Write(p []byte) (n int, err error) { + return w.f.Write(p) +} + +func (w *pipeWriter) DrainBeforeClose() error { + return nil +} + +type pipeNull struct { + err error +} + +func (p *pipeNull) internalOnly() {} + +func (p *pipeNull) Read([]byte) (n int, err error) { + return 0, p.err +} + +func (p *pipeNull) Write([]byte) (n int, err error) { + return 0, p.err +} + +func (p *pipeNull) Close() error { + return nil +} diff --git a/package/gitea/source/modules/git/gitcmd/utils.go b/package/gitea/source/modules/git/gitcmd/utils.go deleted file mode 100644 index ee24eb6a..00000000 --- a/package/gitea/source/modules/git/gitcmd/utils.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package gitcmd - -import "fmt" - -// ConcatenateError concatenats an error with stderr string -func ConcatenateError(err error, stderr string) error { - if len(stderr) == 0 { - return err - } - return fmt.Errorf("%w - %s", err, stderr) -} diff --git a/package/gitea/source/modules/git/gpg.go b/package/gitea/source/modules/git/gpg.go new file mode 100644 index 00000000..dbc55693 --- /dev/null +++ b/package/gitea/source/modules/git/gpg.go @@ -0,0 +1,102 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/process" +) + +// GPGSettings represents the default GPG settings for this repository +type GPGSettings struct { + Sign bool + KeyID string + Email string + Name string + PublicKeyContent string + Format string +} + +// LoadPublicKeyContent will load the key from gpg +func (gpgSettings *GPGSettings) LoadPublicKeyContent() error { + if gpgSettings.PublicKeyContent != "" { + return nil + } + + if gpgSettings.Format == SigningKeyFormatSSH { + content, err := os.ReadFile(gpgSettings.KeyID) + if err != nil { + return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err) + } + gpgSettings.PublicKeyContent = string(content) + return nil + } + content, stderr, err := process.GetManager().Exec( + "gpg -a --export", + "gpg", "-a", "--export", gpgSettings.KeyID) + if err != nil { + return fmt.Errorf("unable to get default signing key: %s, %s, %w", gpgSettings.KeyID, stderr, err) + } + gpgSettings.PublicKeyContent = content + return nil +} + +var ( + loadPublicGPGKeyMutex sync.RWMutex + globalGPGSettings *GPGSettings +) + +// GetDefaultPublicGPGKey will return and cache the default public GPG settings +func GetDefaultPublicGPGKey(ctx context.Context, forceUpdate bool) (*GPGSettings, error) { + if !forceUpdate { + loadPublicGPGKeyMutex.RLock() + if globalGPGSettings != nil { + defer loadPublicGPGKeyMutex.RUnlock() + return globalGPGSettings, nil + } + loadPublicGPGKeyMutex.RUnlock() + } + + loadPublicGPGKeyMutex.Lock() + defer loadPublicGPGKeyMutex.Unlock() + + if globalGPGSettings != nil && !forceUpdate { + return globalGPGSettings, nil + } + + globalGPGSettings = &GPGSettings{ + Sign: true, + } + + value, _, _ := gitcmd.NewCommand("config", "--global", "--get", "commit.gpgsign").RunStdString(ctx) + sign, valid := ParseBool(strings.TrimSpace(value)) + if !sign || !valid { + globalGPGSettings.Sign = false + return globalGPGSettings, nil + } + + signingKey, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.signingkey").RunStdString(ctx) + globalGPGSettings.KeyID = strings.TrimSpace(signingKey) + + format, _, _ := gitcmd.NewCommand("config", "--global", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(ctx) + globalGPGSettings.Format = strings.TrimSpace(format) + + defaultEmail, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.email").RunStdString(ctx) + globalGPGSettings.Email = strings.TrimSpace(defaultEmail) + + defaultName, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.name").RunStdString(ctx) + globalGPGSettings.Name = strings.TrimSpace(defaultName) + + if err := globalGPGSettings.LoadPublicKeyContent(); err != nil { + return nil, err + } + return globalGPGSettings, nil +} diff --git a/package/gitea/source/modules/git/grep.go b/package/gitea/source/modules/git/grep.go index f5f6f120..47f66b88 100644 --- a/package/gitea/source/modules/git/grep.go +++ b/package/gitea/source/modules/git/grep.go @@ -5,14 +5,13 @@ package git import ( "bufio" - "bytes" "context" "errors" "fmt" - "os" "slices" "strconv" "strings" + "time" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/util" @@ -41,16 +40,11 @@ type GrepOptions struct { PathspecList []string } -func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { - stdoutReader, stdoutWriter, err := os.Pipe() - if err != nil { - return nil, fmt.Errorf("unable to create os pipe to grep: %w", err) - } - defer func() { - _ = stdoutReader.Close() - _ = stdoutWriter.Close() - }() +// grepSearchTimeout is the timeout for git grep search, it should be long enough to get results +// but not too long to cause performance issues +const grepSearchTimeout = 30 * time.Second +func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepOptions) ([]*GrepResult, error) { /* The output is like this ( "^@" means \x00): @@ -83,15 +77,12 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO cmd.AddDynamicArguments(util.IfZero(opts.RefName, "HEAD")) cmd.AddDashesAndList(opts.PathspecList...) opts.MaxResultLimit = util.IfZero(opts.MaxResultLimit, 50) - stderr := bytes.Buffer{} - err = cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: stdoutWriter, - Stderr: &stderr, - PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { - _ = stdoutWriter.Close() - defer stdoutReader.Close() + stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() + defer stdoutReaderClose() + err := cmd.WithDir(repo.Path). + WithTimeout(grepSearchTimeout). + WithPipelineFunc(func(ctx gitcmd.Context) error { isInBlock := false rd := bufio.NewReaderSize(stdoutReader, util.IfZero(opts.MaxLineLength, 16*1024)) var res *GrepResult @@ -117,8 +108,7 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO } if line == "" { if len(results) >= opts.MaxResultLimit { - cancel() - break + return ctx.CancelPipeline(nil) } isInBlock = false continue @@ -133,18 +123,18 @@ func GrepSearch(ctx context.Context, repo *Repository, search string, opts GrepO } } return nil - }, - }) + }). + RunWithStderr(ctx) // git grep exits by cancel (killed), usually it is caused by the limit of results - if gitcmd.IsErrorExitCode(err, -1) && stderr.Len() == 0 { + if gitcmd.IsErrorExitCode(err, -1) && err.Stderr() == "" { return results, nil } // git grep exits with 1 if no results are found - if gitcmd.IsErrorExitCode(err, 1) && stderr.Len() == 0 { + if gitcmd.IsErrorExitCode(err, 1) && err.Stderr() == "" { return nil, nil } if err != nil && !errors.Is(err, context.Canceled) { - return nil, fmt.Errorf("unable to run git grep: %w, stderr: %s", err, stderr.String()) + return nil, fmt.Errorf("unable to run git grep: %w", err) } return results, nil } diff --git a/package/gitea/source/modules/git/hook.go b/package/gitea/source/modules/git/hook.go index 9a2dfae2..0e19387d 100644 --- a/package/gitea/source/modules/git/hook.go +++ b/package/gitea/source/modules/git/hook.go @@ -45,7 +45,7 @@ func GetHook(repoPath, name string) (*Hook, error) { } h := &Hook{ name: name, - path: filepath.Join(repoPath, "hooks", name+".d", name), + path: filepath.Join(repoPath, filepath.Join("hooks", name+".d", name)), } if data, err := os.ReadFile(h.path); err == nil { h.IsActive = true diff --git a/package/gitea/source/modules/git/key.go b/package/gitea/source/modules/git/key.go index 8c14742f..9d517045 100644 --- a/package/gitea/source/modules/git/key.go +++ b/package/gitea/source/modules/git/key.go @@ -3,7 +3,13 @@ package git -import "code.gitea.io/gitea/modules/setting" +import ( + "context" + "strings" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" +) // Based on https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat const ( @@ -24,3 +30,48 @@ func (s *SigningKey) String() string { setting.PanicInDevOrTesting("don't call SigningKey.String() - it exposes the KeyID which might be a local file path") return "SigningKey:" + s.Format } + +// GetSigningKey returns the KeyID and git Signature for the repo +func GetSigningKey(ctx context.Context) (*SigningKey, *Signature) { + if setting.Repository.Signing.SigningKey == "none" { + return nil, nil + } + + if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { + // Can ignore the error here as it means that commit.gpgsign is not set + value, _, _ := gitcmd.NewCommand("config", "--global", "--get", "commit.gpgsign").RunStdString(ctx) + sign, valid := ParseBool(strings.TrimSpace(value)) + if !sign || !valid { + return nil, nil + } + + format, _, _ := gitcmd.NewCommand("config", "--global", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(ctx) + signingKey, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.signingkey").RunStdString(ctx) + signingName, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.name").RunStdString(ctx) + signingEmail, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.email").RunStdString(ctx) + + if strings.TrimSpace(signingKey) == "" { + return nil, nil + } + + return &SigningKey{ + KeyID: strings.TrimSpace(signingKey), + Format: strings.TrimSpace(format), + }, &Signature{ + Name: strings.TrimSpace(signingName), + Email: strings.TrimSpace(signingEmail), + } + } + + if setting.Repository.Signing.SigningKey == "" { + return nil, nil + } + + return &SigningKey{ + KeyID: setting.Repository.Signing.SigningKey, + Format: setting.Repository.Signing.SigningFormat, + }, &Signature{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } +} diff --git a/package/gitea/source/modules/git/languagestats/language_stats_gogit.go b/package/gitea/source/modules/git/languagestats/language_stats_gogit.go index 418c05b1..ec03ca31 100644 --- a/package/gitea/source/modules/git/languagestats/language_stats_gogit.go +++ b/package/gitea/source/modules/git/languagestats/language_stats_gogit.go @@ -108,7 +108,7 @@ func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string] if (!isVendored.Has() && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) || (!isDocumentation.Has() && enry.IsDocumentation(f.Name)) || - enry.IsConfiguration(f.Name) { + (!isDetectable.Has() && enry.IsConfiguration(f.Name)) { return nil } diff --git a/package/gitea/source/modules/git/languagestats/language_stats_nogogit.go b/package/gitea/source/modules/git/languagestats/language_stats_nogogit.go index 94cf9fff..442313d4 100644 --- a/package/gitea/source/modules/git/languagestats/language_stats_nogogit.go +++ b/package/gitea/source/modules/git/languagestats/language_stats_nogogit.go @@ -22,33 +22,28 @@ import ( func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, error) { // We will feed the commit IDs in order into cat-file --batch, followed by blobs as necessary. // so let's create a batch stdin and stdout - batchStdinWriter, batchReader, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { return nil, err } defer cancel() - writeID := func(id string) error { - _, err := batchStdinWriter.Write([]byte(id + "\n")) - return err - } - - if err := writeID(commitID); err != nil { + commitInfo, batchReader, err := batch.QueryContent(commitID) + if err != nil { return nil, err } - shaBytes, typ, size, err := git.ReadBatchLine(batchReader) - if typ != "commit" { + if commitInfo.Type != "commit" { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, git.ErrNotExist{ID: commitID} } - sha, err := git.NewIDFromString(string(shaBytes)) + sha, err := git.NewIDFromString(commitInfo.ID) if err != nil { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, git.ErrNotExist{ID: commitID} } - commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, size)) + commit, err := git.CommitFromReader(repo, sha, io.LimitReader(batchReader, commitInfo.Size)) if err != nil { log.Debug("Unable to get commit for: %s. Err: %v", commitID, err) return nil, err @@ -137,27 +132,23 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64, if (!isVendored.Has() && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) || (!isDocumentation.Has() && enry.IsDocumentation(f.Name())) || - enry.IsConfiguration(f.Name()) { + (!isDetectable.Has() && enry.IsConfiguration(f.Name())) { continue } // If content can not be read or file is too big just do detection by filename if f.Size() <= bigFileSize { - if err := writeID(f.ID.String()); err != nil { - return nil, err - } - _, _, size, err := git.ReadBatchLine(batchReader) + info, _, err := batch.QueryContent(f.ID.String()) if err != nil { - log.Debug("Error reading blob: %s Err: %v", f.ID.String(), err) return nil, err } - sizeToRead := size + sizeToRead := info.Size discard := int64(1) - if size > fileSizeLimit { + if info.Size > fileSizeLimit { sizeToRead = fileSizeLimit - discard = size - fileSizeLimit + 1 + discard = info.Size - fileSizeLimit + 1 } _, err = contentBuf.ReadFrom(io.LimitReader(batchReader, sizeToRead)) diff --git a/package/gitea/source/modules/git/languagestats/main_test.go b/package/gitea/source/modules/git/languagestats/main_test.go index b8f9ded0..bf860f2a 100644 --- a/package/gitea/source/modules/git/languagestats/main_test.go +++ b/package/gitea/source/modules/git/languagestats/main_test.go @@ -4,37 +4,11 @@ package languagestats import ( - "fmt" - "os" "testing" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) -func testRun(m *testing.M) error { - gitHomePath, err := os.MkdirTemp(os.TempDir(), "git-home") - if err != nil { - return fmt.Errorf("unable to create temp dir: %w", err) - } - defer util.RemoveAll(gitHomePath) - setting.Git.HomePath = gitHomePath - - if err = git.InitFull(); err != nil { - return fmt.Errorf("failed to call Init: %w", err) - } - - exitCode := m.Run() - if exitCode != 0 { - return fmt.Errorf("run test failed, ExitCode=%d", exitCode) - } - return nil -} - func TestMain(m *testing.M) { - if err := testRun(m); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "Test failed: %v", err) - os.Exit(1) - } + git.RunGitTests(m) } diff --git a/package/gitea/source/modules/git/last_commit_cache.go b/package/gitea/source/modules/git/last_commit_cache.go index cff25560..a013773b 100644 --- a/package/gitea/source/modules/git/last_commit_cache.go +++ b/package/gitea/source/modules/git/last_commit_cache.go @@ -55,12 +55,12 @@ func (c *LastCommitCache) Put(ref, entryPath, commitID string) error { // Get gets the last commit information by commit id and entry path func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) { if c == nil || c.cache == nil { - return nil, nil + return nil, nil //nolint:nilnil // return nil when cache is not available } commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath)) if !ok || commitID == "" { - return nil, nil + return nil, nil //nolint:nilnil // return nil when cache miss } log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, commitID) diff --git a/package/gitea/source/modules/git/log_name_status.go b/package/gitea/source/modules/git/log_name_status.go index 7a5192f5..6e6d9985 100644 --- a/package/gitea/source/modules/git/log_name_status.go +++ b/package/gitea/source/modules/git/log_name_status.go @@ -15,25 +15,12 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git/gitcmd" - - "github.com/djherbis/buffer" - "github.com/djherbis/nio/v3" + "code.gitea.io/gitea/modules/log" ) // LogNameStatusRepo opens git log --raw in the provided repo and returns a stdin pipe, a stdout reader and cancel function func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, paths ...string) (*bufio.Reader, func()) { - // We often want to feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary. - // so let's create a batch stdin and stdout - stdoutReader, stdoutWriter := nio.Pipe(buffer.New(32 * 1024)) - // Lets also create a context so that we can absolutely ensure that the command should die when we're done - ctx, ctxCancel := context.WithCancel(ctx) - - cancel := func() { - ctxCancel() - _ = stdoutReader.Close() - _ = stdoutWriter.Close() - } cmd := gitcmd.NewCommand() cmd.AddArguments("log", "--name-status", "-c", "--format=commit%x00%H %P%x00", "--parents", "--no-renames", "-t", "-z").AddDynamicArguments(head) @@ -63,25 +50,21 @@ func LogNameStatusRepo(ctx context.Context, repository, head, treepath string, p } cmd.AddDashesAndList(files...) + stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() + ctx, ctxCancel := context.WithCancel(ctx) go func() { - stderr := strings.Builder{} - err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: repository, - Stdout: stdoutWriter, - Stderr: &stderr, - }) - if err != nil { - _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) - return + err := cmd.WithDir(repository).RunWithStderr(ctx) + if err != nil && !errors.Is(err, context.Canceled) { + log.Error("Unable to run git command %v: %v", cmd.LogString(), err) } - - _ = stdoutWriter.Close() }() - // For simplicities sake we'll us a buffered reader to read from the cat-file --batch bufReader := bufio.NewReaderSize(stdoutReader, 32*1024) - return bufReader, cancel + return bufReader, func() { + ctxCancel() + stdoutReaderClose() + } } // LogNameStatusRepoParser parses a git log raw output from LogRawRepo @@ -123,7 +106,7 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int case bufio.ErrBufferFull: g.buffull = true case io.EOF: - return nil, nil + return nil, nil //nolint:nilnil // return nil to signal EOF default: return nil, err } @@ -138,7 +121,7 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int case bufio.ErrBufferFull: g.buffull = true case io.EOF: - return nil, nil + return nil, nil //nolint:nilnil // return nil to signal EOF default: return nil, err } diff --git a/package/gitea/source/modules/git/notes_gogit.go b/package/gitea/source/modules/git/notes_gogit.go index f802443b..340f4d5c 100644 --- a/package/gitea/source/modules/git/notes_gogit.go +++ b/package/gitea/source/modules/git/notes_gogit.go @@ -7,6 +7,7 @@ package git import ( "context" + "fmt" "io" "code.gitea.io/gitea/modules/log" @@ -30,7 +31,11 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note) remainingCommitID := commitID path := "" - currentTree := notes.Tree.gogitTree + currentTree, err := notes.Tree.gogitTreeObject() + if err != nil { + return fmt.Errorf("unable to get tree object for notes commit %q: %w", notes.ID.String(), err) + } + log.Trace("Found tree with ID %q while searching for git note corresponding to the commit %q", currentTree.Entries[0].Name, commitID) var file *object.File for len(remainingCommitID) > 2 { diff --git a/package/gitea/source/modules/git/notes_test.go b/package/gitea/source/modules/git/notes_test.go index 7db2dbc0..5abb68b1 100644 --- a/package/gitea/source/modules/git/notes_test.go +++ b/package/gitea/source/modules/git/notes_test.go @@ -47,5 +47,5 @@ func TestGetNonExistentNotes(t *testing.T) { note := Note{} err = GetNote(t.Context(), bareRepo1, "non_existent_sha", ¬e) assert.Error(t, err) - assert.IsType(t, ErrNotExist{}, err) + assert.ErrorAs(t, err, &ErrNotExist{}) } diff --git a/package/gitea/source/modules/git/object_id_test.go b/package/gitea/source/modules/git/object_id_test.go index 03d0c85d..213a0cd3 100644 --- a/package/gitea/source/modules/git/object_id_test.go +++ b/package/gitea/source/modules/git/object_id_test.go @@ -22,4 +22,6 @@ func TestIsValidSHAPattern(t *testing.T) { assert.Equal(t, "2e65efe2a145dda7ee51d1741299f848e5bf752e", ComputeBlobHash(Sha1ObjectFormat, []byte("a")).String()) assert.Equal(t, "473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813", ComputeBlobHash(Sha256ObjectFormat, nil).String()) assert.Equal(t, "eb337bcee2061c5313c9a1392116b6c76039e9e30d71467ae359b36277e17dc7", ComputeBlobHash(Sha256ObjectFormat, []byte("a")).String()) + assert.True(t, IsEmptyCommitID("")) + assert.True(t, IsEmptyCommitID("0000000000000000000000000000000000000000")) } diff --git a/package/gitea/source/modules/git/parse.go b/package/gitea/source/modules/git/parse.go index a7f5c58e..94020e69 100644 --- a/package/gitea/source/modules/git/parse.go +++ b/package/gitea/source/modules/git/parse.go @@ -27,15 +27,15 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) { // \t var err error - 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(LsTreeEntry) - 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 @@ -46,8 +46,8 @@ func parseLsTreeLine(line []byte) (*LsTreeEntry, error) { entry.Size = optional.Some(size) } - entry.EntryMode, err = ParseEntryMode(string(entryMode)) - if err != nil || entry.EntryMode == EntryModeNoEntry { + entry.EntryMode = ParseEntryMode(string(entryMode)) + if entry.EntryMode == EntryModeNoEntry { return nil, fmt.Errorf("invalid ls-tree output (invalid mode): %q, err: %w", line, err) } diff --git a/package/gitea/source/modules/git/parse_gogit.go b/package/gitea/source/modules/git/parse_gogit.go deleted file mode 100644 index 74d258de..00000000 --- a/package/gitea/source/modules/git/parse_gogit.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build gogit - -package git - -import ( - "bytes" - "fmt" - "strconv" - "strings" - - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/hash" - "github.com/go-git/go-git/v5/plumbing/object" -) - -// ParseTreeEntries parses the output of a `git ls-tree -l` command. -func ParseTreeEntries(data []byte) ([]*TreeEntry, error) { - return parseTreeEntries(data, nil) -} - -func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { - entries := make([]*TreeEntry, 0, 10) - for pos := 0; pos < len(data); { - // expect line to be of the form " \t" - entry := new(TreeEntry) - entry.gogitTreeEntry = &object.TreeEntry{} - entry.ptree = ptree - if pos+6 > len(data) { - return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) - } - switch string(data[pos : pos+6]) { - case "100644": - entry.gogitTreeEntry.Mode = filemode.Regular - pos += 12 // skip over "100644 blob " - case "100755": - entry.gogitTreeEntry.Mode = filemode.Executable - pos += 12 // skip over "100755 blob " - case "120000": - entry.gogitTreeEntry.Mode = filemode.Symlink - pos += 12 // skip over "120000 blob " - case "160000": - entry.gogitTreeEntry.Mode = filemode.Submodule - pos += 14 // skip over "160000 object " - case "040000": - entry.gogitTreeEntry.Mode = filemode.Dir - pos += 12 // skip over "040000 tree " - default: - return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6])) - } - - // in hex format, not byte format .... - if pos+hash.Size*2 > len(data) { - return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) - } - var err error - entry.ID, err = NewIDFromString(string(data[pos : pos+hash.Size*2])) - if err != nil { - return nil, fmt.Errorf("invalid ls-tree output: %w", err) - } - entry.gogitTreeEntry.Hash = plumbing.Hash(entry.ID.RawValue()) - pos += 41 // skip over sha and trailing space - - end := pos + bytes.IndexByte(data[pos:], '\t') - if end < pos { - return nil, fmt.Errorf("Invalid ls-tree -l output: %s", string(data)) - } - entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(data[pos:end])), 10, 64) - entry.sized = true - - pos = end + 1 - - end = pos + bytes.IndexByte(data[pos:], '\n') - if end < pos { - return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data)) - } - - // In case entry name is surrounded by double quotes(it happens only in git-shell). - if data[pos] == '"' { - var err error - entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end])) - if err != nil { - return nil, fmt.Errorf("Invalid ls-tree output: %w", err) - } - } else { - entry.gogitTreeEntry.Name = string(data[pos:end]) - } - - pos = end + 1 - entries = append(entries, entry) - } - return entries, nil -} diff --git a/package/gitea/source/modules/git/parse_gogit_test.go b/package/gitea/source/modules/git/parse_gogit_test.go deleted file mode 100644 index 3e171d7e..00000000 --- a/package/gitea/source/modules/git/parse_gogit_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2018 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -//go:build gogit - -package git - -import ( - "fmt" - "testing" - - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/stretchr/testify/assert" -) - -func TestParseTreeEntries(t *testing.T) { - testCases := []struct { - Input string - Expected []*TreeEntry - }{ - { - Input: "", - Expected: []*TreeEntry{}, - }, - { - Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 1022\texample/file2.txt\n", - Expected: []*TreeEntry{ - { - ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), - gogitTreeEntry: &object.TreeEntry{ - Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()), - Name: "example/file2.txt", - Mode: filemode.Regular, - }, - size: 1022, - sized: true, - }, - }, - }, - { - Input: "120000 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 234131\t\"example/\\n.txt\"\n" + - "040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8 -\texample\n", - Expected: []*TreeEntry{ - { - ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"), - gogitTreeEntry: &object.TreeEntry{ - Hash: plumbing.Hash(MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()), - Name: "example/\n.txt", - Mode: filemode.Symlink, - }, - size: 234131, - sized: true, - }, - { - ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"), - sized: true, - gogitTreeEntry: &object.TreeEntry{ - Hash: plumbing.Hash(MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8").RawValue()), - Name: "example", - Mode: filemode.Dir, - }, - }, - }, - }, - } - - for _, testCase := range testCases { - entries, err := ParseTreeEntries([]byte(testCase.Input)) - assert.NoError(t, err) - if len(entries) > 1 { - fmt.Println(testCase.Expected[0].ID) - fmt.Println(entries[0].ID) - } - assert.EqualValues(t, testCase.Expected, entries) - } -} diff --git a/package/gitea/source/modules/git/parse_nogogit.go b/package/gitea/source/modules/git/parse_treeentry.go similarity index 96% rename from package/gitea/source/modules/git/parse_nogogit.go rename to package/gitea/source/modules/git/parse_treeentry.go index 78a01628..d46cd334 100644 --- a/package/gitea/source/modules/git/parse_nogogit.go +++ b/package/gitea/source/modules/git/parse_treeentry.go @@ -1,12 +1,9 @@ // Copyright 2018 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build !gogit - package git import ( - "bufio" "bytes" "fmt" "io" @@ -49,7 +46,7 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) { return entries, nil } -func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.Reader, sz int64) ([]*TreeEntry, error) { +func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd BufferedReader, sz int64) ([]*TreeEntry, error) { fnameBuf := make([]byte, 4096) modeBuf := make([]byte, 40) shaBuf := make([]byte, objectFormat.FullLength()) diff --git a/package/gitea/source/modules/git/parse_nogogit_test.go b/package/gitea/source/modules/git/parse_treeentry_test.go similarity index 99% rename from package/gitea/source/modules/git/parse_nogogit_test.go rename to package/gitea/source/modules/git/parse_treeentry_test.go index 6594c842..4223cbb3 100644 --- a/package/gitea/source/modules/git/parse_nogogit_test.go +++ b/package/gitea/source/modules/git/parse_treeentry_test.go @@ -1,8 +1,6 @@ // Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build !gogit - package git import ( diff --git a/package/gitea/source/modules/git/pipeline/catfile.go b/package/gitea/source/modules/git/pipeline/catfile.go index ced8532e..3d005e28 100644 --- a/package/gitea/source/modules/git/pipeline/catfile.go +++ b/package/gitea/source/modules/git/pipeline/catfile.go @@ -5,83 +5,34 @@ package pipeline import ( "bufio" - "bytes" "context" - "fmt" "io" "strconv" "strings" - "sync" "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/log" ) // CatFileBatchCheck runs cat-file with --batch-check -func CatFileBatchCheck(ctx context.Context, shasToCheckReader *io.PipeReader, catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) { - defer wg.Done() - defer shasToCheckReader.Close() - defer catFileCheckWriter.Close() - - stderr := new(bytes.Buffer) - var errbuf strings.Builder - cmd := gitcmd.NewCommand("cat-file", "--batch-check") - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdin: shasToCheckReader, - Stdout: catFileCheckWriter, - Stderr: stderr, - }); err != nil { - _ = catFileCheckWriter.CloseWithError(fmt.Errorf("git cat-file --batch-check [%s]: %w - %s", tmpBasePath, err, errbuf.String())) - } +func CatFileBatchCheck(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error { + cmd.AddArguments("cat-file", "--batch-check") + return cmd.WithDir(tmpBasePath).RunWithStderr(ctx) } // CatFileBatchCheckAllObjects runs cat-file with --batch-check --batch-all -func CatFileBatchCheckAllObjects(ctx context.Context, catFileCheckWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string, errChan chan<- error) { - defer wg.Done() - defer catFileCheckWriter.Close() - - stderr := new(bytes.Buffer) - var errbuf strings.Builder - cmd := gitcmd.NewCommand("cat-file", "--batch-check", "--batch-all-objects") - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdout: catFileCheckWriter, - Stderr: stderr, - }); err != nil { - log.Error("git cat-file --batch-check --batch-all-object [%s]: %v - %s", tmpBasePath, err, errbuf.String()) - err = fmt.Errorf("git cat-file --batch-check --batch-all-object [%s]: %w - %s", tmpBasePath, err, errbuf.String()) - _ = catFileCheckWriter.CloseWithError(err) - errChan <- err - } +func CatFileBatchCheckAllObjects(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error { + return cmd.AddArguments("cat-file", "--batch-check", "--batch-all-objects").WithDir(tmpBasePath).RunWithStderr(ctx) } // CatFileBatch runs cat-file --batch -func CatFileBatch(ctx context.Context, shasToBatchReader *io.PipeReader, catFileBatchWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) { - defer wg.Done() - defer shasToBatchReader.Close() - defer catFileBatchWriter.Close() - - stderr := new(bytes.Buffer) - var errbuf strings.Builder - if err := gitcmd.NewCommand("cat-file", "--batch").Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdout: catFileBatchWriter, - Stdin: shasToBatchReader, - Stderr: stderr, - }); err != nil { - _ = shasToBatchReader.CloseWithError(fmt.Errorf("git rev-list [%s]: %w - %s", tmpBasePath, err, errbuf.String())) - } +func CatFileBatch(ctx context.Context, cmd *gitcmd.Command, tmpBasePath string) error { + return cmd.AddArguments("cat-file", "--batch").WithDir(tmpBasePath).RunWithStderr(ctx) } // BlobsLessThan1024FromCatFileBatchCheck reads a pipeline from cat-file --batch-check and returns the blobs <1024 in size -func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, shasToBatchWriter *io.PipeWriter, wg *sync.WaitGroup) { - defer wg.Done() - defer catFileCheckReader.Close() - scanner := bufio.NewScanner(catFileCheckReader) - defer func() { - _ = shasToBatchWriter.CloseWithError(scanner.Err()) - }() +func BlobsLessThan1024FromCatFileBatchCheck(in io.ReadCloser, out io.WriteCloser) error { + defer out.Close() + scanner := bufio.NewScanner(in) for scanner.Scan() { line := scanner.Text() if len(line) == 0 { @@ -97,12 +48,12 @@ func BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader *io.PipeReader, s } toWrite := []byte(fields[0] + "\n") for len(toWrite) > 0 { - n, err := shasToBatchWriter.Write(toWrite) + n, err := out.Write(toWrite) if err != nil { - _ = catFileCheckReader.CloseWithError(err) - break + return err } toWrite = toWrite[n:] } } + return scanner.Err() } diff --git a/package/gitea/source/modules/git/pipeline/lfs_common.go b/package/gitea/source/modules/git/pipeline/lfs_common.go index 188e7d4d..914aefbe 100644 --- a/package/gitea/source/modules/git/pipeline/lfs_common.go +++ b/package/gitea/source/modules/git/pipeline/lfs_common.go @@ -4,7 +4,6 @@ package pipeline import ( - "fmt" "time" "code.gitea.io/gitea/modules/git" @@ -26,7 +25,3 @@ type lfsResultSlice []*LFSResult func (a lfsResultSlice) Len() int { return len(a) } func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) } - -func lfsError(msg string, err error) error { - return fmt.Errorf("LFS error occurred, %s: err: %w", msg, err) -} diff --git a/package/gitea/source/modules/git/pipeline/lfs_gogit.go b/package/gitea/source/modules/git/pipeline/lfs_gogit.go index adcf8ed0..c1239756 100644 --- a/package/gitea/source/modules/git/pipeline/lfs_gogit.go +++ b/package/gitea/source/modules/git/pipeline/lfs_gogit.go @@ -6,11 +6,10 @@ package pipeline import ( - "bufio" + "fmt" "io" "sort" "strings" - "sync" "code.gitea.io/gitea/modules/git" @@ -24,7 +23,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err resultsMap := map[string]*LFSResult{} results := make([]*LFSResult, 0) - basePath := repo.Path gogitRepo := repo.GoGitRepo() commitsIter, err := gogitRepo.Log(&gogit.LogOptions{ @@ -32,7 +30,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err All: true, }) if err != nil { - return nil, lfsError("failed to get GoGit CommitsIter", err) + return nil, fmt.Errorf("LFS error occurred, failed to get GoGit CommitsIter: err: %w", err) } err = commitsIter.ForEach(func(gitCommit *object.Commit) error { @@ -66,7 +64,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err return nil }) if err != nil && err != io.EOF { - return nil, lfsError("failure in CommitIter.ForEach", err) + return nil, fmt.Errorf("LFS error occurred, failure in CommitIter.ForEach: %w", err) } for _, result := range resultsMap { @@ -82,65 +80,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err } sort.Sort(lfsResultSlice(results)) - - // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple - shasToNameReader, shasToNameWriter := io.Pipe() - nameRevStdinReader, nameRevStdinWriter := io.Pipe() - errChan := make(chan error, 1) - wg := sync.WaitGroup{} - wg.Add(3) - - go func() { - defer wg.Done() - scanner := bufio.NewScanner(nameRevStdinReader) - i := 0 - for scanner.Scan() { - line := scanner.Text() - if len(line) == 0 { - continue - } - result := results[i] - result.FullCommitName = line - result.BranchName = strings.Split(line, "~")[0] - i++ - } - }() - go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath) - go func() { - defer wg.Done() - defer shasToNameWriter.Close() - for _, result := range results { - i := 0 - if i < len(result.SHA) { - n, err := shasToNameWriter.Write([]byte(result.SHA)[i:]) - if err != nil { - errChan <- err - break - } - i += n - } - n := 0 - for n < 1 { - n, err = shasToNameWriter.Write([]byte{'\n'}) - if err != nil { - errChan <- err - break - } - - } - - } - }() - - wg.Wait() - - select { - case err, has := <-errChan: - if has { - return nil, lfsError("unable to obtain name for LFS files", err) - } - default: - } - - return results, nil + err = fillResultNameRev(repo.Ctx, repo.Path, results) + return results, err } diff --git a/package/gitea/source/modules/git/pipeline/lfs_nogogit.go b/package/gitea/source/modules/git/pipeline/lfs_nogogit.go index d2f14785..1d8fa63c 100644 --- a/package/gitea/source/modules/git/pipeline/lfs_nogogit.go +++ b/package/gitea/source/modules/git/pipeline/lfs_nogogit.go @@ -8,46 +8,33 @@ package pipeline import ( "bufio" "bytes" + "encoding/hex" "io" "sort" - "strings" - "sync" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" ) // FindLFSFile finds commits that contain a provided pointer file hash -func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) { +func FindLFSFile(repo *git.Repository, objectID git.ObjectID) (results []*LFSResult, _ error) { + cmd := gitcmd.NewCommand("rev-list", "--all") + revListReader, revListReaderClose := cmd.MakeStdoutPipe() + defer revListReaderClose() + err := cmd.WithDir(repo.Path). + WithPipelineFunc(func(context gitcmd.Context) (err error) { + results, err = findLFSFileFunc(repo, objectID, revListReader) + return err + }).RunWithStderr(repo.Ctx) + return results, err +} + +func findLFSFileFunc(repo *git.Repository, objectID git.ObjectID, revListReader io.Reader) ([]*LFSResult, error) { resultsMap := map[string]*LFSResult{} results := make([]*LFSResult, 0) - - basePath := repo.Path - - // Use rev-list to provide us with all commits in order - revListReader, revListWriter := io.Pipe() - defer func() { - _ = revListWriter.Close() - _ = revListReader.Close() - }() - - go func() { - stderr := strings.Builder{} - err := gitcmd.NewCommand("rev-list", "--all").Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: revListWriter, - Stderr: &stderr, - }) - if err != nil { - _ = revListWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) - } else { - _ = revListWriter.Close() - } - }() - // Next feed the commits in order into cat-file --batch, followed by their trees and sub trees as necessary. // so let's create a batch stdin and stdout - batchStdinWriter, batchReader, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { return nil, err } @@ -55,7 +42,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err // We'll use a scanner for the revList because it's simpler than a bufio.Reader scan := bufio.NewScanner(revListReader) - trees := [][]byte{} + trees := []string{} paths := []string{} fnameBuf := make([]byte, 4096) @@ -64,14 +51,10 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err for scan.Scan() { // Get the next commit ID - commitID := scan.Bytes() + commitID := scan.Text() // push the commit to the cat-file --batch process - _, err := batchStdinWriter.Write(commitID) - if err != nil { - return nil, err - } - _, err = batchStdinWriter.Write([]byte{'\n'}) + info, batchReader, err := batch.QueryContent(commitID) if err != nil { return nil, err } @@ -81,26 +64,20 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err commitReadingLoop: for { - _, typ, size, err := git.ReadBatchLine(batchReader) - if err != nil { - return nil, err - } - - switch typ { + switch info.Type { case "tag": // This shouldn't happen but if it does well just get the commit and try again - id, err := git.ReadTagObjectID(batchReader, size) + id, err := git.ReadTagObjectID(batchReader, info.Size) if err != nil { return nil, err } - _, err = batchStdinWriter.Write([]byte(id + "\n")) - if err != nil { + if info, batchReader, err = batch.QueryContent(id); err != nil { return nil, err } continue case "commit": // Read in the commit to get its tree and in case this is one of the last used commits - curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(string(commitID)), io.LimitReader(batchReader, size)) + curCommit, err = git.CommitFromReader(repo, git.MustIDFromString(commitID), io.LimitReader(batchReader, info.Size)) if err != nil { return nil, err } @@ -108,13 +85,13 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err return nil, err } - if _, err := batchStdinWriter.Write([]byte(curCommit.Tree.ID.String() + "\n")); err != nil { + if info, _, err = batch.QueryContent(curCommit.Tree.ID.String()); err != nil { return nil, err } curPath = "" case "tree": var n int64 - for n < size { + for n < info.Size { mode, fname, binObjectID, count, err := git.ParseCatFileTreeLine(objectID.Type(), batchReader, modeBuf, fnameBuf, workingShaBuf) if err != nil { return nil, err @@ -124,15 +101,13 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err result := LFSResult{ Name: curPath + string(fname), SHA: curCommit.ID.String(), - Summary: strings.Split(strings.TrimSpace(curCommit.CommitMessage), "\n")[0], + Summary: curCommit.Summary(), When: curCommit.Author.When, ParentHashes: curCommit.Parents, } resultsMap[curCommit.ID.String()+":"+curPath+string(fname)] = &result } else if string(mode) == git.EntryModeTree.String() { - hexObjectID := make([]byte, objectID.Type().FullLength()) - git.BinToHex(objectID.Type(), binObjectID, hexObjectID) - trees = append(trees, hexObjectID) + trees = append(trees, hex.EncodeToString(binObjectID)) paths = append(paths, curPath+string(fname)+"/") } } @@ -140,11 +115,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err return nil, err } if len(trees) > 0 { - _, err := batchStdinWriter.Write(trees[len(trees)-1]) - if err != nil { - return nil, err - } - _, err = batchStdinWriter.Write([]byte("\n")) + info, _, err = batch.QueryContent(trees[len(trees)-1]) if err != nil { return nil, err } @@ -155,7 +126,7 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err break commitReadingLoop } default: - if err := git.DiscardFull(batchReader, size+1); err != nil { + if err := git.DiscardFull(batchReader, info.Size+1); err != nil { return nil, err } } @@ -179,56 +150,6 @@ func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, err } sort.Sort(lfsResultSlice(results)) - - // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple - shasToNameReader, shasToNameWriter := io.Pipe() - nameRevStdinReader, nameRevStdinWriter := io.Pipe() - errChan := make(chan error, 1) - wg := sync.WaitGroup{} - wg.Add(3) - - go func() { - defer wg.Done() - scanner := bufio.NewScanner(nameRevStdinReader) - i := 0 - for scanner.Scan() { - line := scanner.Text() - if len(line) == 0 { - continue - } - result := results[i] - result.FullCommitName = line - result.BranchName = strings.Split(line, "~")[0] - i++ - } - }() - go NameRevStdin(repo.Ctx, shasToNameReader, nameRevStdinWriter, &wg, basePath) - go func() { - defer wg.Done() - defer shasToNameWriter.Close() - for _, result := range results { - _, err := shasToNameWriter.Write([]byte(result.SHA)) - if err != nil { - errChan <- err - break - } - _, err = shasToNameWriter.Write([]byte{'\n'}) - if err != nil { - errChan <- err - break - } - } - }() - - wg.Wait() - - select { - case err, has := <-errChan: - if has { - return nil, lfsError("unable to obtain name for LFS files", err) - } - default: - } - - return results, nil + err = fillResultNameRev(repo.Ctx, repo.Path, results) + return results, err } diff --git a/package/gitea/source/modules/git/pipeline/lfs_test.go b/package/gitea/source/modules/git/pipeline/lfs_test.go new file mode 100644 index 00000000..30fe2f93 --- /dev/null +++ b/package/gitea/source/modules/git/pipeline/lfs_test.go @@ -0,0 +1,38 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pipeline + +import ( + "testing" + "time" + + "code.gitea.io/gitea/modules/git" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindLFSFile(t *testing.T) { + repoPath := "../../../tests/gitea-repositories-meta/user2/lfs.git" + gitRepo, err := git.OpenRepository(t.Context(), repoPath) + require.NoError(t, err) + defer gitRepo.Close() + + objectID := git.MustIDFromString("2b6c6c4eaefa24b22f2092c3d54b263ff26feb58") + + stats, err := FindLFSFile(gitRepo, objectID) + require.NoError(t, err) + + tm, err := time.Parse(time.RFC3339, "2022-12-21T17:56:42-05:00") + require.NoError(t, err) + + assert.Len(t, stats, 1) + assert.Equal(t, "CONTRIBUTING.md", stats[0].Name) + assert.Equal(t, "73cf03db6ece34e12bf91e8853dc58f678f2f82d", stats[0].SHA) + assert.Equal(t, "Initial commit", stats[0].Summary) + assert.Equal(t, tm, stats[0].When) + assert.Empty(t, stats[0].ParentHashes) + assert.Equal(t, "master", stats[0].BranchName) + assert.Equal(t, "master", stats[0].FullCommitName) +} diff --git a/package/gitea/source/modules/git/pipeline/main_test.go b/package/gitea/source/modules/git/pipeline/main_test.go new file mode 100644 index 00000000..fa5832b6 --- /dev/null +++ b/package/gitea/source/modules/git/pipeline/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package pipeline + +import ( + "testing" + + "code.gitea.io/gitea/modules/git" +) + +func TestMain(m *testing.M) { + git.RunGitTests(m) +} diff --git a/package/gitea/source/modules/git/pipeline/namerev.go b/package/gitea/source/modules/git/pipeline/namerev.go index 0081f7a2..24de4429 100644 --- a/package/gitea/source/modules/git/pipeline/namerev.go +++ b/package/gitea/source/modules/git/pipeline/namerev.go @@ -4,30 +4,54 @@ package pipeline import ( - "bytes" + "bufio" "context" - "fmt" - "io" + "errors" "strings" - "sync" "code.gitea.io/gitea/modules/git/gitcmd" + + "golang.org/x/sync/errgroup" ) -// NameRevStdin runs name-rev --stdin -func NameRevStdin(ctx context.Context, shasToNameReader *io.PipeReader, nameRevStdinWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath string) { - defer wg.Done() - defer shasToNameReader.Close() - defer nameRevStdinWriter.Close() +func fillResultNameRev(ctx context.Context, basePath string, results []*LFSResult) error { + // Should really use a go-git function here but name-rev is not completed and recapitulating it is not simple + wg := errgroup.Group{} + cmd := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").WithDir(basePath) + stdin, stdinClose := cmd.MakeStdinPipe() + stdout, stdoutClose := cmd.MakeStdoutPipe() + defer stdinClose() + defer stdoutClose() - stderr := new(bytes.Buffer) - var errbuf strings.Builder - if err := gitcmd.NewCommand("name-rev", "--stdin", "--name-only", "--always").Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdout: nameRevStdinWriter, - Stdin: shasToNameReader, - Stderr: stderr, - }); err != nil { - _ = shasToNameReader.CloseWithError(fmt.Errorf("git name-rev [%s]: %w - %s", tmpBasePath, err, errbuf.String())) - } + wg.Go(func() error { + scanner := bufio.NewScanner(stdout) + i := 0 + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 { + continue + } + result := results[i] + result.FullCommitName = line + result.BranchName = strings.Split(line, "~")[0] + i++ + } + return scanner.Err() + }) + wg.Go(func() error { + defer stdinClose() + for _, result := range results { + _, err := stdin.Write([]byte(result.SHA)) + if err != nil { + return err + } + _, err = stdin.Write([]byte{'\n'}) + if err != nil { + return err + } + } + return nil + }) + err := cmd.RunWithStderr(ctx) + return errors.Join(err, wg.Wait()) } diff --git a/package/gitea/source/modules/git/pipeline/revlist.go b/package/gitea/source/modules/git/pipeline/revlist.go index 9d4ff754..28d4751b 100644 --- a/package/gitea/source/modules/git/pipeline/revlist.go +++ b/package/gitea/source/modules/git/pipeline/revlist.go @@ -5,65 +5,26 @@ package pipeline import ( "bufio" - "bytes" "context" - "fmt" "io" "strings" - "sync" "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/log" ) -// RevListAllObjects runs rev-list --objects --all and writes to a pipewriter -func RevListAllObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, basePath string, errChan chan<- error) { - defer wg.Done() - defer revListWriter.Close() - - stderr := new(bytes.Buffer) - var errbuf strings.Builder - cmd := gitcmd.NewCommand("rev-list", "--objects", "--all") - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: basePath, - Stdout: revListWriter, - Stderr: stderr, - }); err != nil { - log.Error("git rev-list --objects --all [%s]: %v - %s", basePath, err, errbuf.String()) - err = fmt.Errorf("git rev-list --objects --all [%s]: %w - %s", basePath, err, errbuf.String()) - _ = revListWriter.CloseWithError(err) - errChan <- err - } -} - // RevListObjects run rev-list --objects from headSHA to baseSHA -func RevListObjects(ctx context.Context, revListWriter *io.PipeWriter, wg *sync.WaitGroup, tmpBasePath, headSHA, baseSHA string, errChan chan<- error) { - defer wg.Done() - defer revListWriter.Close() - stderr := new(bytes.Buffer) - var errbuf strings.Builder - cmd := gitcmd.NewCommand("rev-list", "--objects").AddDynamicArguments(headSHA) +func RevListObjects(ctx context.Context, cmd *gitcmd.Command, tmpBasePath, headSHA, baseSHA string) error { + cmd.AddArguments("rev-list", "--objects").AddDynamicArguments(headSHA) if baseSHA != "" { cmd = cmd.AddArguments("--not").AddDynamicArguments(baseSHA) } - if err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: tmpBasePath, - Stdout: revListWriter, - Stderr: stderr, - }); err != nil { - log.Error("git rev-list [%s]: %v - %s", tmpBasePath, err, errbuf.String()) - errChan <- fmt.Errorf("git rev-list [%s]: %w - %s", tmpBasePath, err, errbuf.String()) - } + return cmd.WithDir(tmpBasePath).RunWithStderr(ctx) } // BlobsFromRevListObjects reads a RevListAllObjects and only selects blobs -func BlobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io.PipeWriter, wg *sync.WaitGroup) { - defer wg.Done() - defer revListReader.Close() - scanner := bufio.NewScanner(revListReader) - defer func() { - _ = shasToCheckWriter.CloseWithError(scanner.Err()) - }() +func BlobsFromRevListObjects(in io.ReadCloser, out io.WriteCloser) error { + defer out.Close() + scanner := bufio.NewScanner(in) for scanner.Scan() { line := scanner.Text() if len(line) == 0 { @@ -75,12 +36,12 @@ func BlobsFromRevListObjects(revListReader *io.PipeReader, shasToCheckWriter *io } toWrite := []byte(fields[0] + "\n") for len(toWrite) > 0 { - n, err := shasToCheckWriter.Write(toWrite) + n, err := out.Write(toWrite) if err != nil { - _ = revListReader.CloseWithError(err) - break + return err } toWrite = toWrite[n:] } } + return scanner.Err() } diff --git a/package/gitea/source/modules/git/ref.go b/package/gitea/source/modules/git/ref.go index 56b2db85..7b63d06b 100644 --- a/package/gitea/source/modules/git/ref.go +++ b/package/gitea/source/modules/git/ref.go @@ -220,3 +220,14 @@ func (ref RefName) RefWebLinkPath() string { } return string(refType) + "/" + util.PathEscapeSegments(ref.ShortName()) } + +func ParseRefSuffix(ref string) (string, string) { + // Partially support https://git-scm.com/docs/gitrevisions + if idx := strings.Index(ref, "@{"); idx != -1 { + return ref[:idx], ref[idx:] + } + if idx := strings.Index(ref, "^"); idx != -1 { + return ref[:idx], ref[idx:] + } + return ref, "" +} diff --git a/package/gitea/source/modules/git/remote.go b/package/gitea/source/modules/git/remote.go index 9f12142f..ae56c557 100644 --- a/package/gitea/source/modules/git/remote.go +++ b/package/gitea/source/modules/git/remote.go @@ -22,7 +22,7 @@ func GetRemoteAddress(ctx context.Context, repoPath, remoteName string) (string, cmd = gitcmd.NewCommand("config", "--get").AddDynamicArguments("remote." + remoteName + ".url") } - result, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + result, _, err := cmd.WithDir(repoPath).RunStdString(ctx) if err != nil { return "", err } @@ -74,9 +74,9 @@ func (err *ErrInvalidCloneAddr) Unwrap() error { func IsRemoteNotExistError(err error) bool { // see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216 // Should not add space in the end, sometimes git will add a `:` - prefix1 := "exit status 128 - fatal: No such remote" // git < 2.30 - prefix2 := "exit status 2 - error: No such remote" // git >= 2.30 - return strings.HasPrefix(err.Error(), prefix1) || strings.HasPrefix(err.Error(), prefix2) + prefix1 := "fatal: No such remote" // git < 2.30, exit status 128 + prefix2 := "error: No such remote" // git >= 2.30. exit status 2 + return gitcmd.StderrHasPrefix(err, prefix1) || gitcmd.StderrHasPrefix(err, prefix2) } // ParseRemoteAddr checks if given remote address is valid, diff --git a/package/gitea/source/modules/git/repo.go b/package/gitea/source/modules/git/repo.go index 38cb4592..1e31eb1b 100644 --- a/package/gitea/source/modules/git/repo.go +++ b/package/gitea/source/modules/git/repo.go @@ -8,42 +8,24 @@ import ( "bytes" "context" "fmt" - "io" "net/url" "os" "path" - "path/filepath" "strconv" "strings" "time" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/proxy" - "code.gitea.io/gitea/modules/setting" ) -// GPGSettings represents the default GPG settings for this repository -type GPGSettings struct { - Sign bool - KeyID string - Email string - Name string - PublicKeyContent string - Format string -} - const prettyLogFormat = `--pretty=format:%H` -// GetAllCommitsCount returns count of all commits in repository -func (repo *Repository) GetAllCommitsCount() (int64, error) { - return AllCommitsCount(repo.Ctx, repo.Path, false) -} - func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) { // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git [...] -- [...]' logs, _, err := gitcmd.NewCommand("log").AddArguments(prettyLogFormat). - AddDynamicArguments(revisionRange).AddArguments("--"). - RunStdBytes(ctx, &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(revisionRange).AddArguments("--").WithDir(repo.Path). + RunStdBytes(ctx) if err != nil { return nil, err } @@ -71,7 +53,7 @@ func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, erro // IsRepoURLAccessible checks if given repository URL is accessible. func IsRepoURLAccessible(ctx context.Context, url string) bool { - _, _, err := gitcmd.NewCommand("ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(ctx, nil) + _, _, err := gitcmd.NewCommand("ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(ctx) return err == nil } @@ -94,27 +76,25 @@ func InitRepository(ctx context.Context, repoPath string, bare bool, objectForma if bare { cmd.AddArguments("--bare") } - _, _, err = cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) + _, _, err = cmd.WithDir(repoPath).RunStdString(ctx) return err } // IsEmpty Check if repository is empty. func (repo *Repository) IsEmpty() (bool, error) { - var errbuf, output strings.Builder - if err := gitcmd.NewCommand().AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("rev-list", "-n", "1", "--all"). - Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: &output, - Stderr: &errbuf, - }); err != nil { - if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" { + stdout, _, err := gitcmd.NewCommand(). + AddOptionFormat("--git-dir=%s", repo.Path). + AddArguments("rev-list", "-n", "1", "--all"). + WithDir(repo.Path). + RunStdString(repo.Ctx) + if err != nil { + if (gitcmd.IsErrorExitCode(err, 1) && err.Stderr() == "") || gitcmd.IsErrorExitCode(err, 129) { // git 2.11 exits with 129 if the repo is empty return true, nil } - return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String()) + return true, fmt.Errorf("check empty: %w", err) } - - return strings.TrimSpace(output.String()) == "", nil + return strings.TrimSpace(stdout) == "", nil } // CloneRepoOptions options when clone a repository @@ -129,6 +109,8 @@ type CloneRepoOptions struct { Depth int Filter string SkipTLSVerify bool + SingleBranch bool + Env []string } // Clone clones original repository to target path. @@ -163,6 +145,9 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { if opts.Filter != "" { cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter) } + if opts.SingleBranch { + cmd.AddArguments("--single-branch") + } if len(opts.Branch) > 0 { cmd.AddArguments("-b").AddDynamicArguments(opts.Branch) } @@ -173,37 +158,39 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { } envs := os.Environ() - u, err := url.Parse(from) - if err == nil { - envs = proxy.EnvWithProxy(u) + if opts.Env != nil { + envs = opts.Env + } else { + u, err := url.Parse(from) + if err == nil { + envs = proxy.EnvWithProxy(u) + } } - stderr := new(bytes.Buffer) - if err = cmd.Run(ctx, &gitcmd.RunOpts{ - Timeout: opts.Timeout, - Env: envs, - Stdout: io.Discard, - Stderr: stderr, - }); err != nil { - return gitcmd.ConcatenateError(err, stderr.String()) - } - return nil + return cmd. + WithTimeout(opts.Timeout). + WithEnv(envs). + RunWithStderr(ctx) } // PushOptions options when push to remote type PushOptions struct { - Remote string - Branch string - Force bool - Mirror bool - Env []string - Timeout time.Duration + Remote string + LocalRefName string + Branch string + Force bool + ForceWithLease string + Mirror bool + Env []string + Timeout time.Duration } // Push pushs local commits to given remote branch. func Push(ctx context.Context, repoPath string, opts PushOptions) error { cmd := gitcmd.NewCommand("push") - if opts.Force { + if opts.ForceWithLease != "" { + cmd.AddOptionFormat("--force-with-lease=%s", opts.ForceWithLease) + } else if opts.Force { cmd.AddArguments("-f") } if opts.Mirror { @@ -211,11 +198,17 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { } remoteBranchArgs := []string{opts.Remote} if len(opts.Branch) > 0 { - remoteBranchArgs = append(remoteBranchArgs, opts.Branch) + var refspec string + if opts.LocalRefName != "" { + refspec = fmt.Sprintf("%s:%s", opts.LocalRefName, opts.Branch) + } else { + refspec = opts.Branch + } + remoteBranchArgs = append(remoteBranchArgs, refspec) } cmd.AddDashesAndList(remoteBranchArgs...) - stdout, stderr, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath}) + stdout, stderr, err := cmd.WithEnv(opts.Env).WithTimeout(opts.Timeout).WithDir(repoPath).RunStdString(ctx) if err != nil { if strings.Contains(stderr, "non-fast-forward") { return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err} @@ -231,84 +224,3 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error { return nil } - -// GetLatestCommitTime returns time for latest commit in repository (across all branches) -func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) { - cmd := gitcmd.NewCommand("for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)") - stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) - if err != nil { - return time.Time{}, err - } - commitTime := strings.TrimSpace(stdout) - return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) -} - -// DivergeObject represents commit count diverging commits -type DivergeObject struct { - Ahead int - Behind int -} - -// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch -func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) { - cmd := gitcmd.NewCommand("rev-list", "--count", "--left-right"). - AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--") - stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) - if err != nil { - return do, err - } - left, right, found := strings.Cut(strings.Trim(stdout, "\n"), "\t") - if !found { - return do, fmt.Errorf("git rev-list output is missing a tab: %q", stdout) - } - - do.Behind, err = strconv.Atoi(left) - if err != nil { - return do, err - } - do.Ahead, err = strconv.Atoi(right) - if err != nil { - return do, err - } - return do, nil -} - -// CreateBundle create bundle content to the target path -func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error { - tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle") - if err != nil { - return err - } - defer cleanup() - - env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects")) - _, _, err = gitcmd.NewCommand("init", "--bare").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env}) - if err != nil { - return err - } - - _, _, err = gitcmd.NewCommand("reset", "--soft").AddDynamicArguments(commit).RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env}) - if err != nil { - return err - } - - _, _, err = gitcmd.NewCommand("branch", "-m", "bundle").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env}) - if err != nil { - return err - } - - tmpFile := filepath.Join(tmp, "bundle") - _, _, err = gitcmd.NewCommand("bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmp, Env: env}) - if err != nil { - return err - } - - fi, err := os.Open(tmpFile) - if err != nil { - return err - } - defer fi.Close() - - _, err = io.Copy(out, fi) - return err -} diff --git a/package/gitea/source/modules/git/repo_archive.go b/package/gitea/source/modules/git/repo_archive.go deleted file mode 100644 index e1230034..00000000 --- a/package/gitea/source/modules/git/repo_archive.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "context" - "fmt" - "io" - "path/filepath" - "strings" - - "code.gitea.io/gitea/modules/git/gitcmd" -) - -// ArchiveType archive types -type ArchiveType int - -const ( - ArchiveUnknown ArchiveType = iota - ArchiveZip // 1 - ArchiveTarGz // 2 - ArchiveBundle // 3 -) - -// String converts an ArchiveType to string: the extension of the archive file without prefix dot -func (a ArchiveType) String() string { - switch a { - case ArchiveZip: - return "zip" - case ArchiveTarGz: - return "tar.gz" - case ArchiveBundle: - return "bundle" - } - return "unknown" -} - -func SplitArchiveNameType(s string) (string, ArchiveType) { - switch { - case strings.HasSuffix(s, ".zip"): - return strings.TrimSuffix(s, ".zip"), ArchiveZip - case strings.HasSuffix(s, ".tar.gz"): - return strings.TrimSuffix(s, ".tar.gz"), ArchiveTarGz - case strings.HasSuffix(s, ".bundle"): - return strings.TrimSuffix(s, ".bundle"), ArchiveBundle - } - return s, ArchiveUnknown -} - -// CreateArchive create archive content to the target path -func (repo *Repository) CreateArchive(ctx context.Context, format ArchiveType, target io.Writer, usePrefix bool, commitID string) error { - if format.String() == "unknown" { - return fmt.Errorf("unknown format: %v", format) - } - - cmd := gitcmd.NewCommand("archive") - if usePrefix { - cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.Path, ".git"))+"/") - } - cmd.AddOptionFormat("--format=%s", format.String()) - cmd.AddDynamicArguments(commitID) - - var stderr strings.Builder - err := cmd.Run(ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: target, - Stderr: &stderr, - }) - if err != nil { - return gitcmd.ConcatenateError(err, stderr.String()) - } - return nil -} diff --git a/package/gitea/source/modules/git/repo_base_gogit.go b/package/gitea/source/modules/git/repo_base_gogit.go index e0d0b453..986264fd 100644 --- a/package/gitea/source/modules/git/repo_base_gogit.go +++ b/package/gitea/source/modules/git/repo_base_gogit.go @@ -32,7 +32,6 @@ type Repository struct { gogitRepo *gogit.Repository gogitStorage *filesystem.Storage - gpgSettings *GPGSettings Ctx context.Context LastCommitCache *LastCommitCache diff --git a/package/gitea/source/modules/git/repo_base_nogogit.go b/package/gitea/source/modules/git/repo_base_nogogit.go index 4091e708..775bbd4a 100644 --- a/package/gitea/source/modules/git/repo_base_nogogit.go +++ b/package/gitea/source/modules/git/repo_base_nogogit.go @@ -7,9 +7,9 @@ package git import ( - "bufio" "context" "path/filepath" + "sync" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" @@ -23,13 +23,9 @@ type Repository struct { tagCache *ObjectCache[*Tag] - gpgSettings *GPGSettings - - batchInUse bool - batch *Batch - - checkInUse bool - check *Batch + mu sync.Mutex + catFileBatchCloser CatFileBatchCloser + catFileBatchInUse bool Ctx context.Context LastCommitCache *LastCommitCache @@ -58,69 +54,47 @@ func OpenRepository(ctx context.Context, repoPath string) (*Repository, error) { }, nil } -// CatFileBatch obtains a CatFileBatch for this repository -func (repo *Repository) CatFileBatch(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) { - if repo.batch == nil { - var err error - repo.batch, err = NewBatch(ctx, repo.Path) +// CatFileBatch obtains a "batch object provider" for this repository. +// It reuses an existing one if available, otherwise creates a new one. +func (repo *Repository) CatFileBatch(ctx context.Context) (_ CatFileBatch, closeFunc func(), err error) { + repo.mu.Lock() + defer repo.mu.Unlock() + + if repo.catFileBatchCloser == nil { + repo.catFileBatchCloser, err = NewBatch(ctx, repo.Path) if err != nil { - return nil, nil, nil, err + repo.catFileBatchCloser = nil // otherwise it is "interface(nil)" and will cause wrong logic + return nil, nil, err } } - if !repo.batchInUse { - repo.batchInUse = true - return repo.batch.Writer, repo.batch.Reader, func() { - repo.batchInUse = false + if !repo.catFileBatchInUse { + repo.catFileBatchInUse = true + return CatFileBatch(repo.catFileBatchCloser), func() { + repo.mu.Lock() + defer repo.mu.Unlock() + repo.catFileBatchInUse = false }, nil } log.Debug("Opening temporary cat file batch for: %s", repo.Path) tempBatch, err := NewBatch(ctx, repo.Path) if err != nil { - return nil, nil, nil, err + return nil, nil, err } - return tempBatch.Writer, tempBatch.Reader, tempBatch.Close, nil -} - -// CatFileBatchCheck obtains a CatFileBatchCheck for this repository -func (repo *Repository) CatFileBatchCheck(ctx context.Context) (WriteCloserError, *bufio.Reader, func(), error) { - if repo.check == nil { - var err error - repo.check, err = NewBatchCheck(ctx, repo.Path) - if err != nil { - return nil, nil, nil, err - } - } - - if !repo.checkInUse { - repo.checkInUse = true - return repo.check.Writer, repo.check.Reader, func() { - repo.checkInUse = false - }, nil - } - - log.Debug("Opening temporary cat file batch-check for: %s", repo.Path) - tempBatchCheck, err := NewBatchCheck(ctx, repo.Path) - if err != nil { - return nil, nil, nil, err - } - return tempBatchCheck.Writer, tempBatchCheck.Reader, tempBatchCheck.Close, nil + return tempBatch, tempBatch.Close, nil } func (repo *Repository) Close() error { if repo == nil { return nil } - if repo.batch != nil { - repo.batch.Close() - repo.batch = nil - repo.batchInUse = false - } - if repo.check != nil { - repo.check.Close() - repo.check = nil - repo.checkInUse = false + repo.mu.Lock() + defer repo.mu.Unlock() + if repo.catFileBatchCloser != nil { + repo.catFileBatchCloser.Close() + repo.catFileBatchCloser = nil + repo.catFileBatchInUse = false } repo.LastCommitCache = nil repo.tagCache = nil diff --git a/package/gitea/source/modules/git/repo_base_nogogit_test.go b/package/gitea/source/modules/git/repo_base_nogogit_test.go new file mode 100644 index 00000000..a12bbb73 --- /dev/null +++ b/package/gitea/source/modules/git/repo_base_nogogit_test.go @@ -0,0 +1,26 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build !gogit + +package git + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRepoCatFileBatch(t *testing.T) { + t.Run("MissingRepoAndClose", func(t *testing.T) { + repo, err := OpenRepository(t.Context(), filepath.Join(testReposDir, "repo1_bare")) + require.NoError(t, err) + repo.Path = "/no-such" // when the repo is missing (it usually occurs during testing because the fixtures are synced frequently) + _, _, err = repo.CatFileBatch(t.Context()) + require.Error(t, err) + require.NoError(t, repo.Close()) // shouldn't panic + }) + + // TODO: test more methods and concurrency queries +} diff --git a/package/gitea/source/modules/git/repo_blame.go b/package/gitea/source/modules/git/repo_blame.go deleted file mode 100644 index 04f74049..00000000 --- a/package/gitea/source/modules/git/repo_blame.go +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "fmt" - - "code.gitea.io/gitea/modules/git/gitcmd" -) - -// LineBlame returns the latest commit at the given line -func (repo *Repository) LineBlame(revision, path, file string, line uint) (*Commit, error) { - res, _, err := gitcmd.NewCommand("blame"). - AddOptionFormat("-L %d,%d", line, line). - AddOptionValues("-p", revision). - AddDashesAndList(file).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: path}) - if err != nil { - return nil, err - } - if len(res) < 40 { - return nil, fmt.Errorf("invalid result of blame: %s", res) - } - return repo.GetCommit(res[:40]) -} diff --git a/package/gitea/source/modules/git/repo_branch.go b/package/gitea/source/modules/git/repo_branch.go index 5d4c05bb..1eebc721 100644 --- a/package/gitea/source/modules/git/repo_branch.go +++ b/package/gitea/source/modules/git/repo_branch.go @@ -5,84 +5,20 @@ package git import ( - "context" - "errors" - "strings" - "code.gitea.io/gitea/modules/git/gitcmd" ) // BranchPrefix base dir of the branch information file store on git const BranchPrefix = "refs/heads/" -// IsReferenceExist returns true if given reference exists in the repository. -func IsReferenceExist(ctx context.Context, repoPath, name string) bool { - _, _, err := gitcmd.NewCommand("show-ref", "--verify").AddDashesAndList(name).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) - return err == nil -} - -// IsBranchExist returns true if given branch exists in the repository. -func IsBranchExist(ctx context.Context, repoPath, name string) bool { - return IsReferenceExist(ctx, repoPath, BranchPrefix+name) -} - -func GetDefaultBranch(ctx context.Context, repoPath string) (string, error) { - stdout, _, err := gitcmd.NewCommand("symbolic-ref", "HEAD").RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) - if err != nil { - return "", err - } - stdout = strings.TrimSpace(stdout) - if !strings.HasPrefix(stdout, BranchPrefix) { - return "", errors.New("the HEAD is not a branch: " + stdout) - } - return strings.TrimPrefix(stdout, BranchPrefix), nil -} - -// DeleteBranchOptions Option(s) for delete branch -type DeleteBranchOptions struct { - Force bool -} - -// DeleteBranch delete a branch by name on repository. -func (repo *Repository) DeleteBranch(name string, opts DeleteBranchOptions) error { - cmd := gitcmd.NewCommand("branch") - - if opts.Force { - cmd.AddArguments("-D") - } else { - cmd.AddArguments("-d") - } - - cmd.AddDashesAndList(name) - _, _, err := cmd.RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - - return err -} - -// CreateBranch create a new branch -func (repo *Repository) CreateBranch(branch, oldbranchOrCommit string) error { - cmd := gitcmd.NewCommand("branch") - cmd.AddDashesAndList(branch, oldbranchOrCommit) - - _, _, err := cmd.RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - - return err -} - // AddRemote adds a new remote to repository. func (repo *Repository) AddRemote(name, url string, fetch bool) error { cmd := gitcmd.NewCommand("remote", "add") if fetch { cmd.AddArguments("-f") } - cmd.AddDynamicArguments(name, url) - - _, _, err := cmd.RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - return err -} - -// RenameBranch rename a branch -func (repo *Repository) RenameBranch(from, to string) error { - _, _, err := gitcmd.NewCommand("branch", "-m").AddDynamicArguments(from, to).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := cmd.AddDynamicArguments(name, url). + WithDir(repo.Path). + RunStdString(repo.Ctx) return err } diff --git a/package/gitea/source/modules/git/repo_branch_nogogit.go b/package/gitea/source/modules/git/repo_branch_nogogit.go index 255c2974..f925aab3 100644 --- a/package/gitea/source/modules/git/repo_branch_nogogit.go +++ b/package/gitea/source/modules/git/repo_branch_nogogit.go @@ -8,7 +8,6 @@ package git import ( "bufio" - "bytes" "context" "io" "strings" @@ -18,24 +17,24 @@ import ( ) // IsObjectExist returns true if the given object exists in the repository. +// FIXME: this function doesn't seem right, it is only used by GarbageCollectLFSMetaObjectsForRepo func (repo *Repository) IsObjectExist(name string) bool { if name == "" { return false } - wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { - log.Debug("Error writing to CatFileBatchCheck %v", err) + log.Debug("Error opening CatFileBatch %v", err) return false } defer cancel() - _, err = wr.Write([]byte(name + "\n")) + info, err := batch.QueryInfo(name) if err != nil { - log.Debug("Error writing to CatFileBatchCheck %v", err) + log.Debug("Error checking object info %v", err) return false } - sha, _, _, err := ReadBatchLine(rd) - return err == nil && bytes.HasPrefix(sha, []byte(strings.TrimSpace(name))) + return strings.HasPrefix(info.ID, name) // FIXME: this logic doesn't seem right, why "HasPrefix" } // IsReferenceExist returns true if given reference exists in the repository. @@ -44,18 +43,13 @@ func (repo *Repository) IsReferenceExist(name string) bool { return false } - wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { - log.Debug("Error writing to CatFileBatchCheck %v", err) + log.Error("Error opening CatFileBatch %v", err) return false } defer cancel() - _, err = wr.Write([]byte(name + "\n")) - if err != nil { - log.Debug("Error writing to CatFileBatchCheck %v", err) - return false - } - _, _, _, err = ReadBatchLine(rd) + _, err = batch.QueryInfo(name) return err == nil } @@ -100,94 +94,81 @@ func callShowRef(ctx context.Context, repoPath, trimPrefix string, extraArgs git } func WalkShowRef(ctx context.Context, repoPath string, extraArgs gitcmd.TrustedCmdArgs, skip, limit int, walkfn func(sha1, refname string) error) (countAll int, err error) { - stdoutReader, stdoutWriter := io.Pipe() - defer func() { - _ = stdoutReader.Close() - _ = stdoutWriter.Close() - }() - - go func() { - stderrBuilder := &strings.Builder{} - args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"} - args = append(args, extraArgs...) - err := gitcmd.NewCommand(args...).Run(ctx, &gitcmd.RunOpts{ - Dir: repoPath, - Stdout: stdoutWriter, - Stderr: stderrBuilder, - }) - if err != nil { - if stderrBuilder.Len() == 0 { - _ = stdoutWriter.Close() - return - } - _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, stderrBuilder.String())) - } else { - _ = stdoutWriter.Close() - } - }() - i := 0 - bufReader := bufio.NewReader(stdoutReader) - for i < skip { - _, isPrefix, err := bufReader.ReadLine() - if err == io.EOF { - return i, nil - } - if err != nil { - return 0, err - } - if !isPrefix { - i++ - } + args := gitcmd.TrustedCmdArgs{"for-each-ref", "--format=%(objectname) %(refname)"} + args = append(args, extraArgs...) + cmd := gitcmd.NewCommand(args...) + stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() + defer stdoutReaderClose() + cmd.WithDir(repoPath). + WithPipelineFunc(func(gitcmd.Context) error { + bufReader := bufio.NewReader(stdoutReader) + for i < skip { + _, isPrefix, err := bufReader.ReadLine() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + if !isPrefix { + i++ + } + } + for limit == 0 || i < skip+limit { + // The output of show-ref is simply a list: + // SP LF + sha, err := bufReader.ReadString(' ') + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + branchName, err := bufReader.ReadString('\n') + if err == io.EOF { + // This shouldn't happen... but we'll tolerate it for the sake of peace + return nil + } + if err != nil { + return err + } + + if len(branchName) > 0 { + branchName = branchName[:len(branchName)-1] + } + + if len(sha) > 0 { + sha = sha[:len(sha)-1] + } + + err = walkfn(sha, branchName) + if err != nil { + return err + } + i++ + } + // count all refs + for limit != 0 { + _, isPrefix, err := bufReader.ReadLine() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + if !isPrefix { + i++ + } + } + return nil + }) + err = cmd.RunWithStderr(ctx) + if errPipeline, ok := gitcmd.UnwrapPipelineError(err); ok { + return i, errPipeline // keep the old behavior: return pipeline error directly } - for limit == 0 || i < skip+limit { - // The output of show-ref is simply a list: - // SP LF - sha, err := bufReader.ReadString(' ') - if err == io.EOF { - return i, nil - } - if err != nil { - return 0, err - } - - branchName, err := bufReader.ReadString('\n') - if err == io.EOF { - // This shouldn't happen... but we'll tolerate it for the sake of peace - return i, nil - } - if err != nil { - return i, err - } - - if len(branchName) > 0 { - branchName = branchName[:len(branchName)-1] - } - - if len(sha) > 0 { - sha = sha[:len(sha)-1] - } - - err = walkfn(sha, branchName) - if err != nil { - return i, err - } - i++ - } - // count all refs - for limit != 0 { - _, isPrefix, err := bufReader.ReadLine() - if err == io.EOF { - return i, nil - } - if err != nil { - return 0, err - } - if !isPrefix { - i++ - } - } - return i, nil + return i, err } // GetRefsBySha returns all references filtered with prefix that belong to a sha commit hash diff --git a/package/gitea/source/modules/git/repo_commit.go b/package/gitea/source/modules/git/repo_commit.go index 6e5911f1..8ba3f833 100644 --- a/package/gitea/source/modules/git/repo_commit.go +++ b/package/gitea/source/modules/git/repo_commit.go @@ -11,7 +11,6 @@ import ( "strconv" "strings" - "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/setting" ) @@ -60,7 +59,11 @@ func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Com relpath = `\` + relpath } - stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat).AddDynamicArguments(id.String()).AddDashesAndList(relpath).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat). + AddDynamicArguments(id.String()). + AddDashesAndList(relpath). + WithDir(repo.Path). + RunStdString(repo.Ctx) if runErr != nil { return nil, runErr } @@ -75,7 +78,10 @@ func (repo *Repository) getCommitByPathWithID(id ObjectID, relpath string) (*Com // GetCommitByPath returns the last commit of relative path. func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { - stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat).AddDashesAndList(relpath).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, runErr := gitcmd.NewCommand("log", "-1", prettyLogFormat). + AddDashesAndList(relpath). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) if runErr != nil { return nil, runErr } @@ -108,7 +114,7 @@ func (repo *Repository) commitsByRangeWithTime(id ObjectID, page, pageSize int, cmd.AddOptionFormat("--until=%s", until) } - stdout, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil { return nil, err } @@ -162,7 +168,7 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ // search for commits matching given constraints and keywords in commit msg addCommonSearchArgs(cmd) - stdout, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil { return nil, err } @@ -183,7 +189,7 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ hashCmd.AddDynamicArguments(v) // search with given constraints for commit matching sha hash of v - hashMatching, _, err := hashCmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + hashMatching, _, err := hashCmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil || bytes.Contains(stdout, hashMatching) { continue } @@ -198,23 +204,17 @@ func (repo *Repository) searchCommits(id ObjectID, opts SearchCommitsOptions) ([ // FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2 // You must ensure that id1 and id2 are valid commit ids. func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) { - stdout, _, err := gitcmd.NewCommand("diff", "--name-only", "-z").AddDynamicArguments(id1, id2).AddDashesAndList(filename).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("diff", "--name-only", "-z"). + AddDynamicArguments(id1, id2). + AddDashesAndList(filename). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) if err != nil { return false, err } return len(strings.TrimSpace(string(stdout))) > 0, nil } -// FileCommitsCount return the number of files at a revision -func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) { - return CommitsCount(repo.Ctx, - CommitsCountOptions{ - RepoPath: repo.Path, - Revision: []string{revision}, - RelPath: []string{file}, - }) -} - type CommitsByFileAndRangeOptions struct { Revision string File string @@ -226,76 +226,70 @@ type CommitsByFileAndRangeOptions struct { // CommitsByFileAndRange return the commits according revision file and the page func (repo *Repository) CommitsByFileAndRange(opts CommitsByFileAndRangeOptions) ([]*Commit, error) { - stdoutReader, stdoutWriter := io.Pipe() - defer func() { - _ = stdoutReader.Close() - _ = stdoutWriter.Close() - }() - go func() { - stderr := strings.Builder{} - gitCmd := gitcmd.NewCommand("rev-list"). - AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). - AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize) - gitCmd.AddDynamicArguments(opts.Revision) + gitCmd := gitcmd.NewCommand("rev-list"). + AddOptionFormat("--max-count=%d", setting.Git.CommitsRangeSize). + AddOptionFormat("--skip=%d", (opts.Page-1)*setting.Git.CommitsRangeSize) + gitCmd.AddDynamicArguments(opts.Revision) - if opts.Not != "" { - gitCmd.AddOptionValues("--not", opts.Not) - } - if opts.Since != "" { - gitCmd.AddOptionFormat("--since=%s", opts.Since) - } - if opts.Until != "" { - gitCmd.AddOptionFormat("--until=%s", opts.Until) - } - - gitCmd.AddDashesAndList(opts.File) - err := gitCmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: stdoutWriter, - Stderr: &stderr, - }) - if err != nil { - _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, (&stderr).String())) - } else { - _ = stdoutWriter.Close() - } - }() - - objectFormat, err := repo.GetObjectFormat() - if err != nil { - return nil, err + if opts.Not != "" { + gitCmd.AddOptionValues("--not", opts.Not) } + if opts.Since != "" { + gitCmd.AddOptionFormat("--since=%s", opts.Since) + } + if opts.Until != "" { + gitCmd.AddOptionFormat("--until=%s", opts.Until) + } + gitCmd.AddDashesAndList(opts.File) - length := objectFormat.FullLength() - commits := []*Commit{} - shaline := make([]byte, length+1) - for { - n, err := io.ReadFull(stdoutReader, shaline) - if err != nil || n < length { - if err == io.EOF { - err = nil + var commits []*Commit + stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe() + defer stdoutReaderClose() + err := gitCmd.WithDir(repo.Path). + WithPipelineFunc(func(context gitcmd.Context) error { + objectFormat, err := repo.GetObjectFormat() + if err != nil { + return err } - return commits, err - } - objectID, err := NewIDFromString(string(shaline[0:length])) - if err != nil { - return nil, err - } - commit, err := repo.getCommit(objectID) - if err != nil { - return nil, err - } - commits = append(commits, commit) - } + + length := objectFormat.FullLength() + shaline := make([]byte, length+1) + for { + n, err := io.ReadFull(stdoutReader, shaline) + if err != nil || n < length { + if err == io.EOF { + err = nil + } + return err + } + objectID, err := NewIDFromString(string(shaline[0:length])) + if err != nil { + return err + } + commit, err := repo.getCommit(objectID) + if err != nil { + return err + } + commits = append(commits, commit) + } + }). + RunWithStderr(repo.Ctx) + return commits, err } // FilesCountBetween return the number of files changed between two commits func (repo *Repository) FilesCountBetween(startCommitID, endCommitID string) (int, error) { - stdout, _, err := gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(startCommitID+"..."+endCommitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("diff", "--name-only"). + AddDynamicArguments(startCommitID + "..." + endCommitID). + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil && strings.Contains(err.Error(), "no merge base") { // git >= 2.28 now returns an error if startCommitID and endCommitID have become unrelated. // previously it would return the results of git diff --name-only startCommitID endCommitID so let's try that... - stdout, _, err = gitcmd.NewCommand("diff", "--name-only").AddDynamicArguments(startCommitID, endCommitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("diff", "--name-only"). + AddDynamicArguments(startCommitID, endCommitID). + WithDir(repo.Path). + RunStdString(repo.Ctx) } if err != nil { return 0, err @@ -309,13 +303,22 @@ func (repo *Repository) CommitsBetween(last, before *Commit) ([]*Commit, error) var stdout []byte var err error if before == nil { - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("rev-list"). + AddDynamicArguments(last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } else { - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("rev-list"). + AddDynamicArguments(before.ID.String() + ".." + last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list before last so let's try that... - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err = gitcmd.NewCommand("rev-list"). + AddDynamicArguments(before.ID.String(), last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } } if err != nil { @@ -332,40 +335,25 @@ func (repo *Repository) CommitsBetweenLimit(last, before *Commit, limit, skip in stdout, _, err = gitcmd.NewCommand("rev-list"). AddOptionValues("--max-count", strconv.Itoa(limit)). AddOptionValues("--skip", strconv.Itoa(skip)). - AddDynamicArguments(last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } else { stdout, _, err = gitcmd.NewCommand("rev-list"). AddOptionValues("--max-count", strconv.Itoa(limit)). AddOptionValues("--skip", strconv.Itoa(skip)). - AddDynamicArguments(before.ID.String()+".."+last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(before.ID.String() + ".." + last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) if err != nil && strings.Contains(err.Error(), "no merge base") { // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. // previously it would return the results of git rev-list --max-count n before last so let's try that... stdout, _, err = gitcmd.NewCommand("rev-list"). AddOptionValues("--max-count", strconv.Itoa(limit)). AddOptionValues("--skip", strconv.Itoa(skip)). - AddDynamicArguments(before.ID.String(), last.ID.String()).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - } - } - if err != nil { - return nil, err - } - return repo.parsePrettyFormatLogToList(bytes.TrimSpace(stdout)) -} - -// CommitsBetweenNotBase returns a list that contains commits between [before, last), excluding commits in baseBranch. -// If before is detached (removed by reset + push) it is not included. -func (repo *Repository) CommitsBetweenNotBase(last, before *Commit, baseBranch string) ([]*Commit, error) { - var stdout []byte - var err error - if before == nil { - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - } else { - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String()+".."+last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - if err != nil && strings.Contains(err.Error(), "no merge base") { - // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. - // previously it would return the results of git rev-list before last so let's try that... - stdout, _, err = gitcmd.NewCommand("rev-list").AddDynamicArguments(before.ID.String(), last.ID.String()).AddOptionValues("--not", baseBranch).RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(before.ID.String(), last.ID.String()). + WithDir(repo.Path). + RunStdBytes(repo.Ctx) } } if err != nil { @@ -390,25 +378,6 @@ func (repo *Repository) CommitsBetweenIDs(last, before string) ([]*Commit, error return repo.CommitsBetween(lastCommit, beforeCommit) } -// CommitsCountBetween return numbers of commits between two commits -func (repo *Repository) CommitsCountBetween(start, end string) (int64, error) { - count, err := CommitsCount(repo.Ctx, CommitsCountOptions{ - RepoPath: repo.Path, - Revision: []string{start + ".." + end}, - }) - - if err != nil && strings.Contains(err.Error(), "no merge base") { - // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. - // previously it would return the results of git rev-list before last so let's try that... - return CommitsCount(repo.Ctx, CommitsCountOptions{ - RepoPath: repo.Path, - Revision: []string{start, end}, - }) - } - - return count, err -} - // commitsBefore the limit is depth, not total number of returned commits. func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) { cmd := gitcmd.NewCommand("log", prettyLogFormat) @@ -417,7 +386,7 @@ func (repo *Repository) commitsBefore(id ObjectID, limit int) ([]*Commit, error) } cmd.AddDynamicArguments(id.String()) - stdout, _, runErr := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, runErr := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if runErr != nil { return nil, runErr } @@ -457,10 +426,9 @@ func (repo *Repository) getBranches(env []string, commitID string, limit int) ([ stdout, _, err := gitcmd.NewCommand("for-each-ref", "--format=%(refname:strip=2)"). AddOptionFormat("--count=%d", limit). AddOptionValues("--contains", commitID, BranchPrefix). - RunStdString(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Env: env, - }) + WithDir(repo.Path). + WithEnv(env). + RunStdString(repo.Ctx) if err != nil { return nil, err } @@ -469,10 +437,11 @@ func (repo *Repository) getBranches(env []string, commitID string, limit int) ([ return branches, nil } - stdout, _, err := gitcmd.NewCommand("branch").AddOptionValues("--contains", commitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Env: env, - }) + stdout, _, err := gitcmd.NewCommand("branch"). + AddOptionValues("--contains", commitID). + WithDir(repo.Path). + WithEnv(env). + RunStdString(repo.Ctx) if err != nil { return nil, err } @@ -511,39 +480,24 @@ func (repo *Repository) GetCommitsFromIDs(commitIDs []string) []*Commit { // IsCommitInBranch check if the commit is on the branch func (repo *Repository) IsCommitInBranch(commitID, branch string) (r bool, err error) { - stdout, _, err := gitcmd.NewCommand("branch", "--contains").AddDynamicArguments(commitID, branch).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("branch", "--contains"). + AddDynamicArguments(commitID, branch). + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil { return false, err } return len(stdout) > 0, err } -func (repo *Repository) AddLastCommitCache(cacheKey, fullName, sha string) error { - if repo.LastCommitCache == nil { - commitsCount, err := cache.GetInt64(cacheKey, func() (int64, error) { - commit, err := repo.GetCommit(sha) - if err != nil { - return 0, err - } - return commit.CommitsCount() - }) - if err != nil { - return err - } - repo.LastCommitCache = NewLastCommitCache(commitsCount, fullName, repo, cache.GetCache()) - } - return nil -} - // GetCommitBranchStart returns the commit where the branch diverged func (repo *Repository) GetCommitBranchStart(env []string, branch, endCommitID string) (string, error) { cmd := gitcmd.NewCommand("log", prettyLogFormat) cmd.AddDynamicArguments(endCommitID) - stdout, _, runErr := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Env: env, - }) + stdout, _, runErr := cmd.WithDir(repo.Path). + WithEnv(env). + RunStdBytes(repo.Ctx) if runErr != nil { return "", runErr } diff --git a/package/gitea/source/modules/git/repo_commit_gogit.go b/package/gitea/source/modules/git/repo_commit_gogit.go index fc653714..550d1537 100644 --- a/package/gitea/source/modules/git/repo_commit_gogit.go +++ b/package/gitea/source/modules/git/repo_commit_gogit.go @@ -51,7 +51,10 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { } } - actualCommitID, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(commitID).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + actualCommitID, _, err := gitcmd.NewCommand("rev-parse", "--verify"). + AddDynamicArguments(commitID). + WithDir(repo.Path). + RunStdString(repo.Ctx) actualCommitID = strings.TrimSpace(actualCommitID) if err != nil { if strings.Contains(err.Error(), "unknown revision or path") || @@ -64,16 +67,6 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { return NewIDFromString(actualCommitID) } -// IsCommitExist returns true if given commit exists in current repository. -func (repo *Repository) IsCommitExist(name string) bool { - hash, err := repo.ConvertToGitID(name) - if err != nil { - return false - } - _, err = repo.gogitRepo.CommitObject(plumbing.Hash(hash.RawValue())) - return err == nil -} - func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { var tagObject *object.Tag @@ -104,7 +97,7 @@ func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { } commit.Tree.ID = ParseGogitHash(tree.Hash) - commit.Tree.gogitTree = tree + commit.Tree.resolvedGogitTreeObject = tree return commit, nil } diff --git a/package/gitea/source/modules/git/repo_commit_nogogit.go b/package/gitea/source/modules/git/repo_commit_nogogit.go index d2c66a54..2ddb5275 100644 --- a/package/gitea/source/modules/git/repo_commit_nogogit.go +++ b/package/gitea/source/modules/git/repo_commit_nogogit.go @@ -6,7 +6,6 @@ package git import ( - "bufio" "errors" "io" "strings" @@ -17,7 +16,10 @@ import ( // ResolveReference resolves a name to a reference func (repo *Repository) ResolveReference(name string) (string, error) { - stdout, _, err := gitcmd.NewCommand("show-ref", "--hash").AddDynamicArguments(name).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("show-ref", "--hash"). + AddDynamicArguments(name). + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil { if strings.Contains(err.Error(), "not a valid ref") { return "", ErrNotExist{name, ""} @@ -34,47 +36,31 @@ func (repo *Repository) ResolveReference(name string) (string, error) { // GetRefCommitID returns the last commit ID string of given reference (branch or tag). func (repo *Repository) GetRefCommitID(name string) (string, error) { - wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { return "", err } defer cancel() - _, err = wr.Write([]byte(name + "\n")) - if err != nil { - return "", err - } - shaBs, _, _, err := ReadBatchLine(rd) + info, err := batch.QueryInfo(name) if IsErrNotExist(err) { return "", ErrNotExist{name, ""} + } else if err != nil { + return "", err } - - return string(shaBs), nil -} - -// IsCommitExist returns true if given commit exists in current repository. -func (repo *Repository) IsCommitExist(name string) bool { - if err := ensureValidGitRepository(repo.Ctx, repo.Path); err != nil { - log.Error("IsCommitExist: %v", err) - return false - } - _, _, err := gitcmd.NewCommand("cat-file", "-e").AddDynamicArguments(name).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - return err == nil + return info.ID, nil } func (repo *Repository) getCommit(id ObjectID) (*Commit, error) { - wr, rd, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { return nil, err } defer cancel() - - _, _ = wr.Write([]byte(id.String() + "\n")) - - return repo.getCommitFromBatchReader(wr, rd, id) + return repo.getCommitWithBatch(batch, id) } -func (repo *Repository) getCommitFromBatchReader(wr WriteCloserError, rd *bufio.Reader, id ObjectID) (*Commit, error) { - _, typ, size, err := ReadBatchLine(rd) +func (repo *Repository) getCommitWithBatch(batch CatFileBatch, id ObjectID) (*Commit, error) { + info, rd, err := batch.QueryContent(id.String()) if err != nil { if errors.Is(err, io.EOF) || IsErrNotExist(err) { return nil, ErrNotExist{ID: id.String()} @@ -82,13 +68,13 @@ func (repo *Repository) getCommitFromBatchReader(wr WriteCloserError, rd *bufio. return nil, err } - switch typ { + switch info.Type { case "missing": return nil, ErrNotExist{ID: id.String()} case "tag": // then we need to parse the tag // and load the commit - data, err := io.ReadAll(io.LimitReader(rd, size)) + data, err := io.ReadAll(io.LimitReader(rd, info.Size)) if err != nil { return nil, err } @@ -100,19 +86,9 @@ func (repo *Repository) getCommitFromBatchReader(wr WriteCloserError, rd *bufio. if err != nil { return nil, err } - - if _, err := wr.Write([]byte(tag.Object.String() + "\n")); err != nil { - return nil, err - } - - commit, err := repo.getCommitFromBatchReader(wr, rd, tag.Object) - if err != nil { - return nil, err - } - - return commit, nil + return repo.getCommitWithBatch(batch, tag.Object) case "commit": - commit, err := CommitFromReader(repo, id, io.LimitReader(rd, size)) + commit, err := CommitFromReader(repo, id, io.LimitReader(rd, info.Size)) if err != nil { return nil, err } @@ -123,8 +99,8 @@ func (repo *Repository) getCommitFromBatchReader(wr WriteCloserError, rd *bufio. return commit, nil default: - log.Debug("Unknown typ: %s", typ) - if err := DiscardFull(rd, size+1); err != nil { + log.Debug("Unknown cat-file object type: %s", info.Type) + if err := DiscardFull(rd, info.Size+1); err != nil { return nil, err } return nil, ErrNotExist{ @@ -146,16 +122,12 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { } } - wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { return nil, err } defer cancel() - _, err = wr.Write([]byte(commitID + "\n")) - if err != nil { - return nil, err - } - sha, _, _, err := ReadBatchLine(rd) + info, err := batch.QueryInfo(commitID) if err != nil { if IsErrNotExist(err) { return nil, ErrNotExist{commitID, ""} @@ -163,5 +135,5 @@ func (repo *Repository) ConvertToGitID(commitID string) (ObjectID, error) { return nil, err } - return MustIDFromString(string(sha)), nil + return MustIDFromString(info.ID), nil } diff --git a/package/gitea/source/modules/git/repo_commitgraph.go b/package/gitea/source/modules/git/repo_commitgraph.go index 331c799b..3dac7430 100644 --- a/package/gitea/source/modules/git/repo_commitgraph.go +++ b/package/gitea/source/modules/git/repo_commitgraph.go @@ -14,7 +14,7 @@ import ( // this requires git v2.18 to be installed func WriteCommitGraph(ctx context.Context, repoPath string) error { if DefaultFeatures().CheckVersionAtLeast("2.18") { - if _, _, err := gitcmd.NewCommand("commit-graph", "write").RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}); err != nil { + if _, _, err := gitcmd.NewCommand("commit-graph", "write").WithDir(repoPath).RunStdString(ctx); err != nil { return fmt.Errorf("unable to write commit-graph for '%s' : %w", repoPath, err) } } diff --git a/package/gitea/source/modules/git/repo_compare.go b/package/gitea/source/modules/git/repo_compare.go index 69a7314d..aa25e2ec 100644 --- a/package/gitea/source/modules/git/repo_compare.go +++ b/package/gitea/source/modules/git/repo_compare.go @@ -7,38 +7,17 @@ package git import ( "bufio" "bytes" - "context" "errors" "fmt" "io" "os" "path/filepath" "regexp" - "strconv" "strings" "code.gitea.io/gitea/modules/git/gitcmd" ) -// GetMergeBase checks and returns merge base of two branches and the reference used as base. -func (repo *Repository) GetMergeBase(tmpRemote, base, head string) (string, string, error) { - if tmpRemote == "" { - tmpRemote = "origin" - } - - if tmpRemote != "origin" { - tmpBaseName := RemotePrefix + tmpRemote + "/tmp_" + base - // Fetch commit into a temporary branch in order to be able to handle commits and tags - _, _, err := gitcmd.NewCommand("fetch", "--no-tags").AddDynamicArguments(tmpRemote).AddDashesAndList(base+":"+tmpBaseName).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - if err == nil { - base = tmpBaseName - } - } - - stdout, _, err := gitcmd.NewCommand("merge-base").AddDashesAndList(base, head).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - return strings.TrimSpace(stdout), base, err -} - type lineCountWriter struct { numLines int } @@ -55,7 +34,6 @@ func (l *lineCountWriter) Write(p []byte) (n int, err error) { func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparison bool) (int, error) { // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly w := &lineCountWriter{} - stderr := new(bytes.Buffer) separator := "..." if directComparison { @@ -63,109 +41,55 @@ func (repo *Repository) GetDiffNumChangedFiles(base, head string, directComparis } // avoid: ambiguous argument 'refs/a...refs/b': unknown revision or path not in the working tree. Use '--': 'git [...] -- [...]' - if err := gitcmd.NewCommand("diff", "-z", "--name-only").AddDynamicArguments(base+separator+head).AddArguments("--"). - Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }); err != nil { - if strings.Contains(stderr.String(), "no merge base") { + if err := gitcmd.NewCommand("diff", "-z", "--name-only"). + AddDynamicArguments(base + separator + head). + AddArguments("--"). + WithDir(repo.Path). + WithStdoutCopy(w). + RunWithStderr(repo.Ctx); err != nil { + if strings.Contains(err.Stderr(), "no merge base") { // git >= 2.28 now returns an error if base and head have become unrelated. // previously it would return the results of git diff -z --name-only base head so let's try that... w = &lineCountWriter{} - stderr.Reset() - if err = gitcmd.NewCommand("diff", "-z", "--name-only").AddDynamicArguments(base, head).AddArguments("--").Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }); err == nil { + if err = gitcmd.NewCommand("diff", "-z", "--name-only"). + AddDynamicArguments(base, head). + AddArguments("--"). + WithDir(repo.Path). + WithStdoutCopy(w). + RunWithStderr(repo.Ctx); err == nil { return w.numLines, nil } } - return 0, fmt.Errorf("%w: Stderr: %s", err, stderr) + return 0, err } return w.numLines, nil } -// GetDiffShortStatByCmdArgs counts number of changed files, number of additions and deletions -// TODO: it can be merged with another "GetDiffShortStat" in the future -func GetDiffShortStatByCmdArgs(ctx context.Context, repoPath string, trustedArgs gitcmd.TrustedCmdArgs, dynamicArgs ...string) (numFiles, totalAdditions, totalDeletions int, err error) { - // Now if we call: - // $ git diff --shortstat 1ebb35b98889ff77299f24d82da426b434b0cca0...788b8b1440462d477f45b0088875 - // we get: - // " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n" - cmd := gitcmd.NewCommand("diff", "--shortstat").AddArguments(trustedArgs...).AddDynamicArguments(dynamicArgs...) - stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}) - if err != nil { - return 0, 0, 0, err - } - - return parseDiffStat(stdout) -} - -var shortStatFormat = regexp.MustCompile( - `\s*(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?`) - var patchCommits = regexp.MustCompile(`^From\s(\w+)\s`) -func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int, err error) { - if len(stdout) == 0 || stdout == "\n" { - return 0, 0, 0, nil - } - groups := shortStatFormat.FindStringSubmatch(stdout) - if len(groups) != 4 { - return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s groups: %s", stdout, groups) - } - - numFiles, err = strconv.Atoi(groups[1]) - if err != nil { - return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumFiles %w", stdout, err) - } - - if len(groups[2]) != 0 { - totalAdditions, err = strconv.Atoi(groups[2]) - if err != nil { - return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumAdditions %w", stdout, err) - } - } - - if len(groups[3]) != 0 { - totalDeletions, err = strconv.Atoi(groups[3]) - if err != nil { - return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumDeletions %w", stdout, err) - } - } - return numFiles, totalAdditions, totalDeletions, err -} - // GetDiff generates and returns patch data between given revisions, optimized for human readability func (repo *Repository) GetDiff(compareArg string, w io.Writer) error { - stderr := new(bytes.Buffer) return gitcmd.NewCommand("diff", "-p").AddDynamicArguments(compareArg). - Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }) + WithDir(repo.Path). + WithStdoutCopy(w). + Run(repo.Ctx) } // GetDiffBinary generates and returns patch data between given revisions, including binary diffs. func (repo *Repository) GetDiffBinary(compareArg string, w io.Writer) error { - return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram").AddDynamicArguments(compareArg).Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - }) + return gitcmd.NewCommand("diff", "-p", "--binary", "--histogram"). + AddDynamicArguments(compareArg). + WithDir(repo.Path). + WithStdoutCopy(w). + Run(repo.Ctx) } // GetPatch generates and returns format-patch data between given revisions, able to be used with `git apply` func (repo *Repository) GetPatch(compareArg string, w io.Writer) error { - stderr := new(bytes.Buffer) return gitcmd.NewCommand("format-patch", "--binary", "--stdout").AddDynamicArguments(compareArg). - Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: w, - Stderr: stderr, - }) + WithDir(repo.Path). + WithStdoutCopy(w). + Run(repo.Ctx) } // GetFilesChangedBetween returns a list of all files that have been changed between the given commits @@ -182,7 +106,7 @@ func (repo *Repository) GetFilesChangedBetween(base, head string) ([]string, err } else { cmd.AddDynamicArguments(base, head) } - stdout, _, err := cmd.RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.WithDir(repo.Path).RunStdString(repo.Ctx) if err != nil { return nil, err } diff --git a/package/gitea/source/modules/git/repo_compare_test.go b/package/gitea/source/modules/git/repo_compare_test.go index 47fd2ca1..bf16b7cf 100644 --- a/package/gitea/source/modules/git/repo_compare_test.go +++ b/package/gitea/source/modules/git/repo_compare_test.go @@ -103,7 +103,8 @@ func TestReadWritePullHead(t *testing.T) { newCommit := "feaf4ba6bc635fec442f46ddd4512416ec43c2c2" _, _, err = gitcmd.NewCommand("update-ref"). AddDynamicArguments(PullPrefix+"1/head", newCommit). - RunStdString(t.Context(), &gitcmd.RunOpts{Dir: repo.Path}) + WithDir(repo.Path). + RunStdString(t.Context()) if err != nil { assert.NoError(t, err) return @@ -121,8 +122,9 @@ func TestReadWritePullHead(t *testing.T) { // Remove file after the test _, _, err = gitcmd.NewCommand("update-ref", "--no-deref", "-d"). - AddDynamicArguments(PullPrefix+"1/head"). - RunStdString(t.Context(), &gitcmd.RunOpts{Dir: repo.Path}) + AddDynamicArguments(PullPrefix + "1/head"). + WithDir(repo.Path). + RunStdString(t.Context()) assert.NoError(t, err) } diff --git a/package/gitea/source/modules/git/repo_gpg.go b/package/gitea/source/modules/git/repo_gpg.go deleted file mode 100644 index a999d2db..00000000 --- a/package/gitea/source/modules/git/repo_gpg.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "fmt" - "os" - "strings" - - "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/process" -) - -// LoadPublicKeyContent will load the key from gpg -func (gpgSettings *GPGSettings) LoadPublicKeyContent() error { - if gpgSettings.Format == SigningKeyFormatSSH { - content, err := os.ReadFile(gpgSettings.KeyID) - if err != nil { - return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err) - } - gpgSettings.PublicKeyContent = string(content) - return nil - } - content, stderr, err := process.GetManager().Exec( - "gpg -a --export", - "gpg", "-a", "--export", gpgSettings.KeyID) - if err != nil { - return fmt.Errorf("unable to get default signing key: %s, %s, %w", gpgSettings.KeyID, stderr, err) - } - gpgSettings.PublicKeyContent = content - return nil -} - -// GetDefaultPublicGPGKey will return and cache the default public GPG settings for this repository -func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) { - if repo.gpgSettings != nil && !forceUpdate { - return repo.gpgSettings, nil - } - - gpgSettings := &GPGSettings{ - Sign: true, - } - - value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - sign, valid := ParseBool(strings.TrimSpace(value)) - if !sign || !valid { - gpgSettings.Sign = false - repo.gpgSettings = gpgSettings - return gpgSettings, nil - } - - signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - gpgSettings.KeyID = strings.TrimSpace(signingKey) - - format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - gpgSettings.Format = strings.TrimSpace(format) - - defaultEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - gpgSettings.Email = strings.TrimSpace(defaultEmail) - - defaultName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) - gpgSettings.Name = strings.TrimSpace(defaultName) - - if err := gpgSettings.LoadPublicKeyContent(); err != nil { - return nil, err - } - repo.gpgSettings = gpgSettings - return repo.gpgSettings, nil -} diff --git a/package/gitea/source/modules/git/repo_index.go b/package/gitea/source/modules/git/repo_index.go index e7b3792d..1d040d5e 100644 --- a/package/gitea/source/modules/git/repo_index.go +++ b/package/gitea/source/modules/git/repo_index.go @@ -22,7 +22,7 @@ func (repo *Repository) ReadTreeToIndex(treeish string, indexFilename ...string) } if len(treeish) != objectFormat.FullLength() { - res, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(treeish).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + res, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(treeish).WithDir(repo.Path).RunStdString(repo.Ctx) if err != nil { return err } @@ -42,7 +42,7 @@ func (repo *Repository) readTreeToIndex(id ObjectID, indexFilename ...string) er if len(indexFilename) > 0 { env = append(os.Environ(), "GIT_INDEX_FILE="+indexFilename[0]) } - _, _, err := gitcmd.NewCommand("read-tree").AddDynamicArguments(id.String()).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path, Env: env}) + _, _, err := gitcmd.NewCommand("read-tree").AddDynamicArguments(id.String()).WithDir(repo.Path).WithEnv(env).RunStdString(repo.Ctx) if err != nil { return err } @@ -75,14 +75,14 @@ func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (tmpIndexFilena // EmptyIndex empties the index func (repo *Repository) EmptyIndex() error { - _, _, err := gitcmd.NewCommand("read-tree", "--empty").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := gitcmd.NewCommand("read-tree", "--empty").WithDir(repo.Path).RunStdString(repo.Ctx) return err } // LsFiles checks if the given filenames are in the index func (repo *Repository) LsFiles(filenames ...string) ([]string, error) { cmd := gitcmd.NewCommand("ls-files", "-z").AddDashesAndList(filenames...) - res, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + res, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil { return nil, err } @@ -101,21 +101,17 @@ func (repo *Repository) RemoveFilesFromIndex(filenames ...string) error { return err } cmd := gitcmd.NewCommand("update-index", "--remove", "-z", "--index-info") - stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) - buffer := new(bytes.Buffer) + input := new(bytes.Buffer) for _, file := range filenames { if file != "" { // using format: mode SP type SP sha1 TAB path - buffer.WriteString("0 blob " + objectFormat.EmptyObjectID().String() + "\t" + file + "\000") + input.WriteString("0 blob " + objectFormat.EmptyObjectID().String() + "\t" + file + "\000") } } - return cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdin: bytes.NewReader(buffer.Bytes()), - Stdout: stdout, - Stderr: stderr, - }) + return cmd. + WithDir(repo.Path). + WithStdinBytes(input.Bytes()). + RunWithStderr(repo.Ctx) } type IndexObjectInfo struct { @@ -127,19 +123,15 @@ type IndexObjectInfo struct { // AddObjectsToIndex adds the provided object hashes to the index at the provided filenames func (repo *Repository) AddObjectsToIndex(objects ...IndexObjectInfo) error { cmd := gitcmd.NewCommand("update-index", "--add", "--replace", "-z", "--index-info") - stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) - buffer := new(bytes.Buffer) + input := new(bytes.Buffer) for _, object := range objects { // using format: mode SP type SP sha1 TAB path - buffer.WriteString(object.Mode + " blob " + object.Object.String() + "\t" + object.Filename + "\000") + input.WriteString(object.Mode + " blob " + object.Object.String() + "\t" + object.Filename + "\000") } - return cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdin: bytes.NewReader(buffer.Bytes()), - Stdout: stdout, - Stderr: stderr, - }) + return cmd. + WithDir(repo.Path). + WithStdinBytes(input.Bytes()). + RunWithStderr(repo.Ctx) } // AddObjectToIndex adds the provided object hash to the index at the provided filename @@ -149,7 +141,7 @@ func (repo *Repository) AddObjectToIndex(mode string, object ObjectID, filename // WriteTree writes the current index as a tree to the object db and returns its hash func (repo *Repository) WriteTree() (*Tree, error) { - stdout, _, runErr := gitcmd.NewCommand("write-tree").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, runErr := gitcmd.NewCommand("write-tree").WithDir(repo.Path).RunStdString(repo.Ctx) if runErr != nil { return nil, runErr } diff --git a/package/gitea/source/modules/git/repo_object.go b/package/gitea/source/modules/git/repo_object.go index e8f6510c..38e16b46 100644 --- a/package/gitea/source/modules/git/repo_object.go +++ b/package/gitea/source/modules/git/repo_object.go @@ -5,8 +5,6 @@ package git import ( - "bytes" - "io" "strings" "code.gitea.io/gitea/modules/git/gitcmd" @@ -33,18 +31,12 @@ func (o ObjectType) Bytes() []byte { return []byte(o) } -type EmptyReader struct{} - -func (EmptyReader) Read(p []byte) (int, error) { - return 0, io.EOF -} - func (repo *Repository) GetObjectFormat() (ObjectFormat, error) { if repo != nil && repo.objectFormat != nil { return repo.objectFormat, nil } - str, err := repo.hashObject(EmptyReader{}, false) + str, err := repo.hashObjectBytes(nil, false) if err != nil { return nil, err } @@ -58,32 +50,28 @@ func (repo *Repository) GetObjectFormat() (ObjectFormat, error) { return repo.objectFormat, nil } -// HashObject takes a reader and returns hash for that reader -func (repo *Repository) HashObject(reader io.Reader) (ObjectID, error) { - idStr, err := repo.hashObject(reader, true) +// HashObjectBytes returns hash for the content +func (repo *Repository) HashObjectBytes(buf []byte) (ObjectID, error) { + idStr, err := repo.hashObjectBytes(buf, true) if err != nil { return nil, err } return NewIDFromString(idStr) } -func (repo *Repository) hashObject(reader io.Reader, save bool) (string, error) { +func (repo *Repository) hashObjectBytes(buf []byte, save bool) (string, error) { var cmd *gitcmd.Command if save { cmd = gitcmd.NewCommand("hash-object", "-w", "--stdin") } else { cmd = gitcmd.NewCommand("hash-object", "--stdin") } - stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) - err := cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdin: reader, - Stdout: stdout, - Stderr: stderr, - }) + stdout, _, err := cmd. + WithDir(repo.Path). + WithStdinBytes(buf). + RunStdString(repo.Ctx) if err != nil { return "", err } - return strings.TrimSpace(stdout.String()), nil + return strings.TrimSpace(stdout), nil } diff --git a/package/gitea/source/modules/git/repo_ref.go b/package/gitea/source/modules/git/repo_ref.go index 577e17c4..8859a93a 100644 --- a/package/gitea/source/modules/git/repo_ref.go +++ b/package/gitea/source/modules/git/repo_ref.go @@ -28,7 +28,8 @@ func (repo *Repository) ListOccurrences(ctx context.Context, refType, commitSHA default: return nil, util.NewInvalidArgumentErrorf(`can only use "branch" or "tag" for refType, but got %q`, refType) } - stdout, _, err := cmd.AddArguments("--no-color", "--sort=-creatordate", "--contains").AddDynamicArguments(commitSHA).RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := cmd.AddArguments("--no-color", "--sort=-creatordate", "--contains"). + AddDynamicArguments(commitSHA).WithDir(repo.Path).RunStdString(ctx) if err != nil { return nil, err } diff --git a/package/gitea/source/modules/git/repo_ref_nogogit.go b/package/gitea/source/modules/git/repo_ref_nogogit.go index 784efecc..c58992fa 100644 --- a/package/gitea/source/modules/git/repo_ref_nogogit.go +++ b/package/gitea/source/modules/git/repo_ref_nogogit.go @@ -15,75 +15,61 @@ import ( // GetRefsFiltered returns all references of the repository that matches patterm exactly or starting with. func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) { - stdoutReader, stdoutWriter := io.Pipe() - defer func() { - _ = stdoutReader.Close() - _ = stdoutWriter.Close() - }() - - go func() { - stderrBuilder := &strings.Builder{} - err := gitcmd.NewCommand("for-each-ref").Run(repo.Ctx, &gitcmd.RunOpts{ - Dir: repo.Path, - Stdout: stdoutWriter, - Stderr: stderrBuilder, - }) - if err != nil { - _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, stderrBuilder.String())) - } else { - _ = stdoutWriter.Close() - } - }() - refs := make([]*Reference, 0) - bufReader := bufio.NewReader(stdoutReader) - for { - // The output of for-each-ref is simply a list: - // SP TAB LF - sha, err := bufReader.ReadString(' ') - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - sha = sha[:len(sha)-1] + cmd := gitcmd.NewCommand("for-each-ref") + stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() + defer stdoutReaderClose() + err := cmd.WithDir(repo.Path). + WithPipelineFunc(func(context gitcmd.Context) error { + bufReader := bufio.NewReader(stdoutReader) + for { + // The output of for-each-ref is simply a list: + // SP TAB LF + sha, err := bufReader.ReadString(' ') + if err == io.EOF { + break + } + if err != nil { + return err + } + sha = sha[:len(sha)-1] - typ, err := bufReader.ReadString('\t') - if err == io.EOF { - // This should not happen, but we'll tolerate it - break - } - if err != nil { - return nil, err - } - typ = typ[:len(typ)-1] + typ, err := bufReader.ReadString('\t') + if err == io.EOF { + // This should not happen, but we'll tolerate it + break + } + if err != nil { + return err + } + typ = typ[:len(typ)-1] - refName, err := bufReader.ReadString('\n') - if err == io.EOF { - // This should not happen, but we'll tolerate it - break - } - if err != nil { - return nil, err - } - refName = refName[:len(refName)-1] + refName, err := bufReader.ReadString('\n') + if err == io.EOF { + // This should not happen, but we'll tolerate it + break + } + if err != nil { + return err + } + refName = refName[:len(refName)-1] - // refName cannot be HEAD but can be remotes or stash - if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" { - continue - } + // refName cannot be HEAD but can be remotes or stash + if strings.HasPrefix(refName, RemotePrefix) || refName == "/refs/stash" { + continue + } - if pattern == "" || strings.HasPrefix(refName, pattern) { - r := &Reference{ - Name: refName, - Object: MustIDFromString(sha), - Type: typ, - repo: repo, + if pattern == "" || strings.HasPrefix(refName, pattern) { + r := &Reference{ + Name: refName, + Object: MustIDFromString(sha), + Type: typ, + repo: repo, + } + refs = append(refs, r) + } } - refs = append(refs, r) - } - } - - return refs, nil + return nil + }).RunWithStderr(repo.Ctx) + return refs, err } diff --git a/package/gitea/source/modules/git/repo_stats.go b/package/gitea/source/modules/git/repo_stats.go index 22082325..1dd77f05 100644 --- a/package/gitea/source/modules/git/repo_stats.go +++ b/package/gitea/source/modules/git/repo_stats.go @@ -5,9 +5,7 @@ package git import ( "bufio" - "context" "fmt" - "os" "sort" "strconv" "strings" @@ -43,7 +41,8 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) stdout, _, runErr := gitcmd.NewCommand("rev-list", "--count", "--no-merges", "--branches=*", "--date=iso"). AddOptionFormat("--since=%s", since). - RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + WithDir(repo.Path). + RunStdString(repo.Ctx) if runErr != nil { return nil, runErr } @@ -54,15 +53,6 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) } stats.CommitCountInAllBranches = c - stdoutReader, stdoutWriter, err := os.Pipe() - if err != nil { - return nil, err - } - defer func() { - _ = stdoutReader.Close() - _ = stdoutWriter.Close() - }() - gitCmd := gitcmd.NewCommand("log", "--numstat", "--no-merges", "--pretty=format:---%n%h%n%aN%n%aE%n", "--date=iso"). AddOptionFormat("--since=%s", since) if len(branch) == 0 { @@ -71,14 +61,11 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) gitCmd.AddArguments("--first-parent").AddDynamicArguments(branch) } - stderr := new(strings.Builder) - err = gitCmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Env: []string{}, - Dir: repo.Path, - Stdout: stdoutWriter, - Stderr: stderr, - PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { - _ = stdoutWriter.Close() + stdoutReader, stdoutReaderClose := gitCmd.MakeStdoutPipe() + defer stdoutReaderClose() + err = gitCmd. + WithDir(repo.Path). + WithPipelineFunc(func(ctx gitcmd.Context) error { scanner := bufio.NewScanner(stdoutReader) scanner.Split(bufio.ScanLines) stats.CommitCount = 0 @@ -129,7 +116,6 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) } } if err = scanner.Err(); err != nil { - _ = stdoutReader.Close() return fmt.Errorf("GetCodeActivityStats scan: %w", err) } a := make([]*CodeActivityAuthor, 0, len(authors)) @@ -143,12 +129,11 @@ func (repo *Repository) GetCodeActivityStats(fromTime time.Time, branch string) stats.AuthorCount = int64(len(authors)) stats.ChangedFiles = int64(len(files)) stats.Authors = a - _ = stdoutReader.Close() return nil - }, - }) + }). + RunWithStderr(repo.Ctx) if err != nil { - return nil, fmt.Errorf("Failed to get GetCodeActivityStats for repository.\nError: %w\nStderr: %s", err, stderr) + return nil, fmt.Errorf("GetCodeActivityStats: %w", err) } return stats, nil diff --git a/package/gitea/source/modules/git/repo_tag.go b/package/gitea/source/modules/git/repo_tag.go index 0cb09324..2599236a 100644 --- a/package/gitea/source/modules/git/repo_tag.go +++ b/package/gitea/source/modules/git/repo_tag.go @@ -6,7 +6,6 @@ package git import ( "fmt" - "io" "strings" "code.gitea.io/gitea/modules/git/foreachref" @@ -19,13 +18,17 @@ const TagPrefix = "refs/tags/" // CreateTag create one tag in the repository func (repo *Repository) CreateTag(name, revision string) error { - _, _, err := gitcmd.NewCommand("tag").AddDashesAndList(name, revision).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := gitcmd.NewCommand("tag").AddDashesAndList(name, revision).WithDir(repo.Path).RunStdString(repo.Ctx) return err } // CreateAnnotatedTag create one annotated tag in the repository func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error { - _, _, err := gitcmd.NewCommand("tag", "-a", "-m").AddDynamicArguments(message).AddDashesAndList(name, revision).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + _, _, err := gitcmd.NewCommand("tag", "-a", "-m"). + AddDynamicArguments(message). + AddDashesAndList(name, revision). + WithDir(repo.Path). + RunStdString(repo.Ctx) return err } @@ -35,7 +38,7 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { return "", fmt.Errorf("SHA is too short: %s", sha) } - stdout, _, err := gitcmd.NewCommand("show-ref", "--tags", "-d").RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("show-ref", "--tags", "-d").WithDir(repo.Path).RunStdString(repo.Ctx) if err != nil { return "", err } @@ -58,7 +61,7 @@ func (repo *Repository) GetTagNameBySHA(sha string) (string, error) { // GetTagID returns the object ID for a tag (annotated tags have both an object SHA AND a commit SHA) func (repo *Repository) GetTagID(name string) (string, error) { - stdout, _, err := gitcmd.NewCommand("show-ref", "--tags").AddDashesAndList(name).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + stdout, _, err := gitcmd.NewCommand("show-ref", "--tags").AddDashesAndList(name).WithDir(repo.Path).RunStdString(repo.Ctx) if err != nil { return "", err } @@ -111,48 +114,42 @@ func (repo *Repository) GetTagInfos(page, pageSize int) ([]*Tag, int, error) { // https://git-scm.com/docs/git-for-each-ref#Documentation/git-for-each-ref.txt-refname forEachRefFmt := foreachref.NewFormat("objecttype", "refname:lstrip=2", "object", "objectname", "creator", "contents", "contents:signature") - stdoutReader, stdoutWriter := io.Pipe() - defer stdoutReader.Close() - defer stdoutWriter.Close() - stderr := strings.Builder{} - rc := &gitcmd.RunOpts{Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr} - - go func() { - err := gitcmd.NewCommand("for-each-ref"). - AddOptionFormat("--format=%s", forEachRefFmt.Flag()). - AddArguments("--sort", "-*creatordate", "refs/tags").Run(repo.Ctx, rc) - if err != nil { - _ = stdoutWriter.CloseWithError(gitcmd.ConcatenateError(err, stderr.String())) - } else { - _ = stdoutWriter.Close() - } - }() - var tags []*Tag - parser := forEachRefFmt.Parser(stdoutReader) - for { - ref := parser.Next() - if ref == nil { - break - } + var tagsTotal int + cmd := gitcmd.NewCommand("for-each-ref") + stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() + defer stdoutReaderClose() + err := cmd.AddOptionFormat("--format=%s", forEachRefFmt.Flag()). + AddArguments("--sort", "-*creatordate", "refs/tags"). + WithDir(repo.Path). + WithPipelineFunc(func(context gitcmd.Context) error { + parser := forEachRefFmt.Parser(stdoutReader) + for { + ref := parser.Next() + if ref == nil { + break + } - tag, err := parseTagRef(ref) - if err != nil { - return nil, 0, fmt.Errorf("GetTagInfos: parse tag: %w", err) - } - tags = append(tags, tag) - } - if err := parser.Err(); err != nil { - return nil, 0, fmt.Errorf("GetTagInfos: parse output: %w", err) - } + tag, err := parseTagRef(ref) + if err != nil { + return fmt.Errorf("GetTagInfos: parse tag: %w", err) + } + tags = append(tags, tag) + } + if err := parser.Err(); err != nil { + return fmt.Errorf("GetTagInfos: parse output: %w", err) + } - sortTagsByTime(tags) - tagsTotal := len(tags) - if page != 0 { - tags = util.PaginateSlice(tags, page, pageSize).([]*Tag) - } + sortTagsByTime(tags) + tagsTotal = len(tags) + if page != 0 { + tags = util.PaginateSlice(tags, page, pageSize).([]*Tag) + } + return nil + }). + RunWithStderr(repo.Ctx) - return tags, tagsTotal, nil + return tags, tagsTotal, err } // parseTagRef parses a tag from a 'git for-each-ref'-produced reference. diff --git a/package/gitea/source/modules/git/repo_tag_nogogit.go b/package/gitea/source/modules/git/repo_tag_nogogit.go index 5f79b68a..a9ac0408 100644 --- a/package/gitea/source/modules/git/repo_tag_nogogit.go +++ b/package/gitea/source/modules/git/repo_tag_nogogit.go @@ -24,23 +24,19 @@ func (repo *Repository) IsTagExist(name string) bool { // GetTagType gets the type of the tag, either commit (simple) or tag (annotated) func (repo *Repository) GetTagType(id ObjectID) (string, error) { - wr, rd, cancel, err := repo.CatFileBatchCheck(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { return "", err } defer cancel() - _, err = wr.Write([]byte(id.String() + "\n")) - if err != nil { - return "", err - } - _, typ, _, err := ReadBatchLine(rd) + info, err := batch.QueryInfo(id.String()) if err != nil { if IsErrNotExist(err) { return "", ErrNotExist{ID: id.String()} } return "", err } - return typ, nil + return info.Type, nil } func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { @@ -88,22 +84,20 @@ func (repo *Repository) getTag(tagID ObjectID, name string) (*Tag, error) { } // The tag is an annotated tag with a message. - wr, rd, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { return nil, err } defer cancel() - if _, err := wr.Write([]byte(tagID.String() + "\n")); err != nil { - return nil, err - } - _, typ, size, err := ReadBatchLine(rd) + info, rd, err := batch.QueryContent(tagID.String()) if err != nil { if errors.Is(err, io.EOF) || IsErrNotExist(err) { return nil, ErrNotExist{ID: tagID.String()} } return nil, err } + typ, size := info.Type, info.Size if typ != "tag" { if err := DiscardFull(rd, size+1); err != nil { return nil, err diff --git a/package/gitea/source/modules/git/repo_test.go b/package/gitea/source/modules/git/repo_test.go index 813844d1..776c297a 100644 --- a/package/gitea/source/modules/git/repo_test.go +++ b/package/gitea/source/modules/git/repo_test.go @@ -10,16 +10,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestGetLatestCommitTime(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - lct, err := GetLatestCommitTime(t.Context(), bareRepo1Path) - assert.NoError(t, err) - // Time is Sun Nov 13 16:40:14 2022 +0100 - // which is the time of commit - // ce064814f4a0d337b333e646ece456cd39fab612 (refs/heads/master) - assert.EqualValues(t, 1668354014, lct.Unix()) -} - func TestRepoIsEmpty(t *testing.T) { emptyRepo2Path := filepath.Join(testReposDir, "repo2_empty") repo, err := OpenRepository(t.Context(), emptyRepo2Path) @@ -29,27 +19,3 @@ func TestRepoIsEmpty(t *testing.T) { assert.NoError(t, err) assert.True(t, isEmpty) } - -func TestRepoGetDivergingCommits(t *testing.T) { - bareRepo1Path := filepath.Join(testReposDir, "repo1_bare") - do, err := GetDivergingCommits(t.Context(), bareRepo1Path, "master", "branch2") - assert.NoError(t, err) - assert.Equal(t, DivergeObject{ - Ahead: 1, - Behind: 5, - }, do) - - do, err = GetDivergingCommits(t.Context(), bareRepo1Path, "master", "master") - assert.NoError(t, err) - assert.Equal(t, DivergeObject{ - Ahead: 0, - Behind: 0, - }, do) - - do, err = GetDivergingCommits(t.Context(), bareRepo1Path, "master", "test") - assert.NoError(t, err) - assert.Equal(t, DivergeObject{ - Ahead: 0, - Behind: 2, - }, do) -} diff --git a/package/gitea/source/modules/git/repo_tree.go b/package/gitea/source/modules/git/repo_tree.go index 1d8c9409..e65e2441 100644 --- a/package/gitea/source/modules/git/repo_tree.go +++ b/package/gitea/source/modules/git/repo_tree.go @@ -58,17 +58,12 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt cmd.AddArguments("--no-gpg-sign") } - stdout := new(bytes.Buffer) - stderr := new(bytes.Buffer) - err := cmd.Run(repo.Ctx, &gitcmd.RunOpts{ - Env: env, - Dir: repo.Path, - Stdin: messageBytes, - Stdout: stdout, - Stderr: stderr, - }) + stdout, _, err := cmd.WithEnv(env). + WithDir(repo.Path). + WithStdinBytes(messageBytes.Bytes()). + RunStdString(repo.Ctx) if err != nil { - return nil, gitcmd.ConcatenateError(err, stderr.String()) + return nil, err } - return NewIDFromString(strings.TrimSpace(stdout.String())) + return NewIDFromString(strings.TrimSpace(stdout)) } diff --git a/package/gitea/source/modules/git/repo_tree_gogit.go b/package/gitea/source/modules/git/repo_tree_gogit.go index 40524d0c..89d34e87 100644 --- a/package/gitea/source/modules/git/repo_tree_gogit.go +++ b/package/gitea/source/modules/git/repo_tree_gogit.go @@ -26,7 +26,7 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { } tree := NewTree(repo, id) - tree.gogitTree = gogitTree + tree.resolvedGogitTreeObject = gogitTree return tree, nil } @@ -38,7 +38,10 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) { } if len(idStr) != objectFormat.FullLength() { - res, _, err := gitcmd.NewCommand("rev-parse", "--verify").AddDynamicArguments(idStr).RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + res, _, err := gitcmd.NewCommand("rev-parse", "--verify"). + AddDynamicArguments(idStr). + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil { return nil, err } diff --git a/package/gitea/source/modules/git/repo_tree_nogogit.go b/package/gitea/source/modules/git/repo_tree_nogogit.go index 1954f851..82a61072 100644 --- a/package/gitea/source/modules/git/repo_tree_nogogit.go +++ b/package/gitea/source/modules/git/repo_tree_nogogit.go @@ -10,24 +10,21 @@ import ( ) func (repo *Repository) getTree(id ObjectID) (*Tree, error) { - wr, rd, cancel, err := repo.CatFileBatch(repo.Ctx) + batch, cancel, err := repo.CatFileBatch(repo.Ctx) if err != nil { return nil, err } defer cancel() - _, _ = wr.Write([]byte(id.String() + "\n")) - - // ignore the SHA - _, typ, size, err := ReadBatchLine(rd) + info, rd, err := batch.QueryContent(id.String()) if err != nil { return nil, err } - switch typ { + switch info.Type { case "tag": resolvedID := id - data, err := io.ReadAll(io.LimitReader(rd, size)) + data, err := io.ReadAll(io.LimitReader(rd, info.Size)) if err != nil { return nil, err } @@ -36,17 +33,14 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { return nil, err } - if _, err := wr.Write([]byte(tag.Object.String() + "\n")); err != nil { - return nil, err - } - commit, err := repo.getCommitFromBatchReader(wr, rd, tag.Object) + commit, err := repo.getCommitWithBatch(batch, tag.Object) if err != nil { return nil, err } commit.Tree.ResolvedID = resolvedID return &commit.Tree, nil case "commit": - commit, err := CommitFromReader(repo, id, io.LimitReader(rd, size)) + commit, err := CommitFromReader(repo, id, io.LimitReader(rd, info.Size)) if err != nil { return nil, err } @@ -62,14 +56,14 @@ func (repo *Repository) getTree(id ObjectID) (*Tree, error) { if err != nil { return nil, err } - tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, size) + tree.entries, err = catBatchParseTreeEntries(objectFormat, tree, rd, info.Size) if err != nil { return nil, err } tree.entriesParsed = true return tree, nil default: - if err := DiscardFull(rd, size+1); err != nil { + if err := DiscardFull(rd, info.Size+1); err != nil { return nil, err } return nil, ErrNotExist{ diff --git a/package/gitea/source/modules/git/submodule.go b/package/gitea/source/modules/git/submodule.go index 58824adc..ed69cbe5 100644 --- a/package/gitea/source/modules/git/submodule.go +++ b/package/gitea/source/modules/git/submodule.go @@ -7,7 +7,6 @@ import ( "bufio" "context" "fmt" - "os" "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/log" @@ -21,22 +20,15 @@ type TemplateSubmoduleCommit struct { // GetTemplateSubmoduleCommits returns a list of submodules paths and their commits from a repository // This function is only for generating new repos based on existing template, the template couldn't be too large. func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submoduleCommits []TemplateSubmoduleCommit, _ error) { - stdoutReader, stdoutWriter, err := os.Pipe() - if err != nil { - return nil, err - } - opts := &gitcmd.RunOpts{ - Dir: repoPath, - Stdout: stdoutWriter, - PipelineFunc: func(ctx context.Context, cancel context.CancelFunc) error { - _ = stdoutWriter.Close() - defer stdoutReader.Close() - + cmd := gitcmd.NewCommand("ls-tree", "-r", "--", "HEAD") + stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() + defer stdoutReaderClose() + err := cmd.WithDir(repoPath). + WithPipelineFunc(func(ctx gitcmd.Context) error { scanner := bufio.NewScanner(stdoutReader) for scanner.Scan() { entry, err := parseLsTreeLine(scanner.Bytes()) if err != nil { - cancel() return err } if entry.EntryMode == EntryModeCommit { @@ -44,9 +36,8 @@ func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submodul } } return scanner.Err() - }, - } - err = gitcmd.NewCommand("ls-tree", "-r", "--", "HEAD").Run(ctx, opts) + }). + Run(ctx) if err != nil { return nil, fmt.Errorf("GetTemplateSubmoduleCommits: error running git ls-tree: %v", err) } @@ -58,7 +49,7 @@ func GetTemplateSubmoduleCommits(ctx context.Context, repoPath string) (submodul func AddTemplateSubmoduleIndexes(ctx context.Context, repoPath string, submodules []TemplateSubmoduleCommit) error { for _, submodule := range submodules { cmd := gitcmd.NewCommand("update-index", "--add", "--cacheinfo", "160000").AddDynamicArguments(submodule.Commit, submodule.Path) - if stdout, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath}); err != nil { + if stdout, _, err := cmd.WithDir(repoPath).RunStdString(ctx); err != nil { log.Error("Unable to add %s as submodule to repo %s: stdout %s\nError: %v", submodule.Path, repoPath, stdout, err) return err } diff --git a/package/gitea/source/modules/git/submodule_test.go b/package/gitea/source/modules/git/submodule_test.go index d2df8b2a..22bd5bf7 100644 --- a/package/gitea/source/modules/git/submodule_test.go +++ b/package/gitea/source/modules/git/submodule_test.go @@ -32,14 +32,14 @@ func TestAddTemplateSubmoduleIndexes(t *testing.T) { ctx := t.Context() tmpDir := t.TempDir() var err error - _, _, err = gitcmd.NewCommand("init").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmpDir}) + _, _, err = gitcmd.NewCommand("init").WithDir(tmpDir).RunStdString(ctx) require.NoError(t, err) _ = os.Mkdir(filepath.Join(tmpDir, "new-dir"), 0o755) err = AddTemplateSubmoduleIndexes(ctx, tmpDir, []TemplateSubmoduleCommit{{Path: "new-dir", Commit: "1234567890123456789012345678901234567890"}}) require.NoError(t, err) - _, _, err = gitcmd.NewCommand("add", "--all").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmpDir}) + _, _, err = gitcmd.NewCommand("add", "--all").WithDir(tmpDir).RunStdString(ctx) require.NoError(t, err) - _, _, err = gitcmd.NewCommand("-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").RunStdString(ctx, &gitcmd.RunOpts{Dir: tmpDir}) + _, _, err = gitcmd.NewCommand("-c", "user.name=a", "-c", "user.email=b", "commit", "-m=test").WithDir(tmpDir).RunStdString(ctx) require.NoError(t, err) submodules, err := GetTemplateSubmoduleCommits(t.Context(), tmpDir) require.NoError(t, err) diff --git a/package/gitea/source/modules/git/tree.go b/package/gitea/source/modules/git/tree.go index a8c4929c..c1898b20 100644 --- a/package/gitea/source/modules/git/tree.go +++ b/package/gitea/source/modules/git/tree.go @@ -11,11 +11,21 @@ import ( "code.gitea.io/gitea/modules/git/gitcmd" ) +type TreeCommon struct { + ID ObjectID + ResolvedID ObjectID + + repo *Repository + ptree *Tree // parent tree +} + // NewTree create a new tree according the repository and tree id func NewTree(repo *Repository, id ObjectID) *Tree { return &Tree{ - ID: id, - repo: repo, + TreeCommon: TreeCommon{ + ID: id, + repo: repo, + }, } } @@ -53,7 +63,7 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error cmd := gitcmd.NewCommand("ls-tree", "-z", "--name-only"). AddDashesAndList(append([]string{ref}, filenames...)...) - res, _, err := cmd.RunStdBytes(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + res, _, err := cmd.WithDir(repo.Path).RunStdBytes(repo.Ctx) if err != nil { return nil, err } @@ -69,7 +79,8 @@ func (repo *Repository) LsTree(ref string, filenames ...string) ([]string, error func (repo *Repository) GetTreePathLatestCommit(refName, treePath string) (*Commit, error) { stdout, _, err := gitcmd.NewCommand("rev-list", "-1"). AddDynamicArguments(refName).AddDashesAndList(treePath). - RunStdString(repo.Ctx, &gitcmd.RunOpts{Dir: repo.Path}) + WithDir(repo.Path). + RunStdString(repo.Ctx) if err != nil { return nil, err } diff --git a/package/gitea/source/modules/git/tree_blob_gogit.go b/package/gitea/source/modules/git/tree_blob_gogit.go index f29e8f8b..2c0ff0e1 100644 --- a/package/gitea/source/modules/git/tree_blob_gogit.go +++ b/package/gitea/source/modules/git/tree_blob_gogit.go @@ -11,22 +11,16 @@ import ( "strings" "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" ) // GetTreeEntryByPath get the tree entries according the sub dir func (t *Tree) GetTreeEntryByPath(relpath string) (*TreeEntry, error) { if len(relpath) == 0 { return &TreeEntry{ - ID: t.ID, - // Type: ObjectTree, - ptree: t, - gogitTreeEntry: &object.TreeEntry{ - Name: "", - Mode: filemode.Dir, - Hash: plumbing.Hash(t.ID.RawValue()), - }, + ID: t.ID, + ptree: t, + name: "", + entryMode: EntryModeTree, }, nil } diff --git a/package/gitea/source/modules/git/tree_entry.go b/package/gitea/source/modules/git/tree_entry.go index 5099d8ee..e7e4ea2d 100644 --- a/package/gitea/source/modules/git/tree_entry.go +++ b/package/gitea/source/modules/git/tree_entry.go @@ -6,12 +6,60 @@ package git import ( "path" - "sort" + "slices" "strings" "code.gitea.io/gitea/modules/util" ) +// TreeEntry the leaf in the git tree +type TreeEntry struct { + ID ObjectID + + name string + ptree *Tree + + entryMode EntryMode + + size int64 + sized bool +} + +// Name returns the name of the entry (base name) +func (te *TreeEntry) Name() string { + return te.name +} + +// Mode returns the mode of the entry +func (te *TreeEntry) Mode() EntryMode { + return te.entryMode +} + +// IsSubModule if the entry is a submodule +func (te *TreeEntry) IsSubModule() bool { + return te.entryMode.IsSubModule() +} + +// IsDir if the entry is a sub dir +func (te *TreeEntry) IsDir() bool { + return te.entryMode.IsDir() +} + +// IsLink if the entry is a symlink +func (te *TreeEntry) IsLink() bool { + return te.entryMode.IsLink() +} + +// IsRegular if the entry is a regular file +func (te *TreeEntry) IsRegular() bool { + return te.entryMode.IsRegular() +} + +// IsExecutable if the entry is an executable file (not necessarily binary) +func (te *TreeEntry) IsExecutable() bool { + return te.entryMode.IsExecutable() +} + // Type returns the type of the entry (commit, tree, blob) func (te *TreeEntry) Type() string { switch te.Mode() { @@ -109,49 +157,16 @@ func (te *TreeEntry) GetSubJumpablePathName() string { // Entries a list of entry type Entries []*TreeEntry -type customSortableEntries struct { - Comparer func(s1, s2 string) bool - Entries -} - -var sorter = []func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool{ - func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool { - return (t1.IsDir() || t1.IsSubModule()) && !t2.IsDir() && !t2.IsSubModule() - }, - func(t1, t2 *TreeEntry, cmp func(s1, s2 string) bool) bool { - return cmp(t1.Name(), t2.Name()) - }, -} - -func (ctes customSortableEntries) Len() int { return len(ctes.Entries) } - -func (ctes customSortableEntries) Swap(i, j int) { - ctes.Entries[i], ctes.Entries[j] = ctes.Entries[j], ctes.Entries[i] -} - -func (ctes customSortableEntries) Less(i, j int) bool { - t1, t2 := ctes.Entries[i], ctes.Entries[j] - var k int - for k = 0; k < len(sorter)-1; k++ { - s := sorter[k] - switch { - case s(t1, t2, ctes.Comparer): - return true - case s(t2, t1, ctes.Comparer): - return false - } - } - return sorter[k](t1, t2, ctes.Comparer) -} - -// Sort sort the list of entry -func (tes Entries) Sort() { - sort.Sort(customSortableEntries{func(s1, s2 string) bool { - return s1 < s2 - }, tes}) -} - // CustomSort customizable string comparing sort entry list -func (tes Entries) CustomSort(cmp func(s1, s2 string) bool) { - sort.Sort(customSortableEntries{cmp, tes}) +func (tes Entries) CustomSort(cmp func(s1, s2 string) int) { + slices.SortFunc(tes, func(a, b *TreeEntry) int { + s1Dir, s2Dir := a.IsDir() || a.IsSubModule(), b.IsDir() || b.IsSubModule() + if s1Dir != s2Dir { + if s1Dir { + return -1 + } + return 1 + } + return cmp(a.Name(), b.Name()) + }) } diff --git a/package/gitea/source/modules/git/tree_entry_gogit.go b/package/gitea/source/modules/git/tree_entry_gogit.go index 7af1c964..f092e706 100644 --- a/package/gitea/source/modules/git/tree_entry_gogit.go +++ b/package/gitea/source/modules/git/tree_entry_gogit.go @@ -7,34 +7,26 @@ package git import ( + "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/object" ) -// TreeEntry the leaf in the git tree -type TreeEntry struct { - ID ObjectID - - gogitTreeEntry *object.TreeEntry - ptree *Tree - - fullName string - - size int64 - sized bool +// gogitFileModeToEntryMode converts go-git filemode to EntryMode +func gogitFileModeToEntryMode(mode filemode.FileMode) EntryMode { + return EntryMode(mode) } -// Name returns the name of the entry -func (te *TreeEntry) Name() string { - if te.fullName != "" { - return te.fullName +func entryModeToGogitFileMode(mode EntryMode) filemode.FileMode { + return filemode.FileMode(mode) +} + +func (te *TreeEntry) toGogitTreeEntry() *object.TreeEntry { + return &object.TreeEntry{ + Name: te.name, + Mode: entryModeToGogitFileMode(te.entryMode), + Hash: plumbing.Hash(te.ID.RawValue()), } - return te.gogitTreeEntry.Name -} - -// Mode returns the mode of the entry -func (te *TreeEntry) Mode() EntryMode { - return EntryMode(te.gogitTreeEntry.Mode) } // Size returns the size of the entry @@ -45,7 +37,11 @@ func (te *TreeEntry) Size() int64 { return te.size } - file, err := te.ptree.gogitTree.TreeEntryFile(te.gogitTreeEntry) + ptreeGogitTree, err := te.ptree.gogitTreeObject() + if err != nil { + return 0 + } + file, err := ptreeGogitTree.TreeEntryFile(te.toGogitTreeEntry()) if err != nil { return 0 } @@ -55,35 +51,10 @@ func (te *TreeEntry) Size() int64 { return te.size } -// IsSubModule if the entry is a submodule -func (te *TreeEntry) IsSubModule() bool { - return te.gogitTreeEntry.Mode == filemode.Submodule -} - -// IsDir if the entry is a sub dir -func (te *TreeEntry) IsDir() bool { - return te.gogitTreeEntry.Mode == filemode.Dir -} - -// IsLink if the entry is a symlink -func (te *TreeEntry) IsLink() bool { - return te.gogitTreeEntry.Mode == filemode.Symlink -} - -// IsRegular if the entry is a regular file -func (te *TreeEntry) IsRegular() bool { - return te.gogitTreeEntry.Mode == filemode.Regular -} - -// IsExecutable if the entry is an executable file (not necessarily binary) -func (te *TreeEntry) IsExecutable() bool { - return te.gogitTreeEntry.Mode == filemode.Executable -} - // Blob returns the blob object the entry func (te *TreeEntry) Blob() *Blob { return &Blob{ - ID: ParseGogitHash(te.gogitTreeEntry.Hash), + ID: te.ID, repo: te.ptree.repo, name: te.Name(), } diff --git a/package/gitea/source/modules/git/tree_entry_gogit_test.go b/package/gitea/source/modules/git/tree_entry_gogit_test.go new file mode 100644 index 00000000..ed14b45e --- /dev/null +++ b/package/gitea/source/modules/git/tree_entry_gogit_test.go @@ -0,0 +1,27 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build gogit + +package git + +import ( + "testing" + + "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/stretchr/testify/assert" +) + +func TestEntryGogit(t *testing.T) { + cases := map[EntryMode]filemode.FileMode{ + EntryModeBlob: filemode.Regular, + EntryModeCommit: filemode.Submodule, + EntryModeExec: filemode.Executable, + EntryModeSymlink: filemode.Symlink, + EntryModeTree: filemode.Dir, + } + for emode, fmode := range cases { + assert.EqualValues(t, fmode, entryModeToGogitFileMode(emode)) + assert.EqualValues(t, emode, gogitFileModeToEntryMode(fmode)) + } +} diff --git a/package/gitea/source/modules/git/tree_entry_mode.go b/package/gitea/source/modules/git/tree_entry_mode.go index f36c07bc..2ceba113 100644 --- a/package/gitea/source/modules/git/tree_entry_mode.go +++ b/package/gitea/source/modules/git/tree_entry_mode.go @@ -4,7 +4,6 @@ package git import ( - "fmt" "strconv" ) @@ -55,21 +54,38 @@ func (e EntryMode) IsExecutable() bool { return e == EntryModeExec } -func ParseEntryMode(mode string) (EntryMode, error) { +func ParseEntryMode(mode string) EntryMode { switch mode { case "000000": - return EntryModeNoEntry, nil + return EntryModeNoEntry case "100644": - return EntryModeBlob, nil + return EntryModeBlob case "100755": - return EntryModeExec, nil + return EntryModeExec case "120000": - return EntryModeSymlink, nil + return EntryModeSymlink case "160000": - return EntryModeCommit, nil - case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons - return EntryModeTree, nil + return EntryModeCommit + case "040000": + return EntryModeTree default: - return 0, fmt.Errorf("unparsable entry mode: %s", mode) + // git uses 040000 for tree object, but some users may get 040755 from non-standard git implementations + m, _ := strconv.ParseInt(mode, 8, 32) + modeInt := EntryMode(m) + switch modeInt & 0o770000 { + case 0o040000: + return EntryModeTree + case 0o160000: + return EntryModeCommit + case 0o120000: + return EntryModeSymlink + case 0o100000: + if modeInt&0o777 == 0o755 { + return EntryModeExec + } + return EntryModeBlob + default: + return EntryModeNoEntry + } } } diff --git a/package/gitea/source/modules/git/tree_entry_nogogit.go b/package/gitea/source/modules/git/tree_entry_nogogit.go index 8fad96cd..0a19b38d 100644 --- a/package/gitea/source/modules/git/tree_entry_nogogit.go +++ b/package/gitea/source/modules/git/tree_entry_nogogit.go @@ -7,27 +7,6 @@ package git import "code.gitea.io/gitea/modules/log" -// TreeEntry the leaf in the git tree -type TreeEntry struct { - ID ObjectID - ptree *Tree - - entryMode EntryMode - name string - size int64 - sized bool -} - -// Name returns the name of the entry (base name) -func (te *TreeEntry) Name() string { - return te.name -} - -// Mode returns the mode of the entry -func (te *TreeEntry) Mode() EntryMode { - return te.entryMode -} - // Size returns the size of the entry func (te *TreeEntry) Size() int64 { if te.IsDir() { @@ -36,52 +15,23 @@ func (te *TreeEntry) Size() int64 { return te.size } - wr, rd, cancel, err := te.ptree.repo.CatFileBatchCheck(te.ptree.repo.Ctx) + batch, cancel, err := te.ptree.repo.CatFileBatch(te.ptree.repo.Ctx) if err != nil { log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err) return 0 } defer cancel() - _, err = wr.Write([]byte(te.ID.String() + "\n")) - if err != nil { - log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err) - return 0 - } - _, _, te.size, err = ReadBatchLine(rd) + info, err := batch.QueryInfo(te.ID.String()) if err != nil { log.Debug("error whilst reading size for %s in %s. Error: %v", te.ID.String(), te.ptree.repo.Path, err) return 0 } + te.size = info.Size te.sized = true return te.size } -// IsSubModule if the entry is a submodule -func (te *TreeEntry) IsSubModule() bool { - return te.entryMode.IsSubModule() -} - -// IsDir if the entry is a sub dir -func (te *TreeEntry) IsDir() bool { - return te.entryMode.IsDir() -} - -// IsLink if the entry is a symlink -func (te *TreeEntry) IsLink() bool { - return te.entryMode.IsLink() -} - -// IsRegular if the entry is a regular file -func (te *TreeEntry) IsRegular() bool { - return te.entryMode.IsRegular() -} - -// IsExecutable if the entry is an executable file (not necessarily binary) -func (te *TreeEntry) IsExecutable() bool { - return te.entryMode.IsExecutable() -} - // Blob returns the blob object the entry func (te *TreeEntry) Blob() *Blob { return &Blob{ diff --git a/package/gitea/source/modules/git/tree_entry_test.go b/package/gitea/source/modules/git/tree_entry_test.go index 9ca82675..3df6eeab 100644 --- a/package/gitea/source/modules/git/tree_entry_test.go +++ b/package/gitea/source/modules/git/tree_entry_test.go @@ -1,55 +1,57 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build gogit - package git import ( + "math/rand/v2" + "slices" + "strings" "testing" - "github.com/go-git/go-git/v5/plumbing/filemode" - "github.com/go-git/go-git/v5/plumbing/object" "github.com/stretchr/testify/assert" ) -func getTestEntries() Entries { - return Entries{ - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v1.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.1", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.12", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v2.2", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "v12.0", Mode: filemode.Dir}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "abc", Mode: filemode.Regular}}, - &TreeEntry{gogitTreeEntry: &object.TreeEntry{Name: "bcd", Mode: filemode.Regular}}, +func TestEntriesCustomSort(t *testing.T) { + entries := Entries{ + &TreeEntry{name: "a-dir", entryMode: EntryModeTree}, + &TreeEntry{name: "a-submodule", entryMode: EntryModeCommit}, + &TreeEntry{name: "b-dir", entryMode: EntryModeTree}, + &TreeEntry{name: "b-submodule", entryMode: EntryModeCommit}, + &TreeEntry{name: "a-file", entryMode: EntryModeBlob}, + &TreeEntry{name: "b-file", entryMode: EntryModeBlob}, + } + expected := slices.Clone(entries) + for slices.Equal(expected, entries) { + rand.Shuffle(len(entries), func(i, j int) { entries[i], entries[j] = entries[j], entries[i] }) + } + entries.CustomSort(strings.Compare) + assert.Equal(t, expected, entries) +} + +func TestParseEntryMode(t *testing.T) { + tests := []struct { + modeStr string + expectMod EntryMode + }{ + {"000000", EntryModeNoEntry}, + {"000755", EntryModeNoEntry}, + + {"100644", EntryModeBlob}, + {"100755", EntryModeExec}, + + {"120000", EntryModeSymlink}, + {"120755", EntryModeSymlink}, + {"160000", EntryModeCommit}, + {"160755", EntryModeCommit}, + + {"040000", EntryModeTree}, + {"040755", EntryModeTree}, + + {"777777", EntryModeNoEntry}, // invalid mode + } + for _, test := range tests { + mod := ParseEntryMode(test.modeStr) + assert.Equal(t, test.expectMod, mod, "modeStr: %s", test.modeStr) } } - -func TestEntriesSort(t *testing.T) { - entries := getTestEntries() - entries.Sort() - assert.Equal(t, "v1.0", entries[0].Name()) - assert.Equal(t, "v12.0", entries[1].Name()) - assert.Equal(t, "v2.0", entries[2].Name()) - assert.Equal(t, "v2.1", entries[3].Name()) - assert.Equal(t, "v2.12", entries[4].Name()) - assert.Equal(t, "v2.2", entries[5].Name()) - assert.Equal(t, "abc", entries[6].Name()) - assert.Equal(t, "bcd", entries[7].Name()) -} - -func TestEntriesCustomSort(t *testing.T) { - entries := getTestEntries() - entries.CustomSort(func(s1, s2 string) bool { - return s1 > s2 - }) - assert.Equal(t, "v2.2", entries[0].Name()) - assert.Equal(t, "v2.12", entries[1].Name()) - assert.Equal(t, "v2.1", entries[2].Name()) - assert.Equal(t, "v2.0", entries[3].Name()) - assert.Equal(t, "v12.0", entries[4].Name()) - assert.Equal(t, "v1.0", entries[5].Name()) - assert.Equal(t, "bcd", entries[6].Name()) - assert.Equal(t, "abc", entries[7].Name()) -} diff --git a/package/gitea/source/modules/git/tree_gogit.go b/package/gitea/source/modules/git/tree_gogit.go index 421b0ecb..fec6e270 100644 --- a/package/gitea/source/modules/git/tree_gogit.go +++ b/package/gitea/source/modules/git/tree_gogit.go @@ -15,41 +15,34 @@ import ( // Tree represents a flat directory listing. type Tree struct { - ID ObjectID - ResolvedID ObjectID - repo *Repository + TreeCommon - gogitTree *object.Tree - - // parent tree - ptree *Tree + resolvedGogitTreeObject *object.Tree } -func (t *Tree) loadTreeObject() error { - gogitTree, err := t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue())) - if err != nil { - return err - } - - t.gogitTree = gogitTree - return nil -} - -// ListEntries returns all entries of current tree. -func (t *Tree) ListEntries() (Entries, error) { - if t.gogitTree == nil { - err := t.loadTreeObject() +func (t *Tree) gogitTreeObject() (_ *object.Tree, err error) { + if t.resolvedGogitTreeObject == nil { + t.resolvedGogitTreeObject, err = t.repo.gogitRepo.TreeObject(plumbing.Hash(t.ID.RawValue())) if err != nil { return nil, err } } + return t.resolvedGogitTreeObject, nil +} - entries := make([]*TreeEntry, len(t.gogitTree.Entries)) - for i, entry := range t.gogitTree.Entries { +// ListEntries returns all entries of current tree. +func (t *Tree) ListEntries() (Entries, error) { + gogitTree, err := t.gogitTreeObject() + if err != nil { + return nil, err + } + entries := make([]*TreeEntry, len(gogitTree.Entries)) + for i, gogitTreeEntry := range gogitTree.Entries { entries[i] = &TreeEntry{ - ID: ParseGogitHash(entry.Hash), - gogitTreeEntry: &t.gogitTree.Entries[i], - ptree: t, + ID: ParseGogitHash(gogitTreeEntry.Hash), + ptree: t, + name: gogitTreeEntry.Name, + entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode), } } @@ -57,38 +50,28 @@ func (t *Tree) ListEntries() (Entries, error) { } // ListEntriesRecursiveWithSize returns all entries of current tree recursively including all subtrees -func (t *Tree) ListEntriesRecursiveWithSize() (Entries, error) { - if t.gogitTree == nil { - err := t.loadTreeObject() - if err != nil { - return nil, err - } +func (t *Tree) ListEntriesRecursiveWithSize() (entries Entries, _ error) { + gogitTree, err := t.gogitTreeObject() + if err != nil { + return nil, err } - var entries []*TreeEntry - seen := map[plumbing.Hash]bool{} - walker := object.NewTreeWalker(t.gogitTree, true, seen) + walker := object.NewTreeWalker(gogitTree, true, nil) for { - fullName, entry, err := walker.Next() + fullName, gogitTreeEntry, err := walker.Next() if err == io.EOF { break - } - if err != nil { + } else if err != nil { return nil, err } - if seen[entry.Hash] { - continue - } - convertedEntry := &TreeEntry{ - ID: ParseGogitHash(entry.Hash), - gogitTreeEntry: &entry, - ptree: t, - fullName: fullName, + ID: ParseGogitHash(gogitTreeEntry.Hash), + name: fullName, // FIXME: the "name" field is abused, here it is a full path + ptree: t, // FIXME: this ptree is not right, fortunately it isn't really used + entryMode: gogitFileModeToEntryMode(gogitTreeEntry.Mode), } entries = append(entries, convertedEntry) } - return entries, nil } diff --git a/package/gitea/source/modules/git/tree_nogogit.go b/package/gitea/source/modules/git/tree_nogogit.go index 045d78c4..5d951dad 100644 --- a/package/gitea/source/modules/git/tree_nogogit.go +++ b/package/gitea/source/modules/git/tree_nogogit.go @@ -14,18 +14,10 @@ import ( // Tree represents a flat directory listing. type Tree struct { - ID ObjectID - ResolvedID ObjectID - repo *Repository - - // parent tree - ptree *Tree + TreeCommon entries Entries entriesParsed bool - - entriesRecursive Entries - entriesRecursiveParsed bool } // ListEntries returns all entries of current tree. @@ -35,30 +27,29 @@ func (t *Tree) ListEntries() (Entries, error) { } if t.repo != nil { - wr, rd, cancel, err := t.repo.CatFileBatch(t.repo.Ctx) + batch, cancel, err := t.repo.CatFileBatch(t.repo.Ctx) if err != nil { return nil, err } defer cancel() - _, _ = wr.Write([]byte(t.ID.String() + "\n")) - _, typ, sz, err := ReadBatchLine(rd) + info, rd, err := batch.QueryContent(t.ID.String()) if err != nil { return nil, err } - if typ == "commit" { - treeID, err := ReadTreeID(rd, sz) + + if info.Type == "commit" { + treeID, err := ReadTreeID(rd, info.Size) if err != nil && err != io.EOF { return nil, err } - _, _ = wr.Write([]byte(treeID + "\n")) - _, typ, sz, err = ReadBatchLine(rd) + info, rd, err = batch.QueryContent(treeID) if err != nil { return nil, err } } - if typ == "tree" { - t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, sz) + if info.Type == "tree" { + t.entries, err = catBatchParseTreeEntries(t.ID.Type(), t, rd, info.Size) if err != nil { return nil, err } @@ -67,14 +58,14 @@ func (t *Tree) ListEntries() (Entries, error) { } // Not a tree just use ls-tree instead - if err := DiscardFull(rd, sz+1); err != nil { + if err := DiscardFull(rd, info.Size+1); err != nil { return nil, err } } - stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).RunStdBytes(t.repo.Ctx, &gitcmd.RunOpts{Dir: t.repo.Path}) + stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-l").AddDynamicArguments(t.ID.String()).WithDir(t.repo.Path).RunStdBytes(t.repo.Ctx) if runErr != nil { - if strings.Contains(runErr.Error(), "fatal: Not a valid object name") || strings.Contains(runErr.Error(), "fatal: not a tree object") { + if gitcmd.IsStdErrorNotValidObjectName(runErr) || strings.Contains(runErr.Error(), "fatal: not a tree object") { return nil, ErrNotExist{ ID: t.ID.String(), } @@ -94,25 +85,18 @@ func (t *Tree) ListEntries() (Entries, error) { // listEntriesRecursive returns all entries of current tree recursively including all subtrees // extraArgs could be "-l" to get the size, which is slower func (t *Tree) listEntriesRecursive(extraArgs gitcmd.TrustedCmdArgs) (Entries, error) { - if t.entriesRecursiveParsed { - return t.entriesRecursive, nil - } - stdout, _, runErr := gitcmd.NewCommand("ls-tree", "-t", "-r"). AddArguments(extraArgs...). AddDynamicArguments(t.ID.String()). - RunStdBytes(t.repo.Ctx, &gitcmd.RunOpts{Dir: t.repo.Path}) + WithDir(t.repo.Path). + RunStdBytes(t.repo.Ctx) if runErr != nil { return nil, runErr } - var err error - t.entriesRecursive, err = parseTreeEntries(stdout, t) - if err == nil { - t.entriesRecursiveParsed = true - } - - return t.entriesRecursive, err + // FIXME: the "name" field is abused, here it is a full path + // FIXME: this ptree is not right, fortunately it isn't really used + return parseTreeEntries(stdout, t) } // ListEntriesRecursiveFast returns all entries of current tree recursively including all subtrees, no size diff --git a/package/gitea/source/modules/gitrepo/archive.go b/package/gitea/source/modules/gitrepo/archive.go new file mode 100644 index 00000000..191a1bd2 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/archive.go @@ -0,0 +1,80 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "fmt" + "io" + "os" + "path" + "path/filepath" + "slices" + "strings" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/setting" +) + +// CreateArchive create archive content to the target path +func CreateArchive(ctx context.Context, repo Repository, format string, target io.Writer, usePrefix bool, commitID string, paths []string) error { + if format == "unknown" { + return fmt.Errorf("unknown format: %v", format) + } + + cmd := gitcmd.NewCommand("archive") + if usePrefix { + cmd.AddOptionFormat("--prefix=%s", filepath.Base(strings.TrimSuffix(repo.RelativePath(), ".git"))+"/") + } + cmd.AddOptionFormat("--format=%s", format) + cmd.AddDynamicArguments(commitID) + + paths = slices.Clone(paths) + for i := range paths { + // although "git archive" already ensures the paths won't go outside the repo, we still clean them here for safety + paths[i] = path.Clean(paths[i]) + } + cmd.AddDynamicArguments(paths...) + return RunCmdWithStderr(ctx, repo, cmd.WithStdoutCopy(target)) +} + +// CreateBundle create bundle content to the target path +func CreateBundle(ctx context.Context, repo Repository, commit string, out io.Writer) error { + tmp, cleanup, err := setting.AppDataTempDir("git-repo-content").MkdirTempRandom("gitea-bundle") + if err != nil { + return err + } + defer cleanup() + + env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repoPath(repo), "objects")) + _, _, err = gitcmd.NewCommand("init", "--bare").WithDir(tmp).WithEnv(env).RunStdString(ctx) + if err != nil { + return err + } + + _, _, err = gitcmd.NewCommand("reset", "--soft").AddDynamicArguments(commit).WithDir(tmp).WithEnv(env).RunStdString(ctx) + if err != nil { + return err + } + + _, _, err = gitcmd.NewCommand("branch", "-m", "bundle").WithDir(tmp).WithEnv(env).RunStdString(ctx) + if err != nil { + return err + } + + tmpFile := filepath.Join(tmp, "bundle") + _, _, err = gitcmd.NewCommand("bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").WithDir(tmp).WithEnv(env).RunStdString(ctx) + if err != nil { + return err + } + + fi, err := os.Open(tmpFile) + if err != nil { + return err + } + defer fi.Close() + + _, err = io.Copy(out, fi) + return err +} diff --git a/package/gitea/source/modules/git/blame.go b/package/gitea/source/modules/gitrepo/blame.go similarity index 70% rename from package/gitea/source/modules/git/blame.go rename to package/gitea/source/modules/gitrepo/blame.go index 50cadc41..2352da17 100644 --- a/package/gitea/source/modules/git/blame.go +++ b/package/gitea/source/modules/gitrepo/blame.go @@ -1,20 +1,28 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package gitrepo import ( "bufio" "bytes" "context" "io" - "os" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) +func LineBlame(ctx context.Context, repo Repository, revision, file string, line uint) (string, error) { + stdout, _, err := RunCmdString(ctx, repo, + gitcmd.NewCommand("blame"). + AddOptionFormat("-L %d,%d", line, line). + AddOptionValues("-p", revision). + AddDashesAndList(file)) + return stdout, err +} + // BlamePart represents block of blame - continuous lines with one sha type BlamePart struct { Sha string @@ -25,13 +33,11 @@ type BlamePart struct { // BlameReader returns part of file blame one by one type BlameReader struct { - output io.WriteCloser - reader io.ReadCloser bufferedReader *bufio.Reader done chan error lastSha *string ignoreRevsFile string - objectFormat ObjectFormat + objectFormat git.ObjectFormat cleanupFuncs []func() } @@ -122,34 +128,42 @@ func (r *BlameReader) Close() error { err := <-r.done r.bufferedReader = nil - _ = r.reader.Close() - _ = r.output.Close() - for _, cleanup := range r.cleanupFuncs { - if cleanup != nil { - cleanup() - } - } + r.cleanup() return err } +func (r *BlameReader) cleanup() { + for _, cleanup := range r.cleanupFuncs { + cleanup() + } +} + // CreateBlameReader creates reader for given repository, commit and file -func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, err error) { - var ignoreRevsFileName string - var ignoreRevsFileCleanup func() +func CreateBlameReader(ctx context.Context, objectFormat git.ObjectFormat, repo Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (rd *BlameReader, retErr error) { defer func() { - if err != nil && ignoreRevsFileCleanup != nil { - ignoreRevsFileCleanup() + if retErr != nil { + rd.cleanup() } }() + rd = &BlameReader{ + done: make(chan error, 1), + objectFormat: objectFormat, + } + cmd := gitcmd.NewCommand("blame", "--porcelain") - if DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore { - ignoreRevsFileName, ignoreRevsFileCleanup, err = tryCreateBlameIgnoreRevsFile(commit) - if err != nil && !IsErrNotExist(err) { + stdoutReader, stdoutReaderClose := cmd.MakeStdoutPipe() + rd.bufferedReader = bufio.NewReader(stdoutReader) + rd.cleanupFuncs = append(rd.cleanupFuncs, stdoutReaderClose) + + if git.DefaultFeatures().CheckVersionAtLeast("2.23") && !bypassBlameIgnore { + ignoreRevsFileName, ignoreRevsFileCleanup, err := tryCreateBlameIgnoreRevsFile(commit) + if err != nil && !git.IsErrNotExist(err) { return nil, err - } - if ignoreRevsFileName != "" { + } else if err == nil { + rd.ignoreRevsFile = ignoreRevsFileName + rd.cleanupFuncs = append(rd.cleanupFuncs, ignoreRevsFileCleanup) // Possible improvement: use --ignore-revs-file /dev/stdin on unix // There is no equivalent on Windows. May be implemented if Gitea uses an external git backend. cmd.AddOptionValues("--ignore-revs-file", ignoreRevsFileName) @@ -158,40 +172,15 @@ func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath cmd.AddDynamicArguments(commit.ID.String()).AddDashesAndList(file) - done := make(chan error, 1) - reader, stdout, err := os.Pipe() - if err != nil { - return nil, err - } go func() { - stderr := bytes.Buffer{} // TODO: it doesn't work for directories (the directories shouldn't be "blamed"), and the "err" should be returned by "Read" but not by "Close" - err := cmd.Run(ctx, &gitcmd.RunOpts{ - UseContextTimeout: true, - Dir: repoPath, - Stdout: stdout, - Stderr: &stderr, - }) - done <- err - _ = stdout.Close() - if err != nil { - log.Error("Error running git blame (dir: %v): %v, stderr: %v", repoPath, err, stderr.String()) - } + rd.done <- RunCmdWithStderr(ctx, repo, cmd) }() - bufferedReader := bufio.NewReader(reader) - return &BlameReader{ - output: stdout, - reader: reader, - bufferedReader: bufferedReader, - done: done, - ignoreRevsFile: ignoreRevsFileName, - objectFormat: objectFormat, - cleanupFuncs: []func(){ignoreRevsFileCleanup}, - }, nil + return rd, nil } -func tryCreateBlameIgnoreRevsFile(commit *Commit) (string, func(), error) { +func tryCreateBlameIgnoreRevsFile(commit *git.Commit) (string, func(), error) { entry, err := commit.GetTreeEntryByPath(".git-blame-ignore-revs") if err != nil { return "", nil, err diff --git a/package/gitea/source/modules/git/blame_sha256_test.go b/package/gitea/source/modules/gitrepo/blame_sha256_test.go similarity index 88% rename from package/gitea/source/modules/git/blame_sha256_test.go rename to package/gitea/source/modules/gitrepo/blame_sha256_test.go index c0a97bed..e92931d5 100644 --- a/package/gitea/source/modules/git/blame_sha256_test.go +++ b/package/gitea/source/modules/gitrepo/blame_sha256_test.go @@ -1,12 +1,13 @@ // Copyright 2024 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package gitrepo import ( "context" "testing" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -17,13 +18,14 @@ func TestReadingBlameOutputSha256(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() - if isGogit { + if git.DefaultFeatures().UsingGogit { t.Skip("Skipping test since gogit does not support sha256") return } t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { - repo, err := OpenRepository(ctx, "./tests/repos/repo5_pulls_sha256") + storage := &mockRepository{path: "repo5_pulls_sha256"} + repo, err := OpenRepository(ctx, storage) assert.NoError(t, err) defer repo.Close() @@ -47,7 +49,7 @@ func TestReadingBlameOutputSha256(t *testing.T) { } for _, bypass := range []bool{false, true} { - blameReader, err := CreateBlameReader(ctx, Sha256ObjectFormat, "./tests/repos/repo5_pulls_sha256", commit, "README.md", bypass) + blameReader, err := CreateBlameReader(ctx, git.Sha256ObjectFormat, storage, commit, "README.md", bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() @@ -68,7 +70,8 @@ func TestReadingBlameOutputSha256(t *testing.T) { }) t.Run("With .git-blame-ignore-revs", func(t *testing.T) { - repo, err := OpenRepository(ctx, "./tests/repos/repo6_blame_sha256") + storage := &mockRepository{path: "repo6_blame_sha256"} + repo, err := OpenRepository(ctx, storage) assert.NoError(t, err) defer repo.Close() @@ -131,7 +134,7 @@ func TestReadingBlameOutputSha256(t *testing.T) { for _, c := range cases { commit, err := repo.GetCommit(c.CommitID) assert.NoError(t, err) - blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame_sha256", commit, "blame.txt", c.Bypass) + blameReader, err := CreateBlameReader(ctx, objectFormat, storage, commit, "blame.txt", c.Bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() diff --git a/package/gitea/source/modules/git/blame_test.go b/package/gitea/source/modules/gitrepo/blame_test.go similarity index 89% rename from package/gitea/source/modules/git/blame_test.go rename to package/gitea/source/modules/gitrepo/blame_test.go index 809d6fbc..0307a5fd 100644 --- a/package/gitea/source/modules/git/blame_test.go +++ b/package/gitea/source/modules/gitrepo/blame_test.go @@ -1,12 +1,13 @@ // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package git +package gitrepo import ( "context" "testing" + "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" @@ -18,10 +19,10 @@ func TestReadingBlameOutput(t *testing.T) { defer cancel() t.Run("Without .git-blame-ignore-revs", func(t *testing.T) { - repo, err := OpenRepository(ctx, "./tests/repos/repo5_pulls") + storage := &mockRepository{path: "repo5_pulls"} + repo, err := OpenRepository(ctx, storage) assert.NoError(t, err) defer repo.Close() - commit, err := repo.GetCommit("f32b0a9dfd09a60f616f29158f772cedd89942d2") assert.NoError(t, err) @@ -42,7 +43,7 @@ func TestReadingBlameOutput(t *testing.T) { } for _, bypass := range []bool{false, true} { - blameReader, err := CreateBlameReader(ctx, Sha1ObjectFormat, "./tests/repos/repo5_pulls", commit, "README.md", bypass) + blameReader, err := CreateBlameReader(ctx, git.Sha1ObjectFormat, storage, commit, "README.md", bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() @@ -63,7 +64,8 @@ func TestReadingBlameOutput(t *testing.T) { }) t.Run("With .git-blame-ignore-revs", func(t *testing.T) { - repo, err := OpenRepository(ctx, "./tests/repos/repo6_blame") + storage := &mockRepository{path: "repo6_blame"} + repo, err := OpenRepository(ctx, storage) assert.NoError(t, err) defer repo.Close() @@ -127,7 +129,7 @@ func TestReadingBlameOutput(t *testing.T) { commit, err := repo.GetCommit(c.CommitID) assert.NoError(t, err) - blameReader, err := CreateBlameReader(ctx, objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass) + blameReader, err := CreateBlameReader(ctx, objectFormat, storage, commit, "blame.txt", c.Bypass) assert.NoError(t, err) assert.NotNil(t, blameReader) defer blameReader.Close() diff --git a/package/gitea/source/modules/gitrepo/branch.go b/package/gitea/source/modules/gitrepo/branch.go index 4d571f58..4c40d1fb 100644 --- a/package/gitea/source/modules/gitrepo/branch.go +++ b/package/gitea/source/modules/gitrepo/branch.go @@ -5,6 +5,8 @@ package gitrepo import ( "context" + "errors" + "strings" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" @@ -34,23 +36,61 @@ func GetBranchCommitID(ctx context.Context, repo Repository, branch string) (str // SetDefaultBranch sets default branch of repository. func SetDefaultBranch(ctx context.Context, repo Repository, name string) error { - _, _, err := gitcmd.NewCommand("symbolic-ref", "HEAD"). - AddDynamicArguments(git.BranchPrefix+name). - RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) + _, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("symbolic-ref", "HEAD"). + AddDynamicArguments(git.BranchPrefix+name)) return err } // GetDefaultBranch gets default branch of repository. func GetDefaultBranch(ctx context.Context, repo Repository) (string, error) { - return git.GetDefaultBranch(ctx, repoPath(repo)) + stdout, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("symbolic-ref", "HEAD")) + if err != nil { + return "", err + } + stdout = strings.TrimSpace(stdout) + if !strings.HasPrefix(stdout, git.BranchPrefix) { + return "", errors.New("the HEAD is not a branch: " + stdout) + } + return strings.TrimPrefix(stdout, git.BranchPrefix), nil } // IsReferenceExist returns true if given reference exists in the repository. func IsReferenceExist(ctx context.Context, repo Repository, name string) bool { - return git.IsReferenceExist(ctx, repoPath(repo), name) + _, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("show-ref", "--verify").AddDashesAndList(name)) + return err == nil } // IsBranchExist returns true if given branch exists in the repository. func IsBranchExist(ctx context.Context, repo Repository, name string) bool { return IsReferenceExist(ctx, repo, git.BranchPrefix+name) } + +// DeleteBranch delete a branch by name on repository. +func DeleteBranch(ctx context.Context, repo Repository, name string, force bool) error { + cmd := gitcmd.NewCommand("branch") + + if force { + cmd.AddArguments("-D") + } else { + cmd.AddArguments("-d") + } + + cmd.AddDashesAndList(name) + _, _, err := RunCmdString(ctx, repo, cmd) + return err +} + +// CreateBranch create a new branch +func CreateBranch(ctx context.Context, repo Repository, branch, oldbranchOrCommit string) error { + cmd := gitcmd.NewCommand("branch") + cmd.AddDashesAndList(branch, oldbranchOrCommit) + + _, _, err := RunCmdString(ctx, repo, cmd) + return err +} + +// RenameBranch rename a branch +func RenameBranch(ctx context.Context, repo Repository, from, to string) error { + _, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("branch", "-m").AddDynamicArguments(from, to)) + return err +} diff --git a/package/gitea/source/modules/gitrepo/cat_file.go b/package/gitea/source/modules/gitrepo/cat_file.go new file mode 100644 index 00000000..42ca23ac --- /dev/null +++ b/package/gitea/source/modules/gitrepo/cat_file.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func NewBatch(ctx context.Context, repo Repository) (git.CatFileBatchCloser, error) { + return git.NewBatch(ctx, repoPath(repo)) +} diff --git a/package/gitea/source/modules/gitrepo/clone.go b/package/gitea/source/modules/gitrepo/clone.go new file mode 100644 index 00000000..a0e4cc81 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/clone.go @@ -0,0 +1,24 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +// CloneExternalRepo clones an external repository to the managed repository. +func CloneExternalRepo(ctx context.Context, fromRemoteURL string, toRepo Repository, opts git.CloneRepoOptions) error { + return git.Clone(ctx, fromRemoteURL, repoPath(toRepo), opts) +} + +// CloneRepoToLocal clones a managed repository to a local path. +func CloneRepoToLocal(ctx context.Context, fromRepo Repository, toLocalPath string, opts git.CloneRepoOptions) error { + return git.Clone(ctx, repoPath(fromRepo), toLocalPath, opts) +} + +func Clone(ctx context.Context, fromRepo, toRepo Repository, opts git.CloneRepoOptions) error { + return git.Clone(ctx, repoPath(fromRepo), repoPath(toRepo), opts) +} diff --git a/package/gitea/source/modules/gitrepo/command.go b/package/gitea/source/modules/gitrepo/command.go new file mode 100644 index 00000000..fd21b9a7 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/command.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git/gitcmd" +) + +func RunCmd(ctx context.Context, repo Repository, cmd *gitcmd.Command) error { + return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().Run(ctx) +} + +func RunCmdString(ctx context.Context, repo Repository, cmd *gitcmd.Command) (string, string, gitcmd.RunStdError) { + return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().RunStdString(ctx) +} + +func RunCmdBytes(ctx context.Context, repo Repository, cmd *gitcmd.Command) ([]byte, []byte, gitcmd.RunStdError) { + return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().RunStdBytes(ctx) +} + +func RunCmdWithStderr(ctx context.Context, repo Repository, cmd *gitcmd.Command) gitcmd.RunStdError { + return cmd.WithDir(repoPath(repo)).WithParentCallerInfo().RunWithStderr(ctx) +} diff --git a/package/gitea/source/modules/gitrepo/commit.go b/package/gitea/source/modules/gitrepo/commit.go new file mode 100644 index 00000000..0ab17862 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/commit.go @@ -0,0 +1,112 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "strconv" + "strings" + "time" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" +) + +// CommitsCountOptions the options when counting commits +type CommitsCountOptions struct { + Not string + Revision []string + RelPath []string + Since string + Until string +} + +// CommitsCount returns number of total commits of until given revision. +func CommitsCount(ctx context.Context, repo Repository, opts CommitsCountOptions) (int64, error) { + cmd := gitcmd.NewCommand("rev-list", "--count") + + cmd.AddDynamicArguments(opts.Revision...) + + if opts.Not != "" { + cmd.AddOptionValues("--not", opts.Not) + } + + if len(opts.RelPath) > 0 { + cmd.AddDashesAndList(opts.RelPath...) + } + + stdout, _, err := cmd.WithDir(repoPath(repo)).RunStdString(ctx) + if err != nil { + return 0, err + } + + return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) +} + +// CommitsCountBetween return numbers of commits between two commits +func CommitsCountBetween(ctx context.Context, repo Repository, start, end string) (int64, error) { + count, err := CommitsCount(ctx, repo, CommitsCountOptions{ + Revision: []string{start + ".." + end}, + }) + + if err != nil && strings.Contains(err.Error(), "no merge base") { + // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. + // previously it would return the results of git rev-list before last so let's try that... + return CommitsCount(ctx, repo, CommitsCountOptions{ + Revision: []string{start, end}, + }) + } + + return count, err +} + +// FileCommitsCount return the number of files at a revision +func FileCommitsCount(ctx context.Context, repo Repository, revision, file string) (int64, error) { + return CommitsCount(ctx, repo, + CommitsCountOptions{ + Revision: []string{revision}, + RelPath: []string{file}, + }) +} + +// CommitsCountOfCommit returns number of total commits of until current revision. +func CommitsCountOfCommit(ctx context.Context, repo Repository, commitID string) (int64, error) { + return CommitsCount(ctx, repo, CommitsCountOptions{ + Revision: []string{commitID}, + }) +} + +// AllCommitsCount returns count of all commits in repository +func AllCommitsCount(ctx context.Context, repo Repository, hidePRRefs bool, files ...string) (int64, error) { + cmd := gitcmd.NewCommand("rev-list") + if hidePRRefs { + cmd.AddArguments("--exclude=" + git.PullPrefix + "*") + } + cmd.AddArguments("--all", "--count") + if len(files) > 0 { + cmd.AddDashesAndList(files...) + } + + stdout, _, err := RunCmdString(ctx, repo, cmd) + if err != nil { + return 0, err + } + + return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64) +} + +func GetFullCommitID(ctx context.Context, repo Repository, shortID string) (string, error) { + return git.GetFullCommitID(ctx, repoPath(repo), shortID) +} + +// GetLatestCommitTime returns time for latest commit in repository (across all branches) +func GetLatestCommitTime(ctx context.Context, repo Repository) (time.Time, error) { + stdout, _, err := RunCmdString(ctx, repo, + gitcmd.NewCommand("for-each-ref", "--sort=-committerdate", git.BranchPrefix, "--count", "1", "--format=%(committerdate)")) + if err != nil { + return time.Time{}, err + } + commitTime := strings.TrimSpace(stdout) + return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime) +} diff --git a/package/gitea/source/modules/gitrepo/commit_file.go b/package/gitea/source/modules/gitrepo/commit_file.go new file mode 100644 index 00000000..437b3b51 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/commit_file.go @@ -0,0 +1,88 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "bufio" + "context" + "io" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/log" +) + +// CommitFileStatus represents status of files in a commit. +type CommitFileStatus struct { + Added []string + Removed []string + Modified []string +} + +// NewCommitFileStatus creates a CommitFileStatus +func NewCommitFileStatus() *CommitFileStatus { + return &CommitFileStatus{ + []string{}, []string{}, []string{}, + } +} + +func parseCommitFileStatus(fileStatus *CommitFileStatus, stdout io.Reader) { + rd := bufio.NewReader(stdout) + peek, err := rd.Peek(1) + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + if peek[0] == '\n' || peek[0] == '\x00' { + _, _ = rd.Discard(1) + } + for { + modifier, err := rd.ReadString('\x00') + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + file, err := rd.ReadString('\x00') + if err != nil { + if err != io.EOF { + log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err) + } + return + } + file = file[:len(file)-1] + switch modifier[0] { + case 'A': + fileStatus.Added = append(fileStatus.Added, file) + case 'D': + fileStatus.Removed = append(fileStatus.Removed, file) + case 'M': + fileStatus.Modified = append(fileStatus.Modified, file) + } + } +} + +// GetCommitFileStatus returns file status of commit in given repository. +func GetCommitFileStatus(ctx context.Context, repo Repository, commitID string) (*CommitFileStatus, error) { + cmd := gitcmd.NewCommand("log", "--name-status", "-m", "--pretty=format:", "--first-parent", "--no-renames", "-z", "-1") + stdout, stdoutClose := cmd.MakeStdoutPipe() + defer stdoutClose() + done := make(chan struct{}) + fileStatus := NewCommitFileStatus() + go func() { + parseCommitFileStatus(fileStatus, stdout) + close(done) + }() + err := cmd.AddDynamicArguments(commitID). + WithDir(repoPath(repo)). + RunWithStderr(ctx) + if err != nil { + return nil, err + } + + <-done + return fileStatus, nil +} diff --git a/package/gitea/source/modules/gitrepo/commit_file_test.go b/package/gitea/source/modules/gitrepo/commit_file_test.go new file mode 100644 index 00000000..ec1018ee --- /dev/null +++ b/package/gitea/source/modules/gitrepo/commit_file_test.go @@ -0,0 +1,175 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseCommitFileStatus(t *testing.T) { + type testcase struct { + output string + added []string + removed []string + modified []string + } + + kases := []testcase{ + { + // Merge commit + output: "MM\x00options/locale/locale_en-US.ini\x00", + modified: []string{ + "options/locale/locale_en-US.ini", + }, + added: []string{}, + removed: []string{}, + }, + { + // Spaces commit + output: "D\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00", + removed: []string{ + "b", + "b b/b", + }, + modified: []string{}, + added: []string{ + "b b/b b/b b/b", + "b b/b b/b b/b b/b", + }, + }, + { + // larger commit + output: "M\x00go.mod\x00M\x00go.sum\x00M\x00modules/ssh/ssh.go\x00M\x00vendor/github.com/gliderlabs/ssh/circle.yml\x00M\x00vendor/github.com/gliderlabs/ssh/context.go\x00A\x00vendor/github.com/gliderlabs/ssh/go.mod\x00A\x00vendor/github.com/gliderlabs/ssh/go.sum\x00M\x00vendor/github.com/gliderlabs/ssh/server.go\x00M\x00vendor/github.com/gliderlabs/ssh/session.go\x00M\x00vendor/github.com/gliderlabs/ssh/ssh.go\x00M\x00vendor/golang.org/x/sys/unix/mkerrors.sh\x00M\x00vendor/golang.org/x/sys/unix/syscall_darwin.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/zerrors_linux.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go\x00M\x00vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go\x00M\x00vendor/modules.txt\x00", + modified: []string{ + "go.mod", + "go.sum", + "modules/ssh/ssh.go", + "vendor/github.com/gliderlabs/ssh/circle.yml", + "vendor/github.com/gliderlabs/ssh/context.go", + "vendor/github.com/gliderlabs/ssh/server.go", + "vendor/github.com/gliderlabs/ssh/session.go", + "vendor/github.com/gliderlabs/ssh/ssh.go", + "vendor/golang.org/x/sys/unix/mkerrors.sh", + "vendor/golang.org/x/sys/unix/syscall_darwin.go", + "vendor/golang.org/x/sys/unix/zerrors_darwin_amd64.go", + "vendor/golang.org/x/sys/unix/zerrors_darwin_arm64.go", + "vendor/golang.org/x/sys/unix/zerrors_freebsd_386.go", + "vendor/golang.org/x/sys/unix/zerrors_freebsd_amd64.go", + "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm.go", + "vendor/golang.org/x/sys/unix/zerrors_freebsd_arm64.go", + "vendor/golang.org/x/sys/unix/zerrors_linux.go", + "vendor/golang.org/x/sys/unix/ztypes_darwin_amd64.go", + "vendor/golang.org/x/sys/unix/ztypes_darwin_arm64.go", + "vendor/golang.org/x/sys/unix/ztypes_dragonfly_amd64.go", + "vendor/golang.org/x/sys/unix/ztypes_freebsd_386.go", + "vendor/golang.org/x/sys/unix/ztypes_freebsd_amd64.go", + "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm.go", + "vendor/golang.org/x/sys/unix/ztypes_freebsd_arm64.go", + "vendor/golang.org/x/sys/unix/ztypes_netbsd_386.go", + "vendor/golang.org/x/sys/unix/ztypes_netbsd_amd64.go", + "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go", + "vendor/golang.org/x/sys/unix/ztypes_netbsd_arm64.go", + "vendor/modules.txt", + }, + added: []string{ + "vendor/github.com/gliderlabs/ssh/go.mod", + "vendor/github.com/gliderlabs/ssh/go.sum", + }, + removed: []string{}, + }, + { + // git 1.7.2 adds an unnecessary \x00 on merge commit + output: "\x00MM\x00options/locale/locale_en-US.ini\x00", + modified: []string{ + "options/locale/locale_en-US.ini", + }, + added: []string{}, + removed: []string{}, + }, + { + // git 1.7.2 adds an unnecessary \n on normal commit + output: "\nD\x00b\x00D\x00b b/b\x00A\x00b b/b b/b b/b\x00A\x00b b/b b/b b/b b/b\x00", + removed: []string{ + "b", + "b b/b", + }, + modified: []string{}, + added: []string{ + "b b/b b/b b/b", + "b b/b b/b b/b b/b", + }, + }, + } + + for _, kase := range kases { + fileStatus := NewCommitFileStatus() + parseCommitFileStatus(fileStatus, strings.NewReader(kase.output)) + + assert.Equal(t, kase.added, fileStatus.Added) + assert.Equal(t, kase.removed, fileStatus.Removed) + assert.Equal(t, kase.modified, fileStatus.Modified) + } +} + +func TestGetCommitFileStatusMerges(t *testing.T) { + bareRepo6 := &mockRepository{path: "repo6_merge"} + + commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo6, "022f4ce6214973e018f02bf363bf8a2e3691f699") + assert.NoError(t, err) + + expected := CommitFileStatus{ + []string{ + "add_file.txt", + }, + []string{ + "to_remove.txt", + }, + []string{ + "to_modify.txt", + }, + } + + assert.Equal(t, expected.Added, commitFileStatus.Added) + assert.Equal(t, expected.Removed, commitFileStatus.Removed) + assert.Equal(t, expected.Modified, commitFileStatus.Modified) +} + +func TestGetCommitFileStatusMergesSha256(t *testing.T) { + bareRepo6Sha256 := &mockRepository{path: "repo6_merge_sha256"} + + commitFileStatus, err := GetCommitFileStatus(t.Context(), bareRepo6Sha256, "d2e5609f630dd8db500f5298d05d16def282412e3e66ed68cc7d0833b29129a1") + assert.NoError(t, err) + + expected := CommitFileStatus{ + []string{ + "add_file.txt", + }, + []string{}, + []string{ + "to_modify.txt", + }, + } + + assert.Equal(t, expected.Added, commitFileStatus.Added) + assert.Equal(t, expected.Removed, commitFileStatus.Removed) + assert.Equal(t, expected.Modified, commitFileStatus.Modified) + + expected = CommitFileStatus{ + []string{}, + []string{ + "to_remove.txt", + }, + []string{}, + } + + commitFileStatus, err = GetCommitFileStatus(t.Context(), bareRepo6Sha256, "da1ded40dc8e5b7c564171f4bf2fc8370487decfb1cb6a99ef28f3ed73d09172") + assert.NoError(t, err) + + assert.Equal(t, expected.Added, commitFileStatus.Added) + assert.Equal(t, expected.Removed, commitFileStatus.Removed) + assert.Equal(t, expected.Modified, commitFileStatus.Modified) +} diff --git a/package/gitea/source/modules/gitrepo/commit_test.go b/package/gitea/source/modules/gitrepo/commit_test.go new file mode 100644 index 00000000..05cedc39 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/commit_test.go @@ -0,0 +1,45 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCommitsCount(t *testing.T) { + bareRepo1 := &mockRepository{path: "repo1_bare"} + + commitsCount, err := CommitsCount(t.Context(), bareRepo1, + CommitsCountOptions{ + Revision: []string{"8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"}, + }) + + assert.NoError(t, err) + assert.Equal(t, int64(3), commitsCount) +} + +func TestCommitsCountWithoutBase(t *testing.T) { + bareRepo1 := &mockRepository{path: "repo1_bare"} + + commitsCount, err := CommitsCount(t.Context(), bareRepo1, + CommitsCountOptions{ + Not: "master", + Revision: []string{"branch1"}, + }) + + assert.NoError(t, err) + assert.Equal(t, int64(2), commitsCount) +} + +func TestGetLatestCommitTime(t *testing.T) { + bareRepo1 := &mockRepository{path: "repo1_bare"} + lct, err := GetLatestCommitTime(t.Context(), bareRepo1) + assert.NoError(t, err) + // Time is Sun Nov 13 16:40:14 2022 +0100 + // which is the time of commit + // ce064814f4a0d337b333e646ece456cd39fab612 (refs/heads/master) + assert.EqualValues(t, 1668354014, lct.Unix()) +} diff --git a/package/gitea/source/modules/gitrepo/commitgraph.go b/package/gitea/source/modules/gitrepo/commitgraph.go new file mode 100644 index 00000000..7310e167 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/commitgraph.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func WriteCommitGraph(ctx context.Context, repo Repository) error { + return git.WriteCommitGraph(ctx, repoPath(repo)) +} diff --git a/package/gitea/source/modules/gitrepo/compare.go b/package/gitea/source/modules/gitrepo/compare.go new file mode 100644 index 00000000..7e38d33e --- /dev/null +++ b/package/gitea/source/modules/gitrepo/compare.go @@ -0,0 +1,71 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "fmt" + "strconv" + "strings" + + "code.gitea.io/gitea/modules/git/gitcmd" +) + +// DivergeObject represents commit count diverging commits +type DivergeObject struct { + Ahead int + Behind int +} + +// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch +func GetDivergingCommits(ctx context.Context, repo Repository, baseBranch, targetBranch string) (*DivergeObject, error) { + cmd := gitcmd.NewCommand("rev-list", "--count", "--left-right"). + AddDynamicArguments(baseBranch + "..." + targetBranch).AddArguments("--") + stdout, _, err1 := RunCmdString(ctx, repo, cmd) + if err1 != nil { + return nil, err1 + } + + left, right, found := strings.Cut(strings.Trim(stdout, "\n"), "\t") + if !found { + return nil, fmt.Errorf("git rev-list output is missing a tab: %q", stdout) + } + + behind, err := strconv.Atoi(left) + if err != nil { + return nil, err + } + ahead, err := strconv.Atoi(right) + if err != nil { + return nil, err + } + return &DivergeObject{Ahead: ahead, Behind: behind}, nil +} + +// GetCommitIDsBetweenReverse returns the last commit IDs between two commits in reverse order (from old to new) with limit. +// If the result exceeds the limit, the old commits IDs will be ignored +func GetCommitIDsBetweenReverse(ctx context.Context, repo Repository, startRef, endRef, notRef string, limit int) ([]string, error) { + genCmd := func(reversions ...string) *gitcmd.Command { + cmd := gitcmd.NewCommand("rev-list", "--reverse"). + AddArguments("-n").AddDynamicArguments(strconv.Itoa(limit)). + AddDynamicArguments(reversions...) + if notRef != "" { // --not should be kept as the last parameter of git command, otherwise the result will be wrong + cmd.AddOptionValues("--not", notRef) + } + return cmd + } + stdout, _, err := RunCmdString(ctx, repo, genCmd(startRef+".."+endRef)) + // example git error message: fatal: origin/main..HEAD: no merge base + if err != nil && strings.Contains(err.Stderr(), "no merge base") { + // future versions of git >= 2.28 are likely to return an error if before and last have become unrelated. + // previously it would return the results of git rev-list before last so let's try that... + stdout, _, err = RunCmdString(ctx, repo, genCmd(startRef, endRef)) + } + if err != nil { + return nil, err + } + + commitIDs := strings.Fields(strings.TrimSpace(stdout)) + return commitIDs, nil +} diff --git a/package/gitea/source/modules/gitrepo/compare_test.go b/package/gitea/source/modules/gitrepo/compare_test.go new file mode 100644 index 00000000..3f7cc4f6 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/compare_test.go @@ -0,0 +1,178 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockRepository struct { + path string +} + +func (r *mockRepository) RelativePath() string { + return r.path +} + +func commitRootTree(t *testing.T, repoDir, fileName, content, message string) string { + t.Helper() + + require.NoError(t, gitcmd.NewCommand("read-tree", "--empty").WithDir(repoDir).Run(t.Context())) + + stdout, _, err := gitcmd.NewCommand("hash-object", "-w", "--stdin"). + WithDir(repoDir). + WithStdinBytes([]byte(content)). + RunStdString(t.Context()) + require.NoError(t, err) + blobSHA := strings.TrimSpace(stdout) + + _, _, err = gitcmd.NewCommand("update-index", "--add", "--replace", "--cacheinfo"). + AddDynamicArguments("100644", blobSHA, fileName). + WithDir(repoDir). + RunStdString(t.Context()) + require.NoError(t, err) + + stdout, _, err = gitcmd.NewCommand("write-tree").WithDir(repoDir).RunStdString(t.Context()) + require.NoError(t, err) + treeSHA := strings.TrimSpace(stdout) + + commitTimeStr := time.Now().Format(time.RFC3339) + env := append(os.Environ(), + "GIT_AUTHOR_NAME=Test", + "GIT_AUTHOR_EMAIL=test@example.com", + "GIT_AUTHOR_DATE="+commitTimeStr, + "GIT_COMMITTER_NAME=Test", + "GIT_COMMITTER_EMAIL=test@example.com", + "GIT_COMMITTER_DATE="+commitTimeStr, + ) + + messageBytes := bytes.NewBufferString(message + "\n") + stdout, _, err = gitcmd.NewCommand("commit-tree").AddDynamicArguments(treeSHA). + WithEnv(env). + WithDir(repoDir). + WithStdinBytes(messageBytes.Bytes()). + RunStdString(t.Context()) + require.NoError(t, err) + + return strings.TrimSpace(stdout) +} + +func TestMergeBaseNoCommonHistory(t *testing.T) { + repoDir := filepath.Join(t.TempDir(), "repo.git") + require.NoError(t, gitcmd.NewCommand("init").AddDynamicArguments(repoDir).Run(t.Context())) + + baseCommit := commitRootTree(t, repoDir, "base.txt", "base", "base") + headCommit := commitRootTree(t, repoDir, "head.txt", "head", "head") + + mergeBase, err := MergeBase(t.Context(), &mockRepository{path: repoDir}, baseCommit, headCommit) + assert.Empty(t, mergeBase) + assert.ErrorIs(t, err, util.ErrNotExist) +} + +func TestRepoGetDivergingCommits(t *testing.T) { + repo := &mockRepository{path: "repo1_bare"} + do, err := GetDivergingCommits(t.Context(), repo, "master", "branch2") + assert.NoError(t, err) + assert.Equal(t, &DivergeObject{ + Ahead: 1, + Behind: 5, + }, do) + + do, err = GetDivergingCommits(t.Context(), repo, "master", "master") + assert.NoError(t, err) + assert.Equal(t, &DivergeObject{ + Ahead: 0, + Behind: 0, + }, do) + + do, err = GetDivergingCommits(t.Context(), repo, "master", "test") + assert.NoError(t, err) + assert.Equal(t, &DivergeObject{ + Ahead: 0, + Behind: 2, + }, do) +} + +func TestGetCommitIDsBetweenReverse(t *testing.T) { + repo := &mockRepository{path: "repo1_bare"} + + // tests raw commit IDs + commitIDs, err := GetCommitIDsBetweenReverse(t.Context(), repo, + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + "ce064814f4a0d337b333e646ece456cd39fab612", + "", + 100, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0", + "6fbd69e9823458e6c4a2fc5c0f6bc022b2f2acd1", + "37991dec2c8e592043f47155ce4808d4580f9123", + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) + + commitIDs, err = GetCommitIDsBetweenReverse(t.Context(), repo, + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + "ce064814f4a0d337b333e646ece456cd39fab612", + "6fbd69e9823458e6c4a2fc5c0f6bc022b2f2acd1", + 100, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "37991dec2c8e592043f47155ce4808d4580f9123", + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) + + commitIDs, err = GetCommitIDsBetweenReverse(t.Context(), repo, + "8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2", + "ce064814f4a0d337b333e646ece456cd39fab612", + "", + 3, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "37991dec2c8e592043f47155ce4808d4580f9123", + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) + + // test branch names instead of raw commit IDs. + commitIDs, err = GetCommitIDsBetweenReverse(t.Context(), repo, + "test", + "master", + "", + 100, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) + + // add notref to exclude test + commitIDs, err = GetCommitIDsBetweenReverse(t.Context(), repo, + "test", + "master", + "test", + 100, + ) + assert.NoError(t, err) + assert.Equal(t, []string{ + "feaf4ba6bc635fec442f46ddd4512416ec43c2c2", + "ce064814f4a0d337b333e646ece456cd39fab612", + }, commitIDs) +} diff --git a/package/gitea/source/modules/gitrepo/config.go b/package/gitea/source/modules/gitrepo/config.go index c028c6af..9be3ef94 100644 --- a/package/gitea/source/modules/gitrepo/config.go +++ b/package/gitea/source/modules/gitrepo/config.go @@ -12,9 +12,8 @@ import ( ) func GitConfigGet(ctx context.Context, repo Repository, key string) (string, error) { - result, _, err := gitcmd.NewCommand("config", "--get"). - AddDynamicArguments(key). - RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) + result, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config", "--get"). + AddDynamicArguments(key)) if err != nil { return "", err } @@ -28,9 +27,8 @@ func getRepoConfigLockKey(repoStoragePath string) string { // GitConfigAdd add a git configuration key to a specific value for the given repository. func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error { return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { - _, _, err := gitcmd.NewCommand("config", "--add"). - AddDynamicArguments(key, value). - RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) + _, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config", "--add"). + AddDynamicArguments(key, value)) return err }) } @@ -40,9 +38,8 @@ func GitConfigAdd(ctx context.Context, repo Repository, key, value string) error // If the key exists, it will be updated to the new value. func GitConfigSet(ctx context.Context, repo Repository, key, value string) error { return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { - _, _, err := gitcmd.NewCommand("config"). - AddDynamicArguments(key, value). - RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) + _, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("config"). + AddDynamicArguments(key, value)) return err }) } diff --git a/package/gitea/source/modules/gitrepo/diff.go b/package/gitea/source/modules/gitrepo/diff.go new file mode 100644 index 00000000..0092cf0b --- /dev/null +++ b/package/gitea/source/modules/gitrepo/diff.go @@ -0,0 +1,71 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "fmt" + "io" + "regexp" + "strconv" + + "code.gitea.io/gitea/modules/git/gitcmd" +) + +// GetDiffShortStatByCmdArgs counts number of changed files, number of additions and deletions +// TODO: it can be merged with another "GetDiffShortStat" in the future +func GetDiffShortStatByCmdArgs(ctx context.Context, repo Repository, trustedArgs gitcmd.TrustedCmdArgs, dynamicArgs ...string) (numFiles, totalAdditions, totalDeletions int, err error) { + // Now if we call: + // $ git diff --shortstat 1ebb35b98889ff77299f24d82da426b434b0cca0...788b8b1440462d477f45b0088875 + // we get: + // " 9902 files changed, 2034198 insertions(+), 298800 deletions(-)\n" + cmd := gitcmd.NewCommand("diff", "--shortstat").AddArguments(trustedArgs...).AddDynamicArguments(dynamicArgs...) + stdout, _, err := RunCmdString(ctx, repo, cmd) + if err != nil { + return 0, 0, 0, err + } + + return parseDiffStat(stdout) +} + +var shortStatFormat = regexp.MustCompile( + `\s*(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?`) + +func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int, err error) { + if len(stdout) == 0 || stdout == "\n" { + return 0, 0, 0, nil + } + groups := shortStatFormat.FindStringSubmatch(stdout) + if len(groups) != 4 { + return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s groups: %s", stdout, groups) + } + + numFiles, err = strconv.Atoi(groups[1]) + if err != nil { + return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumFiles %w", stdout, err) + } + + if len(groups[2]) != 0 { + totalAdditions, err = strconv.Atoi(groups[2]) + if err != nil { + return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumAdditions %w", stdout, err) + } + } + + if len(groups[3]) != 0 { + totalDeletions, err = strconv.Atoi(groups[3]) + if err != nil { + return 0, 0, 0, fmt.Errorf("unable to parse shortstat: %s. Error parsing NumDeletions %w", stdout, err) + } + } + return numFiles, totalAdditions, totalDeletions, err +} + +// GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer. +func GetReverseRawDiff(ctx context.Context, repo Repository, commitID string, writer io.Writer) error { + return RunCmdWithStderr(ctx, repo, gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R"). + AddDynamicArguments(commitID). + WithStdoutCopy(writer), + ) +} diff --git a/package/gitea/source/modules/gitrepo/fetch.go b/package/gitea/source/modules/gitrepo/fetch.go new file mode 100644 index 00000000..0474d611 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/fetch.go @@ -0,0 +1,28 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/globallock" +) + +// FetchRemoteCommit fetches a specific commit and its related objects from a remote +// repository into the managed repository. +// +// If no reference (branch, tag, or other ref) points to the fetched commit, it will +// be treated as unreachable and cleaned up by `git gc` after the default prune +// expiration period (2 weeks). Ref: https://www.kernel.org/pub/software/scm/git/docs/git-gc.html +// +// This behavior is sufficient for temporary operations, such as determining the +// merge base between commits. +func FetchRemoteCommit(ctx context.Context, repo, remoteRepo Repository, commitID string) error { + return globallock.LockAndDo(ctx, getRepoWriteLockKey(repo.RelativePath()), func(ctx context.Context) error { + return RunCmd(ctx, repo, gitcmd.NewCommand("fetch", "--no-tags"). + AddDynamicArguments(repoPath(remoteRepo)). + AddDynamicArguments(commitID)) + }) +} diff --git a/package/gitea/source/modules/gitrepo/fsck.go b/package/gitea/source/modules/gitrepo/fsck.go index ffccff28..f74ca3b4 100644 --- a/package/gitea/source/modules/gitrepo/fsck.go +++ b/package/gitea/source/modules/gitrepo/fsck.go @@ -12,5 +12,5 @@ import ( // Fsck verifies the connectivity and validity of the objects in the database func Fsck(ctx context.Context, repo Repository, timeout time.Duration, args gitcmd.TrustedCmdArgs) error { - return gitcmd.NewCommand("fsck").AddArguments(args...).Run(ctx, &gitcmd.RunOpts{Timeout: timeout, Dir: repoPath(repo)}) + return RunCmd(ctx, repo, gitcmd.NewCommand("fsck").AddArguments(args...).WithTimeout(timeout)) } diff --git a/package/gitea/source/modules/gitrepo/gitrepo.go b/package/gitea/source/modules/gitrepo/gitrepo.go index fad8f70c..535d72ed 100644 --- a/package/gitea/source/modules/gitrepo/gitrepo.go +++ b/package/gitea/source/modules/gitrepo/gitrepo.go @@ -7,9 +7,12 @@ import ( "context" "fmt" "io" + "io/fs" + "os" "path/filepath" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" @@ -20,9 +23,9 @@ type Repository interface { RelativePath() string // We don't assume how the directory structure of the repository is, so we only need the relative path } -// RelativePath should be an unix style path like username/reponame.git -// This method should change it according to the current OS. -func repoPath(repo Repository) string { +// repoPath resolves the Repository.RelativePath (which is a unix-style path like "username/reponame.git") +// to a local filesystem path according to setting.RepoRootPath +var repoPath = func(repo Repository) string { return filepath.Join(setting.RepoRootPath, filepath.FromSlash(repo.RelativePath())) } @@ -77,7 +80,12 @@ func DeleteRepository(ctx context.Context, repo Repository) error { // RenameRepository renames a repository's name on disk func RenameRepository(ctx context.Context, repo, newRepo Repository) error { - if err := util.Rename(repoPath(repo), repoPath(newRepo)); err != nil { + dstDir := repoPath(newRepo) + if err := os.MkdirAll(filepath.Dir(dstDir), os.ModePerm); err != nil { + return fmt.Errorf("Failed to create dir %s: %w", filepath.Dir(dstDir), err) + } + + if err := util.Rename(repoPath(repo), dstDir); err != nil { return fmt.Errorf("rename repository directory: %w", err) } return nil @@ -86,3 +94,35 @@ func RenameRepository(ctx context.Context, repo, newRepo Repository) error { func InitRepository(ctx context.Context, repo Repository, objectFormatName string) error { return git.InitRepository(ctx, repoPath(repo), true, objectFormatName) } + +func UpdateServerInfo(ctx context.Context, repo Repository) error { + _, _, err := RunCmdBytes(ctx, repo, gitcmd.NewCommand("update-server-info")) + return err +} + +func GetRepoFS(repo Repository) fs.FS { + return os.DirFS(repoPath(repo)) +} + +func IsRepoFileExist(ctx context.Context, repo Repository, relativeFilePath string) (bool, error) { + absoluteFilePath := filepath.Join(repoPath(repo), relativeFilePath) + return util.IsExist(absoluteFilePath) +} + +func IsRepoDirExist(ctx context.Context, repo Repository, relativeDirPath string) (bool, error) { + absoluteDirPath := filepath.Join(repoPath(repo), relativeDirPath) + return util.IsDir(absoluteDirPath) +} + +func RemoveRepoFileOrDir(ctx context.Context, repo Repository, relativeFileOrDirPath string) error { + absoluteFilePath := filepath.Join(repoPath(repo), relativeFileOrDirPath) + return util.Remove(absoluteFilePath) +} + +func CreateRepoFile(ctx context.Context, repo Repository, relativeFilePath string) (io.WriteCloser, error) { + absoluteFilePath := filepath.Join(repoPath(repo), relativeFilePath) + if err := os.MkdirAll(filepath.Dir(absoluteFilePath), os.ModePerm); err != nil { + return nil, err + } + return os.Create(absoluteFilePath) +} diff --git a/package/gitea/source/modules/gitrepo/main_test.go b/package/gitea/source/modules/gitrepo/main_test.go new file mode 100644 index 00000000..08afdffc --- /dev/null +++ b/package/gitea/source/modules/gitrepo/main_test.go @@ -0,0 +1,25 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" +) + +func TestMain(m *testing.M) { + // resolve repository path relative to the test directory + setting.SetupGiteaTestEnv() + giteaRoot := setting.GetGiteaTestSourceRoot() + repoPath = func(repo Repository) string { + if filepath.IsAbs(repo.RelativePath()) { + return repo.RelativePath() // for testing purpose only + } + return filepath.Join(giteaRoot, "modules/git/tests/repos", repo.RelativePath()) + } + git.RunGitTests(m) +} diff --git a/package/gitea/source/modules/gitrepo/merge.go b/package/gitea/source/modules/gitrepo/merge.go new file mode 100644 index 00000000..92803da6 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/merge.go @@ -0,0 +1,26 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + "fmt" + "strings" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/util" +) + +// MergeBase checks and returns merge base of two commits. +func MergeBase(ctx context.Context, repo Repository, baseCommitID, headCommitID string) (string, error) { + mergeBase, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base"). + AddDashesAndList(baseCommitID, headCommitID)) + if err != nil { + if gitcmd.IsErrorExitCode(err, 1) { + return "", util.NewNotExistErrorf("get merge-base of %s and %s failed", baseCommitID, headCommitID) + } + return "", fmt.Errorf("get merge-base of %s and %s failed: %w", baseCommitID, headCommitID, err) + } + return strings.TrimSpace(mergeBase), nil +} diff --git a/package/gitea/source/modules/gitrepo/merge_tree.go b/package/gitea/source/modules/gitrepo/merge_tree.go new file mode 100644 index 00000000..6151b117 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/merge_tree.go @@ -0,0 +1,59 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "bufio" + "context" + "fmt" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/util" +) + +const MaxConflictedDetectFiles = 10 + +// MergeTree performs a merge between two commits (baseRef and headRef) with an optional merge base. +// It returns the resulting tree hash, a list of conflicted files (if any), and an error if the operation fails. +// If there are no conflicts, the list of conflicted files will be nil. +func MergeTree(ctx context.Context, repo Repository, baseRef, headRef, mergeBase string) (treeID string, isErrHasConflicts bool, conflictFiles []string, _ error) { + cmd := gitcmd.NewCommand("merge-tree", "--write-tree", "-z", "--name-only", "--no-messages"). + AddOptionFormat("--merge-base=%s", mergeBase). + AddDynamicArguments(baseRef, headRef) + + stdout, stdoutClose := cmd.MakeStdoutPipe() + defer stdoutClose() + cmd.WithPipelineFunc(func(ctx gitcmd.Context) error { + // https://git-scm.com/docs/git-merge-tree/2.38.0#OUTPUT + // For a conflicted merge, the output is: + // NUL + // NUL + // NUL + // ... + scanner := bufio.NewScanner(stdout) + scanner.Split(util.BufioScannerSplit(0)) + for scanner.Scan() { + line := scanner.Text() + if treeID == "" { // first line is tree ID + treeID = line + continue + } + conflictFiles = append(conflictFiles, line) + if len(conflictFiles) >= MaxConflictedDetectFiles { + break + } + } + return scanner.Err() + }) + + err := RunCmdWithStderr(ctx, repo, cmd) + // For a successful, non-conflicted merge, the exit status is 0. When the merge has conflicts, the exit status is 1. + // A merge can have conflicts without having individual files conflict + // https://git-scm.com/docs/git-merge-tree/2.38.0#_mistakes_to_avoid + isErrHasConflicts = gitcmd.IsErrorExitCode(err, 1) + if err == nil || isErrHasConflicts { + return treeID, isErrHasConflicts, conflictFiles, nil + } + return "", false, nil, fmt.Errorf("run merge-tree failed: %w", err) +} diff --git a/package/gitea/source/modules/gitrepo/merge_tree_test.go b/package/gitea/source/modules/gitrepo/merge_tree_test.go new file mode 100644 index 00000000..9327a0c3 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/merge_tree_test.go @@ -0,0 +1,82 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/modules/git/gitcmd" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func prepareRepoDirRenameConflict(t *testing.T) string { + repoDir := filepath.Join(t.TempDir(), "repo-dir-rename-conflict.git") + require.NoError(t, gitcmd.NewCommand("init", "--bare").AddDynamicArguments(repoDir).Run(t.Context())) + stdin := `blob +mark :1 +data 2 +b + +blob +mark :2 +data 2 +c + +reset refs/heads/master +commit refs/heads/master +mark :3 +author test 1769202331 -0800 +committer test 1769202331 -0800 +data 2 +O +M 100644 :1 z/b +M 100644 :2 z/c + +commit refs/heads/split +mark :4 +author test 1769202336 -0800 +committer test 1769202336 -0800 +data 2 +A +from :3 +M 100644 :2 w/c +M 100644 :1 y/b +D z/b +D z/c + +blob +mark :5 +data 2 +d + +commit refs/heads/add +mark :6 +author test 1769202342 -0800 +committer test 1769202342 -0800 +data 2 +B +from :3 +M 100644 :5 z/d +` + require.NoError(t, gitcmd.NewCommand("fast-import").WithDir(repoDir).WithStdinBytes([]byte(stdin)).Run(t.Context())) + return repoDir +} + +func TestMergeTreeDirectoryRenameConflictWithoutFiles(t *testing.T) { + repoDir := prepareRepoDirRenameConflict(t) + require.DirExists(t, repoDir) + repo := &mockRepository{path: repoDir} + + mergeBase, err := MergeBase(t.Context(), repo, "add", "split") + require.NoError(t, err) + + treeID, conflicted, conflictedFiles, err := MergeTree(t.Context(), repo, "add", "split", mergeBase) + require.NoError(t, err) + assert.True(t, conflicted) + assert.Empty(t, conflictedFiles) + assert.Equal(t, "5e3dd4cfc5b11e278a35b2daa83b7274175e3ab1", treeID) +} diff --git a/package/gitea/source/modules/gitrepo/push.go b/package/gitea/source/modules/gitrepo/push.go new file mode 100644 index 00000000..920c317f --- /dev/null +++ b/package/gitea/source/modules/gitrepo/push.go @@ -0,0 +1,27 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +// PushToExternal pushes a managed repository to an external remote. +func PushToExternal(ctx context.Context, repo Repository, opts git.PushOptions) error { + return git.Push(ctx, repoPath(repo), opts) +} + +// Push pushes from one managed repository to another managed repository. +func Push(ctx context.Context, fromRepo, toRepo Repository, opts git.PushOptions) error { + opts.Remote = repoPath(toRepo) + return git.Push(ctx, repoPath(fromRepo), opts) +} + +// PushFromLocal pushes from a local path to a managed repository. +func PushFromLocal(ctx context.Context, fromLocalPath string, toRepo Repository, opts git.PushOptions) error { + opts.Remote = repoPath(toRepo) + return git.Push(ctx, fromLocalPath, opts) +} diff --git a/package/gitea/source/modules/gitrepo/ref.go b/package/gitea/source/modules/gitrepo/ref.go index babef8b6..52125283 100644 --- a/package/gitea/source/modules/gitrepo/ref.go +++ b/package/gitea/source/modules/gitrepo/ref.go @@ -10,12 +10,10 @@ import ( ) func UpdateRef(ctx context.Context, repo Repository, refName, newCommitID string) error { - _, _, err := gitcmd.NewCommand("update-ref").AddDynamicArguments(refName, newCommitID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) - return err + return RunCmd(ctx, repo, gitcmd.NewCommand("update-ref").AddDynamicArguments(refName, newCommitID)) } func RemoveRef(ctx context.Context, repo Repository, refName string) error { - _, _, err := gitcmd.NewCommand("update-ref", "--no-deref", "-d"). - AddDynamicArguments(refName).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) - return err + return RunCmd(ctx, repo, gitcmd.NewCommand("update-ref", "--no-deref", "-d"). + AddDynamicArguments(refName)) } diff --git a/package/gitea/source/modules/gitrepo/remote.go b/package/gitea/source/modules/gitrepo/remote.go index 20d3cd21..3cbc34ee 100644 --- a/package/gitea/source/modules/gitrepo/remote.go +++ b/package/gitea/source/modules/gitrepo/remote.go @@ -6,8 +6,6 @@ package gitrepo import ( "context" "errors" - "io" - "time" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" @@ -36,9 +34,7 @@ func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL st return errors.New("unknown remote option: " + string(options[0])) } } - _, _, err := cmd. - AddDynamicArguments(remoteName, remoteURL). - RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) + _, _, err := RunCmdString(ctx, repo, cmd.AddDynamicArguments(remoteName, remoteURL)) return err }) } @@ -46,7 +42,7 @@ func GitRemoteAdd(ctx context.Context, repo Repository, remoteName, remoteURL st func GitRemoteRemove(ctx context.Context, repo Repository, remoteName string) error { return globallock.LockAndDo(ctx, getRepoConfigLockKey(repo.RelativePath()), func(ctx context.Context) error { cmd := gitcmd.NewCommand("remote", "rm").AddDynamicArguments(remoteName) - _, _, err := cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath(repo)}) + _, _, err := RunCmdString(ctx, repo, cmd) return err }) } @@ -62,25 +58,3 @@ func GitRemoteGetURL(ctx context.Context, repo Repository, remoteName string) (* } return giturl.ParseGitURL(addr) } - -// GitRemotePrune prunes the remote branches that no longer exist in the remote repository. -func GitRemotePrune(ctx context.Context, repo Repository, remoteName string, timeout time.Duration, stdout, stderr io.Writer) error { - return gitcmd.NewCommand("remote", "prune").AddDynamicArguments(remoteName). - Run(ctx, &gitcmd.RunOpts{ - Timeout: timeout, - Dir: repoPath(repo), - Stdout: stdout, - Stderr: stderr, - }) -} - -// GitRemoteUpdatePrune updates the remote branches and prunes the ones that no longer exist in the remote repository. -func GitRemoteUpdatePrune(ctx context.Context, repo Repository, remoteName string, timeout time.Duration, stdout, stderr io.Writer) error { - return gitcmd.NewCommand("remote", "update", "--prune").AddDynamicArguments(remoteName). - Run(ctx, &gitcmd.RunOpts{ - Timeout: timeout, - Dir: repoPath(repo), - Stdout: stdout, - Stderr: stderr, - }) -} diff --git a/package/gitea/source/modules/gitrepo/repo_lock.go b/package/gitea/source/modules/gitrepo/repo_lock.go new file mode 100644 index 00000000..2eb89ce8 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/repo_lock.go @@ -0,0 +1,10 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +// getRepoWriteLockKey returns the global lock key for write operations on the repository. +// Parallel write operations on the same git repository should be avoided to prevent data corruption. +func getRepoWriteLockKey(repoStoragePath string) string { + return "repo-write:" + repoStoragePath +} diff --git a/package/gitea/source/modules/gitrepo/signing.go b/package/gitea/source/modules/gitrepo/signing.go new file mode 100644 index 00000000..2f77758d --- /dev/null +++ b/package/gitea/source/modules/gitrepo/signing.go @@ -0,0 +1,14 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "context" + + "code.gitea.io/gitea/modules/git" +) + +func GetSigningKey(ctx context.Context) (*git.SigningKey, *git.Signature) { + return git.GetSigningKey(ctx) +} diff --git a/package/gitea/source/modules/gitrepo/size.go b/package/gitea/source/modules/gitrepo/size.go new file mode 100644 index 00000000..7524bb25 --- /dev/null +++ b/package/gitea/source/modules/gitrepo/size.go @@ -0,0 +1,37 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gitrepo + +import ( + "os" + "path/filepath" +) + +const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular + +// CalcRepositorySize returns the disk consumption for a given path +func CalcRepositorySize(repo Repository) (int64, error) { + var size int64 + err := filepath.WalkDir(repoPath(repo), func(_ string, entry os.DirEntry, err error) error { + if os.IsNotExist(err) { // ignore the error because some files (like temp/lock file) may be deleted during traversing. + return nil + } else if err != nil { + return err + } + if entry.IsDir() { + return nil + } + info, err := entry.Info() + if os.IsNotExist(err) { // ignore the error as above + return nil + } else if err != nil { + return err + } + if (info.Mode() & notRegularFileMode) == 0 { + size += info.Size() + } + return nil + }) + return size, err +} diff --git a/package/gitea/source/modules/graceful/manager.go b/package/gitea/source/modules/graceful/manager.go index ee1872b9..51bd5a23 100644 --- a/package/gitea/source/modules/graceful/manager.go +++ b/package/gitea/source/modules/graceful/manager.go @@ -74,12 +74,6 @@ func (g *Manager) RunWithCancel(rc RunCanceler) { g.RunAtShutdown(context.Background(), rc.Cancel) g.runningServerWaitGroup.Add(1) defer g.runningServerWaitGroup.Done() - defer func() { - if err := recover(); err != nil { - log.Critical("PANIC during RunWithCancel: %v\nStacktrace: %s", err, log.Stack(2)) - g.doShutdown() - } - }() rc.Run() } @@ -89,12 +83,6 @@ func (g *Manager) RunWithCancel(rc RunCanceler) { func (g *Manager) RunWithShutdownContext(run func(context.Context)) { g.runningServerWaitGroup.Add(1) defer g.runningServerWaitGroup.Done() - defer func() { - if err := recover(); err != nil { - log.Critical("PANIC during RunWithShutdownContext: %v\nStacktrace: %s", err, log.Stack(2)) - g.doShutdown() - } - }() ctx := g.ShutdownContext() pprof.SetGoroutineLabels(ctx) // We don't have a label to restore back to but I think this is fine run(ctx) diff --git a/package/gitea/source/modules/graceful/net_unix.go b/package/gitea/source/modules/graceful/net_unix.go index 796e0050..a06f78da 100644 --- a/package/gitea/source/modules/graceful/net_unix.go +++ b/package/gitea/source/modules/graceful/net_unix.go @@ -290,11 +290,11 @@ func getActiveListenersToUnlink() []bool { func getNotifySocket() (*net.UnixConn, error) { if err := getProvidedFDs(); err != nil { // This error will be logged elsewhere - return nil, nil + return nil, nil //nolint:nilnil // return nil when no provided FDs are available } if notifySocketAddr == "" { - return nil, nil + return nil, nil //nolint:nilnil // return nil when notify socket is not configured } socketAddr := &net.UnixAddr{ diff --git a/package/gitea/source/modules/graceful/server.go b/package/gitea/source/modules/graceful/server.go index 2525a83e..b440f68a 100644 --- a/package/gitea/source/modules/graceful/server.go +++ b/package/gitea/source/modules/graceful/server.go @@ -11,7 +11,6 @@ import ( "os" "strings" "sync" - "sync/atomic" "syscall" "time" @@ -30,12 +29,15 @@ type ServeFunction = func(net.Listener) error // Server represents our graceful server type Server struct { - network string - address string - listener net.Listener - wg sync.WaitGroup - state state - lock *sync.RWMutex + network string + address string + listener net.Listener + + lock sync.RWMutex + state state + connCounter int64 + connEmptyCond *sync.Cond + BeforeBegin func(network, address string) OnShutdown func() PerWriteTimeout time.Duration @@ -50,14 +52,13 @@ func NewServer(network, address, name string) *Server { log.Info("Starting new %s server: %s:%s on PID: %d", name, network, address, os.Getpid()) } srv := &Server{ - wg: sync.WaitGroup{}, state: stateInit, - lock: &sync.RWMutex{}, network: network, address: address, PerWriteTimeout: setting.PerWriteTimeout, PerWritePerKbTimeout: setting.PerWritePerKbTimeout, } + srv.connEmptyCond = sync.NewCond(&srv.lock) srv.BeforeBegin = func(network, addr string) { log.Debug("Starting server on %s:%s (PID: %d)", network, addr, syscall.Getpid()) @@ -154,7 +155,7 @@ func (srv *Server) Serve(serve ServeFunction) error { GetManager().RegisterServer() err := serve(srv.listener) log.Debug("Waiting for connections to finish... (PID: %d)", syscall.Getpid()) - srv.wg.Wait() + srv.waitForActiveConnections() srv.setState(stateTerminate) GetManager().ServerDone() // use of closed means that the listeners are closed - i.e. we should be shutting down - return nil @@ -178,16 +179,62 @@ func (srv *Server) setState(st state) { srv.state = st } +func (srv *Server) waitForActiveConnections() { + srv.lock.Lock() + for srv.connCounter > 0 { + srv.connEmptyCond.Wait() + } + srv.lock.Unlock() +} + +func (srv *Server) wrapConnection(c net.Conn) (net.Conn, error) { + srv.lock.Lock() + defer srv.lock.Unlock() + + if srv.state != stateRunning { + _ = c.Close() + return nil, syscall.EINVAL // same as AcceptTCP + } + + srv.connCounter++ + return &wrappedConn{Conn: c, server: srv}, nil +} + +func (srv *Server) removeConnection(_ *wrappedConn) { + srv.lock.Lock() + defer srv.lock.Unlock() + + srv.connCounter-- + if srv.connCounter <= 0 { + srv.connEmptyCond.Broadcast() + } +} + +// closeAllConnections forcefully closes all active connections +func (srv *Server) closeAllConnections() { + srv.lock.Lock() + if srv.connCounter > 0 { + log.Warn("After graceful shutdown period, %d connections are still active. Forcefully close.", srv.connCounter) + srv.connCounter = 0 // OS will close all the connections after the process exits, so we just assume there is no active connection now + } + srv.lock.Unlock() + srv.connEmptyCond.Broadcast() +} + type filer interface { File() (*os.File, error) } type wrappedListener struct { net.Listener - stopped bool - server *Server + server *Server } +var ( + _ net.Listener = (*wrappedListener)(nil) + _ filer = (*wrappedListener)(nil) +) + func newWrappedListener(l net.Listener, srv *Server) *wrappedListener { return &wrappedListener{ Listener: l, @@ -195,46 +242,24 @@ func newWrappedListener(l net.Listener, srv *Server) *wrappedListener { } } -func (wl *wrappedListener) Accept() (net.Conn, error) { - var c net.Conn - // Set keepalive on TCPListeners connections. +func (wl *wrappedListener) Accept() (c net.Conn, err error) { if tcl, ok := wl.Listener.(*net.TCPListener); ok { + // Set keepalive on TCPListeners connections if possible, see http.tcpKeepAliveListener tc, err := tcl.AcceptTCP() if err != nil { return nil, err } - _ = tc.SetKeepAlive(true) // see http.tcpKeepAliveListener - _ = tc.SetKeepAlivePeriod(3 * time.Minute) // see http.tcpKeepAliveListener + _ = tc.SetKeepAlive(true) + _ = tc.SetKeepAlivePeriod(3 * time.Minute) c = tc } else { - var err error c, err = wl.Listener.Accept() if err != nil { return nil, err } } - closed := int32(0) - - c = &wrappedConn{ - Conn: c, - server: wl.server, - closed: &closed, - perWriteTimeout: wl.server.PerWriteTimeout, - perWritePerKbTimeout: wl.server.PerWritePerKbTimeout, - } - - wl.server.wg.Add(1) - return c, nil -} - -func (wl *wrappedListener) Close() error { - if wl.stopped { - return syscall.EINVAL - } - - wl.stopped = true - return wl.Listener.Close() + return wl.server.wrapConnection(c) } func (wl *wrappedListener) File() (*os.File, error) { @@ -244,17 +269,14 @@ func (wl *wrappedListener) File() (*os.File, error) { type wrappedConn struct { net.Conn - server *Server - closed *int32 - deadline time.Time - perWriteTimeout time.Duration - perWritePerKbTimeout time.Duration + server *Server + deadline time.Time } func (w *wrappedConn) Write(p []byte) (n int, err error) { - if w.perWriteTimeout > 0 { - minTimeout := time.Duration(len(p)/1024) * w.perWritePerKbTimeout - minDeadline := time.Now().Add(minTimeout).Add(w.perWriteTimeout) + if w.server.PerWriteTimeout > 0 { + minTimeout := time.Duration(len(p)/1024) * w.server.PerWritePerKbTimeout + minDeadline := time.Now().Add(minTimeout).Add(w.server.PerWriteTimeout) w.deadline = w.deadline.Add(minTimeout) if minDeadline.After(w.deadline) { @@ -266,19 +288,6 @@ func (w *wrappedConn) Write(p []byte) (n int, err error) { } func (w *wrappedConn) Close() error { - if atomic.CompareAndSwapInt32(w.closed, 0, 1) { - defer func() { - if err := recover(); err != nil { - select { - case <-GetManager().IsHammer(): - // Likely deadlocked request released at hammertime - log.Warn("Panic during connection close! %v. Likely there has been a deadlocked request which has been released by forced shutdown.", err) - default: - log.Error("Panic during connection close! %v", err) - } - } - }() - w.server.wg.Done() - } + w.server.removeConnection(w) return w.Conn.Close() } diff --git a/package/gitea/source/modules/graceful/server_hooks.go b/package/gitea/source/modules/graceful/server_hooks.go index 9b675895..b800c325 100644 --- a/package/gitea/source/modules/graceful/server_hooks.go +++ b/package/gitea/source/modules/graceful/server_hooks.go @@ -5,7 +5,6 @@ package graceful import ( "os" - "runtime" "code.gitea.io/gitea/modules/log" ) @@ -48,26 +47,8 @@ func (srv *Server) doShutdown() { } func (srv *Server) doHammer() { - defer func() { - // We call srv.wg.Done() until it panics. - // This happens if we call Done() when the WaitGroup counter is already at 0 - // So if it panics -> we're done, Serve() will return and the - // parent will goroutine will exit. - if r := recover(); r != nil { - log.Error("WaitGroup at 0: Error: %v", r) - } - }() if srv.getState() != stateShuttingDown { return } - log.Warn("Forcefully shutting down parent") - for { - if srv.getState() == stateTerminate { - break - } - srv.wg.Done() - - // Give other goroutines a chance to finish before we forcibly stop them. - runtime.Gosched() - } + srv.closeAllConnections() } diff --git a/package/gitea/source/modules/graceful/server_http.go b/package/gitea/source/modules/graceful/server_http.go index 7c855ac6..77a2c3b6 100644 --- a/package/gitea/source/modules/graceful/server_http.go +++ b/package/gitea/source/modules/graceful/server_http.go @@ -12,7 +12,12 @@ import ( func newHTTPServer(network, address, name string, handler http.Handler) (*Server, ServeFunction) { server := NewServer(network, address, name) + protocols := http.Protocols{} + protocols.SetHTTP1(true) + protocols.SetHTTP2(true) // HTTP/2 can only be used when Gitea is configured to use TLS + protocols.SetUnencryptedHTTP2(true) // Allow HTTP/2 without TLS, in case Gitea is behind a reverse proxy httpServer := http.Server{ + Protocols: &protocols, Handler: handler, BaseContext: func(net.Listener) context.Context { return GetManager().HammerContext() }, } diff --git a/package/gitea/source/modules/hcaptcha/hcaptcha_test.go b/package/gitea/source/modules/hcaptcha/hcaptcha_test.go index 5906faf1..6b207bfb 100644 --- a/package/gitea/source/modules/hcaptcha/hcaptcha_test.go +++ b/package/gitea/source/modules/hcaptcha/hcaptcha_test.go @@ -8,7 +8,6 @@ import ( "io" "net/http" "net/url" - "os" "strings" "testing" "time" @@ -20,10 +19,6 @@ const ( dummyToken = "10000000-aaaa-bbbb-cccc-000000000001" ) -func TestMain(m *testing.M) { - os.Exit(m.Run()) -} - type mockTransport struct{} func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { diff --git a/package/gitea/source/modules/highlight/highlight.go b/package/gitea/source/modules/highlight/highlight.go index 77f24fa3..dca28588 100644 --- a/package/gitea/source/modules/highlight/highlight.go +++ b/package/gitea/source/modules/highlight/highlight.go @@ -5,221 +5,159 @@ package highlight import ( - "bufio" "bytes" - "fmt" gohtml "html" "html/template" - "io" - "path" - "path/filepath" - "strings" "sync" - "code.gitea.io/gitea/modules/analyze" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" "github.com/alecthomas/chroma/v2" - "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/alecthomas/chroma/v2/lexers" + chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/alecthomas/chroma/v2/styles" - lru "github.com/hashicorp/golang-lru/v2" ) -// don't index files larger than this many bytes for performance purposes +// don't highlight files larger than this many bytes for performance purposes const sizeLimit = 1024 * 1024 -var ( - // For custom user mapping - highlightMapping = map[string]string{} - - once sync.Once - - cache *lru.TwoQueueCache[string, any] - - githubStyles = styles.Get("github") -) - -// NewContext loads custom highlight map from local config -func NewContext() { - once.Do(func() { - highlightMapping = setting.GetHighlightMapping() - - // The size 512 is simply a conservative rule of thumb - c, err := lru.New2Q[string, any](512) - if err != nil { - panic(fmt.Sprintf("failed to initialize LRU cache for highlighter: %s", err)) - } - cache = c - }) +type globalVarsType struct { + highlightMapping map[string]string + githubStyles *chroma.Style } -// Code returns a HTML version of code string with chroma syntax highlighting classes and the matched lexer name -func Code(fileName, language, code string) (output template.HTML, lexerName string) { - NewContext() +var ( + globalVarsMu sync.Mutex + globalVarsPtr *globalVarsType +) +func globalVars() *globalVarsType { + // in the future, the globalVars might need to be re-initialized when settings change, so don't use sync.Once here + globalVarsMu.Lock() + defer globalVarsMu.Unlock() + if globalVarsPtr == nil { + globalVarsPtr = &globalVarsType{} + globalVarsPtr.githubStyles = styles.Get("github") + globalVarsPtr.highlightMapping = setting.GetHighlightMapping() + } + return globalVarsPtr +} + +// UnsafeSplitHighlightedLines splits highlighted code into lines preserving HTML tags +// It always includes '\n', '\n' can appear at the end of each line or in the middle of HTML tags +// The '\n' is necessary for copying code from web UI to preserve original code lines +// ATTENTION: It uses the unsafe conversion between string and []byte for performance reason +// DO NOT make any modification to the returned [][]byte slice items +func UnsafeSplitHighlightedLines(code template.HTML) (ret [][]byte) { + buf := util.UnsafeStringToBytes(string(code)) + lineCount := bytes.Count(buf, []byte("\n")) + 1 + ret = make([][]byte, 0, lineCount) + nlTagClose := []byte("\n 0 { + ret = append(ret, buf) + } + return ret + } + // Chroma highlighting output sometimes have "" right after \n, sometimes before. + // * "text\n" + // * "text\n" + if bytes.HasPrefix(buf[pos:], nlTagClose) { + pos1 := bytes.IndexByte(buf[pos:], '>') + if pos1 != -1 { + pos += pos1 + } + } + ret = append(ret, buf[:pos+1]) + buf = buf[pos+1:] + } +} + +func htmlEscape(code string) template.HTML { + return template.HTML(gohtml.EscapeString(code)) +} + +// RenderCodeSlowGuess tries to get a lexer by file name and language first, +// if not found, it will try to guess the lexer by code content, which is slow (more than several hundreds of milliseconds). +func RenderCodeSlowGuess(fileName, language, code string) (output template.HTML, lexer chroma.Lexer, lexerDisplayName string) { // diff view newline will be passed as empty, change to literal '\n' so it can be copied // preserve literal newline in blame view if code == "" || code == "\n" { - return "\n", "" + return "\n", nil, "" } if len(code) > sizeLimit { - return template.HTML(template.HTMLEscapeString(code)), "" + return htmlEscape(code), nil, "" } - var lexer chroma.Lexer - - if len(language) > 0 { - lexer = lexers.Get(language) - - if lexer == nil { - // Attempt stripping off the '?' - if idx := strings.IndexByte(language, '?'); idx > 0 { - lexer = lexers.Get(language[:idx]) - } - } - } - - if lexer == nil { - if val, ok := highlightMapping[path.Ext(fileName)]; ok { - // use mapped value to find lexer - lexer = lexers.Get(val) - } - } - - if lexer == nil { - if l, ok := cache.Get(fileName); ok { - lexer = l.(chroma.Lexer) - } - } - - if lexer == nil { - lexer = lexers.Match(fileName) - if lexer == nil { - lexer = lexers.Fallback - } - cache.Add(fileName, lexer) - } - - return CodeFromLexer(lexer, code), formatLexerName(lexer.Config().Name) + lexer = detectChromaLexerWithAnalyze(fileName, language, util.UnsafeStringToBytes(code)) // it is also slow + return RenderCodeByLexer(lexer, code), lexer, formatLexerName(lexer.Config().Name) } -// CodeFromLexer returns a HTML version of code string with chroma syntax highlighting classes -func CodeFromLexer(lexer chroma.Lexer, code string) template.HTML { - formatter := html.New(html.WithClasses(true), - html.WithLineNumbers(false), - html.PreventSurroundingPre(true), +// RenderCodeByLexer returns a HTML version of code string with chroma syntax highlighting classes +func RenderCodeByLexer(lexer chroma.Lexer, code string) template.HTML { + formatter := chromahtml.New(chromahtml.WithClasses(true), + chromahtml.WithLineNumbers(false), + chromahtml.PreventSurroundingPre(true), ) - htmlbuf := bytes.Buffer{} - htmlw := bufio.NewWriter(&htmlbuf) - iterator, err := lexer.Tokenise(nil, code) if err != nil { log.Error("Can't tokenize code: %v", err) - return template.HTML(template.HTMLEscapeString(code)) + return htmlEscape(code) } + + htmlBuf := &bytes.Buffer{} // style not used for live site but need to pass something - err = formatter.Format(htmlw, githubStyles, iterator) + err = formatter.Format(htmlBuf, globalVars().githubStyles, iterator) if err != nil { log.Error("Can't format code: %v", err) - return template.HTML(template.HTMLEscapeString(code)) + return htmlEscape(code) } - - _ = htmlw.Flush() - // Chroma will add newlines for certain lexers in order to highlight them properly - // Once highlighted, strip them here, so they don't cause copy/paste trouble in HTML output - return template.HTML(strings.TrimSuffix(htmlbuf.String(), "\n")) + return template.HTML(util.UnsafeBytesToString(htmlBuf.Bytes())) } -// File returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name -func File(fileName, language string, code []byte) ([]template.HTML, string, error) { - NewContext() - - if len(code) > sizeLimit { - return PlainText(code), "", nil +// RenderFullFile returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name +func RenderFullFile(fileName, language string, code []byte) ([]template.HTML, string) { + if language == LanguagePlaintext || len(code) > sizeLimit { + return renderPlainText(code), formatLexerName(LanguagePlaintext) } - - formatter := html.New(html.WithClasses(true), - html.WithLineNumbers(false), - html.PreventSurroundingPre(true), - ) - - var lexer chroma.Lexer - - // provided language overrides everything - if language != "" { - lexer = lexers.Get(language) - } - - if lexer == nil { - if val, ok := highlightMapping[filepath.Ext(fileName)]; ok { - lexer = lexers.Get(val) - } - } - - if lexer == nil { - guessLanguage := analyze.GetCodeLanguage(fileName, code) - - lexer = lexers.Get(guessLanguage) - if lexer == nil { - lexer = lexers.Match(fileName) - if lexer == nil { - lexer = lexers.Fallback - } - } - } - + lexer := detectChromaLexerWithAnalyze(fileName, language, code) lexerName := formatLexerName(lexer.Config().Name) - - iterator, err := lexer.Tokenise(nil, string(code)) - if err != nil { - return nil, "", fmt.Errorf("can't tokenize code: %w", err) + rendered := RenderCodeByLexer(lexer, util.UnsafeBytesToString(code)) + unsafeLines := UnsafeSplitHighlightedLines(rendered) + lines := make([]template.HTML, len(unsafeLines)) + for idx, lineBytes := range unsafeLines { + lines[idx] = template.HTML(util.UnsafeBytesToString(lineBytes)) } - - tokensLines := chroma.SplitTokensIntoLines(iterator.Tokens()) - htmlBuf := &bytes.Buffer{} - - lines := make([]template.HTML, 0, len(tokensLines)) - for _, tokens := range tokensLines { - iterator = chroma.Literator(tokens...) - err = formatter.Format(htmlBuf, githubStyles, iterator) - if err != nil { - return nil, "", fmt.Errorf("can't format code: %w", err) - } - lines = append(lines, template.HTML(htmlBuf.String())) - htmlBuf.Reset() - } - - return lines, lexerName, nil + return lines, lexerName } -// PlainText returns non-highlighted HTML for code -func PlainText(code []byte) []template.HTML { - r := bufio.NewReader(bytes.NewReader(code)) - m := make([]template.HTML, 0, bytes.Count(code, []byte{'\n'})+1) - for { - content, err := r.ReadString('\n') - if err != nil && err != io.EOF { - log.Error("failed to read string from buffer: %v", err) - break +// renderPlainText returns non-highlighted HTML for code +func renderPlainText(code []byte) []template.HTML { + lines := make([]template.HTML, 0, bytes.Count(code, []byte{'\n'})+1) + pos := 0 + for pos < len(code) { + var content []byte + nextPos := bytes.IndexByte(code[pos:], '\n') + if nextPos == -1 { + content = code[pos:] + pos = len(code) + } else { + content = code[pos : pos+nextPos+1] + pos += nextPos + 1 } - if content == "" && err == io.EOF { - break - } - s := template.HTML(gohtml.EscapeString(content)) - m = append(m, s) + lines = append(lines, htmlEscape(util.UnsafeBytesToString(content))) } - return m + return lines } func formatLexerName(name string) string { - if name == "fallback" { + if name == LanguagePlaintext || name == chromaLexerFallback { return "Plaintext" } - return util.ToTitleCaseNoLower(name) } diff --git a/package/gitea/source/modules/highlight/highlight_test.go b/package/gitea/source/modules/highlight/highlight_test.go index b36de98c..211132b2 100644 --- a/package/gitea/source/modules/highlight/highlight_test.go +++ b/package/gitea/source/modules/highlight/highlight_test.go @@ -108,12 +108,17 @@ c=2 ), lexerName: "Python", }, + { + name: "test.sql", + code: "--\nSELECT", + want: []template.HTML{"--\n", `SELECT`}, + lexerName: "SQL", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - out, lexerName, err := File(tt.name, "", []byte(tt.code)) - assert.NoError(t, err) + out, lexerName := RenderFullFile(tt.name, "", []byte(tt.code)) assert.Equal(t, tt.want, out) assert.Equal(t, tt.lexerName, lexerName) }) @@ -176,8 +181,26 @@ c=2`), for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - out := PlainText([]byte(tt.code)) + out := renderPlainText([]byte(tt.code)) assert.Equal(t, tt.want, out) }) } } + +func TestUnsafeSplitHighlightedLines(t *testing.T) { + ret := UnsafeSplitHighlightedLines("") + assert.Empty(t, ret) + + ret = UnsafeSplitHighlightedLines("a") + assert.Len(t, ret, 1) + assert.Equal(t, "a", string(ret[0])) + + ret = UnsafeSplitHighlightedLines("\n") + assert.Len(t, ret, 1) + assert.Equal(t, "\n", string(ret[0])) + + ret = UnsafeSplitHighlightedLines("a\nb\n") + assert.Len(t, ret, 2) + assert.Equal(t, "a\n", string(ret[0])) + assert.Equal(t, "b\n", string(ret[1])) +} diff --git a/package/gitea/source/modules/highlight/lexerdetect.go b/package/gitea/source/modules/highlight/lexerdetect.go new file mode 100644 index 00000000..fe430f46 --- /dev/null +++ b/package/gitea/source/modules/highlight/lexerdetect.go @@ -0,0 +1,311 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package highlight + +import ( + "path" + "strings" + "sync" + + "code.gitea.io/gitea/modules/analyze" + "code.gitea.io/gitea/modules/log" + + "github.com/alecthomas/chroma/v2" + "github.com/alecthomas/chroma/v2/lexers" + "github.com/go-enry/go-enry/v2" +) + +const ( + mapKeyLowerPrefix = "lower/" + LanguagePlaintext = "plaintext" + chromaLexerFallback = "fallback" +) + +// chromaLexers is fully managed by us to do fast lookup for chroma lexers by file name or language name +// Don't use lexers.Get because it is very slow in many cases (iterate all rules, filepath glob match, etc.) +var chromaLexers = sync.OnceValue(func() (ret struct { + conflictingExtLangMap map[string]string + conflictingAliasLangMap map[string]string + + lowerNameMap map[string]chroma.Lexer // lexer name (lang name) in lower-case + fileBaseMap map[string]chroma.Lexer + fileExtMap map[string]chroma.Lexer + fileParts []struct { + part string + lexer chroma.Lexer + } +}, +) { + ret.lowerNameMap = make(map[string]chroma.Lexer) + ret.fileBaseMap = make(map[string]chroma.Lexer) + ret.fileExtMap = make(map[string]chroma.Lexer) + + // Chroma has conflicts in file extension for different languages, + // When we need to do fast render, there is no way to detect the language by content, + // So we can only choose some default languages for the conflicted file extensions. + ret.conflictingExtLangMap = map[string]string{ + ".as": "ActionScript 3", // ActionScript + ".asm": "NASM", // TASM, NASM, RGBDS Assembly, Z80 Assembly + ".ASM": "NASM", + ".bas": "VB.net", // QBasic + ".bf": "Beef", // Brainfuck + ".fs": "FSharp", // Forth + ".gd": "GDScript", // GDScript3 + ".h": "C", // Objective-C + ".hcl": "Terraform", // HCL + ".hh": "C++", // HolyC + ".inc": "PHP", // ObjectPascal, POVRay, SourcePawn, PHTML + ".m": "Objective-C", // Matlab, Mathematica, Mason + ".mc": "Mason", // MonkeyC + ".network": "SYSTEMD", // INI + ".php": "PHP", // PHTML + ".php3": "PHP", // PHTML + ".php4": "PHP", // PHTML + ".php5": "PHP", // PHTML + ".pl": "Perl", // Prolog, Raku + ".pm": "Perl", // Promela, Raku + ".pp": "ObjectPascal", // Puppet + ".s": "ArmAsm", // GAS + ".S": "ArmAsm", // R, GAS + ".service": "SYSTEMD", // INI + ".socket": "SYSTEMD", // INI + ".sql": "SQL", // MySQL + ".t": "Perl", // Raku + ".ts": "TypeScript", // TypoScript + ".v": "V", // verilog + ".xslt": "HTML", // XML + } + // use widely used language names as the default mapping to resolve name alias conflict + ret.conflictingAliasLangMap = map[string]string{ + "hcl": "HCL", // Terraform + "v": "V", // verilog + } + + isPlainPattern := func(key string) bool { + return !strings.ContainsAny(key, "*?[]") // only support simple patterns + } + + setFileNameMapWithLowerKey := func(m map[string]chroma.Lexer, key string, lexer chroma.Lexer) { + if _, conflict := m[key]; conflict { + panic("duplicate key in lexer map: " + key + ", need to add it to conflictingExtLangMap") + } + m[key] = lexer + m[mapKeyLowerPrefix+strings.ToLower(key)] = lexer + } + + processFileName := func(fileName string, lexer chroma.Lexer) bool { + if isPlainPattern(fileName) { + // full base name match + setFileNameMapWithLowerKey(ret.fileBaseMap, fileName, lexer) + return true + } + if strings.HasPrefix(fileName, "*") { + // ext name match: "*.js" + fileExt := strings.Trim(fileName, "*") + if isPlainPattern(fileExt) { + presetName := ret.conflictingExtLangMap[fileExt] + if presetName == "" || lexer.Config().Name == presetName { + setFileNameMapWithLowerKey(ret.fileExtMap, fileExt, lexer) + } + return true + } + } + if strings.HasSuffix(fileName, "*") { + // part match: "*.env.*" + filePart := strings.Trim(fileName, "*") + if isPlainPattern(filePart) { + ret.fileParts = append(ret.fileParts, struct { + part string + lexer chroma.Lexer + }{ + part: filePart, + lexer: lexer, + }) + return true + } + } + return false + } + + expandGlobPatterns := func(patterns []string) []string { + // expand patterns like "file.[ch]" to "file.c" and "file.h", only one pair of "[]" is supported, enough for current Chroma lexers + for idx, s := range patterns { + idx1 := strings.IndexByte(s, '[') + idx2 := strings.IndexByte(s, ']') + if idx1 != -1 && idx2 != -1 && idx2 > idx1+1 { + left, mid, right := s[:idx1], s[idx1+1:idx2], s[idx2+1:] + patterns[idx] = left + mid[0:1] + right + for i := 1; i < len(mid); i++ { + patterns = append(patterns, left+mid[i:i+1]+right) + } + } + } + return patterns + } + + processLexerNameAliases := func(lexer chroma.Lexer) { + cfg := lexer.Config() + lowerName := strings.ToLower(cfg.Name) + if _, conflicted := ret.lowerNameMap[lowerName]; conflicted { + panic("duplicate language name in lexer map: " + lowerName) + } + ret.lowerNameMap[lowerName] = lexer + + for _, name := range cfg.Aliases { + lowerName := strings.ToLower(name) + if overriddenName, overridden := ret.conflictingAliasLangMap[lowerName]; overridden && overriddenName != cfg.Name { + continue + } + if existingLexer, conflict := ret.lowerNameMap[lowerName]; conflict && existingLexer.Config().Name != cfg.Name { + panic("duplicate alias in lexer map: " + name + ", conflict between " + existingLexer.Config().Name + " and " + cfg.Name) + } + ret.lowerNameMap[lowerName] = lexer + } + } + + // the main loop: build our lookup maps for lexers + for _, lexer := range lexers.GlobalLexerRegistry.Lexers { + cfg := lexer.Config() + processLexerNameAliases(lexer) + for _, s := range expandGlobPatterns(cfg.Filenames) { + if !processFileName(s, lexer) { + panic("unsupported file name pattern in lexer: " + s) + } + } + for _, s := range expandGlobPatterns(cfg.AliasFilenames) { + if !processFileName(s, lexer) { + panic("unsupported alias file name pattern in lexer: " + s) + } + } + } + + // final check: make sure the default overriding mapping is correct, nothing is missing + for lowerName, lexerName := range ret.conflictingAliasLangMap { + if lexer, ok := ret.lowerNameMap[lowerName]; !ok || lexer.Config().Name != lexerName { + panic("missing default name-lang mapping for: " + lowerName) + } + } + for ext, lexerName := range ret.conflictingExtLangMap { + if lexer, ok := ret.fileExtMap[ext]; !ok || lexer.Config().Name != lexerName { + panic("missing default ext-lang mapping for: " + ext) + } + } + return ret +}) + +func normalizeFileNameLang(fileName, fileLang string) (string, string) { + fileName = path.Base(fileName) + fileLang, _, _ = strings.Cut(fileLang, "?") // maybe, the value from gitattributes might contain `?` parameters? + ext := path.Ext(fileName) + // the "lang" might come from enry or gitattributes, it has different naming for some languages + switch fileLang { + case "F#": + fileLang = "FSharp" + case "Pascal": + fileLang = "ObjectPascal" + case "C": + if ext == ".C" || ext == ".H" { + fileLang = "C++" + } + } + return fileName, fileLang +} + +func DetectChromaLexerByFileName(fileName, fileLang string) chroma.Lexer { + lexer, _ := detectChromaLexerByFileName(fileName, fileLang) + return lexer +} + +func detectChromaLexerByFileName(fileName, fileLang string) (_ chroma.Lexer, byLang bool) { + fileName, fileLang = normalizeFileNameLang(fileName, fileLang) + fileExt := path.Ext(fileName) + + // apply custom mapping for file extension, highest priority, for example: + // * ".my-js" -> ".js" + // * ".my-html" -> "HTML" + if fileExt != "" { + if val, ok := globalVars().highlightMapping[fileExt]; ok { + if strings.HasPrefix(val, ".") { + fileName = "dummy" + val + fileLang = "" + } else { + fileLang = val + } + } + } + + // try to use language for lexer name + if fileLang != "" { + lexer := chromaLexers().lowerNameMap[strings.ToLower(fileLang)] + if lexer != nil { + return lexer, true + } + } + + if fileName == "" { + return lexers.Fallback, false + } + + // try base name + { + baseName := path.Base(fileName) + if lexer, ok := chromaLexers().fileBaseMap[baseName]; ok { + return lexer, false + } else if lexer, ok = chromaLexers().fileBaseMap[mapKeyLowerPrefix+strings.ToLower(baseName)]; ok { + return lexer, false + } + } + + if fileExt == "" { + return lexers.Fallback, false + } + + // try ext name + { + if lexer, ok := chromaLexers().fileExtMap[fileExt]; ok { + return lexer, false + } else if lexer, ok = chromaLexers().fileExtMap[mapKeyLowerPrefix+strings.ToLower(fileExt)]; ok { + return lexer, false + } + } + + // try file part match, for example: ".env.local" for "*.env.*" + // it assumes that there must be a dot in filename (fileExt isn't empty) + for _, item := range chromaLexers().fileParts { + if strings.Contains(fileName, item.part) { + return item.lexer, false + } + } + return lexers.Fallback, false +} + +// detectChromaLexerWithAnalyze returns a chroma lexer by given file name, language and code content. All parameters can be optional. +// When code content is provided, it will be slow if no lexer is found by file name or language. +// If no lexer is found, it will return the fallback lexer. +func detectChromaLexerWithAnalyze(fileName, lang string, code []byte) chroma.Lexer { + lexer, byLang := detectChromaLexerByFileName(fileName, lang) + + // if lang is provided, and it matches a lexer, use it directly + if byLang { + return lexer + } + + // if a lexer is detected and there is no conflict for the file extension, use it directly + fileExt := path.Ext(fileName) + _, hasConflicts := chromaLexers().conflictingExtLangMap[fileExt] + if !hasConflicts && lexer != lexers.Fallback { + return lexer + } + + // try to detect language by content, for best guessing for the language + // when using "code" to detect, analyze.GetCodeLanguage is slow, it iterates many rules to detect language from content + analyzedLanguage := analyze.GetCodeLanguage(fileName, code) + lexer = DetectChromaLexerByFileName(fileName, analyzedLanguage) + if lexer == lexers.Fallback { + if analyzedLanguage != enry.OtherLanguage { + log.Warn("No chroma lexer found for enry detected language: %s (file: %s), need to fix the language mapping between enry and chroma.", analyzedLanguage, fileName) + } + } + return lexer +} diff --git a/package/gitea/source/modules/highlight/lexerdetect_test.go b/package/gitea/source/modules/highlight/lexerdetect_test.go new file mode 100644 index 00000000..a06053be --- /dev/null +++ b/package/gitea/source/modules/highlight/lexerdetect_test.go @@ -0,0 +1,116 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package highlight + +import ( + "strings" + "testing" + + "github.com/alecthomas/chroma/v2/lexers" + "github.com/stretchr/testify/assert" +) + +func BenchmarkDetectChromaLexerByFileName(b *testing.B) { + for b.Loop() { + // BenchmarkDetectChromaLexerByFileName-12 18214717 61.35 ns/op + DetectChromaLexerByFileName("a.sql", "") + } +} + +func BenchmarkDetectChromaLexerWithAnalyze(b *testing.B) { + b.StopTimer() + code := []byte(strings.Repeat("SELECT * FROM table;\n", 1000)) + b.StartTimer() + for b.Loop() { + // BenchmarkRenderCodeSlowGuess-12 87946 13310 ns/op + detectChromaLexerWithAnalyze("a", "", code) + } +} + +func BenchmarkChromaAnalyze(b *testing.B) { + b.StopTimer() + code := strings.Repeat("SELECT * FROM table;\n", 1000) + b.StartTimer() + for b.Loop() { + // comparing to detectChromaLexerWithAnalyze (go-enry), "chroma/lexers.Analyse" is very slow + // BenchmarkChromaAnalyze-12 519 2247104 ns/op + lexers.Analyse(code) + } +} + +func BenchmarkRenderCodeByLexer(b *testing.B) { + b.StopTimer() + code := strings.Repeat("SELECT * FROM table;\n", 1000) + lexer := DetectChromaLexerByFileName("a.sql", "") + b.StartTimer() + for b.Loop() { + // Really slow ....... the regexp2 used by Chroma takes most of the time + // BenchmarkRenderCodeByLexer-12 22 47159038 ns/op + RenderCodeByLexer(lexer, code) + } +} + +func TestDetectChromaLexer(t *testing.T) { + globalVars().highlightMapping[".my-html"] = "HTML" + t.Cleanup(func() { delete(globalVars().highlightMapping, ".my-html") }) + + casesWithContent := []struct { + fileName string + language string + content string + expected string + }{ + {"test.v", "", "", "V"}, + {"test.v", "any-lang-name", "", "V"}, + + {"any-file", "javascript", "", "JavaScript"}, + {"any-file", "", "/* vim: set filetype=python */", "Python"}, + {"any-file", "", "", "fallback"}, + + {"test.fs", "", "", "FSharp"}, + {"test.fs", "F#", "", "FSharp"}, + {"test.fs", "", "let x = 1", "FSharp"}, + + {"test.c", "", "", "C"}, + {"test.C", "", "", "C++"}, + {"OLD-CODE.PAS", "", "", "ObjectPascal"}, + {"test.my-html", "", "", "HTML"}, + + {"a.php", "", "", "PHP"}, + {"a.sql", "", "", "SQL"}, + {"dhcpd.conf", "", "", "ISCdhcpd"}, + {".env.my-production", "", "", "Bash"}, + + {"a.hcl", "", "", "HCL"}, // not the same as Chroma, enry detects "*.hcl" as "HCL" + {"a.hcl", "HCL", "", "HCL"}, + {"a.hcl", "Terraform", "", "Terraform"}, + } + for _, c := range casesWithContent { + lexer := detectChromaLexerWithAnalyze(c.fileName, c.language, []byte(c.content)) + if assert.NotNil(t, lexer, "case: %+v", c) { + assert.Equal(t, c.expected, lexer.Config().Name, "case: %+v", c) + } + } + + casesNameLang := []struct { + fileName string + language string + expected string + byLang bool + }{ + {"a.v", "", "V", false}, + {"a.v", "V", "V", true}, + {"a.v", "verilog", "verilog", true}, + {"a.v", "any-lang-name", "V", false}, + + {"a.hcl", "", "Terraform", false}, // not the same as enry + {"a.hcl", "HCL", "HCL", true}, + {"a.hcl", "Terraform", "Terraform", true}, + } + for _, c := range casesNameLang { + lexer, byLang := detectChromaLexerByFileName(c.fileName, c.language) + assert.Equal(t, c.expected, lexer.Config().Name, "case: %+v", c) + assert.Equal(t, c.byLang, byLang, "case: %+v", c) + } +} diff --git a/package/gitea/source/modules/htmlutil/html.go b/package/gitea/source/modules/htmlutil/html.go index efbc174b..8dbfe0c2 100644 --- a/package/gitea/source/modules/htmlutil/html.go +++ b/package/gitea/source/modules/htmlutil/html.go @@ -6,6 +6,7 @@ package htmlutil import ( "fmt" "html/template" + "io" "slices" "strings" ) @@ -31,7 +32,7 @@ func ParseSizeAndClass(defaultSize int, defaultClass string, others ...any) (int return size, class } -func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { +func htmlFormatArgs(s template.HTML, rawArgs []any) []any { if !strings.Contains(string(s), "%") || len(rawArgs) == 0 { panic("HTMLFormat requires one or more arguments") } @@ -50,5 +51,35 @@ func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { args[i] = template.HTMLEscapeString(fmt.Sprint(v)) } } - return template.HTML(fmt.Sprintf(string(s), args...)) + return args +} + +func HTMLFormat(s template.HTML, rawArgs ...any) template.HTML { + return template.HTML(fmt.Sprintf(string(s), htmlFormatArgs(s, rawArgs)...)) +} + +func HTMLPrintf(w io.Writer, s template.HTML, rawArgs ...any) (int, error) { + return fmt.Fprintf(w, string(s), htmlFormatArgs(s, rawArgs)...) +} + +func HTMLPrint(w io.Writer, s template.HTML) (int, error) { + return io.WriteString(w, string(s)) +} + +func HTMLPrintTag(w io.Writer, tag template.HTML, attrs map[string]string) (written int, err error) { + n, err := io.WriteString(w, "<"+string(tag)) + written += n + if err != nil { + return written, err + } + for k, v := range attrs { + n, err = fmt.Fprintf(w, ` %s="%s"`, template.HTMLEscapeString(k), template.HTMLEscapeString(v)) + written += n + if err != nil { + return written, err + } + } + n, err = io.WriteString(w, ">") + written += n + return written, err } diff --git a/package/gitea/source/modules/httpcache/httpcache.go b/package/gitea/source/modules/httpcache/httpcache.go index dd3efab7..e23cf0a9 100644 --- a/package/gitea/source/modules/httpcache/httpcache.go +++ b/package/gitea/source/modules/httpcache/httpcache.go @@ -60,21 +60,6 @@ func CacheControlForPrivateStatic() *CacheControlOptions { } } -// HandleGenericETagCache handles ETag-based caching for a HTTP request. -// It returns true if the request was handled. -func HandleGenericETagCache(req *http.Request, w http.ResponseWriter, etag string) (handled bool) { - if len(etag) > 0 { - w.Header().Set("Etag", etag) - if checkIfNoneMatchIsValid(req, etag) { - w.WriteHeader(http.StatusNotModified) - return true - } - } - // not sure whether it is a public content, so just use "private" (old behavior) - SetCacheControlInHeader(w.Header(), CacheControlForPrivateStatic()) - return false -} - // checkIfNoneMatchIsValid tests if the header If-None-Match matches the ETag func checkIfNoneMatchIsValid(req *http.Request, etag string) bool { ifNoneMatch := req.Header.Get("If-None-Match") @@ -89,10 +74,18 @@ func checkIfNoneMatchIsValid(req *http.Request, etag string) bool { return false } -// HandleGenericETagTimeCache handles ETag-based caching with Last-Modified caching for a HTTP request. +func HandleGenericETagPublicCache(req *http.Request, w http.ResponseWriter, etag string, lastModified *time.Time) bool { + return handleGenericETagTimeCache(req, w, etag, lastModified, CacheControlForPublicStatic()) +} + +func HandleGenericETagPrivateCache(req *http.Request, w http.ResponseWriter, etag string, lastModified *time.Time) bool { + return handleGenericETagTimeCache(req, w, etag, lastModified, CacheControlForPrivateStatic()) +} + +// handleGenericETagTimeCache handles ETag-based caching with Last-Modified caching for the HTTP request. // It returns true if the request was handled. -func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag string, lastModified *time.Time) (handled bool) { - if len(etag) > 0 { +func handleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag string, lastModified *time.Time, cacheControlOpts *CacheControlOptions) (handled bool) { + if etag != "" { w.Header().Set("Etag", etag) } if lastModified != nil && !lastModified.IsZero() { @@ -100,7 +93,7 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s w.Header().Set("Last-Modified", lastModified.UTC().Format(http.TimeFormat)) } - if len(etag) > 0 { + if etag != "" { if checkIfNoneMatchIsValid(req, etag) { w.WriteHeader(http.StatusNotModified) return true @@ -117,7 +110,6 @@ func HandleGenericETagTimeCache(req *http.Request, w http.ResponseWriter, etag s } } - // not sure whether it is a public content, so just use "private" (old behavior) - SetCacheControlInHeader(w.Header(), CacheControlForPrivateStatic()) + SetCacheControlInHeader(w.Header(), cacheControlOpts) return false } diff --git a/package/gitea/source/modules/httpcache/httpcache_test.go b/package/gitea/source/modules/httpcache/httpcache_test.go index d81f0609..d510cbe4 100644 --- a/package/gitea/source/modules/httpcache/httpcache_test.go +++ b/package/gitea/source/modules/httpcache/httpcache_test.go @@ -6,92 +6,73 @@ package httpcache import ( "net/http" "net/http/httptest" - "strings" "testing" + "time" + + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) -func countFormalHeaders(h http.Header) (c int) { - for k := range h { - // ignore our headers for internal usage - if strings.HasPrefix(k, "X-Gitea-") { - continue - } - c++ - } - return c -} - func TestHandleGenericETagCache(t *testing.T) { - etag := `"test"` + matchedEtag := `"matched-etag"` + lastModifiedTime := new(time.Date(2021, time.January, 2, 15, 4, 5, 0, time.FixedZone("test-zone", 8*3600))) + lastModified := lastModifiedTime.UTC().Format(http.TimeFormat) + cacheControl := "max-age=0, private, must-revalidate, no-transform" + type testCase struct { + name string + reqHeaders map[string]string + wantHandled bool + wantHeaders map[string]string + wantStatus int + } + cases := []testCase{ + { + name: "No If-None-Match", + wantHandled: false, + wantHeaders: map[string]string{"Last-Modified": lastModified, "Cache-Control": cacheControl, "Etag": matchedEtag}, + }, + { + name: "Mismatched If-None-Match", + reqHeaders: map[string]string{"If-None-Match": `"mismatched-etag"`}, + wantHandled: false, + wantHeaders: map[string]string{"Last-Modified": lastModified, "Cache-Control": cacheControl, "Etag": matchedEtag}, + }, + { + name: "Matched If-None-Match", + reqHeaders: map[string]string{"If-None-Match": matchedEtag}, + wantHandled: true, + wantHeaders: map[string]string{"Last-Modified": lastModified, "Cache-Control": "", "Etag": matchedEtag}, + wantStatus: http.StatusNotModified, + }, + { + name: "Multiple Mismatched If-None-Match", + reqHeaders: map[string]string{"If-None-Match": `"mismatched-etag1", "mismatched-etag2"`}, + wantHandled: false, + wantHeaders: map[string]string{"Last-Modified": lastModified, "Cache-Control": cacheControl, "Etag": matchedEtag}, + }, + { + name: "Multiple Matched If-None-Match", + reqHeaders: map[string]string{"If-None-Match": `"mismatched-etag", ` + matchedEtag}, + wantHandled: true, + wantHeaders: map[string]string{"Last-Modified": lastModified, "Cache-Control": "", "Etag": matchedEtag}, + wantStatus: http.StatusNotModified, + }, + } - t.Run("No_If-None-Match", func(t *testing.T) { - req := &http.Request{Header: make(http.Header)} - w := httptest.NewRecorder() - - handled := HandleGenericETagCache(req, w, etag) - - assert.False(t, handled) - assert.Equal(t, 2, countFormalHeaders(w.Header())) - assert.Contains(t, w.Header(), "Cache-Control") - assert.Contains(t, w.Header(), "Etag") - assert.Equal(t, etag, w.Header().Get("Etag")) - }) - t.Run("Wrong_If-None-Match", func(t *testing.T) { - req := &http.Request{Header: make(http.Header)} - w := httptest.NewRecorder() - - req.Header.Set("If-None-Match", `"wrong etag"`) - - handled := HandleGenericETagCache(req, w, etag) - - assert.False(t, handled) - assert.Equal(t, 2, countFormalHeaders(w.Header())) - assert.Contains(t, w.Header(), "Cache-Control") - assert.Contains(t, w.Header(), "Etag") - assert.Equal(t, etag, w.Header().Get("Etag")) - }) - t.Run("Correct_If-None-Match", func(t *testing.T) { - req := &http.Request{Header: make(http.Header)} - w := httptest.NewRecorder() - - req.Header.Set("If-None-Match", etag) - - handled := HandleGenericETagCache(req, w, etag) - - assert.True(t, handled) - assert.Equal(t, 1, countFormalHeaders(w.Header())) - assert.Contains(t, w.Header(), "Etag") - assert.Equal(t, etag, w.Header().Get("Etag")) - assert.Equal(t, http.StatusNotModified, w.Code) - }) - t.Run("Multiple_Wrong_If-None-Match", func(t *testing.T) { - req := &http.Request{Header: make(http.Header)} - w := httptest.NewRecorder() - - req.Header.Set("If-None-Match", `"wrong etag", "wrong etag "`) - - handled := HandleGenericETagCache(req, w, etag) - - assert.False(t, handled) - assert.Equal(t, 2, countFormalHeaders(w.Header())) - assert.Contains(t, w.Header(), "Cache-Control") - assert.Contains(t, w.Header(), "Etag") - assert.Equal(t, etag, w.Header().Get("Etag")) - }) - t.Run("Multiple_Correct_If-None-Match", func(t *testing.T) { - req := &http.Request{Header: make(http.Header)} - w := httptest.NewRecorder() - - req.Header.Set("If-None-Match", `"wrong etag", `+etag) - - handled := HandleGenericETagCache(req, w, etag) - - assert.True(t, handled) - assert.Equal(t, 1, countFormalHeaders(w.Header())) - assert.Contains(t, w.Header(), "Etag") - assert.Equal(t, etag, w.Header().Get("Etag")) - assert.Equal(t, http.StatusNotModified, w.Code) - }) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "http://example.com/test", nil) + for k, v := range tc.reqHeaders { + req.Header.Set(k, v) + } + w := httptest.NewRecorder() + assert.Equal(t, tc.wantHandled, HandleGenericETagPrivateCache(req, w, matchedEtag, lastModifiedTime)) + resp := w.Result() + for k, v := range tc.wantHeaders { + assert.Equal(t, v, resp.Header.Get(k)) + } + assert.Equal(t, tc.wantStatus, util.Iif(resp.StatusCode == http.StatusOK, 0, resp.StatusCode)) + }) + } } diff --git a/package/gitea/source/modules/httplib/content_disposition.go b/package/gitea/source/modules/httplib/content_disposition.go new file mode 100644 index 00000000..da23dae2 --- /dev/null +++ b/package/gitea/source/modules/httplib/content_disposition.go @@ -0,0 +1,65 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httplib + +import ( + "mime" + "strings" + + "code.gitea.io/gitea/modules/setting" +) + +type ContentDispositionType string + +const ( + ContentDispositionInline ContentDispositionType = "inline" + ContentDispositionAttachment ContentDispositionType = "attachment" +) + +func needsEncodingRune(b rune) bool { + return (b < ' ' || b > '~') && b != '\t' +} + +// getSafeName replaces all invalid chars in the filename field by underscore +func getSafeName(s string) (_ string, needsEncoding bool) { + var out strings.Builder + for _, b := range s { + if needsEncodingRune(b) { + needsEncoding = true + out.WriteRune('_') + } else { + out.WriteRune(b) + } + } + return out.String(), needsEncoding +} + +func EncodeContentDispositionAttachment(filename string) string { + return encodeContentDisposition(ContentDispositionAttachment, filename) +} + +func EncodeContentDispositionInline(filename string) string { + return encodeContentDisposition(ContentDispositionInline, filename) +} + +// encodeContentDisposition encodes a correct Content-Disposition Header +func encodeContentDisposition(t ContentDispositionType, filename string) string { + safeFilename, needsEncoding := getSafeName(filename) + result := mime.FormatMediaType(string(t), map[string]string{"filename": safeFilename}) + // No need for the utf8 encoding + if !needsEncoding { + return result + } + utf8Result := mime.FormatMediaType(string(t), map[string]string{"filename": filename}) + + // The mime package might have unexpected results in other go versions + // Make tests instance fail, otherwise use the default behavior of the go mime package + if !strings.HasPrefix(result, string(t)+"; filename=") || !strings.HasPrefix(utf8Result, string(t)+"; filename*=") { + setting.PanicInDevOrTesting("Unexpected mime package result %s", result) + return utf8Result + } + + encodedFileName := strings.TrimPrefix(utf8Result, string(t)) + return result + encodedFileName +} diff --git a/package/gitea/source/modules/httplib/content_disposition_test.go b/package/gitea/source/modules/httplib/content_disposition_test.go new file mode 100644 index 00000000..bf5040e1 --- /dev/null +++ b/package/gitea/source/modules/httplib/content_disposition_test.go @@ -0,0 +1,64 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package httplib + +import ( + "mime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestContentDisposition(t *testing.T) { + type testEntry struct { + disposition ContentDispositionType + filename string + header string + } + table := []testEntry{ + {disposition: ContentDispositionInline, filename: "test.txt", header: "inline; filename=test.txt"}, + {disposition: ContentDispositionInline, filename: "test❌.txt", header: "inline; filename=test_.txt; filename*=utf-8''test%E2%9D%8C.txt"}, + {disposition: ContentDispositionInline, filename: "test ❌.txt", header: "inline; filename=\"test _.txt\"; filename*=utf-8''test%20%E2%9D%8C.txt"}, + {disposition: ContentDispositionInline, filename: "\"test.txt", header: "inline; filename=\"\\\"test.txt\""}, + {disposition: ContentDispositionInline, filename: "hello\tworld.txt", header: "inline; filename=\"hello\tworld.txt\""}, + {disposition: ContentDispositionAttachment, filename: "hello\tworld.txt", header: "attachment; filename=\"hello\tworld.txt\""}, + {disposition: ContentDispositionAttachment, filename: "hello\nworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Aworld.txt"}, + {disposition: ContentDispositionAttachment, filename: "hello\rworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Dworld.txt"}, + } + + // Check the needsEncodingRune replacer ranges except tab that is checked above + // Any change in behavior should fail here + for c := ' '; !needsEncodingRune(c); c++ { + var header string + switch { + case strings.ContainsAny(string(c), ` (),/:;<=>?@[]`): + header = "inline; filename=\"hello" + string(c) + "world.txt\"" + case strings.ContainsAny(string(c), `"\`): + // This document advises against for backslash in quoted form: + // https://datatracker.ietf.org/doc/html/rfc6266#appendix-D + // However the mime package is not generating the filename* in this scenario + header = "inline; filename=\"hello\\" + string(c) + "world.txt\"" + default: + header = "inline; filename=hello" + string(c) + "world.txt" + } + table = append(table, testEntry{ + disposition: ContentDispositionInline, + filename: "hello" + string(c) + "world.txt", + header: header, + }) + } + + for _, entry := range table { + t.Run(string(entry.disposition)+"_"+entry.filename, func(t *testing.T) { + encoded := encodeContentDisposition(entry.disposition, entry.filename) + assert.Equal(t, entry.header, encoded) + disposition, params, err := mime.ParseMediaType(encoded) + require.NoError(t, err) + assert.Equal(t, string(entry.disposition), disposition) + assert.Equal(t, entry.filename, params["filename"]) + }) + } +} diff --git a/package/gitea/source/modules/httplib/serve.go b/package/gitea/source/modules/httplib/serve.go index b4c5e7fe..d51c938b 100644 --- a/package/gitea/source/modules/httplib/serve.go +++ b/package/gitea/source/modules/httplib/serve.go @@ -8,10 +8,9 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" - "net/url" "path" - "path/filepath" "strconv" "strings" "time" @@ -19,7 +18,6 @@ import ( charsetModule "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/httpcache" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" @@ -28,18 +26,55 @@ import ( ) type ServeHeaderOptions struct { - ContentType string // defaults to "application/octet-stream" - ContentTypeCharset string - ContentLength *int64 - Disposition string // defaults to "attachment" + ContentType string // defaults to "application/octet-stream" + ContentLength *int64 + Filename string - CacheIsPublic bool - CacheDuration time.Duration // defaults to 5 minutes - LastModified time.Time + ContentDisposition ContentDispositionType + + CacheIsPublic bool + CacheDuration time.Duration // defaults to 5 minutes + LastModified time.Time +} + +const ( + // Disable JS execution on the same origin, since we serve the file from the same origin as Gitea server. + // This rule can be relaxed in the future as long as it is properly sandboxed. + // "style-src" is for SVG inline styles (from Display SVG files as images instead of text #14101) + serveHeaderCspDefault = "default-src 'none'; style-src 'unsafe-inline'; sandbox" + + // No sandbox attribute for PDF as it breaks rendering in at least Safari. + // This should generally be safe as scripts inside PDF can not escape the PDF document. + // See https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion. + // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context + serveHeaderCspPdf = "default-src 'none'; style-src 'unsafe-inline'" + + // For audios and videos, actually it doesn't really need CSP (just like Gitea <= 1.25) + serveHeaderCspAudioVideo = "" +) + +func serveSetHeaderContentRelated(w http.ResponseWriter, contentType string) { + header := w.Header() + contentType = util.IfZero(contentType, typesniffer.MimeTypeApplicationOctetStream) + header.Set("Content-Type", contentType) + header.Set("X-Content-Type-Options", "nosniff") + + csp := serveHeaderCspDefault + if strings.HasPrefix(contentType, "application/pdf") { + csp = serveHeaderCspPdf + } + if strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/") { + csp = serveHeaderCspAudioVideo + } + if csp != "" { + header.Set("Content-Security-Policy", csp) + } else { + header.Del("Content-Security-Policy") + } } // ServeSetHeaders sets necessary content serve headers -func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { +func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) { header := w.Header() skipCompressionExts := container.SetOf(".gz", ".bz2", ".zip", ".xz", ".zst", ".deb", ".apk", ".jar", ".png", ".jpg", ".webp") @@ -47,29 +82,14 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { w.Header().Add(gzhttp.HeaderNoCompression, "1") } - contentType := typesniffer.MimeTypeApplicationOctetStream - if opts.ContentType != "" { - if opts.ContentTypeCharset != "" { - contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset) - } else { - contentType = opts.ContentType - } - } - header.Set("Content-Type", contentType) - header.Set("X-Content-Type-Options", "nosniff") + serveSetHeaderContentRelated(w, opts.ContentType) if opts.ContentLength != nil { header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10)) } - if opts.Filename != "" { - disposition := opts.Disposition - if disposition == "" { - disposition = "attachment" - } - - backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \" - header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename))) + contentDisposition := util.IfZero(opts.ContentDisposition, ContentDispositionAttachment) + header.Set("Content-Disposition", encodeContentDisposition(contentDisposition, path.Base(opts.Filename))) header.Set("Access-Control-Expose-Headers", "Content-Disposition") } @@ -85,54 +105,40 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) { } } -// ServeData download file from io.Reader -func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byte, opts *ServeHeaderOptions) { - // do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests - sniffedType := typesniffer.DetectContentType(mineBuf) - - // the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later - isPlain := sniffedType.IsText() || r.FormValue("render") != "" +func serveSetHeadersByUserContent(w http.ResponseWriter, contentPrefetchBuf []byte, opts ServeHeaderOptions) { + var detectCharset bool if setting.MimeTypeMap.Enabled { - fileExtension := strings.ToLower(filepath.Ext(opts.Filename)) + fileExtension := strings.ToLower(path.Ext(opts.Filename)) opts.ContentType = setting.MimeTypeMap.Map[fileExtension] + detectCharset = strings.HasPrefix(opts.ContentType, "text/") && !strings.Contains(opts.ContentType, "charset=") } if opts.ContentType == "" { + sniffedType := typesniffer.DetectContentType(contentPrefetchBuf) if sniffedType.IsBrowsableBinaryType() { opts.ContentType = sniffedType.GetMimeType() - } else if isPlain { + } else if sniffedType.IsText() { + // intentionally do not render user's HTML content as a page, for safety, and avoid content spamming & abusing opts.ContentType = "text/plain" + detectCharset = true } else { opts.ContentType = typesniffer.MimeTypeApplicationOctetStream } } - if isPlain { - charset, err := charsetModule.DetectEncoding(mineBuf) - if err != nil { - log.Error("Detect raw file %s charset failed: %v, using by default utf-8", opts.Filename, err) - charset = "utf-8" + if detectCharset { + if charset, _ := charsetModule.DetectEncoding(contentPrefetchBuf); charset != "" { + opts.ContentType += "; charset=" + strings.ToLower(charset) } - opts.ContentTypeCharset = strings.ToLower(charset) } - isSVG := sniffedType.IsSvgImage() - - // serve types that can present a security risk with CSP - if isSVG { - w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") - } else if sniffedType.IsPDF() { - // no sandbox attribute for pdf as it breaks rendering in at least safari. this - // should generally be safe as scripts inside PDF can not escape the PDF document - // see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion - // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context - w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'") - } - - opts.Disposition = "inline" - if isSVG && !setting.UI.SVG.Enabled { - opts.Disposition = "attachment" + if opts.ContentDisposition == "" { + sniffedType := typesniffer.FromContentType(opts.ContentType) + opts.ContentDisposition = ContentDispositionInline + if sniffedType.IsSvgImage() && !setting.UI.SVG.Enabled { + opts.ContentDisposition = ContentDispositionAttachment + } } ServeSetHeaders(w, opts) @@ -140,7 +146,10 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byt const mimeDetectionBufferLen = 1024 -func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts *ServeHeaderOptions) { +func ServeUserContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts ServeHeaderOptions) { + if opts.ContentLength != nil { + panic("do not set ContentLength, use size argument instead") + } buf := make([]byte, mimeDetectionBufferLen) n, err := util.ReadAtMost(reader, buf) if err != nil { @@ -150,7 +159,7 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re if n >= 0 { buf = buf[:n] } - setServeHeadersByFile(r, w, buf, opts) + serveSetHeadersByUserContent(w, buf, opts) // reset the reader to the beginning reader = io.MultiReader(bytes.NewReader(buf), reader) @@ -204,32 +213,29 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re partialLength := end - start + 1 w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size)) w.Header().Set("Content-Length", strconv.FormatInt(partialLength, 10)) - if _, err = io.CopyN(io.Discard, reader, start); err != nil { - http.Error(w, "serve content: unable to skip", http.StatusInternalServerError) - return + + if seeker, ok := reader.(io.Seeker); ok { + if _, err = seeker.Seek(start, io.SeekStart); err != nil { + http.Error(w, "serve content: unable to seek", http.StatusInternalServerError) + return + } + } else { + if _, err = io.CopyN(io.Discard, reader, start); err != nil { + http.Error(w, "serve content: unable to skip", http.StatusInternalServerError) + return + } } w.WriteHeader(http.StatusPartialContent) _, _ = io.CopyN(w, reader, partialLength) // just like http.ServeContent, not necessary to handle the error } -func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, modTime *time.Time, reader io.ReadSeeker, opts *ServeHeaderOptions) { - buf := make([]byte, mimeDetectionBufferLen) - n, err := util.ReadAtMost(reader, buf) +func ServeUserContentByFile(r *http.Request, w http.ResponseWriter, file fs.File, opts ServeHeaderOptions) { + info, err := file.Stat() if err != nil { - http.Error(w, "serve content: unable to read", http.StatusInternalServerError) + http.Error(w, "unable to serve file, stat error", http.StatusInternalServerError) return } - if _, err = reader.Seek(0, io.SeekStart); err != nil { - http.Error(w, "serve content: unable to seek", http.StatusInternalServerError) - return - } - if n >= 0 { - buf = buf[:n] - } - setServeHeadersByFile(r, w, buf, opts) - if modTime == nil { - modTime = &time.Time{} - } - http.ServeContent(w, r, opts.Filename, *modTime, reader) + opts.LastModified = info.ModTime() + ServeUserContentByReader(r, w, info.Size(), file, opts) } diff --git a/package/gitea/source/modules/httplib/serve_test.go b/package/gitea/source/modules/httplib/serve_test.go index 78b88c9b..41908523 100644 --- a/package/gitea/source/modules/httplib/serve_test.go +++ b/package/gitea/source/modules/httplib/serve_test.go @@ -12,11 +12,13 @@ import ( "strings" "testing" + "code.gitea.io/gitea/modules/typesniffer" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestServeContentByReader(t *testing.T) { +func TestServeUserContentByReader(t *testing.T) { data := "0123456789abcdef" test := func(t *testing.T, expectedStatusCode int, expectedContent string) { @@ -27,7 +29,7 @@ func TestServeContentByReader(t *testing.T) { } reader := strings.NewReader(data) w := httptest.NewRecorder() - ServeContentByReader(r, w, int64(len(data)), reader, &ServeHeaderOptions{}) + ServeUserContentByReader(r, w, int64(len(data)), reader, ServeHeaderOptions{}) assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length")) @@ -58,7 +60,7 @@ func TestServeContentByReader(t *testing.T) { }) } -func TestServeContentByReadSeeker(t *testing.T) { +func TestServeUserContentByFile(t *testing.T) { data := "0123456789abcdef" tmpFile := t.TempDir() + "/test" err := os.WriteFile(tmpFile, []byte(data), 0o644) @@ -76,7 +78,7 @@ func TestServeContentByReadSeeker(t *testing.T) { defer seekReader.Close() w := httptest.NewRecorder() - ServeContentByReadSeeker(r, w, nil, seekReader, &ServeHeaderOptions{}) + ServeUserContentByFile(r, w, seekReader, ServeHeaderOptions{}) assert.Equal(t, expectedStatusCode, w.Code) if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK { assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length")) @@ -106,3 +108,36 @@ func TestServeContentByReadSeeker(t *testing.T) { test(t, http.StatusPartialContent, data[1:]) }) } + +func TestServeSetHeaderContentRelated(t *testing.T) { + cases := []struct { + contentType string + csp string + }{ + {"", serveHeaderCspDefault}, + {"any", serveHeaderCspDefault}, + {"application/pdf", serveHeaderCspPdf}, + {"application/pdf; other", serveHeaderCspPdf}, + {"audio/mp4", serveHeaderCspAudioVideo}, + {"video/ogg; other", serveHeaderCspAudioVideo}, + {typesniffer.MimeTypeImageSvg, serveHeaderCspDefault}, + } + for _, c := range cases { + w := httptest.NewRecorder() + serveSetHeaderContentRelated(w, c.contentType) + csp := w.Header().Get("Content-Security-Policy") + assert.Equal(t, c.csp, csp, "content-type: %s", c.contentType) + assert.Equal(t, "nosniff", w.Header().Get("X-Content-Type-Options")) // it should always be there + } + + // make sure sandboxed + require.Contains(t, serveHeaderCspDefault, "; sandbox") +} + +func TestServeSetHeaders(t *testing.T) { + w := httptest.NewRecorder() + ServeSetHeaders(w, ServeHeaderOptions{Filename: "foo.zip"}) + assert.Equal(t, "attachment; filename=foo.zip", w.Header().Get("Content-Disposition")) + ServeSetHeaders(w, ServeHeaderOptions{Filename: "foo.zip", ContentDisposition: ContentDispositionInline}) + assert.Equal(t, "inline; filename=foo.zip", w.Header().Get("Content-Disposition")) +} diff --git a/package/gitea/source/modules/httplib/url.go b/package/gitea/source/modules/httplib/url.go index 2a1376b8..a689c9e9 100644 --- a/package/gitea/source/modules/httplib/url.go +++ b/package/gitea/source/modules/httplib/url.go @@ -24,7 +24,18 @@ func urlIsRelative(s string, u *url.URL) bool { if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') { return false } - return u != nil && u.Scheme == "" && u.Host == "" + if u == nil { + return false // invalid URL + } + if u.Scheme != "" || u.Host != "" { + return false // absolute URL with scheme or host + } + // Now, the URL is likely a relative URL + // HINT: GOLANG-HTTP-REDIRECT-BUG: Golang security vulnerability: "http.Redirect" calls "path.Clean" and changes the meaning of a path + // For example, `/a/../\b` will be changed to `/\b`, then it hits the first checked pattern and becomes an open redirect to "{current-scheme}://b" + // For a valid relative URL, its "path" shouldn't contain `\` because such char must be escaped. + // So if the "path" contains `\`, it is not a valid relative URL, then we can prevent open redirect. + return !strings.Contains(u.Path, "\\") } // IsRelativeURL detects if a URL is relative (no scheme or host) @@ -35,14 +46,14 @@ func IsRelativeURL(s string) bool { func getRequestScheme(req *http.Request) string { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto - if s := req.Header.Get("X-Forwarded-Proto"); s != "" { - return s + if proto, ok := parseForwardedProtoValue(req.Header.Get("X-Forwarded-Proto")); ok { + return proto } - if s := req.Header.Get("X-Forwarded-Protocol"); s != "" { - return s + if proto, ok := parseForwardedProtoValue(req.Header.Get("X-Forwarded-Protocol")); ok { + return proto } - if s := req.Header.Get("X-Url-Scheme"); s != "" { - return s + if proto, ok := parseForwardedProtoValue(req.Header.Get("X-Url-Scheme")); ok { + return proto } if s := req.Header.Get("Front-End-Https"); s != "" { return util.Iif(s == "on", "https", "http") @@ -53,6 +64,13 @@ func getRequestScheme(req *http.Request) string { return "" } +func parseForwardedProtoValue(val string) (string, bool) { + if val == "http" || val == "https" { + return val, true + } + return "", false +} + // GuessCurrentAppURL tries to guess the current full public URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL // TODO: should rename it to GuessCurrentPublicURL in the future func GuessCurrentAppURL(ctx context.Context) string { @@ -61,6 +79,10 @@ func GuessCurrentAppURL(ctx context.Context) string { // GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash. func GuessCurrentHostURL(ctx context.Context) string { + // "never" means always trust ROOT_URL and skip any request header detection. + if setting.PublicURLDetection == setting.PublicURLNever { + return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/") + } // Try the best guess to get the current host URL (will be used for public URL) by http headers. // At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong. // There are some cases: diff --git a/package/gitea/source/modules/httplib/url_test.go b/package/gitea/source/modules/httplib/url_test.go index 0ffb0cac..37369369 100644 --- a/package/gitea/source/modules/httplib/url_test.go +++ b/package/gitea/source/modules/httplib/url_test.go @@ -23,6 +23,7 @@ func TestIsRelativeURL(t *testing.T) { "foo", "/", "/foo?k=%20#abc", + "/foo?k=\\", } for _, s := range rel { assert.True(t, IsRelativeURL(s), "rel = %q", s) @@ -32,6 +33,8 @@ func TestIsRelativeURL(t *testing.T) { "\\\\", "/\\", "\\/", + "/a/../\\b", + "/any\\thing", "mailto:a@b.com", "https://test.com", } @@ -44,6 +47,7 @@ func TestGuessCurrentHostURL(t *testing.T) { defer test.MockVariableValue(&setting.AppURL, "http://cfg-host/sub/")() defer test.MockVariableValue(&setting.AppSubURL, "/sub")() headersWithProto := http.Header{"X-Forwarded-Proto": {"https"}} + maliciousProtoHeaders := http.Header{"X-Forwarded-Proto": {"http://attacker.host/?trash="}} t.Run("Legacy", func(t *testing.T) { defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLLegacy)() @@ -57,6 +61,9 @@ func TestGuessCurrentHostURL(t *testing.T) { // if "X-Forwarded-Proto" exists, then use it and "Host" header ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: headersWithProto}) assert.Equal(t, "https://req-host:3000", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: maliciousProtoHeaders}) + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) }) t.Run("Auto", func(t *testing.T) { @@ -73,6 +80,24 @@ func TestGuessCurrentHostURL(t *testing.T) { ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: headersWithProto}) assert.Equal(t, "https://req-host:3000", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: maliciousProtoHeaders}) + assert.Equal(t, "http://req-host:3000", GuessCurrentHostURL(ctx)) + }) + + t.Run("Never", func(t *testing.T) { + defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLNever)() + + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(t.Context())) + + ctx := context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000"}) + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", TLS: &tls.ConnectionState{}}) + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) + + ctx = context.WithValue(t.Context(), RequestContextKey, &http.Request{Host: "req-host:3000", Header: headersWithProto}) + assert.Equal(t, "http://cfg-host", GuessCurrentHostURL(ctx)) }) } diff --git a/package/gitea/source/modules/indexer/code/bleve/bleve.go b/package/gitea/source/modules/indexer/code/bleve/bleve.go index c233f491..010ee396 100644 --- a/package/gitea/source/modules/indexer/code/bleve/bleve.go +++ b/package/gitea/source/modules/indexer/code/bleve/bleve.go @@ -4,7 +4,6 @@ package bleve import ( - "bufio" "context" "fmt" "io" @@ -17,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer" path_filter "code.gitea.io/gitea/modules/indexer/code/bleve/token/path" "code.gitea.io/gitea/modules/indexer/code/internal" @@ -150,7 +150,7 @@ func NewIndexer(indexDir string) *Indexer { } } -func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, commitSha string, +func (b *Indexer) addUpdate(ctx context.Context, catFileBatch git.CatFileBatch, commitSha string, update internal.FileUpdate, repo *repo_model.Repository, batch *inner_bleve.FlushingBatch, ) error { // Ignore vendored files in code search @@ -163,7 +163,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro var err error if !update.Sized { var stdout string - stdout, _, err = gitcmd.NewCommand("cat-file", "-s").AddDynamicArguments(update.BlobSha).RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, _, err = gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("cat-file", "-s").AddDynamicArguments(update.BlobSha)) if err != nil { return err } @@ -176,16 +176,11 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro return b.addDelete(update.Filename, repo, batch) } - if _, err := batchWriter.Write([]byte(update.BlobSha + "\n")); err != nil { - return err - } - - _, _, size, err = git.ReadBatchLine(batchReader) + info, batchReader, err := catFileBatch.QueryContent(update.BlobSha) if err != nil { return err } - - fileContents, err := io.ReadAll(io.LimitReader(batchReader, size)) + fileContents, err := io.ReadAll(io.LimitReader(batchReader, info.Size)) if err != nil { return err } else if !typesniffer.DetectContentType(fileContents).IsText() { @@ -202,7 +197,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro RepoID: repo.ID, CommitID: commitSha, Filename: update.Filename, - Content: string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})), + Content: string(charset.ToUTF8DropErrors(fileContents)), Language: analyze.GetCodeLanguage(update.Filename, fileContents), UpdatedAt: time.Now().UTC(), }) @@ -217,18 +212,17 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository, batch func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { batch := inner_bleve.NewFlushingBatch(b.inner.Indexer, maxBatchSize) if len(changes.Updates) > 0 { - gitBatch, err := git.NewBatch(ctx, repo.RepoPath()) + catfileBatch, err := gitrepo.NewBatch(ctx, repo) if err != nil { return err } - defer gitBatch.Close() + defer catfileBatch.Close() for _, update := range changes.Updates { - if err := b.addUpdate(ctx, gitBatch.Writer, gitBatch.Reader, sha, update, repo, batch); err != nil { + if err := b.addUpdate(ctx, catfileBatch, sha, update, repo, batch); err != nil { return err } } - gitBatch.Close() } for _, filename := range changes.RemovedFilenames { if err := b.addDelete(filename, repo, batch); err != nil { diff --git a/package/gitea/source/modules/indexer/code/bleve/token/path/path.go b/package/gitea/source/modules/indexer/code/bleve/token/path/path.go index 6dfc12f1..e00bb695 100644 --- a/package/gitea/source/modules/indexer/code/bleve/token/path/path.go +++ b/package/gitea/source/modules/indexer/code/bleve/token/path/path.go @@ -27,7 +27,7 @@ func TokenFilterConstructor(config map[string]any, cache *registry.Cache) (analy func (s *TokenFilter) Filter(input analysis.TokenStream) analysis.TokenStream { if len(input) == 1 { - // if there is only one token, we dont need to generate the reversed chain + // if there is only one token, we don't need to generate the reversed chain return generatePathTokens(input, false) } diff --git a/package/gitea/source/modules/indexer/code/elasticsearch/elasticsearch.go b/package/gitea/source/modules/indexer/code/elasticsearch/elasticsearch.go index b08d837a..9d170528 100644 --- a/package/gitea/source/modules/indexer/code/elasticsearch/elasticsearch.go +++ b/package/gitea/source/modules/indexer/code/elasticsearch/elasticsearch.go @@ -4,7 +4,6 @@ package elasticsearch import ( - "bufio" "context" "fmt" "io" @@ -16,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer" "code.gitea.io/gitea/modules/indexer/code/internal" indexer_internal "code.gitea.io/gitea/modules/indexer/internal" @@ -138,7 +138,7 @@ const ( }` ) -func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserError, batchReader *bufio.Reader, sha string, update internal.FileUpdate, repo *repo_model.Repository) ([]elastic.BulkableRequest, error) { +func (b *Indexer) addUpdate(ctx context.Context, catFileBatch git.CatFileBatch, sha string, update internal.FileUpdate, repo *repo_model.Repository) ([]elastic.BulkableRequest, error) { // Ignore vendored files in code search if setting.Indexer.ExcludeVendored && analyze.IsVendor(update.Filename) { return nil, nil @@ -148,7 +148,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro var err error if !update.Sized { var stdout string - stdout, _, err = gitcmd.NewCommand("cat-file", "-s").AddDynamicArguments(update.BlobSha).RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, _, err = gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("cat-file", "-s").AddDynamicArguments(update.BlobSha)) if err != nil { return nil, err } @@ -161,16 +161,12 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro return []elastic.BulkableRequest{b.addDelete(update.Filename, repo)}, nil } - if _, err := batchWriter.Write([]byte(update.BlobSha + "\n")); err != nil { - return nil, err - } - - _, _, size, err = git.ReadBatchLine(batchReader) + info, batchReader, err := catFileBatch.QueryContent(update.BlobSha) if err != nil { return nil, err } - fileContents, err := io.ReadAll(io.LimitReader(batchReader, size)) + fileContents, err := io.ReadAll(io.LimitReader(batchReader, info.Size)) if err != nil { return nil, err } else if !typesniffer.DetectContentType(fileContents).IsText() { @@ -190,7 +186,7 @@ func (b *Indexer) addUpdate(ctx context.Context, batchWriter git.WriteCloserErro Doc(map[string]any{ "repo_id": repo.ID, "filename": update.Filename, - "content": string(charset.ToUTF8DropErrors(fileContents, charset.ConvertOpts{})), + "content": string(charset.ToUTF8DropErrors(fileContents)), "commit_id": sha, "language": analyze.GetCodeLanguage(update.Filename, fileContents), "updated_at": timeutil.TimeStampNow(), @@ -209,14 +205,14 @@ func (b *Indexer) addDelete(filename string, repo *repo_model.Repository) elasti func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha string, changes *internal.RepoChanges) error { reqs := make([]elastic.BulkableRequest, 0) if len(changes.Updates) > 0 { - batch, err := git.NewBatch(ctx, repo.RepoPath()) + batch, err := gitrepo.NewBatch(ctx, repo) if err != nil { return err } defer batch.Close() for _, update := range changes.Updates { - updateReqs, err := b.addUpdate(ctx, batch.Writer, batch.Reader, sha, update, repo) + updateReqs, err := b.addUpdate(ctx, batch, sha, update, repo) if err != nil { return err } @@ -224,7 +220,6 @@ func (b *Indexer) Index(ctx context.Context, repo *repo_model.Repository, sha st reqs = append(reqs, updateReqs...) } } - batch.Close() } for _, filename := range changes.RemovedFilenames { @@ -398,7 +393,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int elastic.NewHighlight(). Field("content"). Field("filename"). - NumOfFragments(0). // return all highting content on fragments + NumOfFragments(0). // return all highlighting content on fragments HighlighterType("fvh"), ). Sort("_score", false). @@ -431,7 +426,7 @@ func (b *Indexer) Search(ctx context.Context, opts *internal.SearchOptions) (int elastic.NewHighlight(). Field("content"). Field("filename"). - NumOfFragments(0). // return all highting content on fragments + NumOfFragments(0). // return all highlighting content on fragments HighlighterType("fvh"), ). Sort("_score", false). diff --git a/package/gitea/source/modules/indexer/code/git.go b/package/gitea/source/modules/indexer/code/git.go index f1513d66..a17b1055 100644 --- a/package/gitea/source/modules/indexer/code/git.go +++ b/package/gitea/source/modules/indexer/code/git.go @@ -11,13 +11,14 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/indexer/code/internal" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" ) func getDefaultBranchSha(ctx context.Context, repo *repo_model.Repository) (string, error) { - stdout, _, err := gitcmd.NewCommand("show-ref", "-s").AddDynamicArguments(git.BranchPrefix+repo.DefaultBranch).RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, _, err := gitrepo.RunCmdString(ctx, repo, gitcmd.NewCommand("show-ref", "-s").AddDynamicArguments(git.BranchPrefix+repo.DefaultBranch)) if err != nil { return "", err } @@ -34,7 +35,7 @@ func getRepoChanges(ctx context.Context, repo *repo_model.Repository, revision s needGenesis := len(status.CommitSha) == 0 if !needGenesis { hasAncestorCmd := gitcmd.NewCommand("merge-base").AddDynamicArguments(status.CommitSha, revision) - stdout, _, _ := hasAncestorCmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, _, _ := gitrepo.RunCmdString(ctx, repo, hasAncestorCmd) // FIXME: error is not handled needGenesis = len(stdout) == 0 } @@ -87,7 +88,7 @@ func parseGitLsTreeOutput(stdout []byte) ([]internal.FileUpdate, error) { // genesisChanges get changes to add repo to the indexer for the first time func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { var changes internal.RepoChanges - stdout, _, runErr := gitcmd.NewCommand("ls-tree", "--full-tree", "-l", "-r").AddDynamicArguments(revision).RunStdBytes(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, _, runErr := gitrepo.RunCmdBytes(ctx, repo, gitcmd.NewCommand("ls-tree", "--full-tree", "-l", "-r").AddDynamicArguments(revision)) if runErr != nil { return nil, runErr } @@ -100,7 +101,7 @@ func genesisChanges(ctx context.Context, repo *repo_model.Repository, revision s // nonGenesisChanges get changes since the previous indexer update func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revision string) (*internal.RepoChanges, error) { diffCmd := gitcmd.NewCommand("diff", "--name-status").AddDynamicArguments(repo.CodeIndexerStatus.CommitSha, revision) - stdout, _, runErr := diffCmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + stdout, _, runErr := gitrepo.RunCmdString(ctx, repo, diffCmd) if runErr != nil { // previous commit sha may have been removed by a force push, so // try rebuilding from scratch @@ -118,7 +119,7 @@ func nonGenesisChanges(ctx context.Context, repo *repo_model.Repository, revisio updateChanges := func() error { cmd := gitcmd.NewCommand("ls-tree", "--full-tree", "-l").AddDynamicArguments(revision). AddDashesAndList(updatedFilenames...) - lsTreeStdout, _, err := cmd.RunStdBytes(ctx, &gitcmd.RunOpts{Dir: repo.RepoPath()}) + lsTreeStdout, _, err := gitrepo.RunCmdBytes(ctx, repo, cmd) if err != nil { return err } diff --git a/package/gitea/source/modules/indexer/code/gitgrep/gitgrep.go b/package/gitea/source/modules/indexer/code/gitgrep/gitgrep.go index 6f6e0b47..5fbd7201 100644 --- a/package/gitea/source/modules/indexer/code/gitgrep/gitgrep.go +++ b/package/gitea/source/modules/indexer/code/gitgrep/gitgrep.go @@ -24,7 +24,7 @@ func indexSettingToGitGrepPathspecList() (list []string) { return list } -func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Repository, ref git.RefName, keyword string, searchMode indexer.SearchModeType) (searchResults []*code_indexer.Result, total int, err error) { +func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Repository, ref git.RefName, keyword string, searchMode indexer.SearchModeType) (searchResults []*code_indexer.Result, total int64, err error) { grepMode := git.GrepModeWords switch searchMode { case indexer.SearchModeExact: @@ -47,7 +47,7 @@ func PerformSearch(ctx context.Context, page int, repoID int64, gitRepo *git.Rep return nil, 0, fmt.Errorf("gitRepo.GetRefCommitID: %w", err) } - total = len(res) + total = int64(len(res)) pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res)) pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res)) res = res[pageStart:pageEnd] diff --git a/package/gitea/source/modules/indexer/code/internal/util.go b/package/gitea/source/modules/indexer/code/internal/util.go index fa958be4..5d62a5cc 100644 --- a/package/gitea/source/modules/indexer/code/internal/util.go +++ b/package/gitea/source/modules/indexer/code/internal/util.go @@ -17,20 +17,20 @@ func FilenameIndexerID(repoID int64, filename string) string { } func ParseIndexerID(indexerID string) (int64, string) { - index := strings.IndexByte(indexerID, '_') - if index == -1 { + before, after, ok := strings.Cut(indexerID, "_") + if !ok { log.Error("Unexpected ID in repo indexer: %s", indexerID) } - repoID, _ := internal.ParseBase36(indexerID[:index]) - return repoID, indexerID[index+1:] + repoID, _ := internal.ParseBase36(before) + return repoID, after } func FilenameOfIndexerID(indexerID string) string { - index := strings.IndexByte(indexerID, '_') - if index == -1 { + _, after, ok := strings.Cut(indexerID, "_") + if !ok { log.Error("Unexpected ID in repo indexer: %s", indexerID) } - return indexerID[index+1:] + return after } // FilenameMatchIndexPos returns the boundaries of its first seven lines. diff --git a/package/gitea/source/modules/indexer/code/search.go b/package/gitea/source/modules/indexer/code/search.go index a7a5d7d2..b5eb5116 100644 --- a/package/gitea/source/modules/indexer/code/search.go +++ b/package/gitea/source/modules/indexer/code/search.go @@ -72,10 +72,11 @@ func writeStrings(buf *bytes.Buffer, strs ...string) error { func HighlightSearchResultCode(filename, language string, lineNums []int, code string) []*ResultLine { // we should highlight the whole code block first, otherwise it doesn't work well with multiple line highlighting - hl, _ := highlight.Code(filename, language, code) - highlightedLines := strings.Split(string(hl), "\n") + lexer := highlight.DetectChromaLexerByFileName(filename, language) + hl := highlight.RenderCodeByLexer(lexer, code) + highlightedLines := highlight.UnsafeSplitHighlightedLines(hl) - // The lineNums outputted by highlight.Code might not match the original lineNums, because "highlight" removes the last `\n` + // The lineNums outputted by render might not match the original lineNums, because "highlight" removes the last `\n` lines := make([]*ResultLine, min(len(highlightedLines), len(lineNums))) for i := range lines { lines[i] = &ResultLine{ @@ -129,7 +130,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) (int64, []*Result, []*SearchResultLanguages, error) { if opts == nil || len(opts.Keyword) == 0 { return 0, nil, nil, nil } @@ -148,5 +149,5 @@ func PerformSearch(ctx context.Context, opts *SearchOptions) (int, []*Result, [] return 0, nil, nil, err } } - return int(total), displayResults, resultLanguages, nil + return total, displayResults, resultLanguages, nil } diff --git a/package/gitea/source/modules/indexer/issues/elasticsearch/elasticsearch_test.go b/package/gitea/source/modules/indexer/issues/elasticsearch/elasticsearch_test.go index dc329c07..cb9ed388 100644 --- a/package/gitea/source/modules/indexer/issues/elasticsearch/elasticsearch_test.go +++ b/package/gitea/source/modules/indexer/issues/elasticsearch/elasticsearch_test.go @@ -30,7 +30,11 @@ func TestElasticsearchIndexer(t *testing.T) { require.Eventually(t, func() bool { resp, err := http.Get(url) - return err == nil && resp.StatusCode == http.StatusOK + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK }, time.Minute, time.Second, "Expected elasticsearch to be up") indexer := NewIndexer(url, fmt.Sprintf("test_elasticsearch_indexer_%d", time.Now().Unix())) diff --git a/package/gitea/source/modules/indexer/issues/meilisearch/meilisearch.go b/package/gitea/source/modules/indexer/issues/meilisearch/meilisearch.go index b7fae6ee..5715cf47 100644 --- a/package/gitea/source/modules/indexer/issues/meilisearch/meilisearch.go +++ b/package/gitea/source/modules/indexer/issues/meilisearch/meilisearch.go @@ -124,7 +124,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 } diff --git a/package/gitea/source/modules/indexer/issues/meilisearch/meilisearch_test.go b/package/gitea/source/modules/indexer/issues/meilisearch/meilisearch_test.go index a32cbdd6..81a27487 100644 --- a/package/gitea/source/modules/indexer/issues/meilisearch/meilisearch_test.go +++ b/package/gitea/source/modules/indexer/issues/meilisearch/meilisearch_test.go @@ -36,7 +36,11 @@ func TestMeilisearchIndexer(t *testing.T) { require.Eventually(t, func() bool { resp, err := http.Get(url) - return err == nil && resp.StatusCode == http.StatusOK + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == http.StatusOK }, time.Minute, time.Second, "Expected meilisearch to be up") indexer := NewIndexer(url, key, fmt.Sprintf("test_meilisearch_indexer_%d", time.Now().Unix())) diff --git a/package/gitea/source/modules/json/json.go b/package/gitea/source/modules/json/json.go index 444dc852..d053f91c 100644 --- a/package/gitea/source/modules/json/json.go +++ b/package/gitea/source/modules/json/json.go @@ -8,8 +8,6 @@ import ( "encoding/binary" "encoding/json" //nolint:depguard // this package wraps it "io" - - jsoniter "github.com/json-iterator/go" ) // Encoder represents an encoder for json @@ -31,71 +29,7 @@ type Interface interface { Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error } -var ( - // DefaultJSONHandler default json handler - DefaultJSONHandler Interface = JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary} - - _ Interface = StdJSON{} - _ Interface = JSONiter{} -) - -// StdJSON implements Interface via encoding/json -type StdJSON struct{} - -// Marshal implements Interface -func (StdJSON) Marshal(v any) ([]byte, error) { - return json.Marshal(v) -} - -// Unmarshal implements Interface -func (StdJSON) Unmarshal(data []byte, v any) error { - return json.Unmarshal(data, v) -} - -// NewEncoder implements Interface -func (StdJSON) NewEncoder(writer io.Writer) Encoder { - return json.NewEncoder(writer) -} - -// NewDecoder implements Interface -func (StdJSON) NewDecoder(reader io.Reader) Decoder { - return json.NewDecoder(reader) -} - -// Indent implements Interface -func (StdJSON) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { - return json.Indent(dst, src, prefix, indent) -} - -// JSONiter implements Interface via jsoniter -type JSONiter struct { - jsoniter.API -} - -// Marshal implements Interface -func (j JSONiter) Marshal(v any) ([]byte, error) { - return j.API.Marshal(v) -} - -// Unmarshal implements Interface -func (j JSONiter) Unmarshal(data []byte, v any) error { - return j.API.Unmarshal(data, v) -} - -// NewEncoder implements Interface -func (j JSONiter) NewEncoder(writer io.Writer) Encoder { - return j.API.NewEncoder(writer) -} - -// NewDecoder implements Interface -func (j JSONiter) NewDecoder(reader io.Reader) Decoder { - return j.API.NewDecoder(reader) -} - -// Indent implements Interface, since jsoniter don't support Indent, just use encoding/json's -func (j JSONiter) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { - return json.Indent(dst, src, prefix, indent) -} +var DefaultJSONHandler = getDefaultJSONHandler() // Marshal converts object as bytes func Marshal(v any) ([]byte, error) { diff --git a/package/gitea/source/modules/json/json_test.go b/package/gitea/source/modules/json/json_test.go index ace71679..2fa4da4c 100644 --- a/package/gitea/source/modules/json/json_test.go +++ b/package/gitea/source/modules/json/json_test.go @@ -4,6 +4,7 @@ package json import ( + "bytes" "testing" "github.com/stretchr/testify/assert" @@ -16,3 +17,12 @@ func TestGiteaDBJSONUnmarshal(t *testing.T) { err = UnmarshalHandleDoubleEncode([]byte(""), &m) assert.NoError(t, err) } + +func TestIndent(t *testing.T) { + buf := &bytes.Buffer{} + err := Indent(buf, []byte(`{"a":1}`), ">", " ") + assert.NoError(t, err) + assert.Equal(t, `{ +> "a": 1 +>}`, buf.String()) +} diff --git a/package/gitea/source/modules/json/jsonlegacy.go b/package/gitea/source/modules/json/jsonlegacy.go new file mode 100644 index 00000000..81d644d4 --- /dev/null +++ b/package/gitea/source/modules/json/jsonlegacy.go @@ -0,0 +1,25 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build !goexperiment.jsonv2 + +package json + +import ( + "encoding/json" //nolint:depguard // this package wraps it + "io" +) + +func getDefaultJSONHandler() Interface { + return jsonV1{} +} + +func MarshalKeepOptionalEmpty(v any) ([]byte, error) { + return DefaultJSONHandler.Marshal(v) +} + +func NewDecoderCaseInsensitive(reader io.Reader) Decoder { + return DefaultJSONHandler.NewDecoder(reader) +} + +type Value = json.RawMessage diff --git a/package/gitea/source/modules/json/jsonv1.go b/package/gitea/source/modules/json/jsonv1.go new file mode 100644 index 00000000..55ec4736 --- /dev/null +++ b/package/gitea/source/modules/json/jsonv1.go @@ -0,0 +1,34 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package json + +import ( + "bytes" + "encoding/json" //nolint:depguard // this package wraps it + "io" +) + +type jsonV1 struct{} + +var _ Interface = jsonV1{} + +func (jsonV1) Marshal(v any) ([]byte, error) { + return json.Marshal(v) +} + +func (jsonV1) Unmarshal(data []byte, v any) error { + return json.Unmarshal(data, v) +} + +func (jsonV1) NewEncoder(writer io.Writer) Encoder { + return json.NewEncoder(writer) +} + +func (jsonV1) NewDecoder(reader io.Reader) Decoder { + return json.NewDecoder(reader) +} + +func (jsonV1) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { + return json.Indent(dst, src, prefix, indent) +} diff --git a/package/gitea/source/modules/json/jsonv2.go b/package/gitea/source/modules/json/jsonv2.go new file mode 100644 index 00000000..c4afc951 --- /dev/null +++ b/package/gitea/source/modules/json/jsonv2.go @@ -0,0 +1,95 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +//go:build goexperiment.jsonv2 + +package json + +import ( + "bytes" + jsonv1 "encoding/json" //nolint:depguard // this package wraps it + "encoding/json/jsontext" //nolint:depguard // this package wraps it + jsonv2 "encoding/json/v2" //nolint:depguard // this package wraps it + "io" +) + +// JSONv2 implements Interface via encoding/json/v2 +// Requires GOEXPERIMENT=jsonv2 to be set at build time +type JSONv2 struct { + marshalOptions jsonv2.Options + marshalKeepOptionalEmptyOptions jsonv2.Options + unmarshalOptions jsonv2.Options + unmarshalCaseInsensitiveOptions jsonv2.Options +} + +var jsonV2 JSONv2 + +func init() { + commonMarshalOptions := []jsonv2.Options{ + jsonv2.FormatNilSliceAsNull(true), + jsonv2.FormatNilMapAsNull(true), + } + jsonV2.marshalOptions = jsonv2.JoinOptions(commonMarshalOptions...) + jsonV2.unmarshalOptions = jsonv2.DefaultOptionsV2() + + // By default, "json/v2" omitempty removes all `""` empty strings, no matter where it comes from. + // v1 has a different behavior: if the `""` is from a null pointer, or a Marshal function, it is kept. + // Golang issue: https://github.com/golang/go/issues/75623 encoding/json/v2: unable to make omitempty work with pointer or Optional type with goexperiment.jsonv2 + jsonV2.marshalKeepOptionalEmptyOptions = jsonv2.JoinOptions(append(commonMarshalOptions, jsonv1.OmitEmptyWithLegacySemantics(true))...) + + // Some legacy code uses case-insensitive matching (for example: parsing oci.ImageConfig) + jsonV2.unmarshalCaseInsensitiveOptions = jsonv2.JoinOptions(jsonv2.MatchCaseInsensitiveNames(true)) +} + +func getDefaultJSONHandler() Interface { + return &jsonV2 +} + +func MarshalKeepOptionalEmpty(v any) ([]byte, error) { + return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions) +} + +func (j *JSONv2) Marshal(v any) ([]byte, error) { + return jsonv2.Marshal(v, j.marshalOptions) +} + +func (j *JSONv2) Unmarshal(data []byte, v any) error { + return jsonv2.Unmarshal(data, v, j.unmarshalOptions) +} + +func (j *JSONv2) NewEncoder(writer io.Writer) Encoder { + return &jsonV2Encoder{writer: writer, opts: j.marshalOptions} +} + +func (j *JSONv2) NewDecoder(reader io.Reader) Decoder { + return &jsonV2Decoder{reader: reader, opts: j.unmarshalOptions} +} + +// Indent implements Interface using standard library (JSON v2 doesn't have Indent yet) +func (*JSONv2) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error { + return jsonv1.Indent(dst, src, prefix, indent) +} + +type jsonV2Encoder struct { + writer io.Writer + opts jsonv2.Options +} + +func (e *jsonV2Encoder) Encode(v any) error { + return jsonv2.MarshalWrite(e.writer, v, e.opts) +} + +type jsonV2Decoder struct { + reader io.Reader + opts jsonv2.Options +} + +func (d *jsonV2Decoder) Decode(v any) error { + return jsonv2.UnmarshalRead(d.reader, v, d.opts) +} + +func NewDecoderCaseInsensitive(reader io.Reader) Decoder { + return &jsonV2Decoder{reader: reader, opts: jsonV2.unmarshalCaseInsensitiveOptions} +} + +type Value = jsontext.Value diff --git a/package/gitea/source/modules/lfs/content_store.go b/package/gitea/source/modules/lfs/content_store.go index 0d9c0c98..be1e6c8e 100644 --- a/package/gitea/source/modules/lfs/content_store.go +++ b/package/gitea/source/modules/lfs/content_store.go @@ -104,7 +104,7 @@ func (s *ContentStore) Verify(pointer Pointer) (bool, error) { } // ReadMetaObject will read a git_model.LFSMetaObject and return a reader -func ReadMetaObject(pointer Pointer) (io.ReadSeekCloser, error) { +func ReadMetaObject(pointer Pointer) (storage.Object, error) { contentStore := NewContentStore() return contentStore.Get(pointer) } diff --git a/package/gitea/source/modules/lfs/http_client_test.go b/package/gitea/source/modules/lfs/http_client_test.go index 179bcdb2..3e5ee9ec 100644 --- a/package/gitea/source/modules/lfs/http_client_test.go +++ b/package/gitea/source/modules/lfs/http_client_test.go @@ -193,7 +193,7 @@ func TestHTTPClientDownload(t *testing.T) { }, { endpoint: "https://invalid-json-response.io", - expectedError: "invalid json", + expectedError: "/(invalid json|invalid character)/", }, { endpoint: "https://valid-batch-request-download.io", @@ -258,7 +258,11 @@ func TestHTTPClientDownload(t *testing.T) { return nil }) if c.expectedError != "" { - assert.ErrorContains(t, err, c.expectedError) + if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") { + assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error()) + } else { + assert.ErrorContains(t, err, c.expectedError) + } } else { assert.NoError(t, err) } @@ -297,7 +301,7 @@ func TestHTTPClientUpload(t *testing.T) { }, { endpoint: "https://invalid-json-response.io", - expectedError: "invalid json", + expectedError: "/(invalid json|invalid character)/", }, { endpoint: "https://valid-batch-request-upload.io", @@ -352,7 +356,11 @@ func TestHTTPClientUpload(t *testing.T) { return io.NopCloser(new(bytes.Buffer)), objectError }) if c.expectedError != "" { - assert.ErrorContains(t, err, c.expectedError) + if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") { + assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error()) + } else { + assert.ErrorContains(t, err, c.expectedError) + } } else { assert.NoError(t, err) } diff --git a/package/gitea/source/modules/lfs/pointer_scanner_gogit.go b/package/gitea/source/modules/lfs/pointer_scanner_gogit.go index e153b8e2..ccfb16b6 100644 --- a/package/gitea/source/modules/lfs/pointer_scanner_gogit.go +++ b/package/gitea/source/modules/lfs/pointer_scanner_gogit.go @@ -15,7 +15,7 @@ import ( ) // SearchPointerBlobs scans the whole repository for LFS pointer files -func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan chan<- PointerBlob, errChan chan<- error) { +func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan chan<- PointerBlob) error { gitRepo := repo.GoGitRepo() err := func() error { @@ -49,14 +49,7 @@ func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan c return nil }) }() - if err != nil { - select { - case <-ctx.Done(): - default: - errChan <- err - } - } close(pointerChan) - close(errChan) + return err } diff --git a/package/gitea/source/modules/lfs/pointer_scanner_nogogit.go b/package/gitea/source/modules/lfs/pointer_scanner_nogogit.go index c37a93e7..29f5d0e3 100644 --- a/package/gitea/source/modules/lfs/pointer_scanner_nogogit.go +++ b/package/gitea/source/modules/lfs/pointer_scanner_nogogit.go @@ -8,96 +8,84 @@ package lfs import ( "bufio" "context" + "errors" "io" "strconv" "strings" - "sync" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/git/pipeline" + "code.gitea.io/gitea/modules/util" + + "golang.org/x/sync/errgroup" ) // SearchPointerBlobs scans the whole repository for LFS pointer files -func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan chan<- PointerBlob, errChan chan<- error) { - basePath := repo.Path +func SearchPointerBlobs(ctx context.Context, repo *git.Repository, pointerChan chan<- PointerBlob) error { + cmd1AllObjs, cmd3BatchContent := gitcmd.NewCommand(), gitcmd.NewCommand() - catFileCheckReader, catFileCheckWriter := io.Pipe() - shasToBatchReader, shasToBatchWriter := io.Pipe() - catFileBatchReader, catFileBatchWriter := io.Pipe() + cmd1AllObjsStdout, cmd1AllObjsStdoutClose := cmd1AllObjs.MakeStdoutPipe() + defer cmd1AllObjsStdoutClose() - wg := sync.WaitGroup{} - wg.Add(4) - - // Create the go-routines in reverse order. + cmd3BatchContentIn, cmd3BatchContentOut, cmd3BatchContentClose := cmd3BatchContent.MakeStdinStdoutPipe() + defer cmd3BatchContentClose() + // Create the go-routines in reverse order (update: the order is not needed any more, the pipes are properly prepared) + wg := errgroup.Group{} // 4. Take the output of cat-file --batch and check if each file in turn // to see if they're pointers to files in the LFS store - go createPointerResultsFromCatFileBatch(ctx, catFileBatchReader, &wg, pointerChan) + wg.Go(func() error { + return createPointerResultsFromCatFileBatch(cmd3BatchContentOut, pointerChan) + }) // 3. Take the shas of the blobs and batch read them - go pipeline.CatFileBatch(ctx, shasToBatchReader, catFileBatchWriter, &wg, basePath) + wg.Go(func() error { + return pipeline.CatFileBatch(ctx, cmd3BatchContent, repo.Path) + }) // 2. From the provided objects restrict to blobs <=1k - go pipeline.BlobsLessThan1024FromCatFileBatchCheck(catFileCheckReader, shasToBatchWriter, &wg) + wg.Go(func() error { + return pipeline.BlobsLessThan1024FromCatFileBatchCheck(cmd1AllObjsStdout, cmd3BatchContentIn) + }) // 1. Run batch-check on all objects in the repository - if !git.DefaultFeatures().CheckVersionAtLeast("2.6.0") { - revListReader, revListWriter := io.Pipe() - shasToCheckReader, shasToCheckWriter := io.Pipe() - wg.Add(2) - go pipeline.CatFileBatchCheck(ctx, shasToCheckReader, catFileCheckWriter, &wg, basePath) - go pipeline.BlobsFromRevListObjects(revListReader, shasToCheckWriter, &wg) - go pipeline.RevListAllObjects(ctx, revListWriter, &wg, basePath, errChan) - } else { - go pipeline.CatFileBatchCheckAllObjects(ctx, catFileCheckWriter, &wg, basePath, errChan) - } - wg.Wait() - + wg.Go(func() error { + return pipeline.CatFileBatchCheckAllObjects(ctx, cmd1AllObjs, repo.Path) + }) + err := wg.Wait() close(pointerChan) - close(errChan) + return err } -func createPointerResultsFromCatFileBatch(ctx context.Context, catFileBatchReader *io.PipeReader, wg *sync.WaitGroup, pointerChan chan<- PointerBlob) { - defer wg.Done() +func createPointerResultsFromCatFileBatch(catFileBatchReader io.ReadCloser, pointerChan chan<- PointerBlob) error { defer catFileBatchReader.Close() bufferedReader := bufio.NewReader(catFileBatchReader) buf := make([]byte, 1025) -loop: for { - select { - case <-ctx.Done(): - break loop - default: - } - // File descriptor line: sha sha, err := bufferedReader.ReadString(' ') if err != nil { - _ = catFileBatchReader.CloseWithError(err) - break + return util.Iif(errors.Is(err, io.EOF), nil, err) } sha = strings.TrimSpace(sha) // Throw away the blob if _, err := bufferedReader.ReadString(' '); err != nil { - _ = catFileBatchReader.CloseWithError(err) - break + return err } sizeStr, err := bufferedReader.ReadString('\n') if err != nil { - _ = catFileBatchReader.CloseWithError(err) - break + return err } size, err := strconv.Atoi(sizeStr[:len(sizeStr)-1]) if err != nil { - _ = catFileBatchReader.CloseWithError(err) - break + return err } pointerBuf := buf[:size+1] if _, err := io.ReadFull(bufferedReader, pointerBuf); err != nil { - _ = catFileBatchReader.CloseWithError(err) - break + return err } pointerBuf = pointerBuf[:size] // Now we need to check if the pointerBuf is an LFS pointer @@ -105,7 +93,6 @@ loop: if !pointer.IsValid() { continue } - pointerChan <- PointerBlob{Hash: sha, Pointer: pointer} } } diff --git a/package/gitea/source/modules/lfs/shared.go b/package/gitea/source/modules/lfs/shared.go index cd9488e3..e04c089e 100644 --- a/package/gitea/source/modules/lfs/shared.go +++ b/package/gitea/source/modules/lfs/shared.go @@ -66,6 +66,21 @@ type Link struct { ExpiresAt *time.Time `json:"expires_at,omitempty"` } +func NewLink(href string) *Link { + return &Link{Href: href} +} + +func (l *Link) WithHeader(k, v string) *Link { + if v == "" { + return l + } + if l.Header == nil { + l.Header = make(map[string]string) + } + l.Header[k] = v + return l +} + // ObjectError defines the JSON structure returned to the client in case of an error. type ObjectError struct { Code int `json:"code"` diff --git a/package/gitea/source/modules/lfstransfer/backend/backend.go b/package/gitea/source/modules/lfstransfer/backend/backend.go index f4e61570..92f08eac 100644 --- a/package/gitea/source/modules/lfstransfer/backend/backend.go +++ b/package/gitea/source/modules/lfstransfer/backend/backend.go @@ -95,7 +95,7 @@ func (g *GiteaBackend) Batch(_ string, pointers []transfer.BatchItem, args trans var respBody lfs.BatchResponse err = json.Unmarshal(respBytes, &respBody) if err != nil { - g.logger.Log("json umarshal error", err) + g.logger.Log("json unmarshal error", err) return nil, err } diff --git a/package/gitea/source/modules/lfstransfer/backend/lock.go b/package/gitea/source/modules/lfstransfer/backend/lock.go index 2c3c16a9..5bfa3a89 100644 --- a/package/gitea/source/modules/lfstransfer/backend/lock.go +++ b/package/gitea/source/modules/lfstransfer/backend/lock.go @@ -69,7 +69,7 @@ func (g *giteaLockBackend) Create(path, refname string) (transfer.Lock, error) { var respBody lfslock.LFSLockResponse err = json.Unmarshal(respBytes, &respBody) if err != nil { - g.logger.Log("json umarshal error", err) + g.logger.Log("json unmarshal error", err) return nil, err } @@ -202,7 +202,7 @@ func (g *giteaLockBackend) queryLocks(v url.Values) ([]transfer.Lock, string, er var respBody lfslock.LFSLockList err = json.Unmarshal(respBytes, &respBody) if err != nil { - g.logger.Log("json umarshal error", err) + g.logger.Log("json unmarshal error", err) return nil, "", err } diff --git a/package/gitea/source/modules/lfstransfer/logger.go b/package/gitea/source/modules/lfstransfer/logger.go index 517c2d9b..dfb9c0bb 100644 --- a/package/gitea/source/modules/lfstransfer/logger.go +++ b/package/gitea/source/modules/lfstransfer/logger.go @@ -17,5 +17,5 @@ func newLogger() transfer.Logger { } // Log implements transfer.Logger -func (g *GiteaLogger) Log(msg string, itms ...any) { +func (g *GiteaLogger) Log(msg string, items ...any) { } diff --git a/package/gitea/source/modules/log/init.go b/package/gitea/source/modules/log/init.go index 3fb5200a..ccaab50d 100644 --- a/package/gitea/source/modules/log/init.go +++ b/package/gitea/source/modules/log/init.go @@ -35,10 +35,10 @@ func init() { } } -func newProcessTypedContext(parent context.Context, desc string) (ctx context.Context, cancel context.CancelFunc) { +func newProcessTypedContext(parent context.Context, desc string) (context.Context, context.CancelFunc) { // the "process manager" also calls "log.Trace()" to output logs, so if we want to create new contexts by the manager, we need to disable the trace temporarily process.TraceLogDisable(true) defer process.TraceLogDisable(false) - ctx, _, cancel = process.GetManager().AddTypedContext(parent, desc, process.SystemProcessType, false) - return ctx, cancel + ctx, _, finished := process.GetManager().AddTypedContext(parent, desc, process.SystemProcessType, false) + return ctx, context.CancelFunc(finished) } diff --git a/package/gitea/source/modules/log/logger_global.go b/package/gitea/source/modules/log/logger_global.go index 07c25cd6..2bc8c4f4 100644 --- a/package/gitea/source/modules/log/logger_global.go +++ b/package/gitea/source/modules/log/logger_global.go @@ -18,6 +18,7 @@ func GetLevel() Level { } func Log(skip int, level Level, format string, v ...any) { + // codeql[disable-next-line=go/clear-text-logging] GetLogger(DEFAULT).Log(skip+1, &Event{Level: level}, format, v...) } diff --git a/package/gitea/source/modules/log/logger_impl.go b/package/gitea/source/modules/log/logger_impl.go index 551c1454..b15dc2f2 100644 --- a/package/gitea/source/modules/log/logger_impl.go +++ b/package/gitea/source/modules/log/logger_impl.go @@ -5,6 +5,7 @@ package log import ( "context" + "net/url" "reflect" "runtime" "strings" @@ -226,6 +227,8 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any) } } else if ls := asLogStringer(v); ls != nil { msgArgs[i] = logStringFormatter{v: ls} + } else if str, ok := v.(string); ok { + msgArgs[i] = protectSensitiveInfo(str) } } @@ -235,6 +238,24 @@ func (l *LoggerImpl) Log(skip int, event *Event, format string, logArgs ...any) l.SendLogEvent(event) } +func protectSensitiveInfo(s string) string { + u, err := url.Parse(s) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return s + } + q := u.Query() + for _, vals := range q { + for i := range vals { + vals[i] = "_" + } + } + masked := &url.URL{Scheme: u.Scheme, Host: u.Host, Path: u.Path, RawQuery: q.Encode()} + if u.User != nil { + masked.User = url.User("_masked_") + } + return masked.String() +} + func (l *LoggerImpl) GetLevel() Level { return Level(l.level.Load()) } diff --git a/package/gitea/source/modules/log/logger_test.go b/package/gitea/source/modules/log/logger_test.go index a74139dc..6e38610d 100644 --- a/package/gitea/source/modules/log/logger_test.go +++ b/package/gitea/source/modules/log/logger_test.go @@ -177,3 +177,10 @@ func TestLoggerExpressionFilter(t *testing.T) { assert.Equal(t, []string{"foo\n", "foo bar\n", "by filename\n"}, w1.FetchLogs()) } + +func TestProtectSensitiveInfo(t *testing.T) { + assert.Empty(t, protectSensitiveInfo("")) + assert.Equal(t, "mailto:user@example.com", protectSensitiveInfo("mailto:user@example.com")) + assert.Equal(t, "https://example.com", protectSensitiveInfo("https://example.com")) + assert.Equal(t, "https://_masked_@example.com/path?k=_", protectSensitiveInfo("https://u:p@example.com/path?k=v#hash")) +} diff --git a/package/gitea/source/modules/log/misc.go b/package/gitea/source/modules/log/misc.go index c9d230e4..a58b3757 100644 --- a/package/gitea/source/modules/log/misc.go +++ b/package/gitea/source/modules/log/misc.go @@ -20,6 +20,7 @@ func BaseLoggerToGeneralLogger(b BaseLogger) Logger { var _ Logger = (*baseToLogger)(nil) func (s *baseToLogger) Log(skip int, event *Event, format string, v ...any) { + // codeql[disable-next-line=go/clear-text-logging] s.base.Log(skip+1, event, format, v...) } diff --git a/package/gitea/source/modules/markup/asciicast/asciicast.go b/package/gitea/source/modules/markup/asciicast/asciicast.go index d86d61d7..b3af5eef 100644 --- a/package/gitea/source/modules/markup/asciicast/asciicast.go +++ b/package/gitea/source/modules/markup/asciicast/asciicast.go @@ -20,14 +20,12 @@ func init() { // See https://github.com/asciinema/asciinema/blob/develop/doc/asciicast-v2.md type Renderer struct{} -// Name implements markup.Renderer func (Renderer) Name() string { return "asciicast" } -// Extensions implements markup.Renderer -func (Renderer) Extensions() []string { - return []string{".cast"} +func (Renderer) FileNamePatterns() []string { + return []string{"*.cast"} } const ( @@ -35,12 +33,10 @@ const ( playerSrcAttr = "data-asciinema-player-src" ) -// SanitizerRules implements markup.Renderer func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{{Element: "div", AllowAttr: playerSrcAttr}} } -// Render implements markup.Renderer func (Renderer) Render(ctx *markup.RenderContext, _ io.Reader, output io.Writer) error { rawURL := fmt.Sprintf("%s/%s/%s/raw/%s/%s", setting.AppSubURL, diff --git a/package/gitea/source/modules/markup/camo.go b/package/gitea/source/modules/markup/camo.go index 7e258346..f07d62d4 100644 --- a/package/gitea/source/modules/markup/camo.go +++ b/package/gitea/source/modules/markup/camo.go @@ -11,7 +11,6 @@ import ( "strings" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) // CamoEncode encodes a lnk to fit with the go-camo and camo proxy links. The purposes of camo-proxy are: @@ -27,7 +26,7 @@ func CamoEncode(link string) string { macSum := b64encode(mac.Sum(nil)) encodedURL := b64encode([]byte(link)) - return util.URLJoin(setting.Camo.ServerURL, macSum, encodedURL) + return strings.TrimSuffix(setting.Camo.ServerURL, "/") + "/" + macSum + "/" + encodedURL } func b64encode(data []byte) string { diff --git a/package/gitea/source/modules/markup/common/footnote.go b/package/gitea/source/modules/markup/common/footnote.go index 1ece436c..e552a282 100644 --- a/package/gitea/source/modules/markup/common/footnote.go +++ b/package/gitea/source/modules/markup/common/footnote.go @@ -405,9 +405,9 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt if entering { n := node.(*FootnoteLink) is := strconv.Itoa(n.Index) - _, _ = w.WriteString(``) // FIXME: here and below, need to keep the classes _, _ = w.WriteString(is) @@ -419,7 +419,7 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { n := node.(*FootnoteBackLink) - _, _ = w.WriteString(` `) _, _ = w.WriteString("↩︎") @@ -431,7 +431,7 @@ func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source [ func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*Footnote) if entering { - _, _ = w.WriteString(`
  • \^{}\[\]` + "`" + `]*)?`) + v.wwwURLRegexp = regexp.MustCompile(`^www\.[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}((?:/|[#?])[-a-zA-Z0-9@:%_\+.~#!?&//=\(\);,'">\^{}\[\]` + "`" + `]*)?`) v.LinkRegex, _ = xurls.StrictMatchingScheme("https?://") return v }) @@ -75,7 +75,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont m = GlobalVars().LinkRegex.FindSubmatchIndex(line) } if m == nil && bytes.HasPrefix(line, domainWWW) { - m = GlobalVars().wwwURLRegxp.FindSubmatchIndex(line) + m = GlobalVars().wwwURLRegexp.FindSubmatchIndex(line) protocol = []byte("http") } if m != nil { @@ -96,6 +96,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont m[1] -= closing } } else if lastChar == ';' { + // exclude HTML entity reference, e.g.: exclude " " from "http://example.com?foo=1 " i := m[1] - 2 for ; i >= m[0]; i-- { if util.IsAlphaNumeric(line[i]) { @@ -105,7 +106,7 @@ func (s *linkifyParser) Parse(parent ast.Node, block text.Reader, pc parser.Cont } if i != m[1]-2 { if line[i] == '&' { - m[1] -= m[1] - i + m[1] = i } } } diff --git a/package/gitea/source/modules/markup/console/console.go b/package/gitea/source/modules/markup/console/console.go index 492579b0..bcd6cfce 100644 --- a/package/gitea/source/modules/markup/console/console.go +++ b/package/gitea/source/modules/markup/console/console.go @@ -20,29 +20,24 @@ func init() { markup.RegisterRenderer(Renderer{}) } -// Renderer implements markup.Renderer type Renderer struct{} var _ markup.RendererContentDetector = (*Renderer)(nil) -// Name implements markup.Renderer func (Renderer) Name() string { return "console" } -// Extensions implements markup.Renderer -func (Renderer) Extensions() []string { - return []string{".sh-session"} +func (Renderer) FileNamePatterns() []string { + return []string{"*.sh-session"} } -// SanitizerRules implements markup.Renderer func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{ {Element: "span", AllowAttr: "class", Regexp: `^term-((fg[ix]?|bg)\d+|container)$`}, } } -// CanRender implements markup.RendererContentDetector func (Renderer) CanRender(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) bool { if !sniffedType.IsTextPlain() { return false diff --git a/package/gitea/source/modules/markup/csv/csv.go b/package/gitea/source/modules/markup/csv/csv.go index 61977d45..910a1518 100644 --- a/package/gitea/source/modules/markup/csv/csv.go +++ b/package/gitea/source/modules/markup/csv/csv.go @@ -20,20 +20,16 @@ func init() { markup.RegisterRenderer(Renderer{}) } -// Renderer implements markup.Renderer for csv files type Renderer struct{} -// Name implements markup.Renderer func (Renderer) Name() string { return "csv" } -// Extensions implements markup.Renderer -func (Renderer) Extensions() []string { - return []string{".csv", ".tsv"} +func (Renderer) FileNamePatterns() []string { + return []string{"*.csv", "*.tsv"} } -// SanitizerRules implements markup.Renderer func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{ {Element: "table", AllowAttr: "class", Regexp: `^data-table$`}, diff --git a/package/gitea/source/modules/markup/external/external.go b/package/gitea/source/modules/markup/external/external.go index 3cbe14b8..4b3c96fd 100644 --- a/package/gitea/source/modules/markup/external/external.go +++ b/package/gitea/source/modules/markup/external/external.go @@ -21,10 +21,35 @@ import ( // RegisterRenderers registers all supported third part renderers according settings func RegisterRenderers() { + markup.RegisterRenderer(&frontendRenderer{ + name: "openapi-swagger", + patterns: []string{ + "openapi.yaml", + "openapi.yml", + "openapi.json", + "swagger.yaml", + "swagger.yml", + "swagger.json", + }, + }) + + markup.RegisterRenderer(&frontendRenderer{ + name: "viewer-3d", + patterns: []string{ + // It needs more logic to make it overall right (render a text 3D model automatically): + // we need to distinguish the ambiguous filename extensions. + // For example: "*.amf, *.obj, *.off, *.step" might be or not be a 3D model file. + // So when it is a text file, we can't assume that "we only render it by 3D plugin", + // otherwise the end users would be impossible to view its real content when the file is not a 3D model. + "*.3dm", "*.3ds", "*.3mf", "*.amf", "*.bim", "*.brep", + "*.dae", "*.fbx", "*.fcstd", "*.glb", "*.gltf", + "*.ifc", "*.igs", "*.iges", "*.stp", "*.step", + "*.stl", "*.obj", "*.off", "*.ply", "*.wrl", + }, + }) + for _, renderer := range setting.ExternalMarkupRenderers { - if renderer.Enabled && renderer.Command != "" && len(renderer.FileExtensions) > 0 { - markup.RegisterRenderer(&Renderer{renderer}) - } + markup.RegisterRenderer(&Renderer{renderer}) } } @@ -38,22 +63,18 @@ var ( _ markup.ExternalRenderer = (*Renderer)(nil) ) -// Name returns the external tool name func (p *Renderer) Name() string { return p.MarkupName } -// NeedPostProcess implements markup.Renderer func (p *Renderer) NeedPostProcess() bool { return p.MarkupRenderer.NeedPostProcess } -// Extensions returns the supported extensions of the tool -func (p *Renderer) Extensions() []string { - return p.FileExtensions +func (p *Renderer) FileNamePatterns() []string { + return p.FilePatterns } -// SanitizerRules implements markup.Renderer func (p *Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return p.MarkupSanitizerRules } diff --git a/package/gitea/source/modules/markup/external/frontend.go b/package/gitea/source/modules/markup/external/frontend.go new file mode 100644 index 00000000..7327503d --- /dev/null +++ b/package/gitea/source/modules/markup/external/frontend.go @@ -0,0 +1,95 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package external + +import ( + "encoding/base64" + "io" + "unicode/utf8" + + "code.gitea.io/gitea/modules/htmlutil" + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/public" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" +) + +type frontendRenderer struct { + name string + patterns []string +} + +var ( + _ markup.PostProcessRenderer = (*frontendRenderer)(nil) + _ markup.ExternalRenderer = (*frontendRenderer)(nil) +) + +func (p *frontendRenderer) Name() string { + return p.name +} + +func (p *frontendRenderer) NeedPostProcess() bool { + return false +} + +func (p *frontendRenderer) FileNamePatterns() []string { + // TODO: the file extensions are ambiguous, even if the file name matches, it doesn't mean that the file is a 3D model + // There are some approaches to make it more accurate, but they are all complicated: + // A. Make backend know everything (detect a file is a 3D model or not) + // B. Let frontend renders to try render one by one + // + // If there would be more frontend renders in the future, we need to implement the "frontend" approach: + // 1. Make backend or parent window collect the supported extensions of frontend renders (done: backend external render framework) + // 2. If the current file matches any extension, start the general iframe embedded render (done: this renderer) + // 3. The iframe window calls the frontend renders one by one (done: frontend external render) + // 4. Report the render result to parent by postMessage (TODO: when needed) + return p.patterns +} + +func (p *frontendRenderer) SanitizerRules() []setting.MarkupSanitizerRule { + return nil +} + +func (p *frontendRenderer) GetExternalRendererOptions() (ret markup.ExternalRendererOptions) { + ret.SanitizerDisabled = true + ret.DisplayInIframe = true + ret.ContentSandbox = "allow-scripts allow-forms allow-modals allow-popups allow-downloads" + return ret +} + +func (p *frontendRenderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { + if ctx.RenderOptions.StandalonePageOptions == nil { + opts := p.GetExternalRendererOptions() + return markup.RenderIFrame(ctx, &opts, output) + } + + content, err := util.ReadWithLimit(input, int(setting.UI.MaxDisplayFileSize)) + if err != nil { + return err + } + + contentEncoding, contentString := "text", util.UnsafeBytesToString(content) + if !utf8.Valid(content) { + contentEncoding = "base64" + contentString = base64.StdEncoding.EncodeToString(content) + } + + _, err = htmlutil.HTMLPrintf(output, + ` + + + + + + +
    + + + +`, + p.name, ctx.RenderOptions.RelativePath, + contentEncoding, contentString, + public.AssetURI("js/external-render-frontend.js")) + return err +} diff --git a/package/gitea/source/modules/markup/html.go b/package/gitea/source/modules/markup/html.go index 51afd4be..1c2ae691 100644 --- a/package/gitea/source/modules/markup/html.go +++ b/package/gitea/source/modules/markup/html.go @@ -12,7 +12,9 @@ import ( "strings" "sync" + "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/markup/common" + "code.gitea.io/gitea/modules/translation" "golang.org/x/net/html" "golang.org/x/net/html/atom" @@ -60,7 +62,7 @@ var globalVars = sync.OnceValue(func() *globalVarsType { v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`) // anyHashPattern splits url containing SHA into parts - v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) + v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})((\.\w+)*)(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`) // comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash" v.comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`) @@ -234,6 +236,49 @@ func postProcessString(ctx *RenderContext, procs []processor, content string) (s return buf.String(), nil } +func RenderTocHeadingItems(ctx *RenderContext, nodeDetailsAttrs map[string]string, out io.Writer) { + locale, ok := ctx.Value(translation.ContextKey).(translation.Locale) + if !ok { + locale = translation.NewLocale("") + } + _, _ = htmlutil.HTMLPrintTag(out, "details", nodeDetailsAttrs) + _, _ = htmlutil.HTMLPrintf(out, "%s\n", locale.TrString("toc")) + + baseLevel := 6 + for _, header := range ctx.TocHeadingItems { + if header.HeadingLevel < baseLevel { + baseLevel = header.HeadingLevel + } + } + + currentLevel := baseLevel + indent := []byte{' ', ' '} + _, _ = htmlutil.HTMLPrint(out, "
    \n") + currentLevel-- + } + _, _ = htmlutil.HTMLPrint(out, "\n\n") +} + func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output io.Writer) error { if !ctx.usedByRender && ctx.RenderHelper != nil { defer ctx.RenderHelper.CleanUp() @@ -284,6 +329,9 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output } // Render everything to buf. + if ctx.TocShowInSection == TocShowInMain && len(ctx.TocHeadingItems) > 0 { + RenderTocHeadingItems(ctx, nil, output) + } for _, node := range newNodes { if err := html.Render(output, node); err != nil { return fmt.Errorf("markup.postProcess: html.Render: %w", err) @@ -314,7 +362,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod return node.NextSibling } - processNodeAttrID(node) + processNodeHeadingAndID(ctx, node) processFootnoteNode(ctx, node) // FIXME: the footnote processing should be done in the "footnote.go" renderer directly if isEmojiNode(node) { diff --git a/package/gitea/source/modules/markup/html_commit.go b/package/gitea/source/modules/markup/html_commit.go index fe7a0349..a2c51606 100644 --- a/package/gitea/source/modules/markup/html_commit.go +++ b/package/gitea/source/modules/markup/html_commit.go @@ -4,24 +4,27 @@ package markup import ( + "fmt" "slices" "strings" "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/references" - "code.gitea.io/gitea/modules/util" "golang.org/x/net/html" "golang.org/x/net/html/atom" ) type anyHashPatternResult struct { - PosStart int - PosEnd int - FullURL string - CommitID string - SubPath string - QueryHash string + PosStart int + PosEnd int + FullURL string + CommitID string + CommitExt string + SubPath string + QueryParams string + QueryHash string } func createCodeLink(href, content, class string) *html.Node { @@ -56,7 +59,11 @@ func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) { return ret, false } - ret.PosStart, ret.PosEnd = m[0], m[1] + pos := 0 + + ret.PosStart, ret.PosEnd = m[pos], m[pos+1] + pos += 2 + ret.FullURL = s[ret.PosStart:ret.PosEnd] if strings.HasSuffix(ret.FullURL, ".") { // if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence. @@ -67,14 +74,24 @@ func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) { } } - ret.CommitID = s[m[2]:m[3]] - if m[5] > 0 { - ret.SubPath = s[m[4]:m[5]] - } + ret.CommitID = s[m[pos]:m[pos+1]] + pos += 2 - lastStart, lastEnd := m[len(m)-2], m[len(m)-1] - if lastEnd > 0 { - ret.QueryHash = s[lastStart:lastEnd][1:] + ret.CommitExt = s[m[pos]:m[pos+1]] + pos += 4 + + if m[pos] > 0 { + ret.SubPath = s[m[pos]:m[pos+1]] + } + pos += 2 + + if m[pos] > 0 { + ret.QueryParams = s[m[pos]:m[pos+1]] + } + pos += 2 + + if m[pos] > 0 { + ret.QueryHash = s[m[pos]:m[pos+1]][1:] } return ret, true } @@ -96,12 +113,20 @@ func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) { continue } text := base.ShortSha(ret.CommitID) + if ret.CommitExt != "" { + text += ret.CommitExt + } if ret.SubPath != "" { text += ret.SubPath } if ret.QueryHash != "" { text += " (" + ret.QueryHash + ")" } + // only turn commit links to the current instance into hash link + if !httplib.IsCurrentGiteaSiteURL(ctx, ret.FullURL) { + node = node.NextSibling + continue + } replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit")) node = node.NextSibling.NextSibling } @@ -148,6 +173,12 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) { } } + // only turn compare links to the current instance into hash link + if !httplib.IsCurrentGiteaSiteURL(ctx, urlFull) { + node = node.NextSibling + continue + } + text := text1 + textDots + text2 if hash != "" { text += " (" + hash + ")" @@ -188,7 +219,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) { continue } - link := "/:root/" + util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], "commit", hash) + link := fmt.Sprintf("/:root/%s/%s/commit/%s", ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], hash) replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit")) start = 0 node = node.NextSibling.NextSibling @@ -205,7 +236,7 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) { } refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha) - linkHref := "/:root/" + util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha) + linkHref := fmt.Sprintf("/:root/%s/%s/commit/%s", ref.Owner, ref.Name, ref.CommitSha) link := createLink(ctx, linkHref, refText, "commit") replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link) diff --git a/package/gitea/source/modules/markup/html_internal_test.go b/package/gitea/source/modules/markup/html_internal_test.go index 467cc509..3a91797e 100644 --- a/package/gitea/source/modules/markup/html_internal_test.go +++ b/package/gitea/source/modules/markup/html_internal_test.go @@ -23,12 +23,12 @@ const ( // externalIssueLink an HTML link to an alphanumeric-style issue func externalIssueLink(baseURL, class, name string) string { - return link(util.URLJoin(baseURL, name), class, name) + return link(strings.TrimSuffix(baseURL, "/")+"/"+name, class, name) } // numericLink an HTML to a numeric-style issue func numericIssueLink(baseURL, class string, index int, marker string) string { - return link(util.URLJoin(baseURL, strconv.Itoa(index)), class, fmt.Sprintf("%s%d", marker, index)) + return link(strings.TrimSuffix(baseURL, "/")+"/"+strconv.Itoa(index), class, fmt.Sprintf("%s%d", marker, index)) } // link an HTML link @@ -116,7 +116,7 @@ func TestRender_IssueIndexPattern2(t *testing.T) { links := make([]any, len(indices)) for i, index := range indices { - links[i] = numericIssueLink(util.URLJoin("/test-owner/test-repo", path), "ref-issue", index, marker) + links[i] = numericIssueLink("/test-owner/test-repo/"+path, "ref-issue", index, marker) } expectedNil := fmt.Sprintf(expectedFmt, links...) testRenderIssueIndexPattern(t, s, expectedNil, NewTestRenderContext(TestAppURL, localMetas)) @@ -210,7 +210,7 @@ func TestRender_IssueIndexPattern5(t *testing.T) { metas["regexp"] = pattern links := make([]any, len(ids)) for i, id := range ids { - links[i] = link(util.URLJoin("https://someurl.com/someUser/someRepo/", id), "ref-issue ref-external-issue", names[i]) + links[i] = link("https://someurl.com/someUser/someRepo/"+id, "ref-issue ref-external-issue", names[i]) } expected := fmt.Sprintf(expectedFmt, links...) @@ -288,18 +288,18 @@ func TestRender_AutoLink(t *testing.T) { } // render valid issue URLs - test(util.URLJoin(TestRepoURL, "issues", "3333"), - numericIssueLink(util.URLJoin(TestRepoURL, "issues"), "ref-issue", 3333, "#")) + test(TestRepoURL+"issues/3333", + numericIssueLink(TestRepoURL+"issues", "ref-issue", 3333, "#")) // render valid commit URLs - tmp := util.URLJoin(TestRepoURL, "commit", "d8a994ef243349f321568f9e36d5c3f444b99cae") + tmp := TestRepoURL + "commit/d8a994ef243349f321568f9e36d5c3f444b99cae" test(tmp, "d8a994ef24") tmp += "#diff-2" test(tmp, "d8a994ef24 (diff-2)") // render other commit URLs tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2" - test(tmp, "d8a994ef24 (diff-2)") + test(tmp, ""+tmp+"") } func TestRender_FullIssueURLs(t *testing.T) { diff --git a/package/gitea/source/modules/markup/html_issue.go b/package/gitea/source/modules/markup/html_issue.go index 85bec5db..a94abb38 100644 --- a/package/gitea/source/modules/markup/html_issue.go +++ b/package/gitea/source/modules/markup/html_issue.go @@ -4,6 +4,7 @@ package markup import ( + "fmt" "strconv" "strings" @@ -162,7 +163,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) { issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner) issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name) issuePath := util.Iif(ref.IsPull, "pulls", "issues") - linkHref := "/:root/" + util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue) + linkHref := fmt.Sprintf("/:root/%s/%s/%s/%s", issueOwner, issueRepo, issuePath, ref.Issue) // at the moment, only render the issue index in a full line (or simple line) as icon+title // otherwise it would be too noisy for "take #1 as an example" in a sentence diff --git a/package/gitea/source/modules/markup/html_link.go b/package/gitea/source/modules/markup/html_link.go index 43faef16..c84e0b90 100644 --- a/package/gitea/source/modules/markup/html_link.go +++ b/package/gitea/source/modules/markup/html_link.go @@ -33,7 +33,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { // Of text and link contents sl := strings.SplitSeq(content, "|") for v := range sl { - if equalPos := strings.IndexByte(v, '='); equalPos == -1 { + if found := strings.Contains(v, "="); !found { // There is no equal in this argument; this is a mandatory arg if props["name"] == "" { if IsFullURLString(v) { @@ -55,8 +55,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 @@ -113,16 +113,17 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) { } childNode.Parent = linkNode absoluteLink := IsFullURLString(link) - if !absoluteLink { + // FIXME: it should be fully refactored in the future, it uses various hacky approaches to guess how to encode a path for wiki + // When a link contains "/", then we assume that the user has provided a well-encoded link. + if !absoluteLink && !strings.Contains(link, "/") { + // So only guess for links without "/". if image { link = strings.ReplaceAll(link, " ", "+") } else { // the hacky wiki name encoding: space to "-" link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-" } - if !strings.Contains(link, "/") { - link = url.PathEscape(link) // FIXME: it doesn't seem right and it might cause double-escaping - } + link = url.PathEscape(link) } if image { title := props["title"] @@ -208,7 +209,6 @@ func createDescriptionLink(href, content string) *html.Node { Attr: []html.Attribute{ {Key: "href", Val: href}, {Key: "target", Val: "_blank"}, - {Key: "rel", Val: "noopener noreferrer"}, }, } textNode.Parent = linkNode diff --git a/package/gitea/source/modules/markup/html_mention.go b/package/gitea/source/modules/markup/html_mention.go index f97c034c..00cd51ca 100644 --- a/package/gitea/source/modules/markup/html_mention.go +++ b/package/gitea/source/modules/markup/html_mention.go @@ -4,6 +4,7 @@ package markup import ( + "fmt" "strings" "code.gitea.io/gitea/modules/references" @@ -26,14 +27,11 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { loc.End += start mention := node.Data[loc.Start:loc.End] teams, ok := ctx.RenderOptions.Metas["teams"] - // FIXME: util.URLJoin may not be necessary here: - // - setting.AppURL is defined to have a terminal '/' so unless mention[1:] - // is an AppSubURL link we can probably fallback to concatenation. - // team mention should follow @orgName/teamName style + if ok && strings.Contains(mention, "/") { mentionOrgAndTeam := strings.Split(mention, "/") if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { - link := "/:root/" + util.URLJoin("org", ctx.RenderOptions.Metas["org"], "teams", mentionOrgAndTeam[1]) + link := fmt.Sprintf("/:root/org/%s/teams/%s", ctx.RenderOptions.Metas["org"], mentionOrgAndTeam[1]) replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/)) node = node.NextSibling.NextSibling start = 0 diff --git a/package/gitea/source/modules/markup/html_node.go b/package/gitea/source/modules/markup/html_node.go index 4eb78fdd..f98e7429 100644 --- a/package/gitea/source/modules/markup/html_node.go +++ b/package/gitea/source/modules/markup/html_node.go @@ -6,13 +6,15 @@ package markup import ( "strings" + "code.gitea.io/gitea/modules/markup/common" + "golang.org/x/net/html" ) func isAnchorIDUserContent(s string) bool { // blackfridayExtRegex is for blackfriday extensions create IDs like fn:user-content-footnote // old logic: blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`) - return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-") + return strings.HasPrefix(s, "user-content-") || strings.Contains(s, ":user-content-") || isAnchorIDFootnote(s) } func isAnchorIDFootnote(s string) bool { @@ -23,16 +25,80 @@ func isAnchorHrefFootnote(s string) bool { return strings.HasPrefix(s, "#fnref:user-content-") || strings.HasPrefix(s, "#fn:user-content-") } -func processNodeAttrID(node *html.Node) { +// isHeadingTag returns true if the node is a heading tag (h1-h6) +func isHeadingTag(node *html.Node) bool { + return node.Type == html.ElementNode && + len(node.Data) == 2 && + node.Data[0] == 'h' && + node.Data[1] >= '1' && node.Data[1] <= '6' +} + +// getNodeText extracts the text content from a node and its children +func getNodeText(node *html.Node, cached **string) string { + if *cached != nil { + return **cached + } + var text strings.Builder + var extractText func(*html.Node) + extractText = func(n *html.Node) { + if n.Type == html.TextNode { + text.WriteString(n.Data) + } + for c := n.FirstChild; c != nil; c = c.NextSibling { + extractText(c) + } + } + extractText(node) + textStr := text.String() + *cached = &textStr + return textStr +} + +func processNodeHeadingAndID(ctx *RenderContext, node *html.Node) { + // TODO: handle duplicate IDs, need to track existing IDs in the document // Add user-content- to IDs and "#" links if they don't already have them, // and convert the link href to a relative link to the host root + attrIDVal := "" for idx, attr := range node.Attr { if attr.Key == "id" { - if !isAnchorIDUserContent(attr.Val) { - node.Attr[idx].Val = "user-content-" + attr.Val + attrIDVal = attr.Val + if !isAnchorIDUserContent(attrIDVal) { + attrIDVal = "user-content-" + attrIDVal + node.Attr[idx].Val = attrIDVal } } } + + if !isHeadingTag(node) || !ctx.RenderOptions.EnableHeadingIDGeneration { + return + } + + // For heading tags (h1-h6) without an id attribute, generate one from the text content. + // This ensures HTML headings like

    Title

    get proper permalink anchors + // matching the behavior of Markdown headings. + // Only enabled for repository files and wiki pages via EnableHeadingIDGeneration option. + var nodeTextCached *string + if attrIDVal == "" { + nodeText := getNodeText(node, &nodeTextCached) + if nodeText != "" { + // Use the same CleanValue function used by Markdown heading ID generation + attrIDVal = string(common.CleanValue([]byte(nodeText))) + if attrIDVal != "" { + attrIDVal = "user-content-" + attrIDVal + node.Attr = append(node.Attr, html.Attribute{Key: "id", Val: attrIDVal}) + } + } + } + if ctx.TocShowInSection != "" { + nodeText := getNodeText(node, &nodeTextCached) + if nodeText != "" && attrIDVal != "" { + ctx.TocHeadingItems = append(ctx.TocHeadingItems, &TocHeadingItem{ + HeadingLevel: int(node.Data[1] - '0'), + AnchorID: attrIDVal, + InnerText: nodeText, + }) + } + } } func processFootnoteNode(ctx *RenderContext, node *html.Node) { diff --git a/package/gitea/source/modules/markup/html_node_test.go b/package/gitea/source/modules/markup/html_node_test.go new file mode 100644 index 00000000..007e3c2a --- /dev/null +++ b/package/gitea/source/modules/markup/html_node_test.go @@ -0,0 +1,104 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProcessNodeAttrID_HTMLHeadingWithoutID(t *testing.T) { + // Test that HTML headings without id get an auto-generated id from their text content + // when EnableHeadingIDGeneration is true (for repo files and wiki pages) + testCases := []struct { + name string + input string + expected string + }{ + { + name: "h1 without id", + input: `

    Heading without ID

    `, + expected: `

    Heading without ID

    `, + }, + { + name: "h2 without id", + input: `

    Another Heading

    `, + expected: `

    Another Heading

    `, + }, + { + name: "h3 without id", + input: `

    Third Level

    `, + expected: `

    Third Level

    `, + }, + { + name: "h1 with existing id should keep it", + input: `

    Heading with ID

    `, + expected: `

    Heading with ID

    `, + }, + { + name: "h1 with user-content prefix should not double prefix", + input: `

    Already Prefixed

    `, + expected: `

    Already Prefixed

    `, + }, + { + name: "heading with special characters", + input: `

    What is Wine Staging?

    `, + expected: `

    What is Wine Staging?

    `, + }, + { + name: "heading with nested elements", + input: `

    Bold and Italic

    `, + expected: `

    Bold and Italic

    `, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var result strings.Builder + ctx := NewTestRenderContext().WithEnableHeadingIDGeneration(true) + err := PostProcessDefault(ctx, strings.NewReader(tc.input), &result) + assert.NoError(t, err) + assert.Equal(t, tc.expected, strings.TrimSpace(result.String())) + }) + } +} + +func TestProcessNodeAttrID_SkipHeadingIDForComments(t *testing.T) { + // Test that HTML headings in comment-like contexts (issue comments) + // do NOT get auto-generated IDs to avoid duplicate IDs on pages with multiple documents. + // This is controlled by EnableHeadingIDGeneration which defaults to false. + testCases := []struct { + name string + input string + expected string + }{ + { + name: "h1 without id in comment context", + input: `

    Heading without ID

    `, + expected: `

    Heading without ID

    `, + }, + { + name: "h2 without id in comment context", + input: `

    Another Heading

    `, + expected: `

    Another Heading

    `, + }, + { + name: "h1 with existing id should still be prefixed", + input: `

    Heading with ID

    `, + expected: `

    Heading with ID

    `, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var result strings.Builder + // Default context without EnableHeadingIDGeneration (simulates comment rendering) + err := PostProcessDefault(NewTestRenderContext(), strings.NewReader(tc.input), &result) + assert.NoError(t, err) + assert.Equal(t, tc.expected, strings.TrimSpace(result.String())) + }) + } +} diff --git a/package/gitea/source/modules/markup/html_test.go b/package/gitea/source/modules/markup/html_test.go index 08b050ba..e62747c7 100644 --- a/package/gitea/source/modules/markup/html_test.go +++ b/package/gitea/source/modules/markup/html_test.go @@ -34,15 +34,15 @@ func TestRender_Commits(t *testing.T) { sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" repo := markup.TestAppURL + testRepoOwnerName + "/" + testRepoName + "/" - commit := util.URLJoin(repo, "commit", sha) + commit := repo + "commit/" + sha commitPath := "/user13/repo11/commit/" + sha - tree := util.URLJoin(repo, "tree", sha, "src") + tree := repo + "tree/" + sha + "/src" - file := util.URLJoin(repo, "commit", sha, "example.txt") + file := repo + "commit/" + sha + "/example.txt" fileWithExtra := file + ":" fileWithHash := file + "#L2" fileWithHasExtra := file + "#L2:" - commitCompare := util.URLJoin(repo, "compare", sha+"..."+sha) + commitCompare := repo + "compare/" + sha + "..." + sha commitCompareWithHash := commitCompare + "#L2" test(sha, `

    65f1bf27bc

    `) @@ -71,6 +71,7 @@ func TestRender_Commits(t *testing.T) { } func TestRender_CrossReferences(t *testing.T) { + defer testModule.MockVariableValue(&setting.AppURL, markup.TestAppURL)() defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() test := func(input, expected string) { rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md") @@ -89,19 +90,29 @@ func TestRender_CrossReferences(t *testing.T) { "/home/gitea/go-gitea/gitea#12345", `

    /home/gitea/go-gitea/gitea#12345

    `) test( - util.URLJoin(markup.TestAppURL, "gogitea", "gitea", "issues", "12345"), - `

    gogitea/gitea#12345

    `) + markup.TestAppURL+"gogitea/gitea/issues/12345", + `

    gogitea/gitea#12345

    `) test( - util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345"), - `

    go-gitea/gitea#12345

    `) + markup.TestAppURL+"go-gitea/gitea/issues/12345", + `

    go-gitea/gitea#12345

    `) test( - util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"), - `

    gogitea/some-repo-name#12345

    `) + markup.TestAppURL+"gogitea/some-repo-name/issues/12345", + `

    gogitea/some-repo-name#12345

    `) - inputURL := "https://host/a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3" + inputURL := setting.AppURL + "a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3" test( inputURL, `

    0123456789/foo.txt (L2-L3)

    `) + + inputURL = setting.AppURL + "repo/owner/archive/0123456789012345678901234567890123456789.tar.gz" + test( + inputURL, + `

    0123456789.tar.gz

    `) + + inputURL = setting.AppURL + "owner/repo/commit/0123456789012345678901234567890123456789.patch?key=val" + test( + inputURL, + `

    0123456789.patch

    `) } func TestRender_links(t *testing.T) { @@ -364,7 +375,7 @@ func TestRender_emoji(t *testing.T) { func TestRender_ShortLinks(t *testing.T) { setting.AppURL = markup.TestAppURL - tree := util.URLJoin(markup.TestRepoURL, "src", "master") + tree := markup.TestRepoURL + "src/master" test := func(input, expected string) { buffer, err := markdown.RenderString(markup.NewTestRenderContext(tree), input) @@ -372,15 +383,15 @@ func TestRender_ShortLinks(t *testing.T) { assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) } - url := util.URLJoin(tree, "Link") - otherURL := util.URLJoin(tree, "Other-Link") - encodedURL := util.URLJoin(tree, "Link%3F") - imgurl := util.URLJoin(tree, "Link.jpg") - otherImgurl := util.URLJoin(tree, "Link+Other.jpg") - encodedImgurl := util.URLJoin(tree, "Link+%23.jpg") - notencodedImgurl := util.URLJoin(tree, "some", "path", "Link+#.jpg") - renderableFileURL := util.URLJoin(tree, "markdown_file.md") - unrenderableFileURL := util.URLJoin(tree, "file.zip") + url := tree + "/Link" + otherURL := tree + "/Other-Link" + encodedURL := tree + "/Link%3F" + imgurl := tree + "/Link.jpg" + otherImgurl := tree + "/Link+Other.jpg" + encodedImgurl := tree + "/Link+%23.jpg" + notencodedImgurl := tree + "/some/path/Link%20#.jpg" + renderableFileURL := tree + "/markdown_file.md" + unrenderableFileURL := tree + "/file.zip" favicon := "http://google.com/favicon.ico" test( @@ -455,6 +466,8 @@ func TestRender_ShortLinks(t *testing.T) { "[[Name|Link #.jpg|alt=\"AltName\"|title='Title']]", `

    AltName

    `, ) + // FIXME: it's unable to resolve: [[link?k=v]] + // FIXME: it is a wrong test case, it is not an image, but a link with anchor "#.jpg" test( "[[some/path/Link #.jpg]]", `

    some/path/Link #.jpg

    `, @@ -565,13 +578,15 @@ func TestFuzz(t *testing.T) { } func TestIssue18471(t *testing.T) { - data := `http://domain/org/repo/compare/783b039...da951ce` + defer testModule.MockVariableValue(&setting.AppURL, markup.TestAppURL)() + + data := markup.TestAppURL + `org/repo/compare/783b039...da951ce` var res strings.Builder err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res) assert.NoError(t, err) - assert.Equal(t, `783b039...da951ce`, res.String()) + assert.Equal(t, `783b039...da951ce`, res.String()) } func TestIsFullURL(t *testing.T) { diff --git a/package/gitea/source/modules/markup/html_toc_test.go b/package/gitea/source/modules/markup/html_toc_test.go new file mode 100644 index 00000000..e93cfc93 --- /dev/null +++ b/package/gitea/source/modules/markup/html_toc_test.go @@ -0,0 +1,60 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package markup_test + +import ( + "regexp" + "testing" + + "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/markup/markdown" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestToCWithHTML(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + + t1 := `tag link and Bold` + t2 := "code block ``" + t3 := "markdown **bold**" + input := `--- +include_toc: true +--- + +# ` + t1 + ` +## ` + t2 + ` +#### ` + t3 + ` +## last +` + + renderCtx := markup.NewTestRenderContext().WithEnableHeadingIDGeneration(true) + resultHTML, err := markdown.RenderString(renderCtx, input) + assert.NoError(t, err) + result := string(resultHTML) + re := regexp.MustCompile(`(?s)
    .*?
    `) + result = re.ReplaceAllString(result, "\n") + expected := `
    toc + +
    + +

    tag link and Bold

    +

    code block <a>

    +

    markdown bold

    +

    last

    +` + assert.Equal(t, expected, result) +} diff --git a/package/gitea/source/modules/markup/main_test.go b/package/gitea/source/modules/markup/main_test.go index 564f55ac..a8dcff47 100644 --- a/package/gitea/source/modules/markup/main_test.go +++ b/package/gitea/source/modules/markup/main_test.go @@ -14,5 +14,7 @@ import ( func TestMain(m *testing.M) { setting.IsInTesting = true markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true + setting.Markdown.FileNamePatterns = []string{"*.md"} + markup.RefreshFileNamePatterns() os.Exit(m.Run()) } diff --git a/package/gitea/source/modules/markup/markdown/goldmark.go b/package/gitea/source/modules/markup/markdown/goldmark.go index b28fa982..555a1716 100644 --- a/package/gitea/source/modules/markup/markdown/goldmark.go +++ b/package/gitea/source/modules/markup/markdown/goldmark.go @@ -41,11 +41,10 @@ func (g *ASTTransformer) applyElementDir(n ast.Node) { // Transform transforms the given AST tree. func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { firstChild := node.FirstChild() - tocMode := "" ctx := pc.Get(renderContextKey).(*markup.RenderContext) rc := pc.Get(renderConfigKey).(*RenderConfig) - tocList := make([]Header, 0, 20) + tocMode := "" if rc.yamlNode != nil { metaNode := rc.toMetaNode(g) if metaNode != nil { @@ -60,8 +59,6 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa } switch v := n.(type) { - case *ast.Heading: - g.transformHeading(ctx, v, reader, &tocList) case *ast.Paragraph: g.applyElementDir(v) case *ast.List: @@ -79,19 +76,18 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa return ast.WalkContinue, nil }) - showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main" - showTocInSidebar := !showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar - if len(tocList) > 0 && (showTocInMain || showTocInSidebar) { - if showTocInMain { - tocNode := createTOCNode(tocList, rc.Lang, nil) - node.InsertBefore(node, firstChild, tocNode) - } else { - tocNode := createTOCNode(tocList, rc.Lang, map[string]string{"open": "open"}) - ctx.SidebarTocNode = tocNode + if ctx.RenderOptions.EnableHeadingIDGeneration { + showTocInMain := tocMode == "true" /* old behavior, in main view */ || tocMode == "main" + showTocInSidebar := !showTocInMain && tocMode != "false" // not hidden, not main, then show it in sidebar + switch { + case showTocInMain: + ctx.TocShowInSection = markup.TocShowInMain + case showTocInSidebar: + ctx.TocShowInSection = markup.TocShowInSidebar } } - if len(rc.Lang) > 0 { + if rc.Lang != "" { node.SetAttributeString("lang", []byte(rc.Lang)) } } diff --git a/package/gitea/source/modules/markup/markdown/markdown.go b/package/gitea/source/modules/markup/markdown/markdown.go index 3b788432..6a81a8d3 100644 --- a/package/gitea/source/modules/markup/markdown/markdown.go +++ b/package/gitea/source/modules/markup/markdown/markdown.go @@ -5,6 +5,7 @@ package markdown import ( + "bytes" "errors" "html/template" "io" @@ -20,11 +21,12 @@ import ( chromahtml "github.com/alecthomas/chroma/v2/formatters/html" "github.com/yuin/goldmark" highlighting "github.com/yuin/goldmark-highlighting/v2" - meta "github.com/yuin/goldmark-meta" + "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" ) @@ -57,7 +59,7 @@ func (l *limitWriter) Write(data []byte) (int, error) { // newParserContext creates a parser.Context with the render context set func newParserContext(ctx *markup.RenderContext) parser.Context { - pc := parser.NewContext(parser.WithIDs(newPrefixedIDs())) + pc := parser.NewContext() pc.Set(renderContextKey, ctx) return pc } @@ -101,12 +103,48 @@ func (r *GlodmarkRender) highlightingRenderer(w util.BufWriter, c highlighting.C } } +type goldmarkEmphasisParser struct { + parser.InlineParser +} + +func goldmarkNewEmphasisParser() parser.InlineParser { + return &goldmarkEmphasisParser{parser.NewEmphasisParser()} +} + +func (s *goldmarkEmphasisParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { + line, _ := block.PeekLine() + if len(line) > 1 && line[0] == '_' { + // a special trick to avoid parsing emphasis in filenames like "module/__init__.py" + end := bytes.IndexByte(line[1:], '_') + mark := bytes.Index(line, []byte("_.py")) + // check whether the "end" matches "_.py" or "__.py" + if mark != -1 && (end == mark || end == mark-1) { + return nil + } + } + return s.InlineParser.Parse(parent, block, pc) +} + +func goldmarkDefaultParser() parser.Parser { + return parser.NewParser(parser.WithBlockParsers(parser.DefaultBlockParsers()...), + parser.WithInlineParsers([]util.PrioritizedValue{ + util.Prioritized(parser.NewCodeSpanParser(), 100), + util.Prioritized(parser.NewLinkParser(), 200), + util.Prioritized(parser.NewAutoLinkParser(), 300), + util.Prioritized(parser.NewRawHTMLParser(), 400), + util.Prioritized(goldmarkNewEmphasisParser(), 500), + }...), + parser.WithParagraphTransformers(parser.DefaultParagraphTransformers()...), + ) +} + // SpecializedMarkdown sets up the Gitea specific markdown extensions func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { // TODO: it could use a pool to cache the renderers to reuse them with different contexts // at the moment it is fast enough (see the benchmarks) r := &GlodmarkRender{ctx: ctx} r.goldmarkMarkdown = goldmark.New( + goldmark.WithParser(goldmarkDefaultParser()), goldmark.WithExtensions( extension.NewTable(extension.WithTableCellAlignMethod(extension.TableCellAlignAttribute)), extension.Strikethrough, @@ -127,11 +165,9 @@ func SpecializedMarkdown(ctx *markup.RenderContext) *GlodmarkRender { ParseBlockDollar: setting.Markdown.MathCodeBlockOptions.ParseBlockDollar, ParseBlockSquareBrackets: setting.Markdown.MathCodeBlockOptions.ParseBlockSquareBrackets, // this is a bad syntax "\[ ... \]", it conflicts with normal markdown escaping }), - meta.Meta, ), goldmark.WithParserOptions( parser.WithAttribute(), - parser.WithAutoHeadingID(), parser.WithASTTransformers(util.Prioritized(NewASTTransformer(&ctx.RenderInternal), 10000)), ), goldmark.WithRendererOptions(html.WithUnsafe()), @@ -202,30 +238,24 @@ func init() { markup.RegisterRenderer(Renderer{}) } -// Renderer implements markup.Renderer type Renderer struct{} var _ markup.PostProcessRenderer = (*Renderer)(nil) -// Name implements markup.Renderer func (Renderer) Name() string { return MarkupName } -// NeedPostProcess implements markup.PostProcessRenderer func (Renderer) NeedPostProcess() bool { return true } -// Extensions implements markup.Renderer -func (Renderer) Extensions() []string { - return setting.Markdown.FileExtensions +func (Renderer) FileNamePatterns() []string { + return setting.Markdown.FileNamePatterns } -// SanitizerRules implements markup.Renderer func (Renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{} } -// Render implements markup.Renderer func (Renderer) Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error { return render(ctx, input, output) } @@ -240,7 +270,9 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error func RenderString(ctx *markup.RenderContext, content string) (template.HTML, error) { var buf strings.Builder if err := Render(ctx, strings.NewReader(content), &buf); err != nil { - return "", err + log.Warn("Unable to RenderString: %v, content: %s", err, giteautil.TruncateRunes(content, 200)) + err = nil + return template.HTML(template.HTMLEscapeString(content)), err } return template.HTML(buf.String()), nil } diff --git a/package/gitea/source/modules/markup/markdown/markdown_test.go b/package/gitea/source/modules/markup/markdown/markdown_test.go index 4eb01bcc..e231b037 100644 --- a/package/gitea/source/modules/markup/markdown/markdown_test.go +++ b/package/gitea/source/modules/markup/markdown/markdown_test.go @@ -14,7 +14,6 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" - "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" ) @@ -23,7 +22,6 @@ const ( AppURL = "http://localhost:3000/" testRepoOwnerName = "user13" testRepoName = "repo11" - FullURL = AppURL + testRepoOwnerName + "/" + testRepoName + "/" ) // these values should match the const above @@ -47,8 +45,9 @@ func TestRender_StandardLinks(t *testing.T) { func TestRender_Images(t *testing.T) { setting.AppURL = AppURL + const baseLink = "http://localhost:3000/user13/repo11" render := func(input, expected string) { - buffer, err := markdown.RenderString(markup.NewTestRenderContext(FullURL), input) + buffer, err := markdown.RenderString(markup.NewTestRenderContext(baseLink), input) assert.NoError(t, err) assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer))) } @@ -56,7 +55,7 @@ func TestRender_Images(t *testing.T) { url := "../../.images/src/02/train.jpg" title := "Train" href := "https://gitea.io" - result := util.URLJoin(FullURL, url) + result := baseLink + "/.images/src/02/train.jpg" // resolved link should not go out of the base link // hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now render( @@ -88,6 +87,8 @@ func TestRender_Images(t *testing.T) { } func TestTotal_RenderString(t *testing.T) { + const FullURL = AppURL + testRepoOwnerName + "/" + testRepoName + "/" + setting.AppURL = AppURL defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() // Test cases without ambiguous links (It is not right to copy a whole file here, instead it should clearly test what is being tested) @@ -258,7 +259,7 @@ This PR has been generated by [Renovate Bot](https://github.com/renovatebot/reno }, }) for i := range sameCases { - line, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), sameCases[i]) + line, err := markdown.RenderString(markup.NewTestRenderContext(localMetas).WithEnableHeadingIDGeneration(true), sameCases[i]) assert.NoError(t, err) assert.Equal(t, testAnswers[i], string(line)) } @@ -428,9 +429,12 @@ test --- test `, - `- item1 -- item2 - + `
    +
      +
    • item1
    • +
    • item2
    • +
    +

    test

    `, }, @@ -442,8 +446,8 @@ anything --- test `, - `anything - + `
    +

    anything

    test

    `, }, @@ -470,18 +474,33 @@ foo: bar
    • task 1
    +`, + }, + // we have our own frontmatter parser, don't need to use github.com/yuin/goldmark-meta + { + "InvalidFrontmatter", + `--- +foo +`, + `
    +

    foo

    `, }, } - for _, test := range testcases { - res, err := markdown.RenderString(markup.NewTestRenderContext(), test.input) - assert.NoError(t, err, "Unexpected error in testcase: %q", test.name) - assert.Equal(t, test.expected, string(res), "Unexpected result in testcase %q", test.name) + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + res, err := markdown.RenderString(markup.NewTestRenderContext(), tt.input) + assert.NoError(t, err, "Unexpected error in testcase: %q", tt.name) + assert.Equal(t, tt.expected, string(res), "Unexpected result in testcase %q", tt.name) + }) } } func TestRenderLinks(t *testing.T) { + defer test.MockVariableValue(&setting.AppURL, AppURL)() + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() + input := ` space @mention-user${SPACE}${SPACE} /just/a/path.bin https://example.com/file.bin @@ -519,9 +538,9 @@ mail@domain.com remote image local image remote link -88fc37a3c0...12fc37a3c0 (hash) +https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare -88fc37a3c0 +https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit 👍 mail@domain.com @@ -529,10 +548,21 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit #123 space

    ` - defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)() result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input) assert.NoError(t, err) assert.Equal(t, expected, string(result)) + + t.Run("LocalCommitAndCompare", func(t *testing.T) { + input := `http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb +http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash` + + expected := `

    88fc37a3c0 +88fc37a3c0...12fc37a3c0 (hash)

    +` + result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input) + assert.NoError(t, err) + assert.Equal(t, expected, string(result)) + }) } func TestMarkdownLink(t *testing.T) { @@ -545,5 +575,28 @@ func TestMarkdownLink(t *testing.T) { assert.Equal(t, `

    link1 link2 link3

    +`, string(result)) + + input = "https://example.com/__init__.py" + result, err = markdown.RenderString(markup.NewTestRenderContext("/base", localMetas), input) + assert.NoError(t, err) + assert.Equal(t, `

    https://example.com/__init__.py

    +`, string(result)) +} + +func TestMarkdownUlDir(t *testing.T) { + defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, false)() + result, err := markdown.RenderString(markup.NewTestRenderContext(), ` +* a + * b +`) + assert.NoError(t, err) + assert.Equal(t, `
      +
    • a +
        +
      • b
      • +
      +
    • +
    `, string(result)) } diff --git a/package/gitea/source/modules/markup/markdown/meta.go b/package/gitea/source/modules/markup/markdown/meta.go index e76b253e..6ddd8921 100644 --- a/package/gitea/source/modules/markup/markdown/meta.go +++ b/package/gitea/source/modules/markup/markdown/meta.go @@ -60,8 +60,8 @@ func ExtractMetadata(contents string, out any) (string, error) { return string(body), err } -// ExtractMetadata consumes a markdown file, parses YAML frontmatter, -// and returns the frontmatter metadata separated from the markdown content +// ExtractMetadataBytes consumes a Markdown content, parses YAML frontmatter, +// and returns the frontmatter metadata separated from the Markdown content func ExtractMetadataBytes(contents []byte, out any) ([]byte, error) { var front, body []byte diff --git a/package/gitea/source/modules/markup/markdown/prefixed_id.go b/package/gitea/source/modules/markup/markdown/prefixed_id.go deleted file mode 100644 index 63d7fadc..00000000 --- a/package/gitea/source/modules/markup/markdown/prefixed_id.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markdown - -import ( - "bytes" - "fmt" - - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/markup/common" - "code.gitea.io/gitea/modules/util" - - "github.com/yuin/goldmark/ast" -) - -type prefixedIDs struct { - values container.Set[string] -} - -// Generate generates a new element id. -func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte { - dft := []byte("id") - if kind == ast.KindHeading { - dft = []byte("heading") - } - return p.GenerateWithDefault(value, dft) -} - -// GenerateWithDefault generates a new element id. -func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte { - result := common.CleanValue(value) - if len(result) == 0 { - result = dft - } - if !bytes.HasPrefix(result, []byte("user-content-")) { - result = append([]byte("user-content-"), result...) - } - if p.values.Add(util.UnsafeBytesToString(result)) { - return result - } - for i := 1; ; i++ { - newResult := fmt.Sprintf("%s-%d", result, i) - if p.values.Add(newResult) { - return []byte(newResult) - } - } -} - -// Put puts a given element id to the used ids table. -func (p *prefixedIDs) Put(value []byte) { - p.values.Add(util.UnsafeBytesToString(value)) -} - -func newPrefixedIDs() *prefixedIDs { - return &prefixedIDs{ - values: make(container.Set[string]), - } -} diff --git a/package/gitea/source/modules/markup/markdown/toc.go b/package/gitea/source/modules/markup/markdown/toc.go deleted file mode 100644 index a11b9d03..00000000 --- a/package/gitea/source/modules/markup/markdown/toc.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markdown - -import ( - "net/url" - - "code.gitea.io/gitea/modules/translation" - - "github.com/yuin/goldmark/ast" -) - -// Header holds the data about a header. -type Header struct { - Level int - Text string - ID string -} - -func createTOCNode(toc []Header, lang string, detailsAttrs map[string]string) ast.Node { - details := NewDetails() - summary := NewSummary() - - for k, v := range detailsAttrs { - details.SetAttributeString(k, []byte(v)) - } - - summary.AppendChild(summary, ast.NewString([]byte(translation.NewLocale(lang).TrString("toc")))) - details.AppendChild(details, summary) - ul := ast.NewList('-') - details.AppendChild(details, ul) - currentLevel := 6 - for _, header := range toc { - if header.Level < currentLevel { - currentLevel = header.Level - } - } - for _, header := range toc { - for currentLevel > header.Level { - ul = ul.Parent().(*ast.List) - currentLevel-- - } - for currentLevel < header.Level { - newL := ast.NewList('-') - ul.AppendChild(ul, newL) - currentLevel++ - ul = newL - } - li := ast.NewListItem(currentLevel * 2) - a := ast.NewLink() - a.Destination = []byte("#" + url.QueryEscape(header.ID)) - a.AppendChild(a, ast.NewString([]byte(header.Text))) - li.AppendChild(li, a) - ul.AppendChild(ul, li) - } - - return details -} diff --git a/package/gitea/source/modules/markup/markdown/transform_heading.go b/package/gitea/source/modules/markup/markdown/transform_heading.go deleted file mode 100644 index a229a7b1..00000000 --- a/package/gitea/source/modules/markup/markdown/transform_heading.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package markdown - -import ( - "fmt" - - "code.gitea.io/gitea/modules/markup" - "code.gitea.io/gitea/modules/util" - - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/text" -) - -func (g *ASTTransformer) transformHeading(_ *markup.RenderContext, v *ast.Heading, reader text.Reader, tocList *[]Header) { - for _, attr := range v.Attributes() { - if _, ok := attr.Value.([]byte); !ok { - v.SetAttribute(attr.Name, fmt.Appendf(nil, "%v", attr.Value)) - } - } - txt := v.Text(reader.Source()) //nolint:staticcheck // Text is deprecated - header := Header{ - Text: util.UnsafeBytesToString(txt), - Level: v.Level, - } - if id, found := v.AttributeString("id"); found { - header.ID = util.UnsafeBytesToString(id.([]byte)) - } - *tocList = append(*tocList, header) - g.applyElementDir(v) -} diff --git a/package/gitea/source/modules/markup/markdown/transform_list.go b/package/gitea/source/modules/markup/markdown/transform_list.go index c89ad2f2..6cafa8ff 100644 --- a/package/gitea/source/modules/markup/markdown/transform_list.go +++ b/package/gitea/source/modules/markup/markdown/transform_list.go @@ -81,5 +81,16 @@ func (g *ASTTransformer) transformList(_ *markup.RenderContext, v *ast.List, rc v.AppendChild(v, newChild) } } - g.applyElementDir(v) + + nestedList := false + for p := v.Parent(); p != nil; p = p.Parent() { + if _, ok := p.(*ast.List); ok { + nestedList = true + break + } + } + if !nestedList { + // "dir=auto" should be only added to top-level "ul". https://github.com/go-gitea/gitea/issues/35058 + g.applyElementDir(v) + } } diff --git a/package/gitea/source/modules/markup/mdstripper/mdstripper.go b/package/gitea/source/modules/markup/mdstripper/mdstripper.go index 5a650441..bf69051e 100644 --- a/package/gitea/source/modules/markup/mdstripper/mdstripper.go +++ b/package/gitea/source/modules/markup/mdstripper/mdstripper.go @@ -46,7 +46,7 @@ func (r *stripRenderer) Render(w io.Writer, source []byte, doc ast.Node) error { coalesce := prevSibIsText r.processString( w, - v.Text(source), //nolint:staticcheck // Text is deprecated + v.Value(source), coalesce) if v.SoftLineBreak() { r.doubleSpace(w) @@ -165,7 +165,6 @@ func StripMarkdownBytes(rawBytes []byte) ([]byte, []string) { ), goldmark.WithParserOptions( parser.WithAttribute(), - parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions( html.WithUnsafe(), diff --git a/package/gitea/source/modules/markup/orgmode/orgmode.go b/package/gitea/source/modules/markup/orgmode/orgmode.go index 93c335d2..fd307164 100644 --- a/package/gitea/source/modules/markup/orgmode/orgmode.go +++ b/package/gitea/source/modules/markup/orgmode/orgmode.go @@ -5,7 +5,6 @@ package orgmode import ( "fmt" - "html" "html/template" "io" "strings" @@ -17,7 +16,6 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/alecthomas/chroma/v2" - "github.com/alecthomas/chroma/v2/lexers" "github.com/niklasfasching/go-org/org" ) @@ -33,20 +31,16 @@ var ( _ markup.PostProcessRenderer = (*renderer)(nil) ) -// Name implements markup.Renderer func (renderer) Name() string { return "orgmode" } -// NeedPostProcess implements markup.PostProcessRenderer func (renderer) NeedPostProcess() bool { return true } -// Extensions implements markup.Renderer -func (renderer) Extensions() []string { - return []string{".org"} +func (renderer) FileNamePatterns() []string { + return []string{"*.org"} } -// SanitizerRules implements markup.Renderer func (renderer) SanitizerRules() []setting.MarkupSanitizerRule { return []setting.MarkupSanitizerRule{} } @@ -57,40 +51,20 @@ func Render(ctx *markup.RenderContext, input io.Reader, output io.Writer) error htmlWriter.HighlightCodeBlock = func(source, lang string, inline bool, params map[string]string) string { defer func() { if err := recover(); err != nil { + // catch the panic, log the error and return empty result log.Error("Panic in HighlightCodeBlock: %v\n%s", err, log.Stack(2)) - panic(err) } }() - w := &strings.Builder{} - lexer := lexers.Get(lang) - if lexer == nil && lang == "" { - lexer = lexers.Analyse(source) - if lexer == nil { - lexer = lexers.Fallback - } - lang = strings.ToLower(lexer.Config().Name) - } + lexer := highlight.DetectChromaLexerByFileName("", lang) // don't use content to detect, it is too slow + lexer = chroma.Coalesce(lexer) + sb := &strings.Builder{} // include language-x class as part of commonmark spec - if err := ctx.RenderInternal.FormatWithSafeAttrs(w, `
    `, lang); err != nil {
    -			return ""
    -		}
    -		if lexer == nil {
    -			if _, err := w.WriteString(html.EscapeString(source)); err != nil {
    -				return ""
    -			}
    -		} else {
    -			lexer = chroma.Coalesce(lexer)
    -			if _, err := w.WriteString(string(highlight.CodeFromLexer(lexer, source))); err != nil {
    -				return ""
    -			}
    -		}
    -		if _, err := w.WriteString("
    "); err != nil { - return "" - } - - return w.String() + _ = ctx.RenderInternal.FormatWithSafeAttrs(sb, `
    `, strings.ToLower(lexer.Config().Name))
    +		_, _ = sb.WriteString(string(highlight.RenderCodeByLexer(lexer, source)))
    +		_, _ = sb.WriteString("
    ") + return sb.String() } w := &orgWriter{rctx: ctx, HTMLWriter: htmlWriter} diff --git a/package/gitea/source/modules/markup/render.go b/package/gitea/source/modules/markup/render.go index c6457490..6e8838d4 100644 --- a/package/gitea/source/modules/markup/render.go +++ b/package/gitea/source/modules/markup/render.go @@ -4,7 +4,9 @@ package markup import ( + "bytes" "context" + "errors" "fmt" "html/template" "io" @@ -15,10 +17,11 @@ import ( "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/markup/internal" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/typesniffer" "code.gitea.io/gitea/modules/util" - "github.com/yuin/goldmark/ast" "golang.org/x/sync/errgroup" ) @@ -36,6 +39,15 @@ var RenderBehaviorForTesting struct { DisableAdditionalAttributes bool } +type WebThemeInterface interface { + PublicAssetURI() string +} + +type StandalonePageOptions struct { + CurrentWebTheme WebThemeInterface + RenderQueryString string +} + type RenderOptions struct { UseAbsoluteLink bool @@ -53,7 +65,24 @@ type RenderOptions struct { Metas map[string]string // used by external render. the router "/org/repo/render/..." will output the rendered content in a standalone page - InStandalonePage bool + StandalonePageOptions *StandalonePageOptions + + // EnableHeadingIDGeneration controls whether to auto-generate IDs for HTML headings without id attribute. + // This should be enabled for repository files and wiki pages, but disabled for comments to avoid duplicate IDs. + EnableHeadingIDGeneration bool +} + +type TocShowInSectionType string + +const ( + TocShowInSidebar TocShowInSectionType = "sidebar" + TocShowInMain TocShowInSectionType = "main" +) + +type TocHeadingItem struct { + HeadingLevel int + AnchorID string + InnerText string } // RenderContext represents a render context @@ -63,7 +92,8 @@ type RenderContext struct { // the context might be used by the "render" function, but it might also be used by "postProcess" function usedByRender bool - SidebarTocNode ast.Node + TocShowInSection TocShowInSectionType + TocHeadingItems []*TocHeadingItem RenderHelper RenderHelper RenderOptions RenderOptions @@ -107,8 +137,13 @@ func (ctx *RenderContext) WithMetas(metas map[string]string) *RenderContext { return ctx } -func (ctx *RenderContext) WithInStandalonePage(v bool) *RenderContext { - ctx.RenderOptions.InStandalonePage = v +func (ctx *RenderContext) WithStandalonePage(opts StandalonePageOptions) *RenderContext { + ctx.RenderOptions.StandalonePageOptions = &opts + return ctx +} + +func (ctx *RenderContext) WithEnableHeadingIDGeneration(v bool) *RenderContext { + ctx.RenderOptions.EnableHeadingIDGeneration = v return ctx } @@ -122,22 +157,29 @@ func (ctx *RenderContext) WithHelper(helper RenderHelper) *RenderContext { return ctx } -// FindRendererByContext finds renderer by RenderContext -// TODO: it should be merged with other similar functions like GetRendererByFileName, DetectMarkupTypeByFileName, etc -func FindRendererByContext(ctx *RenderContext) (Renderer, error) { +func (ctx *RenderContext) DetectMarkupRenderer(prefetchBuf []byte) Renderer { if ctx.RenderOptions.MarkupType == "" && ctx.RenderOptions.RelativePath != "" { - ctx.RenderOptions.MarkupType = DetectMarkupTypeByFileName(ctx.RenderOptions.RelativePath) - if ctx.RenderOptions.MarkupType == "" { - return nil, util.NewInvalidArgumentErrorf("unsupported file to render: %q", ctx.RenderOptions.RelativePath) + var sniffedType typesniffer.SniffedType + if len(prefetchBuf) > 0 { + sniffedType = typesniffer.DetectContentType(prefetchBuf) } + ctx.RenderOptions.MarkupType = DetectRendererTypeByPrefetch(ctx.RenderOptions.RelativePath, sniffedType, prefetchBuf) } + return renderers[ctx.RenderOptions.MarkupType] +} - renderer := renderers[ctx.RenderOptions.MarkupType] +func (ctx *RenderContext) DetectMarkupRendererByReader(in io.Reader) (Renderer, io.Reader, error) { + prefetchBuf := make([]byte, 512) + n, err := util.ReadAtMost(in, prefetchBuf) + if err != nil && err != io.EOF { + return nil, nil, err + } + prefetchBuf = prefetchBuf[:n] + renderer := ctx.DetectMarkupRenderer(prefetchBuf) if renderer == nil { - return nil, util.NewNotExistErrorf("unsupported markup type: %q", ctx.RenderOptions.MarkupType) + return nil, nil, util.NewInvalidArgumentErrorf("unable to find a render") } - - return renderer, nil + return renderer, io.MultiReader(bytes.NewReader(prefetchBuf), in), nil } func RendererNeedPostProcess(renderer Renderer) bool { @@ -148,12 +190,12 @@ func RendererNeedPostProcess(renderer Renderer) bool { } // Render renders markup file to HTML with all specific handling stuff. -func Render(ctx *RenderContext, input io.Reader, output io.Writer) error { - renderer, err := FindRendererByContext(ctx) +func Render(rctx *RenderContext, origInput io.Reader, output io.Writer) error { + renderer, input, err := rctx.DetectMarkupRendererByReader(origInput) if err != nil { return err } - return RenderWithRenderer(ctx, renderer, input, output) + return RenderWithRenderer(rctx, renderer, input, output) } // RenderString renders Markup string to HTML with all specific handling stuff and return string @@ -165,20 +207,24 @@ func RenderString(ctx *RenderContext, content string) (string, error) { return buf.String(), nil } -func renderIFrame(ctx *RenderContext, sandbox string, output io.Writer) error { +func RenderIFrame(ctx *RenderContext, opts *ExternalRendererOptions, output io.Writer) error { + ownerName, repoName := ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"] + refSubURL := ctx.RenderOptions.Metas["RefTypeNameSubURL"] + if ownerName == "" || repoName == "" || refSubURL == "" { + setting.PanicInDevOrTesting("RenderIFrame requires user, repo and RefTypeNameSubURL metas") + return errors.New("RenderIFrame requires user, repo and RefTypeNameSubURL metas") + } src := fmt.Sprintf("%s/%s/%s/render/%s/%s", setting.AppSubURL, - url.PathEscape(ctx.RenderOptions.Metas["user"]), - url.PathEscape(ctx.RenderOptions.Metas["repo"]), - util.PathEscapeSegments(ctx.RenderOptions.Metas["RefTypeNameSubURL"]), + url.PathEscape(ownerName), + url.PathEscape(repoName), + ctx.RenderOptions.Metas["RefTypeNameSubURL"], util.PathEscapeSegments(ctx.RenderOptions.RelativePath), ) - - var sandboxAttrValue template.HTML - if sandbox != "" { - sandboxAttrValue = htmlutil.HTMLFormat(`sandbox="%s"`, sandbox) + var extraAttrs template.HTML + if opts.ContentSandbox != "" { + extraAttrs = htmlutil.HTMLFormat(` sandbox="%s"`, opts.ContentSandbox) } - iframe := htmlutil.HTMLFormat(``, src, sandboxAttrValue) - _, err := io.WriteString(output, string(iframe)) + _, err := htmlutil.HTMLPrintf(output, ``, src, extraAttrs) return err } @@ -190,7 +236,7 @@ func pipes() (io.ReadCloser, io.WriteCloser, func()) { } } -func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) { +func GetExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, _ bool) { if externalRender, ok := renderer.(ExternalRenderer); ok { return externalRender.GetExternalRendererOptions(), true } @@ -199,17 +245,23 @@ func getExternalRendererOptions(renderer Renderer) (ret ExternalRendererOptions, func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader, output io.Writer) error { var extraHeadHTML template.HTML - if extOpts, ok := getExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe { - if !ctx.RenderOptions.InStandalonePage { + if extOpts, ok := GetExternalRendererOptions(renderer); ok && extOpts.DisplayInIframe { + if ctx.RenderOptions.StandalonePageOptions == nil { // for an external "DisplayInIFrame" render, it could only output its content in a standalone page // otherwise, a `, ret) + + ret = render(ctx, ExternalRendererOptions{ContentSandbox: "allow"}) + assert.Equal(t, ``, ret) +} diff --git a/package/gitea/source/modules/markup/renderer.go b/package/gitea/source/modules/markup/renderer.go index 82e93487..c62c28ad 100644 --- a/package/gitea/source/modules/markup/renderer.go +++ b/package/gitea/source/modules/markup/renderer.go @@ -14,8 +14,8 @@ import ( // Renderer defines an interface for rendering markup file to HTML type Renderer interface { - Name() string // markup format name - Extensions() []string + Name() string // markup format name, also the renderer type, also the external tool name + FileNamePatterns() []string SanitizerRules() []setting.MarkupSanitizerRule Render(ctx *RenderContext, input io.Reader, output io.Writer) error } @@ -43,26 +43,52 @@ type RendererContentDetector interface { } var ( - extRenderers = make(map[string]Renderer) - renderers = make(map[string]Renderer) + fileNameRenderers = make(map[string]Renderer) + renderers = make(map[string]Renderer) ) // RegisterRenderer registers a new markup file renderer func RegisterRenderer(renderer Renderer) { + // TODO: need to handle conflicts renderers[renderer.Name()] = renderer - for _, ext := range renderer.Extensions() { - extRenderers[strings.ToLower(ext)] = renderer +} + +func RefreshFileNamePatterns() { + // TODO: need to handle conflicts + fileNameRenderers = make(map[string]Renderer) + for _, renderer := range renderers { + for _, ext := range renderer.FileNamePatterns() { + fileNameRenderers[strings.ToLower(ext)] = renderer + } } } -// GetRendererByFileName get renderer by filename -func GetRendererByFileName(filename string) Renderer { - extension := strings.ToLower(path.Ext(filename)) - return extRenderers[extension] +func DetectRendererTypeByFilename(filename string) Renderer { + basename := path.Base(strings.ToLower(filename)) + ext1 := path.Ext(basename) + if renderer := fileNameRenderers[basename]; renderer != nil { + return renderer + } + if renderer := fileNameRenderers["*"+ext1]; renderer != nil { + return renderer + } + if basename, ok := strings.CutSuffix(basename, ext1); ok { + ext2 := path.Ext(basename) + if renderer := fileNameRenderers["*"+ext2+ext1]; renderer != nil { + return renderer + } + } + return nil } -// DetectRendererType detects the markup type of the content -func DetectRendererType(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) string { +// DetectRendererTypeByPrefetch detects the markup type of the content +func DetectRendererTypeByPrefetch(filename string, sniffedType typesniffer.SniffedType, prefetchBuf []byte) string { + if filename != "" { + byExt := DetectRendererTypeByFilename(filename) + if byExt != nil { + return byExt.Name() + } + } for _, renderer := range renderers { if detector, ok := renderer.(RendererContentDetector); ok && detector.CanRender(filename, sniffedType, prefetchBuf) { return renderer.Name() @@ -71,18 +97,12 @@ func DetectRendererType(filename string, sniffedType typesniffer.SniffedType, pr return "" } -// DetectMarkupTypeByFileName returns the possible markup format type via the filename -func DetectMarkupTypeByFileName(filename string) string { - if parser := GetRendererByFileName(filename); parser != nil { - return parser.Name() - } - return "" -} - func PreviewableExtensions() []string { - extensions := make([]string, 0, len(extRenderers)) - for extension := range extRenderers { - extensions = append(extensions, extension) + exts := make([]string, 0, len(fileNameRenderers)) + for p := range fileNameRenderers { + if s, ok := strings.CutPrefix(p, "*"); ok { + exts = append(exts, s) + } } - return extensions + return exts } diff --git a/package/gitea/source/modules/markup/sanitizer_default.go b/package/gitea/source/modules/markup/sanitizer_default.go index 7fdf66c4..447cf480 100644 --- a/package/gitea/source/modules/markup/sanitizer_default.go +++ b/package/gitea/source/modules/markup/sanitizer_default.go @@ -56,6 +56,11 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { policy.AllowAttrs("src", "autoplay", "controls").OnElements("video") + // Native support of "" + // ATTENTION: it only works with "auto" theme, because "media" query doesn't work with the theme chosen by end user manually. + // For example: browser's color scheme is "dark", but end user chooses "light" theme. Maybe it needs JS to help to make it work. + policy.AllowAttrs("media", "srcset").OnElements("source") + policy.AllowAttrs("loading").OnElements("img") // Allow generally safe attributes (reference: https://github.com/jch/html-pipeline) @@ -81,11 +86,12 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy { "data-markdown-generated-content", "data-attr-class", } generalSafeElements := []string{ - "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt", + "h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "center", "i", "strong", "em", "a", "pre", "code", "img", "tt", "div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label", "dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary", "details", "caption", "figure", "figcaption", "abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr", + "picture", "source", } // FIXME: Need to handle longdesc in img but there is no easy way to do it policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...) diff --git a/package/gitea/source/modules/markup/sanitizer_default_test.go b/package/gitea/source/modules/markup/sanitizer_default_test.go index e5ba018e..e66f00c0 100644 --- a/package/gitea/source/modules/markup/sanitizer_default_test.go +++ b/package/gitea/source/modules/markup/sanitizer_default_test.go @@ -58,6 +58,9 @@ func TestSanitizer(t *testing.T) { `my custom URL scheme`, `my custom URL scheme`, `my custom URL scheme`, `my custom URL scheme`, + // picture + `c`, `c`, + // Disallow dangerous url schemes `bad`, `bad`, `bad`, `bad`, diff --git a/package/gitea/source/modules/markup/sanitizer_description_test.go b/package/gitea/source/modules/markup/sanitizer_description_test.go index ca72491f..51833414 100644 --- a/package/gitea/source/modules/markup/sanitizer_description_test.go +++ b/package/gitea/source/modules/markup/sanitizer_description_test.go @@ -16,7 +16,7 @@ func TestDescriptionSanitizer(t *testing.T) { `THUMBS UP`, `THUMBS UP`, `Hello World`, `Hello World`, `
    `, ``, - `https://example.com`, `https://example.com`, + `https://example.com`, `https://example.com`, `data`, `data`, `Important!`, `Important!`, `
    Click me! Nothing to see here.
    `, `Click me! Nothing to see here.`, diff --git a/package/gitea/source/modules/migration/file_format.go b/package/gitea/source/modules/migration/file_format.go index e8b6891c..fd6ac45a 100644 --- a/package/gitea/source/modules/migration/file_format.go +++ b/package/gitea/source/modules/migration/file_format.go @@ -12,10 +12,17 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/santhosh-tekuri/jsonschema/v6" "gopkg.in/yaml.v3" ) +// schemaLoader implements jsonschema.URLLoader +type schemaLoader struct{} + +func (l *schemaLoader) Load(url string) (any, error) { + return openSchema(url) +} + // Load project data from file, with optional validation func Load(filename string, data any, validation bool) error { isJSON := strings.HasSuffix(filename, ".json") @@ -43,7 +50,7 @@ func unmarshal(bs []byte, data any, isJSON bool) error { func getSchema(filename string) (*jsonschema.Schema, error) { c := jsonschema.NewCompiler() - c.LoadURL = openSchema + c.UseLoader(&schemaLoader{}) return c.Compile(filename) } diff --git a/package/gitea/source/modules/migration/file_format_test.go b/package/gitea/source/modules/migration/file_format_test.go index da997f64..fd7db907 100644 --- a/package/gitea/source/modules/migration/file_format_test.go +++ b/package/gitea/source/modules/migration/file_format_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/santhosh-tekuri/jsonschema/v5" + "github.com/santhosh-tekuri/jsonschema/v6" "github.com/stretchr/testify/assert" ) diff --git a/package/gitea/source/modules/migration/options.go b/package/gitea/source/modules/migration/options.go index 163aa0cf..0f73c55a 100644 --- a/package/gitea/source/modules/migration/options.go +++ b/package/gitea/source/modules/migration/options.go @@ -40,5 +40,7 @@ type MigrateOptions struct { MirrorInterval string `json:"mirror_interval"` AWSAccessKeyID string - AWSSecretAccessKey string + AWSSecretAccessKey string `json:",omitempty"` + + AWSSecretAccessKeyEncrypted string `json:"aws_secret_access_key_encrypted,omitempty"` } diff --git a/package/gitea/source/modules/migration/schemas_bindata.go b/package/gitea/source/modules/migration/schemas_bindata.go index 695c2c11..2f0e2544 100644 --- a/package/gitea/source/modules/migration/schemas_bindata.go +++ b/package/gitea/source/modules/migration/schemas_bindata.go @@ -8,7 +8,6 @@ package migration import ( - "io" "io/fs" "path" "sync" @@ -16,6 +15,8 @@ import ( _ "embed" "code.gitea.io/gitea/modules/assetfs" + + "github.com/santhosh-tekuri/jsonschema/v6" ) //go:embed bindata.dat @@ -25,6 +26,11 @@ var BuiltinAssets = sync.OnceValue(func() fs.FS { return assetfs.NewEmbeddedFS(bindata) }) -func openSchema(filename string) (io.ReadCloser, error) { - return BuiltinAssets().Open(path.Base(filename)) +func openSchema(filename string) (any, error) { + f, err := BuiltinAssets().Open(path.Base(filename)) + if err != nil { + return nil, err + } + defer f.Close() + return jsonschema.UnmarshalJSON(f) } diff --git a/package/gitea/source/modules/migration/schemas_dynamic.go b/package/gitea/source/modules/migration/schemas_dynamic.go index dca109d6..46db8792 100644 --- a/package/gitea/source/modules/migration/schemas_dynamic.go +++ b/package/gitea/source/modules/migration/schemas_dynamic.go @@ -6,14 +6,15 @@ package migration import ( - "io" "net/url" "os" "path" "path/filepath" + + "github.com/santhosh-tekuri/jsonschema/v6" ) -func openSchema(s string) (io.ReadCloser, error) { +func openSchema(s string) (any, error) { u, err := url.Parse(s) if err != nil { return nil, err @@ -34,5 +35,10 @@ func openSchema(s string) (io.ReadCloser, error) { filename = filepath.Join("modules/migration/schemas", basename) } } - return os.Open(filename) + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return jsonschema.UnmarshalJSON(f) } diff --git a/package/gitea/source/modules/migration/uploader.go b/package/gitea/source/modules/migration/uploader.go index 65752e24..dd34ffb1 100644 --- a/package/gitea/source/modules/migration/uploader.go +++ b/package/gitea/source/modules/migration/uploader.go @@ -14,6 +14,7 @@ type Uploader interface { CreateMilestones(ctx context.Context, milestones ...*Milestone) error CreateReleases(ctx context.Context, releases ...*Release) error SyncTags(ctx context.Context) error + SyncBranches(ctx context.Context) error CreateLabels(ctx context.Context, labels ...*Label) error CreateIssues(ctx context.Context, issues ...*Issue) error CreateComments(ctx context.Context, comments ...*Comment) error diff --git a/package/gitea/source/modules/nosql/manager.go b/package/gitea/source/modules/nosql/manager.go index 375c2b5d..9d3ab49c 100644 --- a/package/gitea/source/modules/nosql/manager.go +++ b/package/gitea/source/modules/nosql/manager.go @@ -20,7 +20,7 @@ var manager *Manager // Manager is the nosql connection manager type Manager struct { ctx context.Context - finished context.CancelFunc + finished process.FinishedFunc mutex sync.Mutex RedisConnections map[string]*redisClientHolder diff --git a/package/gitea/source/modules/optional/option.go b/package/gitea/source/modules/optional/option.go index cbecf869..a278723b 100644 --- a/package/gitea/source/modules/optional/option.go +++ b/package/gitea/source/modules/optional/option.go @@ -67,3 +67,17 @@ func ParseBool(s string) Option[bool] { } return Some(v) } + +func AssignPtrValue[T comparable](changed *bool, target, src *T) { + if src != nil && *src != *target { + *target = *src + *changed = true + } +} + +func AssignPtrString[TO, FROM ~string](changed *bool, target *TO, src *FROM) { + if src != nil && string(*src) != string(*target) { + *target = TO(*src) + *changed = true + } +} diff --git a/package/gitea/source/modules/optional/serialization.go b/package/gitea/source/modules/optional/serialization.go index b120a0ed..345ce562 100644 --- a/package/gitea/source/modules/optional/serialization.go +++ b/package/gitea/source/modules/optional/serialization.go @@ -37,7 +37,7 @@ func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error { func (o Option[T]) MarshalYAML() (any, error) { if !o.Has() { - return nil, nil + return nil, nil //nolint:nilnil // return nil to indicate no value to marshal } value := new(yaml.Node) diff --git a/package/gitea/source/modules/optional/serialization_test.go b/package/gitea/source/modules/optional/serialization_test.go index cf81a94c..c059294b 100644 --- a/package/gitea/source/modules/optional/serialization_test.go +++ b/package/gitea/source/modules/optional/serialization_test.go @@ -15,12 +15,17 @@ import ( ) type testSerializationStruct struct { - NormalString string `json:"normal_string" yaml:"normal_string"` - NormalBool bool `json:"normal_bool" yaml:"normal_bool"` - OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"` - OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"` + NormalString string `json:"normal_string" yaml:"normal_string"` + NormalBool bool `json:"normal_bool" yaml:"normal_bool"` + OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"` + + // It causes an undefined behavior: should the "omitempty" tag only omit "null", or also the empty string? + // The behavior is inconsistent between json and v2 packages, and there is no such use case in Gitea. + // If anyone really needs it, they can use json.MarshalKeepOptionalEmpty to revert the v1 behavior + OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"` + OptTwoBool optional.Option[bool] `json:"optional_two_bool" yaml:"optional_two_bool"` - OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"` + OptTwoString optional.Option[string] `json:"optional_two_string" yaml:"optional_two_string"` } func TestOptionalToJson(t *testing.T) { @@ -32,7 +37,7 @@ func TestOptionalToJson(t *testing.T) { { name: "empty", obj: new(testSerializationStruct), - want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`, + want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_two_string":null}`, }, { name: "some", @@ -44,12 +49,12 @@ func TestOptionalToJson(t *testing.T) { OptTwoBool: optional.None[bool](), OptTwoString: optional.None[string](), }, - want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`, + want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - b, err := json.Marshal(tc.obj) + b, err := json.MarshalKeepOptionalEmpty(tc.obj) assert.NoError(t, err) assert.Equal(t, tc.want, string(b), "gitea json module returned unexpected") @@ -75,7 +80,7 @@ func TestOptionalFromJson(t *testing.T) { }, { name: "some", - data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`, + data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`, want: testSerializationStruct{ NormalString: "a string", NormalBool: true, @@ -169,7 +174,7 @@ normal_bool: true optional_bool: false optional_string: "" optional_two_bool: null -optional_twostring: null +optional_two_string: null `, want: testSerializationStruct{ NormalString: "a string", diff --git a/package/gitea/source/modules/packages/composer/metadata.go b/package/gitea/source/modules/packages/composer/metadata.go index 6035eae8..3aac7058 100644 --- a/package/gitea/source/modules/packages/composer/metadata.go +++ b/package/gitea/source/modules/packages/composer/metadata.go @@ -4,8 +4,13 @@ package composer import ( + "archive/tar" "archive/zip" + "compress/bzip2" + "compress/gzip" + "errors" "io" + "io/fs" "path" "regexp" "strings" @@ -29,8 +34,10 @@ var ( ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") ) -// Package represents a Composer package -type Package struct { +// PackageInfo represents Composer package info +type PackageInfo struct { + Filename string + Name string Version string Type string @@ -44,7 +51,7 @@ type Metadata struct { Description string `json:"description,omitempty"` Readme string `json:"readme,omitempty"` Keywords []string `json:"keywords,omitempty"` - Comments Comments `json:"_comments,omitempty"` + Comments Comments `json:"_comment,omitempty"` Homepage string `json:"homepage,omitempty"` License Licenses `json:"license,omitempty"` Authors []Author `json:"authors,omitempty"` @@ -75,7 +82,7 @@ func (l *Licenses) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &values); err != nil { return err } - *l = Licenses(values) + *l = values } return nil } @@ -97,7 +104,7 @@ func (c *Comments) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &values); err != nil { return err } - *c = Comments(values) + *c = values } return nil } @@ -111,39 +118,121 @@ type Author struct { var nameMatch = regexp.MustCompile(`\A[a-z0-9]([_\.-]?[a-z0-9]+)*/[a-z0-9](([_\.]?|-{0,2})[a-z0-9]+)*\z`) -// ParsePackage parses the metadata of a Composer package file -func ParsePackage(r io.ReaderAt, size int64) (*Package, error) { - archive, err := zip.NewReader(r, size) +type ReadSeekAt interface { + io.Reader + io.ReaderAt + io.Seeker + Size() int64 +} + +func readPackageFileZip(r ReadSeekAt, filename string, limit int) ([]byte, error) { + archive, err := zip.NewReader(r, r.Size()) if err != nil { return nil, err } for _, file := range archive.File { - if strings.Count(file.Name, "/") > 1 { - continue - } - if strings.HasSuffix(strings.ToLower(file.Name), "composer.json") { + filePath := path.Clean(file.Name) + if util.AsciiEqualFold(filePath, filename) { f, err := archive.Open(file.Name) if err != nil { return nil, err } defer f.Close() - return ParseComposerFile(archive, path.Dir(file.Name), f) + return util.ReadWithLimit(f, limit) } } - return nil, ErrMissingComposerFile + return nil, fs.ErrNotExist } -// ParseComposerFile parses a composer.json file to retrieve the metadata of a Composer package -func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Package, error) { +func readPackageFileTar(r io.Reader, filename string, limit int) ([]byte, error) { + tarReader := tar.NewReader(r) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + filePath := path.Clean(header.Name) + if util.AsciiEqualFold(filePath, filename) { + return util.ReadWithLimit(tarReader, limit) + } + } + return nil, fs.ErrNotExist +} + +const ( + pkgExtZip = ".zip" + pkgExtTarGz = ".tar.gz" + pkgExtTarBz2 = ".tar.bz2" +) + +func detectPackageExtName(r ReadSeekAt) (string, error) { + headBytes := make([]byte, 4) + _, err := r.ReadAt(headBytes, 0) + if err != nil { + return "", err + } + _, err = r.Seek(0, io.SeekStart) + if err != nil { + return "", err + } + switch { + case headBytes[0] == 'P' && headBytes[1] == 'K': + return pkgExtZip, nil + case string(headBytes[:3]) == "BZh": + return pkgExtTarBz2, nil + case headBytes[0] == 0x1f && headBytes[1] == 0x8b: + return pkgExtTarGz, nil + } + return "", util.NewInvalidArgumentErrorf("not a valid package file") +} + +func readPackageFile(pkgExt string, r ReadSeekAt, filename string, limit int) ([]byte, error) { + _, err := r.Seek(0, io.SeekStart) + if err != nil { + return nil, err + } + + switch pkgExt { + case pkgExtZip: + return readPackageFileZip(r, filename, limit) + case pkgExtTarBz2: + bzip2Reader := bzip2.NewReader(r) + return readPackageFileTar(bzip2Reader, filename, limit) + case pkgExtTarGz: + gzReader, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + return readPackageFileTar(gzReader, filename, limit) + } + return nil, util.NewInvalidArgumentErrorf("not a valid package file") +} + +// ParsePackage parses the metadata of a Composer package file +func ParsePackage(r ReadSeekAt, optVersion ...string) (*PackageInfo, error) { + pkgExt, err := detectPackageExtName(r) + if err != nil { + return nil, err + } + dataComposerJSON, err := readPackageFile(pkgExt, r, "composer.json", 10*1024*1024) + if errors.Is(err, fs.ErrNotExist) { + return nil, ErrMissingComposerFile + } else if err != nil { + return nil, err + } + var cj struct { Name string `json:"name"` Version string `json:"version"` Type string `json:"type"` Metadata } - if err := json.NewDecoder(r).Decode(&cj); err != nil { + if err := json.Unmarshal(dataComposerJSON, &cj); err != nil { return nil, err } @@ -151,6 +240,9 @@ func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Pa return nil, ErrInvalidName } + if cj.Version == "" { + cj.Version = util.OptionalArg(optVersion) + } if cj.Version != "" { if _, err := version.NewSemver(cj.Version); err != nil { return nil, ErrInvalidVersion @@ -168,17 +260,23 @@ func ParseComposerFile(archive *zip.Reader, pathPrefix string, r io.Reader) (*Pa if cj.Readme == "" { cj.Readme = "README.md" } - f, err := archive.Open(path.Join(pathPrefix, cj.Readme)) - if err == nil { - // 10kb limit for readme content - buf, _ := io.ReadAll(io.LimitReader(f, 10*1024)) - cj.Readme = string(buf) - _ = f.Close() - } else { + dataReadmeMd, _ := readPackageFile(pkgExt, r, cj.Readme, 10*1024) + + // FIXME: legacy problem, the "Readme" field is abused, it should always be the path to the readme file + if len(dataReadmeMd) == 0 { cj.Readme = "" + } else { + cj.Readme = string(dataReadmeMd) } - return &Package{ + // FIXME: legacy format: strings.ToLower(fmt.Sprintf("%s.%s.zip", strings.ReplaceAll(cp.Name, "/", "-"), cp.Version)), doesn't read good + pkgFilename := strings.ReplaceAll(cj.Name, "/", "-") + if cj.Version != "" { + pkgFilename += "." + cj.Version + } + pkgFilename += pkgExt + return &PackageInfo{ + Filename: pkgFilename, Name: cj.Name, Version: cj.Version, Type: cj.Type, diff --git a/package/gitea/source/modules/packages/composer/metadata_test.go b/package/gitea/source/modules/packages/composer/metadata_test.go index a5e317da..4eca4d92 100644 --- a/package/gitea/source/modules/packages/composer/metadata_test.go +++ b/package/gitea/source/modules/packages/composer/metadata_test.go @@ -4,14 +4,19 @@ package composer import ( + "archive/tar" "archive/zip" "bytes" + "compress/gzip" + "io" "strings" "testing" "code.gitea.io/gitea/modules/json" + "github.com/dsnet/compress/bzip2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) const ( @@ -26,8 +31,10 @@ const ( license = "MIT" ) -const composerContent = `{ +func buildComposerContent(version string) string { + return `{ "name": "` + name + `", + "version": "` + version + `", "description": "` + description + `", "type": "` + packageType + `", "license": "` + license + `", @@ -44,8 +51,9 @@ const composerContent = `{ "require": { "php": ">=7.2 || ^8.0" }, - "_comments": "` + comments + `" + "_comment": "` + comments + `" }` +} func TestLicenseUnmarshal(t *testing.T) { var l Licenses @@ -73,16 +81,34 @@ func TestParsePackage(t *testing.T) { archive := zip.NewWriter(&buf) for name, content := range files { w, _ := archive.Create(name) - w.Write([]byte(content)) + _, _ = w.Write([]byte(content)) } - archive.Close() + _ = archive.Close() + return buf.Bytes() + } + + createArchiveTar := func(comp func(io.Writer) io.WriteCloser, files map[string]string) []byte { + var buf bytes.Buffer + w := comp(&buf) + archive := tar.NewWriter(w) + for name, content := range files { + hdr := &tar.Header{ + Name: name, + Mode: 0o600, + Size: int64(len(content)), + } + _ = archive.WriteHeader(hdr) + _, _ = archive.Write([]byte(content)) + } + _ = w.Close() + _ = archive.Close() return buf.Bytes() } t.Run("MissingComposerFile", func(t *testing.T) { data := createArchive(map[string]string{"dummy.txt": ""}) - cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) + cp, err := ParsePackage(bytes.NewReader(data)) assert.Nil(t, cp) assert.ErrorIs(t, err, ErrMissingComposerFile) }) @@ -90,7 +116,7 @@ func TestParsePackage(t *testing.T) { t.Run("MissingComposerFileInRoot", func(t *testing.T) { data := createArchive(map[string]string{"sub/sub/composer.json": ""}) - cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) + cp, err := ParsePackage(bytes.NewReader(data)) assert.Nil(t, cp) assert.ErrorIs(t, err, ErrMissingComposerFile) }) @@ -98,7 +124,7 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidComposerFile", func(t *testing.T) { data := createArchive(map[string]string{"composer.json": ""}) - cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) + cp, err := ParsePackage(bytes.NewReader(data)) assert.Nil(t, cp) assert.Error(t, err) }) @@ -106,7 +132,7 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidPackageName", func(t *testing.T) { data := createArchive(map[string]string{"composer.json": "{}"}) - cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) + cp, err := ParsePackage(bytes.NewReader(data)) assert.Nil(t, cp) assert.ErrorIs(t, err, ErrInvalidName) }) @@ -114,7 +140,7 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidPackageVersion", func(t *testing.T) { data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "version": "1.a.3"}`}) - cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) + cp, err := ParsePackage(bytes.NewReader(data)) assert.Nil(t, cp) assert.ErrorIs(t, err, ErrInvalidVersion) }) @@ -122,22 +148,21 @@ func TestParsePackage(t *testing.T) { t.Run("InvalidReadmePath", func(t *testing.T) { data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "readme": "sub/README.md"}`}) - cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) + cp, err := ParsePackage(bytes.NewReader(data)) assert.NoError(t, err) assert.NotNil(t, cp) assert.Empty(t, cp.Metadata.Readme) }) - t.Run("Valid", func(t *testing.T) { - data := createArchive(map[string]string{"composer.json": composerContent, "README.md": readme}) - - cp, err := ParsePackage(bytes.NewReader(data), int64(len(data))) - assert.NoError(t, err) + assertValidPackage := func(t *testing.T, data []byte, version, filename string) { + cp, err := ParsePackage(bytes.NewReader(data)) + require.NoError(t, err) assert.NotNil(t, cp) + assert.Equal(t, filename, cp.Filename) assert.Equal(t, name, cp.Name) - assert.Empty(t, cp.Version) + assert.Equal(t, version, cp.Version) assert.Equal(t, description, cp.Metadata.Description) assert.Equal(t, readme, cp.Metadata.Readme) assert.Len(t, cp.Metadata.Comments, 1) @@ -149,5 +174,25 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, packageType, cp.Type) assert.Len(t, cp.Metadata.License, 1) assert.Equal(t, license, cp.Metadata.License[0]) + } + + t.Run("ValidZip", func(t *testing.T) { + data := createArchive(map[string]string{"composer.json": buildComposerContent(""), "README.md": readme}) + assertValidPackage(t, data, "", "gitea-composer-package.zip") + }) + + t.Run("ValidTarBz2", func(t *testing.T) { + data := createArchiveTar(func(w io.Writer) io.WriteCloser { + bz2Writer, _ := bzip2.NewWriter(w, nil) + return bz2Writer + }, map[string]string{"composer.json": buildComposerContent("1.0"), "README.md": readme}) + assertValidPackage(t, data, "1.0", "gitea-composer-package.1.0.tar.bz2") + }) + + t.Run("ValidTarGz", func(t *testing.T) { + data := createArchiveTar(func(w io.Writer) io.WriteCloser { + return gzip.NewWriter(w) + }, map[string]string{"composer.json": buildComposerContent(""), "README.md": readme}) + assertValidPackage(t, data, "", "gitea-composer-package.tar.gz") }) } diff --git a/package/gitea/source/modules/packages/container/metadata.go b/package/gitea/source/modules/packages/container/metadata.go index 3ef0684d..d8a48120 100644 --- a/package/gitea/source/modules/packages/container/metadata.go +++ b/package/gitea/source/modules/packages/container/metadata.go @@ -103,7 +103,9 @@ func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) { func parseOCIImageConfig(r io.Reader) (*Metadata, error) { var image oci.Image - if err := json.NewDecoder(r).Decode(&image); err != nil { + // FIXME: JSON-KEY-CASE: here seems a abuse of the case-insensitive decoding feature, spec is case-sensitive + // https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json + if err := json.NewDecoderCaseInsensitive(r).Decode(&image); err != nil { return nil, err } diff --git a/package/gitea/source/modules/packages/container/metadata_test.go b/package/gitea/source/modules/packages/container/metadata_test.go index 0f2d7029..2a6389a8 100644 --- a/package/gitea/source/modules/packages/container/metadata_test.go +++ b/package/gitea/source/modules/packages/container/metadata_test.go @@ -22,6 +22,8 @@ func TestParseImageConfig(t *testing.T) { repositoryURL := "https://gitea.com/gitea" documentationURL := "https://docs.gitea.com" + // FIXME: JSON-KEY-CASE: the test case is not right, the config fields are capitalized in the spec + // https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}` metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI)) diff --git a/package/gitea/source/modules/packages/content_store.go b/package/gitea/source/modules/packages/content_store.go index 57974515..4c61233b 100644 --- a/package/gitea/source/modules/packages/content_store.go +++ b/package/gitea/source/modules/packages/content_store.go @@ -36,8 +36,8 @@ func (s *ContentStore) ShouldServeDirect() bool { return setting.Packages.Storage.ServeDirect() } -func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename, method string, reqParams url.Values) (*url.URL, error) { - return s.store.URL(KeyToRelativePath(key), filename, method, reqParams) +func (s *ContentStore) GetServeDirectURL(key BlobHash256Key, filename, method string, reqParams *storage.ServeDirectOptions) (*url.URL, error) { + return s.store.ServeDirectURL(KeyToRelativePath(key), filename, method, reqParams) } // FIXME: Workaround to be removed in v1.20 diff --git a/package/gitea/source/modules/packages/cran/metadata.go b/package/gitea/source/modules/packages/cran/metadata.go index 0b0bfb07..0856565e 100644 --- a/package/gitea/source/modules/packages/cran/metadata.go +++ b/package/gitea/source/modules/packages/cran/metadata.go @@ -34,7 +34,7 @@ var ( var ( fieldPattern = regexp.MustCompile(`\A\S+:`) namePattern = regexp.MustCompile(`\A[a-zA-Z][a-zA-Z0-9\.]*[a-zA-Z0-9]\z`) - versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+){1,3}\z`) + versionPattern = regexp.MustCompile(`\A[0-9]+(?:[.\-][0-9]+)+\z`) authorReplacePattern = regexp.MustCompile(`[\[\(].+?[\]\)]`) ) diff --git a/package/gitea/source/modules/packages/cran/metadata_test.go b/package/gitea/source/modules/packages/cran/metadata_test.go index ff68c34c..1d652a4a 100644 --- a/package/gitea/source/modules/packages/cran/metadata_test.go +++ b/package/gitea/source/modules/packages/cran/metadata_test.go @@ -128,13 +128,22 @@ func TestParseDescription(t *testing.T) { }) t.Run("InvalidVersion", func(t *testing.T) { - for _, version := range []string{"1", "1 0", "1.2.3.4.5", "1-2-3-4-5", "1.", "1.0.", "1-", "1-0-"} { + for _, version := range []string{"1", "1 0", "1.", "1.0.", "1-", "1-0-"} { p, err := ParseDescription(createDescription(packageName, version)) assert.Nil(t, p) assert.ErrorIs(t, err, ErrInvalidVersion) } }) + t.Run("ValidVersionManyComponents", func(t *testing.T) { + for _, version := range []string{"0.3.4.0.2", "1.2.3.4.5", "1-2-3-4-5"} { + p, err := ParseDescription(createDescription(packageName, version)) + assert.NoError(t, err) + assert.NotNil(t, p) + assert.Equal(t, version, p.Version) + } + }) + t.Run("Valid", func(t *testing.T) { p, err := ParseDescription(createDescription(packageName, packageVersion)) assert.NoError(t, err) diff --git a/package/gitea/source/modules/packages/nuget/metadata.go b/package/gitea/source/modules/packages/nuget/metadata.go index 51246273..ae15e4ec 100644 --- a/package/gitea/source/modules/packages/nuget/metadata.go +++ b/package/gitea/source/modules/packages/nuget/metadata.go @@ -140,7 +140,7 @@ type nuspecPackage struct { func ParsePackageMetaData(r io.ReaderAt, size int64) (*Package, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, err + return nil, util.NewInvalidArgumentErrorf("unable to parse package meta: %v", err) } for _, file := range archive.File { diff --git a/package/gitea/source/modules/packages/nuget/symbol_extractor.go b/package/gitea/source/modules/packages/nuget/symbol_extractor.go index 9c952e1f..5e398151 100644 --- a/package/gitea/source/modules/packages/nuget/symbol_extractor.go +++ b/package/gitea/source/modules/packages/nuget/symbol_extractor.go @@ -42,7 +42,7 @@ func (l PortablePdbList) Close() { func ExtractPortablePdb(r io.ReaderAt, size int64) (PortablePdbList, error) { archive, err := zip.NewReader(r, size) if err != nil { - return nil, err + return nil, util.NewInvalidArgumentErrorf("unable to extract portable pdb: %v", err) } var pdbs PortablePdbList @@ -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/package/gitea/source/modules/packages/rpm/metadata.go b/package/gitea/source/modules/packages/rpm/metadata.go index f4f78c2c..d8ac7ea7 100644 --- a/package/gitea/source/modules/packages/rpm/metadata.go +++ b/package/gitea/source/modules/packages/rpm/metadata.go @@ -46,10 +46,11 @@ type Package struct { } type VersionMetadata struct { - License string `json:"license,omitempty"` - ProjectURL string `json:"project_url,omitempty"` - Summary string `json:"summary,omitempty"` - Description string `json:"description,omitempty"` + License string `json:"license,omitempty"` + ProjectURL string `json:"project_url,omitempty"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + Updates []*Update `json:"updates,omitempty"` } type FileMetadata struct { @@ -296,3 +297,43 @@ func getChangelogs(h *rpmutils.RpmHeader) []*Changelog { } return changelogs } + +type DateAttr struct { + Date string `xml:"date,attr" json:"date"` +} + +type Update struct { + From string `xml:"from,attr" json:"from"` + Status string `xml:"status,attr" json:"status"` + Type string `xml:"type,attr" json:"type"` + Version string `xml:"version,attr" json:"version"` + ID string `xml:"id" json:"id"` + Title string `xml:"title" json:"title"` + Severity string `xml:"severity" json:"severity"` + Description string `xml:"description" json:"description"` + Issued *DateAttr `xml:"issued" json:"issued"` + Updated *DateAttr `xml:"updated" json:"updated"` + References []*Reference `xml:"references>reference" json:"references"` + PkgList []*Collection `xml:"pkglist>collection" json:"pkg_list"` +} + +type Reference struct { + Href string `xml:"href,attr" json:"href"` + ID string `xml:"id,attr" json:"id"` + Title string `xml:"title,attr" json:"title"` + Type string `xml:"type,attr" json:"type"` +} + +type Collection struct { + Short string `xml:"short,attr" json:"short"` + Packages []*UpdatePackage `xml:"package" json:"packages"` +} + +type UpdatePackage struct { + Arch string `xml:"arch,attr" json:"arch"` + Name string `xml:"name,attr" json:"name"` + Release string `xml:"release,attr" json:"release"` + Src string `xml:"src,attr" json:"src"` + Version string `xml:"version,attr" json:"version"` + Filename string `xml:"filename" json:"filename"` +} diff --git a/package/gitea/source/modules/packages/rubygems/metadata.go b/package/gitea/source/modules/packages/rubygems/metadata.go index 8a979486..5c01abc7 100644 --- a/package/gitea/source/modules/packages/rubygems/metadata.go +++ b/package/gitea/source/modules/packages/rubygems/metadata.go @@ -9,6 +9,7 @@ import ( "io" "regexp" "strings" + "sync" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/validation" @@ -25,7 +26,9 @@ var ( ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid") ) -var versionMatcher = regexp.MustCompile(`\A[0-9]+(?:\.[0-9a-zA-Z]+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z`) +var versionMatcher = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(`\A[0-9]+(?:\.[0-9a-zA-Z]+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z`) +}) // Package represents a RubyGems package type Package struct { @@ -128,7 +131,7 @@ func (r requirement) AsVersionRequirement() []VersionRequirement { continue } version, ok := versionInt.(string) - if !ok || version == "0" { + if !ok || (version == "0" && restriction == ">=") { continue } @@ -176,7 +179,7 @@ func parseMetadataFile(r io.Reader) (*Package, error) { return nil, ErrInvalidName } - if !versionMatcher.MatchString(spec.Version.Version) { + if !versionMatcher().MatchString(spec.Version.Version) { return nil, ErrInvalidVersion } diff --git a/package/gitea/source/modules/packages/rubygems/metadata_test.go b/package/gitea/source/modules/packages/rubygems/metadata_test.go index ec2fa08b..9a98c066 100644 --- a/package/gitea/source/modules/packages/rubygems/metadata_test.go +++ b/package/gitea/source/modules/packages/rubygems/metadata_test.go @@ -4,42 +4,30 @@ package rubygems import ( - "archive/tar" - "bytes" - "encoding/base64" - "io" "testing" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) func TestParsePackageMetaData(t *testing.T) { - createArchive := func(filename string, content []byte) io.Reader { - var buf bytes.Buffer - tw := tar.NewWriter(&buf) - hdr := &tar.Header{ - Name: filename, - Mode: 0o600, - Size: int64(len(content)), - } - tw.WriteHeader(hdr) - tw.Write(content) - tw.Close() - return &buf - } - t.Run("MissingMetadataFile", func(t *testing.T) { - data := createArchive("dummy.txt", []byte{0}) - + data := test.WriteTarArchive(map[string]string{"dummy.txt": ""}) rp, err := ParsePackageMetaData(data) assert.ErrorIs(t, err, ErrMissingMetadataFile) assert.Nil(t, rp) }) t.Run("Valid", func(t *testing.T) { - content, _ := base64.StdEncoding.DecodeString("H4sICHC/I2EEAG1ldGFkYXRhAAEeAOH/bmFtZTogZwp2ZXJzaW9uOgogIHZlcnNpb246IDEKWw35Tx4AAAA=") - data := createArchive("metadata.gz", content) - + metadataContent := test.CompressGzip(` +name: g +version: + version: 1 +`) + data := test.WriteTarArchive(map[string]string{ + "metadata.gz": metadataContent.String(), + }) rp, err := ParsePackageMetaData(data) assert.NoError(t, err) assert.NotNil(t, rp) @@ -47,17 +35,86 @@ func TestParsePackageMetaData(t *testing.T) { } func TestParseMetadataFile(t *testing.T) { - content, _ := base64.StdEncoding.DecodeString(`H4sIAMe7I2ECA9VVTW/UMBC9+1eYXvaUbJpSQBZUHJAqDlwK4kCFIseZzZrGH9iTqisEv52Js9nd -0KqggiqRXWnX45n3ZuZ5nCzL+JPQ15ulq7+AQnEORoj3HpReaSVRO8usNCB4qxEku4YQySbuCPo4 -bjHOd07HeZGfMt9JXLlgBB9imOxx7UIULOPnCZMMLsDXXgeiYbW2jQ6C0y9TELBSa6kJ6/IzaySS -R1mUx1nxIitPeFGI9M2L6eGfWAMebANWaUgktzN9M3lsKNmxutBb1AYyCibbNhsDFu+q9GK/Tc4z -d2IcLBl9js5eHaXFsLyvXeNz0LQyL/YoLx8EsiCMBZlx46k6sS2PDD5AgA5kJPNKdhH2elWzOv7n -uv9Q9Aau/6ngP84elvNpXh5oRVlB5/yW7BH0+qu0G4gqaI/JdEHBFBS5l+pKtsARIjIwUnfj8Le0 -+TrdJLl2DG5A9SjrjgZ1mG+4QbAD+G4ZZBUap6qVnnzGf6Rwp+vliBRqtnYGPBEKvkb0USyXE8mS -dVoR6hj07u0HZgAl3SRS8G/fmXcRK20jyq6rDMSYQFgidamqkXbbuspLXE/0k7GphtKqe67GuRC/ -yjAbmt9LsOMp8xMamFkSQ38fP5EFjdz8LA4do2C69VvqWXAJgrPbKZb58/xZXrKoW6ttW13Bhvzi -4ftn7/yUxd4YGcglvTmmY8aGY3ZwRn4CqcWcidUGAAA=`) - rp, err := parseMetadataFile(bytes.NewReader(content)) + content := test.CompressGzip(`--- !ruby/object:Gem::Specification +name: gitea +version: !ruby/object:Gem::Version + version: 1.0.5 +platform: ruby +authors: +- Gitea +autorequire: +bindir: bin +cert_chain: [] +date: 2021-08-23 00:00:00.000000000 Z +dependencies: +- !ruby/object:Gem::Dependency + name: runtime-dep + requirement: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 1.2.0 + - - "<" + - !ruby/object:Gem::Version + version: '2.0' + type: :runtime + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 1.2.0 + - - "<" + - !ruby/object:Gem::Version + version: '2.0' +- !ruby/object:Gem::Dependency + name: dev-dep + requirement: !ruby/object:Gem::Requirement + requirements: + - - "~>" + - !ruby/object:Gem::Version + version: '0' + type: :development + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - "~>" + - !ruby/object:Gem::Version + version: '5.2' +description: RubyGems package test +email: rubygems@gitea.io +executables: [] +extensions: [] +extra_rdoc_files: [] +files: +- lib/gitea.rb +homepage: https://gitea.io/ +licenses: +- MIT +metadata: {} +post_install_message: +rdoc_options: [] +require_paths: +- lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 2.3.0 +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' +requirements: [] +rubyforge_project: +rubygems_version: 2.7.6.2 +signing_key: +specification_version: 4 +summary: Gitea package +test_files: [] +`) + rp, err := parseMetadataFile(content) assert.NoError(t, err) assert.NotNil(t, rp) @@ -84,5 +141,5 @@ yjAbmt9LsOMp8xMamFkSQ38fP5EFjdz8LA4do2C69VvqWXAJgrPbKZb58/xZXrKoW6ttW13Bhvzi assert.Equal(t, "dev-dep", rp.Metadata.DevelopmentDependencies[0].Name) assert.Len(t, rp.Metadata.DevelopmentDependencies[0].Version, 1) assert.Equal(t, "~>", rp.Metadata.DevelopmentDependencies[0].Version[0].Restriction) - assert.Equal(t, "5.2", rp.Metadata.DevelopmentDependencies[0].Version[0].Version) + assert.Equal(t, "0", rp.Metadata.DevelopmentDependencies[0].Version[0].Version) } diff --git a/package/gitea/source/modules/packages/swift/metadata.go b/package/gitea/source/modules/packages/swift/metadata.go index 78925c6e..d0137f8d 100644 --- a/package/gitea/source/modules/packages/swift/metadata.go +++ b/package/gitea/source/modules/packages/swift/metadata.go @@ -47,6 +47,7 @@ type Metadata struct { Keywords []string `json:"keywords,omitempty"` RepositoryURL string `json:"repository_url,omitempty"` License string `json:"license,omitempty"` + LicenseURL string `json:"license_url,omitempty"` Author Person `json:"author"` Manifests map[string]*Manifest `json:"manifests,omitempty"` } @@ -67,7 +68,8 @@ type SoftwareSourceCode struct { Keywords []string `json:"keywords,omitempty"` CodeRepository string `json:"codeRepository,omitempty"` License string `json:"license,omitempty"` - Author Person `json:"author"` + LicenseURL string `json:"licenseURL,omitempty"` + Author *Person `json:"author,omitempty"` ProgrammingLanguage ProgrammingLanguage `json:"programmingLanguage"` RepositoryURLs []string `json:"repositoryURLs,omitempty"` } @@ -181,26 +183,31 @@ func ParsePackage(sr io.ReaderAt, size int64, mr io.Reader) (*Package, error) { if err := json.NewDecoder(mr).Decode(&ssc); err != nil { return nil, err } - p.Metadata.Description = ssc.Description p.Metadata.Keywords = ssc.Keywords p.Metadata.License = ssc.License - author := Person{ - Name: ssc.Author.Name, - GivenName: ssc.Author.GivenName, - MiddleName: ssc.Author.MiddleName, - FamilyName: ssc.Author.FamilyName, + p.Metadata.LicenseURL = ssc.LicenseURL + if ssc.Author != nil { + author := Person{ + Name: ssc.Author.Name, + GivenName: ssc.Author.GivenName, + MiddleName: ssc.Author.MiddleName, + FamilyName: ssc.Author.FamilyName, + } + // If Name is not provided, generate it from individual name components + if author.Name == "" { + author.Name = author.String() + } + p.Metadata.Author = author } - // If Name is not provided, generate it from individual name components - if author.Name == "" { - author.Name = author.String() - } - p.Metadata.Author = author p.Metadata.RepositoryURL = ssc.CodeRepository if !validation.IsValidURL(p.Metadata.RepositoryURL) { p.Metadata.RepositoryURL = "" } + if !validation.IsValidURL(p.Metadata.LicenseURL) { + p.Metadata.LicenseURL = "" + } p.RepositoryURLs = ssc.RepositoryURLs } diff --git a/package/gitea/source/modules/packages/swift/metadata_test.go b/package/gitea/source/modules/packages/swift/metadata_test.go index 461773cb..440bcb9f 100644 --- a/package/gitea/source/modules/packages/swift/metadata_test.go +++ b/package/gitea/source/modules/packages/swift/metadata_test.go @@ -4,11 +4,12 @@ package swift import ( - "archive/zip" "bytes" "strings" "testing" + "code.gitea.io/gitea/modules/test" + "github.com/hashicorp/go-version" "github.com/stretchr/testify/assert" ) @@ -18,36 +19,24 @@ const ( packageVersion = "1.0.1" packageDescription = "Package Description" packageRepositoryURL = "https://gitea.io/gitea/gitea" + packageLicenseURL = "https://opensource.org/license/mit" packageAuthor = "KN4CK3R" packageLicense = "MIT" ) func TestParsePackage(t *testing.T) { - createArchive := func(files map[string][]byte) *bytes.Reader { - var buf bytes.Buffer - zw := zip.NewWriter(&buf) - for filename, content := range files { - w, _ := zw.Create(filename) - w.Write(content) - } - zw.Close() - return bytes.NewReader(buf.Bytes()) - } - t.Run("MissingManifestFile", func(t *testing.T) { - data := createArchive(map[string][]byte{"dummy.txt": {}}) - - p, err := ParsePackage(data, data.Size(), nil) + data := test.WriteZipArchive(map[string]string{"dummy.txt": ""}) + p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil) assert.Nil(t, p) assert.ErrorIs(t, err, ErrMissingManifestFile) }) t.Run("ManifestFileTooLarge", func(t *testing.T) { - data := createArchive(map[string][]byte{ - "Package.swift": make([]byte, maxManifestFileSize+1), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": strings.Repeat("a", maxManifestFileSize+1), }) - - p, err := ParsePackage(data, data.Size(), nil) + p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil) assert.Nil(t, p) assert.ErrorIs(t, err, ErrManifestFileTooLarge) }) @@ -56,12 +45,12 @@ func TestParsePackage(t *testing.T) { content1 := "// swift-tools-version:5.7\n//\n// Package.swift" content2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift" - data := createArchive(map[string][]byte{ - "Package.swift": []byte(content1), - "Package@swift-5.5.swift": []byte(content2), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": content1, + "Package@swift-5.5.swift": content2, }) - p, err := ParsePackage(data, data.Size(), nil) + p, err := ParsePackage(bytes.NewReader(data.Bytes()), int64(data.Len()), nil) assert.NotNil(t, p) assert.NoError(t, err) @@ -77,14 +66,13 @@ func TestParsePackage(t *testing.T) { }) t.Run("WithMetadata", func(t *testing.T) { - data := createArchive(map[string][]byte{ - "Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift", }) p, err := ParsePackage( - data, - data.Size(), - strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`), + bytes.NewReader(data.Bytes()), int64(data.Len()), + strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","keywords":["swift","package"],"license":"`+packageLicense+`","licenseURL":"`+packageLicenseURL+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`), ) assert.NotNil(t, p) assert.NoError(t, err) @@ -97,6 +85,7 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, packageDescription, p.Metadata.Description) assert.ElementsMatch(t, []string{"swift", "package"}, p.Metadata.Keywords) assert.Equal(t, packageLicense, p.Metadata.License) + assert.Equal(t, packageLicenseURL, p.Metadata.LicenseURL) assert.Equal(t, packageAuthor, p.Metadata.Author.Name) assert.Equal(t, packageAuthor, p.Metadata.Author.GivenName) assert.Equal(t, packageRepositoryURL, p.Metadata.RepositoryURL) @@ -104,14 +93,13 @@ func TestParsePackage(t *testing.T) { }) t.Run("WithExplicitNameField", func(t *testing.T) { - data := createArchive(map[string][]byte{ - "Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift", }) authorName := "John Doe" p, err := ParsePackage( - data, - data.Size(), + bytes.NewReader(data.Bytes()), int64(data.Len()), strings.NewReader(`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","author":{"name":"`+authorName+`","givenName":"John","familyName":"Doe"}}`), ) assert.NotNil(t, p) @@ -122,15 +110,30 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, "Doe", p.Metadata.Author.FamilyName) }) + t.Run("WithEmptyJSONMetadata", func(t *testing.T) { + data := test.WriteZipArchive(map[string]string{ + "Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift", + }) + + p, err := ParsePackage( + bytes.NewReader(data.Bytes()), int64(data.Len()), + strings.NewReader(`{}`), + ) + assert.NotNil(t, p) + assert.NoError(t, err) + assert.NotNil(t, p.Metadata) + assert.Empty(t, p.Metadata.Author.Name) + assert.Empty(t, p.RepositoryURLs) + }) + t.Run("NameFieldGeneration", func(t *testing.T) { - data := createArchive(map[string][]byte{ - "Package.swift": []byte("// swift-tools-version:5.7\n//\n// Package.swift"), + data := test.WriteZipArchive(map[string]string{ + "Package.swift": "// swift-tools-version:5.7\n//\n// Package.swift", }) // Test with only individual name components - Name should be auto-generated p, err := ParsePackage( - data, - data.Size(), + bytes.NewReader(data.Bytes()), int64(data.Len()), strings.NewReader(`{"author":{"givenName":"John","middleName":"Q","familyName":"Doe"}}`), ) assert.NotNil(t, p) diff --git a/package/gitea/source/modules/packages/terraform/lock.go b/package/gitea/source/modules/packages/terraform/lock.go new file mode 100644 index 00000000..3c326c04 --- /dev/null +++ b/package/gitea/source/modules/packages/terraform/lock.go @@ -0,0 +1,100 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package terraform + +import ( + "context" + "errors" + "io" + "time" + + "code.gitea.io/gitea/models/db" + packages_model "code.gitea.io/gitea/models/packages" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" + + "xorm.io/builder" +) + +const LockFile = "terraform.lock" + +// LockInfo is the metadata for a terraform lock. +type LockInfo struct { + ID string `json:"ID"` + Operation string `json:"Operation"` + Info string `json:"Info"` + Who string `json:"Who"` + Version string `json:"Version"` + Created time.Time `json:"Created"` + Path string `json:"Path"` +} + +func (l *LockInfo) IsLocked() bool { + return l.ID != "" +} + +func ParseLockInfo(r io.Reader) (*LockInfo, error) { + var lock LockInfo + err := json.NewDecoder(r).Decode(&lock) + if err != nil { + return nil, err + } + // ID is required. Rest is less important. + if lock.ID == "" { + return nil, util.NewInvalidArgumentErrorf("terraform lock is missing an ID") + } + return &lock, nil +} + +// GetLock returns the terraform lock for the given package. +// Lock is empty if no lock exists. +func GetLock(ctx context.Context, packageID int64) (LockInfo, error) { + var lock LockInfo + locks, err := packages_model.GetPropertiesByName(ctx, packages_model.PropertyTypePackage, packageID, LockFile) + if err != nil { + return lock, err + } + if len(locks) == 0 || locks[0].Value == "" { + return lock, nil + } + + err = json.Unmarshal([]byte(locks[0].Value), &lock) + return lock, err +} + +// SetLock sets the terraform lock for the given package. +func SetLock(ctx context.Context, packageID int64, lock *LockInfo) error { + jsonBytes, err := json.Marshal(lock) + if err != nil { + return err + } + + return updateLock(ctx, packageID, string(jsonBytes), builder.Eq{"value": ""}) +} + +// RemoveLock removes the terraform lock for the given package. +func RemoveLock(ctx context.Context, packageID int64) error { + return updateLock(ctx, packageID, "", builder.Neq{"value": ""}) +} + +func updateLock(ctx context.Context, refID int64, value string, cond builder.Cond) error { + pp := packages_model.PackageProperty{RefType: packages_model.PropertyTypePackage, RefID: refID, Name: LockFile} + ok, err := db.GetEngine(ctx).Get(&pp) + if err != nil { + return err + } + if ok { + n, err := db.GetEngine(ctx).Where("ref_type=? AND ref_id=? AND name=?", packages_model.PropertyTypePackage, refID, LockFile).And(cond).Cols("value").Update(&packages_model.PackageProperty{Value: value}) + if err != nil { + return err + } + if n == 0 { + return errors.New("failed to update lock state") + } + + return nil + } + _, err = packages_model.InsertProperty(ctx, packages_model.PropertyTypePackage, refID, LockFile, value) + return err +} diff --git a/package/gitea/source/modules/packages/terraform/state.go b/package/gitea/source/modules/packages/terraform/state.go new file mode 100644 index 00000000..57631286 --- /dev/null +++ b/package/gitea/source/modules/packages/terraform/state.go @@ -0,0 +1,38 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package terraform + +import ( + "io" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/util" +) + +// Note: this is a subset of the Terraform state file format as the full one has two forms. +// If needed, it can be expanded in the future. + +type State struct { + Serial uint64 `json:"serial"` + Lineage string `json:"lineage"` +} + +// ParseState parses the required parts of Terraform state file +func ParseState(r io.Reader) (*State, error) { + var state State + err := json.NewDecoder(r).Decode(&state) + if err != nil { + return nil, err + } + // Serial starts at 1; 0 means it wasn't set in the state file + if state.Serial == 0 { + return nil, util.NewInvalidArgumentErrorf("state serial is missing") + } + // Lineage should always be set + if state.Lineage == "" { + return nil, util.NewInvalidArgumentErrorf("state lineage is missing") + } + + return &state, nil +} diff --git a/package/gitea/source/modules/private/hook.go b/package/gitea/source/modules/private/hook.go index 215996b9..ce87ccd8 100644 --- a/package/gitea/source/modules/private/hook.go +++ b/package/gitea/source/modules/private/hook.go @@ -37,7 +37,7 @@ type HookOptions struct { PushTrigger repository.PushTrigger DeployKeyID int64 // if the pusher is a DeployKey, then UserID is the repo's org user. IsWiki bool - ActionPerm int + ActionsTaskID int64 // if the pusher is an Actions user, the task ID } // SSHLogOption ssh log options diff --git a/package/gitea/source/modules/process/manager.go b/package/gitea/source/modules/process/manager.go index 661511ce..c51d6c76 100644 --- a/package/gitea/source/modules/process/manager.go +++ b/package/gitea/source/modules/process/manager.go @@ -13,6 +13,7 @@ import ( "time" "code.gitea.io/gitea/modules/gtprof" + "code.gitea.io/gitea/modules/util" ) // TODO: This packages still uses a singleton for the Manager. @@ -27,12 +28,14 @@ var ( DefaultContext = context.Background() ) -// IDType is a pid type -type IDType string +type ( + // IDType is a pid type + IDType string -// FinishedFunc is a function that marks that the process is finished and can be removed from the process table -// - it is simply an alias for context.CancelFunc and is only for documentary purposes -type FinishedFunc = context.CancelFunc + CancelCauseFunc func(cause ...error) + // FinishedFunc is a function that marks that the process is finished and can be removed from the process table + FinishedFunc func() +) var ( traceDisabled atomic.Int64 @@ -84,6 +87,10 @@ func GetManager() *Manager { return manager } +func cancelCauseFunc(cancelCause context.CancelCauseFunc) CancelCauseFunc { + return func(cause ...error) { cancelCause(util.OptionalArg(cause)) } +} + // AddContext creates a new context and adds it as a process. Once the process is finished, finished must be called // to remove the process from the process table. It should not be called until the process is finished but must always be called. // @@ -92,11 +99,10 @@ func GetManager() *Manager { // // Most processes will not need to use the cancel function but there will be cases whereby you want to cancel the process but not immediately remove it from the // process table. -func (pm *Manager) AddContext(parent context.Context, description string) (ctx context.Context, cancel context.CancelFunc, finished FinishedFunc) { - ctx, cancel = context.WithCancel(parent) - - ctx, _, finished = pm.Add(ctx, description, cancel, NormalProcessType, true) - +func (pm *Manager) AddContext(parent context.Context, description string) (context.Context, CancelCauseFunc, FinishedFunc) { + ctx, ctxCancel := context.WithCancelCause(parent) + cancel := cancelCauseFunc(ctxCancel) + ctx, _, finished := pm.Add(ctx, description, cancel, NormalProcessType, true) return ctx, cancel, finished } @@ -108,11 +114,10 @@ func (pm *Manager) AddContext(parent context.Context, description string) (ctx c // // Most processes will not need to use the cancel function but there will be cases whereby you want to cancel the process but not immediately remove it from the // process table. -func (pm *Manager) AddTypedContext(parent context.Context, description, processType string, currentlyRunning bool) (ctx context.Context, cancel context.CancelFunc, finished FinishedFunc) { - ctx, cancel = context.WithCancel(parent) - - ctx, _, finished = pm.Add(ctx, description, cancel, processType, currentlyRunning) - +func (pm *Manager) AddTypedContext(parent context.Context, description, processType string, currentlyRunning bool) (context.Context, CancelCauseFunc, FinishedFunc) { + ctx, ctxCancel := context.WithCancelCause(parent) + cancel := cancelCauseFunc(ctxCancel) + ctx, _, finished := pm.Add(ctx, description, cancel, processType, currentlyRunning) return ctx, cancel, finished } @@ -124,21 +129,23 @@ func (pm *Manager) AddTypedContext(parent context.Context, description, processT // // Most processes will not need to use the cancel function but there will be cases whereby you want to cancel the process but not immediately remove it from the // process table. -func (pm *Manager) AddContextTimeout(parent context.Context, timeout time.Duration, description string) (ctx context.Context, cancel context.CancelFunc, finished FinishedFunc) { +func (pm *Manager) AddContextTimeout(parent context.Context, timeout time.Duration, description string) (context.Context, CancelCauseFunc, FinishedFunc) { if timeout <= 0 { // it's meaningless to use timeout <= 0, and it must be a bug! so we must panic here to tell developers to make the timeout correct panic("the timeout must be greater than zero, otherwise the context will be cancelled immediately") } - - ctx, cancel = context.WithTimeout(parent, timeout) - - ctx, _, finished = pm.Add(ctx, description, cancel, NormalProcessType, true) - + ctx, ctxCancelTimeout := context.WithTimeout(parent, timeout) + ctx, ctxCancelCause := context.WithCancelCause(ctx) + cancel := func(cause ...error) { + ctxCancelCause(util.OptionalArg(cause)) + ctxCancelTimeout() + } + ctx, _, finished := pm.Add(ctx, description, cancel, NormalProcessType, true) return ctx, cancel, finished } // Add create a new process -func (pm *Manager) Add(ctx context.Context, description string, cancel context.CancelFunc, processType string, currentlyRunning bool) (context.Context, IDType, FinishedFunc) { +func (pm *Manager) Add(ctx context.Context, description string, cancel CancelCauseFunc, processType string, currentlyRunning bool) (context.Context, IDType, FinishedFunc) { parentPID := GetParentPID(ctx) pm.mutex.Lock() diff --git a/package/gitea/source/modules/process/process.go b/package/gitea/source/modules/process/process.go index 06a28c4a..d81f5ffa 100644 --- a/package/gitea/source/modules/process/process.go +++ b/package/gitea/source/modules/process/process.go @@ -4,7 +4,6 @@ package process import ( - "context" "time" ) @@ -21,7 +20,7 @@ type process struct { ParentPID IDType Description string Start time.Time - Cancel context.CancelFunc + Cancel CancelCauseFunc Type string } diff --git a/package/gitea/source/modules/public/manifest.go b/package/gitea/source/modules/public/manifest.go new file mode 100644 index 00000000..f807244c --- /dev/null +++ b/package/gitea/source/modules/public/manifest.go @@ -0,0 +1,164 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "io" + "path" + "sync" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +type manifestEntry struct { + File string `json:"file"` + Name string `json:"name"` + IsEntry bool `json:"isEntry"` + CSS []string `json:"css"` +} + +type manifestDataStruct struct { + paths map[string]string // unhashed path -> hashed path + names map[string]string // hashed path -> entry name + modTime int64 + checkTime time.Time +} + +var ( + manifestData atomic.Pointer[manifestDataStruct] + manifestFS = sync.OnceValue(AssetFS) +) + +const manifestPath = "assets/.vite/manifest.json" + +func parseManifest(data []byte) (map[string]string, map[string]string) { + var manifest map[string]manifestEntry + if err := json.Unmarshal(data, &manifest); err != nil { + log.Error("Failed to parse frontend manifest: %v", err) + return nil, nil + } + + paths := make(map[string]string) + names := make(map[string]string) + for _, entry := range manifest { + if !entry.IsEntry || entry.Name == "" { + continue + } + // Build unhashed key from file path: "js/index.js", "css/theme-gitea-dark.css" + dir := path.Dir(entry.File) + ext := path.Ext(entry.File) + key := dir + "/" + entry.Name + ext + paths[key] = entry.File + names[entry.File] = entry.Name + // Map associated CSS files, e.g. "css/index.css" -> "css/index.B3zrQPqD.css" + // FIXME: INCORRECT-VITE-MANIFEST-PARSER: the logic is wrong, Vite manifest doesn't work this way + // It just happens to be correct for the current modules dependencies + for _, css := range entry.CSS { + cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css) + paths[cssKey] = css + names[css] = entry.Name + } + } + return paths, names +} + +func reloadManifest(existingData *manifestDataStruct) *manifestDataStruct { + now := time.Now() + data := existingData + if data != nil && now.Sub(data.checkTime) < time.Second { + // a single request triggers multiple calls to getHashedPath + // do not check the manifest file too frequently + return data + } + + f, err := manifestFS().Open(manifestPath) + if err != nil { + log.Error("Failed to open frontend manifest: %v", err) + return data + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + log.Error("Failed to stat frontend manifest: %v", err) + return data + } + + needReload := data == nil || fi.ModTime().UnixNano() != data.modTime + if !needReload { + return data + } + manifestContent, err := io.ReadAll(f) + if err != nil { + log.Error("Failed to read frontend manifest: %v", err) + return data + } + return storeManifestFromBytes(manifestContent, fi.ModTime().UnixNano(), now) +} + +func storeManifestFromBytes(manifestContent []byte, modTime int64, checkTime time.Time) *manifestDataStruct { + paths, names := parseManifest(manifestContent) + data := &manifestDataStruct{ + paths: paths, + names: names, + modTime: modTime, + checkTime: checkTime, + } + manifestData.Store(data) + return data +} + +func getManifestData() *manifestDataStruct { + data := manifestData.Load() + + // In production the manifest is immutable (embedded in the binary). + // In dev mode, check if it changed on disk (for watch-frontend). + if data == nil || !setting.IsProd { + data = reloadManifest(data) + } + if data == nil { + data = &manifestDataStruct{} + } + return data +} + +// AssetURI returns the URI for a frontend asset. +// It may return a relative path or a full URL depending on the StaticURLPrefix setting. +// In Vite dev mode, known entry points are mapped to their source paths +// so the reverse proxy serves them from the Vite dev server. +// In production, it resolves the content-hashed path from the manifest. +func AssetURI(originPath string) string { + if IsViteDevMode() { + if src := viteDevSourceURL(originPath); src != "" { + return src + } + // it should be caused by incorrect vite config + setting.PanicInDevOrTesting("Failed to locate local path for managed asset URI: %s", originPath) + } + + // Try to resolve an unhashed asset path (origin path) to its content-hashed path from the frontend manifest. + // Example: "js/index.js" -> "js/index.C6Z2MRVQ.js" + data := getManifestData() + assetPath := data.paths[originPath] + if assetPath == "" { + // it should be caused by either: "incorrect vite config" or "user's custom theme" + assetPath = originPath + if !setting.IsProd { + log.Warn("Failed to find managed asset URI for origin path: %s", originPath) + } + } + + return setting.StaticURLPrefix + "/assets/" + assetPath +} + +// AssetNameFromHashedPath returns the asset entry name for a given hashed asset path. +// Example: returns "theme-gitea-dark" for "css/theme-gitea-dark.CyAaQnn5.css". +// Returns empty string if the path is not found in the manifest. +func AssetNameFromHashedPath(hashedPath string) string { + return getManifestData().names[hashedPath] +} diff --git a/package/gitea/source/modules/public/manifest_test.go b/package/gitea/source/modules/public/manifest_test.go new file mode 100644 index 00000000..acfeaa6d --- /dev/null +++ b/package/gitea/source/modules/public/manifest_test.go @@ -0,0 +1,80 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "testing" + "time" + + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestViteManifest(t *testing.T) { + defer test.MockVariableValue(&setting.IsProd, true)() + + const testManifest = `{ + "web_src/js/index.ts": { + "file": "js/index.C6Z2MRVQ.js", + "name": "index", + "src": "web_src/js/index.ts", + "isEntry": true, + "css": ["css/index.B3zrQPqD.css"] + }, + "web_src/css/themes/theme-gitea-dark.css": { + "file": "css/theme-gitea-dark.CyAaQnn5.css", + "name": "theme-gitea-dark", + "src": "web_src/css/themes/theme-gitea-dark.css", + "isEntry": true + }, + "web_src/js/features/eventsource.sharedworker.ts": { + "file": "js/eventsource.sharedworker.Dug1twio.js", + "name": "eventsource.sharedworker", + "src": "web_src/js/features/eventsource.sharedworker.ts", + "isEntry": true + }, + "_chunk.js": { + "file": "js/chunk.abc123.js", + "name": "chunk" + } +}` + + t.Run("EmptyManifest", func(t *testing.T) { + storeManifestFromBytes([]byte(``), 0, time.Now()) + assert.Equal(t, "/assets/js/index.js", AssetURI("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.css", AssetURI("css/theme-gitea-dark.css")) + assert.Equal(t, "", AssetNameFromHashedPath("css/no-such-file.css")) + }) + + t.Run("ParseManifest", func(t *testing.T) { + storeManifestFromBytes([]byte(testManifest), 0, time.Now()) + paths, names := manifestData.Load().paths, manifestData.Load().names + + // JS entries + assert.Equal(t, "js/index.C6Z2MRVQ.js", paths["js/index.js"]) + assert.Equal(t, "js/eventsource.sharedworker.Dug1twio.js", paths["js/eventsource.sharedworker.js"]) + + // Associated CSS from JS entries + assert.Equal(t, "css/index.B3zrQPqD.css", paths["css/index.css"]) + + // CSS-only entries + assert.Equal(t, "css/theme-gitea-dark.CyAaQnn5.css", paths["css/theme-gitea-dark.css"]) + + // Non-entry chunks should not be included + assert.Empty(t, paths["js/chunk.js"]) + + // Names: hashed path -> entry name + assert.Equal(t, "index", names["js/index.C6Z2MRVQ.js"]) + assert.Equal(t, "index", names["css/index.B3zrQPqD.css"]) + assert.Equal(t, "theme-gitea-dark", names["css/theme-gitea-dark.CyAaQnn5.css"]) + assert.Equal(t, "eventsource.sharedworker", names["js/eventsource.sharedworker.Dug1twio.js"]) + + // Test Asset related functions + assert.Equal(t, "/assets/js/index.C6Z2MRVQ.js", AssetURI("js/index.js")) + assert.Equal(t, "/assets/css/theme-gitea-dark.CyAaQnn5.css", AssetURI("css/theme-gitea-dark.css")) + assert.Equal(t, "theme-gitea-dark", AssetNameFromHashedPath("css/theme-gitea-dark.CyAaQnn5.css")) + }) +} diff --git a/package/gitea/source/modules/public/mime_types.go b/package/gitea/source/modules/public/mime_types.go index 32bdf3bf..fa4691c6 100644 --- a/package/gitea/source/modules/public/mime_types.go +++ b/package/gitea/source/modules/public/mime_types.go @@ -3,38 +3,48 @@ package public -import "strings" +import ( + "mime" + "strings" + "sync" +) -// wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`, see the comment of detectWellKnownMimeType -var wellKnownMimeTypesLower = map[string]string{ - ".avif": "image/avif", - ".css": "text/css; charset=utf-8", - ".gif": "image/gif", - ".htm": "text/html; charset=utf-8", - ".html": "text/html; charset=utf-8", - ".jpeg": "image/jpeg", - ".jpg": "image/jpeg", - ".js": "text/javascript; charset=utf-8", - ".json": "application/json", - ".mjs": "text/javascript; charset=utf-8", - ".pdf": "application/pdf", - ".png": "image/png", - ".svg": "image/svg+xml", - ".wasm": "application/wasm", - ".webp": "image/webp", - ".xml": "text/xml; charset=utf-8", +// wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`, +// see the comment of DetectWellKnownMimeType +var wellKnownMimeTypesLower = sync.OnceValue(func() map[string]string { + return map[string]string{ + ".avif": "image/avif", + ".css": "text/css; charset=utf-8", + ".gif": "image/gif", + ".htm": "text/html; charset=utf-8", + ".html": "text/html; charset=utf-8", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json", + ".mjs": "text/javascript; charset=utf-8", + ".pdf": "application/pdf", + ".png": "image/png", + ".svg": "image/svg+xml", + ".wasm": "application/wasm", + ".webp": "image/webp", + ".xml": "text/xml; charset=utf-8", - // well, there are some types missing from the builtin list - ".txt": "text/plain; charset=utf-8", -} + // well, there are some types missing from the builtin list + ".txt": "text/plain; charset=utf-8", + } +}) -// detectWellKnownMimeType will return the mime-type for a well-known file ext name +// DetectWellKnownMimeType will return the mime-type for a well-known file ext name // The purpose of this function is to bypass the unstable behavior of Golang's mime.TypeByExtension // mime.TypeByExtension would use OS's mime-type config to overwrite the well-known types (see its document). // If the user's OS has incorrect mime-type config, it would make Gitea can not respond a correct Content-Type to browsers. // For example, if Gitea returns `text/plain` for a `.js` file, the browser couldn't run the JS due to security reasons. -// detectWellKnownMimeType makes the Content-Type for well-known files stable. -func detectWellKnownMimeType(ext string) string { +// DetectWellKnownMimeType makes the Content-Type for well-known files stable. +func DetectWellKnownMimeType(ext string) string { ext = strings.ToLower(ext) - return wellKnownMimeTypesLower[ext] + if s, ok := wellKnownMimeTypesLower()[ext]; ok { + return s + } + return mime.TypeByExtension(ext) } diff --git a/package/gitea/source/modules/public/public.go b/package/gitea/source/modules/public/public.go index a7eace15..bb4721a4 100644 --- a/package/gitea/source/modules/public/public.go +++ b/package/gitea/source/modules/public/public.go @@ -18,6 +18,8 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + + "github.com/go-chi/cors" ) func CustomAssets() *assetfs.Layer { @@ -28,6 +30,15 @@ func AssetFS() *assetfs.LayeredFS { return assetfs.Layered(CustomAssets(), BuiltinAssets()) } +func AssetsCors() func(next http.Handler) http.Handler { + // static assets need to be served for external renders (sandboxed) + return cors.Handler(cors.Options{ + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{"HEAD", "GET"}, + MaxAge: 3600 * 24, + }) +} + // FileHandlerFunc implements the static handler for serving files in "public" assets func FileHandlerFunc() http.HandlerFunc { assetFS := AssetFS() @@ -36,7 +47,7 @@ func FileHandlerFunc() http.HandlerFunc { resp.WriteHeader(http.StatusMethodNotAllowed) return } - handleRequest(resp, req, assetFS, req.URL.Path) + handleRequest(resp, req, http.FS(assetFS), req.URL.Path) } } @@ -51,9 +62,9 @@ func parseAcceptEncoding(val string) container.Set[string] { } // setWellKnownContentType will set the Content-Type if the file is a well-known type. -// See the comments of detectWellKnownMimeType +// See the comments of DetectWellKnownMimeType func setWellKnownContentType(w http.ResponseWriter, file string) { - mimeType := detectWellKnownMimeType(path.Ext(file)) + mimeType := DetectWellKnownMimeType(path.Ext(file)) if mimeType != "" { w.Header().Set("Content-Type", mimeType) } diff --git a/package/gitea/source/modules/public/vitedev.go b/package/gitea/source/modules/public/vitedev.go new file mode 100644 index 00000000..7cfe6923 --- /dev/null +++ b/package/gitea/source/modules/public/vitedev.go @@ -0,0 +1,204 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package public + +import ( + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "strings" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/web/routing" +) + +const viteDevPortFile = "public/assets/.vite/dev-port" + +var viteDevProxy atomic.Pointer[httputil.ReverseProxy] + +func getViteDevServerBaseURL() string { + portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile) + portContent, _ := os.ReadFile(portFile) + port := strings.TrimSpace(string(portContent)) + if port == "" { + return "" + } + return "http://localhost:" + port +} + +func getViteDevProxy() *httputil.ReverseProxy { + if proxy := viteDevProxy.Load(); proxy != nil { + return proxy + } + + viteDevServerBaseURL := getViteDevServerBaseURL() + if viteDevServerBaseURL == "" { + return nil + } + + target, err := url.Parse(viteDevServerBaseURL) + if err != nil { + log.Error("Failed to parse Vite dev server base URL %s, err: %v", viteDevServerBaseURL, err) + return nil + } + + // there is a strange error log (from Golang's HTTP package) + // 2026/03/28 19:50:13 modules/log/misc.go:72:(*loggerToWriter).Write() [I] Unsolicited response received on idle HTTP channel starting with "HTTP/1.1 400 Bad Request\r\n\r\n"; err= + // maybe it is caused by that the Vite dev server doesn't support keep-alive connections? or different keep-alive timeouts? + transport := &http.Transport{ + IdleConnTimeout: 5 * time.Second, + ResponseHeaderTimeout: 5 * time.Second, + } + log.Info("Proxying Vite dev server requests to %s", target) + proxy := &httputil.ReverseProxy{ + Transport: transport, + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(target) + r.Out.Host = target.Host + }, + ModifyResponse: func(resp *http.Response) error { + // add a header to indicate the Vite dev server port, + // make developers know that this request is proxied to Vite dev server and which port it is + resp.Header.Add("X-Gitea-Vite-Dev-Server", viteDevServerBaseURL) + return nil + }, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + if r.Context().Err() != nil { + return // request cancelled (e.g. client disconnected), silently ignore + } + log.Error("Error proxying to Vite dev server: %v", err) + http.Error(w, "Error proxying to Vite dev server: "+err.Error(), http.StatusBadGateway) + }, + } + viteDevProxy.Store(proxy) + return proxy +} + +// ViteDevMiddleware proxies matching requests to the Vite dev server. +// It is registered as middleware in non-production mode and lazily discovers +// the Vite dev server port from the port file written by the viteDevServerPortPlugin. +// It is needed because there are container-based development, only Gitea web server's port is exposed. +func ViteDevMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { + if !isViteDevRequest(req) { + next.ServeHTTP(resp, req) + return + } + proxy := getViteDevProxy() + if proxy == nil { + next.ServeHTTP(resp, req) + return + } + routing.MarkLongPolling(resp, req) + proxy.ServeHTTP(resp, req) + }) +} + +var viteDevModeCheck atomic.Pointer[struct { + isDev bool + time time.Time +}] + +// IsViteDevMode returns true if the Vite dev server port file exists and the server is alive +func IsViteDevMode() bool { + if setting.IsProd { + return false + } + + now := time.Now() + lastCheck := viteDevModeCheck.Load() + if lastCheck != nil && time.Now().Sub(lastCheck.time) < time.Second { + return lastCheck.isDev + } + + viteDevServerBaseURL := getViteDevServerBaseURL() + if viteDevServerBaseURL == "" { + return false + } + + req := httplib.NewRequest(viteDevServerBaseURL+"/web_src/js/__vite_dev_server_check", "GET") + resp, _ := req.Response() + if resp != nil { + _ = resp.Body.Close() + } + isDev := resp != nil && resp.StatusCode == http.StatusOK + viteDevModeCheck.Store(&struct { + isDev bool + time time.Time + }{ + isDev: isDev, + time: now, + }) + return isDev +} + +func detectWebSrcPath(webSrcPath string) string { + localPath := util.FilePathJoinAbs(setting.StaticRootPath, "web_src", webSrcPath) + if _, err := os.Stat(localPath); err == nil { + return setting.AppSubURL + "/web_src/" + webSrcPath + } + return "" +} + +func viteDevSourceURL(name string) string { + if strings.HasPrefix(name, "css/theme-") { + // Only redirect built-in themes to Vite source; custom themes are served from custom/public/assets/css/ + themeFilePath := "css/themes/" + strings.TrimPrefix(name, "css/") + if srcPath := detectWebSrcPath(themeFilePath); srcPath != "" { + return srcPath + } + } + // try to map ".js" files to ".ts" files + pathPrefix, ok := strings.CutSuffix(name, ".js") + if ok { + if srcPath := detectWebSrcPath(pathPrefix + ".ts"); srcPath != "" { + return srcPath + } + } + // for all others that the names match + return detectWebSrcPath(name) +} + +// isViteDevRequest returns true if the request should be proxied to the Vite dev server. +// Ref: Vite source packages/vite/src/node/constants.ts and packages/vite/src/shared/constants.ts +func isViteDevRequest(req *http.Request) bool { + if req.Header.Get("Upgrade") == "websocket" { + wsProtocol := req.Header.Get("Sec-WebSocket-Protocol") + return wsProtocol == "vite-hmr" || wsProtocol == "vite-ping" + } + path := req.URL.Path + + // vite internal requests + if strings.HasPrefix(path, "/@vite/") /* HMR client */ || + strings.HasPrefix(path, "/@fs/") /* out-of-root file access, see vite.config.ts: fs.allow */ || + strings.HasPrefix(path, "/@id/") /* virtual modules */ { + return true + } + + // local source requests (VITE-DEV-SERVER-SECURITY: don't serve sensitive files outside the allowed paths) + if strings.HasPrefix(path, "/node_modules/") || + strings.HasPrefix(path, "/public/assets/") || + strings.HasPrefix(path, "/web_src/") { + return true + } + + // Vite uses a path relative to project root and adds "?import" to non-JS/CSS asset imports: + // - {WebSite}/public/assets/... (e.g. SVG icons from "{RepoRoot}/public/assets/img/svg/") + // - {WebSite}/assets/emoji.json: it is an exception for the frontend assets, it is imported by JS code, but: + // - KEEP IN MIND: all static frontend assets are served from "{AssetFS}/assets" to "{WebSite}/assets" by Gitea Web Server + // - "{AssetFS}" is a layered filesystem from "{RepoRoot}/public" or embedded assets, and user's custom files in "{CustomPath}/public" + // - "{RepoRoot}/assets/emoji.json" just happens to have the dir name "assets", it is not related to frontend assets + // - BAD DESIGN: indeed it is a "conflicted and polluted name" sample + if path == "/assets/emoji.json" { + return true + } + return false +} diff --git a/package/gitea/source/modules/queue/workerqueue.go b/package/gitea/source/modules/queue/workerqueue.go index 0f5b1055..d8b0722c 100644 --- a/package/gitea/source/modules/queue/workerqueue.go +++ b/package/gitea/source/modules/queue/workerqueue.go @@ -21,7 +21,7 @@ import ( // It can use different underlying (base) queue types type WorkerPoolQueue[T any] struct { ctxRun context.Context - ctxRunCancel context.CancelFunc + ctxRunCancel process.FinishedFunc shutdownDone chan struct{} shutdownTimeout atomic.Int64 // in case some buggy handlers (workers) would hang forever, "shutdown" should finish in predictable time diff --git a/package/gitea/source/modules/recaptcha/recaptcha.go b/package/gitea/source/modules/recaptcha/recaptcha.go index 1777d169..224fa38e 100644 --- a/package/gitea/source/modules/recaptcha/recaptcha.go +++ b/package/gitea/source/modules/recaptcha/recaptcha.go @@ -13,7 +13,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" ) // Response is the structure of JSON returned from API @@ -24,17 +23,16 @@ type Response struct { ErrorCodes []ErrorCode `json:"error-codes"` } -const apiURL = "api/siteverify" - // Verify calls Google Recaptcha API to verify token func Verify(ctx context.Context, response string) (bool, error) { post := url.Values{ "secret": {setting.Service.RecaptchaSecret}, "response": {response}, } + + reqURL := strings.TrimSuffix(setting.Service.RecaptchaURL, "/") + "/api/siteverify" // Basically a copy of http.PostForm, but with a context - req, err := http.NewRequestWithContext(ctx, http.MethodPost, - util.URLJoin(setting.Service.RecaptchaURL, apiURL), strings.NewReader(post.Encode())) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, strings.NewReader(post.Encode())) if err != nil { return false, fmt.Errorf("Failed to create CAPTCHA request: %w", err) } diff --git a/package/gitea/source/modules/references/references.go b/package/gitea/source/modules/references/references.go index 592bd4cb..ef3568eb 100644 --- a/package/gitea/source/modules/references/references.go +++ b/package/gitea/source/modules/references/references.go @@ -248,7 +248,7 @@ func FindAllIssueReferencesMarkdown(content string) []IssueReference { func findAllIssueReferencesMarkdown(content string) []*rawReference { bcontent, links := mdstripper.StripMarkdownBytes([]byte(content)) - return findAllIssueReferencesBytes(bcontent, links) + return findAllIssueReferencesBytes(bcontent, links, []byte(content)) } func convertFullHTMLReferencesToShortRefs(re *regexp.Regexp, contentBytes *[]byte) { @@ -326,7 +326,7 @@ func FindAllIssueReferences(content string) []IssueReference { } else { log.Debug("No GiteaIssuePullPattern pattern") } - return rawToIssueReferenceList(findAllIssueReferencesBytes(contentBytes, []string{})) + return rawToIssueReferenceList(findAllIssueReferencesBytes(contentBytes, []string{}, nil)) } // FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string. @@ -406,7 +406,8 @@ func FindRenderizableReferenceAlphanumeric(content string) *RenderizableReferenc } // FindAllIssueReferencesBytes returns a list of unvalidated references found in a byte slice. -func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference { +// originalContent is optional and used to detect closing/reopening keywords for URL references. +func findAllIssueReferencesBytes(content []byte, links []string, originalContent []byte) []*rawReference { ret := make([]*rawReference, 0, 10) pos := 0 @@ -470,10 +471,27 @@ func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference default: continue } - // Note: closing/reopening keywords not supported with URLs - bytes := []byte(parts[1] + "/" + parts[2] + sep + parts[4]) - if ref := getCrossReference(bytes, 0, len(bytes), true, false); ref != nil { + refBytes := []byte(parts[1] + "/" + parts[2] + sep + parts[4]) + if ref := getCrossReference(refBytes, 0, len(refBytes), true, false); ref != nil { ref.refLocation = nil + // Detect closing/reopening keywords by finding the URL position in original content + if originalContent != nil { + if idx := bytes.Index(originalContent, []byte(link)); idx > 0 { + // For markdown links [text](url), find the opening bracket before the URL + // to properly detect keywords like "closes [text](url)" + searchStart := idx + if idx >= 2 && originalContent[idx-1] == '(' { + // Find the matching '[' for this markdown link + bracketIdx := bytes.LastIndex(originalContent[:idx-1], []byte{'['}) + if bracketIdx >= 0 { + searchStart = bracketIdx + } + } + action, location := findActionKeywords(originalContent, searchStart) + ref.action = action + ref.actionLocation = location + } + } ret = append(ret, ref) } } diff --git a/package/gitea/source/modules/references/references_test.go b/package/gitea/source/modules/references/references_test.go index a15ae99f..5922a9f5 100644 --- a/package/gitea/source/modules/references/references_test.go +++ b/package/gitea/source/modules/references/references_test.go @@ -227,6 +227,62 @@ func TestFindAllIssueReferences(t *testing.T) { testFixtures(t, fixtures, "default") + // Test closing/reopening keywords with URLs (issue #27549) + // Uses the same AppURL as testFixtures (https://gitea.com:3000/) + urlFixtures := []testFixture{ + { + "Closes [this issue](https://gitea.com:3000/user/repo/issues/123)", + []testResult{ + {123, "user", "repo", "123", false, XRefActionCloses, nil, &RefSpan{Start: 0, End: 6}, ""}, + }, + }, + { + "This fixes [#456](https://gitea.com:3000/org/project/issues/456)", + []testResult{ + {456, "org", "project", "456", false, XRefActionCloses, nil, &RefSpan{Start: 5, End: 10}, ""}, + }, + }, + { + "Reopens [PR](https://gitea.com:3000/owner/repo/pulls/789)", + []testResult{ + {789, "owner", "repo", "789", true, XRefActionReopens, nil, &RefSpan{Start: 0, End: 7}, ""}, + }, + }, + { + "See [issue](https://gitea.com:3000/user/repo/issues/100) but closes [another](https://gitea.com:3000/user/repo/issues/200)", + []testResult{ + {100, "user", "repo", "100", false, XRefActionNone, nil, nil, ""}, + {200, "user", "repo", "200", false, XRefActionCloses, nil, &RefSpan{Start: 61, End: 67}, ""}, + }, + }, + } + + testFixtures(t, urlFixtures, "url-keywords") + + // Test bare URLs (not markdown links) with closing keywords + // These use FindAllIssueReferences (non-markdown) which converts full URLs to short refs first + setting.AppURL = "https://gitea.com:3000/" + bareURLTests := []struct { + name string + input string + expected XRefAction + }{ + {"Fixes bare URL", "Fixes https://gitea.com:3000/org/project/issues/456", XRefActionCloses}, + {"Fixes with colon", "Fixes: https://gitea.com:3000/org/project/issues/456", XRefActionCloses}, + {"Closes bare URL", "Closes https://gitea.com:3000/user/repo/issues/123", XRefActionCloses}, + {"Closes with colon", "Closes: https://gitea.com:3000/user/repo/issues/123", XRefActionCloses}, + } + + for _, tt := range bareURLTests { + t.Run(tt.name, func(t *testing.T) { + refs := FindAllIssueReferences(tt.input) + assert.Len(t, refs, 1, "Expected 1 reference for: %s", tt.input) + if len(refs) > 0 { + assert.Equal(t, tt.expected, refs[0].Action, "Expected action %v for: %s", tt.expected, tt.input) + } + }) + } + type alnumFixture struct { input string issue string diff --git a/package/gitea/source/modules/repository/branch.go b/package/gitea/source/modules/repository/branch.go index 30aa0a6e..0a8f7cc4 100644 --- a/package/gitea/source/modules/repository/branch.go +++ b/package/gitea/source/modules/repository/branch.go @@ -17,6 +17,13 @@ import ( "code.gitea.io/gitea/modules/timeutil" ) +// SyncResult describes a reference update detected during sync. +type SyncResult struct { + RefName git.RefName + OldCommitID string + NewCommitID string +} + // SyncRepoBranches synchronizes branch table with repository branches func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) { repo, err := repo_model.GetRepositoryByID(ctx, repoID) @@ -33,18 +40,19 @@ func SyncRepoBranches(ctx context.Context, repoID, doerID int64) (int64, error) } defer gitRepo.Close() - return SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doerID) + count, _, err := SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doerID) + return count, err } -func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, error) { +func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doerID int64) (int64, []*SyncResult, error) { objFmt, err := gitRepo.GetObjectFormat() if err != nil { - return 0, fmt.Errorf("GetObjectFormat: %w", err) + return 0, nil, fmt.Errorf("GetObjectFormat: %w", err) } if objFmt.Name() != repo.ObjectFormatName { repo.ObjectFormatName = objFmt.Name() if err = repo_model.UpdateRepositoryColsWithAutoTime(ctx, repo, "object_format_name"); err != nil { - return 0, fmt.Errorf("UpdateRepositoryColsWithAutoTime: %w", err) + return 0, nil, fmt.Errorf("UpdateRepositoryColsWithAutoTime: %w", err) } } @@ -52,7 +60,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, { branches, _, err := gitRepo.GetBranchNames(0, 0) if err != nil { - return 0, err + return 0, nil, err } log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches) for _, branch := range branches { @@ -67,7 +75,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, RepoID: repo.ID, }) if err != nil { - return 0, err + return 0, nil, err } for _, branch := range branches { dbBranches[branch.Name] = branch @@ -77,11 +85,12 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, var toAdd []*git_model.Branch var toUpdate []*git_model.Branch var toRemove []int64 + var syncResults []*SyncResult for branch := range allBranches { dbb := dbBranches[branch] commit, err := gitRepo.GetBranchCommit(branch) if err != nil { - return 0, err + return 0, nil, err } if dbb == nil { toAdd = append(toAdd, &git_model.Branch{ @@ -92,7 +101,12 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, PusherID: doerID, CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), }) - } else if commit.ID.String() != dbb.CommitID { + syncResults = append(syncResults, &SyncResult{ + RefName: git.RefNameFromBranch(branch), + OldCommitID: "", + NewCommitID: commit.ID.String(), + }) + } else if commit.ID.String() != dbb.CommitID || dbb.IsDeleted { toUpdate = append(toUpdate, &git_model.Branch{ ID: dbb.ID, RepoID: repo.ID, @@ -102,19 +116,29 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, PusherID: doerID, CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()), }) + syncResults = append(syncResults, &SyncResult{ + RefName: git.RefNameFromBranch(branch), + OldCommitID: dbb.CommitID, + NewCommitID: commit.ID.String(), + }) } } for _, dbBranch := range dbBranches { if !allBranches.Contains(dbBranch.Name) && !dbBranch.IsDeleted { toRemove = append(toRemove, dbBranch.ID) + syncResults = append(syncResults, &SyncResult{ + RefName: git.RefNameFromBranch(dbBranch.Name), + OldCommitID: dbBranch.CommitID, + NewCommitID: "", + }) } } log.Trace("SyncRepoBranches[%s]: toAdd: %v, toUpdate: %v, toRemove: %v", repo.FullName(), toAdd, toUpdate, toRemove) if len(toAdd) == 0 && len(toRemove) == 0 && len(toUpdate) == 0 { - return int64(len(allBranches)), nil + return int64(len(allBranches)), syncResults, nil } if err := db.WithTx(ctx, func(ctx context.Context) error { @@ -140,7 +164,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, return nil }); err != nil { - return 0, err + return 0, nil, err } - return int64(len(allBranches)), nil + return int64(len(allBranches)), syncResults, nil } diff --git a/package/gitea/source/modules/repository/commits.go b/package/gitea/source/modules/repository/commits.go index 878fdc16..a3e253e9 100644 --- a/package/gitea/source/modules/repository/commits.go +++ b/package/gitea/source/modules/repository/commits.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cachegroup" "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/gitrepo" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" @@ -72,7 +73,7 @@ func ToAPIPayloadCommit(ctx context.Context, emailUsers map[string]*user_model.U committerUsername = committer.Name } - fileStatus, err := git.GetCommitFileStatus(ctx, repo.RepoPath(), commit.Sha1) + fileStatus, err := gitrepo.GetCommitFileStatus(ctx, repo, commit.Sha1) if err != nil { return nil, fmt.Errorf("FileStatus [commit_sha1: %s]: %w", commit.Sha1, err) } diff --git a/package/gitea/source/modules/repository/create.go b/package/gitea/source/modules/repository/create.go index a75598a8..f5fb92df 100644 --- a/package/gitea/source/modules/repository/create.go +++ b/package/gitea/source/modules/repository/create.go @@ -6,44 +6,15 @@ package repository import ( "context" "fmt" - "os" - "path/filepath" git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/gitrepo" ) -const notRegularFileMode = os.ModeSymlink | os.ModeNamedPipe | os.ModeSocket | os.ModeDevice | os.ModeCharDevice | os.ModeIrregular - -// getDirectorySize returns the disk consumption for a given path -func getDirectorySize(path string) (int64, error) { - var size int64 - err := filepath.WalkDir(path, func(_ string, entry os.DirEntry, err error) error { - if os.IsNotExist(err) { // ignore the error because some files (like temp/lock file) may be deleted during traversing. - return nil - } else if err != nil { - return err - } - if entry.IsDir() { - return nil - } - info, err := entry.Info() - if os.IsNotExist(err) { // ignore the error as above - return nil - } else if err != nil { - return err - } - if (info.Mode() & notRegularFileMode) == 0 { - size += info.Size() - } - return nil - }) - return size, err -} - // UpdateRepoSize updates the repository size, calculating it using getDirectorySize func UpdateRepoSize(ctx context.Context, repo *repo_model.Repository) error { - size, err := getDirectorySize(repo.RepoPath()) + size, err := gitrepo.CalcRepositorySize(repo) if err != nil { return fmt.Errorf("updateSize: %w", err) } diff --git a/package/gitea/source/modules/repository/create_test.go b/package/gitea/source/modules/repository/create_test.go index 68b0f4de..a3fca02c 100644 --- a/package/gitea/source/modules/repository/create_test.go +++ b/package/gitea/source/modules/repository/create_test.go @@ -8,6 +8,7 @@ import ( repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" + "code.gitea.io/gitea/modules/gitrepo" "github.com/stretchr/testify/assert" ) @@ -16,7 +17,7 @@ func TestGetDirectorySize(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) repo, err := repo_model.GetRepositoryByID(t.Context(), 1) assert.NoError(t, err) - size, err := getDirectorySize(repo.RepoPath()) + size, err := gitrepo.CalcRepositorySize(repo) assert.NoError(t, err) repo.Size = 8165 // real size on the disk assert.Equal(t, repo.Size, size) diff --git a/package/gitea/source/modules/repository/env.go b/package/gitea/source/modules/repository/env.go index 78e06f86..ed2c6fef 100644 --- a/package/gitea/source/modules/repository/env.go +++ b/package/gitea/source/modules/repository/env.go @@ -11,24 +11,26 @@ import ( repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // env keys for git hooks need const ( - EnvRepoName = "GITEA_REPO_NAME" - EnvRepoUsername = "GITEA_REPO_USER_NAME" - EnvRepoID = "GITEA_REPO_ID" - EnvRepoIsWiki = "GITEA_REPO_IS_WIKI" - EnvPusherName = "GITEA_PUSHER_NAME" - EnvPusherEmail = "GITEA_PUSHER_EMAIL" - EnvPusherID = "GITEA_PUSHER_ID" - EnvKeyID = "GITEA_KEY_ID" // public key ID - EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID" - EnvPRID = "GITEA_PR_ID" - EnvPushTrigger = "GITEA_PUSH_TRIGGER" - EnvIsInternal = "GITEA_INTERNAL_PUSH" - EnvAppURL = "GITEA_ROOT_URL" - EnvActionPerm = "GITEA_ACTION_PERM" + EnvRepoName = "GITEA_REPO_NAME" + EnvRepoUsername = "GITEA_REPO_USER_NAME" + EnvRepoID = "GITEA_REPO_ID" + EnvRepoIsWiki = "GITEA_REPO_IS_WIKI" + EnvPusherName = "GITEA_PUSHER_NAME" + EnvPusherEmail = "GITEA_PUSHER_EMAIL" + EnvPusherID = "GITEA_PUSHER_ID" + EnvKeyID = "GITEA_KEY_ID" // public key ID + EnvDeployKeyID = "GITEA_DEPLOY_KEY_ID" + EnvPRID = "GITEA_PR_ID" + EnvPRIndex = "GITEA_PR_INDEX" // not used by Gitea at the moment, it is for custom git hooks + EnvPushTrigger = "GITEA_PUSH_TRIGGER" + EnvIsInternal = "GITEA_INTERNAL_PUSH" + EnvAppURL = "GITEA_ROOT_URL" + EnvActionsTaskID = "GITEA_ACTIONS_TASK_ID" ) type PushTrigger string @@ -50,38 +52,42 @@ func InternalPushingEnvironment(doer *user_model.User, repo *repo_model.Reposito // PushingEnvironment returns an os environment to allow hooks to work on push func PushingEnvironment(doer *user_model.User, repo *repo_model.Repository) []string { - return FullPushingEnvironment(doer, doer, repo, repo.Name, 0) + return FullPushingEnvironment(doer, doer, repo, repo.Name, 0, 0) +} + +func DoerPushingEnvironment(doer *user_model.User, repo *repo_model.Repository, isWiki bool) []string { + env := []string{ + EnvAppURL + "=" + setting.AppURL, + EnvRepoName + "=" + repo.Name + util.Iif(isWiki, ".wiki", ""), + EnvRepoUsername + "=" + repo.OwnerName, + EnvRepoID + "=" + strconv.FormatInt(repo.ID, 10), + EnvRepoIsWiki + "=" + strconv.FormatBool(isWiki), + EnvPusherName + "=" + doer.Name, + EnvPusherID + "=" + strconv.FormatInt(doer.ID, 10), + } + if !doer.KeepEmailPrivate { + env = append(env, EnvPusherEmail+"="+doer.Email) + } + if taskID, isActionsUser := user_model.GetActionsUserTaskID(doer); isActionsUser { + env = append(env, EnvActionsTaskID+"="+strconv.FormatInt(taskID, 10)) + } + return env } // FullPushingEnvironment returns an os environment to allow hooks to work on push -func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID int64) []string { - isWiki := "false" - if strings.HasSuffix(repoName, ".wiki") { - isWiki = "true" - } - +func FullPushingEnvironment(author, committer *user_model.User, repo *repo_model.Repository, repoName string, prID, prIndex int64) []string { + isWiki := strings.HasSuffix(repoName, ".wiki") authorSig := author.NewGitSig() committerSig := committer.NewGitSig() - environ := append(os.Environ(), "GIT_AUTHOR_NAME="+authorSig.Name, "GIT_AUTHOR_EMAIL="+authorSig.Email, "GIT_COMMITTER_NAME="+committerSig.Name, "GIT_COMMITTER_EMAIL="+committerSig.Email, - EnvRepoName+"="+repoName, - EnvRepoUsername+"="+repo.OwnerName, - EnvRepoIsWiki+"="+isWiki, - EnvPusherName+"="+committer.Name, - EnvPusherID+"="+strconv.FormatInt(committer.ID, 10), - EnvRepoID+"="+strconv.FormatInt(repo.ID, 10), EnvPRID+"="+strconv.FormatInt(prID, 10), - EnvAppURL+"="+setting.AppURL, + EnvPRIndex+"="+strconv.FormatInt(prIndex, 10), "SSH_ORIGINAL_COMMAND=gitea-internal", ) - - if !committer.KeepEmailPrivate { - environ = append(environ, EnvPusherEmail+"="+committer.Email) - } - + environ = append(environ, DoerPushingEnvironment(committer, repo, isWiki)...) return environ } diff --git a/package/gitea/source/modules/repository/repo.go b/package/gitea/source/modules/repository/repo.go index 9353043f..76125f5e 100644 --- a/package/gitea/source/modules/repository/repo.go +++ b/package/gitea/source/modules/repository/repo.go @@ -53,7 +53,8 @@ func SyncRepoTags(ctx context.Context, repoID int64) error { } defer gitRepo.Close() - return SyncReleasesWithTags(ctx, repo, gitRepo) + _, err = SyncReleasesWithTags(ctx, repo, gitRepo) + return err } // StoreMissingLfsObjectsInRepository downloads missing LFS objects @@ -62,7 +63,9 @@ func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Re pointerChan := make(chan lfs.PointerBlob) errChan := make(chan error, 1) - go lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan, errChan) + go func() { + errChan <- lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan) + }() downloadObjects := func(pointers []lfs.Pointer) error { err := lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error { @@ -150,13 +153,12 @@ func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Re } } - err, has := <-errChan - if has { + err := <-errChan + if err != nil { log.Error("Repo[%-v]: Error enumerating LFS objects for repository: %v", repo, err) - return err } - return nil + return err } // shortRelease to reduce load memory, this struct can replace repo_model.Release @@ -177,13 +179,14 @@ func (shortRelease) TableName() string { // upstream. Hence, after each sync we want the release set to be // identical to the upstream tag set. This is much more efficient for // repositories like https://github.com/vim/vim (with over 13000 tags). -func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) error { +func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository) ([]*SyncResult, error) { log.Debug("SyncReleasesWithTags: in Repo[%d:%s/%s]", repo.ID, repo.OwnerName, repo.Name) tags, _, err := gitRepo.GetTagInfos(0, 0) if err != nil { - return fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) + return nil, fmt.Errorf("unable to GetTagInfos in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) } var added, deleted, updated int + var syncResults []*SyncResult err = db.WithTx(ctx, func(ctx context.Context) error { dbReleases, err := db.Find[shortRelease](ctx, repo_model.FindReleasesOptions{ RepoID: repo.ID, @@ -194,7 +197,45 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR return fmt.Errorf("unable to FindReleases in pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) } + dbReleasesByID := make(map[int64]*shortRelease, len(dbReleases)) + dbReleasesByTag := make(map[string]*shortRelease, len(dbReleases)) + for _, release := range dbReleases { + dbReleasesByID[release.ID] = release + dbReleasesByTag[release.TagName] = release + } + inserts, deletes, updates := calcSync(tags, dbReleases) + syncResults = make([]*SyncResult, 0, len(inserts)+len(deletes)+len(updates)) + for _, tag := range inserts { + syncResults = append(syncResults, &SyncResult{ + RefName: git.RefNameFromTag(tag.Name), + OldCommitID: "", + NewCommitID: tag.Object.String(), + }) + } + for _, deleteID := range deletes { + release := dbReleasesByID[deleteID] + if release == nil { + continue + } + syncResults = append(syncResults, &SyncResult{ + RefName: git.RefNameFromTag(release.TagName), + OldCommitID: release.Sha1, + NewCommitID: "", + }) + } + for _, tag := range updates { + release := dbReleasesByTag[tag.Name] + oldSha := "" + if release != nil { + oldSha = release.Sha1 + } + syncResults = append(syncResults, &SyncResult{ + RefName: git.RefNameFromTag(tag.Name), + OldCommitID: oldSha, + NewCommitID: tag.Object.String(), + }) + } // // make release set identical to upstream tags // @@ -237,11 +278,11 @@ func SyncReleasesWithTags(ctx context.Context, repo *repo_model.Repository, gitR return nil }) if err != nil { - return fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) + return nil, fmt.Errorf("unable to rebuild release table for pull-mirror Repo[%d:%s/%s]: %w", repo.ID, repo.OwnerName, repo.Name, err) } log.Trace("SyncReleasesWithTags: %d tags added, %d tags deleted, %d tags updated", added, deleted, updated) - return nil + return syncResults, nil } func calcSync(destTags []*git.Tag, dbTags []*shortRelease) ([]*git.Tag, []int64, []*git.Tag) { diff --git a/package/gitea/source/modules/session/db.go b/package/gitea/source/modules/session/db.go index 7b5be1bc..577e20a4 100644 --- a/package/gitea/source/modules/session/db.go +++ b/package/gitea/source/modules/session/db.go @@ -5,6 +5,7 @@ package session import ( "context" + "fmt" "log" "sync" @@ -121,12 +122,12 @@ func (p *DBProvider) Read(sid string) (session.RawStore, error) { } // Exist returns true if session with given ID exists. -func (p *DBProvider) Exist(sid string) bool { +func (p *DBProvider) Exist(sid string) (bool, error) { has, err := auth.ExistSession(dbContext(), sid) if err != nil { - panic("session/DB: error checking existence: " + err.Error()) + return false, fmt.Errorf("session/DB: error checking existence: %w", err) } - return has + return has, nil } // Destroy deletes a session by session ID. @@ -155,12 +156,12 @@ func (p *DBProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err err } // Count counts and returns number of sessions. -func (p *DBProvider) Count() int { +func (p *DBProvider) Count() (int, error) { total, err := auth.CountSessions(dbContext()) if err != nil { - panic("session/DB: error counting records: " + err.Error()) + return 0, fmt.Errorf("session/DB: error counting records: %w", err) } - return int(total) + return int(total), nil } // GC calls GC to clean expired sessions. diff --git a/package/gitea/source/modules/session/redis.go b/package/gitea/source/modules/session/redis.go index d89d8bc6..083869f4 100644 --- a/package/gitea/source/modules/session/redis.go +++ b/package/gitea/source/modules/session/redis.go @@ -135,10 +135,12 @@ func (p *RedisProvider) Init(maxlifetime int64, configs string) (err error) { // Read returns raw session store by session ID. func (p *RedisProvider) Read(sid string) (session.RawStore, error) { psid := p.prefix + sid - if !p.Exist(sid) { + if exist, err := p.Exist(sid); err == nil && !exist { if err := p.c.Set(graceful.GetManager().HammerContext(), psid, "", p.duration).Err(); err != nil { return nil, err } + } else if err != nil { + return nil, err } var kv map[any]any @@ -159,9 +161,9 @@ func (p *RedisProvider) Read(sid string) (session.RawStore, error) { } // Exist returns true if session with given ID exists. -func (p *RedisProvider) Exist(sid string) bool { +func (p *RedisProvider) Exist(sid string) (bool, error) { v, err := p.c.Exists(graceful.GetManager().HammerContext(), p.prefix+sid).Result() - return err == nil && v == 1 + return err == nil && v == 1, err } // Destroy deletes a session by session ID. @@ -174,13 +176,18 @@ func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err poldsid := p.prefix + oldsid psid := p.prefix + sid - if p.Exist(sid) { + if exist, err := p.Exist(sid); err != nil { + return nil, err + } else if exist { return nil, fmt.Errorf("new sid '%s' already exists", sid) - } else if !p.Exist(oldsid) { + } + if exist, err := p.Exist(oldsid); err == nil && !exist { // Make a fake old session. - if err = p.c.Set(graceful.GetManager().HammerContext(), poldsid, "", p.duration).Err(); err != nil { + if err := p.c.Set(graceful.GetManager().HammerContext(), poldsid, "", p.duration).Err(); err != nil { return nil, err } + } else if err != nil { + return nil, err } // do not use Rename here, because the old sid and new sid may be in different redis cluster slot. @@ -211,12 +218,9 @@ func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err } // Count counts and returns number of sessions. -func (p *RedisProvider) Count() int { +func (p *RedisProvider) Count() (int, error) { size, err := p.c.DBSize(graceful.GetManager().HammerContext()).Result() - if err != nil { - return 0 - } - return int(size) + return int(size), err } // GC calls GC to clean expired sessions. diff --git a/package/gitea/source/modules/session/virtual.go b/package/gitea/source/modules/session/virtual.go index 2e29b5fc..597b9e55 100644 --- a/package/gitea/source/modules/session/virtual.go +++ b/package/gitea/source/modules/session/virtual.go @@ -59,17 +59,18 @@ func (o *VirtualSessionProvider) Init(gcLifetime int64, config string) error { func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) { o.lock.RLock() defer o.lock.RUnlock() - if o.provider.Exist(sid) { + if exist, err := o.provider.Exist(sid); err == nil && exist { return o.provider.Read(sid) + } else if err != nil { + return nil, fmt.Errorf("check if '%s' exist failed: %w", sid, err) } kv := make(map[any]any) - kv["_old_uid"] = "0" return NewVirtualStore(o, sid, kv), nil } // Exist returns true if session with given ID exists. -func (o *VirtualSessionProvider) Exist(sid string) bool { - return true +func (o *VirtualSessionProvider) Exist(sid string) (bool, error) { + return true, nil } // Destroy deletes a session by session ID. @@ -87,7 +88,7 @@ func (o *VirtualSessionProvider) Regenerate(oldsid, sid string) (session.RawStor } // Count counts and returns number of sessions. -func (o *VirtualSessionProvider) Count() int { +func (o *VirtualSessionProvider) Count() (int, error) { o.lock.RLock() defer o.lock.RUnlock() return o.provider.Count() @@ -158,13 +159,17 @@ 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 - if !s.released && realProvider.Exist(s.sid) { - // This is an error! - return fmt.Errorf("new sid '%s' already exists", s.sid) + if !s.released { + if exist, err := realProvider.Exist(s.sid); err == nil && exist { + // This is an error! + return fmt.Errorf("new sid '%s' already exists", s.sid) + } else if err != nil { + return fmt.Errorf("check if '%s' exist failed: %w", s.sid, err) + } } realStore, err := realProvider.Read(s.sid) if err != nil { diff --git a/package/gitea/source/modules/setting/actions.go b/package/gitea/source/modules/setting/actions.go index 34346b62..7a91ecb5 100644 --- a/package/gitea/source/modules/setting/actions.go +++ b/package/gitea/source/modules/setting/actions.go @@ -4,6 +4,7 @@ package setting import ( + "errors" "fmt" "strings" "time" @@ -25,10 +26,12 @@ var ( EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"` AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"` SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"` + WorkflowDirs []string `ini:"WORKFLOW_DIRS"` }{ Enabled: true, DefaultActionsURL: defaultActionsURLGitHub, SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"}, + WorkflowDirs: []string{".gitea/workflows", ".github/workflows"}, } ) @@ -119,5 +122,20 @@ func loadActionsFrom(rootCfg ConfigProvider) error { return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression) } + workflowDirs := make([]string, 0, len(Actions.WorkflowDirs)) + for _, dir := range Actions.WorkflowDirs { + dir = strings.TrimSpace(dir) + if dir == "" { + continue + } + dir = strings.ReplaceAll(dir, `\`, `/`) + dir = strings.TrimRight(dir, "/") + workflowDirs = append(workflowDirs, dir) + } + if len(workflowDirs) == 0 { + return errors.New("[actions] WORKFLOW_DIRS must contain at least one entry") + } + Actions.WorkflowDirs = workflowDirs + return nil } diff --git a/package/gitea/source/modules/setting/actions_test.go b/package/gitea/source/modules/setting/actions_test.go index 353cc657..5c7ab268 100644 --- a/package/gitea/source/modules/setting/actions_test.go +++ b/package/gitea/source/modules/setting/actions_test.go @@ -97,6 +97,65 @@ STORAGE_TYPE = minio assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path)) } +func Test_WorkflowDirs(t *testing.T) { + oldActions := Actions + defer func() { + Actions = oldActions + }() + + tests := []struct { + name string + iniStr string + wantDirs []string + wantErr bool + }{ + { + name: "default", + iniStr: `[actions]`, + wantDirs: []string{".gitea/workflows", ".github/workflows"}, + }, + { + name: "single dir", + iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows", + wantDirs: []string{".github/workflows"}, + }, + { + name: "custom order", + iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows,.gitea/workflows", + wantDirs: []string{".github/workflows", ".gitea/workflows"}, + }, + { + name: "whitespace trimming", + iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows , .github/workflows ", + wantDirs: []string{".gitea/workflows", ".github/workflows"}, + }, + { + name: "trailing slash normalization", + iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows/,.github/workflows/", + wantDirs: []string{".gitea/workflows", ".github/workflows"}, + }, + { + name: "only commas and whitespace", + iniStr: "[actions]\nWORKFLOW_DIRS = , , ,", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, err := NewConfigProviderFromData(tt.iniStr) + require.NoError(t, err) + err = loadActionsFrom(cfg) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantDirs, Actions.WorkflowDirs) + }) + } +} + func Test_getDefaultActionsURLForActions(t *testing.T) { oldActions := Actions oldAppURL := AppURL diff --git a/package/gitea/source/modules/setting/admin.go b/package/gitea/source/modules/setting/admin.go index fde291ad..782c73f0 100644 --- a/package/gitea/source/modules/setting/admin.go +++ b/package/gitea/source/modules/setting/admin.go @@ -5,6 +5,7 @@ package setting import ( "code.gitea.io/gitea/modules/container" + "code.gitea.io/gitea/modules/log" ) // Admin settings @@ -15,12 +16,33 @@ var Admin struct { ExternalUserDisableFeatures container.Set[string] } +var validUserFeatures = container.SetOf( + UserFeatureDeletion, + UserFeatureManageSSHKeys, + UserFeatureManageGPGKeys, + UserFeatureManageMFA, + UserFeatureManageCredentials, + UserFeatureChangeUsername, + UserFeatureChangeFullName, +) + func loadAdminFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("admin") Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false) Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled") Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...) Admin.ExternalUserDisableFeatures = container.SetOf(sec.Key("EXTERNAL_USER_DISABLE_FEATURES").Strings(",")...).Union(Admin.UserDisabledFeatures) + + for feature := range Admin.UserDisabledFeatures { + if !validUserFeatures.Contains(feature) { + log.Warn("USER_DISABLED_FEATURES contains unknown feature %q", feature) + } + } + for feature := range Admin.ExternalUserDisableFeatures { + if !validUserFeatures.Contains(feature) && !Admin.UserDisabledFeatures.Contains(feature) { + log.Warn("EXTERNAL_USER_DISABLE_FEATURES contains unknown feature %q", feature) + } + } } const ( diff --git a/package/gitea/source/modules/setting/attachment.go b/package/gitea/source/modules/setting/attachment.go index 5d420c98..30e5cfbf 100644 --- a/package/gitea/source/modules/setting/attachment.go +++ b/package/gitea/source/modules/setting/attachment.go @@ -16,13 +16,9 @@ var Attachment AttachmentSettingType func loadAttachmentFrom(rootCfg ConfigProvider) (err error) { Attachment = AttachmentSettingType{ AllowedTypes: ".avif,.cpuprofile,.csv,.dmp,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.json,.jsonc,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.webp,.xls,.xlsx,.zip", - - // FIXME: this size is used for both "issue attachment" and "release attachment" - // The design is not right, these two should be different settings - MaxSize: 2048, - - MaxFiles: 5, - Enabled: true, + MaxSize: 100, + MaxFiles: 5, + Enabled: true, } sec, _ := rootCfg.GetSection("attachment") if sec == nil { diff --git a/package/gitea/source/modules/setting/config.go b/package/gitea/source/modules/setting/config.go index 4c5d2df7..a41734a8 100644 --- a/package/gitea/source/modules/setting/config.go +++ b/package/gitea/source/modules/setting/config.go @@ -4,6 +4,7 @@ package setting import ( + "strings" "sync" "code.gitea.io/gitea/modules/log" @@ -11,8 +12,8 @@ import ( ) type PictureStruct struct { - DisableGravatar *config.Value[bool] - EnableFederatedAvatar *config.Value[bool] + DisableGravatar *config.Option[bool] + EnableFederatedAvatar *config.Option[bool] } type OpenWithEditorApp struct { @@ -22,15 +23,18 @@ type OpenWithEditorApp struct { type OpenWithEditorAppsType []OpenWithEditorApp +// ToTextareaString is only used in templates, for help prompt only +// TODO: OPEN-WITH-EDITOR-APP-JSON: Because there is no "rich UI", a plain text editor is used to manage the list of apps +// Maybe we can use some better formats like Yaml in the future, then a simple textarea can manage the config clearly 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 { +func openWithEditorAppsDefaultValue() OpenWithEditorAppsType { return OpenWithEditorAppsType{ { DisplayName: "VS Code", @@ -48,13 +52,14 @@ func DefaultOpenWithEditorApps() OpenWithEditorAppsType { } type RepositoryStruct struct { - OpenWithEditorApps *config.Value[OpenWithEditorAppsType] - GitGuideRemoteName *config.Value[string] + OpenWithEditorApps *config.Option[OpenWithEditorAppsType] + GitGuideRemoteName *config.Option[string] } type ConfigStruct struct { Picture *PictureStruct Repository *RepositoryStruct + Instance *InstanceStruct } var ( @@ -66,12 +71,16 @@ func initDefaultConfig() { config.SetCfgSecKeyGetter(&cfgSecKeyGetter{}) defaultConfig = &ConfigStruct{ Picture: &PictureStruct{ - DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), - EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}), + DisableGravatar: config.NewOption[bool]("picture.disable_gravatar").WithDefaultSimple(true).WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}), + EnableFederatedAvatar: config.NewOption[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}), }, Repository: &RepositoryStruct{ - OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"), - GitGuideRemoteName: config.ValueJSON[string]("repository.git-guide-remote-name").WithDefault("origin"), + OpenWithEditorApps: config.NewOption[OpenWithEditorAppsType]("repository.open-with.editor-apps").WithEmptyAsDefault().WithDefaultFunc(openWithEditorAppsDefaultValue), + GitGuideRemoteName: config.NewOption[string]("repository.git-guide-remote-name").WithEmptyAsDefault().WithDefaultSimple("origin"), + }, + Instance: &InstanceStruct{ + WebBanner: config.NewOption[WebBannerType]("instance.web_banner"), + MaintenanceMode: config.NewOption[MaintenanceModeType]("instance.maintenance_mode"), }, } } diff --git a/package/gitea/source/modules/setting/config/value.go b/package/gitea/source/modules/setting/config/value.go index 301c60f5..c01f4130 100644 --- a/package/gitea/source/modules/setting/config/value.go +++ b/package/gitea/source/modules/setting/config/value.go @@ -5,6 +5,7 @@ package config import ( "context" + "reflect" "sync" "code.gitea.io/gitea/modules/json" @@ -16,18 +17,31 @@ type CfgSecKey struct { Sec, Key string } -type Value[T any] struct { +// OptionInterface is used to overcome Golang's generic interface limitation +type OptionInterface interface { + GetDefaultValue() any +} + +type Option[T any] struct { mu sync.RWMutex cfgSecKey CfgSecKey dynKey string - def, value T + value T + defSimple T + defFunc func() T + emptyAsDef bool + has bool revision int } -func (value *Value[T]) parse(key, valStr string) (v T) { - v = value.def +func (opt *Option[T]) GetDefaultValue() any { + return opt.DefaultValue() +} + +func (opt *Option[T]) parse(key, valStr string) (v T) { + v = opt.DefaultValue() if valStr != "" { if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil { log.Error("Unable to unmarshal json config for key %q, err: %v", key, err) @@ -36,7 +50,35 @@ func (value *Value[T]) parse(key, valStr string) (v T) { return v } -func (value *Value[T]) Value(ctx context.Context) (v T) { +func (opt *Option[T]) HasValue(ctx context.Context) bool { + _, _, has := opt.ValueRevision(ctx) + return has +} + +func (opt *Option[T]) Value(ctx context.Context) (v T) { + v, _, _ = opt.ValueRevision(ctx) + return v +} + +func isZeroOrEmpty(v any) bool { + if v == nil { + return true // interface itself is nil + } + r := reflect.ValueOf(v) + if r.IsZero() { + return true + } + + if r.Kind() == reflect.Slice || r.Kind() == reflect.Map { + if r.IsNil() { + return true + } + return r.Len() == 0 + } + return false +} + +func (opt *Option[T]) ValueRevision(ctx context.Context) (v T, rev int, has bool) { dg := GetDynGetter() if dg == nil { // this is an edge case: the database is not initialized but the system setting is going to be used @@ -44,55 +86,96 @@ func (value *Value[T]) Value(ctx context.Context) (v T) { panic("no config dyn value getter") } - rev := dg.GetRevision(ctx) + rev = dg.GetRevision(ctx) // if the revision in the database doesn't change, use the last value - value.mu.RLock() - if rev == value.revision { - v = value.value - value.mu.RUnlock() - return v + opt.mu.RLock() + if rev == opt.revision { + v = opt.value + has = opt.has + opt.mu.RUnlock() + return v, rev, has } - value.mu.RUnlock() + opt.mu.RUnlock() // try to parse the config and cache it var valStr *string - if dynVal, has := dg.GetValue(ctx, value.dynKey); has { + if dynVal, hasDbValue := dg.GetValue(ctx, opt.dynKey); hasDbValue { valStr = &dynVal - } else if cfgVal, has := GetCfgSecKeyGetter().GetValue(value.cfgSecKey.Sec, value.cfgSecKey.Key); has { + } else if cfgVal, hasCfgValue := GetCfgSecKeyGetter().GetValue(opt.cfgSecKey.Sec, opt.cfgSecKey.Key); hasCfgValue { valStr = &cfgVal } if valStr == nil { - v = value.def + v = opt.DefaultValue() + has = false } else { - v = value.parse(value.dynKey, *valStr) + v = opt.parse(opt.dynKey, *valStr) + if opt.emptyAsDef && isZeroOrEmpty(v) { + v = opt.DefaultValue() + } else { + has = true + } } - value.mu.Lock() - value.value = v - value.revision = rev - value.mu.Unlock() + opt.mu.Lock() + opt.value = v + opt.revision = rev + opt.has = has + opt.mu.Unlock() + return v, rev, has +} + +func (opt *Option[T]) DynKey() string { + return opt.dynKey +} + +// WithDefaultFunc sets the default value with a function +// The "def" value might be changed during runtime (e.g.: Unmarshal with default), so it shouldn't use the same pointer or slice +func (opt *Option[T]) WithDefaultFunc(f func() T) *Option[T] { + opt.defFunc = f + return opt +} + +func (opt *Option[T]) WithDefaultSimple(def T) *Option[T] { + v := any(def) + switch v.(type) { + case string, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + default: + // TODO: use reflect to support convertible basic types like `type State string` + r := reflect.ValueOf(v) + if r.Kind() != reflect.Struct { + panic("invalid type for default value, use WithDefaultFunc instead") + } + } + opt.defSimple = def + return opt +} + +func (opt *Option[T]) WithEmptyAsDefault() *Option[T] { + opt.emptyAsDef = true + return opt +} + +func (opt *Option[T]) DefaultValue() T { + if opt.defFunc != nil { + return opt.defFunc() + } + return opt.defSimple +} + +func (opt *Option[T]) WithFileConfig(cfgSecKey CfgSecKey) *Option[T] { + opt.cfgSecKey = cfgSecKey + return opt +} + +var allConfigOptions = map[string]OptionInterface{} + +func NewOption[T any](dynKey string) *Option[T] { + v := &Option[T]{dynKey: dynKey} + allConfigOptions[dynKey] = v return v } -func (value *Value[T]) DynKey() string { - return value.dynKey -} - -func (value *Value[T]) WithDefault(def T) *Value[T] { - value.def = def - return value -} - -func (value *Value[T]) DefaultValue() T { - return value.def -} - -func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] { - value.cfgSecKey = cfgSecKey - return value -} - -func ValueJSON[T any](dynKey string) *Value[T] { - return &Value[T]{dynKey: dynKey} +func GetConfigOption(dynKey string) OptionInterface { + return allConfigOptions[dynKey] } diff --git a/package/gitea/source/modules/setting/config_env.go b/package/gitea/source/modules/setting/config_env.go index 409588dc..e956e344 100644 --- a/package/gitea/source/modules/setting/config_env.go +++ b/package/gitea/source/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, cutOk := strings.Cut(preceding, "__"); cutOk { + section += before inKey = true - key += preceding[splitter+2:] + key += after } else { section += preceding } @@ -65,7 +65,7 @@ func decodeEnvSectionKey(encoded string) (ok bool, section, key string) { decodedBytes := make([]byte, len(toDecode)/2) for i := 0; i < len(toDecode)/2; i++ { // Can ignore error here as we know these should be hexadecimal from the regexp - byteInt, _ := strconv.ParseInt(toDecode[2*i:2*i+2], 16, 0) + byteInt, _ := strconv.ParseInt(toDecode[2*i:2*i+2], 16, 8) decodedBytes[i] = byte(byteInt) } if inKey { @@ -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, cutOk := strings.Cut(remaining, "__"); cutOk { + section += before + key += after } else { section += remaining } @@ -111,21 +111,21 @@ func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, sect func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { for _, kv := range envs { - idx := strings.IndexByte(kv, '=') - if idx < 0 { + before, after, ok := strings.Cut(kv, "=") + if !ok { continue } // parse the environment variable to config section name and key name - envKey := kv[:idx] - envValue := kv[idx+1:] + envKey := before + envValue := after ok, sectionName, keyName, useFileValue := decodeEnvironmentKey(EnvConfigKeyPrefixGitea, 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 + keyValue := envValue //nolint:staticcheck // false positive if useFileValue { fileContent, err := os.ReadFile(envValue) if err != nil { @@ -167,24 +167,13 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { return changed } -// InitGiteaEnvVars initializes the environment variables for gitea -func InitGiteaEnvVars() { +func UnsetUnnecessaryEnvVars() { // Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want, - // but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users, - // so at the moment we could still keep "unsetting the unnecessary environments" + // but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users. + // So at the moment we just keep "unsetting the unnecessary environment variables". // HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig". // But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist, // then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set. _ = os.Unsetenv("XDG_CONFIG_HOME") } - -func InitGiteaEnvVarsForTesting() { - InitGiteaEnvVars() - _ = os.Unsetenv("GIT_AUTHOR_NAME") - _ = os.Unsetenv("GIT_AUTHOR_EMAIL") - _ = os.Unsetenv("GIT_AUTHOR_DATE") - _ = os.Unsetenv("GIT_COMMITTER_NAME") - _ = os.Unsetenv("GIT_COMMITTER_EMAIL") - _ = os.Unsetenv("GIT_COMMITTER_DATE") -} diff --git a/package/gitea/source/modules/setting/config_option_instance.go b/package/gitea/source/modules/setting/config_option_instance.go new file mode 100644 index 00000000..6d97055a --- /dev/null +++ b/package/gitea/source/modules/setting/config_option_instance.go @@ -0,0 +1,58 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "time" + + "code.gitea.io/gitea/modules/setting/config" +) + +// WebBannerType fields are directly used in templates, +// do remember to update the template if you change the fields +type WebBannerType struct { + DisplayEnabled bool + ContentMessage string + StartTimeUnix int64 + EndTimeUnix int64 +} + +func (b WebBannerType) ShouldDisplay() bool { + if !b.DisplayEnabled || b.ContentMessage == "" { + return false + } + now := time.Now().Unix() + if b.StartTimeUnix > 0 && now < b.StartTimeUnix { + return false + } + if b.EndTimeUnix > 0 && now > b.EndTimeUnix { + return false + } + return true +} + +type MaintenanceModeType struct { + AdminWebAccessOnly bool + StartTimeUnix int64 + EndTimeUnix int64 +} + +func (m MaintenanceModeType) IsActive() bool { + if !m.AdminWebAccessOnly { + return false + } + now := time.Now().Unix() + if m.StartTimeUnix > 0 && now < m.StartTimeUnix { + return false + } + if m.EndTimeUnix > 0 && now > m.EndTimeUnix { + return false + } + return true +} + +type InstanceStruct struct { + WebBanner *config.Option[WebBannerType] + MaintenanceMode *config.Option[MaintenanceModeType] +} diff --git a/package/gitea/source/modules/setting/config_provider.go b/package/gitea/source/modules/setting/config_provider.go index e9e051e5..a734f3bf 100644 --- a/package/gitea/source/modules/setting/config_provider.go +++ b/package/gitea/source/modules/setting/config_provider.go @@ -41,6 +41,7 @@ type ConfigSection interface { HasKey(key string) bool NewKey(name, value string) (ConfigKey, error) Key(key string) ConfigKey + DeleteKey(key string) Keys() []ConfigKey ChildSections() []ConfigSection } @@ -51,6 +52,7 @@ type ConfigProvider interface { Sections() []ConfigSection NewSection(name string) (ConfigSection, error) GetSection(name string) (ConfigSection, error) + DeleteSection(name string) Save() error SaveTo(filename string) error @@ -168,6 +170,10 @@ func (s *iniConfigSection) Keys() (keys []ConfigKey) { return keys } +func (s *iniConfigSection) DeleteKey(key string) { + s.sec.DeleteKey(key) +} + func (s *iniConfigSection) ChildSections() (sections []ConfigSection) { for _, s := range s.sec.ChildSections() { sections = append(sections, &iniConfigSection{s}) @@ -249,6 +255,10 @@ func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) { return &iniConfigSection{sec: sec}, nil } +func (p *iniConfigProvider) DeleteSection(name string) { + p.ini.DeleteSection(name) +} + var errDisableSaving = errors.New("this config can't be saved, developers should prepare a new config to save") // Save saves the content into file @@ -338,23 +348,6 @@ func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) { } } -// NewConfigProviderForLocale loads locale configuration from source and others. "string" if for a local file path, "[]byte" is for INI content -func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, error) { - iniFile, err := ini.LoadSources(ini.LoadOptions{ - IgnoreInlineComment: true, - UnescapeValueCommentSymbols: true, - IgnoreContinuation: true, - }, source, others...) - if err != nil { - return nil, fmt.Errorf("unable to load locale ini: %w", err) - } - iniFile.BlockMode = false - return &iniConfigProvider{ - ini: iniFile, - loadedFromEmpty: true, - }, nil -} - func init() { ini.PrettyFormat = false } diff --git a/package/gitea/source/modules/setting/config_provider_test.go b/package/gitea/source/modules/setting/config_provider_test.go index 63121f00..dcbe280e 100644 --- a/package/gitea/source/modules/setting/config_provider_test.go +++ b/package/gitea/source/modules/setting/config_provider_test.go @@ -113,24 +113,6 @@ func TestNewConfigProviderFromFile(t *testing.T) { assert.Equal(t, "[foo]\nk1 = a\n\n[bar]\nk1 = b\n", string(bs)) } -func TestNewConfigProviderForLocale(t *testing.T) { - // load locale from file - localeFile := t.TempDir() + "/locale.ini" - _ = os.WriteFile(localeFile, []byte(`k1=a`), 0o644) - cfg, err := NewConfigProviderForLocale(localeFile) - assert.NoError(t, err) - assert.Equal(t, "a", cfg.Section("").Key("k1").String()) - - // load locale from bytes - cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar")) - assert.NoError(t, err) - assert.Equal(t, "foo", cfg.Section("").Key("k1").String()) - cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar"), []byte("k2=xxx")) - assert.NoError(t, err) - assert.Equal(t, "foo", cfg.Section("").Key("k1").String()) - assert.Equal(t, "xxx", cfg.Section("").Key("k2").String()) -} - func TestDisableSaving(t *testing.T) { testFile := t.TempDir() + "/test.ini" _ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644) diff --git a/package/gitea/source/modules/setting/cors.go b/package/gitea/source/modules/setting/cors.go index 5260887d..a4906b8a 100644 --- a/package/gitea/source/modules/setting/cors.go +++ b/package/gitea/source/modules/setting/cors.go @@ -15,13 +15,11 @@ var CORSConfig = struct { MaxAge time.Duration AllowCredentials bool Headers []string - XFrameOptions string }{ - AllowDomain: []string{"*"}, - Methods: []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, - Headers: []string{"Content-Type", "User-Agent"}, - MaxAge: 10 * time.Minute, - XFrameOptions: "SAMEORIGIN", + AllowDomain: []string{"*"}, + Methods: []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, + Headers: []string{"Content-Type", "User-Agent"}, + MaxAge: 10 * time.Minute, } func loadCorsFrom(rootCfg ConfigProvider) { diff --git a/package/gitea/source/modules/setting/git.go b/package/gitea/source/modules/setting/git.go index 48a4e7f3..29fd3daf 100644 --- a/package/gitea/source/modules/setting/git.go +++ b/package/gitea/source/modules/setting/git.go @@ -5,6 +5,7 @@ package setting import ( "path/filepath" + "regexp" "strings" "time" @@ -17,54 +18,47 @@ var Git = struct { HomePath string DisableDiffHighlight bool - MaxGitDiffLines int - MaxGitDiffLineCharacters int - MaxGitDiffFiles int - CommitsRangeSize int // CommitsRangeSize the default commits range size - BranchesRangeSize int // BranchesRangeSize the default branches range size - VerbosePush bool - VerbosePushDelay time.Duration - GCArgs []string `ini:"GC_ARGS" delim:" "` - EnableAutoGitWireProtocol bool - PullRequestPushMessage bool - LargeObjectThreshold int64 - DisableCoreProtectNTFS bool - DisablePartialClone bool - Timeout struct { - Default int + MaxGitDiffLines int + MaxGitDiffLineCharacters int + MaxGitDiffFiles int + CommitsRangeSize int // CommitsRangeSize the default commits range size + BranchesRangeSize int // BranchesRangeSize the default branches range size + VerbosePush bool + VerbosePushDelay time.Duration + GCArgs []string `ini:"GC_ARGS" delim:" "` + EnableAutoGitWireProtocol bool + PullRequestPushMessage bool + LargeObjectThreshold int64 + DisableCoreProtectNTFS bool + DisablePartialClone bool + DiffRenameSimilarityThreshold string + Timeout struct { Migrate int Mirror int - Clone int - Pull int GC int `ini:"GC"` } `ini:"git.timeout"` }{ - DisableDiffHighlight: false, - MaxGitDiffLines: 1000, - MaxGitDiffLineCharacters: 5000, - MaxGitDiffFiles: 100, - CommitsRangeSize: 50, - BranchesRangeSize: 20, - VerbosePush: true, - VerbosePushDelay: 5 * time.Second, - GCArgs: []string{}, - EnableAutoGitWireProtocol: true, - PullRequestPushMessage: true, - LargeObjectThreshold: 1024 * 1024, - DisablePartialClone: false, + DisableDiffHighlight: false, + MaxGitDiffLines: 1000, + MaxGitDiffLineCharacters: 5000, + MaxGitDiffFiles: 100, + CommitsRangeSize: 50, + BranchesRangeSize: 20, + VerbosePush: true, + VerbosePushDelay: 5 * time.Second, + GCArgs: []string{}, + EnableAutoGitWireProtocol: true, + PullRequestPushMessage: true, + LargeObjectThreshold: 1024 * 1024, + DisablePartialClone: false, + DiffRenameSimilarityThreshold: "50%", Timeout: struct { - Default int Migrate int Mirror int - Clone int - Pull int GC int `ini:"GC"` }{ - Default: 360, Migrate: 600, Mirror: 300, - Clone: 300, - Pull: 300, GC: 60, }, } @@ -117,4 +111,9 @@ func loadGitFrom(rootCfg ConfigProvider) { } else { Git.HomePath = filepath.Clean(Git.HomePath) } + + // validate for a integer percentage between 0% and 100% + if !regexp.MustCompile(`^([0-9]|[1-9][0-9]|100)%$`).MatchString(Git.DiffRenameSimilarityThreshold) { + log.Fatal("Invalid git.DIFF_RENAME_SIMILARITY_THRESHOLD: %s", Git.DiffRenameSimilarityThreshold) + } } diff --git a/package/gitea/source/modules/setting/incoming_email.go b/package/gitea/source/modules/setting/incoming_email.go index 4e433dde..bf9f7f97 100644 --- a/package/gitea/source/modules/setting/incoming_email.go +++ b/package/gitea/source/modules/setting/incoming_email.go @@ -12,10 +12,11 @@ import ( "code.gitea.io/gitea/modules/log" ) +const IncomingEmailTokenPlaceholder = "%{token}" + var IncomingEmail = struct { Enabled bool ReplyToAddress string - TokenPlaceholder string `ini:"-"` Host string Port int UseTLS bool `ini:"USE_TLS"` @@ -28,7 +29,6 @@ var IncomingEmail = struct { }{ Mailbox: "INBOX", DeleteHandledMessage: true, - TokenPlaceholder: "%{token}", MaximumMessageSize: 10485760, } @@ -54,19 +54,10 @@ func checkReplyToAddress() error { return errors.New("name must not be set") } - c := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmail.TokenPlaceholder) - switch c { - case 0: - return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder) - case 1: - default: - return fmt.Errorf("%s must appear only once", IncomingEmail.TokenPlaceholder) + placeholderCount := strings.Count(IncomingEmail.ReplyToAddress, IncomingEmailTokenPlaceholder) + userPart, _, _ := strings.Cut(IncomingEmail.ReplyToAddress, "@") + if placeholderCount != 1 || !strings.Contains(userPart, IncomingEmailTokenPlaceholder) { + return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmailTokenPlaceholder) } - - parts := strings.Split(IncomingEmail.ReplyToAddress, "@") - if !strings.Contains(parts[0], IncomingEmail.TokenPlaceholder) { - return fmt.Errorf("%s must appear in the user part of the address (before the @)", IncomingEmail.TokenPlaceholder) - } - return nil } diff --git a/package/gitea/source/modules/setting/indexer_test.go b/package/gitea/source/modules/setting/indexer_test.go index 8f0437be..498f8752 100644 --- a/package/gitea/source/modules/setting/indexer_test.go +++ b/package/gitea/source/modules/setting/indexer_test.go @@ -65,7 +65,7 @@ func checkGlobMatch(t *testing.T, globstr string, list []indexerMatchList) { } } if !found { - assert.Equal(t, m.position, -1, "Test string `%s` doesn't match `%s` anywhere; expected @%d", m.value, globstr, m.position) + assert.Equal(t, -1, m.position, "Test string `%s` doesn't match `%s` anywhere; expected @%d", m.value, globstr, m.position) } } } diff --git a/package/gitea/source/modules/setting/lfs.go b/package/gitea/source/modules/setting/lfs.go index 7f2d0ae1..3ec21860 100644 --- a/package/gitea/source/modules/setting/lfs.go +++ b/package/gitea/source/modules/setting/lfs.go @@ -81,10 +81,7 @@ func loadLFSFrom(rootCfg ConfigProvider) error { jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64) if err != nil { - LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64() - if err != nil { - return fmt.Errorf("error generating JWT Secret for custom config: %v", err) - } + LFS.JWTSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64() // Save secret saveCfg, err := rootCfg.PrepareSaving() diff --git a/package/gitea/source/modules/setting/mailer_test.go b/package/gitea/source/modules/setting/mailer_test.go index ceef35b0..7e1c9885 100644 --- a/package/gitea/source/modules/setting/mailer_test.go +++ b/package/gitea/source/modules/setting/mailer_test.go @@ -6,17 +6,19 @@ package setting import ( "testing" + "code.gitea.io/gitea/modules/test" + "github.com/stretchr/testify/assert" ) func Test_loadMailerFrom(t *testing.T) { kases := map[string]*Mailer{ - "smtp.mydomain.com": { - SMTPAddr: "smtp.mydomain.com", + "smtp.mydomain.test": { + SMTPAddr: "smtp.mydomain.test", SMTPPort: "465", }, - "smtp.mydomain.com:123": { - SMTPAddr: "smtp.mydomain.com", + "smtp.mydomain.test:123": { + SMTPAddr: "smtp.mydomain.test", SMTPPort: "123", }, ":123": { @@ -39,3 +41,30 @@ func Test_loadMailerFrom(t *testing.T) { }) } } + +func TestLoadSettingsForInstallMailServiceFlags(t *testing.T) { + defer test.MockVariableValue(&Service)() + defer test.MockVariableValue(&MailService)() + + cfg, err := NewConfigProviderFromData(` +[database] +DB_TYPE = postgres + +[mailer] +ENABLED = true +SMTP_ADDR = 127.0.0.1 +SMTP_PORT = 465 +FROM = noreply@example.com + +[service] +REGISTER_EMAIL_CONFIRM = true +ENABLE_NOTIFY_MAIL = true +`) + assert.NoError(t, err) + loadDBSetting(cfg) + loadServiceFrom(cfg) + loadMailsFrom(cfg) + + assert.True(t, Service.RegisterEmailConfirm) + assert.True(t, Service.EnableNotifyMail) +} diff --git a/package/gitea/source/modules/setting/markup.go b/package/gitea/source/modules/setting/markup.go index e105506f..921af60f 100644 --- a/package/gitea/source/modules/setting/markup.go +++ b/package/gitea/source/modules/setting/markup.go @@ -6,6 +6,7 @@ package setting import ( "regexp" "strings" + "sync" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" @@ -43,22 +44,20 @@ var Markdown = struct { RenderOptionsRepoFile MarkdownRenderOptions `ini:"-"` CustomURLSchemes []string `ini:"CUSTOM_URL_SCHEMES"` // Actually it is a "markup" option because it is used in "post processor" - FileExtensions []string + FileNamePatterns []string `ini:"-"` EnableMath bool MathCodeBlockDetection []string MathCodeBlockOptions MarkdownMathCodeBlockOptions `ini:"-"` }{ - FileExtensions: strings.Split(".md,.markdown,.mdown,.mkd,.livemd", ","), - EnableMath: true, + EnableMath: true, } // MarkupRenderer defines the external parser configured in ini type MarkupRenderer struct { - Enabled bool MarkupName string Command string - FileExtensions []string + FilePatterns []string IsInputFile bool NeedPostProcess bool MarkupSanitizerRules []MarkupSanitizerRule @@ -77,6 +76,13 @@ type MarkupSanitizerRule struct { func loadMarkupFrom(rootCfg ConfigProvider) { mustMapSetting(rootCfg, "markdown", &Markdown) + + markdownFileExtensions := rootCfg.Section("markdown").Key("FILE_EXTENSIONS").Strings(",") + if len(markdownFileExtensions) == 0 || len(markdownFileExtensions) == 1 && markdownFileExtensions[0] == "" { + markdownFileExtensions = []string{".md", ".markdown", ".mdown", ".mkd", ".livemd"} + } + Markdown.FileNamePatterns = fileExtensionsToPatterns("markdown", markdownFileExtensions) + const none = "none" const renderOptionShortIssuePattern = "short-issue-pattern" @@ -215,21 +221,30 @@ func createMarkupSanitizerRule(name string, sec ConfigSection) (MarkupSanitizerR return rule, true } -func newMarkupRenderer(name string, sec ConfigSection) { - extensionReg := regexp.MustCompile(`\.\w`) +var extensionReg = sync.OnceValue(func() *regexp.Regexp { + return regexp.MustCompile(`^(\.[-\w]+)+$`) +}) - extensions := sec.Key("FILE_EXTENSIONS").Strings(",") - exts := make([]string, 0, len(extensions)) +func fileExtensionsToPatterns(sectionName string, extensions []string) []string { + patterns := make([]string, 0, len(extensions)) for _, extension := range extensions { - if !extensionReg.MatchString(extension) { - log.Warn(sec.Name() + " file extension " + extension + " is invalid. Extension ignored") + if !extensionReg().MatchString(extension) { + log.Warn("Config section %s file extension %s is invalid. Extension ignored", sectionName, extension) } else { - exts = append(exts, extension) + patterns = append(patterns, "*"+extension) } } + return patterns +} - if len(exts) == 0 { - log.Warn(sec.Name() + " file extension is empty, markup " + name + " ignored") +func newMarkupRenderer(name string, sec ConfigSection) { + if !sec.Key("ENABLED").MustBool(false) { + return + } + + fileNamePatterns := fileExtensionsToPatterns(name, sec.Key("FILE_EXTENSIONS").Strings(",")) + if len(fileNamePatterns) == 0 { + log.Warn("Config section %s file extension is empty, markup render is ignored", name) return } @@ -255,18 +270,17 @@ func newMarkupRenderer(name string, sec ConfigSection) { } // ATTENTION! at the moment, only a safe set like "allow-scripts" are allowed for sandbox mode. - // "allow-same-origin" should never be used, it leads to XSS attack, and it makes the JS in iframe can access parent window's config and CSRF token + // "allow-same-origin" should NEVER be used, it leads to XSS attack: makes the JS in iframe can access parent window's config and send requests with user's credentials. renderContentSandbox := sec.Key("RENDER_CONTENT_SANDBOX").MustString("allow-scripts allow-popups") if renderContentSandbox == "disabled" { renderContentSandbox = "" } ExternalMarkupRenderers = append(ExternalMarkupRenderers, &MarkupRenderer{ - Enabled: sec.Key("ENABLED").MustBool(false), - MarkupName: name, - FileExtensions: exts, - Command: command, - IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false), + MarkupName: name, + FilePatterns: fileNamePatterns, + Command: command, + IsInputFile: sec.Key("IS_INPUT_FILE").MustBool(false), RenderContentMode: renderContentMode, RenderContentSandbox: renderContentSandbox, diff --git a/package/gitea/source/modules/setting/oauth2.go b/package/gitea/source/modules/setting/oauth2.go index ae2a9d7b..8e0210aa 100644 --- a/package/gitea/source/modules/setting/oauth2.go +++ b/package/gitea/source/modules/setting/oauth2.go @@ -133,16 +133,13 @@ func loadOAuth2From(rootCfg ConfigProvider) { // 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 ... + // Including: 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.DecodeJwtSecretBase64(jwtSecretBase64) if err != nil { - jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64() - if err != nil { - log.Fatal("error generating JWT secret: %v", err) - } + jwtSecretBytes, jwtSecretBase64 = generate.NewJwtSecretWithBase64() saveCfg, err := rootCfg.PrepareSaving() if err != nil { log.Fatal("save oauth2.JWT_SECRET failed: %v", err) @@ -162,10 +159,7 @@ var generalSigningSecret atomic.Pointer[[]byte] func GetGeneralTokenSigningSecret() []byte { old := generalSigningSecret.Load() if old == nil || len(*old) == 0 { - jwtSecret, _, err := generate.NewJwtSecretWithBase64() - if err != nil { - log.Fatal("Unable to generate general JWT secret: %v", err) - } + jwtSecret, _ := generate.NewJwtSecretWithBase64() if generalSigningSecret.CompareAndSwap(old, &jwtSecret) { return jwtSecret } diff --git a/package/gitea/source/modules/setting/packages.go b/package/gitea/source/modules/setting/packages.go index b5984240..38ee2ad5 100644 --- a/package/gitea/source/modules/setting/packages.go +++ b/package/gitea/source/modules/setting/packages.go @@ -16,30 +16,31 @@ var ( Storage *Storage Enabled bool - LimitTotalOwnerCount int64 - LimitTotalOwnerSize int64 - LimitSizeAlpine int64 - LimitSizeArch int64 - LimitSizeCargo int64 - LimitSizeChef int64 - LimitSizeComposer int64 - LimitSizeConan int64 - LimitSizeConda int64 - LimitSizeContainer int64 - LimitSizeCran int64 - LimitSizeDebian int64 - LimitSizeGeneric int64 - LimitSizeGo int64 - LimitSizeHelm int64 - LimitSizeMaven int64 - LimitSizeNpm int64 - LimitSizeNuGet int64 - LimitSizePub int64 - LimitSizePyPI int64 - LimitSizeRpm int64 - LimitSizeRubyGems int64 - LimitSizeSwift int64 - LimitSizeVagrant int64 + LimitTotalOwnerCount int64 + LimitTotalOwnerSize int64 + LimitSizeAlpine int64 + LimitSizeArch int64 + LimitSizeCargo int64 + LimitSizeChef int64 + LimitSizeComposer int64 + LimitSizeConan int64 + LimitSizeConda int64 + LimitSizeContainer int64 + LimitSizeCran int64 + LimitSizeDebian int64 + LimitSizeGeneric int64 + LimitSizeGo int64 + LimitSizeHelm int64 + LimitSizeMaven int64 + LimitSizeNpm int64 + LimitSizeNuGet int64 + LimitSizePub int64 + LimitSizePyPI int64 + LimitSizeRpm int64 + LimitSizeRubyGems int64 + LimitSizeSwift int64 + LimitSizeTerraformState int64 + LimitSizeVagrant int64 DefaultRPMSignEnabled bool }{ @@ -86,6 +87,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) { Packages.LimitSizeRpm = mustBytes(sec, "LIMIT_SIZE_RPM") Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS") Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT") + Packages.LimitSizeTerraformState = mustBytes(sec, "LIMIT_SIZE_TERRAFORM_STATE") Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT") Packages.DefaultRPMSignEnabled = sec.Key("DEFAULT_RPM_SIGN_ENABLED").MustBool(false) return nil diff --git a/package/gitea/source/modules/setting/picture.go b/package/gitea/source/modules/setting/picture.go index fafae45b..d20a110b 100644 --- a/package/gitea/source/modules/setting/picture.go +++ b/package/gitea/source/modules/setting/picture.go @@ -22,9 +22,7 @@ var ( RenderedSizeFactor: 2, } - GravatarSource string - DisableGravatar bool // Depreciated: migrated to database - EnableFederatedAvatar bool // Depreciated: migrated to database + GravatarSource string RepoAvatar = struct { Storage *Storage @@ -65,29 +63,12 @@ func loadAvatarsFrom(rootCfg ConfigProvider) error { GravatarSource = source } - DisableGravatar = sec.Key("DISABLE_GRAVATAR").MustBool(GetDefaultDisableGravatar()) - deprecatedSettingDB(rootCfg, "", "DISABLE_GRAVATAR") - EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(GetDefaultEnableFederatedAvatar(DisableGravatar)) - deprecatedSettingDB(rootCfg, "", "ENABLE_FEDERATED_AVATAR") + deprecatedSettingDB(rootCfg, "picture", "DISABLE_GRAVATAR") + deprecatedSettingDB(rootCfg, "picture", "ENABLE_FEDERATED_AVATAR") return nil } -func GetDefaultDisableGravatar() bool { - return OfflineMode -} - -func GetDefaultEnableFederatedAvatar(disableGravatar bool) bool { - v := !InstallLock - if OfflineMode { - v = false - } - if disableGravatar { - v = false - } - return v -} - func loadRepoAvatarFrom(rootCfg ConfigProvider) error { sec := rootCfg.Section("picture") diff --git a/package/gitea/source/modules/setting/repository.go b/package/gitea/source/modules/setting/repository.go index 90c4f22a..9ae64fcd 100644 --- a/package/gitea/source/modules/setting/repository.go +++ b/package/gitea/source/modules/setting/repository.go @@ -18,6 +18,12 @@ const ( RepoCreatingPublic = "public" ) +// enumerates the values for [repository.pull-request] DEFAULT_TITLE_SOURCE +const ( + RepoPRTitleSourceFirstCommit = "first-commit" + RepoPRTitleSourceAuto = "auto" +) + // ItemsPerPage maximum items per page in forks, watchers and stars of a repo const ItemsPerPage = 40 @@ -86,9 +92,10 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool - TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool DelayCheckForInactiveDays int + DefaultDeleteBranchAfterMerge bool + DefaultTitleSource string } `ini:"repository.pull-request"` // Issue Setting @@ -100,6 +107,8 @@ var ( Release struct { AllowedTypes string DefaultPagingNum int + FileMaxSize int64 + MaxFiles int64 } `ini:"repository.release"` Signing struct { @@ -208,9 +217,10 @@ var ( DefaultMergeMessageOfficialApproversOnly bool PopulateSquashCommentWithCommitMessages bool AddCoCommitterTrailers bool - TestConflictingPatchesWithGitApply bool RetargetChildrenOnMerge bool DelayCheckForInactiveDays int + DefaultDeleteBranchAfterMerge bool + DefaultTitleSource string }{ WorkInProgressPrefixes: []string{"WIP:", "[WIP]"}, // Same as GitHub. See @@ -227,6 +237,7 @@ var ( AddCoCommitterTrailers: true, RetargetChildrenOnMerge: true, DelayCheckForInactiveDays: 7, + DefaultTitleSource: RepoPRTitleSourceAuto, }, // Issue settings @@ -241,9 +252,13 @@ var ( Release: struct { AllowedTypes string DefaultPagingNum int + FileMaxSize int64 + MaxFiles int64 }{ AllowedTypes: "", DefaultPagingNum: 10, + FileMaxSize: 2048, + MaxFiles: 5, }, // Signing settings diff --git a/package/gitea/source/modules/setting/security.go b/package/gitea/source/modules/setting/security.go index 153b6bc9..152bcffd 100644 --- a/package/gitea/source/modules/setting/security.go +++ b/package/gitea/source/modules/setting/security.go @@ -14,6 +14,12 @@ import ( ) // Security settings +var Security = struct { + // TODO: move more settings to this struct in future + XFrameOptions string +}{ + XFrameOptions: "SAMEORIGIN", +} var ( InstallLock bool @@ -25,6 +31,7 @@ var ( ReverseProxyAuthEmail string ReverseProxyAuthFullName string ReverseProxyLimit int + ReverseProxyLogoutRedirect string ReverseProxyTrustedProxies []string MinPasswordLength int ImportLocalPaths bool @@ -36,8 +43,6 @@ var ( PasswordCheckPwn bool SuccessfulTokensCacheSize int DisableQueryAuthToken bool - CSRFCookieName = "_csrf" - CSRFCookieHTTPOnly = true RecordUserSignupMetadata = false TwoFactorAuthEnforced = false ) @@ -105,7 +110,6 @@ func generateSaveInternalToken(rootCfg ConfigProvider) { func loadSecurityFrom(rootCfg ConfigProvider) { sec := rootCfg.Section("security") - InstallLock = HasInstallLock(rootCfg) LogInRememberDays = sec.Key("LOGIN_REMEMBER_DAYS").MustInt(31) SecretKey = loadSecret(sec, "SECRET_KEY_URI", "SECRET_KEY") if SecretKey == "" { @@ -121,6 +125,7 @@ func loadSecurityFrom(rootCfg ConfigProvider) { ReverseProxyAuthFullName = sec.Key("REVERSE_PROXY_AUTHENTICATION_FULL_NAME").MustString("X-WEBAUTH-FULLNAME") ReverseProxyLimit = sec.Key("REVERSE_PROXY_LIMIT").MustInt(1) + ReverseProxyLogoutRedirect = sec.Key("REVERSE_PROXY_LOGOUT_REDIRECT").String() ReverseProxyTrustedProxies = sec.Key("REVERSE_PROXY_TRUSTED_PROXIES").Strings(",") if len(ReverseProxyTrustedProxies) == 0 { ReverseProxyTrustedProxies = []string{"127.0.0.0/8", "::1/128"} @@ -139,10 +144,16 @@ func loadSecurityFrom(rootCfg ConfigProvider) { log.Fatal("The provided password hash algorithm was invalid: %s", sec.Key("PASSWORD_HASH_ALGO").MustString("")) } - CSRFCookieHTTPOnly = sec.Key("CSRF_COOKIE_HTTP_ONLY").MustBool(true) PasswordCheckPwn = sec.Key("PASSWORD_CHECK_PWN").MustBool(false) SuccessfulTokensCacheSize = sec.Key("SUCCESSFUL_TOKENS_CACHE_SIZE").MustInt(20) + deprecatedSetting(rootCfg, "cors", "X_FRAME_OPTIONS", "security", "X_FRAME_OPTIONS", "v1.26.0") + if sec.HasKey("X_FRAME_OPTIONS") { + Security.XFrameOptions = sec.Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions) + } else { + Security.XFrameOptions = rootCfg.Section("cors").Key("X_FRAME_OPTIONS").MustString(Security.XFrameOptions) + } + twoFactorAuth := sec.Key("TWO_FACTOR_AUTH").String() switch twoFactorAuth { case "": diff --git a/package/gitea/source/modules/setting/server.go b/package/gitea/source/modules/setting/server.go index cedca32d..1085e052 100644 --- a/package/gitea/source/modules/setting/server.go +++ b/package/gitea/source/modules/setting/server.go @@ -15,7 +15,6 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/util" ) // Scheme describes protocol types @@ -44,6 +43,7 @@ const ( const ( PublicURLAuto = "auto" PublicURLLegacy = "legacy" + PublicURLNever = "never" ) // Server settings @@ -72,9 +72,6 @@ var ( // It maps to ini:"LOCAL_ROOT_URL" in [server] LocalURL string - // AssetVersion holds an opaque value that is used for cache-busting assets - AssetVersion string - // appTempPathInternal is the temporary path for the app, it is only an internal variable // DO NOT use it directly, always use AppDataTempDir appTempPathInternal string @@ -91,7 +88,6 @@ var ( RedirectOtherPort bool RedirectorUseProxyProtocol bool PortToRedirect string - OfflineMode bool CertFile string KeyFile string StaticRootPath string @@ -163,7 +159,7 @@ func MakeManifestData(appName, appURL, absoluteAssetURL string) []byte { } // MakeAbsoluteAssetURL returns the absolute asset url prefix without a trailing slash -func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string { +func MakeAbsoluteAssetURL(appURL *url.URL, staticURLPrefix string) string { parsedPrefix, err := url.Parse(strings.TrimSuffix(staticURLPrefix, "/")) if err != nil { log.Fatal("Unable to parse STATIC_URL_PREFIX: %v", err) @@ -171,11 +167,12 @@ func MakeAbsoluteAssetURL(appURL, staticURLPrefix string) string { if err == nil && parsedPrefix.Hostname() == "" { if staticURLPrefix == "" { - return strings.TrimSuffix(appURL, "/") + return strings.TrimSuffix(appURL.String(), "/") } // StaticURLPrefix is just a path - return util.URLJoin(appURL, strings.TrimSuffix(staticURLPrefix, "/")) + appHostURL := &url.URL{Scheme: appURL.Scheme, Host: appURL.Host} + return appHostURL.String() + "/" + strings.Trim(staticURLPrefix, "/") } return strings.TrimSuffix(staticURLPrefix, "/") @@ -286,8 +283,8 @@ func loadServerFrom(rootCfg ConfigProvider) { defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL) - PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLLegacy) - if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy { + PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto) + if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever { log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection) } @@ -316,9 +313,7 @@ func loadServerFrom(rootCfg ConfigProvider) { Domain = urlHostname } - AbsoluteAssetURL = MakeAbsoluteAssetURL(AppURL, StaticURLPrefix) - AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed) - + AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix) manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL) ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes) @@ -346,7 +341,6 @@ func loadServerFrom(rootCfg ConfigProvider) { RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false) PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80") RedirectorUseProxyProtocol = sec.Key("REDIRECTOR_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol) - OfflineMode = sec.Key("OFFLINE_MODE").MustBool(true) if len(StaticRootPath) == 0 { StaticRootPath = AppWorkPath } @@ -370,6 +364,10 @@ func loadServerFrom(rootCfg ConfigProvider) { } } + // TODO: GOLANG-HTTP-TMPDIR: Some Golang packages (like "http") use os.TempDir() to create temporary files when uploading files. + // So ideally we should set the TMPDIR environment variable to make them use our managed temp directory. + // But there is no clear place to set it currently, for example: when running "install" page, the AppDataPath is not ready yet, then AppDataTempDir won't work + EnableGzip = sec.Key("ENABLE_GZIP").MustBool() EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false) PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof")) diff --git a/package/gitea/source/modules/setting/session.go b/package/gitea/source/modules/setting/session.go index 19a05ce2..cb9b6024 100644 --- a/package/gitea/source/modules/setting/session.go +++ b/package/gitea/source/modules/setting/session.go @@ -10,6 +10,7 @@ import ( "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/util" ) // SessionConfig defines Session settings @@ -49,10 +50,8 @@ func loadSessionFrom(rootCfg ConfigProvider) { checkOverlappedPath("[session].PROVIDER_CONFIG", SessionConfig.ProviderConfig) } SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea") - SessionConfig.CookiePath = AppSubURL - if SessionConfig.CookiePath == "" { - SessionConfig.CookiePath = "/" - } + // HINT: INSTALL-PAGE-COOKIE-INIT: the cookie system is not properly initialized on the Install page, so there is no CookiePath + SessionConfig.CookiePath = util.IfZero(AppSubURL, "/") SessionConfig.Secure = sec.Key("COOKIE_SECURE").MustBool(strings.HasPrefix(strings.ToLower(AppURL), "https://")) SessionConfig.Gclifetime = sec.Key("GC_INTERVAL_TIME").MustInt64(86400) SessionConfig.Maxlifetime = sec.Key("SESSION_LIFE_TIME").MustInt64(86400) diff --git a/package/gitea/source/modules/setting/setting.go b/package/gitea/source/modules/setting/setting.go index e1499780..58ef3c86 100644 --- a/package/gitea/source/modules/setting/setting.go +++ b/package/gitea/source/modules/setting/setting.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/user" + "code.gitea.io/gitea/modules/util" ) // settings @@ -28,8 +29,10 @@ var ( CfgProvider ConfigProvider IsWindows bool - // IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing - // TODO: this is only a temporary solution, we should make the test code more reliable + // IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for: + // * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable) + // * Panic in dev or testing mode to make the problem more obvious and easier to debug + // * Mock some functions or options to make testing easier (eg: session store, time, URL detection, etc.) IsInTesting = false ) @@ -57,6 +60,10 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) { return currentUser, runUser == currentUser } +func IsInE2eTesting() bool { + return os.Getenv("GITEA_TEST_E2E") == "true" +} + // PrepareAppDataPath creates app data directory if necessary func PrepareAppDataPath() error { // FIXME: There are too many calls to MkdirAll in old code. It is incorrect. @@ -108,6 +115,9 @@ func LoadCommonSettings() { // loadCommonSettingsFrom loads common configurations from a configuration provider. func loadCommonSettingsFrom(cfg ConfigProvider) error { + // a lot of logic depends on InstallLock value, so it must be loaded before any other settings + InstallLock = HasInstallLock(cfg) + // WARNING: don't change the sequence except you know what you are doing. loadRunModeFrom(cfg) loadLogGlobalFrom(cfg) @@ -154,32 +164,38 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error { func loadRunModeFrom(rootCfg ConfigProvider) { rootSec := rootCfg.Section("") + mustNotRunAsRoot(rootSec) + + runModeValue := os.Getenv("GITEA_RUN_MODE") + runModeValue = util.IfZero(runModeValue, rootSec.Key("RUN_MODE").String()) + // non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value. + IsProd = !strings.EqualFold(runModeValue, "dev") // TODO: can use case-sensitive comparing in the future + RunMode = util.Iif(IsProd, "prod", "dev") + + // there is a separate check: mustCurrentRunUserMatch (IsRunUserMatchCurrentUser) RunUser = rootSec.Key("RUN_USER").MustString(user.CurrentUsername()) +} + +func mustNotRunAsRoot(rootSec ConfigSection) { + if os.Getuid() != 0 { + return + } + + mustRunAsRoot := os.Getenv("SNAP") != "" && os.Getenv("SNAP_NAME") != "" // snap container runs the app as uid=0 + if mustRunAsRoot { + return + } // The following is a purposefully undocumented option. Please do not run Gitea 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() - RunMode = os.Getenv("GITEA_RUN_MODE") - if RunMode == "" { - RunMode = rootSec.Key("RUN_MODE").MustString("prod") - } + allowRunAsRoot := ConfigSectionKeyBool(rootSec, "I_AM_BEING_UNSAFE_RUNNING_AS_ROOT") || // check gitea config + optional.ParseBool(os.Getenv("GITEA_I_AM_BEING_UNSAFE_RUNNING_AS_ROOT")).Value() // check gitea env var - // non-dev mode is treated as prod mode, to protect users from accidentally running in dev mode if there is a typo in this value. - RunMode = strings.ToLower(RunMode) - if RunMode != "dev" { - RunMode = "prod" - } - IsProd = RunMode != "dev" - - // check if we run as root - if os.Getuid() == 0 { - if !unsafeAllowRunAsRoot { - // Special thanks to VLC which inspired the wording of this messaging. - log.Fatal("Gitea is not supposed to be run as root. Sorry. If you need to use privileged TCP ports please instead use setcap and the `cap_net_bind_service` permission") - } - log.Critical("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.") + if !allowRunAsRoot { + // Special thanks to VLC which inspired the wording of this messaging. + log.Fatal("Gitea is not supposed to be run as root. If you need to use privileged TCP ports please instead use `setcap` and the `cap_net_bind_service` permission.") } + log.Warn("You are running Gitea using the root user, and have purposely chosen to skip built-in protections around this. You have been warned against this.") } // HasInstallLock checks the install-lock in ConfigProvider directly, because sometimes the config file is not loaded into setting variables yet. @@ -192,7 +208,7 @@ func mustCurrentRunUserMatch(rootCfg ConfigProvider) { if HasInstallLock(rootCfg) { currentUser, match := IsRunUserMatchCurrentUser(RunUser) if !match { - log.Fatal("Expect user '%s' but current user is: %s", RunUser, currentUser) + log.Fatal("Expect user '%s' (RUN_USER in app.ini) but current user is: %s", RunUser, currentUser) } } } @@ -223,7 +239,7 @@ func LoadSettings() { func LoadSettingsForInstall() { loadDBSetting(CfgProvider) loadServiceFrom(CfgProvider) - loadMailerFrom(CfgProvider) + loadMailsFrom(CfgProvider) } var configuredPaths = make(map[string]string) @@ -240,4 +256,5 @@ func PanicInDevOrTesting(msg string, a ...any) { if !IsProd || IsInTesting { panic(fmt.Sprintf(msg, a...)) } + log.Error(msg, a...) } diff --git a/package/gitea/source/modules/setting/setting_test.go b/package/gitea/source/modules/setting/setting_test.go index f77ee659..13575f52 100644 --- a/package/gitea/source/modules/setting/setting_test.go +++ b/package/gitea/source/modules/setting/setting_test.go @@ -4,6 +4,7 @@ package setting import ( + "net/url" "testing" "code.gitea.io/gitea/modules/json" @@ -12,18 +13,26 @@ import ( ) func TestMakeAbsoluteAssetURL(t *testing.T) { - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234", "https://localhost:2345")) - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234/", "https://localhost:2345")) - assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL("https://localhost:1234/", "https://localhost:2345/")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234", "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/", "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/", "/foo/")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/foo", "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/foo")) - assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/foo/")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo", "/bar")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/bar")) - assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL("https://localhost:1234/foo/", "/bar/")) + appURL1, _ := url.Parse("https://localhost:1234") + appURL2, _ := url.Parse("https://localhost:1234/") + appURLSub1, _ := url.Parse("https://localhost:1234/foo") + appURLSub2, _ := url.Parse("https://localhost:1234/foo/") + + // static URL is an absolute URL, so should be used + assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345")) + assert.Equal(t, "https://localhost:2345", MakeAbsoluteAssetURL(appURL1, "https://localhost:2345/")) + + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo")) + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL2, "/foo")) + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURL1, "/foo/")) + + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo")) + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub2, "/foo")) + assert.Equal(t, "https://localhost:1234/foo", MakeAbsoluteAssetURL(appURLSub1, "/foo/")) + + assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar")) + assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub2, "/bar")) + assert.Equal(t, "https://localhost:1234/bar", MakeAbsoluteAssetURL(appURLSub1, "/bar/")) } func TestMakeManifestData(t *testing.T) { diff --git a/package/gitea/source/modules/setting/storage.go b/package/gitea/source/modules/setting/storage.go index ee246158..9ee3f016 100644 --- a/package/gitea/source/modules/setting/storage.go +++ b/package/gitea/source/modules/setting/storage.go @@ -172,11 +172,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 @@ -202,7 +202,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/package/gitea/source/modules/setting/testenv.go b/package/gitea/source/modules/setting/testenv.go new file mode 100644 index 00000000..853521c3 --- /dev/null +++ b/package/gitea/source/modules/setting/testenv.go @@ -0,0 +1,72 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "code.gitea.io/gitea/modules/util" +) + +var giteaTestSourceRoot *string + +func GetGiteaTestSourceRoot() string { + return *giteaTestSourceRoot +} + +func SetupGiteaTestEnv() { + if giteaTestSourceRoot != nil { + return // already initialized + } + + IsInTesting = true + giteaRoot := os.Getenv("GITEA_TEST_ROOT") + if giteaRoot == "" { + _, filename, _, _ := runtime.Caller(0) + giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) + fixturesDir := filepath.Join(giteaRoot, "models", "fixtures") + if _, err := os.Stat(fixturesDir); err != nil { + panic("in gitea source code directory, fixtures directory not found: " + fixturesDir) + } + } + + appWorkPathBuiltin = giteaRoot + AppWorkPath = giteaRoot + AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "") + StaticRootPath = giteaRoot // need to load assets (options, public) from the source code directory for testing + + // giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF" + giteaConf := os.Getenv("GITEA_TEST_CONF") + if giteaConf == "" { + // By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger. + // It's easier for developers to debug bugs step by step with a debugger. + // Notice: when doing "ssh push", Gitea executes sub processes, debugger won't work for the sub processes. + giteaConf = "tests/sqlite.ini" + _, _ = fmt.Fprintf(os.Stderr, "Environment variable GITEA_TEST_CONF not set - defaulting to %s\n", giteaConf) + if !EnableSQLite3 { + _, _ = fmt.Fprintf(os.Stderr, "sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n") + os.Exit(1) + } + } + // CustomConf must be absolute path to make tests pass, + CustomConf = filepath.Join(AppWorkPath, giteaConf) + + // also unset unnecessary env vars for testing (only keep "GITEA_TEST_*" ones) + UnsetUnnecessaryEnvVars() + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GIT_") || (strings.HasPrefix(env, "GITEA_") && !strings.HasPrefix(env, "GITEA_TEST_")) { + k, _, _ := strings.Cut(env, "=") + _ = os.Unsetenv(k) + } + } + + // TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future + _ = os.Setenv("GITEA_ROOT", giteaRoot) + _ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts + giteaTestSourceRoot = &giteaRoot +} diff --git a/package/gitea/source/modules/setting/ui.go b/package/gitea/source/modules/setting/ui.go index 13cb0f5c..722341a7 100644 --- a/package/gitea/source/modules/setting/ui.go +++ b/package/gitea/source/modules/setting/ui.go @@ -25,10 +25,10 @@ var UI = struct { ReactionMaxUserNum int MaxDisplayFileSize int64 ShowUserEmail bool - DefaultShowFullName bool DefaultTheme string Themes []string FileIconTheme string + FolderIconTheme string Reactions []string ReactionsLookup container.Set[string] `ini:"-"` CustomEmojis []string @@ -42,6 +42,15 @@ var UI = struct { AmbiguousUnicodeDetection bool + // TODO: DefaultShowFullName is introduced by https://github.com/go-gitea/gitea/pull/6710 + // But there are still many edge cases: + // * Many places still use "username", not respecting this setting + // * Many places use "Full Name" if it is not empty, cause inconsistent UI for users who have set their full name but some others don't + // * Even if DefaultShowFullName=false, many places still need to show the full name + // For most cases, either "username" or "username (Full Name)" should be used and are good enough. + // Only in very few cases (e.g.: unimportant lists, narrow layout), "username" or "Full Name" can be used. + DefaultShowFullName bool + Notification struct { MinTimeout time.Duration TimeoutStep time.Duration @@ -88,6 +97,7 @@ var UI = struct { MaxDisplayFileSize: 8388608, DefaultTheme: `gitea-auto`, FileIconTheme: `material`, + FolderIconTheme: `basic`, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, diff --git a/package/gitea/source/modules/storage/azureblob.go b/package/gitea/source/modules/storage/azureblob.go index e7297cec..a273a777 100644 --- a/package/gitea/source/modules/storage/azureblob.go +++ b/package/gitea/source/modules/storage/azureblob.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "net/http" "net/url" "os" "path" @@ -246,16 +247,53 @@ func (a *AzureBlobStorage) Delete(path string) error { return convertAzureBlobErr(err) } -// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. -func (a *AzureBlobStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) { - blobClient := a.getBlobClient(path) +func (a *AzureBlobStorage) getSasURL(b *blob.Client, template sas.BlobSignatureValues) (string, error) { + urlParts, err := blob.ParseURL(b.URL()) + if err != nil { + return "", err + } - // TODO: OBJECT-STORAGE-CONTENT-TYPE: "browser inline rendering images/PDF" needs proper Content-Type header from storage - startTime := time.Now() - u, err := blobClient.GetSASURL(sas.BlobPermissions{ - Read: true, - }, time.Now().Add(5*time.Minute), &blob.GetSASURLOptions{ - StartTime: &startTime, + var t time.Time + if urlParts.Snapshot == "" { + t = time.Time{} + } else { + t, err = time.Parse(blob.SnapshotTimeFormat, urlParts.Snapshot) + if err != nil { + return "", err + } + } + + template.ContainerName = urlParts.ContainerName + template.BlobName = urlParts.BlobName + template.SnapshotTime = t + template.Version = sas.Version + + qps, err := template.SignWithSharedKey(a.credential) + if err != nil { + return "", err + } + + endpoint := b.URL() + "?" + qps.Encode() + + return endpoint, nil +} + +func (a *AzureBlobStorage) ServeDirectURL(storePath, name, method string, reqParams *ServeDirectOptions) (*url.URL, error) { + blobClient := a.getBlobClient(storePath) + + startTime := time.Now().UTC() + + param := prepareServeDirectOptions(reqParams, name) + + u, err := a.getSasURL(blobClient, sas.BlobSignatureValues{ + Permissions: (&sas.BlobPermissions{ + Read: method == http.MethodGet || method == http.MethodHead, + Write: method == http.MethodPut, + }).String(), + StartTime: startTime, + ExpiryTime: startTime.Add(5 * time.Minute), + ContentDisposition: param.ContentDisposition, + ContentType: param.ContentType, }) if err != nil { return nil, convertAzureBlobErr(err) diff --git a/package/gitea/source/modules/storage/azureblob_test.go b/package/gitea/source/modules/storage/azureblob_test.go index b3791b49..b5d5d0fe 100644 --- a/package/gitea/source/modules/storage/azureblob_test.go +++ b/package/gitea/source/modules/storage/azureblob_test.go @@ -14,12 +14,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestAzureBlobStorageIterator(t *testing.T) { +func TestAzureBlobStorage(t *testing.T) { if os.Getenv("CI") == "" { t.Skip("azureBlobStorage not present outside of CI") return } - testStorageIterator(t, setting.AzureBlobStorageType, &setting.Storage{ + storageType := setting.AzureBlobStorageType + config := &setting.Storage{ AzureBlobConfig: setting.AzureBlobStorageConfig{ // https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio-code#ip-style-url Endpoint: "http://devstoreaccount1.azurite.local:10000", @@ -28,7 +29,25 @@ func TestAzureBlobStorageIterator(t *testing.T) { AccountKey: "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==", Container: "test", }, - }) + } + table := []struct { + name string + test func(t *testing.T, typStr Type, cfg *setting.Storage) + }{ + { + name: "iterator", + test: testStorageIterator, + }, + { + name: "testBlobStorageURLContentTypeAndDisposition", + test: testBlobStorageURLContentTypeAndDisposition, + }, + } + for _, entry := range table { + t.Run(entry.name, func(t *testing.T) { + entry.test(t, storageType, config) + }) + } } func TestAzureBlobStoragePath(t *testing.T) { diff --git a/package/gitea/source/modules/storage/helper.go b/package/gitea/source/modules/storage/helper.go index f6c3d5ee..30d96a33 100644 --- a/package/gitea/source/modules/storage/helper.go +++ b/package/gitea/source/modules/storage/helper.go @@ -30,7 +30,7 @@ func (s discardStorage) Delete(_ string) error { return fmt.Errorf("%s", s) } -func (s discardStorage) URL(_, _, _ string, _ url.Values) (*url.URL, error) { +func (s discardStorage) ServeDirectURL(_, _, _ string, _ *ServeDirectOptions) (*url.URL, error) { return nil, fmt.Errorf("%s", s) } diff --git a/package/gitea/source/modules/storage/helper_test.go b/package/gitea/source/modules/storage/helper_test.go index 3cba1e13..ae64c8ac 100644 --- a/package/gitea/source/modules/storage/helper_test.go +++ b/package/gitea/source/modules/storage/helper_test.go @@ -37,7 +37,7 @@ func Test_discardStorage(t *testing.T) { assert.Error(t, err, string(tt)) } { - got, err := tt.URL("path", "name", "GET", nil) + got, err := tt.ServeDirectURL("path", "name", "GET", nil) assert.Nil(t, got) assert.Errorf(t, err, string(tt)) } diff --git a/package/gitea/source/modules/storage/local.go b/package/gitea/source/modules/storage/local.go index 8a1776f6..04e3e05f 100644 --- a/package/gitea/source/modules/storage/local.go +++ b/package/gitea/source/modules/storage/local.go @@ -5,6 +5,7 @@ package storage import ( "context" + "errors" "fmt" "io" "net/url" @@ -27,25 +28,32 @@ type LocalStorage struct { // NewLocalStorage returns a local files func NewLocalStorage(ctx context.Context, config *setting.Storage) (ObjectStorage, error) { + // prepare storage root path if !filepath.IsAbs(config.Path) { - return nil, fmt.Errorf("LocalStorageConfig.Path should have been prepared by setting/storage.go and should be an absolute path, but not: %q", config.Path) - } - log.Info("Creating new Local Storage at %s", config.Path) - if err := os.MkdirAll(config.Path, os.ModePerm); err != nil { - return nil, err + return nil, fmt.Errorf("LocalStorage config.Path should have been prepared by setting/storage.go and should be an absolute path, but not: %q", config.Path) } + storageRoot := util.FilePathJoinAbs(config.Path) - if config.TemporaryPath == "" { - config.TemporaryPath = filepath.Join(config.Path, "tmp") + // prepare storage temporary path + storageTmp := config.TemporaryPath + if storageTmp == "" { + storageTmp = filepath.Join(storageRoot, "tmp") } - if !filepath.IsAbs(config.TemporaryPath) { - return nil, fmt.Errorf("LocalStorageConfig.TemporaryPath should be an absolute path, but not: %q", config.TemporaryPath) + if !filepath.IsAbs(storageTmp) { + return nil, fmt.Errorf("LocalStorage config.TemporaryPath should be an absolute path, but not: %q", config.TemporaryPath) + } + storageTmp = util.FilePathJoinAbs(storageTmp) + + // create the storage root if not exist + log.Info("Creating new Local Storage at %s", storageRoot) + if err := os.MkdirAll(storageRoot, os.ModePerm); err != nil { + return nil, err } return &LocalStorage{ ctx: ctx, - dir: config.Path, - tmpdir: config.TemporaryPath, + dir: storageRoot, + tmpdir: storageTmp, }, nil } @@ -108,44 +116,59 @@ func (l *LocalStorage) Stat(path string) (os.FileInfo, error) { return os.Stat(l.buildLocalPath(path)) } -// Delete delete a file -func (l *LocalStorage) Delete(path string) error { - return util.Remove(l.buildLocalPath(path)) +func (l *LocalStorage) deleteEmptyParentDirs(localFullPath string) { + for parent := filepath.Dir(localFullPath); len(parent) > len(l.dir); parent = filepath.Dir(parent) { + if err := os.Remove(parent); err != nil { + // since the target file has been deleted, parent dir error is not related to the file deletion itself. + break + } + } } -// URL gets the redirect URL to a file -func (l *LocalStorage) URL(path, name, _ string, reqParams url.Values) (*url.URL, error) { +// Delete deletes the file in storage and removes the empty parent directories (if possible) +func (l *LocalStorage) Delete(path string) error { + localFullPath := l.buildLocalPath(path) + err := util.Remove(localFullPath) + l.deleteEmptyParentDirs(localFullPath) + return err +} + +func (l *LocalStorage) ServeDirectURL(path, name, _ string, reqParams *ServeDirectOptions) (*url.URL, error) { return nil, ErrURLNotSupported } +func (l *LocalStorage) normalizeWalkError(err error) error { + if errors.Is(err, os.ErrNotExist) { + // ignore it because the file may be deleted during the walk, and we don't care about it + return nil + } + return err +} + // IterateObjects iterates across the objects in the local storage func (l *LocalStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error { dir := l.buildLocalPath(dirName) - return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { - if err != nil { + return filepath.WalkDir(dir, func(path string, d os.DirEntry, errWalk error) error { + if err := l.ctx.Err(); err != nil { return err } - select { - case <-l.ctx.Done(): - return l.ctx.Err() - default: + if errWalk != nil { + return l.normalizeWalkError(errWalk) } - if path == l.dir { - return nil - } - if d.IsDir() { + if path == l.dir || d.IsDir() { return nil } + relPath, err := filepath.Rel(l.dir, path) if err != nil { - return err + return l.normalizeWalkError(err) } obj, err := os.Open(path) if err != nil { - return err + return l.normalizeWalkError(err) } defer obj.Close() - return fn(relPath, obj) + return fn(filepath.ToSlash(relPath), obj) }) } diff --git a/package/gitea/source/modules/storage/local_test.go b/package/gitea/source/modules/storage/local_test.go index 0592fd71..29c13539 100644 --- a/package/gitea/source/modules/storage/local_test.go +++ b/package/gitea/source/modules/storage/local_test.go @@ -4,11 +4,14 @@ package storage import ( + "os" + "strings" "testing" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBuildLocalPath(t *testing.T) { @@ -53,6 +56,49 @@ func TestBuildLocalPath(t *testing.T) { } } +func TestLocalStorageDelete(t *testing.T) { + rootDir := t.TempDir() + st, err := NewLocalStorage(t.Context(), &setting.Storage{Path: rootDir}) + require.NoError(t, err) + + assertExists := func(t *testing.T, path string, exists bool) { + _, err = os.Stat(rootDir + "/" + path) + if exists { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, os.ErrNotExist) + } + } + + _, err = st.Save("dir/sub1/1-a.txt", strings.NewReader(""), -1) + require.NoError(t, err) + _, err = st.Save("dir/sub1/1-b.txt", strings.NewReader(""), -1) + require.NoError(t, err) + _, err = st.Save("dir/sub2/2-a.txt", strings.NewReader(""), -1) + require.NoError(t, err) + + assertExists(t, "dir/sub1/1-a.txt", true) + assertExists(t, "dir/sub1/1-b.txt", true) + assertExists(t, "dir/sub2/2-a.txt", true) + + require.NoError(t, st.Delete("dir/sub1/1-a.txt")) + assertExists(t, "dir/sub1", true) + assertExists(t, "dir/sub1/1-a.txt", false) + assertExists(t, "dir/sub1/1-b.txt", true) + assertExists(t, "dir/sub2/2-a.txt", true) + + require.NoError(t, st.Delete("dir/sub1/1-b.txt")) + assertExists(t, ".", true) + assertExists(t, "dir/sub1", false) + assertExists(t, "dir/sub1/1-a.txt", false) + assertExists(t, "dir/sub1/1-b.txt", false) + assertExists(t, "dir/sub2/2-a.txt", true) + + require.NoError(t, st.Delete("dir/sub2/2-a.txt")) + assertExists(t, ".", true) + assertExists(t, "dir", false) +} + func TestLocalStorageIterator(t *testing.T) { testStorageIterator(t, setting.LocalStorageType, &setting.Storage{Path: t.TempDir()}) } diff --git a/package/gitea/source/modules/storage/minio.go b/package/gitea/source/modules/storage/minio.go index 6993ac2d..ace78bb6 100644 --- a/package/gitea/source/modules/storage/minio.go +++ b/package/gitea/source/modules/storage/minio.go @@ -23,11 +23,7 @@ import ( "github.com/minio/minio-go/v7/pkg/credentials" ) -var ( - _ ObjectStorage = &MinioStorage{} - - quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") -) +var _ ObjectStorage = &MinioStorage{} type minioObject struct { *minio.Object @@ -278,37 +274,16 @@ func (m *MinioStorage) Delete(path string) error { return convertMinioErr(err) } -// URL gets the redirect URL to a file. The presigned link is valid for 5 minutes. -func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams url.Values) (*url.URL, error) { - // copy serveDirectReqParams - reqParams, err := url.ParseQuery(serveDirectReqParams.Encode()) - if err != nil { - return nil, err - } +func (m *MinioStorage) ServeDirectURL(storePath, name, method string, opt *ServeDirectOptions) (*url.URL, error) { + reqParams := url.Values{} - // Here we might not know the real filename, and it's quite inefficient to detect the mine type by pre-fetching the object head. - // So we just do a quick detection by extension name, at least if works for the "View Raw File" for an LFS file on the Web UI. - // Detect content type by extension name, only support the well-known safe types for inline rendering. - // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future - ext := path.Ext(name) - inlineExtMimeTypes := map[string]string{ - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".webp": "image/webp", - ".avif": "image/avif", - // ATTENTION! Don't support unsafe types like HTML/SVG due to security concerns: they can contain JS code, and maybe they need proper Content-Security-Policy - // HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context, it seems fine to render it inline - ".pdf": "application/pdf", - - // TODO: refactor with "modules/public/mime_types.go", for example: "DetectWellKnownSafeInlineMimeType" + param := prepareServeDirectOptions(opt, name) + // minio does not ignore empty params + if param.ContentType != "" { + reqParams.Set("response-content-type", param.ContentType) } - if mimeType, ok := inlineExtMimeTypes[ext]; ok { - reqParams.Set("response-content-type", mimeType) - reqParams.Set("response-content-disposition", "inline") - } else { - reqParams.Set("response-content-disposition", fmt.Sprintf(`attachment; filename="%s"`, quoteEscaper.Replace(name))) + if param.ContentDisposition != "" { + reqParams.Set("response-content-disposition", param.ContentDisposition) } expires := 5 * time.Minute @@ -323,6 +298,7 @@ func (m *MinioStorage) URL(storePath, name, method string, serveDirectReqParams // IterateObjects iterates across the objects in the miniostorage func (m *MinioStorage) IterateObjects(dirName string, fn func(path string, obj Object) error) error { opts := minio.GetObjectOptions{} + // FIXME: this loop is not right and causes resource leaking, see the comment of ListObjects for mObjInfo := range m.client.ListObjects(m.ctx, m.bucket, minio.ListObjectsOptions{ Prefix: m.buildMinioDirPrefix(dirName), Recursive: true, diff --git a/package/gitea/source/modules/storage/minio_test.go b/package/gitea/source/modules/storage/minio_test.go index 2726d765..5c15ee1e 100644 --- a/package/gitea/source/modules/storage/minio_test.go +++ b/package/gitea/source/modules/storage/minio_test.go @@ -16,12 +16,13 @@ import ( "github.com/stretchr/testify/assert" ) -func TestMinioStorageIterator(t *testing.T) { +func TestMinioStorage(t *testing.T) { if os.Getenv("CI") == "" { t.Skip("minioStorage not present outside of CI") return } - testStorageIterator(t, setting.MinioStorageType, &setting.Storage{ + storageType := setting.MinioStorageType + config := &setting.Storage{ MinioConfig: setting.MinioStorageConfig{ Endpoint: "minio:9000", AccessKeyID: "123456", @@ -29,7 +30,25 @@ func TestMinioStorageIterator(t *testing.T) { Bucket: "gitea", Location: "us-east-1", }, - }) + } + table := []struct { + name string + test func(t *testing.T, typStr Type, cfg *setting.Storage) + }{ + { + name: "iterator", + test: testStorageIterator, + }, + { + name: "testBlobStorageURLContentTypeAndDisposition", + test: testBlobStorageURLContentTypeAndDisposition, + }, + } + for _, entry := range table { + t.Run(entry.name, func(t *testing.T) { + entry.test(t, storageType, config) + }) + } } func TestMinioStoragePath(t *testing.T) { diff --git a/package/gitea/source/modules/storage/storage.go b/package/gitea/source/modules/storage/storage.go index 1868817c..e19c421b 100644 --- a/package/gitea/source/modules/storage/storage.go +++ b/package/gitea/source/modules/storage/storage.go @@ -10,8 +10,11 @@ import ( "io" "net/url" "os" + "path" + "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" ) @@ -56,6 +59,37 @@ type Object interface { Stat() (os.FileInfo, error) } +// ServeDirectOptions customizes HTTP headers for a generated signed URL. +type ServeDirectOptions struct { + // Overrides the automatically detected MIME type. + ContentType string +} + +// Safe defaults are applied only when not explicitly overridden by the caller. +func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret struct { + ContentType string + ContentDisposition string +}, +) { + // Here we might not know the real filename, and it's quite inefficient to detect the MIME type by pre-fetching the object head. + // So we just do a quick detection by extension name, at least it works for the "View Raw File" for an LFS file on the Web UI. + // TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future + + if optsOptional != nil { + ret.ContentType = optsOptional.ContentType + } + name = path.Base(name) + if ret.ContentType == "" { + ext := path.Ext(name) + ret.ContentType = public.DetectWellKnownMimeType(ext) + } + // When using ServeDirect, the URL is from the object storage's web server, + // it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly. + // If a browser doesn't support the content type to be displayed inline, browser will download with the filename. + ret.ContentDisposition = httplib.EncodeContentDispositionInline(name) + return ret +} + // ObjectStorage represents an object storage to handle a bucket and files type ObjectStorage interface { Open(path string) (Object, error) @@ -67,8 +101,21 @@ type ObjectStorage interface { Stat(path string) (os.FileInfo, error) Delete(path string) error - URL(path, name, method string, reqParams url.Values) (*url.URL, error) - IterateObjects(path string, iterator func(path string, obj Object) error) error + + // ServeDirectURL generates a "serve-direct" URL for the specified blob storage file, + // end user (browser) will use this URL to access the file directly from the object storage, bypassing Gitea server. + // Usually the link is time-limited (a few minutes) and contains a signature to ensure security. + // The generated URL must NOT use the same origin as Gitea server, otherwise it will cause security issues. + // * method defines which HTTP method is permitted for certain storage providers (e.g., MinIO). + // * opt allows customizing the Content-Type and Content-Disposition headers. + // TODO: need to merge "ServeDirect()" check into this function, avoid duplicate code and potential inconsistency. + ServeDirectURL(path, name, method string, opt *ServeDirectOptions) (*url.URL, error) + + // IterateObjects calls the iterator function for each object in the storage with the given path as prefix + // The "fullPath" argument in callback is the full path in this storage. + // * IterateObjects("", ...): iterate all objects in this storage + // * IterateObjects("sub-path", ...): iterate all objects with "sub-path" as prefix in this storage, the "fullPath" will be like "sub-path/xxx" + IterateObjects(basePath string, iterator func(fullPath string, obj Object) error) error } // Copy copies a file from source ObjectStorage to dest ObjectStorage @@ -131,7 +178,7 @@ var ( // Actions represents actions storage Actions ObjectStorage = uninitializedStorage - // Actions Artifacts represents actions artifacts storage + // ActionsArtifacts Artifacts represents actions artifacts storage ActionsArtifacts ObjectStorage = uninitializedStorage ) diff --git a/package/gitea/source/modules/storage/storage_test.go b/package/gitea/source/modules/storage/storage_test.go index 08f274e7..83ee2ef7 100644 --- a/package/gitea/source/modules/storage/storage_test.go +++ b/package/gitea/source/modules/storage/storage_test.go @@ -4,12 +4,14 @@ package storage import ( + "net/http" "strings" "testing" "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { @@ -50,3 +52,53 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) { assert.Len(t, expected, count) } } + +type expectedServeDirectHeaders struct { + ContentType string + ContentDisposition string +} + +func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected expectedServeDirectHeaders, reqParams *ServeDirectOptions) { + u, err := s.ServeDirectURL(path, name, http.MethodGet, reqParams) + require.NoError(t, err) + resp, err := http.Get(u.String()) + require.NoError(t, err) + defer resp.Body.Close() + if expected.ContentType != "" { + assert.Equal(t, expected.ContentType, resp.Header.Get("Content-Type")) + } + if expected.ContentDisposition != "" { + assert.Equal(t, expected.ContentDisposition, resp.Header.Get("Content-Disposition")) + } +} + +func testBlobStorageURLContentTypeAndDisposition(t *testing.T, typStr Type, cfg *setting.Storage) { + s, err := NewStorage(typStr, cfg) + assert.NoError(t, err) + + testFilename := "test.txt" + _, err = s.Save(testFilename, strings.NewReader("dummy-content"), -1) + assert.NoError(t, err) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.txt", expectedServeDirectHeaders{ + ContentType: "text/plain; charset=utf-8", + ContentDisposition: `inline; filename=test.txt`, + }, nil) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.pdf", expectedServeDirectHeaders{ + ContentType: "application/pdf", + ContentDisposition: `inline; filename=test.pdf`, + }, nil) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentDisposition: `inline; filename=test.wasm`, + }, nil) + + testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{ + ContentType: "application/wasm", + ContentDisposition: `inline; filename=test.wasm`, + }, &ServeDirectOptions{ + ContentType: "application/wasm", + }) + assert.NoError(t, s.Delete(testFilename)) +} diff --git a/package/gitea/source/modules/structs/activity.go b/package/gitea/source/modules/structs/activity.go index 90854955..b896adfe 100644 --- a/package/gitea/source/modules/structs/activity.go +++ b/package/gitea/source/modules/structs/activity.go @@ -12,7 +12,7 @@ type Activity struct { UserID int64 `json:"user_id"` // Receiver user // the type of action // - // enum: create_repo,rename_repo,star_repo,watch_repo,commit_repo,create_issue,create_pull_request,transfer_repo,push_tag,comment_issue,merge_pull_request,close_issue,reopen_issue,close_pull_request,reopen_pull_request,delete_tag,delete_branch,mirror_sync_push,mirror_sync_create,mirror_sync_delete,approve_pull_request,reject_pull_request,comment_pull,publish_release,pull_review_dismissed,pull_request_ready_for_review,auto_merge_pull_request + // enum: ["create_repo","rename_repo","star_repo","watch_repo","commit_repo","create_issue","create_pull_request","transfer_repo","push_tag","comment_issue","merge_pull_request","close_issue","reopen_issue","close_pull_request","reopen_pull_request","delete_tag","delete_branch","mirror_sync_push","mirror_sync_create","mirror_sync_delete","approve_pull_request","reject_pull_request","comment_pull","publish_release","pull_review_dismissed","pull_request_ready_for_review","auto_merge_pull_request"] OpType string `json:"op_type"` // The ID of the user who performed the action ActUserID int64 `json:"act_user_id"` diff --git a/package/gitea/source/modules/structs/activitypub.go b/package/gitea/source/modules/structs/activitypub.go deleted file mode 100644 index 39a6c1ac..00000000 --- a/package/gitea/source/modules/structs/activitypub.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package structs - -// ActivityPub type -type ActivityPub struct { - // Context defines the JSON-LD context for ActivityPub - Context string `json:"@context"` -} diff --git a/package/gitea/source/modules/structs/hook.go b/package/gitea/source/modules/structs/hook.go index 57af3846..99c15351 100644 --- a/package/gitea/source/modules/structs/hook.go +++ b/package/gitea/source/modules/structs/hook.go @@ -19,6 +19,8 @@ var ErrInvalidReceiveHook = errors.New("Invalid JSON payload received over webho type Hook struct { // The unique identifier of the webhook ID int64 `json:"id"` + // Optional human-readable name for the webhook + Name string `json:"name"` // The type of the webhook (e.g., gitea, slack, discord) Type string `json:"type"` // Branch filter pattern to determine which branches trigger the webhook @@ -51,7 +53,7 @@ type CreateHookOptionConfig map[string]string // CreateHookOption options when create a hook type CreateHookOption struct { // required: true - // enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist + // enum: ["dingtalk","discord","gitea","gogs","msteams","slack","telegram","feishu","wechatwork","packagist"] // The type of the webhook to create Type string `json:"type" binding:"Required"` // required: true @@ -66,6 +68,8 @@ type CreateHookOption struct { // default: false // Whether the webhook should be active upon creation Active bool `json:"active"` + // Optional human-readable name for the webhook + Name string `json:"name" binding:"MaxSize(255)"` } // EditHookOption options when modify one hook @@ -80,6 +84,8 @@ type EditHookOption struct { AuthorizationHeader string `json:"authorization_header"` // Whether the webhook is active and will be triggered Active *bool `json:"active"` + // Optional human-readable name + Name *string `json:"name,omitzero" binding:"MaxSize(255)"` } // Payloader payload is some part of one hook diff --git a/package/gitea/source/modules/structs/issue.go b/package/gitea/source/modules/structs/issue.go index 2540481d..fd29727a 100644 --- a/package/gitea/source/modules/structs/issue.go +++ b/package/gitea/source/modules/structs/issue.go @@ -14,6 +14,8 @@ import ( ) // StateType issue state type +// +// swagger:enum StateType type StateType string const ( @@ -21,10 +23,11 @@ const ( StateOpen StateType = "open" // StateClosed pr is closed StateClosed StateType = "closed" - // StateAll is all - StateAll StateType = "all" ) +// StateAll is a query parameter filter value, not a valid object state. +const StateAll = "all" + // PullRequestMeta PR info if an issue is a PR type PullRequestMeta struct { HasMerged bool `json:"merged"` @@ -58,15 +61,11 @@ type Issue struct { Labels []*Label `json:"labels"` Milestone *Milestone `json:"milestone"` // deprecated - Assignee *User `json:"assignee"` - Assignees []*User `json:"assignees"` - // Whether the issue is open or closed - // - // type: string - // enum: open,closed - State StateType `json:"state"` - IsLocked bool `json:"is_locked"` - Comments int `json:"comments"` + Assignee *User `json:"assignee"` + Assignees []*User `json:"assignees"` + State StateType `json:"state"` + IsLocked bool `json:"is_locked"` + Comments int `json:"comments"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time @@ -82,6 +81,8 @@ type Issue struct { Repo *RepositoryMeta `json:"repository"` PinOrder int `json:"pin_order"` + // The version of the issue content for optimistic locking + ContentVersion int `json:"content_version"` } // CreateIssueOption options to create one issue @@ -115,6 +116,8 @@ type EditIssueOption struct { // swagger:strfmt date-time Deadline *time.Time `json:"due_date"` RemoveDeadline *bool `json:"unset_due_date"` + // The current version of the issue content to detect conflicts during editing + ContentVersion *int `json:"content_version"` } // EditDeadlineOption options for creating a deadline @@ -132,6 +135,8 @@ type IssueDeadline struct { } // IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes" +// +// swagger:enum IssueFormFieldType type IssueFormFieldType string const ( @@ -168,7 +173,8 @@ func (iff IssueFormField) VisibleInContent() bool { } // IssueFormFieldVisible defines issue form field visible -// swagger:model +// +// swagger:enum IssueFormFieldVisible type IssueFormFieldVisible string const ( diff --git a/package/gitea/source/modules/structs/issue_milestone.go b/package/gitea/source/modules/structs/issue_milestone.go index 226c613d..dd8bdc6c 100644 --- a/package/gitea/source/modules/structs/issue_milestone.go +++ b/package/gitea/source/modules/structs/issue_milestone.go @@ -40,7 +40,7 @@ type CreateMilestoneOption struct { // swagger:strfmt date-time // Deadline is the due date for the milestone Deadline *time.Time `json:"due_on"` - // enum: open,closed + // enum: ["open","closed"] // State indicates the initial state of the milestone State string `json:"state"` } @@ -52,6 +52,7 @@ type EditMilestoneOption struct { // Description provides updated details about the milestone Description *string `json:"description"` // State indicates the updated state of the milestone + // enum: ["open","closed"] State *string `json:"state"` // Deadline is the updated due date for the milestone Deadline *time.Time `json:"due_on"` diff --git a/package/gitea/source/modules/structs/notifications.go b/package/gitea/source/modules/structs/notifications.go index cee5da66..d7aa0783 100644 --- a/package/gitea/source/modules/structs/notifications.go +++ b/package/gitea/source/modules/structs/notifications.go @@ -40,7 +40,7 @@ type NotificationSubject struct { // Type indicates the type of the notification subject Type NotifySubjectType `json:"type" binding:"In(Issue,Pull,Commit,Repository)"` // State indicates the current state of the notification subject - State StateType `json:"state"` + State NotifySubjectStateType `json:"state"` } // NotificationCount number of unread notifications @@ -49,7 +49,22 @@ type NotificationCount struct { New int64 `json:"new"` } +// NotifySubjectStateType represents the state of a notification subject +// swagger:enum NotifySubjectStateType +type NotifySubjectStateType string + +const ( + // NotifySubjectStateOpen is an open subject + NotifySubjectStateOpen NotifySubjectStateType = "open" + // NotifySubjectStateClosed is a closed subject + NotifySubjectStateClosed NotifySubjectStateType = "closed" + // NotifySubjectStateMerged is a merged pull request + NotifySubjectStateMerged NotifySubjectStateType = "merged" +) + // NotifySubjectType represent type of notification subject +// +// swagger:enum NotifySubjectType type NotifySubjectType string const ( diff --git a/package/gitea/source/modules/structs/org.go b/package/gitea/source/modules/structs/org.go index c3d70ebf..723689cb 100644 --- a/package/gitea/source/modules/structs/org.go +++ b/package/gitea/source/modules/structs/org.go @@ -60,29 +60,27 @@ type CreateOrgOption struct { // The location of the organization Location string `json:"location" binding:"MaxSize(50)"` // possible values are `public` (default), `limited` or `private` - // enum: public,limited,private + // enum: ["public","limited","private"] Visibility string `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` } -// TODO: make EditOrgOption fields optional after https://gitea.com/go-chi/binding/pulls/5 got merged - // EditOrgOption options for editing an organization type EditOrgOption struct { // The full display name of the organization - FullName string `json:"full_name" binding:"MaxSize(100)"` - // The email address of the organization - Email string `json:"email" binding:"MaxSize(255)"` + FullName *string `json:"full_name" binding:"MaxSize(100)"` + // The email address of the organization; use empty string to clear + Email *string `json:"email" binding:"MaxSize(255)"` // The description of the organization - Description string `json:"description" binding:"MaxSize(255)"` + Description *string `json:"description" binding:"MaxSize(255)"` // The website URL of the organization - Website string `json:"website" binding:"ValidUrl;MaxSize(255)"` + Website *string `json:"website" binding:"ValidUrl;MaxSize(255)"` // The location of the organization - Location string `json:"location" binding:"MaxSize(50)"` + Location *string `json:"location" binding:"MaxSize(50)"` // possible values are `public`, `limited` or `private` - // enum: public,limited,private - Visibility string `json:"visibility" binding:"In(,public,limited,private)"` + // enum: ["public","limited","private"] + Visibility *string `json:"visibility" binding:"In(,public,limited,private)"` // Whether repository administrators can change team access RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"` } diff --git a/package/gitea/source/modules/structs/org_team.go b/package/gitea/source/modules/structs/org_team.go index d34de5b6..f730a568 100644 --- a/package/gitea/source/modules/structs/org_team.go +++ b/package/gitea/source/modules/structs/org_team.go @@ -16,7 +16,7 @@ type Team struct { Organization *Organization `json:"organization"` // Whether the team has access to all repositories in the organization IncludesAllRepositories bool `json:"includes_all_repositories"` - // enum: none,read,write,admin,owner + // enum: ["none","read","write","admin","owner"] Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. @@ -35,7 +35,7 @@ type CreateTeamOption struct { Description string `json:"description" binding:"MaxSize(255)"` // Whether the team has access to all repositories in the organization IncludesAllRepositories bool `json:"includes_all_repositories"` - // enum: read,write,admin + // enum: ["read","write","admin"] Permission string `json:"permission"` // example: ["repo.actions","repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.ext_wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. @@ -54,7 +54,7 @@ type EditTeamOption struct { Description *string `json:"description" binding:"MaxSize(255)"` // Whether the team has access to all repositories in the organization IncludesAllRepositories *bool `json:"includes_all_repositories"` - // enum: read,write,admin + // enum: ["read","write","admin"] Permission string `json:"permission"` // example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"] // Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions. diff --git a/package/gitea/source/modules/structs/pull.go b/package/gitea/source/modules/structs/pull.go index 7cc58217..cd2ffbe7 100644 --- a/package/gitea/source/modules/structs/pull.go +++ b/package/gitea/source/modules/structs/pull.go @@ -91,6 +91,8 @@ type PullRequest struct { // The pin order for the pull request PinOrder int `json:"pin_order"` + // The version of the pull request content for optimistic locking + ContentVersion int `json:"content_version"` } // PRBranchInfo information about a branch @@ -140,6 +142,8 @@ type CreatePullRequestOption struct { Reviewers []string `json:"reviewers"` // The list of team reviewer names TeamReviewers []string `json:"team_reviewers"` + // Whether maintainers can edit the pull request + AllowMaintainerEdit *bool `json:"allow_maintainer_edit"` } // EditPullRequestOption options when modify pull request @@ -166,6 +170,8 @@ type EditPullRequestOption struct { RemoveDeadline *bool `json:"unset_due_date"` // Whether to allow maintainer edits AllowMaintainerEdit *bool `json:"allow_maintainer_edit"` + // The current version of the pull request content to detect conflicts during editing + ContentVersion *int `json:"content_version"` } // ChangedFile store information about files affected by the pull request diff --git a/package/gitea/source/modules/structs/pull_review.go b/package/gitea/source/modules/structs/pull_review.go index e93e4e97..de0677ef 100644 --- a/package/gitea/source/modules/structs/pull_review.go +++ b/package/gitea/source/modules/structs/pull_review.go @@ -8,6 +8,8 @@ import ( ) // ReviewStateType review state type +// +// swagger:enum ReviewStateType type ReviewStateType string const ( @@ -21,10 +23,11 @@ const ( ReviewStateRequestChanges ReviewStateType = "REQUEST_CHANGES" // ReviewStateRequestReview review is requested from user ReviewStateRequestReview ReviewStateType = "REQUEST_REVIEW" - // ReviewStateUnknown state of pr is unknown - ReviewStateUnknown ReviewStateType = "" ) +// ReviewStateUnknown is an internal sentinel for unknown review state, not a valid API value. +const ReviewStateUnknown = "" + // PullReview represents a pull request review type PullReview struct { ID int64 `json:"id"` @@ -72,7 +75,7 @@ type PullReviewComment struct { HTMLPullURL string `json:"pull_request_url"` } -// CreatePullReviewOptions are options to create a pull review +// CreatePullReviewOptions are options to create a pull request review type CreatePullReviewOptions struct { Event ReviewStateType `json:"event"` Body string `json:"body"` @@ -91,19 +94,19 @@ type CreatePullReviewComment struct { NewLineNum int64 `json:"new_position"` } -// SubmitPullReviewOptions are options to submit a pending pull review +// SubmitPullReviewOptions are options to submit a pending pull request review type SubmitPullReviewOptions struct { Event ReviewStateType `json:"event"` Body string `json:"body"` } -// DismissPullReviewOptions are options to dismiss a pull review +// DismissPullReviewOptions are options to dismiss a pull request review type DismissPullReviewOptions struct { Message string `json:"message"` Priors bool `json:"priors"` } -// PullReviewRequestOptions are options to add or remove pull review requests +// PullReviewRequestOptions are options to add or remove pull request review requests type PullReviewRequestOptions struct { Reviewers []string `json:"reviewers"` TeamReviewers []string `json:"team_reviewers"` diff --git a/package/gitea/source/modules/structs/repo.go b/package/gitea/source/modules/structs/repo.go index c1c85837..7cd64fd7 100644 --- a/package/gitea/source/modules/structs/repo.go +++ b/package/gitea/source/modules/structs/repo.go @@ -58,26 +58,28 @@ type Repository struct { Fork bool `json:"fork"` Template bool `json:"template"` // the original repository if this repository is a fork, otherwise null - Parent *Repository `json:"parent,omitempty"` - Mirror bool `json:"mirror"` - Size int `json:"size"` - Language string `json:"language"` - LanguagesURL string `json:"languages_url"` - HTMLURL string `json:"html_url"` - URL string `json:"url"` - Link string `json:"link"` - SSHURL string `json:"ssh_url"` - CloneURL string `json:"clone_url"` - OriginalURL string `json:"original_url"` - Website string `json:"website"` - Stars int `json:"stars_count"` - Forks int `json:"forks_count"` - Watchers int `json:"watchers_count"` - OpenIssues int `json:"open_issues_count"` - OpenPulls int `json:"open_pr_counter"` - Releases int `json:"release_counter"` - DefaultBranch string `json:"default_branch"` - Archived bool `json:"archived"` + Parent *Repository `json:"parent,omitempty"` + Mirror bool `json:"mirror"` + Size int `json:"size"` + Language string `json:"language"` + LanguagesURL string `json:"languages_url"` + HTMLURL string `json:"html_url"` + URL string `json:"url"` + Link string `json:"link"` + SSHURL string `json:"ssh_url"` + CloneURL string `json:"clone_url"` + OriginalURL string `json:"original_url"` + Website string `json:"website"` + Stars int `json:"stars_count"` + Forks int `json:"forks_count"` + Watchers int `json:"watchers_count"` + BranchCount int `json:"branch_count"` + OpenIssues int `json:"open_issues_count"` + OpenPulls int `json:"open_pr_counter"` + Releases int `json:"release_counter"` + DefaultBranch string `json:"default_branch"` + DefaultTargetBranch string `json:"default_target_branch,omitempty"` + Archived bool `json:"archived"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time @@ -112,7 +114,7 @@ type Repository struct { Internal bool `json:"internal"` MirrorInterval string `json:"mirror_interval"` // ObjectFormatName of the underlying git repository - // enum: sha1,sha256 + // enum: ["sha1","sha256"] ObjectFormatName string `json:"object_format_name"` // swagger:strfmt date-time MirrorUpdated time.Time `json:"mirror_updated"` @@ -134,7 +136,7 @@ type CreateRepoOption struct { // Whether the repository is private Private bool `json:"private"` // Label-Set to use - IssueLabels string `json:"issue_labels"` + IssueLabels string `json:"issue_labels" binding:"MaxSize(255)"` // Whether the repository should be auto-initialized? AutoInit bool `json:"auto_init"` // Whether the repository is template @@ -142,16 +144,16 @@ type CreateRepoOption struct { // Gitignores to use Gitignores string `json:"gitignores"` // License to use - License string `json:"license"` + License string `json:"license" binding:"MaxSize(100)"` // Readme of the repository to create - Readme string `json:"readme"` + Readme string `json:"readme" binding:"MaxSize(255)"` // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` // TrustModel of the repository - // enum: default,collaborator,committer,collaboratorcommitter + // enum: ["default","collaborator","committer","collaboratorcommitter"] TrustModel string `json:"trust_model"` - // ObjectFormatName of the underlying git repository - // enum: sha1,sha256 + // ObjectFormatName of the underlying git repository, empty string for default (sha1) + // enum: ["sha1","sha256"] ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"` } @@ -292,6 +294,21 @@ type RenameBranchRepoOption struct { Name string `json:"name" binding:"Required;GitRefName;MaxSize(100)"` } +// UpdateBranchRepoOption options when updating a branch reference in a repository +// swagger:model +type UpdateBranchRepoOption struct { + // New commit SHA (or any ref) the branch should point to + // + // required: true + NewCommitID string `json:"new_commit_id" binding:"Required"` + + // Expected old commit SHA of the branch; if provided it must match the current tip + OldCommitID string `json:"old_commit_id"` + + // Force update even if the change is not a fast-forward + Force bool `json:"force"` +} + // TransferRepoOption options when transfer a repository's ownership // swagger:model type TransferRepoOption struct { @@ -361,7 +378,7 @@ type MigrateRepoOptions struct { // required: true RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` - // enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase,codecommit + // enum: ["git","github","gitea","gitlab","gogs","onedev","gitbucket","codebase","codecommit"] Service string `json:"service"` AuthUsername string `json:"auth_username"` AuthPassword string `json:"auth_password"` diff --git a/package/gitea/source/modules/structs/repo_actions.go b/package/gitea/source/modules/structs/repo_actions.go index b491d6cc..92ca9bcc 100644 --- a/package/gitea/source/modules/structs/repo_actions.go +++ b/package/gitea/source/modules/structs/repo_actions.go @@ -196,12 +196,27 @@ type ActionRunner struct { Name string `json:"name"` Status string `json:"status"` Busy bool `json:"busy"` + Disabled bool `json:"disabled"` Ephemeral bool `json:"ephemeral"` Labels []*ActionRunnerLabel `json:"labels"` } +// EditActionRunnerOption represents the editable fields for a runner. +// swagger:model +type EditActionRunnerOption struct { + // required: true + Disabled *bool `json:"disabled"` +} + // ActionRunnersResponse returns Runners type ActionRunnersResponse struct { Entries []*ActionRunner `json:"runners"` TotalCount int64 `json:"total_count"` } + +// RunDetails returns workflow_dispatch runid and url +type RunDetails struct { + WorkflowRunID int64 `json:"workflow_run_id"` + RunURL string `json:"run_url"` + HTMLURL string `json:"html_url"` +} diff --git a/package/gitea/source/modules/structs/repo_collaborator.go b/package/gitea/source/modules/structs/repo_collaborator.go index 9ede7f07..6b315df4 100644 --- a/package/gitea/source/modules/structs/repo_collaborator.go +++ b/package/gitea/source/modules/structs/repo_collaborator.go @@ -5,7 +5,7 @@ package structs // AddCollaboratorOption options when adding a user as a collaborator of a repository type AddCollaboratorOption struct { - // enum: read,write,admin + // enum: ["read","write","admin"] // Permission level to grant the collaborator Permission *string `json:"permission"` } diff --git a/package/gitea/source/modules/structs/repo_file.go b/package/gitea/source/modules/structs/repo_file.go index 99efe19e..53ce5aea 100644 --- a/package/gitea/source/modules/structs/repo_file.go +++ b/package/gitea/source/modules/structs/repo_file.go @@ -8,12 +8,14 @@ import "time" // FileOptions options for all file APIs type FileOptions struct { - // message (optional) for the commit of this file. if not supplied, a default message will be used + // message (optional) is the commit message of the changes. If not supplied, a default message will be used Message string `json:"message"` - // branch (optional) to base this file from. if not given, the default branch is used + // branch (optional) is the base branch for the changes. If not supplied, the default branch is used BranchName string `json:"branch" binding:"GitRefName;MaxSize(100)"` - // new_branch (optional) will make a new branch from `branch` before creating the file + // new_branch (optional) will make a new branch from base branch for the changes. If not supplied, the changes will be committed to the base branch NewBranchName string `json:"new_branch" binding:"GitRefName;MaxSize(100)"` + // force_push (optional) will do a force-push if the new branch already exists + ForcePush bool `json:"force_push"` // `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) Author Identity `json:"author"` Committer Identity `json:"committer"` @@ -22,13 +24,6 @@ type FileOptions struct { Signoff bool `json:"signoff"` } -type FileOptionsWithSHA struct { - FileOptions - // the blob ID (SHA) for the file that already exists, it is required for changing existing files - // required: true - SHA string `json:"sha" binding:"Required"` -} - func (f *FileOptions) GetFileOptions() *FileOptions { return f } @@ -39,7 +34,7 @@ type FileOptionsInterface interface { var _ FileOptionsInterface = (*FileOptions)(nil) -// CreateFileOptions options for creating files +// CreateFileOptions options for creating a file // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type CreateFileOptions struct { FileOptions @@ -48,16 +43,21 @@ type CreateFileOptions struct { ContentBase64 string `json:"content"` } -// DeleteFileOptions options for deleting files (used for other File structs below) +// DeleteFileOptions options for deleting a file // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type DeleteFileOptions struct { - FileOptionsWithSHA + FileOptions + // the blob ID (SHA) for the file to delete + // required: true + SHA string `json:"sha" binding:"Required"` } -// UpdateFileOptions options for updating files +// UpdateFileOptions options for updating or creating a file // Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used) type UpdateFileOptions struct { - FileOptionsWithSHA + FileOptions + // the blob ID (SHA) for the file that already exists to update, or leave it empty to create a new file + SHA string `json:"sha"` // content must be base64 encoded // required: true ContentBase64 string `json:"content"` @@ -72,7 +72,7 @@ type ChangeFileOperation struct { // indicates what to do with the file: "create" for creating a new file, "update" for updating an existing file, // "upload" for creating or updating a file, "rename" for renaming a file, and "delete" for deleting an existing file. // required: true - // enum: create,update,upload,rename,delete + // enum: ["create","update","upload","rename","delete"] Operation string `json:"operation" binding:"Required"` // path to the existing or new file // required: true diff --git a/package/gitea/source/modules/svg/svg.go b/package/gitea/source/modules/svg/svg.go index fded9d08..234b1f8c 100644 --- a/package/gitea/source/modules/svg/svg.go +++ b/package/gitea/source/modules/svg/svg.go @@ -8,13 +8,32 @@ import ( "html/template" "path" "strings" + "sync" gitea_html "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/public" ) -var svgIcons map[string]string +type svgIconItem struct { + html string + mocking bool +} + +type svgCacheKey struct { + icon string + size int + class string +} + +var ( + svgIcons map[string]svgIconItem + + svgCacheMu sync.Mutex + svgCache sync.Map + svgCacheCount int + svgCacheLimit = 10000 +) const defaultSize = 16 @@ -26,7 +45,7 @@ func Init() error { return err } - svgIcons = make(map[string]string, len(files)) + svgIcons = make(map[string]svgIconItem, len(files)) for _, file := range files { if path.Ext(file) != ".svg" { continue @@ -35,7 +54,7 @@ func Init() error { if err != nil { log.Error("Failed to read SVG file %s: %v", file, err) } else { - svgIcons[file[:len(file)-4]] = string(Normalize(bs, defaultSize)) + svgIcons[file[:len(file)-4]] = svgIconItem{html: string(Normalize(bs, defaultSize))} } } return nil @@ -43,10 +62,13 @@ func Init() error { func MockIcon(icon string) func() { if svgIcons == nil { - svgIcons = make(map[string]string) + svgIcons = make(map[string]svgIconItem) } orig, exist := svgIcons[icon] - svgIcons[icon] = fmt.Sprintf(``, icon, defaultSize, defaultSize) + svgIcons[icon] = svgIconItem{ + html: fmt.Sprintf(``, icon, defaultSize, defaultSize), + mocking: true, + } return func() { if exist { svgIcons[icon] = orig @@ -58,8 +80,28 @@ func MockIcon(icon string) func() { // RenderHTML renders icons - arguments icon name (string), size (int), class (string) func RenderHTML(icon string, others ...any) template.HTML { + result, _ := renderHTML(icon, others...) + return result +} + +func renderHTML(icon string, others ...any) (_ template.HTML, usingCache bool) { + if icon == "" { + return "", false + } size, class := gitea_html.ParseSizeAndClass(defaultSize, "", others...) - if svgStr, ok := svgIcons[icon]; ok { + if svgItem, ok := svgIcons[icon]; ok { + svgStr := svgItem.html + // fast path for default size and no classes + if size == defaultSize && class == "" { + return template.HTML(svgStr), false + } + + cacheKey := svgCacheKey{icon, size, class} + cachedHTML, cached := svgCache.Load(cacheKey) + if cached && !svgItem.mocking { + return cachedHTML.(template.HTML), true + } + // the code is somewhat hacky, but it just works, because the SVG contents are all normalized if size != defaultSize { svgStr = strings.Replace(svgStr, fmt.Sprintf(`width="%d"`, defaultSize), fmt.Sprintf(`width="%d"`, size), 1) @@ -68,8 +110,24 @@ func RenderHTML(icon string, others ...any) template.HTML { if class != "" { svgStr = strings.Replace(svgStr, `class="`, fmt.Sprintf(`class="%s `, class), 1) } - return template.HTML(svgStr) + result := template.HTML(svgStr) + + if !svgItem.mocking { + // no need to double-check, the rendering is fast enough and the cache is just an optimization + svgCacheMu.Lock() + if svgCacheCount >= svgCacheLimit { + svgCache.Clear() + svgCacheCount = 0 + } + svgCacheCount++ + svgCache.Store(cacheKey, result) + svgCacheMu.Unlock() + } + + return result, false } + // during test (or something wrong happens), there is no SVG loaded, so use a dummy span to tell that the icon is missing - return template.HTML(fmt.Sprintf("%s(%d/%s)", template.HTMLEscapeString(icon), size, template.HTMLEscapeString(class))) + dummy := template.HTML(fmt.Sprintf("%s(%d/%s)", template.HTMLEscapeString(icon), size, template.HTMLEscapeString(class))) + return dummy, false } diff --git a/package/gitea/source/modules/svg/svg_test.go b/package/gitea/source/modules/svg/svg_test.go new file mode 100644 index 00000000..a42f57ce --- /dev/null +++ b/package/gitea/source/modules/svg/svg_test.go @@ -0,0 +1,54 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package svg + +import ( + "testing" + + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestRenderHTMLCache(t *testing.T) { + const svgRealContent = "RealContent" + svgIcons = map[string]svgIconItem{ + "test": {html: `` + svgRealContent + ``}, + } + + // default params: no cache entry + _, usingCache := renderHTML("test") + assert.False(t, usingCache) + _, usingCache = renderHTML("test") + assert.False(t, usingCache) + + // non-default params: cached + _, usingCache = renderHTML("test", 24) + assert.False(t, usingCache) + _, usingCache = renderHTML("test", 24) + assert.True(t, usingCache) + + // mocked svg shouldn't be cached + revertMock := MockIcon("test") + mockedHTML, usingCache := renderHTML("test", 24) + assert.False(t, usingCache) + assert.NotContains(t, mockedHTML, svgRealContent) + revertMock() + realHTML, usingCache := renderHTML("test", 24) + assert.True(t, usingCache) + assert.Contains(t, realHTML, svgRealContent) + + t.Run("CacheWithLimit", func(t *testing.T) { + assert.NotZero(t, svgCacheCount) + const testLimit = 3 + defer test.MockVariableValue(&svgCacheLimit, testLimit)() + for i := range 10 { + _, usingCache = renderHTML("test", 100+i) + assert.False(t, usingCache) + _, usingCache = renderHTML("test", 100+i) + assert.True(t, usingCache) + assert.LessOrEqual(t, svgCacheCount, testLimit) + } + }) +} diff --git a/package/gitea/source/modules/sync/status_pool.go b/package/gitea/source/modules/sync/status_pool.go deleted file mode 100644 index 6f075d54..00000000 --- a/package/gitea/source/modules/sync/status_pool.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2016 The Gogs Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package sync - -import ( - "sync" - - "code.gitea.io/gitea/modules/container" -) - -// StatusTable is a table maintains true/false values. -// -// This table is particularly useful for un/marking and checking values -// in different goroutines. -type StatusTable struct { - lock sync.RWMutex - pool container.Set[string] -} - -// NewStatusTable initializes and returns a new StatusTable object. -func NewStatusTable() *StatusTable { - return &StatusTable{ - pool: make(container.Set[string]), - } -} - -// StartIfNotRunning sets value of given name to true if not already in pool. -// Returns whether set value was set to true -func (p *StatusTable) StartIfNotRunning(name string) bool { - p.lock.Lock() - added := p.pool.Add(name) - p.lock.Unlock() - return added -} - -// Start sets value of given name to true in the pool. -func (p *StatusTable) Start(name string) { - p.lock.Lock() - p.pool.Add(name) - p.lock.Unlock() -} - -// Stop sets value of given name to false in the pool. -func (p *StatusTable) Stop(name string) { - p.lock.Lock() - p.pool.Remove(name) - p.lock.Unlock() -} - -// IsRunning checks if value of given name is set to true in the pool. -func (p *StatusTable) IsRunning(name string) bool { - p.lock.RLock() - exists := p.pool.Contains(name) - p.lock.RUnlock() - return exists -} diff --git a/package/gitea/source/modules/sync/status_pool_test.go b/package/gitea/source/modules/sync/status_pool_test.go deleted file mode 100644 index e2e48862..00000000 --- a/package/gitea/source/modules/sync/status_pool_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package sync - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func Test_StatusTable(t *testing.T) { - table := NewStatusTable() - - assert.False(t, table.IsRunning("xyz")) - - table.Start("xyz") - assert.True(t, table.IsRunning("xyz")) - - assert.False(t, table.StartIfNotRunning("xyz")) - assert.True(t, table.IsRunning("xyz")) - - table.Stop("xyz") - assert.False(t, table.IsRunning("xyz")) - - assert.True(t, table.StartIfNotRunning("xyz")) - assert.True(t, table.IsRunning("xyz")) - - table.Stop("xyz") - assert.False(t, table.IsRunning("xyz")) -} diff --git a/package/gitea/source/modules/tempdir/tempdir.go b/package/gitea/source/modules/tempdir/tempdir.go index 22c2e4ea..e5e928e4 100644 --- a/package/gitea/source/modules/tempdir/tempdir.go +++ b/package/gitea/source/modules/tempdir/tempdir.go @@ -19,7 +19,7 @@ type TempDir struct { } func (td *TempDir) JoinPath(elems ...string) string { - return filepath.Join(append([]string{td.base, td.sub}, elems...)...) + return filepath.Join(append([]string{td.base, td.sub}, filepath.Join(elems...))...) } // MkdirAllSub works like os.MkdirAll, but the base directory must exist diff --git a/package/gitea/source/modules/templates/base.go b/package/gitea/source/modules/templates/base.go index 2c2f35bb..c8697cc7 100644 --- a/package/gitea/source/modules/templates/base.go +++ b/package/gitea/source/modules/templates/base.go @@ -4,9 +4,6 @@ package templates import ( - "slices" - "strings" - "code.gitea.io/gitea/modules/assetfs" "code.gitea.io/gitea/modules/setting" ) @@ -18,23 +15,3 @@ func AssetFS() *assetfs.LayeredFS { func CustomAssets() *assetfs.Layer { return assetfs.Local("custom", setting.CustomPath, "templates") } - -func ListWebTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) { - files, err := assets.ListAllFiles(".", true) - if err != nil { - return nil, err - } - return slices.DeleteFunc(files, func(file string) bool { - return strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl") - }), nil -} - -func ListMailTemplateAssetNames(assets *assetfs.LayeredFS) ([]string, error) { - files, err := assets.ListAllFiles(".", true) - if err != nil { - return nil, err - } - return slices.DeleteFunc(files, func(file string) bool { - return !strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl") - }), nil -} diff --git a/package/gitea/source/modules/templates/helper.go b/package/gitea/source/modules/templates/helper.go index e454bce4..f81be125 100644 --- a/package/gitea/source/modules/templates/helper.go +++ b/package/gitea/source/modules/templates/helper.go @@ -6,29 +6,27 @@ package templates import ( "fmt" + "html" "html/template" "net/url" "strconv" "strings" + "sync" "time" - user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/htmlutil" "code.gitea.io/gitea/modules/markup" + "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/svg" "code.gitea.io/gitea/modules/templates/eval" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/gitdiff" - "code.gitea.io/gitea/services/webtheme" ) -// NewFuncMap returns functions for injecting to templates -func NewFuncMap() template.FuncMap { +func newFuncMapWebPage() template.FuncMap { return map[string]any{ - "ctx": func() any { return nil }, // template context function - "DumpVar": dumpVar, "NIL": func() any { return nil }, @@ -41,8 +39,6 @@ func NewFuncMap() template.FuncMap { "QueryEscape": queryEscape, "QueryBuild": QueryBuild, "SanitizeHTML": SanitizeHTML, - "URLJoin": util.URLJoin, - "DotEscape": dotEscape, "PathEscape": url.PathEscape, "PathEscapeSegments": util.PathEscapeSegments, @@ -63,6 +59,7 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // time / number / format + "ShortSha": base.ShortSha, "FileSize": base.FileSize, "CountFmt": countFmt, "Sec2Hour": util.SecToHours, @@ -73,6 +70,9 @@ func NewFuncMap() template.FuncMap { return strconv.FormatInt(time.Since(startTime).Nanoseconds()/1e6, 10) + "ms" }, + "AssetURI": public.AssetURI, + "ScriptImport": scriptImport, + // ----------------------------------------------------------------- // setting "AppName": func() string { @@ -84,25 +84,12 @@ func NewFuncMap() template.FuncMap { "AssetUrlPrefix": func() string { return setting.StaticURLPrefix + "/assets" }, - "AppUrl": func() string { - // The usage of AppUrl should be avoided as much as possible, - // because the AppURL(ROOT_URL) may not match user's visiting site and the ROOT_URL in app.ini may be incorrect. - // And it's difficult for Gitea to guess absolute URL correctly with zero configuration, - // because Gitea doesn't know whether the scheme is HTTP or HTTPS unless the reverse proxy could tell Gitea. - return setting.AppURL - }, "AppVer": func() string { return setting.AppVer }, - "AppDomain": func() string { // documented in mail-templates.md + "AppDomain": func() string { // TODO: helm registry still uses it, need to use current request host in the future return setting.Domain }, - "AssetVersion": func() string { - return setting.AssetVersion - }, - "DefaultShowFullName": func() bool { - return setting.UI.DefaultShowFullName - }, "ShowFooterTemplateLoadTime": func() bool { return setting.Other.ShowFooterTemplateLoadTime }, @@ -130,7 +117,6 @@ func NewFuncMap() template.FuncMap { "DisableWebhooks": func() bool { return setting.DisableWebhooks }, - "UserThemeName": userThemeName, "NotificationSettings": func() map[string]any { return map[string]any{ "MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond), @@ -149,8 +135,7 @@ func NewFuncMap() template.FuncMap { "ReactionToEmoji": reactionToEmoji, // ----------------------------------------------------------------- - // misc - "ShortSha": base.ShortSha, + // misc (TODO: move them to MiscUtils to avoid bloating the main func map) "ActionContent2Commits": ActionContent2Commits, "IsMultilineCommitMessage": isMultilineCommitMessage, "CommentMustAsDiff": gitdiff.CommentMustAsDiff, @@ -184,11 +169,6 @@ func queryEscape(s string) template.URL { return template.URL(url.QueryEscape(s)) } -// dotEscape wraps a dots in names with ZWJ [U+200D] in order to prevent auto-linkers from detecting these as urls -func dotEscape(raw string) string { - return strings.ReplaceAll(raw, ".", "\u200d.\u200d") -} - // iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version, // and it could be simply used as "{{iif expr trueVal}}" (omit the falseVal). func iif(condition any, vals ...any) any { @@ -217,16 +197,6 @@ func evalTokens(tokens ...any) (any, error) { return n.Value, err } -func userThemeName(user *user_model.User) string { - if user == nil || user.Theme == "" { - return setting.UI.DefaultTheme - } - if webtheme.IsThemeAvailable(user.Theme) { - return user.Theme - } - return setting.UI.DefaultTheme -} - func isQueryParamEmpty(v any) bool { return v == nil || v == false || v == 0 || v == int64(0) || v == "" } @@ -322,3 +292,30 @@ func QueryBuild(a ...any) template.URL { } return template.URL(s) } + +var globalVars = sync.OnceValue(func() (ret struct { + scriptImportRemainingPart string +}, +) { + // add onerror handler to alert users when the script fails to load: + // * for end users: there were many users reporting that "UI doesn't work", actually they made mistakes in their config + // * for developers: help them to remember to run "make watch-frontend" to build frontend assets + // the message will be directly put in the onerror JS code's string + onScriptErrorPrompt := `Please make sure the asset files can be accessed.` + if !setting.IsProd { + onScriptErrorPrompt += `\n\nFor development, run: make watch-frontend.` + } + onScriptErrorJS := fmt.Sprintf(`alert('Failed to load asset file from ' + this.src + '. %s')`, onScriptErrorPrompt) + ret.scriptImportRemainingPart = `onerror="` + html.EscapeString(onScriptErrorJS) + `">` + return ret +}) + +func scriptImport(path string, typ ...string) template.HTML { + if len(typ) > 0 { + if typ[0] == "module" { + return template.HTML(` - +{{ScriptImport "js/iife.js"}} diff --git a/package/gitea/source/templates/base/head_style.tmpl b/package/gitea/source/templates/base/head_style.tmpl index f97e1880..4a4fb9d9 100644 --- a/package/gitea/source/templates/base/head_style.tmpl +++ b/package/gitea/source/templates/base/head_style.tmpl @@ -1,2 +1,2 @@ - - + + diff --git a/package/gitea/source/templates/base/markup_codepreview.tmpl b/package/gitea/source/templates/base/markup_codepreview.tmpl index e1684858..4c04b509 100644 --- a/package/gitea/source/templates/base/markup_codepreview.tmpl +++ b/package/gitea/source/templates/base/markup_codepreview.tmpl @@ -13,10 +13,7 @@ {{- range $idx, $line := .HighlightLines -}} - {{- if $.EscapeStatus.Escaped -}} - {{- $lineEscapeStatus := index $.LineEscapeStatus $idx -}} - {{if $lineEscapeStatus.Escaped}}{{end}} - {{- end}} + {{- ctx.RenderUtils.RenderUnicodeEscapeToggleTd $.EscapeStatus (index $.LineEscapeStatus $idx)}}
    {{$line.FormattedContent}}
    {{/* only div works, span generates incorrect HTML structure */}} {{- end -}} diff --git a/package/gitea/source/templates/base/modal_actions_confirm.tmpl b/package/gitea/source/templates/base/modal_actions_confirm.tmpl index ec6942b9..4fde7e09 100644 --- a/package/gitea/source/templates/base/modal_actions_confirm.tmpl +++ b/package/gitea/source/templates/base/modal_actions_confirm.tmpl @@ -12,15 +12,15 @@ The ".ok.button" and ".cancel.button" selectors are also used by Fomantic Modal {{if .ModalButtonDangerText}} {{else}} - {{$textNegitive := ctx.Locale.Tr "modal.no"}} + {{$textNegative := ctx.Locale.Tr "modal.no"}} {{$textPositive := ctx.Locale.Tr "modal.yes"}} {{if eq .ModalButtonTypes "confirm"}} - {{$textNegitive = ctx.Locale.Tr "modal.cancel"}} + {{$textNegative = ctx.Locale.Tr "modal.cancel"}} {{$textPositive = ctx.Locale.Tr "modal.confirm"}} {{end}} - {{if .ModalButtonCancelText}}{{$textNegitive = .ModalButtonCancelText}}{{end}} + {{if .ModalButtonCancelText}}{{$textNegative = .ModalButtonCancelText}}{{end}} {{if .ModalButtonOkText}}{{$textPositive = .ModalButtonOkText}}{{end}} - + {{end}} diff --git a/package/gitea/source/templates/devtest/devtest-footer.tmpl b/package/gitea/source/templates/devtest/devtest-footer.tmpl index a1b3b86e..091a1035 100644 --- a/package/gitea/source/templates/devtest/devtest-footer.tmpl +++ b/package/gitea/source/templates/devtest/devtest-footer.tmpl @@ -1,3 +1 @@ -{{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} - {{template "base/footer" ctx.RootData}} diff --git a/package/gitea/source/templates/devtest/devtest-header.tmpl b/package/gitea/source/templates/devtest/devtest-header.tmpl index 0775dccc..628e4388 100644 --- a/package/gitea/source/templates/devtest/devtest-header.tmpl +++ b/package/gitea/source/templates/devtest/devtest-header.tmpl @@ -1,3 +1,4 @@ {{template "base/head" ctx.RootData}} - + +
    {{template "base/alert" .}} diff --git a/package/gitea/source/templates/devtest/flex-list.tmpl b/package/gitea/source/templates/devtest/flex-list.tmpl index 0db84b0c..c0863ba4 100644 --- a/package/gitea/source/templates/devtest/flex-list.tmpl +++ b/package/gitea/source/templates/devtest/flex-list.tmpl @@ -48,7 +48,7 @@
    Very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong content - Truncate very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong content + Truncate very loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong content
    @@ -65,7 +65,7 @@
    diff --git a/package/gitea/source/templates/devtest/form-fields.tmpl b/package/gitea/source/templates/devtest/form-fields.tmpl new file mode 100644 index 00000000..ee6df2e8 --- /dev/null +++ b/package/gitea/source/templates/devtest/form-fields.tmpl @@ -0,0 +1,109 @@ +{{template "devtest/devtest-header"}} +
    +
    +

    Input

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +

    Textarea

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +

    Dropdown

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +

    Required

    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +{{template "devtest/devtest-footer"}} diff --git a/package/gitea/source/templates/devtest/gitea-ui.tmpl b/package/gitea/source/templates/devtest/gitea-ui.tmpl index cb5aad7b..1584792b 100644 --- a/package/gitea/source/templates/devtest/gitea-ui.tmpl +++ b/package/gitea/source/templates/devtest/gitea-ui.tmpl @@ -84,12 +84,6 @@
    -
    -

    <origin-url>

    -
    -
    -
    -

    <overflow-menu>

    @@ -117,16 +111,6 @@
    -
    -

    GiteaAbsoluteDate

    -
    -
    -
    -
    -
    -
    relative-time:
    -
    -

    LocaleNumber

    {{ctx.Locale.PrettyNumber 1}}
    @@ -138,17 +122,6 @@
    {{ctx.Locale.PrettyNumber 1234567}}
    -
    -

    TimeSince

    -
    Now: {{DateUtils.TimeSince .TimeNow}}
    -
    5s past: {{DateUtils.TimeSince .TimePast5s}}
    -
    5s future: {{DateUtils.TimeSince .TimeFuture5s}}
    -
    2m past: {{DateUtils.TimeSince .TimePast2m}}
    -
    2m future: {{DateUtils.TimeSince .TimeFuture2m}}
    -
    1y past: {{DateUtils.TimeSince .TimePast1y}}
    -
    1y future: {{DateUtils.TimeSince .TimeFuture1y}}
    -
    -

    SVG alignment

    @@ -185,7 +158,7 @@

    ComboMarkdownEditor

    - {{template "shared/combomarkdowneditor" dict "MarkdownPreviewContext" "/owner/path"}} + {{template "shared/combomarkdowneditor" dict "MarkdownEditorContext" (ctx.MiscUtils.MarkdownEditorGeneral nil)}}

    Tailwind CSS Demo

    diff --git a/package/gitea/source/templates/devtest/keyboard-shortcut.tmpl b/package/gitea/source/templates/devtest/keyboard-shortcut.tmpl new file mode 100644 index 00000000..4b55c0a1 --- /dev/null +++ b/package/gitea/source/templates/devtest/keyboard-shortcut.tmpl @@ -0,0 +1,23 @@ +{{template "devtest/devtest-header"}} +
    +

    Keyboard Shortcut

    + +
    +
    + + S +
    +
    + +
    +
    +
    + + T +
    + +
    +
    +
    + +{{template "devtest/devtest-footer"}} diff --git a/package/gitea/source/templates/devtest/relative-time.tmpl b/package/gitea/source/templates/devtest/relative-time.tmpl new file mode 100644 index 00000000..041ce49f --- /dev/null +++ b/package/gitea/source/templates/devtest/relative-time.tmpl @@ -0,0 +1,66 @@ +{{template "devtest/devtest-header"}} +
    +
    +
    +

    Relative (auto)

    +
    now:
    +
    3m ago:
    +
    3h ago:
    +
    1d ago:
    +
    3d ago:
    +
    3d future:
    +
    40d ago (threshold):
    +
    +
    +

    tense=past

    +
    3m ago:
    +
    future clamped:
    +
    60d ago:
    +
    +
    +

    tense=future

    +
    3h future:
    +
    past clamped:
    +
    +
    +

    Duration

    +
    0s:
    +
    3h:
    +
    1d 2h:
    +
    short:
    +
    narrow:
    +
    +
    +

    Datetime (absolute)

    +
    default:
    +
    month=short:
    +
    month=long:
    +
    numeric:
    +
    weekday:
    +
    with time:
    +
    +
    +

    Threshold

    +
    P0Y:
    +
    P1D:
    +
    P30D:
    +
    +
    +

    Prefix

    +
    default:
    +
    prefix="":
    +
    prefix="at":
    +
    +
    +

    TimeSince (Go helper)

    +
    now: {{DateUtils.TimeSince .TimeNow}}
    +
    5s past: {{DateUtils.TimeSince .TimePast5s}}
    +
    5s future: {{DateUtils.TimeSince .TimeFuture5s}}
    +
    2m past: {{DateUtils.TimeSince .TimePast2m}}
    +
    2m future: {{DateUtils.TimeSince .TimeFuture2m}}
    +
    1y past: {{DateUtils.TimeSince .TimePast1y}}
    +
    1y future: {{DateUtils.TimeSince .TimeFuture1y}}
    +
    +
    +
    +{{template "devtest/devtest-footer"}} diff --git a/package/gitea/source/templates/devtest/repo-action-view.tmpl b/package/gitea/source/templates/devtest/repo-action-view.tmpl index 677eccc0..46f040d8 100644 --- a/package/gitea/source/templates/devtest/repo-action-view.tmpl +++ b/package/gitea/source/templates/devtest/repo-action-view.tmpl @@ -1,14 +1,14 @@ {{template "base/head" .}}
    -
    - Run:CanCancel - Run:CanApprove - Run:CanRerun + {{template "repo/actions/view_component" (dict - "RunIndex" (or .RunID 10) - "JobIndex" (or .JobID 100) - "ActionsURL" (print AppSubUrl "/devtest/actions-mock") + "RunID" (or .RunID 10) + "JobID" (or .JobID 0) + "ActionsURL" (print AppSubUrl "/devtest/repo-action-view") )}}
    {{template "base/footer" .}} diff --git a/package/gitea/source/templates/devtest/severity-colors.tmpl b/package/gitea/source/templates/devtest/severity-colors.tmpl new file mode 100644 index 00000000..43a51614 --- /dev/null +++ b/package/gitea/source/templates/devtest/severity-colors.tmpl @@ -0,0 +1,115 @@ +{{template "devtest/devtest-header"}} +
    +

    Severity Colors

    + +

    Messages

    +
    +
    Error Message
    +

    This is an error message using --color-error-* variables.

    +
    +
    +
    Warning Message
    +

    This is a warning message using --color-warning-* variables.

    +
    +
    +
    Success Message
    +

    This is a success message using --color-success-* variables.

    +
    +
    +
    Info Message
    +

    This is an info message using --color-info-* variables.

    +
    + +

    Markdown Attention Blocks

    +
    +

    {{svg "octicon-info" 16 "attention-icon attention-note"}}Note

    +

    Useful information that users should know, even when skimming content.

    +
    +

    {{svg "octicon-light-bulb" 16 "attention-icon attention-tip"}}Tip

    +

    Helpful advice for doing things better or more easily.

    +
    +

    {{svg "octicon-report" 16 "attention-icon attention-important"}}Important

    +

    Key information users need to know to achieve their goal.

    +
    +

    {{svg "octicon-alert" 16 "attention-icon attention-warning"}}Warning

    +

    Urgent info that needs immediate user attention to avoid problems.

    +
    +

    {{svg "octicon-stop" 16 "attention-icon attention-caution"}}Caution

    +

    Advises about risks or negative outcomes of certain actions.

    +
    +
    + +

    Form Fields

    +
    +
    + + +
    +
    + +

    Error Input

    +
    + +
    + +

    Attached Section Boxes

    +

    Error Header

    +
    Error section body content.
    +

    Warning Header

    +
    Warning section body content.
    + +

    Banner Preview (info-tinted)

    +
    +
    Banner preview content
    +
    + +

    Labels

    +
    +
    Red
    +
    Orange
    +
    Yellow
    +
    Green
    +
    Blue
    +
    Violet
    +
    Purple
    +
    + +

    Color Swatches

    +

    Error

    +
    +
    +
    Text
    + error-bg +
    +
    +
    Hover
    + error-bg-hover +
    +
    +
    Active
    + error-bg-active +
    +
    +

    Warning

    +
    +
    +
    Text
    + warning-bg +
    +
    +

    Success

    +
    +
    +
    Text
    + success-bg +
    +
    +

    Info

    +
    +
    +
    Text
    + info-bg +
    +
    +
    +{{template "devtest/devtest-footer"}} diff --git a/package/gitea/source/templates/devtest/unicode-escape.tmpl b/package/gitea/source/templates/devtest/unicode-escape.tmpl new file mode 100644 index 00000000..a61813f5 --- /dev/null +++ b/package/gitea/source/templates/devtest/unicode-escape.tmpl @@ -0,0 +1,17 @@ +{{template "devtest/devtest-header"}} +
    +
    + + + {{range $idx, $line := .HighlightLines}} + + + {{ctx.RenderUtils.RenderUnicodeEscapeToggleTd $.EscapeStatus (index $.LineEscapeStatus $idx)}} + + + {{end}} + +
    {{$line.FormattedContent}}
    +
    +
    +{{template "devtest/devtest-footer"}} diff --git a/package/gitea/source/templates/home.tmpl b/package/gitea/source/templates/home.tmpl index cc9da826..4d7e7b93 100644 --- a/package/gitea/source/templates/home.tmpl +++ b/package/gitea/source/templates/home.tmpl @@ -1,5 +1,5 @@ {{template "base/head" .}} -
    +
    diff --git a/package/gitea/source/templates/install.tmpl b/package/gitea/source/templates/install.tmpl index 0aec52f2..bc6fed08 100644 --- a/package/gitea/source/templates/install.tmpl +++ b/package/gitea/source/templates/install.tmpl @@ -117,7 +117,7 @@ {{ctx.Locale.Tr "install.lfs_path_helper"}}
    -
    +
    {{ctx.Locale.Tr "install.run_user_helper"}} @@ -160,7 +160,7 @@
    - + {{ctx.Locale.Tr "install.email_title"}}
    @@ -200,27 +200,9 @@
    - + {{ctx.Locale.Tr "install.server_service_title"}} -
    -
    - - -
    -
    -
    -
    - - -
    -
    -
    -
    - - -
    -
    @@ -298,7 +280,7 @@
    - + {{ctx.Locale.Tr "install.admin_title"}}

    {{ctx.Locale.Tr "install.admin_setting_desc"}}

    diff --git a/package/gitea/source/templates/mail/repo/release.tmpl b/package/gitea/source/templates/mail/repo/release.tmpl index b306ddf5..9e7295d3 100644 --- a/package/gitea/source/templates/mail/repo/release.tmpl +++ b/package/gitea/source/templates/mail/repo/release.tmpl @@ -41,7 +41,7 @@ {{if .Release.Attachments}} {{range .Release.Attachments}}
  • - + {{.Name}} ({{.Size | FileSize}})
  • diff --git a/package/gitea/source/templates/org/create.tmpl b/package/gitea/source/templates/org/create.tmpl index 2d6dca54..60d659c8 100644 --- a/package/gitea/source/templates/org/create.tmpl +++ b/package/gitea/source/templates/org/create.tmpl @@ -7,7 +7,6 @@
    - {{.CsrfTokenHtml}}
    diff --git a/package/gitea/source/templates/org/header.tmpl b/package/gitea/source/templates/org/header.tmpl index 90798b5d..31f43449 100644 --- a/package/gitea/source/templates/org/header.tmpl +++ b/package/gitea/source/templates/org/header.tmpl @@ -7,7 +7,7 @@ {{if .Org.Visibility.IsLimited}}{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}{{end}} {{if .Org.Visibility.IsPrivate}}{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}{{end}} - + {{if .EnableFeed}} {{svg "octicon-rss" 24}} @@ -19,9 +19,9 @@
    {{if .RenderedDescription}}
    {{.RenderedDescription}}
    {{end}} -
    +
    {{if .Org.Location}}
    {{svg "octicon-location"}} {{.Org.Location}}
    {{end}} - {{if .Org.Website}}
    {{end}} + {{if .Org.Website}}
    {{svg "octicon-link"}} {{.Org.Website}}
    {{end}} {{if .IsSigned}} {{if .Org.Email}}
    {{svg "octicon-mail"}} {{.Org.Email}}
    {{end}} {{end}} diff --git a/package/gitea/source/templates/org/home.tmpl b/package/gitea/source/templates/org/home.tmpl index 3cde3554..cc8dae50 100644 --- a/package/gitea/source/templates/org/home.tmpl +++ b/package/gitea/source/templates/org/home.tmpl @@ -9,8 +9,16 @@
    {{.ProfileReadmeContent}}
    {{end}} {{template "shared/repo/search" .}} - {{template "shared/repo/list" .}} - {{template "base/paginate" .}} + {{if not .Repos}} +
    + {{svg "octicon-repo" 48}} +

    {{ctx.Locale.Tr "org.repos.empty"}}

    +

    {{ctx.Locale.Tr "org.repos.empty_description"}}

    +
    + {{else}} + {{template "shared/repo/list" .}} + {{template "base/paginate" .}} + {{end}}
    {{if .ShowMemberAndTeamTab}} @@ -49,13 +57,13 @@ {{if .NumMembers}}

    {{ctx.Locale.Tr "org.members"}} - {{.NumMembers}} {{svg "octicon-chevron-right"}} + {{.NumMembers}} {{svg "octicon-chevron-right"}}

    {{$isMember := .IsOrganizationMember}} {{range .Members}} {{if or $isMember (call $.IsPublicMember .ID)}} - {{ctx.AvatarUtils.Avatar . 48}} + {{template "shared/user/avatarlink" dict "user" . "size" 32 "tooltip" true}} {{end}} {{end}}
    @@ -63,13 +71,13 @@ {{if .IsOrganizationMember}}
    {{range .Teams}}
    {{.Name}} -

    +

    {{.NumMembers}} {{ctx.Locale.Tr "org.lower_members"}} · {{.NumRepos}} {{ctx.Locale.Tr "org.lower_repositories"}}

    diff --git a/package/gitea/source/templates/org/member/members.tmpl b/package/gitea/source/templates/org/member/members.tmpl index 2d0f4bc4..1cb50f78 100644 --- a/package/gitea/source/templates/org/member/members.tmpl +++ b/package/gitea/source/templates/org/member/members.tmpl @@ -4,6 +4,13 @@
    {{template "base/alert" .}} + {{if .IsOrganizationOwner}} +
    +
    {{ctx.Locale.Tr "org.teams.manage_team_member_prompt"}}
    + {{ctx.Locale.Tr "org.teams.manage_team_member"}} +
    +
    + {{end}}
    {{range .Members}} {{$isPublic := index $.MembersIsPublicMember .ID}} @@ -15,27 +22,27 @@
    {{template "shared/user/name" .}} {{if not $isPublic}} - {{ctx.Locale.Tr "org.members.private"}} + {{ctx.Locale.Tr "org.members.private"}} {{end}}
    - {{if not $.PublicOnly}} -
    +
    + {{if not $.PublicOnly}} +
    {{ctx.Locale.Tr "org.members.member_role"}} {{if index $.MembersIsUserOrgOwner .ID}}{{svg "octicon-shield-lock"}} {{ctx.Locale.Tr "org.members.owner"}}{{else}}{{ctx.Locale.Tr "org.members.member"}}{{end}}
    + {{end}} {{if $.IsOrganizationOwner}} -
    - {{ctx.Locale.Tr "admin.users.2fa"}} - - {{if index $.MembersTwoFaStatus .ID}} - {{svg "octicon-check"}} - {{else}} - {{svg "octicon-x"}} - {{end}} - +
    + {{ctx.Locale.Tr "admin.users.2fa"}}: + {{if index $.MembersTwoFaStatus .ID}} + {{svg "octicon-check"}} + {{else}} + {{svg "octicon-x"}} + {{end}}
    {{end}} - {{end}} +
    {{if or (eq $.SignedUser.ID .ID) $.IsOrganizationOwner}} @@ -46,45 +53,23 @@ {{end}} {{end}} {{if eq $.SignedUser.ID .ID}} - - - + {{else if $.IsOrganizationOwner}} -
    - -
    + {{end}}
    {{end}}
    - {{template "base/paginate" .}}
    - - - {{template "base/footer" .}} diff --git a/package/gitea/source/templates/org/settings/actions_general.tmpl b/package/gitea/source/templates/org/settings/actions_general.tmpl new file mode 100644 index 00000000..ebf9482f --- /dev/null +++ b/package/gitea/source/templates/org/settings/actions_general.tmpl @@ -0,0 +1,5 @@ +{{template "org/settings/layout_head" (dict "ctxData" .)}} +
    + {{template "shared/actions/owner_general_settings" .}} +
    +{{template "org/settings/layout_footer" .}} diff --git a/package/gitea/source/templates/org/settings/labels.tmpl b/package/gitea/source/templates/org/settings/labels.tmpl index 21d7c0ef..283b2199 100644 --- a/package/gitea/source/templates/org/settings/labels.tmpl +++ b/package/gitea/source/templates/org/settings/labels.tmpl @@ -1,6 +1,6 @@ {{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings labels")}}
    -
    +
    {{ctx.Locale.Tr "org.settings.labels_desc"}}
    diff --git a/package/gitea/source/templates/org/settings/navbar.tmpl b/package/gitea/source/templates/org/settings/navbar.tmpl index 58475de7..4c06b2cb 100644 --- a/package/gitea/source/templates/org/settings/navbar.tmpl +++ b/package/gitea/source/templates/org/settings/navbar.tmpl @@ -26,9 +26,12 @@ {{end}} {{if .EnableActions}} -
    +
    {{ctx.Locale.Tr "actions.actions"}}