mirror of
https://github.com/go-gitea/gitea.git
synced 2025-12-13 18:32:54 +00:00
Compare commits
9 Commits
01351cc6c7
...
3102c04c1e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3102c04c1e | ||
|
|
3e57ba5b36 | ||
|
|
4c06c98dda | ||
|
|
87b855bd15 | ||
|
|
906adff0c1 | ||
|
|
4cbcb91b7b | ||
|
|
bfbc38f40c | ||
|
|
d2a372fc59 | ||
|
|
f25409fab8 |
4
.github/workflows/cron-licenses.yml
vendored
4
.github/workflows/cron-licenses.yml
vendored
@@ -9,8 +9,10 @@ jobs:
|
|||||||
cron-licenses:
|
cron-licenses:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'go-gitea/gitea'
|
if: github.repository == 'go-gitea/gitea'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|||||||
4
.github/workflows/cron-translations.yml
vendored
4
.github/workflows/cron-translations.yml
vendored
@@ -9,8 +9,10 @@ jobs:
|
|||||||
crowdin-pull:
|
crowdin-pull:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'go-gitea/gitea'
|
if: github.repository == 'go-gitea/gitea'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: crowdin/github-action@v1
|
- uses: crowdin/github-action@v1
|
||||||
with:
|
with:
|
||||||
upload_sources: true
|
upload_sources: true
|
||||||
|
|||||||
4
.github/workflows/files-changed.yml
vendored
4
.github/workflows/files-changed.yml
vendored
@@ -24,6 +24,8 @@ jobs:
|
|||||||
detect:
|
detect:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
backend: ${{ steps.changes.outputs.backend }}
|
backend: ${{ steps.changes.outputs.backend }}
|
||||||
frontend: ${{ steps.changes.outputs.frontend }}
|
frontend: ${{ steps.changes.outputs.frontend }}
|
||||||
@@ -34,7 +36,7 @@ jobs:
|
|||||||
swagger: ${{ steps.changes.outputs.swagger }}
|
swagger: ${{ steps.changes.outputs.swagger }}
|
||||||
yaml: ${{ steps.changes.outputs.yaml }}
|
yaml: ${{ steps.changes.outputs.yaml }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: dorny/paths-filter@v3
|
- uses: dorny/paths-filter@v3
|
||||||
id: changes
|
id: changes
|
||||||
with:
|
with:
|
||||||
|
|||||||
50
.github/workflows/pull-compliance.yml
vendored
50
.github/workflows/pull-compliance.yml
vendored
@@ -10,13 +10,17 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
files-changed:
|
files-changed:
|
||||||
uses: ./.github/workflows/files-changed.yml
|
uses: ./.github/workflows/files-changed.yml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
lint-backend:
|
lint-backend:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -30,8 +34,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.templates == 'true'
|
if: needs.files-changed.outputs.templates == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: astral-sh/setup-uv@v6
|
- uses: astral-sh/setup-uv@v6
|
||||||
- run: uv python install 3.12
|
- run: uv python install 3.12
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
@@ -46,8 +52,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.yaml == 'true'
|
if: needs.files-changed.outputs.yaml == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: astral-sh/setup-uv@v6
|
- uses: astral-sh/setup-uv@v6
|
||||||
- run: uv python install 3.12
|
- run: uv python install 3.12
|
||||||
- run: make deps-py
|
- run: make deps-py
|
||||||
@@ -57,8 +65,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.swagger == 'true'
|
if: needs.files-changed.outputs.swagger == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
@@ -70,8 +80,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.templates == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -82,8 +94,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -99,8 +113,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -114,8 +130,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -127,8 +145,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
@@ -143,8 +163,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -175,8 +197,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v5
|
- uses: actions/setup-node@v5
|
||||||
with:
|
with:
|
||||||
@@ -188,8 +212,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|||||||
22
.github/workflows/pull-db-tests.yml
vendored
22
.github/workflows/pull-db-tests.yml
vendored
@@ -10,11 +10,15 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
files-changed:
|
files-changed:
|
||||||
uses: ./.github/workflows/files-changed.yml
|
uses: ./.github/workflows/files-changed.yml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
test-pgsql:
|
test-pgsql:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
services:
|
services:
|
||||||
pgsql:
|
pgsql:
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
@@ -38,7 +42,7 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- "9000:9000"
|
- "9000:9000"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -65,8 +69,10 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -90,6 +96,8 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
services:
|
services:
|
||||||
elasticsearch:
|
elasticsearch:
|
||||||
image: elasticsearch:7.5.0
|
image: elasticsearch:7.5.0
|
||||||
@@ -124,7 +132,7 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 10000:10000
|
- 10000:10000
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -152,6 +160,8 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
# the bitnami mysql image has more options than the official one, it's easier to customize
|
# the bitnami mysql image has more options than the official one, it's easier to customize
|
||||||
@@ -177,7 +187,7 @@ jobs:
|
|||||||
- "587:587"
|
- "587:587"
|
||||||
- "993:993"
|
- "993:993"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
@@ -203,6 +213,8 @@ jobs:
|
|||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
services:
|
services:
|
||||||
mssql:
|
mssql:
|
||||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||||
@@ -217,7 +229,7 @@ jobs:
|
|||||||
ports:
|
ports:
|
||||||
- 10000:10000
|
- 10000:10000
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|||||||
6
.github/workflows/pull-docker-dryrun.yml
vendored
6
.github/workflows/pull-docker-dryrun.yml
vendored
@@ -10,13 +10,17 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
files-changed:
|
files-changed:
|
||||||
uses: ./.github/workflows/files-changed.yml
|
uses: ./.github/workflows/files-changed.yml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
container:
|
container:
|
||||||
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
needs: files-changed
|
needs: files-changed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v3
|
||||||
- name: Build regular container image
|
- name: Build regular container image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
|
|||||||
8
.github/workflows/release-nightly.yml
vendored
8
.github/workflows/release-nightly.yml
vendored
@@ -11,8 +11,10 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
nightly-binary:
|
nightly-binary:
|
||||||
runs-on: namespace-profile-gitea-release-binary
|
runs-on: namespace-profile-gitea-release-binary
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
@@ -56,12 +58,14 @@ jobs:
|
|||||||
- name: upload binaries to s3
|
- name: upload binaries to s3
|
||||||
run: |
|
run: |
|
||||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||||
|
|
||||||
nightly-container:
|
nightly-container:
|
||||||
runs-on: namespace-profile-gitea-release-docker
|
runs-on: namespace-profile-gitea-release-docker
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
|
|||||||
8
.github/workflows/release-tag-rc.yml
vendored
8
.github/workflows/release-tag-rc.yml
vendored
@@ -12,8 +12,10 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
binary:
|
binary:
|
||||||
runs-on: namespace-profile-gitea-release-binary
|
runs-on: namespace-profile-gitea-release-binary
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
@@ -66,12 +68,14 @@ jobs:
|
|||||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
|
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
|
||||||
container:
|
container:
|
||||||
runs-on: namespace-profile-gitea-release-docker
|
runs-on: namespace-profile-gitea-release-docker
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
|
|||||||
7
.github/workflows/release-tag-version.yml
vendored
7
.github/workflows/release-tag-version.yml
vendored
@@ -15,9 +15,10 @@ jobs:
|
|||||||
binary:
|
binary:
|
||||||
runs-on: namespace-profile-gitea-release-binary
|
runs-on: namespace-profile-gitea-release-binary
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
@@ -70,12 +71,14 @@ jobs:
|
|||||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
|
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
|
||||||
container:
|
container:
|
||||||
runs-on: namespace-profile-gitea-release-docker
|
runs-on: namespace-profile-gitea-release-docker
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
packages: write # to publish to ghcr.io
|
packages: write # to publish to ghcr.io
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v6
|
||||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||||
- run: git fetch --unshallow --quiet --tags --force
|
- run: git fetch --unshallow --quiet --tags --force
|
||||||
|
|||||||
@@ -18,6 +18,23 @@ import (
|
|||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AdminUserOrderByMap represents all possible admin user search orders
|
||||||
|
// This should only be used for admin API endpoints as we should not expose "updated" ordering which could expose recent user activity including logins.
|
||||||
|
var AdminUserOrderByMap = map[string]map[string]db.SearchOrderBy{
|
||||||
|
"asc": {
|
||||||
|
"name": db.SearchOrderByAlphabetically,
|
||||||
|
"created": db.SearchOrderByOldest,
|
||||||
|
"updated": db.SearchOrderByLeastUpdated,
|
||||||
|
"id": db.SearchOrderByID,
|
||||||
|
},
|
||||||
|
"desc": {
|
||||||
|
"name": db.SearchOrderByAlphabeticallyReverse,
|
||||||
|
"created": db.SearchOrderByNewest,
|
||||||
|
"updated": db.SearchOrderByRecentUpdated,
|
||||||
|
"id": db.SearchOrderByIDReverse,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// SearchUserOptions contains the options for searching
|
// SearchUserOptions contains the options for searching
|
||||||
type SearchUserOptions struct {
|
type SearchUserOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
|
|||||||
@@ -32,20 +32,6 @@ func GetRawDiff(repo *Repository, commitID string, diffType RawDiffType, writer
|
|||||||
return GetRepoRawDiffForFile(repo, "", commitID, diffType, "", writer)
|
return GetRepoRawDiffForFile(repo, "", commitID, diffType, "", writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer.
|
|
||||||
func GetReverseRawDiff(ctx context.Context, repoPath, commitID string, writer io.Writer) error {
|
|
||||||
stderr := new(bytes.Buffer)
|
|
||||||
if err := gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R").
|
|
||||||
AddDynamicArguments(commitID).
|
|
||||||
WithDir(repoPath).
|
|
||||||
WithStdout(writer).
|
|
||||||
WithStderr(stderr).
|
|
||||||
Run(ctx); err != nil {
|
|
||||||
return fmt.Errorf("Run: %w - %s", err, stderr)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepoRawDiffForFile dumps diff results of file in given commit ID to io.Writer according given repository
|
// GetRepoRawDiffForFile dumps diff results of file in given commit ID to io.Writer according given repository
|
||||||
func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
|
func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diffType RawDiffType, file string, writer io.Writer) error {
|
||||||
commit, err := repo.GetCommit(endCommit)
|
commit, err := repo.GetCommit(endCommit)
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ type CloneRepoOptions struct {
|
|||||||
Depth int
|
Depth int
|
||||||
Filter string
|
Filter string
|
||||||
SkipTLSVerify bool
|
SkipTLSVerify bool
|
||||||
|
SingleBranch bool
|
||||||
|
Env []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone clones original repository to target path.
|
// Clone clones original repository to target path.
|
||||||
@@ -157,6 +159,9 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
|
|||||||
if opts.Filter != "" {
|
if opts.Filter != "" {
|
||||||
cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter)
|
cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter)
|
||||||
}
|
}
|
||||||
|
if opts.SingleBranch {
|
||||||
|
cmd.AddArguments("--single-branch")
|
||||||
|
}
|
||||||
if len(opts.Branch) > 0 {
|
if len(opts.Branch) > 0 {
|
||||||
cmd.AddArguments("-b").AddDynamicArguments(opts.Branch)
|
cmd.AddArguments("-b").AddDynamicArguments(opts.Branch)
|
||||||
}
|
}
|
||||||
@@ -167,13 +172,17 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
envs := os.Environ()
|
envs := os.Environ()
|
||||||
u, err := url.Parse(from)
|
if opts.Env != nil {
|
||||||
if err == nil {
|
envs = opts.Env
|
||||||
envs = proxy.EnvWithProxy(u)
|
} else {
|
||||||
|
u, err := url.Parse(from)
|
||||||
|
if err == nil {
|
||||||
|
envs = proxy.EnvWithProxy(u)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stderr := new(bytes.Buffer)
|
stderr := new(bytes.Buffer)
|
||||||
if err = cmd.
|
if err := cmd.
|
||||||
WithTimeout(opts.Timeout).
|
WithTimeout(opts.Timeout).
|
||||||
WithEnv(envs).
|
WithEnv(envs).
|
||||||
WithStdout(io.Discard).
|
WithStdout(io.Discard).
|
||||||
@@ -228,14 +237,3 @@ func Push(ctx context.Context, repoPath string, opts PushOptions) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestCommitTime returns time for latest commit in repository (across all branches)
|
|
||||||
func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) {
|
|
||||||
cmd := gitcmd.NewCommand("for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)")
|
|
||||||
stdout, _, err := cmd.WithDir(repoPath).RunStdString(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, err
|
|
||||||
}
|
|
||||||
commitTime := strings.TrimSpace(stdout)
|
|
||||||
return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,16 +10,6 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetLatestCommitTime(t *testing.T) {
|
|
||||||
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
|
|
||||||
lct, err := GetLatestCommitTime(t.Context(), bareRepo1Path)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
// Time is Sun Nov 13 16:40:14 2022 +0100
|
|
||||||
// which is the time of commit
|
|
||||||
// ce064814f4a0d337b333e646ece456cd39fab612 (refs/heads/master)
|
|
||||||
assert.EqualValues(t, 1668354014, lct.Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepoIsEmpty(t *testing.T) {
|
func TestRepoIsEmpty(t *testing.T) {
|
||||||
emptyRepo2Path := filepath.Join(testReposDir, "repo2_empty")
|
emptyRepo2Path := filepath.Join(testReposDir, "repo2_empty")
|
||||||
repo, err := OpenRepository(t.Context(), emptyRepo2Path)
|
repo, err := OpenRepository(t.Context(), emptyRepo2Path)
|
||||||
|
|||||||
@@ -18,3 +18,7 @@ func CloneExternalRepo(ctx context.Context, fromRemoteURL string, toRepo Reposit
|
|||||||
func CloneRepoToLocal(ctx context.Context, fromRepo Repository, toLocalPath string, opts git.CloneRepoOptions) error {
|
func CloneRepoToLocal(ctx context.Context, fromRepo Repository, toLocalPath string, opts git.CloneRepoOptions) error {
|
||||||
return git.Clone(ctx, repoPath(fromRepo), toLocalPath, opts)
|
return git.Clone(ctx, repoPath(fromRepo), toLocalPath, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Clone(ctx context.Context, fromRepo, toRepo Repository, opts git.CloneRepoOptions) error {
|
||||||
|
return git.Clone(ctx, repoPath(fromRepo), repoPath(toRepo), opts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||||
@@ -94,3 +95,18 @@ func AllCommitsCount(ctx context.Context, repo Repository, hidePRRefs bool, file
|
|||||||
|
|
||||||
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
|
return strconv.ParseInt(strings.TrimSpace(stdout), 10, 64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFullCommitID(ctx context.Context, repo Repository, shortID string) (string, error) {
|
||||||
|
return git.GetFullCommitID(ctx, repoPath(repo), shortID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestCommitTime returns time for latest commit in repository (across all branches)
|
||||||
|
func GetLatestCommitTime(ctx context.Context, repo Repository) (time.Time, error) {
|
||||||
|
stdout, err := RunCmdString(ctx, repo,
|
||||||
|
gitcmd.NewCommand("for-each-ref", "--sort=-committerdate", git.BranchPrefix, "--count", "1", "--format=%(committerdate)"))
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
commitTime := strings.TrimSpace(stdout)
|
||||||
|
return time.Parse("Mon Jan _2 15:04:05 2006 -0700", commitTime)
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,3 +33,13 @@ func TestCommitsCountWithoutBase(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, int64(2), commitsCount)
|
assert.Equal(t, int64(2), commitsCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetLatestCommitTime(t *testing.T) {
|
||||||
|
bareRepo1 := &mockRepository{path: "repo1_bare"}
|
||||||
|
lct, err := GetLatestCommitTime(t.Context(), bareRepo1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// Time is Sun Nov 13 16:40:14 2022 +0100
|
||||||
|
// which is the time of commit
|
||||||
|
// ce064814f4a0d337b333e646ece456cd39fab612 (refs/heads/master)
|
||||||
|
assert.EqualValues(t, 1668354014, lct.Unix())
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
package gitrepo
|
package gitrepo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -60,3 +62,15 @@ func parseDiffStat(stdout string) (numFiles, totalAdditions, totalDeletions int,
|
|||||||
}
|
}
|
||||||
return numFiles, totalAdditions, totalDeletions, err
|
return numFiles, totalAdditions, totalDeletions, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetReverseRawDiff dumps the reverse diff results of repository in given commit ID to io.Writer.
|
||||||
|
func GetReverseRawDiff(ctx context.Context, repo Repository, commitID string, writer io.Writer) error {
|
||||||
|
stderr := new(bytes.Buffer)
|
||||||
|
if err := RunCmd(ctx, repo, gitcmd.NewCommand("show", "--pretty=format:revert %H%n", "-R").
|
||||||
|
AddDynamicArguments(commitID).
|
||||||
|
WithStdout(writer).
|
||||||
|
WithStderr(stderr)); err != nil {
|
||||||
|
return fmt.Errorf("GetReverseRawDiff: %w - %s", err, stderr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -98,3 +98,23 @@ func UpdateServerInfo(ctx context.Context, repo Repository) error {
|
|||||||
func GetRepoFS(repo Repository) fs.FS {
|
func GetRepoFS(repo Repository) fs.FS {
|
||||||
return os.DirFS(repoPath(repo))
|
return os.DirFS(repoPath(repo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsRepoFileExist(ctx context.Context, repo Repository, relativeFilePath string) (bool, error) {
|
||||||
|
absoluteFilePath := filepath.Join(repoPath(repo), relativeFilePath)
|
||||||
|
return util.IsExist(absoluteFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsRepoDirExist(ctx context.Context, repo Repository, relativeDirPath string) (bool, error) {
|
||||||
|
absoluteDirPath := filepath.Join(repoPath(repo), relativeDirPath)
|
||||||
|
return util.IsDir(absoluteDirPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveRepoFile(ctx context.Context, repo Repository, relativeFilePath string) error {
|
||||||
|
absoluteFilePath := filepath.Join(repoPath(repo), relativeFilePath)
|
||||||
|
return util.Remove(absoluteFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRepoFile(ctx context.Context, repo Repository, relativeFilePath string) (io.WriteCloser, error) {
|
||||||
|
absoluteFilePath := filepath.Join(repoPath(repo), relativeFilePath)
|
||||||
|
return os.Create(absoluteFilePath)
|
||||||
|
}
|
||||||
|
|||||||
@@ -370,6 +370,10 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: GOLANG-HTTP-TMPDIR: Some Golang packages (like "http") use os.TempDir() to create temporary files when uploading files.
|
||||||
|
// So ideally we should set the TMPDIR environment variable to make them use our managed temp directory.
|
||||||
|
// But there is no clear place to set it currently, for example: when running "install" page, the AppDataPath is not ready yet, then AppDataTempDir won't work
|
||||||
|
|
||||||
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
|
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
|
||||||
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
|
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
|
||||||
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
|
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
|
||||||
|
|||||||
@@ -46,11 +46,15 @@ func RouterMockPoint(pointName string) func(next http.Handler) http.Handler {
|
|||||||
//
|
//
|
||||||
// Then the mock function will be executed as a middleware at the mock point.
|
// Then the mock function will be executed as a middleware at the mock point.
|
||||||
// It only takes effect in testing mode (setting.IsInTesting == true).
|
// It only takes effect in testing mode (setting.IsInTesting == true).
|
||||||
func RouteMock(pointName string, h any) {
|
func RouteMock(pointName string, h any) func() {
|
||||||
if _, ok := routeMockPoints[pointName]; !ok {
|
if _, ok := routeMockPoints[pointName]; !ok {
|
||||||
panic("route mock point not found: " + pointName)
|
panic("route mock point not found: " + pointName)
|
||||||
}
|
}
|
||||||
|
old := routeMockPoints[pointName]
|
||||||
routeMockPoints[pointName] = toHandlerProvider(h)
|
routeMockPoints[pointName] = toHandlerProvider(h)
|
||||||
|
return func() {
|
||||||
|
routeMockPoints[pointName] = old
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteMockReset resets all mock points (no mock anymore)
|
// RouteMockReset resets all mock points (no mock anymore)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func NewRouter() *Router {
|
|||||||
// Use supports two middlewares
|
// Use supports two middlewares
|
||||||
func (r *Router) Use(middlewares ...any) {
|
func (r *Router) Use(middlewares ...any) {
|
||||||
for _, m := range middlewares {
|
for _, m := range middlewares {
|
||||||
if m != nil {
|
if !isNilOrFuncNil(m) {
|
||||||
r.chiRouter.Use(toHandlerProvider(m))
|
r.chiRouter.Use(toHandlerProvider(m))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -414,22 +414,116 @@ func SearchUsers(ctx *context.APIContext) {
|
|||||||
// in: query
|
// in: query
|
||||||
// description: page size of results
|
// description: page size of results
|
||||||
// type: integer
|
// type: integer
|
||||||
|
// - name: sort
|
||||||
|
// in: query
|
||||||
|
// description: sort users by attribute. Supported values are
|
||||||
|
// "name", "created", "updated" and "id".
|
||||||
|
// Default is "name"
|
||||||
|
// type: string
|
||||||
|
// - name: order
|
||||||
|
// in: query
|
||||||
|
// description: sort order, either "asc" (ascending) or "desc" (descending).
|
||||||
|
// Default is "asc", ignored if "sort" is not specified.
|
||||||
|
// type: string
|
||||||
|
// - name: q
|
||||||
|
// in: query
|
||||||
|
// description: search term (username, full name, email)
|
||||||
|
// type: string
|
||||||
|
// - name: visibility
|
||||||
|
// in: query
|
||||||
|
// description: visibility filter. Supported values are
|
||||||
|
// "public", "limited" and "private".
|
||||||
|
// type: string
|
||||||
|
// - name: is_active
|
||||||
|
// in: query
|
||||||
|
// description: filter active users
|
||||||
|
// type: boolean
|
||||||
|
// - name: is_admin
|
||||||
|
// in: query
|
||||||
|
// description: filter admin users
|
||||||
|
// type: boolean
|
||||||
|
// - name: is_restricted
|
||||||
|
// in: query
|
||||||
|
// description: filter restricted users
|
||||||
|
// type: boolean
|
||||||
|
// - name: is_2fa_enabled
|
||||||
|
// in: query
|
||||||
|
// description: filter 2FA enabled users
|
||||||
|
// type: boolean
|
||||||
|
// - name: is_prohibit_login
|
||||||
|
// in: query
|
||||||
|
// description: filter login prohibited users
|
||||||
|
// type: boolean
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/UserList"
|
// "$ref": "#/responses/UserList"
|
||||||
// "403":
|
// "403":
|
||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
listOptions := utils.GetListOptions(ctx)
|
listOptions := utils.GetListOptions(ctx)
|
||||||
|
|
||||||
users, maxResults, err := user_model.SearchUsers(ctx, user_model.SearchUserOptions{
|
orderBy := db.SearchOrderByAlphabetically
|
||||||
Actor: ctx.Doer,
|
sortMode := ctx.FormString("sort")
|
||||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
if len(sortMode) > 0 {
|
||||||
LoginName: ctx.FormTrim("login_name"),
|
sortOrder := ctx.FormString("order")
|
||||||
SourceID: ctx.FormInt64("source_id"),
|
if len(sortOrder) == 0 {
|
||||||
OrderBy: db.SearchOrderByAlphabetically,
|
sortOrder = "asc"
|
||||||
ListOptions: listOptions,
|
}
|
||||||
})
|
if searchModeMap, ok := user_model.AdminUserOrderByMap[sortOrder]; ok {
|
||||||
|
if order, ok := searchModeMap[sortMode]; ok {
|
||||||
|
orderBy = order
|
||||||
|
} else {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort mode: \"%s\"", sortMode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid sort order: \"%s\"", sortOrder))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var visible []api.VisibleType
|
||||||
|
visibilityParam := ctx.FormString("visibility")
|
||||||
|
if len(visibilityParam) > 0 {
|
||||||
|
if visibility, ok := api.VisibilityModes[visibilityParam]; ok {
|
||||||
|
visible = []api.VisibleType{visibility}
|
||||||
|
} else {
|
||||||
|
ctx.APIError(http.StatusUnprocessableEntity, fmt.Errorf("Invalid visibility: \"%s\"", visibilityParam))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchOpts := user_model.SearchUserOptions{
|
||||||
|
Actor: ctx.Doer,
|
||||||
|
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
||||||
|
LoginName: ctx.FormTrim("login_name"),
|
||||||
|
SourceID: ctx.FormInt64("source_id"),
|
||||||
|
Keyword: ctx.FormTrim("q"),
|
||||||
|
Visible: visible,
|
||||||
|
OrderBy: orderBy,
|
||||||
|
ListOptions: listOptions,
|
||||||
|
SearchByEmail: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.FormString("is_active") != "" {
|
||||||
|
searchOpts.IsActive = optional.Some(ctx.FormBool("is_active"))
|
||||||
|
}
|
||||||
|
if ctx.FormString("is_admin") != "" {
|
||||||
|
searchOpts.IsAdmin = optional.Some(ctx.FormBool("is_admin"))
|
||||||
|
}
|
||||||
|
if ctx.FormString("is_restricted") != "" {
|
||||||
|
searchOpts.IsRestricted = optional.Some(ctx.FormBool("is_restricted"))
|
||||||
|
}
|
||||||
|
if ctx.FormString("is_2fa_enabled") != "" {
|
||||||
|
searchOpts.IsTwoFactorEnabled = optional.Some(ctx.FormBool("is_2fa_enabled"))
|
||||||
|
}
|
||||||
|
if ctx.FormString("is_prohibit_login") != "" {
|
||||||
|
searchOpts.IsProhibitLogin = optional.Some(ctx.FormBool("is_prohibit_login"))
|
||||||
|
}
|
||||||
|
|
||||||
|
users, maxResults, err := user_model.SearchUsers(ctx, searchOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -72,8 +72,13 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
|
|||||||
req = req.WithContext(cache.WithCacheContext(ctx))
|
req = req.WithContext(cache.WithCacheContext(ctx))
|
||||||
ds.SetContextValue(httplib.RequestContextKey, req)
|
ds.SetContextValue(httplib.RequestContextKey, req)
|
||||||
ds.AddCleanUp(func() {
|
ds.AddCleanUp(func() {
|
||||||
if req.MultipartForm != nil {
|
// TODO: GOLANG-HTTP-TMPDIR: Golang saves the uploaded files to temp directory (TMPDIR) when parsing multipart-form.
|
||||||
_ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
// The "req" might have changed due to the new "req.WithContext" calls
|
||||||
|
// For example: in NewBaseContext, a new "req" with context is created, and the multipart-form is parsed there.
|
||||||
|
// So we always use the latest "req" from the data store.
|
||||||
|
ctxReq := ds.GetContextValue(httplib.RequestContextKey).(*http.Request)
|
||||||
|
if ctxReq.MultipartForm != nil {
|
||||||
|
_ = ctxReq.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
next.ServeHTTP(respWriter, req)
|
next.ServeHTTP(respWriter, req)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
@@ -66,7 +67,7 @@ func CherryPickPost(ctx *context.Context) {
|
|||||||
// Drop through to the "apply" method
|
// Drop through to the "apply" method
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
if parsed.form.Revert {
|
if parsed.form.Revert {
|
||||||
err = git.GetReverseRawDiff(ctx, ctx.Repo.Repository.RepoPath(), fromCommitID, buf)
|
err = gitrepo.GetReverseRawDiff(ctx, ctx.Repo.Repository, fromCommitID, buf)
|
||||||
} else {
|
} else {
|
||||||
err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
|
err = git.GetRawDiff(ctx.Repo.GitRepo, fromCommitID, "patch", buf)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func NewComment(ctx *context.Context) {
|
|||||||
ctx.ServerError("Unable to load base repo", err)
|
ctx.ServerError("Unable to load base repo", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
prHeadCommitID, err := git.GetFullCommitID(ctx, pull.BaseRepo.RepoPath(), prHeadRef)
|
prHeadCommitID, err := gitrepo.GetFullCommitID(ctx, pull.BaseRepo, prHeadRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("Get head commit Id of pr fail", err)
|
ctx.ServerError("Get head commit Id of pr fail", err)
|
||||||
return
|
return
|
||||||
@@ -128,7 +128,7 @@ func NewComment(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
headBranchRef := git.RefNameFromBranch(pull.HeadBranch)
|
headBranchRef := git.RefNameFromBranch(pull.HeadBranch)
|
||||||
headBranchCommitID, err := git.GetFullCommitID(ctx, pull.HeadRepo.RepoPath(), headBranchRef.String())
|
headBranchCommitID, err := gitrepo.GetFullCommitID(ctx, pull.HeadRepo, headBranchRef.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("Get head commit Id of head branch fail", err)
|
ctx.ServerError("Get head commit Id of head branch fail", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -227,6 +227,8 @@ func ctxDataSet(args ...any) func(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RouterMockPointBeforeWebRoutes = "before-web-routes"
|
||||||
|
|
||||||
// Routes returns all web routes
|
// Routes returns all web routes
|
||||||
func Routes() *web.Router {
|
func Routes() *web.Router {
|
||||||
routes := web.NewRouter()
|
routes := web.NewRouter()
|
||||||
@@ -285,7 +287,7 @@ func Routes() *web.Router {
|
|||||||
|
|
||||||
webRoutes := web.NewRouter()
|
webRoutes := web.NewRouter()
|
||||||
webRoutes.Use(mid...)
|
webRoutes.Use(mid...)
|
||||||
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS())
|
webRoutes.Group("", func() { registerWebRoutes(webRoutes) }, common.BlockExpensive(), common.QoS(), web.RouterMockPoint(RouterMockPointBeforeWebRoutes))
|
||||||
routes.Mount("", webRoutes)
|
routes.Mount("", webRoutes)
|
||||||
return routes
|
return routes
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,10 @@ type Base struct {
|
|||||||
Locale translation.Locale
|
Locale translation.Locale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ParseMultipartFormMaxMemory = int64(32 << 20)
|
||||||
|
|
||||||
func (b *Base) ParseMultipartForm() bool {
|
func (b *Base) ParseMultipartForm() bool {
|
||||||
err := b.Req.ParseMultipartForm(32 << 20)
|
err := b.Req.ParseMultipartForm(ParseMultipartFormMaxMemory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: all errors caused by client side should be ignored (connection closed).
|
// TODO: all errors caused by client side should be ignored (connection closed).
|
||||||
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
|
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ package doctor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
@@ -20,7 +18,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
|
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
@@ -142,10 +139,10 @@ func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create/Remove git-daemon-export-ok for git-daemon...
|
// Create/Remove git-daemon-export-ok for git-daemon...
|
||||||
daemonExportFile := filepath.Join(repo.RepoPath(), `git-daemon-export-ok`)
|
daemonExportFile := `git-daemon-export-ok`
|
||||||
isExist, err := util.IsExist(daemonExportFile)
|
isExist, err := gitrepo.IsRepoFileExist(ctx, repo, daemonExportFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
|
log.Error("Unable to check if %s:%s exists. Error: %v", repo.FullName(), daemonExportFile, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic
|
isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic
|
||||||
@@ -154,12 +151,12 @@ func checkDaemonExport(ctx context.Context, logger log.Logger, autofix bool) err
|
|||||||
numNeedUpdate++
|
numNeedUpdate++
|
||||||
if autofix {
|
if autofix {
|
||||||
if !isPublic && isExist {
|
if !isPublic && isExist {
|
||||||
if err = util.Remove(daemonExportFile); err != nil {
|
if err = gitrepo.RemoveRepoFile(ctx, repo, daemonExportFile); err != nil {
|
||||||
log.Error("Failed to remove %s: %v", daemonExportFile, err)
|
log.Error("Failed to remove %s:%s: %v", repo.FullName(), daemonExportFile, err)
|
||||||
}
|
}
|
||||||
} else if isPublic && !isExist {
|
} else if isPublic && !isExist {
|
||||||
if f, err := os.Create(daemonExportFile); err != nil {
|
if f, err := gitrepo.CreateRepoFile(ctx, repo, daemonExportFile); err != nil {
|
||||||
log.Error("Failed to create %s: %v", daemonExportFile, err)
|
log.Error("Failed to create %s:%s: %v", repo.FullName(), daemonExportFile, err)
|
||||||
} else {
|
} else {
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
@@ -190,16 +187,16 @@ func checkCommitGraph(ctx context.Context, logger log.Logger, autofix bool) erro
|
|||||||
|
|
||||||
commitGraphExists := func() (bool, error) {
|
commitGraphExists := func() (bool, error) {
|
||||||
// Check commit-graph exists
|
// Check commit-graph exists
|
||||||
commitGraphFile := filepath.Join(repo.RepoPath(), `objects/info/commit-graph`)
|
commitGraphFile := `objects/info/commit-graph`
|
||||||
isExist, err := util.IsExist(commitGraphFile)
|
isExist, err := gitrepo.IsRepoFileExist(ctx, repo, commitGraphFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Unable to check if %s exists. Error: %v", commitGraphFile, err)
|
logger.Error("Unable to check if %s exists. Error: %v", commitGraphFile, err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isExist {
|
if !isExist {
|
||||||
commitGraphsDir := filepath.Join(repo.RepoPath(), `objects/info/commit-graphs`)
|
commitGraphsDir := `objects/info/commit-graphs`
|
||||||
isExist, err = util.IsExist(commitGraphsDir)
|
isExist, err = gitrepo.IsRepoDirExist(ctx, repo, commitGraphsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Unable to check if %s exists. Error: %v", commitGraphsDir, err)
|
logger.Error("Unable to check if %s exists. Error: %v", commitGraphsDir, err)
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
|
|||||||
log.Error("SyncMirrors [repo_id: %v]: unable to GetMirrorByRepoID: %v", repoID, err)
|
log.Error("SyncMirrors [repo_id: %v]: unable to GetMirrorByRepoID: %v", repoID, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
_ = m.GetRepository(ctx) // force load repository of mirror
|
repo := m.GetRepository(ctx) // force load repository of mirror
|
||||||
|
|
||||||
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Syncing Mirror %s/%s", m.Repo.OwnerName, m.Repo.Name))
|
ctx, _, finished := process.GetManager().AddContext(ctx, fmt.Sprintf("Syncing Mirror %s/%s", m.Repo.OwnerName, m.Repo.Name))
|
||||||
defer finished()
|
defer finished()
|
||||||
@@ -515,12 +515,12 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Push commits
|
// Push commits
|
||||||
oldCommitID, err := git.GetFullCommitID(gitRepo.Ctx, gitRepo.Path, result.oldCommitID)
|
oldCommitID, err := gitrepo.GetFullCommitID(ctx, repo, result.oldCommitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID[%s]: %v", m.Repo, result.oldCommitID, err)
|
log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID[%s]: %v", m.Repo, result.oldCommitID, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newCommitID, err := git.GetFullCommitID(gitRepo.Ctx, gitRepo.Path, result.newCommitID)
|
newCommitID, err := gitrepo.GetFullCommitID(ctx, repo, result.newCommitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID [%s]: %v", m.Repo, result.newCommitID, err)
|
log.Error("SyncMirrors [repo: %-v]: unable to get GetFullCommitID [%s]: %v", m.Repo, result.newCommitID, err)
|
||||||
continue
|
continue
|
||||||
@@ -560,7 +560,7 @@ func SyncPullMirror(ctx context.Context, repoID int64) bool {
|
|||||||
}
|
}
|
||||||
if !isEmpty {
|
if !isEmpty {
|
||||||
// Get latest commit date and update to current repository updated time
|
// Get latest commit date and update to current repository updated time
|
||||||
commitDate, err := git.GetLatestCommitTime(ctx, m.Repo.RepoPath())
|
commitDate, err := gitrepo.GetLatestCommitTime(ctx, m.Repo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err)
|
log.Error("SyncMirrors [repo: %-v]: unable to GetLatestCommitDate: %v", m.Repo, err)
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ func getMergeCommit(ctx context.Context, pr *issues_model.PullRequest) (*git.Com
|
|||||||
// If merge-base successfully exits then prHeadRef is an ancestor of pr.BaseBranch
|
// If merge-base successfully exits then prHeadRef is an ancestor of pr.BaseBranch
|
||||||
|
|
||||||
// Find the head commit id
|
// Find the head commit id
|
||||||
prHeadCommitID, err := git.GetFullCommitID(ctx, pr.BaseRepo.RepoPath(), prHeadRef)
|
prHeadCommitID, err := gitrepo.GetFullCommitID(ctx, pr.BaseRepo, prHeadRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("GetFullCommitID(%s) in %s: %w", prHeadRef, pr.BaseRepo.FullName(), err)
|
return nil, fmt.Errorf("GetFullCommitID(%s) in %s: %w", prHeadRef, pr.BaseRepo.FullName(), err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
|
|||||||
|
|
||||||
compareInfo := new(CompareInfo)
|
compareInfo := new(CompareInfo)
|
||||||
|
|
||||||
compareInfo.HeadCommitID, err = git.GetFullCommitID(ctx, headGitRepo.Path, headBranch)
|
compareInfo.HeadCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, headBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
compareInfo.HeadCommitID = headBranch
|
compareInfo.HeadCommitID = headBranch
|
||||||
}
|
}
|
||||||
|
|
||||||
compareInfo.MergeBase, remoteBranch, err = headGitRepo.GetMergeBase(tmpRemote, baseBranch, headBranch)
|
compareInfo.MergeBase, remoteBranch, err = headGitRepo.GetMergeBase(tmpRemote, baseBranch, headBranch)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
compareInfo.BaseCommitID, err = git.GetFullCommitID(ctx, headGitRepo.Path, remoteBranch)
|
compareInfo.BaseCommitID, err = gitrepo.GetFullCommitID(ctx, headRepo, remoteBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
compareInfo.BaseCommitID = remoteBranch
|
compareInfo.BaseCommitID = remoteBranch
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
compareInfo.Commits = []*git.Commit{}
|
compareInfo.Commits = []*git.Commit{}
|
||||||
compareInfo.MergeBase, err = git.GetFullCommitID(ctx, headGitRepo.Path, remoteBranch)
|
compareInfo.MergeBase, err = gitrepo.GetFullCommitID(ctx, headRepo, remoteBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
compareInfo.MergeBase = remoteBranch
|
compareInfo.MergeBase = remoteBranch
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,9 +69,10 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Clone to temporary path and do the init commit.
|
// Clone to temporary path and do the init commit.
|
||||||
if stdout, _, err := gitcmd.NewCommand("clone").AddDynamicArguments(repo.RepoPath(), tmpDir).
|
if err := gitrepo.CloneRepoToLocal(ctx, repo, tmpDir, git.CloneRepoOptions{
|
||||||
WithEnv(env).RunStdString(ctx); err != nil {
|
Env: env,
|
||||||
log.Error("Failed to clone from %v into %s: stdout: %s\nError: %v", repo, tmpDir, stdout, err)
|
}); err != nil {
|
||||||
|
log.Error("Failed to clone from %v into %s\nError: %v", repo, tmpDir, err)
|
||||||
return fmt.Errorf("git clone: %w", err)
|
return fmt.Errorf("git clone: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,11 @@ func (t *TemporaryUploadRepository) Close() {
|
|||||||
|
|
||||||
// Clone the base repository to our path and set branch as the HEAD
|
// Clone the base repository to our path and set branch as the HEAD
|
||||||
func (t *TemporaryUploadRepository) Clone(ctx context.Context, branch string, bare bool) error {
|
func (t *TemporaryUploadRepository) Clone(ctx context.Context, branch string, bare bool) error {
|
||||||
cmd := gitcmd.NewCommand("clone", "-s", "-b").AddDynamicArguments(branch, t.repo.RepoPath(), t.basePath)
|
if err := gitrepo.CloneRepoToLocal(ctx, t.repo, t.basePath, git.CloneRepoOptions{
|
||||||
if bare {
|
Bare: bare,
|
||||||
cmd.AddArguments("--bare")
|
Branch: branch,
|
||||||
}
|
Shared: true,
|
||||||
|
}); err != nil {
|
||||||
if _, _, err := cmd.RunStdString(ctx); err != nil {
|
|
||||||
stderr := err.Error()
|
stderr := err.Error()
|
||||||
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
|
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
|
||||||
return git.ErrBranchNotExist{
|
return git.ErrBranchNotExist{
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
@@ -147,15 +146,16 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3 - Clone the repository
|
// 3 - Clone the repository
|
||||||
cloneCmd := gitcmd.NewCommand("clone", "--bare")
|
cloneOpts := git.CloneRepoOptions{
|
||||||
if opts.SingleBranch != "" {
|
Bare: true,
|
||||||
cloneCmd.AddArguments("--single-branch", "--branch").AddDynamicArguments(opts.SingleBranch)
|
Timeout: 10 * time.Minute,
|
||||||
}
|
}
|
||||||
var stdout []byte
|
if opts.SingleBranch != "" {
|
||||||
if stdout, _, err = cloneCmd.AddDynamicArguments(opts.BaseRepo.RepoPath(), repo.RepoPath()).
|
cloneOpts.SingleBranch = true
|
||||||
WithTimeout(10 * time.Minute).
|
cloneOpts.Branch = opts.SingleBranch
|
||||||
RunStdBytes(ctx); err != nil {
|
}
|
||||||
log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err)
|
if err = gitrepo.Clone(ctx, opts.BaseRepo, repo, cloneOpts); err != nil {
|
||||||
|
log.Error("Fork Repository (git clone) Failed for %v (from %v):\nError: %v", repo, opts.BaseRepo, err)
|
||||||
return nil, fmt.Errorf("git clone: %w", err)
|
return nil, fmt.Errorf("git clone: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
@@ -28,7 +26,6 @@ import (
|
|||||||
repo_module "code.gitea.io/gitea/modules/repository"
|
repo_module "code.gitea.io/gitea/modules/repository"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
notify_service "code.gitea.io/gitea/services/notify"
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
pull_service "code.gitea.io/gitea/services/pull"
|
pull_service "code.gitea.io/gitea/services/pull"
|
||||||
)
|
)
|
||||||
@@ -251,9 +248,8 @@ func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create/Remove git-daemon-export-ok for git-daemon...
|
// Create/Remove git-daemon-export-ok for git-daemon...
|
||||||
daemonExportFile := filepath.Join(repo.RepoPath(), `git-daemon-export-ok`)
|
daemonExportFile := `git-daemon-export-ok`
|
||||||
|
isExist, err := gitrepo.IsRepoFileExist(ctx, repo, daemonExportFile)
|
||||||
isExist, err := util.IsExist(daemonExportFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
|
log.Error("Unable to check if %s exists. Error: %v", daemonExportFile, err)
|
||||||
return err
|
return err
|
||||||
@@ -261,11 +257,11 @@ func CheckDaemonExportOK(ctx context.Context, repo *repo_model.Repository) error
|
|||||||
|
|
||||||
isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic
|
isPublic := !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePublic
|
||||||
if !isPublic && isExist {
|
if !isPublic && isExist {
|
||||||
if err = util.Remove(daemonExportFile); err != nil {
|
if err = gitrepo.RemoveRepoFile(ctx, repo, daemonExportFile); err != nil {
|
||||||
log.Error("Failed to remove %s: %v", daemonExportFile, err)
|
log.Error("Failed to remove %s: %v", daemonExportFile, err)
|
||||||
}
|
}
|
||||||
} else if isPublic && !isExist {
|
} else if isPublic && !isExist {
|
||||||
if f, err := os.Create(daemonExportFile); err != nil {
|
if f, err := gitrepo.CreateRepoFile(ctx, repo, daemonExportFile); err != nil {
|
||||||
log.Error("Failed to create %s: %v", daemonExportFile, err)
|
log.Error("Failed to create %s: %v", daemonExportFile, err)
|
||||||
} else {
|
} else {
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{else if eq .Type 2}}
|
{{else if eq .Type 2}}
|
||||||
<div class="timeline-item event" id="{{.HashTag}}">
|
<div class="timeline-item event" id="{{.HashTag}}">
|
||||||
<span class="badge tw-bg-red tw-text-white">{{svg "octicon-circle-slash"}}</span>
|
<span class="badge tw-bg-red tw-text-white">{{svg "octicon-issue-closed"}}</span>
|
||||||
{{if not .OriginalAuthor}}
|
{{if not .OriginalAuthor}}
|
||||||
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
{{template "shared/user/avatarlink" dict "user" .Poster}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
{{if not .IsDisplayingSource}}data-raw-file-link="{{$.RawFileLink}}"{{end}}
|
{{if not .IsDisplayingSource}}data-raw-file-link="{{$.RawFileLink}}"{{end}}
|
||||||
data-tooltip-content="{{if .CanCopyContent}}{{ctx.Locale.Tr "copy_content"}}{{else}}{{ctx.Locale.Tr "copy_type_unsupported"}}{{end}}"
|
data-tooltip-content="{{if .CanCopyContent}}{{ctx.Locale.Tr "copy_content"}}{{else}}{{ctx.Locale.Tr "copy_type_unsupported"}}{{end}}"
|
||||||
>{{svg "octicon-copy"}}</a>
|
>{{svg "octicon-copy"}}</a>
|
||||||
{{if .EnableFeed}}
|
{{if and .EnableFeed .RefFullName.IsBranch}}
|
||||||
<a class="btn-octicon" href="{{$.RepoLink}}/rss/{{$.RefTypeNameSubURL}}/{{PathEscapeSegments .TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
|
<a class="btn-octicon" href="{{$.RepoLink}}/rss/{{$.RefTypeNameSubURL}}/{{PathEscapeSegments .TreePath}}" data-tooltip-content="{{ctx.Locale.Tr "rss_feed"}}">
|
||||||
{{svg "octicon-rss"}}
|
{{svg "octicon-rss"}}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
57
templates/swagger/v1_json.tmpl
generated
57
templates/swagger/v1_json.tmpl
generated
@@ -781,6 +781,60 @@
|
|||||||
"description": "page size of results",
|
"description": "page size of results",
|
||||||
"name": "limit",
|
"name": "limit",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "sort users by attribute. Supported values are \"name\", \"created\", \"updated\" and \"id\". Default is \"name\"",
|
||||||
|
"name": "sort",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "sort order, either \"asc\" (ascending) or \"desc\" (descending). Default is \"asc\", ignored if \"sort\" is not specified.",
|
||||||
|
"name": "order",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "search term (username, full name, email)",
|
||||||
|
"name": "q",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "visibility filter. Supported values are \"public\", \"limited\" and \"private\".",
|
||||||
|
"name": "visibility",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "filter active users",
|
||||||
|
"name": "is_active",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "filter admin users",
|
||||||
|
"name": "is_admin",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "filter restricted users",
|
||||||
|
"name": "is_restricted",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "filter 2FA enabled users",
|
||||||
|
"name": "is_2fa_enabled",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "filter login prohibited users",
|
||||||
|
"name": "is_prohibit_login",
|
||||||
|
"in": "query"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -789,6 +843,9 @@
|
|||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"$ref": "#/responses/forbidden"
|
"$ref": "#/responses/forbidden"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"$ref": "#/responses/validationError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,17 +7,23 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
"image/png"
|
||||||
|
"io/fs"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
route_web "code.gitea.io/gitea/routers/web"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/tests"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testGeneratePngBytes() []byte {
|
func testGeneratePngBytes() []byte {
|
||||||
@@ -52,14 +58,38 @@ func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL
|
|||||||
return obj["uuid"]
|
return obj["uuid"]
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateAnonymousAttachment(t *testing.T) {
|
func TestAttachments(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
t.Run("CreateAnonymousAttachment", testCreateAnonymousAttachment)
|
||||||
|
t.Run("CreateUser2IssueAttachment", testCreateUser2IssueAttachment)
|
||||||
|
t.Run("UploadAttachmentDeleteTemp", testUploadAttachmentDeleteTemp)
|
||||||
|
t.Run("GetAttachment", testGetAttachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUploadAttachmentDeleteTemp(t *testing.T) {
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
countTmpFile := func() int {
|
||||||
|
// TODO: GOLANG-HTTP-TMPDIR: Golang saves the uploaded file to os.TempDir() when it exceeds the max memory limit.
|
||||||
|
files, err := fs.Glob(os.DirFS(os.TempDir()), "multipart-*") //nolint:usetesting // Golang's "http" package's behavior
|
||||||
|
require.NoError(t, err)
|
||||||
|
return len(files)
|
||||||
|
}
|
||||||
|
var tmpFileCountDuringUpload int
|
||||||
|
defer test.MockVariableValue(&context.ParseMultipartFormMaxMemory, 1)()
|
||||||
|
defer web.RouteMock(route_web.RouterMockPointBeforeWebRoutes, func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
tmpFileCountDuringUpload = countTmpFile()
|
||||||
|
})()
|
||||||
|
_ = testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
|
||||||
|
assert.Equal(t, 1, tmpFileCountDuringUpload, "the temp file should exist when uploaded size exceeds the parse form's max memory")
|
||||||
|
assert.Equal(t, 0, countTmpFile(), "the temp file should be deleted after upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCreateAnonymousAttachment(t *testing.T) {
|
||||||
session := emptyTestSession(t)
|
session := emptyTestSession(t)
|
||||||
testCreateIssueAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
|
testCreateIssueAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateIssueAttachment(t *testing.T) {
|
func testCreateUser2IssueAttachment(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
const repoURL = "user2/repo1"
|
const repoURL = "user2/repo1"
|
||||||
session := loginUser(t, "user2")
|
session := loginUser(t, "user2")
|
||||||
uuid := testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
|
uuid := testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
|
||||||
@@ -90,8 +120,7 @@ func TestCreateIssueAttachment(t *testing.T) {
|
|||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetAttachment(t *testing.T) {
|
func testGetAttachment(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
adminSession := loginUser(t, "user1")
|
adminSession := loginUser(t, "user1")
|
||||||
user2Session := loginUser(t, "user2")
|
user2Session := loginUser(t, "user2")
|
||||||
user8Session := loginUser(t, "user8")
|
user8Session := loginUser(t, "user8")
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
|
|
||||||
--gap-inline: 0.25rem; /* gap for inline texts and elements, for example: the spaces for sentence with labels, button text, etc */
|
--gap-inline: 0.25rem; /* gap for inline texts and elements, for example: the spaces for sentence with labels, button text, etc */
|
||||||
--gap-block: 0.5rem; /* gap for element blocks, for example: spaces between buttons, menu image & title, header icon & title etc */
|
--gap-block: 0.5rem; /* gap for element blocks, for example: spaces between buttons, menu image & title, header icon & title etc */
|
||||||
|
|
||||||
|
--background-view-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAG0lEQVQYlWN4+vTpf3SMDTAMBYXYBLFpHgoKAeiOf0SGE9kbAAAAAElFTkSuQmCC") right bottom var(--color-primary-light-7);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) and (max-width: 1200px) {
|
@media (min-width: 768px) and (max-width: 1200px) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
.image-diff-container img {
|
.image-diff-container img {
|
||||||
border: 1px solid var(--color-primary-light-7);
|
border: 1px solid var(--color-primary-light-7);
|
||||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAG0lEQVQYlWN4+vTpf3SMDTAMBYXYBLFpHgoKAeiOf0SGE9kbAAAAAElFTkSuQmCC") right bottom var(--color-primary-light-7);
|
background: var(--background-view-image);
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-diff-container .before-container {
|
.image-diff-container .before-container {
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
.view-raw img[src$=".svg" i] {
|
.view-raw img[src$=".svg" i] {
|
||||||
max-height: 600px !important;
|
max-height: 600px !important;
|
||||||
max-width: 600px !important;
|
max-width: 600px !important;
|
||||||
|
background: var(--background-view-image);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-view-render-container {
|
.file-view-render-container {
|
||||||
|
|||||||
@@ -3,7 +3,33 @@ import {hideElem, loadElem, queryElemChildren, queryElems} from '../utils/dom.ts
|
|||||||
import {parseDom} from '../utils.ts';
|
import {parseDom} from '../utils.ts';
|
||||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
|
||||||
function getDefaultSvgBoundsIfUndefined(text: string, src: string) {
|
type ImageContext = {
|
||||||
|
imageBefore: HTMLImageElement | undefined,
|
||||||
|
imageAfter: HTMLImageElement | undefined,
|
||||||
|
sizeBefore: {width: number, height: number},
|
||||||
|
sizeAfter: {width: number, height: number},
|
||||||
|
maxSize: {width: number, height: number},
|
||||||
|
ratio: [number, number, number, number],
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImageInfo = {
|
||||||
|
path: string | null,
|
||||||
|
mime: string | null,
|
||||||
|
images: NodeListOf<HTMLImageElement>,
|
||||||
|
boundsInfo: HTMLElement | null,
|
||||||
|
};
|
||||||
|
|
||||||
|
type Bounds = {
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
type SvgBoundsInfo = {
|
||||||
|
before: Bounds,
|
||||||
|
after: Bounds,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getDefaultSvgBoundsIfUndefined(text: string, src: string): Bounds | null {
|
||||||
const defaultSize = 300;
|
const defaultSize = 300;
|
||||||
const maxSize = 99999;
|
const maxSize = 99999;
|
||||||
|
|
||||||
@@ -38,14 +64,14 @@ function getDefaultSvgBoundsIfUndefined(text: string, src: string) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageElement) {
|
function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageElement, svgBoundsInfo: SvgBoundsInfo): ImageContext {
|
||||||
const sizeAfter = {
|
const sizeAfter = {
|
||||||
width: imageAfter?.width || 0,
|
width: svgBoundsInfo.after?.width || imageAfter?.width || 0,
|
||||||
height: imageAfter?.height || 0,
|
height: svgBoundsInfo.after?.height || imageAfter?.height || 0,
|
||||||
};
|
};
|
||||||
const sizeBefore = {
|
const sizeBefore = {
|
||||||
width: imageBefore?.width || 0,
|
width: svgBoundsInfo.before?.width || imageBefore?.width || 0,
|
||||||
height: imageBefore?.height || 0,
|
height: svgBoundsInfo.before?.height || imageBefore?.height || 0,
|
||||||
};
|
};
|
||||||
const maxSize = {
|
const maxSize = {
|
||||||
width: Math.max(sizeBefore.width, sizeAfter.width),
|
width: Math.max(sizeBefore.width, sizeAfter.width),
|
||||||
@@ -80,7 +106,7 @@ class ImageDiff {
|
|||||||
// the container may be hidden by "viewed" checkbox, so use the parent's width for reference
|
// the container may be hidden by "viewed" checkbox, so use the parent's width for reference
|
||||||
this.diffContainerWidth = Math.max(containerEl.closest('.diff-file-box')!.clientWidth - 300, 100);
|
this.diffContainerWidth = Math.max(containerEl.closest('.diff-file-box')!.clientWidth - 300, 100);
|
||||||
|
|
||||||
const imageInfos = [{
|
const imagePair: [ImageInfo, ImageInfo] = [{
|
||||||
path: containerEl.getAttribute('data-path-after'),
|
path: containerEl.getAttribute('data-path-after'),
|
||||||
mime: containerEl.getAttribute('data-mime-after'),
|
mime: containerEl.getAttribute('data-mime-after'),
|
||||||
images: containerEl.querySelectorAll<HTMLImageElement>('img.image-after'), // matches 3 <img>
|
images: containerEl.querySelectorAll<HTMLImageElement>('img.image-after'), // matches 3 <img>
|
||||||
@@ -92,7 +118,8 @@ class ImageDiff {
|
|||||||
boundsInfo: containerEl.querySelector('.bounds-info-before'),
|
boundsInfo: containerEl.querySelector('.bounds-info-before'),
|
||||||
}];
|
}];
|
||||||
|
|
||||||
await Promise.all(imageInfos.map(async (info) => {
|
const svgBoundsInfo: SvgBoundsInfo = {before: null, after: null};
|
||||||
|
await Promise.all(imagePair.map(async (info, index) => {
|
||||||
const [success] = await Promise.all(Array.from(info.images, (img) => {
|
const [success] = await Promise.all(Array.from(info.images, (img) => {
|
||||||
return loadElem(img, info.path!);
|
return loadElem(img, info.path!);
|
||||||
}));
|
}));
|
||||||
@@ -102,115 +129,112 @@ class ImageDiff {
|
|||||||
const resp = await GET(info.path!);
|
const resp = await GET(info.path!);
|
||||||
const text = await resp.text();
|
const text = await resp.text();
|
||||||
const bounds = getDefaultSvgBoundsIfUndefined(text, info.path!);
|
const bounds = getDefaultSvgBoundsIfUndefined(text, info.path!);
|
||||||
|
svgBoundsInfo[index === 0 ? 'after' : 'before'] = bounds;
|
||||||
if (bounds) {
|
if (bounds) {
|
||||||
for (const el of info.images) {
|
|
||||||
el.setAttribute('width', String(bounds.width));
|
|
||||||
el.setAttribute('height', String(bounds.height));
|
|
||||||
}
|
|
||||||
hideElem(info.boundsInfo!);
|
hideElem(info.boundsInfo!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const imagesAfter = imageInfos[0].images;
|
const imagesAfter = imagePair[0].images;
|
||||||
const imagesBefore = imageInfos[1].images;
|
const imagesBefore = imagePair[1].images;
|
||||||
|
|
||||||
this.initSideBySide(createContext(imagesAfter[0], imagesBefore[0]));
|
this.initSideBySide(createContext(imagesAfter[0], imagesBefore[0], svgBoundsInfo));
|
||||||
if (imagesAfter.length > 0 && imagesBefore.length > 0) {
|
if (imagesAfter.length > 0 && imagesBefore.length > 0) {
|
||||||
this.initSwipe(createContext(imagesAfter[1], imagesBefore[1]));
|
this.initSwipe(createContext(imagesAfter[1], imagesBefore[1], svgBoundsInfo));
|
||||||
this.initOverlay(createContext(imagesAfter[2], imagesBefore[2]));
|
this.initOverlay(createContext(imagesAfter[2], imagesBefore[2], svgBoundsInfo));
|
||||||
}
|
}
|
||||||
queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading'));
|
queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading'));
|
||||||
}
|
}
|
||||||
|
|
||||||
initSideBySide(sizes: Record<string, any>) {
|
initSideBySide(ctx: ImageContext) {
|
||||||
let factor = 1;
|
let factor = 1;
|
||||||
if (sizes.maxSize.width > (this.diffContainerWidth - 24) / 2) {
|
if (ctx.maxSize.width > (this.diffContainerWidth - 24) / 2) {
|
||||||
factor = (this.diffContainerWidth - 24) / 2 / sizes.maxSize.width;
|
factor = (this.diffContainerWidth - 24) / 2 / ctx.maxSize.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
const widthChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalWidth !== sizes.imageBefore.naturalWidth;
|
const widthChanged = ctx.imageAfter && ctx.imageBefore && ctx.imageAfter.naturalWidth !== ctx.imageBefore.naturalWidth;
|
||||||
const heightChanged = sizes.imageAfter && sizes.imageBefore && sizes.imageAfter.naturalHeight !== sizes.imageBefore.naturalHeight;
|
const heightChanged = ctx.imageAfter && ctx.imageBefore && ctx.imageAfter.naturalHeight !== ctx.imageBefore.naturalHeight;
|
||||||
if (sizes.imageAfter) {
|
if (ctx.imageAfter) {
|
||||||
const boundsInfoAfterWidth = this.containerEl.querySelector('.bounds-info-after .bounds-info-width');
|
const boundsInfoAfterWidth = this.containerEl.querySelector('.bounds-info-after .bounds-info-width');
|
||||||
if (boundsInfoAfterWidth) {
|
if (boundsInfoAfterWidth) {
|
||||||
boundsInfoAfterWidth.textContent = `${sizes.imageAfter.naturalWidth}px`;
|
boundsInfoAfterWidth.textContent = `${ctx.imageAfter.naturalWidth}px`;
|
||||||
boundsInfoAfterWidth.classList.toggle('green', widthChanged);
|
boundsInfoAfterWidth.classList.toggle('green', widthChanged);
|
||||||
}
|
}
|
||||||
const boundsInfoAfterHeight = this.containerEl.querySelector('.bounds-info-after .bounds-info-height');
|
const boundsInfoAfterHeight = this.containerEl.querySelector('.bounds-info-after .bounds-info-height');
|
||||||
if (boundsInfoAfterHeight) {
|
if (boundsInfoAfterHeight) {
|
||||||
boundsInfoAfterHeight.textContent = `${sizes.imageAfter.naturalHeight}px`;
|
boundsInfoAfterHeight.textContent = `${ctx.imageAfter.naturalHeight}px`;
|
||||||
boundsInfoAfterHeight.classList.toggle('green', heightChanged);
|
boundsInfoAfterHeight.classList.toggle('green', heightChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sizes.imageBefore) {
|
if (ctx.imageBefore) {
|
||||||
const boundsInfoBeforeWidth = this.containerEl.querySelector('.bounds-info-before .bounds-info-width');
|
const boundsInfoBeforeWidth = this.containerEl.querySelector('.bounds-info-before .bounds-info-width');
|
||||||
if (boundsInfoBeforeWidth) {
|
if (boundsInfoBeforeWidth) {
|
||||||
boundsInfoBeforeWidth.textContent = `${sizes.imageBefore.naturalWidth}px`;
|
boundsInfoBeforeWidth.textContent = `${ctx.imageBefore.naturalWidth}px`;
|
||||||
boundsInfoBeforeWidth.classList.toggle('red', widthChanged);
|
boundsInfoBeforeWidth.classList.toggle('red', widthChanged);
|
||||||
}
|
}
|
||||||
const boundsInfoBeforeHeight = this.containerEl.querySelector('.bounds-info-before .bounds-info-height');
|
const boundsInfoBeforeHeight = this.containerEl.querySelector('.bounds-info-before .bounds-info-height');
|
||||||
if (boundsInfoBeforeHeight) {
|
if (boundsInfoBeforeHeight) {
|
||||||
boundsInfoBeforeHeight.textContent = `${sizes.imageBefore.naturalHeight}px`;
|
boundsInfoBeforeHeight.textContent = `${ctx.imageBefore.naturalHeight}px`;
|
||||||
boundsInfoBeforeHeight.classList.toggle('red', heightChanged);
|
boundsInfoBeforeHeight.classList.toggle('red', heightChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sizes.imageAfter) {
|
if (ctx.imageAfter) {
|
||||||
const container = sizes.imageAfter.parentNode;
|
const container = ctx.imageAfter.parentNode as HTMLElement;
|
||||||
sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
|
ctx.imageAfter.style.width = `${ctx.sizeAfter.width * factor}px`;
|
||||||
sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
|
ctx.imageAfter.style.height = `${ctx.sizeAfter.height * factor}px`;
|
||||||
container.style.margin = '10px auto';
|
container.style.margin = '10px auto';
|
||||||
container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
|
container.style.width = `${ctx.sizeAfter.width * factor + 2}px`;
|
||||||
container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
|
container.style.height = `${ctx.sizeAfter.height * factor + 2}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sizes.imageBefore) {
|
if (ctx.imageBefore) {
|
||||||
const container = sizes.imageBefore.parentNode;
|
const container = ctx.imageBefore.parentNode as HTMLElement;
|
||||||
sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
|
ctx.imageBefore.style.width = `${ctx.sizeBefore.width * factor}px`;
|
||||||
sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
|
ctx.imageBefore.style.height = `${ctx.sizeBefore.height * factor}px`;
|
||||||
container.style.margin = '10px auto';
|
container.style.margin = '10px auto';
|
||||||
container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
|
container.style.width = `${ctx.sizeBefore.width * factor + 2}px`;
|
||||||
container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
|
container.style.height = `${ctx.sizeBefore.height * factor + 2}px`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initSwipe(sizes: Record<string, any>) {
|
initSwipe(ctx: ImageContext) {
|
||||||
let factor = 1;
|
let factor = 1;
|
||||||
if (sizes.maxSize.width > this.diffContainerWidth - 12) {
|
if (ctx.maxSize.width > this.diffContainerWidth - 12) {
|
||||||
factor = (this.diffContainerWidth - 12) / sizes.maxSize.width;
|
factor = (this.diffContainerWidth - 12) / ctx.maxSize.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sizes.imageAfter) {
|
if (ctx.imageAfter) {
|
||||||
const imgParent = sizes.imageAfter.parentNode;
|
const imgParent = ctx.imageAfter.parentNode as HTMLElement;
|
||||||
const swipeFrame = imgParent.parentNode;
|
const swipeFrame = imgParent.parentNode as HTMLElement;
|
||||||
sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
|
ctx.imageAfter.style.width = `${ctx.sizeAfter.width * factor}px`;
|
||||||
sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
|
ctx.imageAfter.style.height = `${ctx.sizeAfter.height * factor}px`;
|
||||||
imgParent.style.margin = `0px ${sizes.ratio[0] * factor}px`;
|
imgParent.style.margin = `0px ${ctx.ratio[0] * factor}px`;
|
||||||
imgParent.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
|
imgParent.style.width = `${ctx.sizeAfter.width * factor + 2}px`;
|
||||||
imgParent.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
|
imgParent.style.height = `${ctx.sizeAfter.height * factor + 2}px`;
|
||||||
swipeFrame.style.padding = `${sizes.ratio[1] * factor}px 0 0 0`;
|
swipeFrame.style.padding = `${ctx.ratio[1] * factor}px 0 0 0`;
|
||||||
swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
|
swipeFrame.style.width = `${ctx.maxSize.width * factor + 2}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sizes.imageBefore) {
|
if (ctx.imageBefore) {
|
||||||
const imgParent = sizes.imageBefore.parentNode;
|
const imgParent = ctx.imageBefore.parentNode as HTMLElement;
|
||||||
const swipeFrame = imgParent.parentNode;
|
const swipeFrame = imgParent.parentNode as HTMLElement;
|
||||||
sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
|
ctx.imageBefore.style.width = `${ctx.sizeBefore.width * factor}px`;
|
||||||
sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
|
ctx.imageBefore.style.height = `${ctx.sizeBefore.height * factor}px`;
|
||||||
imgParent.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
|
imgParent.style.margin = `${ctx.ratio[3] * factor}px ${ctx.ratio[2] * factor}px`;
|
||||||
imgParent.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
|
imgParent.style.width = `${ctx.sizeBefore.width * factor + 2}px`;
|
||||||
imgParent.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
|
imgParent.style.height = `${ctx.sizeBefore.height * factor + 2}px`;
|
||||||
swipeFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
|
swipeFrame.style.width = `${ctx.maxSize.width * factor + 2}px`;
|
||||||
swipeFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
|
swipeFrame.style.height = `${ctx.maxSize.height * factor + 2}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// extra height for inner "position: absolute" elements
|
// extra height for inner "position: absolute" elements
|
||||||
const swipe = this.containerEl.querySelector<HTMLElement>('.diff-swipe');
|
const swipe = this.containerEl.querySelector<HTMLElement>('.diff-swipe');
|
||||||
if (swipe) {
|
if (swipe) {
|
||||||
swipe.style.width = `${sizes.maxSize.width * factor + 2}px`;
|
swipe.style.width = `${ctx.maxSize.width * factor + 2}px`;
|
||||||
swipe.style.height = `${sizes.maxSize.height * factor + 30}px`;
|
swipe.style.height = `${ctx.maxSize.height * factor + 30}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.containerEl.querySelector('.swipe-bar')!.addEventListener('mousedown', (e) => {
|
this.containerEl.querySelector('.swipe-bar')!.addEventListener('mousedown', (e) => {
|
||||||
@@ -237,40 +261,40 @@ class ImageDiff {
|
|||||||
document.addEventListener('mouseup', removeEventListeners);
|
document.addEventListener('mouseup', removeEventListeners);
|
||||||
}
|
}
|
||||||
|
|
||||||
initOverlay(sizes: Record<string, any>) {
|
initOverlay(ctx: ImageContext) {
|
||||||
let factor = 1;
|
let factor = 1;
|
||||||
if (sizes.maxSize.width > this.diffContainerWidth - 12) {
|
if (ctx.maxSize.width > this.diffContainerWidth - 12) {
|
||||||
factor = (this.diffContainerWidth - 12) / sizes.maxSize.width;
|
factor = (this.diffContainerWidth - 12) / ctx.maxSize.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sizes.imageAfter) {
|
if (ctx.imageAfter) {
|
||||||
const container = sizes.imageAfter.parentNode;
|
const container = ctx.imageAfter.parentNode as HTMLElement;
|
||||||
sizes.imageAfter.style.width = `${sizes.sizeAfter.width * factor}px`;
|
ctx.imageAfter.style.width = `${ctx.sizeAfter.width * factor}px`;
|
||||||
sizes.imageAfter.style.height = `${sizes.sizeAfter.height * factor}px`;
|
ctx.imageAfter.style.height = `${ctx.sizeAfter.height * factor}px`;
|
||||||
container.style.margin = `${sizes.ratio[1] * factor}px ${sizes.ratio[0] * factor}px`;
|
container.style.margin = `${ctx.ratio[1] * factor}px ${ctx.ratio[0] * factor}px`;
|
||||||
container.style.width = `${sizes.sizeAfter.width * factor + 2}px`;
|
container.style.width = `${ctx.sizeAfter.width * factor + 2}px`;
|
||||||
container.style.height = `${sizes.sizeAfter.height * factor + 2}px`;
|
container.style.height = `${ctx.sizeAfter.height * factor + 2}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sizes.imageBefore) {
|
if (ctx.imageBefore) {
|
||||||
const container = sizes.imageBefore.parentNode;
|
const container = ctx.imageBefore.parentNode as HTMLElement;
|
||||||
const overlayFrame = container.parentNode;
|
const overlayFrame = container.parentNode as HTMLElement;
|
||||||
sizes.imageBefore.style.width = `${sizes.sizeBefore.width * factor}px`;
|
ctx.imageBefore.style.width = `${ctx.sizeBefore.width * factor}px`;
|
||||||
sizes.imageBefore.style.height = `${sizes.sizeBefore.height * factor}px`;
|
ctx.imageBefore.style.height = `${ctx.sizeBefore.height * factor}px`;
|
||||||
container.style.margin = `${sizes.ratio[3] * factor}px ${sizes.ratio[2] * factor}px`;
|
container.style.margin = `${ctx.ratio[3] * factor}px ${ctx.ratio[2] * factor}px`;
|
||||||
container.style.width = `${sizes.sizeBefore.width * factor + 2}px`;
|
container.style.width = `${ctx.sizeBefore.width * factor + 2}px`;
|
||||||
container.style.height = `${sizes.sizeBefore.height * factor + 2}px`;
|
container.style.height = `${ctx.sizeBefore.height * factor + 2}px`;
|
||||||
|
|
||||||
// some inner elements are `position: absolute`, so the container's height must be large enough
|
// some inner elements are `position: absolute`, so the container's height must be large enough
|
||||||
overlayFrame.style.width = `${sizes.maxSize.width * factor + 2}px`;
|
overlayFrame.style.width = `${ctx.maxSize.width * factor + 2}px`;
|
||||||
overlayFrame.style.height = `${sizes.maxSize.height * factor + 2}px`;
|
overlayFrame.style.height = `${ctx.maxSize.height * factor + 2}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rangeInput = this.containerEl.querySelector<HTMLInputElement>('input[type="range"]')!;
|
const rangeInput = this.containerEl.querySelector<HTMLInputElement>('input[type="range"]')!;
|
||||||
|
|
||||||
function updateOpacity() {
|
function updateOpacity() {
|
||||||
if (sizes.imageAfter) {
|
if (ctx.imageAfter) {
|
||||||
sizes.imageAfter.parentNode.style.opacity = `${Number(rangeInput.value) / 100}`;
|
(ctx.imageAfter.parentNode as HTMLElement).style.opacity = `${Number(rangeInput.value) / 100}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user