diff --git a/.github/actions/free-disk-space/action.yml b/.github/actions/free-disk-space/action.yml
new file mode 100644
index 0000000000..510b643a33
--- /dev/null
+++ b/.github/actions/free-disk-space/action.yml
@@ -0,0 +1,9 @@
+name: free-disk-space
+description: Free space on / before large cache restores
+
+# Delete preinstalled toolchains which gitea doesn't use
+runs:
+ using: composite
+ steps:
+ - shell: bash
+ run: sudo rm -rf /usr/local/lib/android /usr/local/.ghcup /opt/ghc /usr/share/dotnet
diff --git a/.github/actions/go-setup/action.yml b/.github/actions/go-setup/action.yml
new file mode 100644
index 0000000000..a99ef0629a
--- /dev/null
+++ b/.github/actions/go-setup/action.yml
@@ -0,0 +1,24 @@
+name: go-setup
+description: Set up go and restore caches
+
+inputs:
+ cache:
+ description: Restore go caches
+ default: "true"
+ lint-cache:
+ description: Also restore the golangci-lint cache
+ default: "false"
+
+runs:
+ using: composite
+ steps:
+ - uses: ./.github/actions/free-disk-space
+ - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
+ with:
+ go-version-file: go.mod
+ check-latest: true
+ cache: false
+ - if: ${{ inputs.cache == 'true' }}
+ uses: ./.github/actions/go-cache
+ with:
+ lint-cache: ${{ inputs.lint-cache }}
diff --git a/.github/actions/node-setup/action.yml b/.github/actions/node-setup/action.yml
new file mode 100644
index 0000000000..c9ab484d57
--- /dev/null
+++ b/.github/actions/node-setup/action.yml
@@ -0,0 +1,22 @@
+name: node-setup
+description: Set up pnpm and node and restore caches
+
+inputs:
+ cache:
+ description: Cache pnpm downloads
+ default: "true"
+
+runs:
+ using: composite
+ steps:
+ - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
+ - if: ${{ inputs.cache == 'true' }}
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: 24
+ cache: pnpm
+ cache-dependency-path: pnpm-lock.yaml
+ - if: ${{ inputs.cache != 'true' }}
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: 24
diff --git a/.github/actions/pgsql-shard/action.yml b/.github/actions/pgsql-shard/action.yml
new file mode 100644
index 0000000000..9a5a03ae0d
--- /dev/null
+++ b/.github/actions/pgsql-shard/action.yml
@@ -0,0 +1,40 @@
+name: pgsql-shard
+description: Run one pgsql integration test shard
+
+inputs:
+ shard:
+ description: Shard index
+ required: true
+ total-shards:
+ description: Total shard count
+ required: true
+ run-migration:
+ description: Also run migration tests
+ default: "false"
+
+runs:
+ using: composite
+ steps:
+ - name: Add hosts to /etc/hosts
+ shell: bash
+ run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
+ - shell: bash
+ run: make deps-backend
+ - shell: bash
+ run: make backend
+ env:
+ TAGS: bindata
+ - name: run migration tests
+ if: ${{ inputs.run-migration == 'true' }}
+ shell: bash
+ run: GITEA_TEST_DATABASE=pgsql make test-migration
+ - name: run tests
+ shell: bash
+ run: GITEA_TEST_DATABASE=pgsql make test-integration
+ env:
+ # pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
+ GOTEST_FLAGS: -race -timeout=40m
+ TAGS: bindata gogit
+ TEST_LDAP: 1
+ TEST_SHARD: ${{ inputs.shard }}
+ TEST_TOTAL_SHARDS: ${{ inputs.total-shards }}
diff --git a/.github/workflows/cache-seeder.yml b/.github/workflows/cache-seeder.yml
index 91109b1c06..8ec7adee07 100644
--- a/.github/workflows/cache-seeder.yml
+++ b/.github/workflows/cache-seeder.yml
@@ -15,6 +15,7 @@ on:
- "go.sum"
- ".golangci.yml"
- ".github/actions/go-cache/action.yml"
+ - ".github/actions/go-setup/action.yml"
- ".github/workflows/cache-seeder.yml"
concurrency:
@@ -29,12 +30,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
+ - uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools
- run: TAGS="bindata" make backend
- run: TAGS="bindata gogit" GOEXPERIMENT="" make backend
@@ -64,12 +60,7 @@ jobs:
- { tags: "bindata", target: "lint-backend" }
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
+ - uses: ./.github/actions/go-setup
with:
lint-cache: "true"
- run: make deps-backend deps-tools
diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml
index ac2bd1a5ba..c17afbca97 100644
--- a/.github/workflows/files-changed.yml
+++ b/.github/workflows/files-changed.yml
@@ -25,6 +25,8 @@ on:
value: ${{ jobs.detect.outputs.json }}
e2e:
value: ${{ jobs.detect.outputs.e2e }}
+ shell:
+ value: ${{ jobs.detect.outputs.shell }}
permissions:
contents: read
@@ -45,6 +47,7 @@ jobs:
yaml: ${{ steps.changes.outputs.yaml }}
json: ${{ steps.changes.outputs.json }}
e2e: ${{ steps.changes.outputs.e2e }}
+ shell: ${{ steps.changes.outputs.shell }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
@@ -66,6 +69,8 @@ jobs:
- "*.ts"
- "web_src/**"
- "tools/generate-svg.ts"
+ - "tools/generate-svg-vscode-extensions.json"
+ - "tsconfig.json"
- "assets/emoji.json"
- "package.json"
- "pnpm-lock.yaml"
@@ -81,16 +86,18 @@ jobs:
actions:
- ".github/workflows/*"
+ - ".github/actions/**"
- "Makefile"
templates:
- - "tools/lint-templates-*.js"
+ - "tools/lint-templates-*.ts"
- "templates/**/*.tmpl"
- "pyproject.toml"
- "uv.lock"
docker:
- ".github/workflows/pull-docker-dryrun.yml"
+ - ".github/actions/docker-dryrun/**"
- "Dockerfile"
- "Dockerfile.rootless"
- "docker/**"
@@ -118,8 +125,13 @@ jobs:
json:
- "**/*.json"
- "**/*.json5"
+ - "eslint.json.config.ts"
e2e:
- "tests/e2e/**"
- "tools/test-e2e.sh"
- "playwright.config.ts"
+
+ shell:
+ - "**/*.sh"
+ - ".shellcheckrc"
diff --git a/.github/workflows/pull-compliance.yml b/.github/workflows/pull-compliance.yml
index ada537a188..801966e144 100644
--- a/.github/workflows/pull-compliance.yml
+++ b/.github/workflows/pull-compliance.yml
@@ -20,12 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
+ - uses: ./.github/actions/go-setup
with:
lint-cache: "true"
- run: make deps-backend deps-tools
@@ -37,15 +32,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
+ - uses: ./.github/actions/go-setup
with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ cache: "false"
+ - uses: ./.github/actions/node-setup
with:
- node-version: 24
+ cache: "false"
- run: make lint-spell
@@ -62,18 +54,16 @@ jobs:
- if: needs.files-changed.outputs.actions == 'true'
run: make lint-actions
+ - if: needs.files-changed.outputs.shell == 'true'
+ run: make lint-shell
+
checks-backend:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
+ - uses: ./.github/actions/go-setup
- run: make deps-backend deps-tools
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs
@@ -83,12 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- with:
- node-version: 24
- cache: pnpm
- cache-dependency-path: pnpm-lock.yaml
+ - uses: ./.github/actions/node-setup
- run: make deps-frontend
- run: make lint-frontend
- run: make checks-frontend
@@ -101,12 +86,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
+ - uses: ./.github/actions/go-setup
- run: make deps-backend generate-go
# no frontend build here as backend should be able to build, even without any frontend files
# CGO is not used when cross-compile, so these steps also test if the code is compatible with CGO disabled
diff --git a/.github/workflows/pull-db-tests.yml b/.github/workflows/pull-db-tests.yml
index bc73d6391c..dd2c11a40b 100644
--- a/.github/workflows/pull-db-tests.yml
+++ b/.github/workflows/pull-db-tests.yml
@@ -14,14 +14,11 @@ jobs:
files-changed:
uses: ./.github/workflows/files-changed.yml
- test-pgsql-shards:
- if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
+ test-pgsql-shard-1:
+ if: needs.files-changed.outputs.backend == 'true'
needs: files-changed
runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- shard: [1, 2]
+ timeout-minutes: 50
services:
pgsql:
image: postgres:14
@@ -46,31 +43,47 @@ jobs:
- "9000:9000"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
+ - uses: ./.github/actions/go-setup
+ - uses: ./.github/actions/pgsql-shard
with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
- - name: Add hosts to /etc/hosts
- run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 pgsql ldap minio" | sudo tee -a /etc/hosts'
- - run: make deps-backend
- - run: make backend
+ shard: 1
+ total-shards: 2
+ run-migration: "true"
+
+ test-pgsql-shard-2:
+ if: needs.files-changed.outputs.backend == 'true'
+ needs: files-changed
+ runs-on: ubuntu-latest
+ timeout-minutes: 50
+ services:
+ pgsql:
+ image: postgres:14
env:
- TAGS: bindata
- - name: run migration tests
- if: matrix.shard == 1
- run: GITEA_TEST_DATABASE=pgsql make test-migration
- - name: run tests
- run: GITEA_TEST_DATABASE=pgsql make test-integration
- timeout-minutes: 50
+ POSTGRES_DB: test
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - "5432:5432"
+ ldap:
+ image: gitea/test-openldap:latest@sha256:4ac633b01d684e6b2a458cc0c8530c92f9b3702f6e040ce5f365607df34fbda0
+ ports:
+ - "389:389"
+ - "636:636"
+ minio:
+ # as github actions doesn't support "entrypoint", we need to use a non-official image
+ # that has a custom entrypoint set to "minio server /data"
+ image: bitnamilegacy/minio:2025.7.23
env:
- # pgsql is chosen to be the unlucky one to run with the slow "race detector", it is about 60% slower.
- GOTEST_FLAGS: -race -timeout=40m
- TAGS: bindata gogit
- TEST_LDAP: 1
- TEST_SHARD: ${{ matrix.shard }}
- TEST_TOTAL_SHARDS: ${{ strategy.job-total }}
+ MINIO_ROOT_USER: 123456
+ MINIO_ROOT_PASSWORD: 12345678
+ ports:
+ - "9000:9000"
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: ./.github/actions/go-setup
+ - uses: ./.github/actions/pgsql-shard
+ with:
+ shard: 2
+ total-shards: 2
test-sqlite:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
@@ -78,12 +91,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
+ - uses: ./.github/actions/go-setup
- run: make deps-backend
- run: make backend
env:
@@ -102,7 +110,7 @@ jobs:
GOEXPERIMENT:
test-unit:
- if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
+ if: needs.files-changed.outputs.backend == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
@@ -111,6 +119,7 @@ jobs:
env:
discovery.type: single-node
xpack.security.enabled: false
+ ES_JAVA_OPTS: "-Xms512m -Xmx512m" # reduce from ES default of 50%
ports:
- "9200:9200"
meilisearch:
@@ -141,12 +150,7 @@ jobs:
- 10000:10000
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
+ - uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 minio devstoreaccount1.azurite.local mysql elasticsearch meilisearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
@@ -170,7 +174,7 @@ jobs:
- run: make test-check
test-mysql:
- if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
+ if: needs.files-changed.outputs.backend == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
@@ -189,6 +193,7 @@ jobs:
env:
discovery.type: single-node
xpack.security.enabled: false
+ ES_JAVA_OPTS: "-Xms512m -Xmx512m" # reduce from ES default of 50%
ports:
- "9200:9200"
smtpimap:
@@ -200,12 +205,7 @@ jobs:
- "993:993"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
+ - uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mysql elasticsearch smtpimap" | sudo tee -a /etc/hosts'
- run: make deps-backend
@@ -220,7 +220,7 @@ jobs:
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'
+ if: needs.files-changed.outputs.backend == 'true'
needs: files-changed
runs-on: ubuntu-latest
services:
@@ -238,12 +238,7 @@ jobs:
- 10000:10000
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
+ - uses: ./.github/actions/go-setup
- name: Add hosts to /etc/hosts
run: '[ -e "/.dockerenv" ] || [ -e "/run/.containerenv" ] || echo "127.0.0.1 mssql devstoreaccount1.azurite.local" | sudo tee -a /etc/hosts'
- run: make deps-backend
diff --git a/.github/workflows/pull-e2e-tests.yml b/.github/workflows/pull-e2e-tests.yml
index 7bf73d4a0f..f0283f4022 100644
--- a/.github/workflows/pull-e2e-tests.yml
+++ b/.github/workflows/pull-e2e-tests.yml
@@ -20,18 +20,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
- with:
- go-version-file: go.mod
- check-latest: true
- cache: false
- - uses: ./.github/actions/go-cache
- - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8
- - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- with:
- node-version: 24
- cache: pnpm
- cache-dependency-path: pnpm-lock.yaml
+ - uses: ./.github/actions/go-setup
+ - uses: ./.github/actions/node-setup
- run: make deps-frontend
- run: make frontend
- run: make deps-backend
diff --git a/.github/workflows/pull-labeler.yml b/.github/workflows/pull-labeler.yml
index b27fc32cdb..34395c8d9e 100644
--- a/.github/workflows/pull-labeler.yml
+++ b/.github/workflows/pull-labeler.yml
@@ -1,8 +1,10 @@
name: labeler
on:
- pull_request_target:
- types: [opened, synchronize, reopened]
+ # pull_request_target is required to label PRs from forks; jobs only use pinned
+ # actions or base-branch checkout, never PR-head code.
+ pull_request_target: # zizmor: ignore[dangerous-triggers]
+ types: [opened, synchronize, reopened, edited, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -18,3 +20,28 @@ jobs:
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
sync-labels: true
+
+ pr-title:
+ if: github.event.pull_request.draft == false
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: read
+ pull-requests: write
+ steps:
+ # Base-branch checkout only: pull_request_target runs with elevated token; never run PR-head code here.
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ ref: ${{ github.event.pull_request.base.sha }}
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: 24
+ # Labels are only synced after the title lints, so an invalid title never reaches the label diff.
+ - run: node ./tools/ci-tools.ts lint-pr-title
+ env:
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ - run: node ./tools/ci-tools.ts set-pr-labels
+ env:
+ PR_TITLE: ${{ github.event.pull_request.title }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ GITHUB_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/pull-pr-title.yml b/.github/workflows/pull-pr-title.yml
deleted file mode 100644
index 6dadcb7058..0000000000
--- a/.github/workflows/pull-pr-title.yml
+++ /dev/null
@@ -1,31 +0,0 @@
-name: pr-title
-
-on:
- pull_request:
- types:
- - opened
- - edited
- - reopened
- - synchronize
- - ready_for_review
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
- cancel-in-progress: true
-
-permissions:
- contents: read
-
-jobs:
- lint-pr-title:
- if: github.event.pull_request.draft == false
- runs-on: ubuntu-latest
- timeout-minutes: 5
- steps:
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
- with:
- node-version: 24
- - run: make lint-pr-title
- env:
- PR_TITLE: ${{ github.event.pull_request.title }}
diff --git a/.github/workflows/release-nightly-snapcraft.yml b/.github/workflows/release-nightly-snapcraft.yml
new file mode 100644
index 0000000000..0f9ac1d423
--- /dev/null
+++ b/.github/workflows/release-nightly-snapcraft.yml
@@ -0,0 +1,41 @@
+name: release-nightly-snapcraft
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build-and-publish:
+ runs-on: ubuntu-latest
+
+ env:
+ SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
+
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+
+ - name: Install snapcraft
+ run: sudo snap install snapcraft --classic
+
+ - name: Remote build
+ run: |
+ snapcraft remote-build \
+ --launchpad-accept-public-upload \
+ --build-for=amd64,arm64,armhf
+
+ - name: List built snaps
+ run: find . -maxdepth 1 -type f -name '*.snap' -print
+
+ - name: Upload and release snapcraft nightly build
+ run: |
+ set -euo pipefail
+
+ for snap in ./*.snap; do
+ echo "Uploading $snap to edge"
+ snapcraft upload --release="latest/edge" "$snap"
+ done
diff --git a/.shellcheckrc b/.shellcheckrc
new file mode 100644
index 0000000000..a012fba96e
--- /dev/null
+++ b/.shellcheckrc
@@ -0,0 +1 @@
+disable=SC1091,SC2001,SC2002,SC2016,SC2028,SC2046,SC2124,SC2128,SC2129,SC2154,SC2155,SC2164,SC2181,SC2207
diff --git a/Makefile b/Makefile
index 94f07a4412..649465b2b4 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,9 @@ SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.2 # renova
XGO_PACKAGE ?= src.techknowlogick.com/xgo@v1.9.0 # renovate: datasource=go
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1.3.0 # renovate: datasource=go
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 # renovate: datasource=go
+SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:61862eba1fcf09a484ebcc6feea46f1782532571a34ed51fedf90dd25f925a8d # renovate: datasource=docker
+
+CONTAINER_RUNTIME ?= $(shell hash docker >/dev/null 2>&1 && echo docker || echo podman)
HAS_GO := $(shell hash $(GO) > /dev/null 2>&1 && echo yes)
ifeq ($(HAS_GO), yes)
@@ -271,7 +274,7 @@ checks-frontend: lockfile-check svg-check ## check frontend files
checks-backend: tidy-check swagger-check openapi3-check fmt-check swagger-validate security-check ## check backend files
.PHONY: lint
-lint: lint-frontend lint-backend lint-templates lint-swagger lint-spell lint-md lint-actions lint-json lint-yaml ## lint everything
+lint: lint-frontend lint-backend lint-templates lint-swagger lint-spell lint-md lint-actions lint-json lint-yaml lint-shell ## lint everything
.PHONY: lint-fix
lint-fix: lint-frontend-fix lint-backend-fix lint-spell-fix ## lint everything and fix issues
@@ -318,10 +321,6 @@ lint-md: node_modules ## lint markdown files
lint-md-fix: node_modules ## lint markdown files and fix issues
pnpm exec markdownlint --fix *.md
-.PHONY: lint-pr-title
-lint-pr-title: ## lint PR title against Conventional Commits (set PR_TITLE=...)
- @node ./tools/lint-pr-title.ts
-
.PHONY: lint-spell
lint-spell: ## lint spelling
@git ls-files $(SPELLCHECK_FILES) | xargs go run $(MISSPELL_PACKAGE) -dict assets/misspellings.csv -error
@@ -348,6 +347,10 @@ lint-actions: .venv ## lint action workflow files
@$(GO) run $(ACTIONLINT_PACKAGE)
@uv run --frozen zizmor --quiet --min-confidence=medium .github
+.PHONY: lint-shell
+lint-shell: ## lint shell scripts
+ @SHELLCHECK_IMAGE=$(SHELLCHECK_IMAGE) CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) ./tools/lint-shell.sh $$(git ls-files '*.sh')
+
.PHONY: lint-templates
lint-templates: .venv node_modules ## lint template files
@node tools/lint-templates-svg.ts
@@ -473,11 +476,11 @@ migrations.individual.test\#%:
.PHONY: playwright
playwright: deps-frontend
- @./tools/test-e2e.sh install
+ @CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) ./tools/test-e2e.sh install
.PHONY: test-e2e
test-e2e: playwright frontend backend
- @EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh run $(GITEA_TEST_E2E_FLAGS)
+ @CONTAINER_RUNTIME=$(CONTAINER_RUNTIME) EXECUTABLE=$(EXECUTABLE) ./tools/test-e2e.sh run $(GITEA_TEST_E2E_FLAGS)
.PHONY: build
build: frontend backend ## build everything
diff --git a/contrib/upgrade.sh b/contrib/upgrade.sh
index 2593d24509..71d0435d14 100755
--- a/contrib/upgrade.sh
+++ b/contrib/upgrade.sh
@@ -126,6 +126,7 @@ giteacmd manager flush-queues
echo "Stopping gitea at $(date)"
$service_stop
echo "Creating backup in $giteahome"
+# shellcheck disable=SC2086 # flag string
giteacmd dump $backupopts
echo "Updating binary at $giteabin"
cp -f "$giteabin" "$giteabin.bak" && mv -f "$binname" "$giteabin"
diff --git a/docker/rootless/usr/local/bin/docker-entrypoint.sh b/docker/rootless/usr/local/bin/docker-entrypoint.sh
index ca509214bf..4ffc4ff3a1 100755
--- a/docker/rootless/usr/local/bin/docker-entrypoint.sh
+++ b/docker/rootless/usr/local/bin/docker-entrypoint.sh
@@ -13,5 +13,5 @@ fi
if [ $# -gt 0 ]; then
exec "$@"
else
- exec /usr/local/bin/gitea -c ${GITEA_APP_INI} web
+ exec /usr/local/bin/gitea -c "${GITEA_APP_INI}" web
fi
diff --git a/docker/rootless/usr/local/bin/docker-setup.sh b/docker/rootless/usr/local/bin/docker-setup.sh
index feab02a379..3320d69dde 100755
--- a/docker/rootless/usr/local/bin/docker-setup.sh
+++ b/docker/rootless/usr/local/bin/docker-setup.sh
@@ -1,23 +1,23 @@
#!/bin/bash
# Prepare git folder
-mkdir -p ${HOME} && chmod 0700 ${HOME}
-if [ ! -w ${HOME} ]; then echo "${HOME} is not writable"; exit 1; fi
+mkdir -p "${HOME}" && chmod 0700 "${HOME}"
+if [ ! -w "${HOME}" ]; then echo "${HOME} is not writable"; exit 1; fi
# Prepare custom folder
-mkdir -p ${GITEA_CUSTOM} && chmod 0700 ${GITEA_CUSTOM}
+mkdir -p "${GITEA_CUSTOM}" && chmod 0700 "${GITEA_CUSTOM}"
# Prepare temp folder
-mkdir -p ${GITEA_TEMP} && chmod 0700 ${GITEA_TEMP}
-if [ ! -w ${GITEA_TEMP} ]; then echo "${GITEA_TEMP} is not writable"; exit 1; fi
+mkdir -p "${GITEA_TEMP}" && chmod 0700 "${GITEA_TEMP}"
+if [ ! -w "${GITEA_TEMP}" ]; then echo "${GITEA_TEMP} is not writable"; exit 1; fi
#Prepare config file
-if [ ! -f ${GITEA_APP_INI} ]; then
+if [ ! -f "${GITEA_APP_INI}" ]; then
#Prepare config file folder
- GITEA_APP_INI_DIR=$(dirname ${GITEA_APP_INI})
- mkdir -p ${GITEA_APP_INI_DIR} && chmod 0700 ${GITEA_APP_INI_DIR}
- if [ ! -w ${GITEA_APP_INI_DIR} ]; then echo "${GITEA_APP_INI_DIR} is not writable"; exit 1; fi
+ GITEA_APP_INI_DIR=$(dirname "${GITEA_APP_INI}")
+ mkdir -p "${GITEA_APP_INI_DIR}" && chmod 0700 "${GITEA_APP_INI_DIR}"
+ if [ ! -w "${GITEA_APP_INI_DIR}" ]; then echo "${GITEA_APP_INI_DIR} is not writable"; exit 1; fi
# Set INSTALL_LOCK to true only if SECRET_KEY is not empty and
# INSTALL_LOCK is empty
@@ -34,7 +34,7 @@ if [ ! -f ${GITEA_APP_INI} ]; then
ROOT_URL=${ROOT_URL:-""} \
DISABLE_SSH=${DISABLE_SSH:-"false"} \
SSH_PORT=${SSH_PORT:-"2222"} \
- SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-$SSH_PORT} \
+ SSH_LISTEN_PORT=${SSH_LISTEN_PORT:-} \
DB_TYPE=${DB_TYPE:-"sqlite3"} \
DB_HOST=${DB_HOST:-"localhost:3306"} \
DB_NAME=${DB_NAME:-"gitea"} \
@@ -44,8 +44,8 @@ if [ ! -f ${GITEA_APP_INI} ]; then
DISABLE_REGISTRATION=${DISABLE_REGISTRATION:-"false"} \
REQUIRE_SIGNIN_VIEW=${REQUIRE_SIGNIN_VIEW:-"false"} \
SECRET_KEY=${SECRET_KEY:-""} \
- envsubst < /etc/templates/app.ini > ${GITEA_APP_INI}
+ envsubst < /etc/templates/app.ini > "${GITEA_APP_INI}"
fi
# Replace app.ini settings with env variables in the form GITEA__SECTION_NAME__KEY_NAME
-environment-to-ini --config ${GITEA_APP_INI}
+environment-to-ini --config "${GITEA_APP_INI}"
diff --git a/go.mod b/go.mod
index 963e5f0882..63767aa7b0 100644
--- a/go.mod
+++ b/go.mod
@@ -110,12 +110,12 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
gitlab.com/gitlab-org/api/client-go/v2 v2.26.0
go.yaml.in/yaml/v4 v4.0.0-rc.3
- golang.org/x/crypto v0.51.0
+ golang.org/x/crypto v0.52.0
golang.org/x/image v0.40.0
- golang.org/x/net v0.54.0
+ golang.org/x/net v0.55.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/sys v0.45.0
golang.org/x/text v0.37.0
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
diff --git a/go.sum b/go.sum
index 0510378cb4..9ade1fab9e 100644
--- a/go.sum
+++ b/go.sum
@@ -790,8 +790,8 @@ 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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
-golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
+golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
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.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
@@ -822,8 +822,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
-golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
-golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
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=
@@ -870,8 +870,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.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
-golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
+golang.org/x/sys v0.45.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=
diff --git a/models/issues/review.go b/models/issues/review.go
index 78ef0d20c2..9936622219 100644
--- a/models/issues/review.go
+++ b/models/issues/review.go
@@ -483,6 +483,14 @@ func SubmitReview(ctx context.Context, doer *user_model.User, issue *Issue, revi
if _, err := sess.ID(review.ID).Cols("content, type, official, commit_id, stale").Update(review); err != nil {
return nil, nil, err
}
+
+ // make sure the leftover review request is cleared, consistent with CreateReview
+ if reviewType != ReviewTypePending {
+ if _, err := sess.Where(builder.Eq{"reviewer_id": doer.ID, "issue_id": issue.ID, "type": ReviewTypeRequest}).
+ Delete(new(Review)); err != nil {
+ return nil, nil, err
+ }
+ }
}
comm, err := CreateComment(ctx, &CreateCommentOptions{
diff --git a/models/issues/review_test.go b/models/issues/review_test.go
index 092d88d174..a384dbd30f 100644
--- a/models/issues/review_test.go
+++ b/models/issues/review_test.go
@@ -303,6 +303,46 @@ func TestDeleteDismissedReview(t *testing.T) {
unittest.AssertNotExistsBean(t, &issues_model.Comment{ID: comment.ID})
}
+func TestSubmitReviewClearsStaleReviewRequest(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
+ assert.NoError(t, issue.LoadRepo(t.Context()))
+ assert.NoError(t, issue.Repo.LoadOwner(t.Context()))
+ reviewer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ // the reviewer is requested to review the pull request
+ requestReview, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{
+ Type: issues_model.ReviewTypeRequest,
+ Issue: issue,
+ Reviewer: reviewer,
+ })
+ assert.NoError(t, err)
+
+ // the reviewer starts a pending review (e.g. by adding code comments)
+ pendingReview, err := issues_model.CreateReview(t.Context(), issues_model.CreateReviewOptions{
+ Type: issues_model.ReviewTypePending,
+ Issue: issue,
+ Reviewer: reviewer,
+ })
+ assert.NoError(t, err)
+
+ // submitting the pending review must clear the leftover review request,
+ // otherwise the reviewer can no longer be re-requested afterwards
+ review, _, err := issues_model.SubmitReview(t.Context(), reviewer, issue, issues_model.ReviewTypeComment, "looks good", "", false, nil)
+ assert.NoError(t, err)
+ assert.Equal(t, pendingReview.ID, review.ID)
+ assert.Equal(t, issues_model.ReviewTypeComment, review.Type)
+
+ unittest.AssertNotExistsBean(t, &issues_model.Review{ID: requestReview.ID})
+
+ // the reviewer can be re-requested afterwards (no-op before the fix)
+ comment, err := issues_model.AddReviewRequest(t.Context(), issue, reviewer, doer, false)
+ assert.NoError(t, err)
+ assert.NotNil(t, comment)
+}
+
func TestAddReviewRequest(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go
index cc7695726b..c49e1267a7 100644
--- a/modules/packages/npm/creator.go
+++ b/modules/packages/npm/creator.go
@@ -181,10 +181,11 @@ func (u *User) UnmarshalJSON(data []byte) error {
return nil
}
-// Repository https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#version
+// Repository https://docs.npmjs.com/cli/v11/configuring-npm/package-json#repository
type Repository struct {
- Type string `json:"type"`
- URL string `json:"url"`
+ Type string `json:"type"`
+ URL string `json:"url"`
+ Directory string `json:"directory,omitempty"`
}
// PackageAttachment https://github.com/npm/registry/blob/master/docs/REGISTRY-API.md#package
diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go
index 40c50de91f..7474d6c9f4 100644
--- a/modules/packages/npm/creator_test.go
+++ b/modules/packages/npm/creator_test.go
@@ -28,8 +28,9 @@ func TestParsePackage(t *testing.T) {
data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
integrity := "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg=="
repository := Repository{
- Type: "gitea",
- URL: "http://localhost:3000/gitea/test.git",
+ Type: "gitea",
+ URL: "http://localhost:3000/gitea/test.git",
+ Directory: "packages/test-package",
}
t.Run("InvalidUpload", func(t *testing.T) {
@@ -298,6 +299,7 @@ func TestParsePackage(t *testing.T) {
assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"])
assert.Equal(t, repository.Type, p.Metadata.Repository.Type)
assert.Equal(t, repository.URL, p.Metadata.Repository.URL)
+ assert.Equal(t, repository.Directory, p.Metadata.Repository.Directory)
})
t.Run("ValidLicenseMap", func(t *testing.T) {
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index 99c1535155..176b913515 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -434,6 +434,10 @@ type ChangesPayload struct {
type PullRequestPayload struct {
// The action performed on the pull request
Action HookIssueAction `json:"action"`
+ // The SHA of the most recent commit on the PR head branch before the push
+ Before string `json:"before,omitempty"`
+ // The SHA of the most recent commit on the PR head branch after the push
+ After string `json:"after,omitempty"`
// The index number of the pull request
Index int64 `json:"number"`
// Changes made to the pull request (for edit actions)
diff --git a/modules/structs/hook_test.go b/modules/structs/hook_test.go
new file mode 100644
index 0000000000..a593b4eb98
--- /dev/null
+++ b/modules/structs/hook_test.go
@@ -0,0 +1,59 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package structs
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/modules/json"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestPullRequestPayloadSynchronizeBeforeAfter(t *testing.T) {
+ payload := &PullRequestPayload{
+ Action: HookIssueSynchronized,
+ Before: "1111111111111111111111111111111111111111",
+ After: "2222222222222222222222222222222222222222",
+ Index: 12,
+ }
+
+ data, err := json.Marshal(payload)
+ require.NoError(t, err)
+
+ assert.JSONEq(t, `{
+ "action": "synchronized",
+ "before": "1111111111111111111111111111111111111111",
+ "after": "2222222222222222222222222222222222222222",
+ "number": 12,
+ "commit_id": "",
+ "pull_request": null,
+ "repository": null,
+ "requested_reviewer": null,
+ "review": null,
+ "sender": null
+ }`, string(data))
+}
+
+func TestPullRequestPayloadNonSynchronizeOmitsBeforeAfter(t *testing.T) {
+ payload := &PullRequestPayload{
+ Action: HookIssueOpened,
+ Index: 12,
+ }
+
+ data, err := json.Marshal(payload)
+ require.NoError(t, err)
+
+ assert.JSONEq(t, `{
+ "action": "opened",
+ "number": 12,
+ "commit_id": "",
+ "pull_request": null,
+ "repository": null,
+ "requested_reviewer": null,
+ "review": null,
+ "sender": null
+ }`, string(data))
+}
diff --git a/modules/templates/util_render.go b/modules/templates/util_render.go
index 9d05bb2a0b..b78196bd5c 100644
--- a/modules/templates/util_render.go
+++ b/modules/templates/util_render.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/emoji"
+ "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
@@ -223,6 +224,25 @@ func (ut *RenderUtils) MarkdownToHtml(input string) template.HTML { //nolint:rev
return output
}
+// RenderPackageMarkdown renders package page Markdown so relative links resolve against the
+// linked repository's default branch instead of the site root, falling back to plain rendering
+// when there is no linked repository. pkgTreePath optionally roots links in a subdirectory
+// (e.g. npm's repository.directory for monorepo packages).
+func (ut *RenderUtils) RenderPackageMarkdown(input string, linkedRepo *repo.Repository, pkgTreePath ...string) template.HTML {
+ if linkedRepo == nil {
+ return `
` + ut.MarkdownToHtml(input) + `
`
+ }
+ rctx := renderhelper.NewRenderContextRepoFile(ut.ctx, linkedRepo, renderhelper.RepoFileOptions{
+ CurrentRefSubURL: git.RefNameFromBranch(linkedRepo.DefaultBranch).RefWebLinkPath(),
+ CurrentTreePath: util.OptionalArg(pkgTreePath),
+ })
+ output, err := markdown.RenderString(rctx, input)
+ if err != nil {
+ log.Error("RenderString: %v", err)
+ }
+ return `` + output + `
`
+}
+
func (ut *RenderUtils) RenderLabels(labels []*issues_model.Label, repoLink string, issue *issues_model.Issue) template.HTML {
isPullRequest := issue != nil && issue.IsPull
baseLink := fmt.Sprintf("%s/%s", repoLink, util.Iif(isPullRequest, "pulls", "issues"))
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index de7768e91c..61dcb4937f 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -194,6 +194,38 @@ space
assert.Equal(t, expected, string(newTestRenderUtils(t).MarkdownToHtml(testInput())))
}
+func TestRenderPackageMarkdown(t *testing.T) {
+ defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
+ mockRepo := &repo.Repository{
+ ID: 1, OwnerName: "user13", Name: "repo11", DefaultBranch: "main",
+ Owner: &user_model.User{ID: 13, Name: "user13"},
+ Units: []*repo.RepoUnit{},
+ }
+ ut := newTestRenderUtils(t)
+
+ t.Run("LinkedRepoWithDirectory", func(t *testing.T) {
+ rendered := ut.RenderPackageMarkdown("[docs](docs/getting-started.md)\n", mockRepo, "pkg-subdir")
+ expected := ``
+ assert.Equal(t, expected, strings.TrimSpace(string(rendered)))
+ })
+
+ t.Run("LinkedRepoWithEmptyDirectory", func(t *testing.T) {
+ rendered := ut.RenderPackageMarkdown("[docs](docs/getting-started.md)", mockRepo, "")
+ expected := ``
+ assert.Equal(t, expected, strings.TrimSpace(string(rendered)))
+ })
+
+ t.Run("UnlinkedRepo", func(t *testing.T) {
+ rendered := ut.RenderPackageMarkdown("[docs](docs/getting-started.md)", nil, "pkg-subdir")
+ expected := ``
+ assert.Equal(t, expected, strings.TrimSpace(string(rendered)))
+ })
+}
+
func TestRenderLabels(t *testing.T) {
ut := newTestRenderUtils(t)
label := &issues.Label{ID: 123, Name: "label-name", Color: "label-color"}
diff --git a/options/locale/locale_ga-IE.json b/options/locale/locale_ga-IE.json
index 43f83c3423..3ef188568b 100644
--- a/options/locale/locale_ga-IE.json
+++ b/options/locale/locale_ga-IE.json
@@ -105,6 +105,7 @@
"copy_error": "Theip ar an gcóipeáil",
"copy_type_unsupported": "Ní féidir an cineál comhaid seo a chóipeáil",
"copy_filename": "Cóipeáil ainm comhaid",
+ "copy_output": "Cóipeáil aschur",
"write": "Scríobh",
"preview": "Réamhamharc",
"loading": "Á lódáil...",
diff --git a/options/locale/locale_ja-JP.json b/options/locale/locale_ja-JP.json
index ffbd8c0d9e..4af9d945b2 100644
--- a/options/locale/locale_ja-JP.json
+++ b/options/locale/locale_ja-JP.json
@@ -81,9 +81,11 @@
"retry": "再試行",
"rerun": "再実行",
"rerun_all": "すべてのジョブを再実行",
+ "rerun_failed": "失敗したジョブを再実行",
"save": "保存",
"add": "追加",
"add_all": "すべて追加",
+ "dismiss": "閉じる",
"remove": "除去",
"remove_all": "すべて除去",
"remove_label_str": "アイテム「%s」を削除",
@@ -103,6 +105,7 @@
"copy_error": "コピーに失敗しました",
"copy_type_unsupported": "このファイルタイプはコピーできません",
"copy_filename": "ファイル名をコピー",
+ "copy_output": "出力をコピー",
"write": "書き込み",
"preview": "プレビュー",
"loading": "読み込み中…",
@@ -120,6 +123,7 @@
"unpin": "ピン留め解除",
"artifacts": "成果物",
"expired": "期限切れ",
+ "artifact_expires_at": "保存期限 %s",
"confirm_delete_artifact": "アーティファクト '%s' を削除してよろしいですか?",
"archived": "アーカイブ",
"concept_system_global": "グローバル",
@@ -167,9 +171,12 @@
"search.exact_tooltip": "検索語と完全に一致する結果だけを含めます",
"search.repo_kind": "リポジトリを検索…",
"search.user_kind": "ユーザーを検索…",
+ "search.badge_kind": "バッジを検索…",
"search.org_kind": "組織を検索…",
"search.team_kind": "チームを検索…",
"search.code_kind": "コードを検索…",
+ "search.code_empty": "コード検索を開始。",
+ "search.code_empty_description": "コード全体から検索するキーワードを入力してください。",
"search.code_search_unavailable": "コード検索は現在利用できません。 サイト管理者にお問い合わせください。",
"search.code_search_by_git_grep": "現在のコード検索は \"git grep\" によって行われています。 サイト管理者がリポジトリインデクサーを有効にすれば、より優れた結果が得られる可能性があります。",
"search.package_kind": "パッケージを検索…",
@@ -210,11 +217,15 @@
"editor.buttons.switch_to_legacy.tooltip": "レガシーエディタを使用する",
"editor.buttons.enable_monospace_font": "等幅フォントを有効にする",
"editor.buttons.disable_monospace_font": "等幅フォントを無効にする",
+ "editor.code_editor.command_palette": "コマンドパレット",
+ "editor.code_editor.find": "検索",
+ "editor.code_editor.placeholder": "ファイルの内容をここに入力",
"filter.string.asc": "A–Z",
"filter.string.desc": "Z–A",
"error.occurred": "エラーが発生しました",
"error.report_message": "Gitea のバグが疑われる場合は、GitHubでIssueを検索して、見つからなければ新しいIssueを作成してください。",
"error.not_found": "ターゲットが見つかりませんでした。",
+ "error.permission_denied": "権限がありません。",
"error.network_error": "ネットワークエラー",
"startpage.app_desc": "自分で立てる、超簡単 Git サービス",
"startpage.install": "簡単インストール",
@@ -261,7 +272,7 @@
"install.lfs_path": "Git LFSルートパス",
"install.lfs_path_helper": "Git LFSで管理するファイルが、このディレクトリに保存されます。 空欄にするとGit LFSを無効にします。",
"install.run_user": "実行ユーザー名",
- "install.run_user_helper": "オペレーティングシステム上のユーザー名です。 Giteaをこのユーザーとして実行します。 このユーザーはリポジトリルートパスへのアクセス権を持っている必要があります。",
+ "install.run_user_helper": "Giteaを実行しているオペレーティングシステム上のユーザー名です。 このユーザーにはデータパスへの書き込み権限が必要です。 この値は自動的に検出されたもので、ここでは変更できません。 別のユーザーを使用するには、そのアカウントからGiteaを再起動してください。",
"install.domain": "サーバードメイン",
"install.domain_helper": "サーバーのドメインまたはホストアドレス。",
"install.ssh_port": "SSHサーバーのポート",
@@ -284,12 +295,6 @@
"install.register_confirm": "登録にはメールによる確認が必要",
"install.mail_notify": "メール通知を有効にする",
"install.server_service_title": "サーバーと外部サービスの設定",
- "install.offline_mode": "ローカルモードを有効にする",
- "install.offline_mode_popup": "外のCDNサービスを使わず、すべてのリソースを自前で提供します。",
- "install.disable_gravatar": "Gravatarを無効にする",
- "install.disable_gravatar_popup": "Gravatarと外のアバターソースを無効にします。 アバターをローカルにアップロードしていないユーザーには、デフォルトのアバターが使用されます。",
- "install.federated_avatar_lookup": "フェデレーテッド・アバターを有効にする",
- "install.federated_avatar_lookup_popup": "Libravatarを使用したフェデレーテッド・アバター検索を有効にします。",
"install.disable_registration": "セルフ登録を無効にする",
"install.disable_registration_popup": "ユーザーのセルフ登録を無効にします。 新しいユーザーアカウントを作成できるのは管理者だけとなります。",
"install.allow_only_external_registration_popup": "外部サービスを使用した登録のみを許可",
@@ -309,12 +314,10 @@
"install.admin_email": "メールアドレス",
"install.install_btn_confirm": "Giteaをインストール",
"install.test_git_failed": "'git'コマンドが確認できません: %v",
- "install.sqlite3_not_available": "GiteaのこのバージョンはSQLite3をサポートしていません。 公式のバイナリ版を %s からダウンロードしてください。 ('gobuild'版でないもの)",
"install.invalid_db_setting": "データベース設定が無効です: %v",
"install.invalid_db_table": "データベーステーブルの \"%s\" が無効です: %v",
"install.invalid_repo_path": "リポジトリのルートパスが無効です: %v",
"install.invalid_app_data_path": "アプリのデータパス (APP_DATA_PATH) が無効です: %v",
- "install.run_user_not_match": "実行ユーザー名が、現在のユーザー名ではありません: %s -> %s",
"install.internal_token_failed": "内部トークンの生成に失敗しました: %v",
"install.secret_key_failed": "シークレットキーの生成に失敗しました: %v",
"install.save_config_failed": "設定ファイルの保存に失敗しました: %v",
@@ -547,6 +550,7 @@
"form.glob_pattern_error": "のglobパターンが不正です: %s.",
"form.regex_pattern_error": "の正規表現パターンが不正です: %s.",
"form.username_error": "は、英数字('0-9','a-z','A-Z')、ダッシュ('-')、アンダースコア('_')、ドット('.')だけを含めることができます。 先頭と末尾は英数字以外の文字にはできません。 また、連続した英数字以外の文字も許されません。",
+ "form.invalid_slug_error": " は無効です。",
"form.invalid_group_team_map_error": "のマッピングが無効です: %s",
"form.unknown_error": "不明なエラー:",
"form.captcha_incorrect": "CAPTCHAコードが正しくありません。",
@@ -635,14 +639,8 @@
"user.block.unblock.failure": "ユーザーのブロック解除に失敗しました: %s",
"user.block.blocked": "あなたはこのユーザーをブロックしています。",
"user.block.title": "ユーザーをブロックする",
- "user.block.info": "ユーザーをブロックすると、そのユーザーは、プルリクエストやイシューの作成、コメントの投稿など、リポジトリに対する操作ができなくなります。 ユーザーのブロックについてはよく確認してください。",
- "user.block.info_1": "ユーザーをブロックすることで、あなたのアカウントとあなたのリポジトリに対する以下の行為を阻止します:",
- "user.block.info_2": "あなたのアカウントのフォロー",
- "user.block.info_3": "あなたのユーザー名で@メンションして通知を送ること",
- "user.block.info_4": "そのユーザーのリポジトリに、あなたを共同作業者として招待すること",
- "user.block.info_5": "リポジトリへの、スター、フォーク、ウォッチ",
- "user.block.info_6": "イシューやプルリクエストの作成、コメント投稿",
- "user.block.info_7": "イシューやプルリクエストでの、あなたのコメントに対するリアクションの送信",
+ "user.block.info": "ユーザーをブロックすると、そのユーザーは、プルリクエストやイシューの作成、コメントの投稿など、リポジトリに対する操作ができなくなります。",
+ "user.block.info.docs": "ユーザーのブロックについて詳しくはこちらをご覧ください。",
"user.block.user_to_block": "ブロックするユーザー",
"user.block.note": "メモ",
"user.block.note.title": "メモ(任意):",
@@ -650,6 +648,7 @@
"user.block.note.edit": "メモを編集",
"user.block.list": "ブロックしたユーザー",
"user.block.list.none": "ブロックしているユーザーはいません。",
+ "settings.general": "一般",
"settings.profile": "プロフィール",
"settings.account": "アカウント",
"settings.appearance": "外観",
@@ -970,7 +969,6 @@
"repo.visibility_description": "オーナー、または権限を持つ組織のメンバーだけが、リポジトリを見ることができます。",
"repo.visibility_helper": "リポジトリをプライベートにする",
"repo.visibility_helper_forced": "サイト管理者の設定により、新しいリポジトリは強制的にプライベートになります。",
- "repo.visibility_fork_helper": "(この変更はすべてのフォークに適用されます)",
"repo.clone_helper": "クローンに関してお困りであればヘルプを参照しましょう。",
"repo.fork_repo": "リポジトリをフォーク",
"repo.fork_from": "フォーク元",
@@ -1042,6 +1040,7 @@
"repo.forks": "フォーク",
"repo.stars": "スター",
"repo.reactions_more": "さらに %d 件",
+ "repo.reactions": "リアクション",
"repo.unit_disabled": "サイト管理者がこのリポジトリセクションを無効にしています。",
"repo.language_other": "その他",
"repo.adopt_search": "未登録リポジトリを探すユーザー名を入力… (空ですべてを探索)",
@@ -1062,8 +1061,8 @@
"repo.transfer.accept_desc": "\"%s\" に移転",
"repo.transfer.reject": "移転を拒否",
"repo.transfer.reject_desc": "\"%s\" への移転をキャンセル",
- "repo.transfer.no_permission_to_accept": "この移転を承認する権限がありません。",
- "repo.transfer.no_permission_to_reject": "この移転を拒否する権限がありません。",
+ "repo.transfer.is_transferring": "移転処理中…",
+ "repo.transfer.is_transferring_prompt": "リポジトリは %s に移転処理中です",
"repo.desc.private": "プライベート",
"repo.desc.public": "公開",
"repo.desc.public_access": "公開アクセス",
@@ -1214,7 +1213,7 @@
"repo.ambiguous_runes_description": "このファイルには、他の文字と見間違える可能性があるUnicode文字が含まれています。 それが意図的なものと考えられる場合は、この警告を無視して構いません。 それらの文字を表示するにはエスケープボタンを使用します。",
"repo.invisible_runes_line": "この行には不可視のUnicode文字があります",
"repo.ambiguous_runes_line": "この行には曖昧(ambiguous)なUnicode文字があります",
- "repo.ambiguous_character": "%[1]c [U+%04[1]X] は %[2]c [U+%04[2]X] と混同するおそれがあります",
+ "repo.ambiguous_character": "%[1]s は %[2]s と混同するおそれがあります",
"repo.escape_control_characters": "エスケープ",
"repo.unescape_control_characters": "エスケープ解除",
"repo.file_copy_permalink": "パーマリンクをコピー",
@@ -1307,9 +1306,9 @@
"repo.editor.upload_file_is_locked": "ファイル \"%s\" は %s がロックしています。",
"repo.editor.upload_files_to_dir": "\"%s\" にファイルをアップロード",
"repo.editor.cannot_commit_to_protected_branch": "保護されたブランチ \"%s\" にコミットすることはできません。",
- "repo.editor.no_commit_to_branch": "ブランチに直接コミットすることはできません、なぜなら:",
+ "repo.editor.no_commit_to_branch": "ブランチに直接コミットすることは許可されません、なぜなら:",
"repo.editor.user_no_push_to_branch": "ユーザーはブランチにプッシュできません",
- "repo.editor.require_signed_commit": "ブランチでは署名されたコミットが必須です",
+ "repo.editor.require_signed_commit": "ブランチには署名済みコミットが必要です",
"repo.editor.cherry_pick": "チェリーピック %s:",
"repo.editor.revert": "リバート %s:",
"repo.editor.failed_to_commit": "変更のコミットに失敗しました。",
@@ -1323,7 +1322,7 @@
"repo.commits.desc": "ソースコードの変更履歴を参照します。",
"repo.commits.commits": "コミット",
"repo.commits.no_commits": "共通のコミットはありません。 \"%s\" と \"%s\" の履歴はすべて異なっています。",
- "repo.commits.nothing_to_compare": "二つのブランチは同じ内容です。",
+ "repo.commits.nothing_to_compare": "差分はありません。",
"repo.commits.search.tooltip": "キーワード \"author:\"、\"committer:\"、\"after:\"、\"before:\" を付けて指定できます。 例 \"revert author:Alice before:2019-01-13\"",
"repo.commits.search_branch": "このブランチ",
"repo.commits.search_all": "すべてのブランチ",
@@ -1355,10 +1354,13 @@
"repo.projects.desc": "プロジェクトでイシューとプルリクエストを管理します。",
"repo.projects.description": "説明 (オプション)",
"repo.projects.description_placeholder": "説明",
+ "repo.projects.empty": "プロジェクトはまだありません。",
+ "repo.projects.empty_description": "イシューとプルリクエストを整理するために、プロジェクトを作成してください。",
"repo.projects.create": "プロジェクトを作成",
"repo.projects.title": "タイトル",
"repo.projects.new": "新しいプロジェクト",
"repo.projects.new_subheader": "作業の調整・追跡・更新をひとつの場所で行い、プロジェクトの透明性と良好な進捗を維持します。",
+ "repo.projects.no_results": "検索条件に一致するプロジェクトはありません。",
"repo.projects.create_success": "プロジェクト \"%s\" を作成しました。",
"repo.projects.deletion": "プロジェクトの削除",
"repo.projects.deletion_desc": "プロジェクトを削除し、関連するすべてのイシューから除去します。続行しますか?",
@@ -1383,6 +1385,7 @@
"repo.projects.column.delete": "列を削除",
"repo.projects.column.deletion_desc": "プロジェクト列を削除すると、関連するすべてのイシューがデフォルトの列に移動します。 続行しますか?",
"repo.projects.column.color": "カラー",
+ "repo.projects.column": "列",
"repo.projects.open": "オープン",
"repo.projects.close": "クローズ",
"repo.projects.column.assigned_to": "担当",
@@ -1400,11 +1403,12 @@
"repo.issues.new": "新しいイシュー",
"repo.issues.new.title_empty": "タイトルは空にできません",
"repo.issues.new.labels": "ラベル",
- "repo.issues.new.no_label": "ラベルなし",
+ "repo.issues.new.no_labels": "ラベルなし",
"repo.issues.new.clear_labels": "ラベルをクリア",
"repo.issues.new.projects": "プロジェクト",
"repo.issues.new.clear_projects": "プロジェクトをクリア",
"repo.issues.new.no_projects": "プロジェクトなし",
+ "repo.issues.new.no_column": "列なし",
"repo.issues.new.open_projects": "オープン中のプロジェクト",
"repo.issues.new.closed_projects": "クローズ済みプロジェクト",
"repo.issues.new.no_items": "項目なし",
@@ -1524,16 +1528,18 @@
"repo.issues.commented_at": "がコメント %s",
"repo.issues.delete_comment_confirm": "このコメントを削除してよろしいですか?",
"repo.issues.context.copy_link": "リンクをコピー",
+ "repo.issues.context.copy_source": "ソースをコピー",
"repo.issues.context.quote_reply": "引用して返信",
"repo.issues.context.reference_issue": "新しいイシューから参照",
"repo.issues.context.edit": "編集",
"repo.issues.context.delete": "削除",
"repo.issues.no_content": "説明はありません。",
+ "repo.issues.comment_no_content": "コメントはありません。",
"repo.issues.close": "イシューをクローズ",
"repo.issues.comment_pull_merged_at": "がコミット %[1]s を %[2]s にマージ %[3]s",
"repo.issues.comment_manually_pull_merged_at": "がコミット %[1]s を %[2]s に手動マージ %[3]s",
"repo.issues.close_comment_issue": "コメントしてクローズ",
- "repo.issues.reopen_issue": "再オープンする",
+ "repo.issues.reopen_issue": "イシューを再オープン",
"repo.issues.reopen_comment_issue": "コメントして再オープン",
"repo.issues.create_comment": "コメントする",
"repo.issues.comment.blocked_user": "投稿者またはリポジトリのオーナーがあなたをブロックしているため、コメントの作成や編集はできません。",
@@ -1778,8 +1784,9 @@
"repo.pulls.select_commit_hold_shift_for_range": "コミットを選択。シフトを押しながらクリックで範囲選択。",
"repo.pulls.review_only_possible_for_full_diff": "すべての差分を表示しているときだけレビューが可能です",
"repo.pulls.filter_changes_by_commit": "コミットで絞り込み",
- "repo.pulls.nothing_to_compare": "同じブランチ同士のため、 プルリクエストを作成する必要がありません。",
- "repo.pulls.nothing_to_compare_have_tag": "選択したブランチ/タグは同一のものです。",
+ "repo.pulls.nothing_to_compare": "差分がないため、 プルリクエストを作成する必要はありません。",
+ "repo.pulls.no_common_history": "これらのブランチには共通のマージベースがありません。 別のベースブランチまたは比較ブランチを選択してください。",
+ "repo.pulls.nothing_to_compare_have_tag": "選択したブランチ/タグの間で差分がありません。",
"repo.pulls.nothing_to_compare_and_allow_empty_pr": "これらのブランチは内容が同じです。 空のプルリクエストになります。",
"repo.pulls.has_pull_request": "同じブランチのプルリクエストはすでに存在します: %[2]s#%[3]d",
"repo.pulls.create": "プルリクエストを作成",
@@ -1813,6 +1820,7 @@
"repo.pulls.required_status_check_failed": "いくつかの必要なステータスチェックが成功していません。",
"repo.pulls.required_status_check_missing": "必要なチェックがいくつか抜けています。",
"repo.pulls.required_status_check_administrator": "管理者であるため、このプルリクエストをマージすることは可能です。",
+ "repo.pulls.required_status_check_bypass_allowlist": "あなたには、このマージでブランチ保護ルールのバイパスが許可されています。",
"repo.pulls.blocked_by_approvals": "このプルリクエストはまだ必要な承認数を満たしていません。 公式の承認を %[1]d / %[2]d 得ています。",
"repo.pulls.blocked_by_approvals_whitelisted": "このプルリクエストはまだ必要な承認数を満たしていません。 許可リストのユーザーまたはチームからの承認を %[1]d / %[2]d 得ています。",
"repo.pulls.blocked_by_rejection": "このプルリクエストは公式レビューアにより変更要請されています。",
@@ -1844,7 +1852,8 @@
"repo.pulls.fast_forward_only_merge_pull_request": "ファストフォワードのみ",
"repo.pulls.merge_manually": "手動マージ済みにする",
"repo.pulls.merge_commit_id": "マージコミットID",
- "repo.pulls.require_signed_wont_sign": "ブランチでは署名されたコミットが必須ですが、このマージでは署名がされません",
+ "repo.pulls.require_signed_wont_sign": "ブランチには署名済みコミットが必要ですが、このマージでは署名がされません",
+ "repo.pulls.require_signed_head_commits_unverified": "ブランチには署名済みコミットが必要ですが、このプルリクエストに対する 1つ以上のコミットは検証されていません",
"repo.pulls.invalid_merge_option": "このプルリクエストでは、指定したマージ方法は使えません。",
"repo.pulls.merge_conflict": "マージ失敗: マージ中にコンフリクトがありました。 ヒント: 別のストラテジーを試してみてください。",
"repo.pulls.merge_conflict_summary": "エラーメッセージ",
@@ -1877,6 +1886,7 @@
"repo.pulls.update_not_allowed": "ブランチを更新する権限がありません",
"repo.pulls.outdated_with_base_branch": "このブランチはベースブランチに対して最新ではありません",
"repo.pulls.close": "プルリクエストをクローズ",
+ "repo.pulls.reopen": "プルリクエストを再オープン",
"repo.pulls.closed_at": "がプルリクエストをクローズ %[2]s",
"repo.pulls.reopened_at": "がプルリクエストを再オープン %[2]s",
"repo.pulls.cmd_instruction_hint": "コマンドラインの手順を表示",
@@ -1949,7 +1959,6 @@
"repo.signing.wont_sign.headsigned": "HEADコミットが署名されていないため、マージは署名されません。",
"repo.signing.wont_sign.commitssigned": "関連するコミットすべてが署名されていないため、マージは署名されません。",
"repo.signing.wont_sign.approved": "PRが未承認のため、マージは署名されません。",
- "repo.signing.wont_sign.not_signed_in": "サインインしていません。",
"repo.ext_wiki": "外部Wikiへのアクセス",
"repo.ext_wiki.desc": "外部Wikiへのリンク。",
"repo.wiki": "Wiki",
@@ -2131,7 +2140,9 @@
"repo.settings.pulls_desc": "プルリクエストを有効にする",
"repo.settings.pulls.ignore_whitespace": "空白文字のコンフリクトを無視する",
"repo.settings.pulls.enable_autodetect_manual_merge": "手動マージの自動検出を有効にする (注意: 特殊なケースでは判定ミスが発生する場合があります)",
+ "repo.settings.pulls.allow_merge_update": "マージでプルリクエストのブランチの更新を可能にする",
"repo.settings.pulls.allow_rebase_update": "リベースでプルリクエストのブランチの更新を可能にする",
+ "repo.settings.pulls.default_update_style": "デフォルトのブランチ更新スタイル",
"repo.settings.pulls.default_target_branch": "新しいプルリクエストのデフォルトのターゲットブランチ",
"repo.settings.pulls.default_target_branch_default": "デフォルトブランチ (%s)",
"repo.settings.pulls.default_delete_branch_after_merge": "デフォルトでプルリクエストのブランチをマージ後に削除する",
@@ -2173,7 +2184,8 @@
"repo.settings.transfer_abort_invalid": "存在しないリポジトリの移転はキャンセルできません。",
"repo.settings.transfer_abort_success": "%s へのリポジトリ移転は正常にキャンセルされました。",
"repo.settings.transfer_desc": "別のユーザーやあなたが管理者権限を持っている組織にリポジトリを移転します。",
- "repo.settings.transfer_form_title": "確認のためリポジトリ名を入力:",
+ "repo.settings.enter_repo_name_to_confirm": "確認のためリポジトリ名を入力:",
+ "repo.settings.enter_repo_full_name_to_confirm": "確認のため完全なリポジトリ名(owner/name形式)を入力:",
"repo.settings.transfer_in_progress": "現在進行中の移転があります。このリポジトリを別のユーザーに移転したい場合はキャンセルしてください。",
"repo.settings.transfer_notices_1": "- 個人ユーザーに移転すると、あなたはリポジトリへのアクセス権を失います。",
"repo.settings.transfer_notices_2": "- あなたが所有(または共同で所有)している組織に移転すると、リポジトリへのアクセス権は維持されます。",
@@ -2249,13 +2261,14 @@
"repo.settings.webhook.delivery.success": "イベントを配信キューに追加しました。 配信履歴に表示されるまで数秒かかります。",
"repo.settings.githooks_desc": "GitのフックはGit自体が提供する仕組みです。 以下のフックファイルを編集するとカスタム処理を設定できます。",
"repo.settings.githook_edit_desc": "もしフックがアクティブではない場合はサンプルの内容が表示されます。 内容を空にするとフックが無効になります。",
- "repo.settings.githook_name": "フックの名称",
- "repo.settings.githook_content": "フックの内容",
"repo.settings.update_githook": "フックを更新",
"repo.settings.add_webhook_desc": "GiteaはターゲットURLに、指定したContent TypeでPOSTリクエストを送ります。 詳細はWebhookガイドへ。",
"repo.settings.payload_url": "ターゲットURL",
"repo.settings.http_method": "HTTPメソッド",
"repo.settings.content_type": "POST Content Type",
+ "repo.settings.webhook.name": "Webhook名",
+ "repo.settings.webhook.name_helper": "任意で、このWebhookにわかりやすい名前をつける",
+ "repo.settings.webhook.name_empty": "無名のWebhook",
"repo.settings.secret": "シークレット",
"repo.settings.webhook_secret_desc": "Webhookサーバーがsecretの使用をサポートしている場合は、webhookのマニュアルに従いここにsecretを入力できます。",
"repo.settings.slack_username": "ユーザー名",
@@ -2404,6 +2417,11 @@
"repo.settings.protect_merge_whitelist_committers_desc": "許可リストに登録したユーザーまたはチームにのみ、このブランチへのプルリクエストのマージを許可します。",
"repo.settings.protect_merge_whitelist_users": "マージ許可リストに含むユーザー:",
"repo.settings.protect_merge_whitelist_teams": "マージ許可リストに含むチーム:",
+ "repo.settings.protect_bypass_allowlist": "ブランチ保護のバイパス",
+ "repo.settings.protect_enable_bypass_allowlist": "選択されたユーザーまたはチームにブランチ保護のバイパスを許可",
+ "repo.settings.protect_enable_bypass_allowlist_desc": "許可リストに指定したユーザーまたはチームは、必要な承認、ステータスチェック、保護ファイルのルールにより通常であればブロックされる場合でも、マージやプッシュを行うことができます。",
+ "repo.settings.protect_bypass_allowlist_users": "保護のバイパスを許可するユーザー:",
+ "repo.settings.protect_bypass_allowlist_teams": "保護のバイパスを許可するチーム:",
"repo.settings.protect_check_status_contexts": "ステータスチェックを有効にする",
"repo.settings.protect_status_check_patterns": "ステータスチェック パターン:",
"repo.settings.protect_status_check_patterns_desc": "このルールの対象ブランチがマージ可能になる前に、どのステータスチェックがパスしなければならないかを、パターンで入力します。 各行にパターンを指定します。 この設定は空にできません。",
@@ -2445,7 +2463,7 @@
"repo.settings.block_outdated_branch": "遅れているプルリクエストのマージをブロック",
"repo.settings.block_outdated_branch_desc": "baseブランチがheadブランチより進んでいる場合、マージできないようにします。",
"repo.settings.block_admin_merge_override": "管理者もブランチ保護のルールに従う",
- "repo.settings.block_admin_merge_override_desc": "管理者はブランチ保護のルールに従う必要があり、回避することはできません。",
+ "repo.settings.block_admin_merge_override_desc": "管理者はブランチ保護のルールに従う必要があり、回避することはできません。 バイパス許可リストを有効にしている場合、許可リストに含まれるユーザーやチームは、引き続きルールのバイパスが可能です。",
"repo.settings.default_branch_desc": "コミット表示のデフォルトのブランチを選択します。",
"repo.settings.default_target_branch_desc": "プルリクエストでは、リポジトリ拡張設定の「プルリクエスト」セクションで設定することで、別のデフォルトターゲットブランチを使用できます。",
"repo.settings.merge_style_desc": "マージ スタイル",
@@ -2476,7 +2494,10 @@
"repo.settings.visibility.private.text": "プライベートに変更することで、リポジトリは許可されたメンバーにのみ表示されるようになり、既存のフォーク、ウォッチャー、スターとの関係は解除される可能性があります。",
"repo.settings.visibility.private.bullet_title": "プライベートに変更すると:",
"repo.settings.visibility.private.bullet_one": "許可されたメンバーにのみリポジトリを表示します。",
- "repo.settings.visibility.private.bullet_two": "フォーク、ウォッチャー、スターとの関係を解除する可能性があります。",
+ "repo.settings.visibility.private.bullet_two": "非公開設定はフォークにも適用し、ウォッチャーとスターは削除します。",
+ "repo.settings.visibility.private.stats_stars": "このリポジトリには、失われるおそれのあるスターが %d 個あります。",
+ "repo.settings.visibility.private.stats_watchers": "このリポジトリには、失われるおそれのあるウォッチャーが %d 件あります。",
+ "repo.settings.visibility.private.stats_forks": "このリポジトリには、関連するフォークが %d 件あります。",
"repo.settings.visibility.public.button": "公開する",
"repo.settings.visibility.public.text": "公開に変更すると、リポジトリを誰でも閲覧できるようにします。",
"repo.settings.visibility.public.bullet_title": "公開に変更すると:",
@@ -2712,6 +2733,8 @@
"org.members": "メンバー",
"org.teams": "チーム",
"org.code": "コード",
+ "org.repos.empty": "リポジトリはまだありません。",
+ "org.repos.empty_description": "コードを組織内で共有するため、リポジトリを作成してください。",
"org.lower_members": "メンバー",
"org.lower_repositories": "リポジトリ",
"org.create_new_team": "新しいチーム",
@@ -2804,7 +2827,10 @@
"org.teams.no_desc": "このチームには説明がありません。",
"org.teams.settings": "設定",
"org.teams.owners_permission_desc": "オーナーはすべてのリポジトリへのフルアクセス権と、組織への管理者アクセス権を持ちます。",
+ "org.teams.owners_permission_suggestion": "メンバーのアクセス権限を細かく管理するため、新しくチームを作成することもできます。",
"org.teams.members": "チームメンバー",
+ "org.teams.manage_team_member": "チームとメンバーを管理",
+ "org.teams.manage_team_member_prompt": "メンバーの管理はチームを使って行います。 ユーザーをこの組織に招待するには、チームに追加してください。",
"org.teams.update_settings": "設定の更新",
"org.teams.delete_team": "チームを削除",
"org.teams.add_team_member": "チームメンバーを追加",
@@ -2845,6 +2871,8 @@
"org.worktime.date_range_end": "期間 (至)",
"org.worktime.query": "集計",
"org.worktime.time": "時間",
+ "org.worktime.empty": "作業時間のデータはまだありません。",
+ "org.worktime.empty_description": "記録された作業時間を表示するには、日付範囲を調整してください。",
"org.worktime.by_repositories": "リポジトリ別",
"org.worktime.by_milestones": "マイルストーン別",
"org.worktime.by_members": "メンバー別",
@@ -2859,6 +2887,30 @@
"admin.hooks": "Webhook",
"admin.integrations": "連携",
"admin.authentication": "認証ソース",
+ "admin.badges": "バッジ",
+ "admin.badges.badges_manage_panel": "バッジ管理",
+ "admin.badges.details": "バッジ詳細",
+ "admin.badges.new_badge": "新しいバッジを作成",
+ "admin.badges.slug": "スラッグ",
+ "admin.badges.slug_been_taken": "スラッグが既に使用されています。",
+ "admin.badges.description": "説明",
+ "admin.badges.image_url": "画像URL",
+ "admin.badges.new_success": "バッジ \"%s\" を作成しました。",
+ "admin.badges.update_success": "バッジを更新しました。",
+ "admin.badges.deletion_success": "バッジを削除しました。",
+ "admin.badges.edit_badge": "バッジを編集",
+ "admin.badges.update_badge": "バッジを更新",
+ "admin.badges.delete_badge": "バッジを削除",
+ "admin.badges.delete_badge_desc": "このバッジを完全に削除してよろしいですか?",
+ "admin.badges.users_with_badge": "バッジ保持者: %s",
+ "admin.badges.not_found": "バッジが見つかりません。",
+ "admin.badges.user_already_has": "ユーザーは既にこのバッジを保有しています。",
+ "admin.badges.user_add_success": "ユーザーにバッジを付与しました。",
+ "admin.badges.user_remove_success": "ユーザーのバッジを取り消しました。",
+ "admin.badges.manage_users": "ユーザーを管理",
+ "admin.badges.add_user": "ユーザーを追加",
+ "admin.badges.remove_user": "ユーザーを削除",
+ "admin.badges.delete_user_desc": "このユーザーのバッジを取り消してもよろしいですか?",
"admin.emails": "ユーザーメールアドレス",
"admin.config": "設定",
"admin.config_summary": "サマリー",
@@ -3135,6 +3187,8 @@
"admin.auths.oauth2_required_claim_name_helper": "このClaim名を設定すると、このソースからのログインを、指定したClaim名を持つユーザーに限定します。",
"admin.auths.oauth2_required_claim_value": "必須Claim値",
"admin.auths.oauth2_required_claim_value_helper": "この値を設定すると、このソースからのログインを、指定したClaim名とClaim値を持つユーザーに限定します。",
+ "admin.auths.open_id_connect_external_id_claim": "外部ID Claim名 (オプション)",
+ "admin.auths.open_id_connect_external_id_claim_helper": "ユーザーの外部IDに使用するClaim名。 デフォルトは \"sub\" です。 Azure AD / Entra IDについては、Azure AD V2 プロバイダーから移行したときに継続性を維持するため、この値を \"oid\" にしてください。 注: \"oid\" Claimを使う場合は、上にあるスコープ設定に \"profile\" スコープを含める必要があります。",
"admin.auths.oauth2_group_claim_name": "このソースでグループ名を提供するClaim名 (オプション)",
"admin.auths.oauth2_full_name_claim_name": "フルネームのClaim名。 (オプション — 設定すると、ユーザーのフルネームは常にこのClaimに同期します)",
"admin.auths.oauth2_ssh_public_key_claim_name": "SSH公開鍵のClaim名",
@@ -3187,11 +3241,8 @@
"admin.config.server_config": "サーバー設定",
"admin.config.app_name": "サイトのタイトル",
"admin.config.app_ver": "Giteaのバージョン",
- "admin.config.app_url": "GiteaのベースURL",
"admin.config.custom_conf": "設定ファイルのパス",
"admin.config.custom_file_root_path": "カスタムファイルのルートパス",
- "admin.config.domain": "サーバードメイン",
- "admin.config.offline_mode": "ローカルモード",
"admin.config.disable_router_log": "ルーターのログが無効",
"admin.config.run_user": "実行ユーザー名",
"admin.config.run_mode": "実行モード",
@@ -3271,15 +3322,20 @@
"admin.config.cache_config": "キャッシュ設定",
"admin.config.cache_adapter": "キャッシュ アダプター",
"admin.config.cache_interval": "キャッシュ間隔",
- "admin.config.cache_conn": "キャッシュ接続",
"admin.config.cache_item_ttl": "キャッシュアイテムのTTL",
"admin.config.cache_test": "キャッシュのテスト",
"admin.config.cache_test_failed": "キャッシュの調査に失敗しました: %v.",
"admin.config.cache_test_slow": "キャッシュのテストは成功しましたが、応答が遅いです: %s",
"admin.config.cache_test_succeeded": "キャッシュのテストは成功し、応答時間は %s でした。",
+ "admin.config.common.start_time": "開始時刻",
+ "admin.config.common.end_time": "終了時刻",
+ "admin.config.common.skip_time_check": "時刻を空に(入力欄をクリア)すると時刻チェックをスキップします",
+ "admin.config.instance_maintenance": "インスタンスのメンテナンス",
+ "admin.config.instance_maintenance_mode.admin_web_access_only": "管理者にのみWeb UIへのアクセスを許可",
+ "admin.config.instance_web_banner.enabled": "バナーを表示",
+ "admin.config.instance_web_banner.message_placeholder": "バナーメッセージ (Markdownをサポート)",
"admin.config.session_config": "セッション設定",
"admin.config.session_provider": "セッション プロバイダー",
- "admin.config.provider_config": "プロバイダーの設定",
"admin.config.cookie_name": "Cookieの名称",
"admin.config.gc_interval_time": "GCの間隔",
"admin.config.session_life_time": "セッションの有効期間",
@@ -3287,7 +3343,7 @@
"admin.config.cookie_life_time": "Cookieの有効期間",
"admin.config.picture_config": "画像とアバターの設定",
"admin.config.picture_service": "画像サービス",
- "admin.config.disable_gravatar": "Gravatarが無効",
+ "admin.config.enable_gravatar": "Gravatar有効",
"admin.config.enable_federated_avatar": "フェデレーテッド・アバター有効",
"admin.config.open_with_editor_app_help": "クローンメニューの「~で開く」に表示するエディタ。 空白のままにするとデフォルトが使用されます。 展開するとデフォルトを確認できます。",
"admin.config.git_guide_remote_name": "操作説明内のgitコマンドで使うリポジトリリモート名",
@@ -3465,6 +3521,7 @@
"packages.dependencies": "依存関係",
"packages.keywords": "キーワード",
"packages.details": "詳細",
+ "packages.name": "パッケージ名",
"packages.details.author": "著作者",
"packages.details.project_site": "プロジェクトサイト",
"packages.details.repository_site": "リポジトリサイト",
@@ -3560,9 +3617,27 @@
"packages.swift.registry": "このレジストリをコマンドラインからセットアップします:",
"packages.swift.install": "あなたの Package.swift ファイルにパッケージを追加します:",
"packages.swift.install2": "そして次のコマンドを実行します:",
+ "packages.terraform.install": "HTTPバックエンドを使用するよう状態を設定します",
+ "packages.terraform.install2": "そして次のコマンドを実行します:",
+ "packages.terraform.lock_status": "ロック状態",
+ "packages.terraform.locked_by": "%s がロックしています",
+ "packages.terraform.unlocked": "アンロックされています",
+ "packages.terraform.lock": "ロック",
+ "packages.terraform.unlock": "アンロック",
+ "packages.terraform.lock.success": "Terraform状態は正常にロックされました。",
+ "packages.terraform.unlock.success": "Terraform状態は正常にアンロックされました。",
+ "packages.terraform.lock.error.already_locked": "Terraform状態は既にロックされています。",
+ "packages.terraform.delete.locked": "Terraform状態はロックされているため削除できません。",
+ "packages.terraform.delete.latest": "Terraform状態の最新バージョンは削除できません。",
"packages.vagrant.install": "Vagrant ボックスを追加するには、次のコマンドを実行します。",
"packages.settings.link": "このパッケージをリポジトリにリンク",
- "packages.settings.link.description": "パッケージをリポジトリにリンクすると、リポジトリのパッケージ一覧にそのパッケージが表示されます。 リンクできるのは、同じオーナーが所有するリポジトリのみです。 入力欄を空にするとリンクを削除します。",
+ "packages.settings.link.description": "パッケージをリポジトリにリンクすると、リポジトリのパッケージ一覧に表示されるようになります。",
+ "packages.settings.link.notice1": "同じオーナーが所有するリポジトリにだけリンクできます。",
+ "packages.settings.link.notice2": "リポジトリにリンクしてもパッケージの公開範囲は変わりません。",
+ "packages.settings.link.notice3": "入力欄を空にするとリンクを削除します。",
+ "packages.settings.visibility": "パッケージの公開範囲",
+ "packages.settings.visibility.inherit": "パッケージの公開範囲は所有者から継承されており、ここで個別に変更することはできません。 変更するには、このパッケージを所有するユーザーまたは組織の公開設定を変更してください。",
+ "packages.settings.visibility.button": "所有者の公開範囲を変更",
"packages.settings.link.select": "リポジトリを選択",
"packages.settings.link.button": "リポジトリのリンクを更新",
"packages.settings.link.success": "リポジトリのリンクを更新しました。",
@@ -3573,8 +3648,13 @@
"packages.settings.delete": "パッケージ削除",
"packages.settings.delete.description": "パッケージの削除は恒久的で元に戻すことはできません。",
"packages.settings.delete.notice": "%s (%s) を削除しようとしています。この操作は元に戻せません。よろしいですか?",
+ "packages.settings.delete.notice.package": "%s とその全てのバージョンを削除しようとしています。この操作は元に戻せません。よろしいですか?",
"packages.settings.delete.success": "パッケージを削除しました。",
+ "packages.settings.delete.version.success": "パッケージバージョンを削除しました。",
"packages.settings.delete.error": "パッケージの削除に失敗しました。",
+ "packages.settings.delete.version": "バージョンを削除",
+ "packages.settings.delete.confirm": "確認のためパッケージ名を入力してください",
+ "packages.settings.delete.invalid_package_name": "入力したパッケージ名が間違っています。",
"packages.owner.settings.cargo.title": "Cargoレジストリ インデックス",
"packages.owner.settings.cargo.initialize": "インデックスを初期化",
"packages.owner.settings.cargo.initialize.description": "Cargoレジストリを使用するには、インデックス用の特別なgitリポジトリが必要です。 このオプションを使用するとそのリポジトリを(再)作成し、自動的に構成します。",
@@ -3630,9 +3710,10 @@
"actions.status.running": "実行中",
"actions.status.success": "成功",
"actions.status.failure": "失敗",
- "actions.status.cancelled": "キャンセル",
- "actions.status.skipped": "スキップ",
- "actions.status.blocked": "ブロックされた",
+ "actions.status.cancelled": "キャンセルされました",
+ "actions.status.cancelling": "キャンセル中",
+ "actions.status.skipped": "スキップされました",
+ "actions.status.blocked": "ブロックされました",
"actions.runners": "ランナー",
"actions.runners.runner_manage_panel": "ランナーの管理",
"actions.runners.new": "新しいランナーを作成",
@@ -3641,6 +3722,7 @@
"actions.runners.id": "ID",
"actions.runners.name": "名称",
"actions.runners.owner_type": "タイプ",
+ "actions.runners.availability": "利用可否",
"actions.runners.description": "説明",
"actions.runners.labels": "ラベル",
"actions.runners.last_online": "最終オンライン時刻",
@@ -3656,6 +3738,12 @@
"actions.runners.update_runner": "変更を保存",
"actions.runners.update_runner_success": "ランナーを更新しました",
"actions.runners.update_runner_failed": "ランナーの更新に失敗しました",
+ "actions.runners.enable_runner": "このランナーを有効化",
+ "actions.runners.enable_runner_success": "ランナーを有効化しました",
+ "actions.runners.enable_runner_failed": "ランナーの有効化に失敗しました",
+ "actions.runners.disable_runner": "このランナーを無効化",
+ "actions.runners.disable_runner_success": "ランナーを無効化しました",
+ "actions.runners.disable_runner_failed": "ランナーの無効化に失敗しました",
"actions.runners.delete_runner": "このランナーを削除",
"actions.runners.delete_runner_success": "ランナーを削除しました",
"actions.runners.delete_runner_failed": "ランナーの削除に失敗しました",
@@ -3671,7 +3759,11 @@
"actions.runners.reset_registration_token_confirm": "現在のトークンを無効にして、新しいトークンを生成しますか?",
"actions.runners.reset_registration_token_success": "ランナー登録トークンをリセットしました",
"actions.runs.all_workflows": "すべてのワークフロー",
+ "actions.runs.workflow_run_count_1": "%d 件のワークフロー実行",
+ "actions.runs.workflow_run_count_n": "%d 件のワークフロー実行",
"actions.runs.commit": "コミット",
+ "actions.runs.run_details": "実行の詳細",
+ "actions.runs.workflow_file": "ワークフローのファイル",
"actions.runs.scheduled": "スケジュール済み",
"actions.runs.pushed_by": "pushed by",
"actions.runs.invalid_workflow_helper": "ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s",
@@ -3694,6 +3786,13 @@
"actions.runs.delete.description": "このワークフローを完全に削除してもよろしいですか?この操作は元に戻せません。",
"actions.runs.not_done": "このワークフローの実行は完了していません。",
"actions.runs.view_workflow_file": "ワークフローファイルを表示",
+ "actions.runs.summary": "サマリー",
+ "actions.runs.all_jobs": "すべてのジョブ",
+ "actions.runs.attempt": "試行",
+ "actions.runs.latest": "最新",
+ "actions.runs.latest_attempt": "最新 試行",
+ "actions.runs.triggered_via": "%s によりトリガー",
+ "actions.runs.total_duration": "総所要時間:",
"actions.workflow.disable": "ワークフローを無効にする",
"actions.workflow.disable_success": "ワークフロー '%s' が無効になりました。",
"actions.workflow.enable": "ワークフローを有効にする",
@@ -3743,5 +3842,24 @@
"git.filemode.normal_file": "ノーマルファイル",
"git.filemode.executable_file": "実行可能ファイル",
"git.filemode.symbolic_link": "シンボリックリンク",
- "git.filemode.submodule": "サブモジュール"
+ "git.filemode.submodule": "サブモジュール",
+ "org.repos.none": "リポジトリはありません。",
+ "actions.general.permissions": "Actionsトークンの権限",
+ "actions.general.token_permissions.mode": "トークンのデフォルトの権限",
+ "actions.general.token_permissions.mode.desc": "ワークフローファイルでActionsジョブに権限(permissions)を指定していない場合、デフォルトの権限が使用されます。",
+ "actions.general.token_permissions.mode.permissive": "許可",
+ "actions.general.token_permissions.mode.permissive.desc": "ジョブの対象リポジトリについて、読み取りと書き込みを許可。",
+ "actions.general.token_permissions.mode.restricted": "制限あり",
+ "actions.general.token_permissions.mode.restricted.desc": "ジョブの対象リポジトリのコンテンツ・ユニット(コード、リリース)について、読み取りのみ許可。",
+ "actions.general.token_permissions.override_owner": "オーナーレベルの設定を上書きする",
+ "actions.general.token_permissions.override_owner_desc": "有効にすると、このリポジトリは(ユーザーまたは組織の)オーナーのActions設定に従うのではなく、独自の設定を使用します。",
+ "actions.general.token_permissions.maximum": "トークン権限の上限",
+ "actions.general.token_permissions.maximum.description": "Actionsジョブの実効権限はこの上限によって制限されます。",
+ "actions.general.token_permissions.fork_pr_note": "フォークからのプルリクエストによってジョブが開始された場合は、実効権限は読み取り専用までとなります。",
+ "actions.general.token_permissions.customize_max_permissions": "権限の上限をカスタマイズする",
+ "actions.general.cross_repo": "クロスリポジトリ アクセス",
+ "actions.general.cross_repo_desc": "Actionsジョブの実行時に、このオーナーのすべてのリポジトリから選択したリポジトリへ、GITEA_TOKENを使用して(読み取り専用で)アクセスすることを許可します。",
+ "actions.general.cross_repo_selected": "選択したリポジトリ",
+ "actions.general.cross_repo_target_repos": "対象リポジトリ",
+ "actions.general.cross_repo_add": "対象リポジトリの追加"
}
diff --git a/renovate.json5 b/renovate.json5
index c5d712dc0a..bf913011fb 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -18,6 +18,7 @@
"managerFilePatterns": ["/(^|/)Makefile$/"],
"matchStrings": [
"[A-Z_]+_PACKAGE\\s*\\?=\\s*(?[^@\\s]+?)(?:/cmd/[^@/\\s]+)?@(?\\S+)\\s+# renovate: datasource=(?\\S+)",
+ "[A-Z_]+_IMAGE\\s*\\?=\\s*(?[^:\\s]+):(?[^@\\s]+)@(?sha256:[a-f0-9]+)\\s+# renovate: datasource=(?\\S+)",
],
},
],
diff --git a/routers/api/v1/repo/mirror.go b/routers/api/v1/repo/mirror.go
index ac2d8bba06..bdca030b76 100644
--- a/routers/api/v1/repo/mirror.go
+++ b/routers/api/v1/repo/mirror.go
@@ -6,6 +6,7 @@ package repo
import (
"errors"
"net/http"
+ "strings"
"time"
"code.gitea.io/gitea/models/db"
@@ -101,6 +102,8 @@ func PushMirrorSync(ctx *context.APIContext) {
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
+ // "422":
+ // "$ref": "#/responses/validationError"
if !setting.Mirror.Enabled {
ctx.APIError(http.StatusBadRequest, "Mirror feature is disabled")
@@ -112,14 +115,18 @@ func PushMirrorSync(ctx *context.APIContext) {
ctx.APIError(http.StatusNotFound, err)
return
}
+
+ failedPushMirrors := make([]string, 0)
for _, mirror := range pushMirrors {
ok := mirror_service.SyncPushMirror(ctx, mirror.ID)
if !ok {
- ctx.APIErrorInternal(errors.New("error occurred when syncing push mirror " + mirror.RemoteName))
- return
+ failedPushMirrors = append(failedPushMirrors, mirror.RemoteName)
}
}
-
+ if len(failedPushMirrors) != 0 {
+ ctx.APIError(http.StatusUnprocessableEntity, "error occurred when syncing push mirrors: "+strings.Join(failedPushMirrors, ", "))
+ return
+ }
ctx.Status(http.StatusOK)
}
diff --git a/routers/api/v1/repo/mirror_test.go b/routers/api/v1/repo/mirror_test.go
new file mode 100644
index 0000000000..6cbed49d89
--- /dev/null
+++ b/routers/api/v1/repo/mirror_test.go
@@ -0,0 +1,40 @@
+// Copyright 2026 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repo
+
+import (
+ "net/http"
+ "testing"
+
+ "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/setting"
+ "code.gitea.io/gitea/modules/test"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// TestPushMirrorSync verifies the endpoint attempts every push mirror instead
+// of aborting on the first failure, reporting all failed remotes with a 422.
+// Each remote name is not a configured git remote, so SyncPushMirror fails fast
+// without any network access.
+func TestPushMirrorSync(t *testing.T) {
+ unittest.PrepareTestEnv(t)
+ defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
+
+ for _, remoteName := range []string{"broken_remote_1", "broken_remote_2"} {
+ assert.NoError(t, db.Insert(t.Context(), &repo_model.PushMirror{RepoID: 1, RemoteName: remoteName}))
+ }
+
+ ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
+ contexttest.LoadRepo(t, ctx, 1)
+
+ PushMirrorSync(ctx)
+
+ assert.Equal(t, http.StatusUnprocessableEntity, ctx.Resp.WrittenStatus())
+ assert.Contains(t, resp.Body.String(), "broken_remote_1")
+ assert.Contains(t, resp.Body.String(), "broken_remote_2")
+}
diff --git a/services/actions/notifier.go b/services/actions/notifier.go
index 4b2e87afad..0069da8c6b 100644
--- a/services/actions/notifier.go
+++ b/services/actions/notifier.go
@@ -687,7 +687,7 @@ func (n *actionsNotifier) AutoMergePullRequest(ctx context.Context, doer *user_m
n.MergePullRequest(ctx, doer, pr)
}
-func (n *actionsNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
+func (n *actionsNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string) {
ctx = withMethod(ctx, "PullRequestSynchronized")
if err := pr.LoadIssue(ctx); err != nil {
@@ -703,6 +703,8 @@ func (n *actionsNotifier) PullRequestSynchronized(ctx context.Context, doer *use
newNotifyInput(pr.Issue.Repo, doer, webhook_module.HookEventPullRequestSync).
WithPayload(&api.PullRequestPayload{
Action: api.HookIssueSynchronized,
+ Before: before,
+ After: after,
Index: pr.Issue.Index,
PullRequest: convert.ToAPIPullRequest(ctx, pr, nil),
Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}),
diff --git a/services/agit/agit.go b/services/agit/agit.go
index c7c46651c0..66f0d1c0db 100644
--- a/services/agit/agit.go
+++ b/services/agit/agit.go
@@ -286,7 +286,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
} else if commentCreated {
notify_service.PullRequestPushCommits(ctx, pusher, pr, comment)
}
- notify_service.PullRequestSynchronized(ctx, pusher, pr)
+ notify_service.PullRequestSynchronized(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
results = append(results, private.HookProcReceiveRefResult{
OldOID: oldCommitID,
diff --git a/services/notify/notifier.go b/services/notify/notifier.go
index 875a70e564..bc990e9cb6 100644
--- a/services/notify/notifier.go
+++ b/services/notify/notifier.go
@@ -45,7 +45,7 @@ type Notifier interface {
NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User)
MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest)
AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest)
- PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest)
+ PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string)
PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User)
PullRequestCodeComment(ctx context.Context, pr *issues_model.PullRequest, comment *issues_model.Comment, mentions []*user_model.User)
PullRequestChangeTargetBranch(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, oldBranch string)
diff --git a/services/notify/notify.go b/services/notify/notify.go
index 152d53b01c..aee2047e1c 100644
--- a/services/notify/notify.go
+++ b/services/notify/notify.go
@@ -120,9 +120,9 @@ func NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions
}
// PullRequestSynchronized notifies Synchronized pull request
-func PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
+func PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string) {
for _, notifier := range notifiers {
- notifier.PullRequestSynchronized(ctx, doer, pr)
+ notifier.PullRequestSynchronized(ctx, doer, pr, before, after)
}
}
diff --git a/services/notify/null.go b/services/notify/null.go
index c3085d7c9e..f10a132da7 100644
--- a/services/notify/null.go
+++ b/services/notify/null.go
@@ -63,7 +63,7 @@ func (*NullNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.
}
// PullRequestSynchronized places a place holder function
-func (*NullNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
+func (*NullNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string) {
}
// PullRequestChangeTargetBranch places a place holder function
diff --git a/services/pull/pull.go b/services/pull/pull.go
index 4f76df31da..64d9f60dae 100644
--- a/services/pull/pull.go
+++ b/services/pull/pull.go
@@ -479,7 +479,7 @@ func AddTestPullRequestTask(opts TestPullRequestOptions) {
}
}
- notify_service.PullRequestSynchronized(ctx, opts.Doer, pr)
+ notify_service.PullRequestSynchronized(ctx, opts.Doer, pr, opts.OldCommitID, opts.NewCommitID)
}
}
}
diff --git a/services/webhook/notifier.go b/services/webhook/notifier.go
index 7627935a32..d53dfaaa6a 100644
--- a/services/webhook/notifier.go
+++ b/services/webhook/notifier.go
@@ -818,7 +818,7 @@ func (m *webhookNotifier) CreateRef(ctx context.Context, pusher *user_model.User
}
}
-func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
+func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest, before, after string) {
if err := pr.LoadIssue(ctx); err != nil {
log.Error("LoadIssue: %v", err)
return
@@ -830,6 +830,8 @@ func (m *webhookNotifier) PullRequestSynchronized(ctx context.Context, doer *use
if err := PrepareWebhooks(ctx, EventSource{Repository: pr.Issue.Repo}, webhook_module.HookEventPullRequestSync, &api.PullRequestPayload{
Action: api.HookIssueSynchronized,
+ Before: before,
+ After: after,
Index: pr.Issue.Index,
PullRequest: convert.ToAPIPullRequest(ctx, pr, doer),
Repository: convert.ToRepo(ctx, pr.Issue.Repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
diff --git a/snap/part-gitea-build.sh b/snap/part-gitea-build.sh
index 8388c16e06..005162488f 100755
--- a/snap/part-gitea-build.sh
+++ b/snap/part-gitea-build.sh
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
-if [ ! -f go.mod -o ! -d snap ]; then
+if [ ! -f go.mod ] || [ ! -d snap ]; then
echo "This script should be run from the root of the gitea repository"
exit 1
fi
diff --git a/snap/part-gitea-pull.sh b/snap/part-gitea-pull.sh
index 0d117bb4d1..83fde11a12 100755
--- a/snap/part-gitea-pull.sh
+++ b/snap/part-gitea-pull.sh
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
-if [ ! -f go.mod -o ! -d snap ]; then
+if [ ! -f go.mod ] || [ ! -d snap ]; then
echo "This script should be run from the root of the gitea repository"
exit 1
fi
diff --git a/templates/package/content/cargo.tmpl b/templates/package/content/cargo.tmpl
index ebb23e8659..5c04ab8aae 100644
--- a/templates/package/content/cargo.tmpl
+++ b/templates/package/content/cargo.tmpl
@@ -27,7 +27,7 @@ git-fetch-with-cli = true
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Readme}}
{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}
{{end}}
- {{if .PackageDescriptor.Metadata.Readme}}{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}}
{{end}}
+ {{if .PackageDescriptor.Metadata.Readme}}{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository}}
{{end}}
{{end}}
{{if .PackageDescriptor.Metadata.Dependencies}}
diff --git a/templates/package/content/chef.tmpl b/templates/package/content/chef.tmpl
index d1c17d36a4..0912aff792 100644
--- a/templates/package/content/chef.tmpl
+++ b/templates/package/content/chef.tmpl
@@ -20,7 +20,7 @@
{{if .PackageDescriptor.Metadata.Description}}
{{.PackageDescriptor.Metadata.Description}}
{{end}}
- {{if .PackageDescriptor.Metadata.LongDescription}}{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.LongDescription}}{{end}}
+ {{if .PackageDescriptor.Metadata.LongDescription}}{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.LongDescription .PackageDescriptor.Repository}}{{end}}
{{end}}
diff --git a/templates/package/content/composer.tmpl b/templates/package/content/composer.tmpl
index 2e8cfb77eb..5bbc19e9f2 100644
--- a/templates/package/content/composer.tmpl
+++ b/templates/package/content/composer.tmpl
@@ -25,7 +25,7 @@
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Comments}}
{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}
{{end}}
- {{if .PackageDescriptor.Metadata.Readme}}{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}}
{{end}}
+ {{if .PackageDescriptor.Metadata.Readme}}{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository}}
{{end}}
{{if .PackageDescriptor.Metadata.Comments}}{{StringUtils.Join .PackageDescriptor.Metadata.Comments " "}}
{{end}}
{{end}}
diff --git a/templates/package/content/npm.tmpl b/templates/package/content/npm.tmpl
index 28bcf45ee8..89f4c94008 100644
--- a/templates/package/content/npm.tmpl
+++ b/templates/package/content/npm.tmpl
@@ -22,15 +22,8 @@
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Readme}}
-
- {{if .PackageDescriptor.Metadata.Readme}}
-
- {{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}}
-
- {{else if .PackageDescriptor.Metadata.Description}}
- {{.PackageDescriptor.Metadata.Description}}
- {{end}}
-
+ {{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}
{{end}}
+ {{if .PackageDescriptor.Metadata.Readme}}{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository .PackageDescriptor.Metadata.Repository.Directory}}
{{end}}
{{end}}
{{if or .PackageDescriptor.Metadata.Dependencies .PackageDescriptor.Metadata.DevelopmentDependencies .PackageDescriptor.Metadata.PeerDependencies .PackageDescriptor.Metadata.OptionalDependencies}}
diff --git a/templates/package/content/nuget.tmpl b/templates/package/content/nuget.tmpl
index 7f874044cc..fc9a715a28 100644
--- a/templates/package/content/nuget.tmpl
+++ b/templates/package/content/nuget.tmpl
@@ -18,9 +18,9 @@
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Metadata.Readme}}
- {{if .PackageDescriptor.Metadata.Description}}{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Description}}
{{end}}
- {{if .PackageDescriptor.Metadata.Readme}}{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}}
{{end}}
- {{if .PackageDescriptor.Metadata.ReleaseNotes}}{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.ReleaseNotes}}
{{end}}
+ {{if .PackageDescriptor.Metadata.Description}}{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Description .PackageDescriptor.Repository}}
{{end}}
+ {{if .PackageDescriptor.Metadata.Readme}}{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository}}
{{end}}
+ {{if .PackageDescriptor.Metadata.ReleaseNotes}}{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.ReleaseNotes .PackageDescriptor.Repository}}
{{end}}
{{end}}
{{if .PackageDescriptor.Metadata.Dependencies}}
diff --git a/templates/package/content/pub.tmpl b/templates/package/content/pub.tmpl
index 9eefcf7162..5dafa2db47 100644
--- a/templates/package/content/pub.tmpl
+++ b/templates/package/content/pub.tmpl
@@ -14,6 +14,6 @@
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Readme}}
{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}
{{end}}
- {{if .PackageDescriptor.Metadata.Readme}}{{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Readme}}
{{end}}
+ {{if .PackageDescriptor.Metadata.Readme}}{{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Readme .PackageDescriptor.Repository}}
{{end}}
{{end}}
{{end}}
diff --git a/templates/package/content/pypi.tmpl b/templates/package/content/pypi.tmpl
index 4afdc1b72a..c570e60e6b 100644
--- a/templates/package/content/pypi.tmpl
+++ b/templates/package/content/pypi.tmpl
@@ -16,9 +16,9 @@
{{if .PackageDescriptor.Metadata.Summary}}{{.PackageDescriptor.Metadata.Summary}}{{end}}
{{if .PackageDescriptor.Metadata.LongDescription}}
- {{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.LongDescription}}
+ {{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.LongDescription .PackageDescriptor.Repository}}
{{else if .PackageDescriptor.Metadata.Description}}
- {{ctx.RenderUtils.MarkdownToHtml .PackageDescriptor.Metadata.Description}}
+ {{ctx.RenderUtils.RenderPackageMarkdown .PackageDescriptor.Metadata.Description .PackageDescriptor.Repository}}
{{end}}
{{end}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 6ecc6cfd53..bc2681c3e5 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -15729,6 +15729,9 @@
},
"404": {
"$ref": "#/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
}
}
}
diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1_openapi3_json.tmpl
index f3ed072bc0..9dd147f0e5 100644
--- a/templates/swagger/v1_openapi3_json.tmpl
+++ b/templates/swagger/v1_openapi3_json.tmpl
@@ -27535,6 +27535,9 @@
},
"404": {
"$ref": "#/components/responses/notFound"
+ },
+ "422": {
+ "$ref": "#/components/responses/validationError"
}
},
"summary": "Sync all push mirrored repository",
diff --git a/tests/e2e/events.test.ts b/tests/e2e/events.test.ts
index c7cfef6e9c..54b4774a5c 100644
--- a/tests/e2e/events.test.ts
+++ b/tests/e2e/events.test.ts
@@ -12,7 +12,7 @@ test.describe('events', () => {
// Create repo and login in parallel — repo is needed for the issue, login for the event stream
await Promise.all([
- apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(owner)}),
+ apiCreateRepo(request, {name: repoName, autoInit: false, headers: apiUserHeaders(owner)}),
loginUser(page, owner),
]);
await page.goto('/');
@@ -36,7 +36,7 @@ test.describe('events', () => {
await Promise.all([
loginUser(page, name),
(async () => {
- await apiCreateRepo(request, {name, headers});
+ await apiCreateRepo(request, {name, autoInit: false, headers});
await apiCreateIssue(request, {owner: name, repo: name, title: 'events stopwatch test', headers});
await apiStartStopwatch(request, name, name, 1, {headers});
})(),
diff --git a/tests/e2e/fork.test.ts b/tests/e2e/fork.test.ts
new file mode 100644
index 0000000000..77ccda1242
--- /dev/null
+++ b/tests/e2e/fork.test.ts
@@ -0,0 +1,18 @@
+import {env} from 'node:process';
+import {test, expect} from '@playwright/test';
+import {login, apiCreateRepo, apiCreateUser, apiUserHeaders, randomString} from './utils.ts';
+
+test('fork a repository', async ({page, request}) => {
+ const upstream = `fork-owner-${randomString(8)}`;
+ const repoName = `e2e-fork-${randomString(8)}`;
+ await apiCreateUser(request, upstream);
+ await Promise.all([
+ apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(upstream)}),
+ login(page),
+ ]);
+ await page.goto(`/${upstream}/${repoName}/fork`);
+
+ await page.getByRole('button', {name: 'Fork Repository'}).click();
+ await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
+ await expect(page.getByRole('link', {name: `${upstream}/${repoName}`})).toBeVisible();
+});
diff --git a/tests/e2e/issue-comment.test.ts b/tests/e2e/issue-comment.test.ts
new file mode 100644
index 0000000000..d3de59ba5f
--- /dev/null
+++ b/tests/e2e/issue-comment.test.ts
@@ -0,0 +1,24 @@
+import {env} from 'node:process';
+import {test, expect} from '@playwright/test';
+import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts';
+
+test('comment on and close an issue', async ({page, request}) => {
+ const repoName = `e2e-issue-comment-${randomString(8)}`;
+ const owner = env.GITEA_TEST_E2E_USER;
+ await apiCreateRepo(request, {name: repoName, autoInit: false});
+ await Promise.all([
+ apiCreateIssue(request, {owner, repo: repoName, title: 'Comment test'}),
+ login(page),
+ ]);
+ await page.goto(`/${owner}/${repoName}/issues/1`);
+
+ const body = `e2e-comment-${randomString(8)}`;
+ await page.getByPlaceholder('Leave a comment').fill(body);
+ // exact match: the status button reads "Close with Comment" while the box has content, which substring-matches "Comment"
+ await page.getByRole('button', {name: 'Comment', exact: true}).click();
+ await expect(page.locator('.comment-body').filter({hasText: body})).toBeVisible();
+
+ // posting reloaded the page with an empty box, so the status button now reads "Close Issue"
+ await page.getByRole('button', {name: 'Close Issue'}).click();
+ await expect(page.getByRole('button', {name: 'Reopen Issue'})).toBeVisible();
+});
diff --git a/tests/e2e/issue-project.test.ts b/tests/e2e/issue-project.test.ts
index b1fed72a6f..1595cfadc5 100644
--- a/tests/e2e/issue-project.test.ts
+++ b/tests/e2e/issue-project.test.ts
@@ -5,7 +5,7 @@ import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, createProject, crea
test('assign issue to project and change column', async ({page}) => {
const repoName = `e2e-issue-project-${randomString(8)}`;
const user = env.GITEA_TEST_E2E_USER;
- await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
+ await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName, autoInit: false})]);
await page.goto(`/${user}/${repoName}/projects/new`);
await page.locator('input[name="title"]').fill('Kanban Board');
await page.getByRole('button', {name: 'Create Project'}).click();
@@ -33,7 +33,7 @@ test('create a project', async ({page}) => {
const projectTitle = 'Test Project';
await login(page);
- await apiCreateRepo(page.request, {name: repoName});
+ await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Navigate to new project page
@@ -62,7 +62,7 @@ test('assign issue to multiple projects via sidebar', async ({page}) => {
const issueTitle = 'Test issue for multiple projects';
await login(page);
- await apiCreateRepo(page.request, {name: repoName});
+ await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -112,7 +112,7 @@ test('create issue with multiple projects pre-selected', async ({page}) => {
const issueTitle = 'Issue with multiple projects';
await login(page);
- await apiCreateRepo(page.request, {name: repoName});
+ await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -163,7 +163,7 @@ test('filter issues by multiple projects in issue list', async ({page}) => {
const project2Title = 'Filter Project B';
await login(page);
- await apiCreateRepo(page.request, {name: repoName});
+ await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -229,7 +229,7 @@ test('remove issue from one project keeping others', async ({page}) => {
const issueTitle = 'Issue to modify projects';
await login(page);
- await apiCreateRepo(page.request, {name: repoName});
+ await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -288,7 +288,7 @@ test('filter issues with no project using project=-1', async ({page}) => {
const projectTitle = 'Some Project';
await login(page);
- await apiCreateRepo(page.request, {name: repoName});
+ await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create a project via UI
@@ -349,7 +349,7 @@ test('close project and view in closed projects list', async ({page}) => {
const closedProjectTitle = 'Project To Close';
await login(page);
- await apiCreateRepo(page.request, {name: repoName});
+ await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects via UI
@@ -404,7 +404,7 @@ test('select projects on new issue page shows in sidebar', async ({page}) => {
const project2Title = 'Project Two';
await login(page);
- await apiCreateRepo(page.request, {name: repoName});
+ await apiCreateRepo(page.request, {name: repoName, autoInit: false});
try {
// Create two projects
diff --git a/tests/e2e/mermaid.test.ts b/tests/e2e/mermaid.test.ts
index b2e066e551..1523577eaa 100644
--- a/tests/e2e/mermaid.test.ts
+++ b/tests/e2e/mermaid.test.ts
@@ -5,7 +5,7 @@ import {apiCreateRepo, apiCreateIssue, assertNoJsError, randomString} from './ut
test('mermaid diagram in issue', async ({page, request}) => {
const repoName = `e2e-mermaid-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
- await apiCreateRepo(request, {name: repoName});
+ await apiCreateRepo(request, {name: repoName, autoInit: false});
const body = '```mermaid\nflowchart LR\n Alpha --> Beta\n Beta --> Gamma\n```\n';
const {index} = await apiCreateIssue(request, {owner, repo: repoName, title: 'mermaid test', body});
await page.goto(`/${owner}/${repoName}/issues/${index}`);
diff --git a/tests/e2e/milestone.test.ts b/tests/e2e/milestone.test.ts
index 5a688fb128..106381e64f 100644
--- a/tests/e2e/milestone.test.ts
+++ b/tests/e2e/milestone.test.ts
@@ -4,7 +4,7 @@ import {login, apiCreateRepo, randomString} from './utils.ts';
test('create a milestone', async ({page}) => {
const repoName = `e2e-milestone-${randomString(8)}`;
- await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
+ await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName, autoInit: false})]);
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/milestones/new`);
await page.getByPlaceholder('Title').fill('Test Milestone');
await page.getByRole('button', {name: 'Create Milestone'}).click();
diff --git a/tests/e2e/pr-create.test.ts b/tests/e2e/pr-create.test.ts
new file mode 100644
index 0000000000..592512683f
--- /dev/null
+++ b/tests/e2e/pr-create.test.ts
@@ -0,0 +1,23 @@
+import {env} from 'node:process';
+import {test, expect} from '@playwright/test';
+import {login, apiCreateRepo, apiCreateFile, randomString} from './utils.ts';
+
+test('create a pull request from the compare page', async ({page, request}) => {
+ const repoName = `e2e-pr-create-${randomString(8)}`;
+ const owner = env.GITEA_TEST_E2E_USER;
+ await apiCreateRepo(request, {name: repoName});
+ await Promise.all([
+ apiCreateFile(request, owner, repoName, 'feat.txt', 'feature content\n', {branch: 'main', newBranch: 'feat'}),
+ login(page),
+ ]);
+ // expand=1 renders the PR form directly, skipping the "New Pull Request" toggle click
+ await page.goto(`/${owner}/${repoName}/compare/main...feat?expand=1`);
+
+ const title = `e2e-pr-${randomString(8)}`;
+ await page.getByPlaceholder('Title').fill(title);
+ await page.getByRole('button', {name: 'Create Pull Request'}).click();
+
+ // commit, not full load: the PR title heading is server-rendered, so the assertion can resolve before the heavy diff/timeline finishes
+ await page.waitForURL(new RegExp(`/${owner}/${repoName}/pulls/\\d+$`), {waitUntil: 'commit'});
+ await expect(page.getByRole('heading', {name: title})).toBeVisible();
+});
diff --git a/tests/e2e/reactions.test.ts b/tests/e2e/reactions.test.ts
index 2048b938cf..a6b6183741 100644
--- a/tests/e2e/reactions.test.ts
+++ b/tests/e2e/reactions.test.ts
@@ -5,7 +5,7 @@ import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts';
test('toggle issue reactions', async ({page, request}) => {
const repoName = `e2e-reactions-${randomString(8)}`;
const owner = env.GITEA_TEST_E2E_USER;
- await apiCreateRepo(request, {name: repoName});
+ await apiCreateRepo(request, {name: repoName, autoInit: false});
await Promise.all([
apiCreateIssue(request, {owner, repo: repoName, title: 'Reaction test'}),
login(page),
diff --git a/tests/e2e/release.test.ts b/tests/e2e/release.test.ts
new file mode 100644
index 0000000000..219a16d2c5
--- /dev/null
+++ b/tests/e2e/release.test.ts
@@ -0,0 +1,19 @@
+import {env} from 'node:process';
+import {test, expect} from '@playwright/test';
+import {login, apiCreateRepo, randomString} from './utils.ts';
+
+test('create a release', async ({page, request}) => {
+ const repoName = `e2e-release-${randomString(8)}`;
+ const owner = env.GITEA_TEST_E2E_USER;
+ await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
+ await page.goto(`/${owner}/${repoName}/releases/new`);
+
+ const tag = `v1.0.0-${randomString(8)}`;
+ const title = `e2e-release-${randomString(8)}`;
+ await page.getByLabel('Tag name').fill(tag);
+ await page.getByLabel('Release title').fill(title);
+ await page.getByRole('button', {name: 'Publish Release'}).click();
+
+ await page.waitForURL(new RegExp(`/${owner}/${repoName}/releases$`));
+ await expect(page.locator('.release-list-title')).toContainText(title);
+});
diff --git a/tests/e2e/repo-star-watch.test.ts b/tests/e2e/repo-star-watch.test.ts
new file mode 100644
index 0000000000..d5f8449165
--- /dev/null
+++ b/tests/e2e/repo-star-watch.test.ts
@@ -0,0 +1,20 @@
+import {test, expect} from '@playwright/test';
+import {login, apiCreateRepo, apiCreateUser, apiUserHeaders, randomString} from './utils.ts';
+
+test('star and watch a repository', async ({page, request}) => {
+ const owner = `sw-owner-${randomString(8)}`;
+ const repoName = `e2e-star-watch-${randomString(8)}`;
+ await apiCreateUser(request, owner);
+ await Promise.all([
+ apiCreateRepo(request, {name: repoName, autoInit: false, headers: apiUserHeaders(owner)}),
+ login(page),
+ ]);
+ await page.goto(`/${owner}/${repoName}`);
+
+ // exact match so "Star"/"Watch" don't also match "Unstar"/"Unwatch"
+ await page.getByRole('button', {name: 'Star', exact: true}).click();
+ await expect(page.getByRole('button', {name: 'Unstar'})).toBeVisible();
+
+ await page.getByRole('button', {name: 'Watch', exact: true}).click();
+ await expect(page.getByRole('button', {name: 'Unwatch'})).toBeVisible();
+});
diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go
index 92f9b61081..8fb892cc86 100644
--- a/tests/integration/api_packages_npm_test.go
+++ b/tests/integration/api_packages_npm_test.go
@@ -13,6 +13,7 @@ import (
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/packages"
+ 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/packages/npm"
@@ -20,6 +21,7 @@ import (
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
func TestPackageNpm(t *testing.T) {
@@ -39,6 +41,7 @@ func TestPackageNpm(t *testing.T) {
packageBinPath := "./cli.sh"
repoType := "gitea"
repoURL := "http://localhost:3000/gitea/test.git"
+ repoDirectory := "package-subdir"
data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
@@ -67,8 +70,10 @@ func TestPackageNpm(t *testing.T) {
},
"repository": {
"type": "` + repoType + `",
- "url": "` + repoURL + `"
+ "url": "` + repoURL + `",
+ "directory": "` + repoDirectory + `"
},
+ "readme": "[docs](docs/usage.md)\n",
"peerDependencies": {
"tea": "2.x",
"soy-milk": "1.2"
@@ -282,6 +287,28 @@ func TestPackageNpm(t *testing.T) {
}
})
+ t.Run("WebViewReadmeRepoLinks", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNpm)
+ assert.NoError(t, err)
+ require.Len(t, pvs, 1)
+
+ // link the package to a repository so README relative links resolve against
+ // repository files instead of the site root
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ assert.NoError(t, packages.SetRepositoryLink(t.Context(), pvs[0].PackageID, repo.ID))
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/npm/%s/%s", user.Name, url.PathEscape(packageName), packageVersion)).
+ AddBasicAuth(user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+ doc := NewHTMLParser(t, resp.Body)
+ rendered, _ := doc.Find(".markup.markdown").Html()
+ assert.Equal(t, `docs
+
+`, rendered)
+ })
+
t.Run("Delete", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go
index 2b19c3452d..406c82e475 100644
--- a/tests/integration/api_repo_test.go
+++ b/tests/integration/api_repo_test.go
@@ -351,48 +351,52 @@ func TestAPIGetRepoByIDUnauthorized(t *testing.T) {
}
func TestAPIRepoMigrate(t *testing.T) {
- testCases := []struct {
- ctxUserID, userID int64
- cloneURL, repoName string
- expectedStatus int
- }{
- {ctxUserID: 1, userID: 2, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-admin", expectedStatus: http.StatusCreated},
- {ctxUserID: 2, userID: 2, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-own", expectedStatus: http.StatusCreated},
- {ctxUserID: 2, userID: 1, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad", expectedStatus: http.StatusForbidden},
- {ctxUserID: 2, userID: 3, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-org", expectedStatus: http.StatusCreated},
- {ctxUserID: 2, userID: 6, cloneURL: "https://github.com/go-gitea/test_repo.git", repoName: "git-bad-org", expectedStatus: http.StatusForbidden},
- {ctxUserID: 2, userID: 3, cloneURL: "https://localhost:3000/user/test_repo.git", repoName: "private-ip", expectedStatus: http.StatusUnprocessableEntity},
- {ctxUserID: 2, userID: 3, cloneURL: "https://10.0.0.1/user/test_repo.git", repoName: "private-ip", expectedStatus: http.StatusUnprocessableEntity},
- }
+ onGiteaRun(t, func(t *testing.T, u *url.URL) {
+ // migrate from a local fixture repo (user2/repo1) via the live listener so the test runs offline
+ repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
+ cloneAddr := fmt.Sprintf("%s%s/%s.git", u.String(), repo1.OwnerName, repo1.Name)
- defer tests.PrepareTestEnv(t)()
- defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, false)()
- require.NoError(t, migrations.Init())
-
- for _, testCase := range testCases {
- user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
- session := loginUser(t, user.Name)
- token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
- req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOptions{
- CloneAddr: testCase.cloneURL,
- RepoOwnerID: testCase.userID,
- RepoName: testCase.repoName,
- }).AddTokenAuth(token)
- resp := MakeRequest(t, req, NoExpectedStatus)
- if resp.Code == http.StatusUnprocessableEntity {
- respJSON := DecodeJSON(t, resp, map[string]string{})
- switch respJSON["message"] {
- case "Remote visit addressed rate limitation.":
- t.Log("test hit github rate limitation")
- case "You can not import from disallowed hosts.":
- assert.Equal(t, "private-ip", testCase.repoName)
- default:
- assert.FailNow(t, "unexpected error", "unexpected error '%v' on url '%s'", respJSON["message"], testCase.cloneURL)
+ t.Run("Permitted", func(t *testing.T) {
+ // migrations.Init builds the host allowlist from AllowLocalNetworks, so set it first
+ defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
+ require.NoError(t, migrations.Init())
+ for _, testCase := range []struct {
+ ctxUserID, ownerID int64
+ repoName string
+ expectedStatus int
+ }{
+ {ctxUserID: 1, ownerID: 2, repoName: "git-admin", expectedStatus: http.StatusCreated},
+ {ctxUserID: 2, ownerID: 2, repoName: "git-own", expectedStatus: http.StatusCreated},
+ {ctxUserID: 2, ownerID: 1, repoName: "git-bad", expectedStatus: http.StatusForbidden},
+ {ctxUserID: 2, ownerID: 3, repoName: "git-org", expectedStatus: http.StatusCreated},
+ {ctxUserID: 2, ownerID: 6, repoName: "git-bad-org", expectedStatus: http.StatusForbidden},
+ } {
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
+ token := getTokenForLoggedInUser(t, loginUser(t, user.Name), auth_model.AccessTokenScopeWriteRepository)
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOptions{
+ CloneAddr: cloneAddr,
+ RepoOwnerID: testCase.ownerID,
+ RepoName: testCase.repoName,
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, testCase.expectedStatus)
}
- } else {
- assert.Equal(t, testCase.expectedStatus, resp.Code)
- }
- }
+ })
+
+ t.Run("DisallowedHost", func(t *testing.T) {
+ defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, false)()
+ require.NoError(t, migrations.Init())
+ token := getTokenForLoggedInUser(t, loginUser(t, "user2"), auth_model.AccessTokenScopeWriteRepository)
+ for _, cloneURL := range []string{"https://localhost:3000/user/test_repo.git", "https://10.0.0.1/user/test_repo.git"} {
+ req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", &api.MigrateRepoOptions{
+ CloneAddr: cloneURL,
+ RepoOwnerID: 3,
+ RepoName: "private-ip",
+ }).AddTokenAuth(token)
+ resp := MakeRequest(t, req, http.StatusUnprocessableEntity)
+ assert.Equal(t, "You can not import from disallowed hosts.", DecodeJSON(t, resp, map[string]string{})["message"])
+ }
+ })
+ })
}
func TestAPIRepoMigrateConflict(t *testing.T) {
diff --git a/tools/ci-tools.ts b/tools/ci-tools.ts
new file mode 100644
index 0000000000..4562643247
--- /dev/null
+++ b/tools/ci-tools.ts
@@ -0,0 +1,133 @@
+#!/usr/bin/env node
+import {argv, env, exit} from 'node:process';
+
+const allowedTypes = [
+ 'build',
+ 'chore',
+ 'ci',
+ 'docs',
+ 'enhance',
+ 'feat',
+ 'fix',
+ 'perf',
+ 'refactor',
+ 'revert',
+ 'style',
+ 'test',
+] as const;
+type CommitType = typeof allowedTypes[number];
+
+const allowedTypesList = allowedTypes.join(', ');
+const titlePattern = new RegExp(`^(${allowedTypes.join('|')})(\\([\\w/.-]+\\))?(!)?: .+$`);
+
+function parsePrTitle(title: string): {type: CommitType; breaking: boolean} | null {
+ const match = titlePattern.exec(title);
+ return match ? {type: match[1] as CommitType, breaking: Boolean(match[3])} : null;
+}
+
+const breakingLabel = 'pr/breaking';
+
+// Mutually exclusive type labels, fully synced with the title type (added and removed).
+const typeLabels: Partial> = {
+ feat: 'type/feature',
+ enhance: 'type/enhancement',
+ fix: 'type/bug',
+ docs: 'type/docs',
+ test: 'type/testing',
+};
+
+// Non-type labels, only added, never auto-removed, so manual labeling is not clobbered.
+const extraLabels: Partial> = {
+ chore: 'skip-changelog',
+ ci: 'skip-changelog',
+ build: 'topic/build',
+};
+
+// Labels this tool may remove when the title no longer implies them.
+const removableLabels = [...Object.values(typeLabels), breakingLabel];
+
+function labelsForPrTitle(title: string): string[] {
+ const parsed = parsePrTitle(title);
+ if (!parsed) return [];
+ return [typeLabels[parsed.type], extraLabels[parsed.type], parsed.breaking ? breakingLabel : undefined]
+ .filter((label): label is string => label !== undefined);
+}
+
+// Command: validate PR_TITLE against the allowed Conventional Commits format.
+function lintPrTitle(): void {
+ if (!env.PR_TITLE) {
+ console.error('Missing PR_TITLE');
+ exit(1);
+ }
+ if (!parsePrTitle(env.PR_TITLE)) {
+ console.error(`Invalid PR title: ${env.PR_TITLE}`);
+ console.error('Expected format: type(scope): subject (scope optional, append "!" for breaking changes)');
+ console.error(`Allowed types: ${allowedTypesList}`);
+ exit(1);
+ }
+}
+
+// Command: sync the title-derived labels onto the PR via the GitHub API.
+async function setPrLabels(): Promise {
+ if (!env.PR_TITLE || !env.GITHUB_TOKEN || !env.GITHUB_REPOSITORY || !env.PR_NUMBER) {
+ console.error('set-pr-labels requires PR_TITLE, GITHUB_TOKEN, GITHUB_REPOSITORY and PR_NUMBER');
+ exit(1);
+ }
+
+ const labelsUrl = `https://api.github.com/repos/${env.GITHUB_REPOSITORY}/issues/${env.PR_NUMBER}/labels`;
+
+ async function request(url: string, method = 'GET', body?: unknown): Promise {
+ const response = await fetch(url, {
+ method,
+ headers: {
+ Accept: 'application/vnd.github+json',
+ Authorization: `Bearer ${env.GITHUB_TOKEN}`,
+ 'X-GitHub-Api-Version': '2022-11-28',
+ ...(body ? {'Content-Type': 'application/json'} : {}),
+ },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ if (!response.ok) {
+ throw new Error(`GitHub API ${method} ${url} failed (${response.status}): ${await response.text()}`);
+ }
+ return response;
+ }
+
+ const desired = labelsForPrTitle(env.PR_TITLE);
+ const response = await request(`${labelsUrl}?per_page=100`);
+ const current = ((await response.json()) as Array<{name: string}>).map((label) => label.name);
+
+ const toAdd = desired.filter((name) => !current.includes(name));
+ const toRemove = removableLabels.filter((name) => current.includes(name) && !desired.includes(name));
+
+ if (toAdd.length) {
+ await request(labelsUrl, 'POST', {labels: toAdd});
+ console.info(`Added labels: ${toAdd.join(', ')}`);
+ }
+ for (const name of toRemove) {
+ await request(`${labelsUrl}/${encodeURIComponent(name)}`, 'DELETE');
+ console.info(`Removed label: ${name}`);
+ }
+ if (!toAdd.length && !toRemove.length) {
+ console.info('PR labels already in sync');
+ }
+}
+
+const commands: Record void | Promise> = {
+ 'lint-pr-title': lintPrTitle,
+ 'set-pr-labels': setPrLabels,
+};
+
+const command = argv[2];
+const handler = commands[command];
+if (!handler) {
+ console.error(`Usage: ci-tools.ts <${Object.keys(commands).join('|')}>`);
+ exit(1);
+}
+
+try {
+ await handler();
+} catch (error) {
+ console.error(error instanceof Error ? error.message : error);
+ exit(1);
+}
diff --git a/tools/lint-pr-title.ts b/tools/lint-pr-title.ts
deleted file mode 100644
index a63defe972..0000000000
--- a/tools/lint-pr-title.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env node
-import {env, exit} from 'node:process';
-
-const allowedTypes = 'build, chore, ci, docs, enhance, feat, fix, perf, refactor, revert, style, test';
-const title = env.PR_TITLE;
-
-if (!title) {
- console.error('Missing PR_TITLE');
- exit(1);
-}
-
-const validTitlePattern = new RegExp(`^(${allowedTypes.replaceAll(', ', '|')})(\\([\\w.-]+\\))?(!)?: .+$`);
-
-if (!validTitlePattern.test(title)) {
- console.error(`Invalid PR title: ${title}`);
- console.error('Expected format: type(scope): subject');
- console.error(`Allowed types: ${allowedTypes}`);
- exit(1);
-}
diff --git a/tools/lint-shell.sh b/tools/lint-shell.sh
new file mode 100755
index 0000000000..dac838e833
--- /dev/null
+++ b/tools/lint-shell.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -euo pipefail
+
+CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}"
+VERSION=$(echo "$SHELLCHECK_IMAGE" | sed -E 's/.*:v([0-9.]+)@.*/\1/')
+
+if hash shellcheck 2>/dev/null && shellcheck --version | grep -qx "version: $VERSION"; then
+ exec shellcheck --color=always "$@"
+else
+ exec "$CONTAINER_RUNTIME" run --rm -v "$PWD":/mnt -w /mnt "$SHELLCHECK_IMAGE" --color=always "$@"
+fi
diff --git a/tools/test-e2e.sh b/tools/test-e2e.sh
index d6c053ff1f..be3e848ee1 100755
--- a/tools/test-e2e.sh
+++ b/tools/test-e2e.sh
@@ -71,8 +71,10 @@ if [ "$CMD" = "install" ]; then
if [ "$PLAYWRIGHT_MODE" = "local" ]; then
# on GitHub Actions VMs, playwright's system deps are pre-installed
if [ -z "${GITHUB_ACTIONS:-}" ]; then
+ # shellcheck disable=SC2086 # flag string
pnpm exec playwright install --with-deps chromium firefox ${PLAYWRIGHT_FLAGS:-}
else
+ # shellcheck disable=SC2086 # flag string
pnpm exec playwright install chromium firefox ${PLAYWRIGHT_FLAGS:-}
fi
else
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index f3b169aeb9..b47e03b8ce 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -1578,6 +1578,7 @@ tbody.commit-list {
#diff-container {
display: flex;
+ gap: var(--page-spacing);
}
#diff-file-boxes {