mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-21 16:07:33 +02:00
Merge upstream/main to bring branch up to date with Vite migration and recent changes
Co-Authored-By: Epid <dev@epid.co>
This commit is contained in:
@@ -20,7 +20,6 @@
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {},
|
||||
// same extensions as Gitpod, should match /.gitpod.yml
|
||||
"extensions": [
|
||||
"editorconfig.editorconfig",
|
||||
"dbaeumer.vscode-eslint",
|
||||
|
||||
@@ -43,7 +43,6 @@ modifies/internal:
|
||||
- ".editorconfig"
|
||||
- ".eslintrc.cjs"
|
||||
- ".golangci.yml"
|
||||
- ".gitpod.yml"
|
||||
- ".markdownlint.yaml"
|
||||
- ".spectral.yaml"
|
||||
- "stylelint.config.*"
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: DeterminateSystems/determinate-nix-action@v3
|
||||
- uses: DeterminateSystems/update-flake-lock@main
|
||||
with:
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
json: ${{ steps.changes.outputs.json }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dorny/paths-filter@v3
|
||||
- uses: dorny/paths-filter@v4
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- run: uv python install 3.14
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -21,17 +21,17 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-qemu-action@v4
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- name: Build regular container image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
push: false
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
- name: Build rootless container image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
- name: import gpg key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
uses: crazy-max/ghaction-import-gpg@v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
@@ -71,14 +71,14 @@ jobs:
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-qemu-action@v4
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
|
||||
- uses: docker/metadata-action@v5
|
||||
- uses: docker/metadata-action@v6
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
type=raw,value=${{ steps.clean_name.outputs.branch }}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@v5
|
||||
- uses: docker/metadata-action@v6
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
@@ -102,18 +102,18 @@ jobs:
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular docker image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
cache-from: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful
|
||||
cache-to: type=registry,ref=ghcr.io/go-gitea/gitea:buildcache-rootful,mode=max
|
||||
- name: build rootless docker image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
- name: import gpg key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
uses: crazy-max/ghaction-import-gpg@v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
@@ -81,9 +81,9 @@ jobs:
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/metadata-action@v5
|
||||
- uses: docker/setup-qemu-action@v4
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- uses: docker/metadata-action@v6
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
type=semver,pattern={{version}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@v5
|
||||
- uses: docker/metadata-action@v6
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
@@ -112,18 +112,18 @@ jobs:
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular container image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
- name: build rootless container image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
- uses: pnpm/action-setup@v4
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
- name: import gpg key
|
||||
id: import_gpg
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
uses: crazy-max/ghaction-import-gpg@v7
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
|
||||
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
aws-region: ${{ secrets.AWS_REGION }}
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
@@ -84,9 +84,9 @@ jobs:
|
||||
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
|
||||
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
|
||||
- run: git fetch --unshallow --quiet --tags --force
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/metadata-action@v5
|
||||
- uses: docker/setup-qemu-action@v4
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
- uses: docker/metadata-action@v6
|
||||
id: meta
|
||||
with:
|
||||
images: |-
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- uses: docker/metadata-action@v5
|
||||
- uses: docker/metadata-action@v6
|
||||
id: meta_rootless
|
||||
with:
|
||||
images: |-
|
||||
@@ -124,18 +124,18 @@ jobs:
|
||||
annotations: |
|
||||
org.opencontainers.image.authors="maintainers@gitea.io"
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR using PAT
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: build regular container image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
- name: build rootless container image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/riscv64
|
||||
|
||||
+1
-2
@@ -79,6 +79,7 @@ cpu.out
|
||||
/yarn-error.log
|
||||
/npm-debug.log*
|
||||
/.pnpm-store
|
||||
/public/assets/.vite
|
||||
/public/assets/js
|
||||
/public/assets/css
|
||||
/public/assets/fonts
|
||||
@@ -87,8 +88,6 @@ cpu.out
|
||||
/VERSION
|
||||
/.air
|
||||
|
||||
# Files and folders that were previously generated
|
||||
/public/assets/img/webpack
|
||||
|
||||
# Snapcraft
|
||||
/gitea_a*.txt
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
tasks:
|
||||
- name: Setup
|
||||
init: |
|
||||
cp -r contrib/ide/vscode .vscode
|
||||
make deps
|
||||
make build
|
||||
command: |
|
||||
gp sync-done setup
|
||||
exit 0
|
||||
- name: Run backend
|
||||
command: |
|
||||
gp sync-await setup
|
||||
|
||||
# Get the URL and extract the domain
|
||||
url=$(gp url 3000)
|
||||
domain=$(echo $url | awk -F[/:] '{print $4}')
|
||||
|
||||
if [ -f custom/conf/app.ini ]; then
|
||||
sed -i "s|^ROOT_URL =.*|ROOT_URL = ${url}/|" custom/conf/app.ini
|
||||
sed -i "s|^DOMAIN =.*|DOMAIN = ${domain}|" custom/conf/app.ini
|
||||
sed -i "s|^SSH_DOMAIN =.*|SSH_DOMAIN = ${domain}|" custom/conf/app.ini
|
||||
sed -i "s|^NO_REPLY_ADDRESS =.*|SSH_DOMAIN = noreply.${domain}|" custom/conf/app.ini
|
||||
else
|
||||
mkdir -p custom/conf/
|
||||
echo -e "[server]\nROOT_URL = ${url}/" > custom/conf/app.ini
|
||||
echo -e "\n[database]\nDB_TYPE = sqlite3\nPATH = $GITPOD_REPO_ROOT/data/gitea.db" >> custom/conf/app.ini
|
||||
fi
|
||||
export TAGS="sqlite sqlite_unlock_notify"
|
||||
make watch-backend
|
||||
- name: Run frontend
|
||||
command: |
|
||||
gp sync-await setup
|
||||
make watch-frontend
|
||||
openMode: split-right
|
||||
|
||||
vscode:
|
||||
extensions:
|
||||
- editorconfig.editorconfig
|
||||
- dbaeumer.vscode-eslint
|
||||
- golang.go
|
||||
- stylelint.vscode-stylelint
|
||||
- DavidAnson.vscode-markdownlint
|
||||
- Vue.volar
|
||||
- ms-azuretools.vscode-docker
|
||||
- vitest.explorer
|
||||
- cweijan.vscode-database-client2
|
||||
- GitHub.vscode-pull-request-github
|
||||
|
||||
ports:
|
||||
- name: Gitea
|
||||
port: 3000
|
||||
@@ -1,10 +1,11 @@
|
||||
# Instructions for agents
|
||||
|
||||
- Use `make help` to find available development targets
|
||||
- Before committing `.go` changes, run `make fmt` to format, and run `make lint-go` to lint
|
||||
- Before committing `.ts` changes, run `make lint-js` to lint
|
||||
- Before committing `go.mod` changes, run `make tidy`
|
||||
- Before committing new `.go` files, add the current year into the copyright header
|
||||
- Before committing any files, remove all trailing whitespace from source code lines
|
||||
- Never force-push to pull request branches
|
||||
- Always start issue and pull request comments with an authorship attribution
|
||||
- Run `make fmt` to format `.go` files, and run `make lint-go` to lint them
|
||||
- Run `make lint-js` to lint `.ts` files
|
||||
- Run `make tidy` after any `go.mod` changes
|
||||
- Add the current year into the copyright header of new `.go` files
|
||||
- Ensure no trailing whitespace in edited files
|
||||
- Never force-push, amend, or squash unless asked. Use new commits and normal push for pull request updates
|
||||
- Preserve existing code comments, do not remove or rewrite comments that are still relevant
|
||||
- In TypeScript, use `!` (non-null assertion) instead of `?.`/`??` when a value is known to always exist
|
||||
- Include authorship attribution in issue and pull request comments
|
||||
- Add `Co-Authored-By` lines to all commits, indicating name and model used
|
||||
|
||||
+10
-8
@@ -70,16 +70,18 @@ For configuring IDEs for Gitea development, see the [contributed IDE configurati
|
||||
|
||||
## AI Contribution Policy
|
||||
|
||||
Contributions made with the assistance of AI tools are welcome, but contributors must use them responsibly.
|
||||
Contributions made with the assistance of AI tools are welcome, but contributors must use them responsibly and disclose that use clearly.
|
||||
|
||||
1. Include related issues or pull requests in the prompt so that the AI has ideal context.
|
||||
2. Review AI-generated code closely before submitting a pull request.
|
||||
3. Manually test the changes and add appropriate automated tests where feasible.
|
||||
4. Only use AI to assist in contributions that you understand well enough to respond to feedback without relying on AI.
|
||||
5. Indicate AI-generated content in issue and pull requests descriptions and comments. Specify which model was used.
|
||||
6. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model.
|
||||
1. Review AI-generated code closely before marking a pull request ready for review.
|
||||
2. Manually test the changes and add appropriate automated tests where feasible.
|
||||
3. Only use AI to assist in contributions that you understand well enough to explain, defend, and revise yourself during review.
|
||||
4. Disclose AI-assisted content clearly.
|
||||
5. Do not use AI to reply to questions about your issue or pull request. The questions are for you, not an AI model.
|
||||
6. AI may be used to help draft issues and pull requests, but contributors remain responsible for the accuracy, completeness, and intent of what they submit.
|
||||
|
||||
Maintainers reserve the right to close pull requests and issues that appear to be low-quality AI-generated content. We welcome new contributors, but cannot sustain the effort of supporting contributors who primarily defer to AI rather than engaging substantively with the review process.
|
||||
Maintainers reserve the right to close pull requests and issues that do not disclose AI assistance, that appear to be low-quality AI-generated content, or where the contributor cannot explain or defend the proposed changes themselves.
|
||||
|
||||
We welcome new contributors, but cannot sustain the effort of supporting contributors who primarily defer to AI rather than engaging substantively with the review process.
|
||||
|
||||
## Issues
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack
|
||||
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
|
||||
RUN apk --no-cache add build-base git nodejs pnpm
|
||||
WORKDIR /src
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack
|
||||
# Build frontend on the native platform to avoid QEMU-related issues with nodejs ecosystem
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
|
||||
RUN apk --no-cache add build-base git nodejs pnpm
|
||||
WORKDIR /src
|
||||
|
||||
@@ -120,10 +120,10 @@ LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/r
|
||||
GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration,$(shell $(GO) list ./... | grep -v /vendor/))
|
||||
MIGRATE_TEST_PACKAGES ?= $(shell $(GO) list code.gitea.io/gitea/models/migrations/...)
|
||||
|
||||
WEBPACK_SOURCES := $(shell find web_src/js web_src/css -type f)
|
||||
WEBPACK_CONFIGS := webpack.config.ts tailwind.config.ts
|
||||
WEBPACK_DEST := public/assets/js/index.js public/assets/css/index.css
|
||||
WEBPACK_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts
|
||||
FRONTEND_SOURCES := $(shell find web_src/js web_src/css -type f)
|
||||
FRONTEND_CONFIGS := vite.config.ts tailwind.config.ts
|
||||
FRONTEND_DEST := public/assets/.vite/manifest.json
|
||||
FRONTEND_DEST_ENTRIES := public/assets/js public/assets/css public/assets/fonts public/assets/.vite
|
||||
|
||||
BINDATA_DEST_WILDCARD := modules/migration/bindata.* modules/public/bindata.* modules/options/bindata.* modules/templates/bindata.*
|
||||
|
||||
@@ -199,7 +199,7 @@ git-check:
|
||||
|
||||
.PHONY: clean-all
|
||||
clean-all: clean ## delete backend, frontend and integration files
|
||||
rm -rf $(WEBPACK_DEST_ENTRIES) node_modules
|
||||
rm -rf $(FRONTEND_DEST_ENTRIES) node_modules
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## delete backend and integration files
|
||||
@@ -380,9 +380,8 @@ watch: ## watch everything and continuously rebuild
|
||||
@bash tools/watch.sh
|
||||
|
||||
.PHONY: watch-frontend
|
||||
watch-frontend: node_modules ## watch frontend files and continuously rebuild
|
||||
@rm -rf $(WEBPACK_DEST_ENTRIES)
|
||||
NODE_ENV=development $(NODE_VARS) pnpm exec webpack --watch --progress --disable-interpret
|
||||
watch-frontend: node_modules ## start vite dev server for frontend
|
||||
NODE_ENV=development $(NODE_VARS) pnpm exec vite
|
||||
|
||||
.PHONY: watch-backend
|
||||
watch-backend: ## watch backend files and continuously rebuild
|
||||
@@ -645,7 +644,7 @@ install: $(wildcard *.go)
|
||||
build: frontend backend ## build everything
|
||||
|
||||
.PHONY: frontend
|
||||
frontend: $(WEBPACK_DEST) ## build frontend files
|
||||
frontend: $(FRONTEND_DEST) ## build frontend files
|
||||
|
||||
.PHONY: backend
|
||||
backend: generate-backend $(EXECUTABLE) ## build backend files
|
||||
@@ -672,7 +671,7 @@ ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
||||
endif
|
||||
CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
|
||||
|
||||
$(EXECUTABLE_E2E): $(GO_SOURCES)
|
||||
$(EXECUTABLE_E2E): $(GO_SOURCES) $(FRONTEND_DEST)
|
||||
CGO_ENABLED=1 $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TEST_TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@
|
||||
|
||||
.PHONY: release
|
||||
@@ -776,15 +775,15 @@ update-py: node_modules ## update py dependencies
|
||||
uv sync
|
||||
@touch .venv
|
||||
|
||||
.PHONY: webpack
|
||||
webpack: $(WEBPACK_DEST) ## build webpack files
|
||||
.PHONY: vite
|
||||
vite: $(FRONTEND_DEST) ## build vite files
|
||||
|
||||
$(WEBPACK_DEST): $(WEBPACK_SOURCES) $(WEBPACK_CONFIGS) pnpm-lock.yaml
|
||||
$(FRONTEND_DEST): $(FRONTEND_SOURCES) $(FRONTEND_CONFIGS) pnpm-lock.yaml
|
||||
@$(MAKE) -s node_modules
|
||||
@rm -rf $(WEBPACK_DEST_ENTRIES)
|
||||
@echo "Running webpack..."
|
||||
@BROWSERSLIST_IGNORE_OLD_DATA=true $(NODE_VARS) pnpm exec webpack --disable-interpret
|
||||
@touch $(WEBPACK_DEST)
|
||||
@rm -rf $(FRONTEND_DEST_ENTRIES)
|
||||
@echo "Running vite build..."
|
||||
@$(NODE_VARS) pnpm exec vite build
|
||||
@touch $(FRONTEND_DEST)
|
||||
|
||||
.PHONY: svg
|
||||
svg: node_modules ## build svg files
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||
[](https://opensource.org/licenses/MIT "License: MIT")
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
[繁體中文](./README.zh-tw.md) | [简体中文](./README.zh-cn.md)
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||
[](https://opensource.org/licenses/MIT "License: MIT")
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
[English](./README.md) | [繁體中文](./README.zh-tw.md)
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||
[](https://opensource.org/licenses/MIT "License: MIT")
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
[English](./README.md) | [简体中文](./README.zh-cn.md)
|
||||
|
||||
Generated
+1
-1
File diff suppressed because one or more lines are too long
+7
-6
@@ -13,17 +13,18 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// CmdActions represents the available actions sub-commands.
|
||||
CmdActions = &cli.Command{
|
||||
func newActionsCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "actions",
|
||||
Usage: "Manage Gitea Actions",
|
||||
Commands: []*cli.Command{
|
||||
subcmdActionsGenRunnerToken,
|
||||
newActionsGenerateRunnerTokenCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdActionsGenRunnerToken = &cli.Command{
|
||||
func newActionsGenerateRunnerTokenCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "generate-runner-token",
|
||||
Usage: "Generate a new token for a runner to use to register with the server",
|
||||
Action: runGenerateActionsRunnerToken,
|
||||
@@ -37,7 +38,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func runGenerateActionsRunnerToken(ctx context.Context, c *cli.Command) error {
|
||||
setting.MustInstalled()
|
||||
|
||||
+24
-17
@@ -18,36 +18,41 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// CmdAdmin represents the available admin sub-command.
|
||||
CmdAdmin = &cli.Command{
|
||||
func newAdminCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "admin",
|
||||
Usage: "Perform common administrative operations",
|
||||
Commands: []*cli.Command{
|
||||
subcmdUser,
|
||||
subcmdRepoSyncReleases,
|
||||
subcmdRegenerate,
|
||||
subcmdAuth,
|
||||
subcmdSendMail,
|
||||
newUserCommand(),
|
||||
newRepoSyncReleasesCommand(),
|
||||
newRegenerateCommand(),
|
||||
newAuthCommand(),
|
||||
newSendMailCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdRepoSyncReleases = &cli.Command{
|
||||
func newRepoSyncReleasesCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "repo-sync-releases",
|
||||
Usage: "Synchronize repository releases with tags",
|
||||
Action: runRepoSyncReleases,
|
||||
}
|
||||
}
|
||||
|
||||
subcmdRegenerate = &cli.Command{
|
||||
func newRegenerateCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "regenerate",
|
||||
Usage: "Regenerate specific files",
|
||||
Commands: []*cli.Command{
|
||||
microcmdRegenHooks,
|
||||
microcmdRegenKeys,
|
||||
newRegenerateHooksCommand(),
|
||||
newRegenerateKeysCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdAuth = &cli.Command{
|
||||
func newAuthCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "auth",
|
||||
Usage: "Modify external auth providers",
|
||||
Commands: []*cli.Command{
|
||||
@@ -59,12 +64,14 @@ var (
|
||||
microcmdAuthUpdateLdapSimpleAuth(),
|
||||
microcmdAuthAddSMTP(),
|
||||
microcmdAuthUpdateSMTP(),
|
||||
microcmdAuthList,
|
||||
microcmdAuthDelete,
|
||||
newAuthListCommand(),
|
||||
newAuthDeleteCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdSendMail = &cli.Command{
|
||||
func newSendMailCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "sendmail",
|
||||
Usage: "Send a message to all users",
|
||||
Action: runSendMail,
|
||||
@@ -86,7 +93,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func idFlag() *cli.Int64Flag {
|
||||
return &cli.Int64Flag{
|
||||
|
||||
+7
-4
@@ -17,14 +17,17 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
microcmdAuthDelete = &cli.Command{
|
||||
func newAuthDeleteCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "delete",
|
||||
Usage: "Delete specific auth source",
|
||||
Flags: []cli.Flag{idFlag()},
|
||||
Action: runDeleteAuth,
|
||||
}
|
||||
microcmdAuthList = &cli.Command{
|
||||
}
|
||||
|
||||
func newAuthListCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List auth sources",
|
||||
Action: runListAuth,
|
||||
@@ -55,7 +58,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func runListAuth(ctx context.Context, c *cli.Command) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
||||
@@ -13,19 +13,21 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
microcmdRegenHooks = &cli.Command{
|
||||
func newRegenerateHooksCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "hooks",
|
||||
Usage: "Regenerate git-hooks",
|
||||
Action: runRegenerateHooks,
|
||||
}
|
||||
}
|
||||
|
||||
microcmdRegenKeys = &cli.Command{
|
||||
func newRegenerateKeysCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "keys",
|
||||
Usage: "Regenerate authorized_keys file",
|
||||
Action: runRegenerateKeys,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func runRegenerateHooks(ctx context.Context, _ *cli.Command) error {
|
||||
if err := initDB(ctx); err != nil {
|
||||
|
||||
+13
-11
@@ -7,15 +7,17 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var subcmdUser = &cli.Command{
|
||||
Name: "user",
|
||||
Usage: "Modify users",
|
||||
Commands: []*cli.Command{
|
||||
microcmdUserCreate(),
|
||||
microcmdUserList,
|
||||
microcmdUserChangePassword(),
|
||||
microcmdUserDelete(),
|
||||
microcmdUserGenerateAccessToken,
|
||||
microcmdUserMustChangePassword(),
|
||||
},
|
||||
func newUserCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "user",
|
||||
Usage: "Modify users",
|
||||
Commands: []*cli.Command{
|
||||
microcmdUserCreate(),
|
||||
newUserListCommand(),
|
||||
microcmdUserChangePassword(),
|
||||
microcmdUserDelete(),
|
||||
newUserGenerateAccessTokenCommand(),
|
||||
microcmdUserMustChangePassword(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,32 +14,34 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var microcmdUserGenerateAccessToken = &cli.Command{
|
||||
Name: "generate-access-token",
|
||||
Usage: "Generate an access token for a specific user",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username",
|
||||
func newUserGenerateAccessTokenCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "generate-access-token",
|
||||
Usage: "Generate an access token for a specific user",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Usage: "Username",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token-name",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Token name",
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "raw",
|
||||
Usage: "Display only the token value",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scopes",
|
||||
Value: "all",
|
||||
Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token-name",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "Token name",
|
||||
Value: "gitea-admin",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "raw",
|
||||
Usage: "Display only the token value",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "scopes",
|
||||
Value: "all",
|
||||
Usage: `Comma separated list of scopes to apply to access token, examples: "all", "public-only,read:issue", "write:repository,write:user"`,
|
||||
},
|
||||
},
|
||||
Action: runGenerateAccessToken,
|
||||
Action: runGenerateAccessToken,
|
||||
}
|
||||
}
|
||||
|
||||
func runGenerateAccessToken(ctx context.Context, c *cli.Command) error {
|
||||
|
||||
+11
-9
@@ -14,16 +14,18 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var microcmdUserList = &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List users",
|
||||
Action: runListUsers,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "List only admin users",
|
||||
func newUserListCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List users",
|
||||
Action: runListUsers,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "admin",
|
||||
Usage: "List only admin users",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runListUsers(ctx context.Context, c *cli.Command) error {
|
||||
|
||||
+17
-16
@@ -13,23 +13,24 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdDocs represents the available docs sub-command.
|
||||
var CmdDocs = &cli.Command{
|
||||
Name: "docs",
|
||||
Usage: "Output CLI documentation",
|
||||
Description: "A command to output Gitea's CLI documentation, optionally to a file.",
|
||||
Action: runDocs,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "man",
|
||||
Usage: "Output man pages instead",
|
||||
func newDocsCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "docs",
|
||||
Usage: "Output CLI documentation",
|
||||
Description: "A command to output Gitea's CLI documentation, optionally to a file.",
|
||||
Action: runDocs,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "man",
|
||||
Usage: "Output man pages instead",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "Path to output to instead of stdout (will overwrite if exists)",
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "Path to output to instead of stdout (will overwrite if exists)",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runDocs(_ context.Context, cmd *cli.Command) error {
|
||||
|
||||
+61
-57
@@ -24,73 +24,77 @@ import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CmdDoctor represents the available doctor sub-command.
|
||||
var CmdDoctor = &cli.Command{
|
||||
Name: "doctor",
|
||||
Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
|
||||
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
|
||||
Commands: []*cli.Command{
|
||||
cmdDoctorCheck,
|
||||
cmdRecreateTable,
|
||||
cmdDoctorConvert,
|
||||
},
|
||||
func newDoctorCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "doctor",
|
||||
Usage: "Diagnose and optionally fix problems, convert or re-create database tables",
|
||||
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
Commands: []*cli.Command{
|
||||
newDoctorCheckCommand(),
|
||||
newRecreateTableCommand(),
|
||||
newDoctorConvertCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var cmdDoctorCheck = &cli.Command{
|
||||
Name: "check",
|
||||
Usage: "Diagnose and optionally fix problems",
|
||||
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
Action: runDoctorCheck,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "list",
|
||||
Usage: "List the available checks",
|
||||
func newDoctorCheckCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "check",
|
||||
Usage: "Diagnose and optionally fix problems",
|
||||
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
Action: runDoctorCheck,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "list",
|
||||
Usage: "List the available checks",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "default",
|
||||
Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "run",
|
||||
Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "all",
|
||||
Usage: "Run all the available checks",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "fix",
|
||||
Usage: "Automatically fix what we can",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-file",
|
||||
Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "color",
|
||||
Aliases: []string{"H"},
|
||||
Usage: "Use color for outputted information",
|
||||
},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "default",
|
||||
Usage: "Run the default checks (if neither --run or --all is set, this is the default behaviour)",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "run",
|
||||
Usage: "Run the provided checks - (if --default is set, the default checks will also run)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "all",
|
||||
Usage: "Run all the available checks",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "fix",
|
||||
Usage: "Automatically fix what we can",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-file",
|
||||
Usage: `Name of the log file (no verbose log output by default). Set to "-" to output to stdout`,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "color",
|
||||
Aliases: []string{"H"},
|
||||
Usage: "Use color for outputted information",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var cmdRecreateTable = &cli.Command{
|
||||
Name: "recreate-table",
|
||||
Usage: "Recreate tables from XORM definitions and copy the data.",
|
||||
ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Print SQL commands sent",
|
||||
func newRecreateTableCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "recreate-table",
|
||||
Usage: "Recreate tables from XORM definitions and copy the data.",
|
||||
ArgsUsage: "[TABLE]... : (TABLEs to recreate - leave blank for all)",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Print SQL commands sent",
|
||||
},
|
||||
},
|
||||
},
|
||||
Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns.
|
||||
Description: `The database definitions Gitea uses change across versions, sometimes changing default values and leaving old unused columns.
|
||||
|
||||
This command will cause Xorm to recreate tables, copying over the data and deleting the old table.
|
||||
|
||||
You should back-up your database before doing this and ensure that your database is up-to-date first.`,
|
||||
Action: runRecreateTable,
|
||||
Action: runRecreateTable,
|
||||
}
|
||||
}
|
||||
|
||||
func runRecreateTable(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
@@ -14,12 +14,13 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// cmdDoctorConvert represents the available convert sub-command.
|
||||
var cmdDoctorConvert = &cli.Command{
|
||||
Name: "convert",
|
||||
Usage: "Convert the database",
|
||||
Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar",
|
||||
Action: runDoctorConvert,
|
||||
func newDoctorConvertCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "convert",
|
||||
Usage: "Convert the database",
|
||||
Description: "A command to convert an existing MySQL database from utf8 to utf8mb4 or MSSQL database from varchar to nvarchar",
|
||||
Action: runDoctorConvert,
|
||||
}
|
||||
}
|
||||
|
||||
func runDoctorConvert(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ func TestDoctorRun(t *testing.T) {
|
||||
SkipDatabaseInitialization: true,
|
||||
})
|
||||
app := &cli.Command{
|
||||
Commands: []*cli.Command{cmdDoctorCheck},
|
||||
Commands: []*cli.Command{newDoctorCheckCommand()},
|
||||
}
|
||||
err := app.Run(t.Context(), []string{"./gitea", "check", "--run", "test-check"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
+72
-71
@@ -23,78 +23,79 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdDump represents the available dump sub-command.
|
||||
var CmdDump = &cli.Command{
|
||||
Name: "dump",
|
||||
Usage: "Dump Gitea files and database",
|
||||
Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`,
|
||||
Action: runDump,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
|
||||
func newDumpCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "dump",
|
||||
Usage: "Dump Gitea files and database",
|
||||
Description: `Dump compresses all related files and database into zip file. It can be used for backup and capture Gitea server image to send to maintainer`,
|
||||
Action: runDump,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "file",
|
||||
Aliases: []string{"f"},
|
||||
Usage: `Name of the dump file which will be created, default to "gitea-dump-{time}.zip". Supply '-' for stdout. See type for available types.`,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "Show process details",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Aliases: []string{"q"},
|
||||
Usage: "Only display warnings and errors",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "tempdir",
|
||||
Aliases: []string{"t"},
|
||||
Value: os.TempDir(),
|
||||
Usage: "Temporary dir path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "database",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-repository",
|
||||
Aliases: []string{"R"},
|
||||
Usage: "Skip the repository dumping",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-log",
|
||||
Aliases: []string{"L"},
|
||||
Usage: "Skip the log dumping",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-custom-dir",
|
||||
Usage: "Skip custom directory",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-lfs-data",
|
||||
Usage: "Skip LFS data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-attachment-data",
|
||||
Usage: "Skip attachment data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-package-data",
|
||||
Usage: "Skip package data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-index",
|
||||
Usage: "Skip bleve index data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-db",
|
||||
Usage: "Skip database",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
|
||||
},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "Show process details",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Aliases: []string{"q"},
|
||||
Usage: "Only display warnings and errors",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "tempdir",
|
||||
Aliases: []string{"t"},
|
||||
Value: os.TempDir(),
|
||||
Usage: "Temporary dir path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "database",
|
||||
Aliases: []string{"d"},
|
||||
Usage: "Specify the database SQL syntax: sqlite3, mysql, mssql, postgres",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-repository",
|
||||
Aliases: []string{"R"},
|
||||
Usage: "Skip the repository dumping",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-log",
|
||||
Aliases: []string{"L"},
|
||||
Usage: "Skip the log dumping",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-custom-dir",
|
||||
Usage: "Skip custom directory",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-lfs-data",
|
||||
Usage: "Skip LFS data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-attachment-data",
|
||||
Usage: "Skip attachment data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-package-data",
|
||||
Usage: "Skip package data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-index",
|
||||
Usage: "Skip bleve index data",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "skip-db",
|
||||
Usage: "Skip database",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Usage: `Dump output format, default to "zip", supported types: ` + strings.Join(dump.SupportedOutputTypes, ", "),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
|
||||
+54
-53
@@ -22,61 +22,62 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdDumpRepository represents the available dump repository sub-command.
|
||||
var CmdDumpRepository = &cli.Command{
|
||||
Name: "dump-repo",
|
||||
Usage: "Dump the repository from git/github/gitea/gitlab",
|
||||
Description: "This is a command for dumping the repository data.",
|
||||
Action: runDumpRepository,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "git_service",
|
||||
Value: "",
|
||||
Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_dir",
|
||||
Aliases: []string{"r"},
|
||||
Value: "./data",
|
||||
Usage: "Repository dir path to store the data",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "clone_addr",
|
||||
Value: "",
|
||||
Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_username",
|
||||
Value: "",
|
||||
Usage: "The username to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_password",
|
||||
Value: "",
|
||||
Usage: "The password to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Usage: "The personal token to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "owner_name",
|
||||
Value: "",
|
||||
Usage: "The data will be stored on a directory with owner name if not empty",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_name",
|
||||
Value: "",
|
||||
Usage: "The data will be stored on a directory with repository name if not empty",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "units",
|
||||
Value: "",
|
||||
Usage: `Which items will be migrated, one or more units should be separated as comma.
|
||||
func newDumpRepositoryCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "dump-repo",
|
||||
Usage: "Dump the repository from git/github/gitea/gitlab",
|
||||
Description: "This is a command for dumping the repository data.",
|
||||
Action: runDumpRepository,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "git_service",
|
||||
Value: "",
|
||||
Usage: "Git service, git, github, gitea, gitlab. If clone_addr could be recognized, this could be ignored.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_dir",
|
||||
Aliases: []string{"r"},
|
||||
Value: "./data",
|
||||
Usage: "Repository dir path to store the data",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "clone_addr",
|
||||
Value: "",
|
||||
Usage: "The URL will be clone, currently could be a git/github/gitea/gitlab http/https URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_username",
|
||||
Value: "",
|
||||
Usage: "The username to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_password",
|
||||
Value: "",
|
||||
Usage: "The password to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Usage: "The personal token to visit the clone_addr",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "owner_name",
|
||||
Value: "",
|
||||
Usage: "The data will be stored on a directory with owner name if not empty",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_name",
|
||||
Value: "",
|
||||
Usage: "The data will be stored on a directory with repository name if not empty",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "units",
|
||||
Value: "",
|
||||
Usage: `Which items will be migrated, one or more units should be separated as comma.
|
||||
wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runDumpRepository(ctx context.Context, cmd *cli.Command) error {
|
||||
|
||||
+17
-12
@@ -23,20 +23,23 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdEmbedded represents the available extract sub-command.
|
||||
var (
|
||||
CmdEmbedded = &cli.Command{
|
||||
var matchedAssetFiles []assetFile
|
||||
|
||||
func newEmbeddedCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "embedded",
|
||||
Usage: "Extract embedded resources",
|
||||
Description: "A command for extracting embedded resources, like templates and images",
|
||||
Commands: []*cli.Command{
|
||||
subcmdList,
|
||||
subcmdView,
|
||||
subcmdExtract,
|
||||
newEmbeddedListCommand(),
|
||||
newEmbeddedViewCommand(),
|
||||
newEmbeddedExtractCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdList = &cli.Command{
|
||||
func newEmbeddedListCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List files matching the given pattern",
|
||||
Action: runList,
|
||||
@@ -48,8 +51,10 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdView = &cli.Command{
|
||||
func newEmbeddedViewCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "view",
|
||||
Usage: "View a file matching the given pattern",
|
||||
Action: runView,
|
||||
@@ -61,8 +66,10 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdExtract = &cli.Command{
|
||||
func newEmbeddedExtractCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "extract",
|
||||
Usage: "Extract resources",
|
||||
Action: runExtract,
|
||||
@@ -91,9 +98,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
matchedAssetFiles []assetFile
|
||||
)
|
||||
}
|
||||
|
||||
type assetFile struct {
|
||||
fs *assetfs.LayeredFS
|
||||
|
||||
+19
-12
@@ -15,45 +15,52 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// CmdGenerate represents the available generate sub-command.
|
||||
CmdGenerate = &cli.Command{
|
||||
func newGenerateCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "generate",
|
||||
Usage: "Generate Gitea's secrets/keys/tokens",
|
||||
Commands: []*cli.Command{
|
||||
subcmdSecret,
|
||||
newGenerateSecretCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdSecret = &cli.Command{
|
||||
func newGenerateSecretCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "secret",
|
||||
Usage: "Generate a secret token",
|
||||
Commands: []*cli.Command{
|
||||
microcmdGenerateInternalToken,
|
||||
microcmdGenerateLfsJwtSecret,
|
||||
microcmdGenerateSecretKey,
|
||||
newGenerateInternalTokenCommand(),
|
||||
newGenerateLfsJWTSecretCommand(),
|
||||
newGenerateSecretKeyCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
microcmdGenerateInternalToken = &cli.Command{
|
||||
func newGenerateInternalTokenCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "INTERNAL_TOKEN",
|
||||
Usage: "Generate a new INTERNAL_TOKEN",
|
||||
Action: runGenerateInternalToken,
|
||||
}
|
||||
}
|
||||
|
||||
microcmdGenerateLfsJwtSecret = &cli.Command{
|
||||
func newGenerateLfsJWTSecretCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "JWT_SECRET",
|
||||
Aliases: []string{"LFS_JWT_SECRET"},
|
||||
Usage: "Generate a new JWT_SECRET",
|
||||
Action: runGenerateLfsJwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
microcmdGenerateSecretKey = &cli.Command{
|
||||
func newGenerateSecretKeyCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "SECRET_KEY",
|
||||
Usage: "Generate a new SECRET_KEY",
|
||||
Action: runGenerateSecretKey,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func runGenerateInternalToken(_ context.Context, c *cli.Command) error {
|
||||
internalToken, err := generate.NewInternalToken()
|
||||
|
||||
+31
-13
@@ -28,23 +28,24 @@ const (
|
||||
hookBatchSize = 500
|
||||
)
|
||||
|
||||
var (
|
||||
// CmdHook represents the available hooks sub-command.
|
||||
CmdHook = &cli.Command{
|
||||
func newHookCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "hook",
|
||||
Usage: "(internal) Should only be called by Git",
|
||||
Hidden: true, // internal commands shouldn't be visible
|
||||
Description: "Delegate commands to corresponding Git hooks",
|
||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||
Commands: []*cli.Command{
|
||||
subcmdHookPreReceive,
|
||||
subcmdHookUpdate,
|
||||
subcmdHookPostReceive,
|
||||
subcmdHookProcReceive,
|
||||
newHookPreReceiveCommand(),
|
||||
newHookUpdateCommand(),
|
||||
newHookPostReceiveCommand(),
|
||||
newHookProcReceiveCommand(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdHookPreReceive = &cli.Command{
|
||||
func newHookPreReceiveCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "pre-receive",
|
||||
Usage: "Delegate pre-receive Git hook",
|
||||
Description: "This command should only be called by Git",
|
||||
@@ -55,7 +56,10 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
subcmdHookUpdate = &cli.Command{
|
||||
}
|
||||
|
||||
func newHookUpdateCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "update",
|
||||
Usage: "Delegate update Git hook",
|
||||
Description: "This command should only be called by Git",
|
||||
@@ -66,7 +70,10 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
subcmdHookPostReceive = &cli.Command{
|
||||
}
|
||||
|
||||
func newHookPostReceiveCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "post-receive",
|
||||
Usage: "Delegate post-receive Git hook",
|
||||
Description: "This command should only be called by Git",
|
||||
@@ -77,8 +84,11 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
// Note: new hook since git 2.29
|
||||
subcmdHookProcReceive = &cli.Command{
|
||||
}
|
||||
|
||||
// Note: new hook since git 2.29
|
||||
func newHookProcReceiveCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "proc-receive",
|
||||
Usage: "Delegate proc-receive Git hook",
|
||||
Description: "This command should only be called by Git",
|
||||
@@ -89,7 +99,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
type delayWriter struct {
|
||||
internal io.Writer
|
||||
@@ -276,6 +286,9 @@ Gitea or set your environment appropriately.`, "")
|
||||
lastline = 0
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err)
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
hookOptions.OldCommitIDs = oldCommitIDs[:count]
|
||||
@@ -415,6 +428,11 @@ Gitea or set your environment appropriately.`, "")
|
||||
count = 0
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
_ = dWriter.Close()
|
||||
hookPrintResults(results)
|
||||
return fail(ctx, "Hook failed: stdin read error", "scanner error: %v", err)
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
if wasEmpty && masterPushed {
|
||||
|
||||
+35
-33
@@ -15,40 +15,42 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdKeys represents the available keys sub-command
|
||||
var CmdKeys = &cli.Command{
|
||||
Name: "keys",
|
||||
Usage: "(internal) Should only be called by SSH server",
|
||||
Hidden: true, // internal commands shouldn't be visible
|
||||
Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
|
||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||
Action: runKeys,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "expected",
|
||||
Aliases: []string{"e"},
|
||||
Value: "git",
|
||||
Usage: "Expected user for whom provide key commands",
|
||||
// NewKeysCommand returns the internal SSH key lookup sub-command.
|
||||
func NewKeysCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "keys",
|
||||
Usage: "(internal) Should only be called by SSH server",
|
||||
Hidden: true, // internal commands shouldn't be visible
|
||||
Description: "Queries the Gitea database to get the authorized command for a given ssh key fingerprint",
|
||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||
Action: runKeys,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "expected",
|
||||
Aliases: []string{"e"},
|
||||
Value: "git",
|
||||
Usage: "Expected user for whom provide key commands",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Value: "",
|
||||
Usage: "Username trying to log in by SSH",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Value: "",
|
||||
Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "content",
|
||||
Aliases: []string{"k"},
|
||||
Value: "",
|
||||
Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)",
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "username",
|
||||
Aliases: []string{"u"},
|
||||
Value: "",
|
||||
Usage: "Username trying to log in by SSH",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Value: "",
|
||||
Usage: "Type of the SSH key provided to the SSH Server (requires content to be provided too)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "content",
|
||||
Aliases: []string{"k"},
|
||||
Value: "",
|
||||
Usage: "Base64 encoded content of the SSH key provided to the SSH Server (requires type to be provided too)",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runKeys(ctx context.Context, c *cli.Command) error {
|
||||
|
||||
+18
-17
@@ -112,35 +112,36 @@ func NewMainApp(appVer AppVersion) *cli.Command {
|
||||
Usage: "Set custom path (defaults to '{WorkPath}/custom')",
|
||||
},
|
||||
}
|
||||
webCmd := newWebCommand()
|
||||
// these sub-commands need to use a config file
|
||||
subCmdWithConfig := []*cli.Command{
|
||||
CmdWeb,
|
||||
CmdServ,
|
||||
CmdHook,
|
||||
CmdKeys,
|
||||
CmdDump,
|
||||
CmdAdmin,
|
||||
CmdMigrate,
|
||||
CmdDoctor,
|
||||
CmdManager,
|
||||
CmdEmbedded,
|
||||
CmdMigrateStorage,
|
||||
CmdDumpRepository,
|
||||
CmdRestoreRepository,
|
||||
CmdActions,
|
||||
webCmd,
|
||||
newServCommand(),
|
||||
newHookCommand(),
|
||||
NewKeysCommand(),
|
||||
newDumpCommand(),
|
||||
newAdminCommand(),
|
||||
newMigrateCommand(),
|
||||
newDoctorCommand(),
|
||||
newManagerCommand(),
|
||||
newEmbeddedCommand(),
|
||||
newMigrateStorageCommand(),
|
||||
newDumpRepositoryCommand(),
|
||||
newRestoreRepositoryCommand(),
|
||||
newActionsCommand(),
|
||||
}
|
||||
|
||||
// these sub-commands do not need the config file, and they do not depend on any path or environment variable.
|
||||
subCmdStandalone := []*cli.Command{
|
||||
cmdConfig(),
|
||||
cmdCert(),
|
||||
CmdGenerate,
|
||||
CmdDocs,
|
||||
newGenerateCommand(),
|
||||
newDocsCommand(),
|
||||
}
|
||||
|
||||
// TODO: we should eventually drop the default command,
|
||||
// but not sure whether it would break Windows users who used to double-click the EXE to run.
|
||||
app.DefaultCommand = CmdWeb.Name
|
||||
app.DefaultCommand = webCmd.Name
|
||||
|
||||
app.Before = PrepareConsoleLoggerLevel(log.INFO)
|
||||
for i := range subCmdWithConfig {
|
||||
|
||||
+29
-15
@@ -13,22 +13,24 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// CmdManager represents the manager command
|
||||
CmdManager = &cli.Command{
|
||||
func newManagerCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "manager",
|
||||
Usage: "Manage the running gitea process",
|
||||
Description: "This is a command for managing the running gitea process",
|
||||
Commands: []*cli.Command{
|
||||
subcmdShutdown,
|
||||
subcmdRestart,
|
||||
subcmdReloadTemplates,
|
||||
subcmdFlushQueues,
|
||||
subcmdLogging,
|
||||
subCmdProcesses,
|
||||
newShutdownCommand(),
|
||||
newRestartCommand(),
|
||||
newReloadTemplatesCommand(),
|
||||
newFlushQueuesCommand(),
|
||||
newLoggingCommand(),
|
||||
newProcessesCommand(),
|
||||
},
|
||||
}
|
||||
subcmdShutdown = &cli.Command{
|
||||
}
|
||||
|
||||
func newShutdownCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "shutdown",
|
||||
Usage: "Gracefully shutdown the running process",
|
||||
Flags: []cli.Flag{
|
||||
@@ -38,7 +40,10 @@ var (
|
||||
},
|
||||
Action: runShutdown,
|
||||
}
|
||||
subcmdRestart = &cli.Command{
|
||||
}
|
||||
|
||||
func newRestartCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "restart",
|
||||
Usage: "Gracefully restart the running process - (not implemented for windows servers)",
|
||||
Flags: []cli.Flag{
|
||||
@@ -48,7 +53,10 @@ var (
|
||||
},
|
||||
Action: runRestart,
|
||||
}
|
||||
subcmdReloadTemplates = &cli.Command{
|
||||
}
|
||||
|
||||
func newReloadTemplatesCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "reload-templates",
|
||||
Usage: "Reload template files in the running process",
|
||||
Flags: []cli.Flag{
|
||||
@@ -58,7 +66,10 @@ var (
|
||||
},
|
||||
Action: runReloadTemplates,
|
||||
}
|
||||
subcmdFlushQueues = &cli.Command{
|
||||
}
|
||||
|
||||
func newFlushQueuesCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "flush-queues",
|
||||
Usage: "Flush queues in the running process",
|
||||
Action: runFlushQueues,
|
||||
@@ -77,7 +88,10 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
subCmdProcesses = &cli.Command{
|
||||
}
|
||||
|
||||
func newProcessesCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "processes",
|
||||
Usage: "Display running processes within the current process",
|
||||
Action: runProcesses,
|
||||
@@ -107,7 +121,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func runShutdown(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLoggingFlags = []cli.Flag{
|
||||
func defaultLoggingFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "logger",
|
||||
Usage: `Logger name - will default to "default"`,
|
||||
@@ -57,8 +57,10 @@ var (
|
||||
Name: "debug",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
subcmdLogging = &cli.Command{
|
||||
func newLoggingCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "logging",
|
||||
Usage: "Adjust logging commands",
|
||||
Commands: []*cli.Command{
|
||||
@@ -109,7 +111,7 @@ var (
|
||||
{
|
||||
Name: "file",
|
||||
Usage: "Add a file logger",
|
||||
Flags: append(defaultLoggingFlags, []cli.Flag{
|
||||
Flags: append(defaultLoggingFlags(), []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "filename",
|
||||
Aliases: []string{"f"},
|
||||
@@ -150,7 +152,7 @@ var (
|
||||
}, {
|
||||
Name: "conn",
|
||||
Usage: "Add a net conn logger",
|
||||
Flags: append(defaultLoggingFlags, []cli.Flag{
|
||||
Flags: append(defaultLoggingFlags(), []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "reconnect-on-message",
|
||||
Aliases: []string{"R"},
|
||||
@@ -191,7 +193,7 @@ var (
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func runRemoveLogger(ctx context.Context, c *cli.Command) error {
|
||||
setup(ctx, c.Bool("debug"))
|
||||
|
||||
+7
-6
@@ -14,12 +14,13 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdMigrate represents the available migrate sub-command.
|
||||
var CmdMigrate = &cli.Command{
|
||||
Name: "migrate",
|
||||
Usage: "Migrate the database",
|
||||
Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`,
|
||||
Action: runMigrate,
|
||||
func newMigrateCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "migrate",
|
||||
Usage: "Migrate the database",
|
||||
Description: `This is a command for migrating the database, so that you can run "gitea admin create user" before starting the server.`,
|
||||
Action: runMigrate,
|
||||
}
|
||||
}
|
||||
|
||||
func runMigrate(ctx context.Context, c *cli.Command) error {
|
||||
|
||||
+101
-100
@@ -25,107 +25,108 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdMigrateStorage represents the available migrate storage sub-command.
|
||||
var CmdMigrateStorage = &cli.Command{
|
||||
Name: "migrate-storage",
|
||||
Usage: "Migrate the storage",
|
||||
Description: "Copies stored files from storage configured in app.ini to parameter-configured storage",
|
||||
Action: runMigrateStorage,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Value: "",
|
||||
Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'",
|
||||
func newMigrateStorageCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "migrate-storage",
|
||||
Usage: "Migrate the storage",
|
||||
Description: "Copies stored files from storage configured in app.ini to parameter-configured storage",
|
||||
Action: runMigrateStorage,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Aliases: []string{"t"},
|
||||
Value: "",
|
||||
Usage: "Type of stored files to copy. Allowed types: 'attachments', 'lfs', 'avatars', 'repo-avatars', 'repo-archivers', 'packages', 'actions-log', 'actions-artifacts'",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "storage",
|
||||
Aliases: []string{"s"},
|
||||
Value: "",
|
||||
Usage: "New storage type: local (default), minio or azureblob",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Value: "",
|
||||
Usage: "New storage placement if store is local (leave blank for default)",
|
||||
},
|
||||
// Minio Storage special configurations
|
||||
&cli.StringFlag{
|
||||
Name: "minio-endpoint",
|
||||
Value: "",
|
||||
Usage: "Minio storage endpoint",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-access-key-id",
|
||||
Value: "",
|
||||
Usage: "Minio storage accessKeyID",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-secret-access-key",
|
||||
Value: "",
|
||||
Usage: "Minio storage secretAccessKey",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-bucket",
|
||||
Value: "",
|
||||
Usage: "Minio storage bucket",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-location",
|
||||
Value: "",
|
||||
Usage: "Minio storage location to create bucket",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-base-path",
|
||||
Value: "",
|
||||
Usage: "Minio storage base path on the bucket",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "minio-use-ssl",
|
||||
Usage: "Enable SSL for minio",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "minio-insecure-skip-verify",
|
||||
Usage: "Skip SSL verification",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-checksum-algorithm",
|
||||
Value: "",
|
||||
Usage: "Minio checksum algorithm (default/md5)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-bucket-lookup-type",
|
||||
Value: "",
|
||||
Usage: "Minio bucket lookup type",
|
||||
},
|
||||
// Azure Blob Storage special configurations
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-endpoint",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage endpoint",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-account-name",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage account name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-account-key",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage account key",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-container",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage container",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-base-path",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage base path",
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "storage",
|
||||
Aliases: []string{"s"},
|
||||
Value: "",
|
||||
Usage: "New storage type: local (default), minio or azureblob",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "path",
|
||||
Aliases: []string{"p"},
|
||||
Value: "",
|
||||
Usage: "New storage placement if store is local (leave blank for default)",
|
||||
},
|
||||
// Minio Storage special configurations
|
||||
&cli.StringFlag{
|
||||
Name: "minio-endpoint",
|
||||
Value: "",
|
||||
Usage: "Minio storage endpoint",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-access-key-id",
|
||||
Value: "",
|
||||
Usage: "Minio storage accessKeyID",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-secret-access-key",
|
||||
Value: "",
|
||||
Usage: "Minio storage secretAccessKey",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-bucket",
|
||||
Value: "",
|
||||
Usage: "Minio storage bucket",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-location",
|
||||
Value: "",
|
||||
Usage: "Minio storage location to create bucket",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-base-path",
|
||||
Value: "",
|
||||
Usage: "Minio storage base path on the bucket",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "minio-use-ssl",
|
||||
Usage: "Enable SSL for minio",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "minio-insecure-skip-verify",
|
||||
Usage: "Skip SSL verification",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-checksum-algorithm",
|
||||
Value: "",
|
||||
Usage: "Minio checksum algorithm (default/md5)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "minio-bucket-lookup-type",
|
||||
Value: "",
|
||||
Usage: "Minio bucket lookup type",
|
||||
},
|
||||
// Azure Blob Storage special configurations
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-endpoint",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage endpoint",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-account-name",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage account name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-account-key",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage account key",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-container",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage container",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "azureblob-base-path",
|
||||
Value: "",
|
||||
Usage: "Azure Blob storage base path",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func migrateAttachments(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
|
||||
+33
-32
@@ -13,40 +13,41 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdRestoreRepository represents the available restore a repository sub-command.
|
||||
var CmdRestoreRepository = &cli.Command{
|
||||
Name: "restore-repo",
|
||||
Usage: "Restore the repository from disk",
|
||||
Description: "This is a command for restoring the repository data.",
|
||||
Action: runRestoreRepository,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "repo_dir",
|
||||
Aliases: []string{"r"},
|
||||
Value: "./data",
|
||||
Usage: "Repository dir path to restore from",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "owner_name",
|
||||
Value: "",
|
||||
Usage: "Restore destination owner name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_name",
|
||||
Value: "",
|
||||
Usage: "Restore destination repository name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "units",
|
||||
Value: "",
|
||||
Usage: `Which items will be restored, one or more units should be separated as comma.
|
||||
func newRestoreRepositoryCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "restore-repo",
|
||||
Usage: "Restore the repository from disk",
|
||||
Description: "This is a command for restoring the repository data.",
|
||||
Action: runRestoreRepository,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "repo_dir",
|
||||
Aliases: []string{"r"},
|
||||
Value: "./data",
|
||||
Usage: "Repository dir path to restore from",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "owner_name",
|
||||
Value: "",
|
||||
Usage: "Restore destination owner name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "repo_name",
|
||||
Value: "",
|
||||
Usage: "Restore destination repository name",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "units",
|
||||
Value: "",
|
||||
Usage: `Which items will be restored, one or more units should be separated as comma.
|
||||
wiki, issues, labels, releases, release_assets, milestones, pull_requests, comments are allowed. Empty means all units.`,
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "validation",
|
||||
Usage: "Sanity check the content of the files before trying to load them",
|
||||
},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "validation",
|
||||
Usage: "Sanity check the content of the files before trying to load them",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runRestoreRepository(ctx context.Context, c *cli.Command) error {
|
||||
|
||||
+16
-15
@@ -35,22 +35,23 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdServ represents the available serv sub-command.
|
||||
var CmdServ = &cli.Command{
|
||||
Name: "serv",
|
||||
Usage: "(internal) Should only be called by SSH shell",
|
||||
Description: "Serv provides access auth for repositories",
|
||||
Hidden: true, // Internal commands shouldn't be visible in help
|
||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||
Action: runServ,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-pprof",
|
||||
func newServCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "serv",
|
||||
Usage: "(internal) Should only be called by SSH shell",
|
||||
Description: "Serv provides access auth for repositories",
|
||||
Hidden: true, // Internal commands shouldn't be visible in help
|
||||
Before: PrepareConsoleLoggerLevel(log.FATAL),
|
||||
Action: runServ,
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "enable-pprof",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "debug",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func setup(ctx context.Context, debug bool) {
|
||||
|
||||
+35
-34
@@ -34,42 +34,43 @@ import (
|
||||
// PIDFile could be set from build tag
|
||||
var PIDFile = "/run/gitea.pid"
|
||||
|
||||
// CmdWeb represents the available web sub-command.
|
||||
var CmdWeb = &cli.Command{
|
||||
Name: "web",
|
||||
Usage: "Start Gitea web server",
|
||||
Description: `Gitea web server is the only thing you need to run,
|
||||
func newWebCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "web",
|
||||
Usage: "Start Gitea web server",
|
||||
Description: `Gitea web server is the only thing you need to run,
|
||||
and it takes care of all the other things for you`,
|
||||
Before: PrepareConsoleLoggerLevel(log.INFO),
|
||||
Action: runWeb,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "port",
|
||||
Aliases: []string{"p"},
|
||||
Value: "3000",
|
||||
Usage: "Temporary port number to prevent conflict",
|
||||
Before: PrepareConsoleLoggerLevel(log.INFO),
|
||||
Action: runWeb,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "port",
|
||||
Aliases: []string{"p"},
|
||||
Value: "3000",
|
||||
Usage: "Temporary port number to prevent conflict",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "install-port",
|
||||
Value: "3000",
|
||||
Usage: "Temporary port number to run the install page on to prevent conflict",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "pid",
|
||||
Aliases: []string{"P"},
|
||||
Value: PIDFile,
|
||||
Usage: "Custom pid file path",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Aliases: []string{"q"},
|
||||
Usage: "Only display Fatal logging errors until logging is set-up",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Usage: "Set initial logging to TRACE level until logging is properly set-up",
|
||||
},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "install-port",
|
||||
Value: "3000",
|
||||
Usage: "Temporary port number to run the install page on to prevent conflict",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "pid",
|
||||
Aliases: []string{"P"},
|
||||
Value: PIDFile,
|
||||
Usage: "Custom pid file path",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "quiet",
|
||||
Aliases: []string{"q"},
|
||||
Usage: "Only display Fatal logging errors until logging is set-up",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Usage: "Set initial logging to TRACE level until logging is properly set-up",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runHTTPRedirector() {
|
||||
|
||||
@@ -69,13 +69,12 @@ RUN_USER = ; git
|
||||
;; Most users should set it to the real website URL of their Gitea instance when there is a reverse proxy.
|
||||
;ROOT_URL =
|
||||
;;
|
||||
;; Controls how to detect the public URL.
|
||||
;; Although it defaults to "legacy" (to avoid breaking existing users), most instances should use the "auto" behavior,
|
||||
;; Controls how to detect the public URL. Most instances should use the "auto" behavior,
|
||||
;; especially when the Gitea instance needs to be accessed in a container network.
|
||||
;; * legacy: detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL".
|
||||
;; * auto: always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL".
|
||||
;; * legacy: (default <= 1.25) detect the public URL from "Host" header if "X-Forwarded-Proto" header exists, otherwise use "ROOT_URL".
|
||||
;; * auto: (default >= 1.26) always use "Host" header, and also use "X-Forwarded-Proto" header if it exists. If no "Host" header, use "ROOT_URL".
|
||||
;; * never: always use "ROOT_URL", never detect from request headers.
|
||||
;PUBLIC_URL_DETECTION = legacy
|
||||
;PUBLIC_URL_DETECTION = auto
|
||||
;;
|
||||
;; For development purpose only. It makes Gitea handle sub-path ("/sub-path/owner/repo/...") directly when debugging without a reverse proxy.
|
||||
;; DO NOT USE IT IN PRODUCTION!!!
|
||||
@@ -1153,9 +1152,6 @@ LEVEL = Info
|
||||
;; Add co-authored-by and co-committed-by trailers if committer does not match author
|
||||
;ADD_CO_COMMITTER_TRAILERS = true
|
||||
;;
|
||||
;; In addition to testing patches using the three-way merge method, re-test conflicting patches with git apply
|
||||
;TEST_CONFLICTING_PATCHES_WITH_GIT_APPLY = false
|
||||
;;
|
||||
;; Retarget child pull requests to the parent pull request branch target on merge of parent pull request. It only works on merged PRs where the head and base branch target the same repo.
|
||||
;RETARGET_CHILDREN_ON_MERGE = true
|
||||
;;
|
||||
@@ -2280,6 +2276,22 @@ LEVEL = Info
|
||||
;; Unreferenced blobs created more than OLDER_THAN ago are subject to deletion
|
||||
;OLDER_THAN = 24h
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Synchronize repository licenses
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[cron.sync_repo_licenses]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Whether to enable the job
|
||||
;ENABLED = false
|
||||
;; Whether to always run at least once at start up time (if ENABLED)
|
||||
;RUN_AT_START = false
|
||||
;; Whether to emit notice on successful execution too
|
||||
;NOTICE_ON_SUCCESS = false
|
||||
;; Time interval for job to run
|
||||
;SCHEDULE = @annually
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -2341,6 +2353,18 @@ LEVEL = Info
|
||||
;NOTICE_ON_SUCCESS = false
|
||||
;SCHEDULE = @every 72h
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Update the '.ssh/authorized_principals' file
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[cron.resync_all_sshprincipals]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;ENABLED = false
|
||||
;RUN_AT_START = false
|
||||
;NOTICE_ON_SUCCESS = false
|
||||
;SCHEDULE = @every 72h
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...)
|
||||
@@ -2449,6 +2473,70 @@ LEVEL = Info
|
||||
;Check at least this proportion of LFSMetaObjects per repo. (This may cause all stale LFSMetaObjects to be checked.)
|
||||
;PROPORTION_TO_CHECK_PER_REPO = 0.6
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Rebuild issue index
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[cron.rebuild_issue_indexer]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;ENABLED = false
|
||||
;RUN_AT_START = false
|
||||
;NO_SUCCESS_NOTICE = false
|
||||
;SCHEDULE = @annually
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Actions cron tasks
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Stop running tasks which haven't been updated for a long time
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[cron.stop_zombie_tasks]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;ENABLED = true
|
||||
;RUN_AT_START = true
|
||||
;NO_SUCCESS_NOTICE = false
|
||||
;SCHEDULE = @every 5m
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Stop running tasks which have running status and continuous updates but don't end for a long time
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[cron.stop_endless_tasks]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;ENABLED = true
|
||||
;RUN_AT_START = true
|
||||
;NO_SUCCESS_NOTICE = false
|
||||
;SCHEDULE = @every 30m
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Cancel jobs which haven't been picked up for a long time
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[cron.cancel_abandoned_jobs]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;ENABLED = true
|
||||
;RUN_AT_START = false
|
||||
;NO_SUCCESS_NOTICE = false
|
||||
;SCHEDULE = @every 6h
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Start cron based actions
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[cron.start_schedule_tasks]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;ENABLED = true
|
||||
;RUN_AT_START = false
|
||||
;NO_SUCCESS_NOTICE = false
|
||||
;SCHEDULE = @every 1m
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;[mirror]
|
||||
|
||||
+6
-2
@@ -572,7 +572,11 @@ export default defineConfig([
|
||||
'no-restricted-exports': [0],
|
||||
'no-restricted-globals': [2, ...restrictedGlobals],
|
||||
'no-restricted-properties': [2, ...restrictedProperties],
|
||||
'no-restricted-imports': [0],
|
||||
'no-restricted-imports': [2, {paths: [
|
||||
{name: 'jquery', message: 'Use the global $ instead', allowTypeImports: true},
|
||||
{name: 'htmx.org', message: 'Use the global htmx instead', allowTypeImports: true},
|
||||
{name: 'idiomorph/htmx', message: 'Loaded in globals.ts', allowTypeImports: true},
|
||||
]}],
|
||||
'no-restricted-syntax': [2, 'WithStatement', 'ForInStatement', 'LabeledStatement', 'SequenceExpression'],
|
||||
'no-return-assign': [0],
|
||||
'no-script-url': [2],
|
||||
@@ -1014,6 +1018,6 @@ export default defineConfig([
|
||||
},
|
||||
{
|
||||
files: ['web_src/**/*'],
|
||||
languageOptions: {globals: {...globals.browser, ...globals.webpack}},
|
||||
languageOptions: {globals: {...globals.browser, ...globals.jquery, htmx: false}},
|
||||
},
|
||||
]);
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1773821835,
|
||||
"narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=",
|
||||
"lastModified": 1774386573,
|
||||
"narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0",
|
||||
"rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
inherit (pkgs) lib;
|
||||
|
||||
# only bump toolchain versions here
|
||||
go = pkgs.go_1_25;
|
||||
go = pkgs.go_1_26;
|
||||
nodejs = pkgs.nodejs_24;
|
||||
python3 = pkgs.python312;
|
||||
python3 = pkgs.python314;
|
||||
pnpm = pkgs.pnpm_10;
|
||||
|
||||
# Platform-specific dependencies
|
||||
|
||||
@@ -112,7 +112,7 @@ require (
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
gitlab.com/gitlab-org/api/client-go v0.142.4
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/image v0.35.0
|
||||
golang.org/x/net v0.49.0
|
||||
@@ -167,7 +167,7 @@ require (
|
||||
github.com/blevesearch/zapx/v14 v14.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v15 v15.4.2 // indirect
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
@@ -220,7 +220,7 @@ require (
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/markbates/going v1.0.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.17 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.4 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
@@ -248,7 +248,7 @@ require (
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/rhysd/actionlint v1.7.7 // indirect
|
||||
github.com/rhysd/actionlint v1.7.11 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
@@ -291,7 +291,7 @@ ignore (
|
||||
|
||||
replace github.com/jaytaylor/html2text => github.com/Necoro/html2text v0.0.0-20250804200300-7bf1ce1c7347
|
||||
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.8
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.261.10
|
||||
|
||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
gitea.com/gitea/act v0.261.8 h1:rUWB5GOZOubfe2VteKb7XP3HRIbcW3UUmfh7bVAgQcA=
|
||||
gitea.com/gitea/act v0.261.8/go.mod h1:lTp4136rwbZiZS3ZVQeHCvd4qRAZ7LYeiRBqOSdMY/4=
|
||||
gitea.com/gitea/act v0.261.10 h1:ndwbtuMXXz1dpYF2iwY1/PkgKNETo4jmPXfinTZt8cs=
|
||||
gitea.com/gitea/act v0.261.10/go.mod h1:oIkqQHvU0lfuIWwcpqa4FmU+t3prA89tgkuHUTsrI2c=
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso=
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed/go.mod h1:E3i3cgB04dDx0v3CytCgRTTn9Z/9x891aet3r456RVw=
|
||||
gitea.com/go-chi/cache v0.2.1 h1:bfAPkvXlbcZxPCpcmDVCWoHgiBSBmZN/QosnZvEC0+g=
|
||||
@@ -182,8 +182,8 @@ github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFx
|
||||
github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
|
||||
github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
|
||||
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
@@ -551,8 +551,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ=
|
||||
github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
@@ -669,8 +669,8 @@ github.com/redis/rueidis/rueidiscompat v1.0.69 h1:IWVYY9lXdjNO3do2VpJT7aDFi8zbCU
|
||||
github.com/redis/rueidis/rueidiscompat v1.0.69/go.mod h1:iC4Y8DoN0Uth0Uezg9e2trvNRC7QAgGeuP2OPLb5ccI=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rhysd/actionlint v1.7.7 h1:0KgkoNTrYY7vmOCs9BW2AHxLvvpoY9nEUzgBHiPUr0k=
|
||||
github.com/rhysd/actionlint v1.7.7/go.mod h1:AE6I6vJEkNaIfWqC2GNE5spIJNhxf8NCtLEKU4NnUXg=
|
||||
github.com/rhysd/actionlint v1.7.11 h1:m+aSuCpCIClS8X02xMG4Z8s87fCHPsAtYkAoWGQZgEE=
|
||||
github.com/rhysd/actionlint v1.7.11/go.mod h1:8n50YougV9+50niD7oxgDTZ1KbN/ZnKiQ2xpLFeVhsI=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -821,8 +821,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
|
||||
+27
-11
@@ -53,6 +53,11 @@ func init() {
|
||||
db.RegisterModel(new(ActionArtifact))
|
||||
}
|
||||
|
||||
const (
|
||||
ContentEncodingV3Gzip = "gzip"
|
||||
ContentTypeZip = "application/zip"
|
||||
)
|
||||
|
||||
// ActionArtifact is a file that is stored in the artifact storage.
|
||||
type ActionArtifact struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
@@ -61,16 +66,26 @@ type ActionArtifact struct {
|
||||
RepoID int64 `xorm:"index"`
|
||||
OwnerID int64
|
||||
CommitSHA string
|
||||
StoragePath string // The path to the artifact in the storage
|
||||
FileSize int64 // The size of the artifact in bytes
|
||||
FileCompressedSize int64 // The size of the artifact in bytes after gzip compression
|
||||
ContentEncoding string // The content encoding of the artifact
|
||||
ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it
|
||||
ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it
|
||||
Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
|
||||
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
|
||||
StoragePath string // The path to the artifact in the storage
|
||||
FileSize int64 // The size of the artifact in bytes
|
||||
FileCompressedSize int64 // The size of the artifact in bytes after gzip compression
|
||||
|
||||
// The content encoding or content type of the artifact
|
||||
// * empty or null: legacy (v3) uncompressed content
|
||||
// * magic string "gzip" (ContentEncodingV3Gzip): v3 gzip compressed content
|
||||
// * requires gzip decoding before storing in a zip for download
|
||||
// * requires gzip content-encoding header when downloaded single files within a workflow
|
||||
// * mime type for "Content-Type":
|
||||
// * "application/zip" (ContentTypeZip), seems to be an abuse, fortunately there is no conflict, and it won't cause problems?
|
||||
// * "application/pdf", "text/html", etc.: real content type of the artifact
|
||||
ContentEncodingOrType string `xorm:"content_encoding"`
|
||||
|
||||
ArtifactPath string `xorm:"index unique(runid_name_path)"` // The path to the artifact when runner uploads it
|
||||
ArtifactName string `xorm:"index unique(runid_name_path)"` // The name of the artifact when runner uploads it
|
||||
Status ArtifactStatus `xorm:"index"` // The status of the artifact, uploading, expired or need-delete
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated index"`
|
||||
ExpiredUnix timeutil.TimeStamp `xorm:"index"` // The time when the artifact will be expired
|
||||
}
|
||||
|
||||
func CreateArtifact(ctx context.Context, t *ActionTask, artifactName, artifactPath string, expiredDays int64) (*ActionArtifact, error) {
|
||||
@@ -156,7 +171,8 @@ func (opts FindArtifactsOptions) ToConds() builder.Cond {
|
||||
}
|
||||
if opts.FinalizedArtifactsV4 {
|
||||
cond = cond.And(builder.Eq{"status": ArtifactStatusUploadConfirmed}.Or(builder.Eq{"status": ArtifactStatusExpired}))
|
||||
cond = cond.And(builder.Eq{"content_encoding": "application/zip"})
|
||||
// see the comment of ActionArtifact.ContentEncodingOrType: "*/*" means the field is a content type
|
||||
cond = cond.And(builder.Like{"content_encoding", "%/%"})
|
||||
}
|
||||
|
||||
return cond
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
@@ -139,7 +140,10 @@ func init() {
|
||||
// BeforeSet is invoked from XORM before setting the value of a field of this object.
|
||||
func (source *Source) BeforeSet(colName string, val xorm.Cell) {
|
||||
if colName == "type" {
|
||||
typ := Type(db.Cell2Int64(val))
|
||||
typ, _, err := db.CellToInt(val, NoType)
|
||||
if err != nil {
|
||||
setting.PanicInDevOrTesting("Unable to convert login source (id=%d) type: %v", source.ID, err)
|
||||
}
|
||||
constructor, ok := registeredConfigs[typ]
|
||||
if !ok {
|
||||
return
|
||||
|
||||
+16
-24
@@ -17,13 +17,9 @@ import (
|
||||
)
|
||||
|
||||
type TestSource struct {
|
||||
auth_model.ConfigBase
|
||||
auth_model.ConfigBase `json:"-"`
|
||||
|
||||
Provider string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
OpenIDConnectAutoDiscoveryURL string
|
||||
IconURL string
|
||||
TestField string
|
||||
}
|
||||
|
||||
// FromDB fills up a LDAPConfig from serialized format.
|
||||
@@ -37,27 +33,23 @@ func (source *TestSource) ToDB() ([]byte, error) {
|
||||
}
|
||||
|
||||
func TestDumpAuthSource(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
authSourceSchema, err := unittest.GetXORMEngine().TableInfo(new(auth_model.Source))
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
auth_model.RegisterTypeConfig(auth_model.OAuth2, new(TestSource))
|
||||
source := &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "TestSource",
|
||||
Cfg: &TestSource{TestField: "TestValue"},
|
||||
}
|
||||
require.NoError(t, auth_model.CreateSource(t.Context(), source))
|
||||
|
||||
auth_model.CreateSource(t.Context(), &auth_model.Source{
|
||||
Type: auth_model.OAuth2,
|
||||
Name: "TestSource",
|
||||
IsActive: false,
|
||||
Cfg: &TestSource{
|
||||
Provider: "ConvertibleSourceName",
|
||||
ClientID: "42",
|
||||
},
|
||||
})
|
||||
|
||||
sb := new(strings.Builder)
|
||||
|
||||
// TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump
|
||||
engine := unittest.GetXORMEngine()
|
||||
require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb))
|
||||
assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`)
|
||||
// intentionally test the "dump" to make sure the dumped JSON is correct: https://github.com/go-gitea/gitea/pull/16847
|
||||
sb := &strings.Builder{}
|
||||
require.NoError(t, unittest.GetXORMEngine().DumpTables([]*schemas.Table{authSourceSchema}, sb))
|
||||
// the dumped SQL is something like:
|
||||
// INSERT INTO `login_source` (`id`, `type`, `name`, `is_active`, `is_sync_enabled`, `two_factor_policy`, `cfg`, `created_unix`, `updated_unix`) VALUES (1,6,'TestSource',0,0,'','{"TestField":"TestValue"}',1774179784,1774179784);
|
||||
assert.Contains(t, sb.String(), `'{"TestField":"TestValue"}'`)
|
||||
}
|
||||
|
||||
+10
-12
@@ -5,12 +5,11 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/convert"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
@@ -74,15 +73,14 @@ WHERE ST.name ='varchar'`)
|
||||
return err
|
||||
}
|
||||
|
||||
// Cell2Int64 converts a xorm.Cell type to int64,
|
||||
// and handles possible irregular cases.
|
||||
func Cell2Int64(val xorm.Cell) int64 {
|
||||
switch (*val).(type) {
|
||||
case []uint8:
|
||||
log.Trace("Cell2Int64 ([]uint8): %v", *val)
|
||||
|
||||
v, _ := strconv.ParseInt(string((*val).([]uint8)), 10, 64)
|
||||
return v
|
||||
// CellToInt converts a xorm.Cell field value to an int value
|
||||
func CellToInt[T ~int | int64](cell xorm.Cell, def T) (ret T, has bool, err error) {
|
||||
if *cell == nil {
|
||||
return def, false, nil
|
||||
}
|
||||
return (*val).(int64)
|
||||
val, err := convert.AsInt64(*cell)
|
||||
if err != nil {
|
||||
return def, false, err
|
||||
}
|
||||
return T(val), true, err
|
||||
}
|
||||
|
||||
@@ -141,3 +141,39 @@
|
||||
created_unix: 1730330775
|
||||
updated_unix: 1730330775
|
||||
expired_unix: 1738106775
|
||||
|
||||
-
|
||||
id: 26
|
||||
run_id: 792
|
||||
runner_id: 1
|
||||
repo_id: 4
|
||||
owner_id: 1
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
storage_path: "27/5/1730330775594233150.chunk"
|
||||
file_size: 1024
|
||||
file_compressed_size: 1024
|
||||
content_encoding: "application/pdf"
|
||||
artifact_path: "report.pdf"
|
||||
artifact_name: "report.pdf"
|
||||
status: 2
|
||||
created_unix: 1730330775
|
||||
updated_unix: 1730330775
|
||||
expired_unix: 1738106775
|
||||
|
||||
-
|
||||
id: 27
|
||||
run_id: 792
|
||||
runner_id: 1
|
||||
repo_id: 4
|
||||
owner_id: 1
|
||||
commit_sha: c2d72f548424103f01ee1dc02889c1e2bff816b0
|
||||
storage_path: "27/5/1730330775594233150.chunk"
|
||||
file_size: 1024
|
||||
file_compressed_size: 1024
|
||||
content_encoding: "application/html"
|
||||
artifact_path: "report.html"
|
||||
artifact_name: "report.html"
|
||||
status: 2
|
||||
created_unix: 1730330775
|
||||
updated_unix: 1730330775
|
||||
expired_unix: 1738106775
|
||||
|
||||
@@ -495,9 +495,9 @@ func updateUserWhitelist(ctx context.Context, repo *repo_model.Repository, curre
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserByID [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
|
||||
}
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
|
||||
perm, err := access_model.GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
|
||||
return nil, fmt.Errorf("GetIndividualUserRepoPermission [user_id: %d, repo_id: %d]: %v", userID, repo.ID, err)
|
||||
}
|
||||
|
||||
if !perm.CanWrite(unit.TypeCode) {
|
||||
|
||||
@@ -356,7 +356,7 @@ func ClearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User)
|
||||
return err
|
||||
}
|
||||
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -665,9 +665,9 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
|
||||
continue
|
||||
}
|
||||
// Normal users must have read access to the referencing issue
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user)
|
||||
perm, err := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err)
|
||||
return nil, fmt.Errorf("GetIndividualUserRepoPermission [%d]: %w", user.ID, err)
|
||||
}
|
||||
if !perm.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
continue
|
||||
|
||||
@@ -207,7 +207,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
|
||||
|
||||
// Check doer permissions; set action to None if the doer can't change the destination
|
||||
if refIssue.RepoID != ctx.OrigIssue.RepoID || ref.Action != references.XRefActionNone {
|
||||
perm, err := access_model.GetUserRepoPermission(stdCtx, refIssue.Repo, ctx.Doer)
|
||||
perm, err := access_model.GetDoerRepoPermission(stdCtx, refIssue.Repo, ctx.Doer)
|
||||
if err != nil {
|
||||
return nil, references.XRefActionNone, err
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission,
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
prPerm, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, user)
|
||||
prPerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.BaseRepo, user)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -919,7 +919,7 @@ func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.Use
|
||||
return false, nil
|
||||
}
|
||||
if doer.ID != issue.PosterID {
|
||||
p, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
|
||||
p, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -91,43 +91,80 @@ func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mo
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
|
||||
// refreshAccesses updates the repository's access records in the database by comparing the provided accessMap
|
||||
// with existing records. It minimizes DB operations by performing selective inserts, updates, and deletes
|
||||
// instead of removing all existing records and re-adding them.
|
||||
func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) {
|
||||
minMode := perm.AccessModeRead
|
||||
minModeToKeep := perm.AccessModeRead
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return fmt.Errorf("LoadOwner: %w", err)
|
||||
}
|
||||
|
||||
// If the repo isn't private and isn't owned by a organization,
|
||||
// If the repo isn't private and isn't owned by an organization,
|
||||
// increase the minMode to Write.
|
||||
if !repo.IsPrivate && !repo.Owner.IsOrganization() {
|
||||
minMode = perm.AccessModeWrite
|
||||
minModeToKeep = perm.AccessModeWrite
|
||||
}
|
||||
|
||||
newAccesses := make([]Access, 0, len(accessMap))
|
||||
// Query existing accesses for cross-comparison
|
||||
var existingAccesses []Access
|
||||
if err := db.GetEngine(ctx).Where(builder.Eq{"repo_id": repo.ID}).Find(&existingAccesses); err != nil {
|
||||
return fmt.Errorf("find existing accesses: %w", err)
|
||||
}
|
||||
existingMap := make(map[int64]perm.AccessMode, len(existingAccesses))
|
||||
for _, a := range existingAccesses {
|
||||
existingMap[a.UserID] = a.Mode
|
||||
}
|
||||
|
||||
var toDelete []int64
|
||||
var toInsert, toUpdate []Access
|
||||
|
||||
// Determine changes
|
||||
for userID, ua := range accessMap {
|
||||
if ua.Mode < minMode && !ua.User.IsRestricted {
|
||||
continue
|
||||
if ua.Mode < minModeToKeep && !ua.User.IsRestricted {
|
||||
// No explicit access record needed (handled by default permissions, e.g., public repo access)
|
||||
if _, exists := existingMap[userID]; exists {
|
||||
toDelete = append(toDelete, userID)
|
||||
}
|
||||
} else {
|
||||
desiredMode := ua.Mode
|
||||
if existingMode, exists := existingMap[userID]; exists {
|
||||
if existingMode != desiredMode {
|
||||
toUpdate = append(toUpdate, Access{UserID: userID, RepoID: repo.ID, Mode: desiredMode})
|
||||
}
|
||||
} else {
|
||||
toInsert = append(toInsert, Access{UserID: userID, RepoID: repo.ID, Mode: desiredMode})
|
||||
}
|
||||
}
|
||||
|
||||
newAccesses = append(newAccesses, Access{
|
||||
UserID: userID,
|
||||
RepoID: repo.ID,
|
||||
Mode: ua.Mode,
|
||||
})
|
||||
delete(existingMap, userID)
|
||||
}
|
||||
|
||||
// Delete old accesses and insert new ones for repository.
|
||||
if _, err = db.DeleteByBean(ctx, &Access{RepoID: repo.ID}); err != nil {
|
||||
return fmt.Errorf("delete old accesses: %w", err)
|
||||
}
|
||||
if len(newAccesses) == 0 {
|
||||
return nil
|
||||
// Remaining in existingMap should be deleted
|
||||
for userID := range existingMap {
|
||||
toDelete = append(toDelete, userID)
|
||||
}
|
||||
|
||||
if err = db.Insert(ctx, newAccesses); err != nil {
|
||||
return fmt.Errorf("insert new accesses: %w", err)
|
||||
// Execute deletions
|
||||
if len(toDelete) > 0 {
|
||||
if _, err = db.GetEngine(ctx).In("user_id", toDelete).And("repo_id = ?", repo.ID).Delete(&Access{}); err != nil {
|
||||
return fmt.Errorf("delete accesses: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute updates
|
||||
for _, u := range toUpdate {
|
||||
if _, err = db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", u.UserID, repo.ID).Cols("mode").Update(&Access{Mode: u.Mode}); err != nil {
|
||||
return fmt.Errorf("update access for user %d: %w", u.UserID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute insertions
|
||||
if len(toInsert) > 0 {
|
||||
if err = db.Insert(ctx, toInsert); err != nil {
|
||||
return fmt.Errorf("insert new accesses: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,11 +15,20 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAccessLevel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func TestAccess(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
t.Run("AccessLevel", testAccessLevel)
|
||||
t.Run("HasAccess", testHasAccess)
|
||||
t.Run("RecalculateAccesses", testRecalculateAccesses)
|
||||
t.Run("RecalculateAccesses2", testRecalculateAccesses2)
|
||||
t.Run("RecalculateAccessesUpdateMode", testRecalculateAccessesUpdateMode)
|
||||
t.Run("RecalculateAccessesRemoveAccess", testRecalculateAccessesRemoveAccess)
|
||||
}
|
||||
|
||||
func testAccessLevel(t *testing.T) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
|
||||
@@ -75,9 +84,7 @@ func TestAccessLevel(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeRead, level)
|
||||
}
|
||||
|
||||
func TestHasAccess(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testHasAccess(t *testing.T) {
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
// A public repository owned by User 2
|
||||
@@ -101,9 +108,8 @@ func TestHasAccess(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses(t *testing.T) {
|
||||
func testRecalculateAccesses(t *testing.T) {
|
||||
// test with organization repo
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.NoError(t, repo1.LoadOwner(t.Context()))
|
||||
|
||||
@@ -118,9 +124,8 @@ func TestRepository_RecalculateAccesses(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeOwner, access.Mode)
|
||||
}
|
||||
|
||||
func TestRepository_RecalculateAccesses2(t *testing.T) {
|
||||
func testRecalculateAccesses2(t *testing.T) {
|
||||
// test with non-organization repo
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.NoError(t, repo1.LoadOwner(t.Context()))
|
||||
|
||||
@@ -132,3 +137,67 @@ func TestRepository_RecalculateAccesses2(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
}
|
||||
|
||||
func testRecalculateAccessesUpdateMode(t *testing.T) {
|
||||
// Test the update path in refreshAccesses optimization
|
||||
// Scenario: User's access mode changes from Read to Write
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.NoError(t, repo.LoadOwner(t.Context()))
|
||||
|
||||
// Verify initial access mode
|
||||
_ = db.Insert(t.Context(), &repo_model.Collaboration{UserID: 4, RepoID: 4, Mode: perm_model.AccessModeWrite})
|
||||
_ = db.Insert(t.Context(), &access_model.Access{UserID: 4, RepoID: 4, Mode: perm_model.AccessModeWrite})
|
||||
initialAccess := &access_model.Access{UserID: 4, RepoID: 4}
|
||||
has, err := db.GetEngine(t.Context()).Get(initialAccess)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
initialMode := initialAccess.Mode
|
||||
|
||||
// Change collaboration mode to trigger update path
|
||||
newMode := perm_model.AccessModeAdmin
|
||||
assert.NotEqual(t, initialMode, newMode, "New mode should differ from initial mode")
|
||||
|
||||
_, err = db.GetEngine(t.Context()).
|
||||
Where("user_id = ? AND repo_id = ?", 4, 4).
|
||||
Cols("mode").
|
||||
Update(&repo_model.Collaboration{Mode: newMode})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Recalculate accesses - should UPDATE existing access, not delete+insert
|
||||
assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo))
|
||||
|
||||
// Verify access was updated, not deleted and re-inserted
|
||||
updatedAccess := &access_model.Access{UserID: 4, RepoID: 4}
|
||||
has, err = db.GetEngine(t.Context()).Get(updatedAccess)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has, "Access should still exist")
|
||||
assert.Equal(t, newMode, updatedAccess.Mode, "Access mode should be updated to new collaboration mode")
|
||||
}
|
||||
|
||||
func testRecalculateAccessesRemoveAccess(t *testing.T) {
|
||||
// Test the delete path in refreshAccesses optimization
|
||||
// Scenario: Remove a user's collaboration, access should be deleted
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
assert.NoError(t, repo.LoadOwner(t.Context()))
|
||||
|
||||
// Verify initial access exists
|
||||
initialAccess := &access_model.Access{UserID: 4, RepoID: 4}
|
||||
has, err := db.GetEngine(t.Context()).Get(initialAccess)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has, "Access should exist initially")
|
||||
|
||||
// Remove the collaboration to trigger delete path
|
||||
_, err = db.GetEngine(t.Context()).
|
||||
Where("user_id = ? AND repo_id = ?", 4, 4).
|
||||
Delete(&repo_model.Collaboration{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Recalculate accesses - should DELETE the access record
|
||||
assert.NoError(t, access_model.RecalculateAccesses(t.Context(), repo))
|
||||
|
||||
// Verify access was deleted
|
||||
removedAccess := &access_model.Access{UserID: 4, RepoID: 4}
|
||||
has, err = db.GetEngine(t.Context()).Get(removedAccess)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has, "Access should be deleted after removing collaboration")
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
|
||||
|
||||
// Check permission like simple user but limit to read-only (PR #36095)
|
||||
// Enhanced to also grant read-only access if isSameRepo is true and target repository is public
|
||||
botPerm, err := GetUserRepoPermission(ctx, repo, user_model.NewActionsUser())
|
||||
botPerm, err := GetIndividualUserRepoPermission(ctx, repo, user_model.NewActionsUser())
|
||||
if err != nil {
|
||||
return perm, err
|
||||
}
|
||||
@@ -379,8 +379,19 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
|
||||
return perm, nil
|
||||
}
|
||||
|
||||
// GetUserRepoPermission returns the user permissions to the repository
|
||||
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
|
||||
// GetDoerRepoPermission returns the repository permission for the current actor,
|
||||
// dispatching to GetActionsUserRepoPermission when the actor is an Actions token user.
|
||||
func GetDoerRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (Permission, error) {
|
||||
if taskID, ok := user_model.GetActionsUserTaskID(user); ok {
|
||||
return GetActionsUserRepoPermission(ctx, repo, user, taskID)
|
||||
}
|
||||
return GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
}
|
||||
|
||||
// GetIndividualUserRepoPermission returns the permissions for an explicit user identity.
|
||||
// In most request paths, callers should use GetDoerRepoPermission instead.
|
||||
// Unlike GetDoerRepoPermission, this helper does not resolve Actions task users.
|
||||
func GetIndividualUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
finalProcessRepoUnitPermission(user, &perm)
|
||||
@@ -539,8 +550,9 @@ func AccessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re
|
||||
|
||||
// AccessLevelUnit returns the Access a user has to a repository's. Will return NoneAccess if the
|
||||
// user does not have access.
|
||||
// This helper only supports explicit user identities and does not resolve Actions task users.
|
||||
func AccessLevelUnit(ctx context.Context, user *user_model.User, repo *repo_model.Repository, unitType unit.Type) (perm_model.AccessMode, error) { //nolint:revive // export stutter
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return perm_model.AccessModeNone, err
|
||||
}
|
||||
@@ -559,7 +571,7 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.
|
||||
if user.IsOrganization() {
|
||||
return false, fmt.Errorf("organization can't be added as assignee [user_id: %d, repo_id: %d]", user.ID, repo.ID)
|
||||
}
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -568,6 +580,7 @@ func CanBeAssigned(ctx context.Context, user *user_model.User, repo *repo_model.
|
||||
}
|
||||
|
||||
// HasAnyUnitAccess see the comment of "perm.HasAnyUnitAccess"
|
||||
// This helper only supports explicit user identities and does not resolve Actions task users.
|
||||
func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (bool, error) {
|
||||
var user *user_model.User
|
||||
var err error
|
||||
@@ -577,7 +590,7 @@ func HasAnyUnitAccess(ctx context.Context, userID int64, repo *repo_model.Reposi
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -625,13 +638,14 @@ func GetUserIDsWithUnitAccess(ctx context.Context, repo *repo_model.Repository,
|
||||
}
|
||||
|
||||
// CheckRepoUnitUser check whether user could visit the unit of this repository
|
||||
// This helper only supports explicit user identities and does not resolve Actions task users.
|
||||
func CheckRepoUnitUser(ctx context.Context, repo *repo_model.Repository, user *user_model.User, unitType unit.Type) bool {
|
||||
if user != nil && user.IsAdmin {
|
||||
return true
|
||||
}
|
||||
perm, err := GetUserRepoPermission(ctx, repo, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo, user)
|
||||
if err != nil {
|
||||
log.Error("GetUserRepoPermission: %w", err)
|
||||
log.Error("GetIndividualUserRepoPermission: %w", err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package access
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
perm_model "code.gitea.io/gitea/models/perm"
|
||||
@@ -157,8 +158,13 @@ func TestUnitAccessMode(t *testing.T) {
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.UnitAccessMode(unit.TypeWiki), "has unit, and map, use map")
|
||||
}
|
||||
|
||||
func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func TestGetRepoPermission(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
t.Run("GetIndividualUserRepoPermission", testGetIndividualUserRepoPermission)
|
||||
t.Run("GetDoerRepoPermission", testGetDoerRepoPermission)
|
||||
}
|
||||
|
||||
func testGetIndividualUserRepoPermission(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
repo32 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 32}) // org public repo
|
||||
require.NoError(t, repo32.LoadOwner(ctx))
|
||||
@@ -172,7 +178,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUser{OrgID: org.ID, TeamID: team.ID, UID: user.ID}))
|
||||
|
||||
t.Run("DoerInTeamWithNoRepo", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Nil(t, perm.unitsMode) // doer in the team, but has no access to the repo
|
||||
@@ -181,7 +187,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo32.ID}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
|
||||
t.Run("DoerWithTeamUnitAccessNone", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.unitsMode[unit.TypeCode])
|
||||
@@ -191,7 +197,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeWrite}))
|
||||
t.Run("DoerWithTeamUnitAccessWrite", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo32, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo32, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeRead, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||
@@ -204,7 +210,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.TruncateBeans(ctx, &organization.TeamUnit{}, &Access{})) // The user has access set of that repo, remove it, it is useless for our test
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamRepo{OrgID: org.ID, TeamID: team.ID, RepoID: repo3.ID}))
|
||||
t.Run("DoerWithNoopTeamOnPrivateRepo", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo3, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo3, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||
@@ -214,7 +220,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeCode, AccessMode: perm_model.AccessModeNone}))
|
||||
require.NoError(t, db.Insert(ctx, &organization.TeamUnit{OrgID: org.ID, TeamID: team.ID, Type: unit.TypeIssues, AccessMode: perm_model.AccessModeRead}))
|
||||
t.Run("DoerWithReadIssueTeamOnPrivateRepo", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo3, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo3, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeNone, perm.unitsMode[unit.TypeCode])
|
||||
@@ -233,7 +239,7 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
require.NoError(t, db.Insert(ctx, repo_model.Collaboration{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||
require.NoError(t, db.Insert(ctx, Access{RepoID: repo3.ID, UserID: user.ID, Mode: perm_model.AccessModeWrite}))
|
||||
t.Run("DoerWithReadIssueTeamAndWriteCollaboratorOnPrivateRepo", func(t *testing.T) {
|
||||
perm, err := GetUserRepoPermission(ctx, repo3, user)
|
||||
perm, err := GetIndividualUserRepoPermission(ctx, repo3, user)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.AccessMode)
|
||||
assert.Equal(t, perm_model.AccessModeWrite, perm.unitsMode[unit.TypeCode])
|
||||
@@ -245,3 +251,25 @@ func TestGetUserRepoPermission(t *testing.T) {
|
||||
assert.Equal(t, user.ID, users[0].ID)
|
||||
})
|
||||
}
|
||||
|
||||
func testGetDoerRepoPermission(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
repo4 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
task47 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
|
||||
actionsDoer := user_model.NewActionsUserWithTaskID(task47.ID)
|
||||
regularUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
actionsPerm, err := GetDoerRepoPermission(ctx, repo4, actionsDoer)
|
||||
require.NoError(t, err)
|
||||
directPerm, err := GetActionsUserRepoPermission(ctx, repo4, actionsDoer, task47.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, directPerm, actionsPerm)
|
||||
|
||||
doerPerm, err := GetDoerRepoPermission(ctx, repo1, regularUser)
|
||||
require.NoError(t, err)
|
||||
individualPerm, err := GetIndividualUserRepoPermission(ctx, repo1, regularUser)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, individualPerm, doerPerm)
|
||||
}
|
||||
|
||||
@@ -227,7 +227,11 @@ func (cfg *ProjectsConfig) IsProjectsAllowed(m ProjectsMode) bool {
|
||||
func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
|
||||
switch colName {
|
||||
case "type":
|
||||
r.Type = unit.Type(db.Cell2Int64(val))
|
||||
var err error
|
||||
r.Type, _, err = db.CellToInt(val, unit.TypeInvalid)
|
||||
if err != nil {
|
||||
setting.PanicInDevOrTesting("Unable to convert repo unit (id=%d) type: %v", r.ID, err)
|
||||
}
|
||||
switch r.Type {
|
||||
case unit.TypeExternalWiki:
|
||||
r.Config = new(ExternalWikiConfig)
|
||||
|
||||
@@ -5,44 +5,61 @@ package actions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// Artifacts using the v4 backend are stored as a single combined zip file per artifact on the backend
|
||||
// The v4 backend ensures ContentEncoding is set to "application/zip", which is not the case for the old backend
|
||||
// IsArtifactV4 detects whether the artifact is likely from v4.
|
||||
// V4 backend stores the files as a single combined zip file per artifact, and ensures ContentEncoding contains a slash
|
||||
// (otherwise this uses application/zip instead of the custom mime type), which is not the case for the old backend.
|
||||
func IsArtifactV4(art *actions_model.ActionArtifact) bool {
|
||||
return art.ArtifactName+".zip" == art.ArtifactPath && art.ContentEncoding == "application/zip"
|
||||
return strings.Contains(art.ContentEncodingOrType, "/")
|
||||
}
|
||||
|
||||
func DownloadArtifactV4ServeDirectOnly(ctx *context.Base, art *actions_model.ActionArtifact) (bool, error) {
|
||||
if setting.Actions.ArtifactStorage.ServeDirect() {
|
||||
u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, ctx.Req.Method, nil)
|
||||
if u != nil && err == nil {
|
||||
ctx.Redirect(u.String(), http.StatusFound)
|
||||
return true, nil
|
||||
}
|
||||
func GetArtifactV4ServeDirectURL(art *actions_model.ActionArtifact, method string) (string, error) {
|
||||
contentType := art.ContentEncodingOrType
|
||||
u, err := storage.ActionsArtifacts.ServeDirectURL(art.StoragePath, art.ArtifactPath, method, &storage.ServeDirectOptions{ContentType: contentType})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return false, nil
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func DownloadArtifactV4Fallback(ctx *context.Base, art *actions_model.ActionArtifact) error {
|
||||
func DownloadArtifactV4ServeDirect(ctx *context.Base, art *actions_model.ActionArtifact) bool {
|
||||
if !setting.Actions.ArtifactStorage.ServeDirect() {
|
||||
return false
|
||||
}
|
||||
u, err := GetArtifactV4ServeDirectURL(art, ctx.Req.Method)
|
||||
if err != nil {
|
||||
log.Error("GetArtifactV4ServeDirectURL: %v", err)
|
||||
return false
|
||||
}
|
||||
ctx.Redirect(u, http.StatusFound)
|
||||
return true
|
||||
}
|
||||
|
||||
func DownloadArtifactV4ReadStorage(ctx *context.Base, art *actions_model.ActionArtifact) error {
|
||||
f, err := storage.ActionsArtifacts.Open(art.StoragePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
http.ServeContent(ctx.Resp, ctx.Req, art.ArtifactName+".zip", art.CreatedUnix.AsLocalTime(), f)
|
||||
httplib.ServeUserContentByFile(ctx.Req, ctx.Resp, f, httplib.ServeHeaderOptions{
|
||||
Filename: art.ArtifactPath,
|
||||
ContentType: art.ContentEncodingOrType, // v4 guarantees that the field is Content-Type
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func DownloadArtifactV4(ctx *context.Base, art *actions_model.ActionArtifact) error {
|
||||
ok, err := DownloadArtifactV4ServeDirectOnly(ctx, art)
|
||||
if ok || err != nil {
|
||||
return err
|
||||
if DownloadArtifactV4ServeDirect(ctx, art) {
|
||||
return nil
|
||||
}
|
||||
return DownloadArtifactV4Fallback(ctx, art)
|
||||
return DownloadArtifactV4ReadStorage(ctx, art)
|
||||
}
|
||||
|
||||
@@ -22,16 +22,16 @@ import (
|
||||
var catFileBatchDebugWaitClose atomic.Int64
|
||||
|
||||
type catFileBatchCommunicator struct {
|
||||
cancel context.CancelFunc
|
||||
closeFunc func(err error)
|
||||
reqWriter io.Writer
|
||||
respReader *bufio.Reader
|
||||
debugGitCmd *gitcmd.Command
|
||||
}
|
||||
|
||||
func (b *catFileBatchCommunicator) Close() {
|
||||
if b.cancel != nil {
|
||||
b.cancel()
|
||||
b.cancel = nil
|
||||
if b.closeFunc != nil {
|
||||
b.closeFunc(nil)
|
||||
b.closeFunc = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,10 +47,19 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co
|
||||
}
|
||||
stdPipeClose()
|
||||
}
|
||||
closeFunc := func(err error) {
|
||||
ctxCancel(err)
|
||||
pipeClose()
|
||||
}
|
||||
return newCatFileBatchWithCloseFunc(ctx, repoPath, cmdCatFile, stdinWriter, stdoutReader, closeFunc)
|
||||
}
|
||||
|
||||
ret = &catFileBatchCommunicator{
|
||||
func newCatFileBatchWithCloseFunc(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Command,
|
||||
stdinWriter gitcmd.PipeWriter, stdoutReader gitcmd.PipeReader, closeFunc func(err error),
|
||||
) *catFileBatchCommunicator {
|
||||
ret := &catFileBatchCommunicator{
|
||||
debugGitCmd: cmdCatFile,
|
||||
cancel: func() { ctxCancel(nil) },
|
||||
closeFunc: closeFunc,
|
||||
reqWriter: stdinWriter,
|
||||
respReader: bufio.NewReaderSize(stdoutReader, 32*1024), // use a buffered reader for rich operations
|
||||
}
|
||||
@@ -60,8 +69,7 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co
|
||||
log.Error("Unable to start git command %v: %v", cmdCatFile.LogString(), err)
|
||||
// ideally here it should return the error, but it would require refactoring all callers
|
||||
// so just return a dummy communicator that does nothing, almost the same behavior as before, not bad
|
||||
ctxCancel(err)
|
||||
pipeClose()
|
||||
closeFunc(err)
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -70,8 +78,7 @@ func newCatFileBatch(ctx context.Context, repoPath string, cmdCatFile *gitcmd.Co
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
log.Error("cat-file --batch command failed in repo %s, error: %v", repoPath, err)
|
||||
}
|
||||
ctxCancel(err)
|
||||
pipeClose()
|
||||
closeFunc(err)
|
||||
}()
|
||||
|
||||
return ret
|
||||
|
||||
@@ -12,7 +12,12 @@ import (
|
||||
|
||||
func newHTTPServer(network, address, name string, handler http.Handler) (*Server, ServeFunction) {
|
||||
server := NewServer(network, address, name)
|
||||
protocols := http.Protocols{}
|
||||
protocols.SetHTTP1(true)
|
||||
protocols.SetHTTP2(true) // HTTP/2 can only be used when Gitea is configured to use TLS
|
||||
protocols.SetUnencryptedHTTP2(true) // Allow HTTP/2 without TLS, in case Gitea is behind a reverse proxy
|
||||
httpServer := http.Server{
|
||||
Protocols: &protocols,
|
||||
Handler: handler,
|
||||
BaseContext: func(net.Listener) context.Context { return GetManager().HammerContext() },
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package httplib
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
type ContentDispositionType string
|
||||
|
||||
const (
|
||||
ContentDispositionInline ContentDispositionType = "inline"
|
||||
ContentDispositionAttachment ContentDispositionType = "attachment"
|
||||
)
|
||||
|
||||
func needsEncodingRune(b rune) bool {
|
||||
return (b < ' ' || b > '~') && b != '\t'
|
||||
}
|
||||
|
||||
// getSafeName replaces all invalid chars in the filename field by underscore
|
||||
func getSafeName(s string) (_ string, needsEncoding bool) {
|
||||
var out strings.Builder
|
||||
for _, b := range s {
|
||||
if needsEncodingRune(b) {
|
||||
needsEncoding = true
|
||||
out.WriteRune('_')
|
||||
} else {
|
||||
out.WriteRune(b)
|
||||
}
|
||||
}
|
||||
return out.String(), needsEncoding
|
||||
}
|
||||
|
||||
func EncodeContentDispositionAttachment(filename string) string {
|
||||
return encodeContentDisposition(ContentDispositionAttachment, filename)
|
||||
}
|
||||
|
||||
func EncodeContentDispositionInline(filename string) string {
|
||||
return encodeContentDisposition(ContentDispositionInline, filename)
|
||||
}
|
||||
|
||||
// encodeContentDisposition encodes a correct Content-Disposition Header
|
||||
func encodeContentDisposition(t ContentDispositionType, filename string) string {
|
||||
safeFilename, needsEncoding := getSafeName(filename)
|
||||
result := mime.FormatMediaType(string(t), map[string]string{"filename": safeFilename})
|
||||
// No need for the utf8 encoding
|
||||
if !needsEncoding {
|
||||
return result
|
||||
}
|
||||
utf8Result := mime.FormatMediaType(string(t), map[string]string{"filename": filename})
|
||||
|
||||
// The mime package might have unexpected results in other go versions
|
||||
// Make tests instance fail, otherwise use the default behavior of the go mime package
|
||||
if !strings.HasPrefix(result, string(t)+"; filename=") || !strings.HasPrefix(utf8Result, string(t)+"; filename*=") {
|
||||
setting.PanicInDevOrTesting("Unexpected mime package result %s", result)
|
||||
return utf8Result
|
||||
}
|
||||
|
||||
encodedFileName := strings.TrimPrefix(utf8Result, string(t))
|
||||
return result + encodedFileName
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package httplib
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContentDisposition(t *testing.T) {
|
||||
type testEntry struct {
|
||||
disposition ContentDispositionType
|
||||
filename string
|
||||
header string
|
||||
}
|
||||
table := []testEntry{
|
||||
{disposition: ContentDispositionInline, filename: "test.txt", header: "inline; filename=test.txt"},
|
||||
{disposition: ContentDispositionInline, filename: "test❌.txt", header: "inline; filename=test_.txt; filename*=utf-8''test%E2%9D%8C.txt"},
|
||||
{disposition: ContentDispositionInline, filename: "test ❌.txt", header: "inline; filename=\"test _.txt\"; filename*=utf-8''test%20%E2%9D%8C.txt"},
|
||||
{disposition: ContentDispositionInline, filename: "\"test.txt", header: "inline; filename=\"\\\"test.txt\""},
|
||||
{disposition: ContentDispositionInline, filename: "hello\tworld.txt", header: "inline; filename=\"hello\tworld.txt\""},
|
||||
{disposition: ContentDispositionAttachment, filename: "hello\tworld.txt", header: "attachment; filename=\"hello\tworld.txt\""},
|
||||
{disposition: ContentDispositionAttachment, filename: "hello\nworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Aworld.txt"},
|
||||
{disposition: ContentDispositionAttachment, filename: "hello\rworld.txt", header: "attachment; filename=hello_world.txt; filename*=utf-8''hello%0Dworld.txt"},
|
||||
}
|
||||
|
||||
// Check the needsEncodingRune replacer ranges except tab that is checked above
|
||||
// Any change in behavior should fail here
|
||||
for c := ' '; !needsEncodingRune(c); c++ {
|
||||
var header string
|
||||
switch {
|
||||
case strings.ContainsAny(string(c), ` (),/:;<=>?@[]`):
|
||||
header = "inline; filename=\"hello" + string(c) + "world.txt\""
|
||||
case strings.ContainsAny(string(c), `"\`):
|
||||
// This document advises against for backslash in quoted form:
|
||||
// https://datatracker.ietf.org/doc/html/rfc6266#appendix-D
|
||||
// However the mime package is not generating the filename* in this scenario
|
||||
header = "inline; filename=\"hello\\" + string(c) + "world.txt\""
|
||||
default:
|
||||
header = "inline; filename=hello" + string(c) + "world.txt"
|
||||
}
|
||||
table = append(table, testEntry{
|
||||
disposition: ContentDispositionInline,
|
||||
filename: "hello" + string(c) + "world.txt",
|
||||
header: header,
|
||||
})
|
||||
}
|
||||
|
||||
for _, entry := range table {
|
||||
t.Run(string(entry.disposition)+"_"+entry.filename, func(t *testing.T) {
|
||||
encoded := encodeContentDisposition(entry.disposition, entry.filename)
|
||||
assert.Equal(t, entry.header, encoded)
|
||||
disposition, params, err := mime.ParseMediaType(encoded)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(entry.disposition), disposition)
|
||||
assert.Equal(t, entry.filename, params["filename"])
|
||||
})
|
||||
}
|
||||
}
|
||||
+61
-73
@@ -8,10 +8,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -27,18 +26,19 @@ import (
|
||||
)
|
||||
|
||||
type ServeHeaderOptions struct {
|
||||
ContentType string // defaults to "application/octet-stream"
|
||||
ContentTypeCharset string
|
||||
ContentLength *int64
|
||||
Disposition string // defaults to "attachment"
|
||||
ContentType string // defaults to "application/octet-stream"
|
||||
ContentLength *int64
|
||||
|
||||
Filename string
|
||||
CacheIsPublic bool
|
||||
CacheDuration time.Duration // defaults to 5 minutes
|
||||
LastModified time.Time
|
||||
ContentDisposition ContentDispositionType
|
||||
|
||||
CacheIsPublic bool
|
||||
CacheDuration time.Duration // defaults to 5 minutes
|
||||
LastModified time.Time
|
||||
}
|
||||
|
||||
// ServeSetHeaders sets necessary content serve headers
|
||||
func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
|
||||
func ServeSetHeaders(w http.ResponseWriter, opts ServeHeaderOptions) {
|
||||
header := w.Header()
|
||||
|
||||
skipCompressionExts := container.SetOf(".gz", ".bz2", ".zip", ".xz", ".zst", ".deb", ".apk", ".jar", ".png", ".jpg", ".webp")
|
||||
@@ -46,14 +46,7 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
|
||||
w.Header().Add(gzhttp.HeaderNoCompression, "1")
|
||||
}
|
||||
|
||||
contentType := typesniffer.MimeTypeApplicationOctetStream
|
||||
if opts.ContentType != "" {
|
||||
if opts.ContentTypeCharset != "" {
|
||||
contentType = opts.ContentType + "; charset=" + strings.ToLower(opts.ContentTypeCharset)
|
||||
} else {
|
||||
contentType = opts.ContentType
|
||||
}
|
||||
}
|
||||
contentType := util.IfZero(opts.ContentType, typesniffer.MimeTypeApplicationOctetStream)
|
||||
header.Set("Content-Type", contentType)
|
||||
header.Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
@@ -61,14 +54,18 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
|
||||
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10))
|
||||
}
|
||||
|
||||
if opts.Filename != "" {
|
||||
disposition := opts.Disposition
|
||||
if disposition == "" {
|
||||
disposition = "attachment"
|
||||
}
|
||||
// Disable script execution of HTML/SVG files, since we serve the file from the same origin as Gitea server
|
||||
header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
if strings.Contains(contentType, "application/pdf") {
|
||||
// no sandbox attribute for PDF as it breaks rendering in at least safari. this
|
||||
// should generally be safe as scripts inside PDF can not escape the PDF document
|
||||
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
|
||||
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context
|
||||
header.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
||||
}
|
||||
|
||||
backslashEscapedName := strings.ReplaceAll(strings.ReplaceAll(opts.Filename, `\`, `\\`), `"`, `\"`) // \ -> \\, " -> \"
|
||||
header.Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"; filename*=UTF-8''%s`, disposition, backslashEscapedName, url.PathEscape(opts.Filename)))
|
||||
if opts.Filename != "" && opts.ContentDisposition != "" {
|
||||
header.Set("Content-Disposition", encodeContentDisposition(opts.ContentDisposition, path.Base(opts.Filename)))
|
||||
header.Set("Access-Control-Expose-Headers", "Content-Disposition")
|
||||
}
|
||||
|
||||
@@ -84,49 +81,40 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// ServeData download file from io.Reader
|
||||
func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byte, opts *ServeHeaderOptions) {
|
||||
// do not set "Content-Length", because the length could only be set by callers, and it needs to support range requests
|
||||
sniffedType := typesniffer.DetectContentType(mineBuf)
|
||||
|
||||
// the "render" parameter came from year 2016: 638dd24c, it doesn't have clear meaning, so I think it could be removed later
|
||||
isPlain := sniffedType.IsText() || r.FormValue("render") != ""
|
||||
func serveSetHeadersByUserContent(w http.ResponseWriter, contentPrefetchBuf []byte, opts ServeHeaderOptions) {
|
||||
var detectCharset bool
|
||||
|
||||
if setting.MimeTypeMap.Enabled {
|
||||
fileExtension := strings.ToLower(filepath.Ext(opts.Filename))
|
||||
fileExtension := strings.ToLower(path.Ext(opts.Filename))
|
||||
opts.ContentType = setting.MimeTypeMap.Map[fileExtension]
|
||||
detectCharset = strings.HasPrefix(opts.ContentType, "text/") && !strings.Contains(opts.ContentType, "charset=")
|
||||
}
|
||||
|
||||
if opts.ContentType == "" {
|
||||
sniffedType := typesniffer.DetectContentType(contentPrefetchBuf)
|
||||
if sniffedType.IsBrowsableBinaryType() {
|
||||
opts.ContentType = sniffedType.GetMimeType()
|
||||
} else if isPlain {
|
||||
} else if sniffedType.IsText() {
|
||||
// intentionally do not render user's HTML content as a page, for safety, and avoid content spamming & abusing
|
||||
opts.ContentType = "text/plain"
|
||||
detectCharset = true
|
||||
} else {
|
||||
opts.ContentType = typesniffer.MimeTypeApplicationOctetStream
|
||||
}
|
||||
}
|
||||
|
||||
if isPlain {
|
||||
charset, _ := charsetModule.DetectEncoding(mineBuf)
|
||||
opts.ContentTypeCharset = strings.ToLower(charset)
|
||||
if detectCharset {
|
||||
if charset, _ := charsetModule.DetectEncoding(contentPrefetchBuf); charset != "" {
|
||||
opts.ContentType += "; charset=" + strings.ToLower(charset)
|
||||
}
|
||||
}
|
||||
|
||||
// serve types that can present a security risk with CSP
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox")
|
||||
|
||||
if sniffedType.IsPDF() {
|
||||
// no sandbox attribute for PDF as it breaks rendering in at least safari. this
|
||||
// should generally be safe as scripts inside PDF can not escape the PDF document
|
||||
// see https://bugs.chromium.org/p/chromium/issues/detail?id=413851 for more discussion
|
||||
// HINT: PDF-RENDER-SANDBOX: PDF won't render in sandboxed context
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'")
|
||||
}
|
||||
|
||||
// TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE
|
||||
opts.Disposition = "inline"
|
||||
if sniffedType.IsSvgImage() && !setting.UI.SVG.Enabled {
|
||||
opts.Disposition = "attachment"
|
||||
if opts.ContentDisposition == "" {
|
||||
sniffedType := typesniffer.FromContentType(opts.ContentType)
|
||||
opts.ContentDisposition = ContentDispositionInline
|
||||
if sniffedType.IsSvgImage() && !setting.UI.SVG.Enabled {
|
||||
opts.ContentDisposition = ContentDispositionAttachment
|
||||
}
|
||||
}
|
||||
|
||||
ServeSetHeaders(w, opts)
|
||||
@@ -134,7 +122,10 @@ func setServeHeadersByFile(r *http.Request, w http.ResponseWriter, mineBuf []byt
|
||||
|
||||
const mimeDetectionBufferLen = 1024
|
||||
|
||||
func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts *ServeHeaderOptions) {
|
||||
func ServeUserContentByReader(r *http.Request, w http.ResponseWriter, size int64, reader io.Reader, opts ServeHeaderOptions) {
|
||||
if opts.ContentLength != nil {
|
||||
panic("do not set ContentLength, use size argument instead")
|
||||
}
|
||||
buf := make([]byte, mimeDetectionBufferLen)
|
||||
n, err := util.ReadAtMost(reader, buf)
|
||||
if err != nil {
|
||||
@@ -144,7 +135,7 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re
|
||||
if n >= 0 {
|
||||
buf = buf[:n]
|
||||
}
|
||||
setServeHeadersByFile(r, w, buf, opts)
|
||||
serveSetHeadersByUserContent(w, buf, opts)
|
||||
|
||||
// reset the reader to the beginning
|
||||
reader = io.MultiReader(bytes.NewReader(buf), reader)
|
||||
@@ -198,32 +189,29 @@ func ServeContentByReader(r *http.Request, w http.ResponseWriter, size int64, re
|
||||
partialLength := end - start + 1
|
||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(partialLength, 10))
|
||||
if _, err = io.CopyN(io.Discard, reader, start); err != nil {
|
||||
http.Error(w, "serve content: unable to skip", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
if seeker, ok := reader.(io.Seeker); ok {
|
||||
if _, err = seeker.Seek(start, io.SeekStart); err != nil {
|
||||
http.Error(w, "serve content: unable to seek", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err = io.CopyN(io.Discard, reader, start); err != nil {
|
||||
http.Error(w, "serve content: unable to skip", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusPartialContent)
|
||||
_, _ = io.CopyN(w, reader, partialLength) // just like http.ServeContent, not necessary to handle the error
|
||||
}
|
||||
|
||||
func ServeContentByReadSeeker(r *http.Request, w http.ResponseWriter, modTime *time.Time, reader io.ReadSeeker, opts *ServeHeaderOptions) {
|
||||
buf := make([]byte, mimeDetectionBufferLen)
|
||||
n, err := util.ReadAtMost(reader, buf)
|
||||
func ServeUserContentByFile(r *http.Request, w http.ResponseWriter, file fs.File, opts ServeHeaderOptions) {
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
http.Error(w, "serve content: unable to read", http.StatusInternalServerError)
|
||||
http.Error(w, "unable to serve file, stat error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if _, err = reader.Seek(0, io.SeekStart); err != nil {
|
||||
http.Error(w, "serve content: unable to seek", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if n >= 0 {
|
||||
buf = buf[:n]
|
||||
}
|
||||
setServeHeadersByFile(r, w, buf, opts)
|
||||
if modTime == nil {
|
||||
modTime = &time.Time{}
|
||||
}
|
||||
http.ServeContent(w, r, opts.Filename, *modTime, reader)
|
||||
opts.LastModified = info.ModTime()
|
||||
ServeUserContentByReader(r, w, info.Size(), file, opts)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServeContentByReader(t *testing.T) {
|
||||
func TestServeUserContentByReader(t *testing.T) {
|
||||
data := "0123456789abcdef"
|
||||
|
||||
test := func(t *testing.T, expectedStatusCode int, expectedContent string) {
|
||||
@@ -27,7 +27,7 @@ func TestServeContentByReader(t *testing.T) {
|
||||
}
|
||||
reader := strings.NewReader(data)
|
||||
w := httptest.NewRecorder()
|
||||
ServeContentByReader(r, w, int64(len(data)), reader, &ServeHeaderOptions{})
|
||||
ServeUserContentByReader(r, w, int64(len(data)), reader, ServeHeaderOptions{})
|
||||
assert.Equal(t, expectedStatusCode, w.Code)
|
||||
if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
|
||||
assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length"))
|
||||
@@ -58,7 +58,7 @@ func TestServeContentByReader(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestServeContentByReadSeeker(t *testing.T) {
|
||||
func TestServeUserContentByFile(t *testing.T) {
|
||||
data := "0123456789abcdef"
|
||||
tmpFile := t.TempDir() + "/test"
|
||||
err := os.WriteFile(tmpFile, []byte(data), 0o644)
|
||||
@@ -76,7 +76,7 @@ func TestServeContentByReadSeeker(t *testing.T) {
|
||||
defer seekReader.Close()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
ServeContentByReadSeeker(r, w, nil, seekReader, &ServeHeaderOptions{})
|
||||
ServeUserContentByFile(r, w, seekReader, ServeHeaderOptions{})
|
||||
assert.Equal(t, expectedStatusCode, w.Code)
|
||||
if expectedStatusCode == http.StatusPartialContent || expectedStatusCode == http.StatusOK {
|
||||
assert.Equal(t, strconv.Itoa(len(expectedContent)), w.Header().Get("Content-Length"))
|
||||
|
||||
@@ -104,7 +104,7 @@ func (s *ContentStore) Verify(pointer Pointer) (bool, error) {
|
||||
}
|
||||
|
||||
// ReadMetaObject will read a git_model.LFSMetaObject and return a reader
|
||||
func ReadMetaObject(pointer Pointer) (io.ReadSeekCloser, error) {
|
||||
func ReadMetaObject(pointer Pointer) (storage.Object, error) {
|
||||
contentStore := NewContentStore()
|
||||
return contentStore.Get(pointer)
|
||||
}
|
||||
|
||||
Vendored
+5
-6
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
@@ -61,19 +62,17 @@ func (p *openAPIRenderer) Render(ctx *markup.RenderContext, input io.Reader, out
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="%s/assets/css/swagger.css?v=%s">
|
||||
<link rel="stylesheet" href="%s">
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"><textarea class="swagger-spec-content" data-spec-filename="%s">%s</textarea></div>
|
||||
<script src="%s/assets/js/swagger.js?v=%s"></script>
|
||||
<script type="module" src="%s"></script>
|
||||
</body>
|
||||
</html>`,
|
||||
setting.StaticURLPrefix,
|
||||
setting.AssetVersion,
|
||||
public.AssetURI("css/swagger.css"),
|
||||
html.EscapeString(ctx.RenderOptions.RelativePath),
|
||||
html.EscapeString(util.UnsafeBytesToString(content)),
|
||||
setting.StaticURLPrefix,
|
||||
setting.AssetVersion,
|
||||
public.AssetURI("js/swagger.js"),
|
||||
))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/markup/internal"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@@ -237,10 +238,10 @@ func RenderWithRenderer(ctx *RenderContext, renderer Renderer, input io.Reader,
|
||||
return renderIFrame(ctx, extOpts.ContentSandbox, output)
|
||||
}
|
||||
// else: this is a standalone page, fallthrough to the real rendering, and add extra JS/CSS
|
||||
extraStyleHref := setting.AppSubURL + "/assets/css/external-render-iframe.css"
|
||||
extraScriptSrc := setting.AppSubURL + "/assets/js/external-render-iframe.js"
|
||||
extraStyleHref := public.AssetURI("css/external-render-iframe.css")
|
||||
extraScriptSrc := public.AssetURI("js/external-render-iframe.js")
|
||||
// "<script>" must go before "<link>", to make Golang's http.DetectContentType() can still recognize the content as "text/html"
|
||||
extraHeadHTML = htmlutil.HTMLFormat(`<script src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraStyleHref)
|
||||
extraHeadHTML = htmlutil.HTMLFormat(`<script type="module" src="%s"></script><link rel="stylesheet" href="%s">`, extraScriptSrc, extraStyleHref)
|
||||
}
|
||||
|
||||
ctx.usedByRender = true
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package public
|
||||
|
||||
import (
|
||||
"io"
|
||||
"path"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
type manifestEntry struct {
|
||||
File string `json:"file"`
|
||||
Name string `json:"name"`
|
||||
IsEntry bool `json:"isEntry"`
|
||||
CSS []string `json:"css"`
|
||||
}
|
||||
|
||||
type manifestDataStruct struct {
|
||||
paths map[string]string // unhashed path -> hashed path
|
||||
names map[string]string // hashed path -> entry name
|
||||
modTime int64
|
||||
checkTime time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
manifestData atomic.Pointer[manifestDataStruct]
|
||||
manifestFS = sync.OnceValue(AssetFS)
|
||||
)
|
||||
|
||||
const manifestPath = "assets/.vite/manifest.json"
|
||||
|
||||
func parseManifest(data []byte) (map[string]string, map[string]string) {
|
||||
var manifest map[string]manifestEntry
|
||||
if err := json.Unmarshal(data, &manifest); err != nil {
|
||||
log.Error("Failed to parse frontend manifest: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
paths := make(map[string]string)
|
||||
names := make(map[string]string)
|
||||
for _, entry := range manifest {
|
||||
if !entry.IsEntry || entry.Name == "" {
|
||||
continue
|
||||
}
|
||||
// Build unhashed key from file path: "js/index.js", "css/theme-gitea-dark.css"
|
||||
dir := path.Dir(entry.File)
|
||||
ext := path.Ext(entry.File)
|
||||
key := dir + "/" + entry.Name + ext
|
||||
paths[key] = entry.File
|
||||
names[entry.File] = entry.Name
|
||||
// Map associated CSS files, e.g. "css/index.css" -> "css/index.B3zrQPqD.css"
|
||||
for _, css := range entry.CSS {
|
||||
cssKey := path.Dir(css) + "/" + entry.Name + path.Ext(css)
|
||||
paths[cssKey] = css
|
||||
names[css] = entry.Name
|
||||
}
|
||||
}
|
||||
return paths, names
|
||||
}
|
||||
|
||||
func reloadManifest(existingData *manifestDataStruct) *manifestDataStruct {
|
||||
now := time.Now()
|
||||
data := existingData
|
||||
if data != nil && now.Sub(data.checkTime) < time.Second {
|
||||
// a single request triggers multiple calls to getHashedPath
|
||||
// do not check the manifest file too frequently
|
||||
return data
|
||||
}
|
||||
|
||||
f, err := manifestFS().Open(manifestPath)
|
||||
if err != nil {
|
||||
log.Error("Failed to open frontend manifest: %v", err)
|
||||
return data
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
log.Error("Failed to stat frontend manifest: %v", err)
|
||||
return data
|
||||
}
|
||||
|
||||
needReload := data == nil || fi.ModTime().UnixNano() != data.modTime
|
||||
if !needReload {
|
||||
return data
|
||||
}
|
||||
manifestContent, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
log.Error("Failed to read frontend manifest: %v", err)
|
||||
return data
|
||||
}
|
||||
return storeManifestFromBytes(manifestContent, fi.ModTime().UnixNano(), now)
|
||||
}
|
||||
|
||||
func storeManifestFromBytes(manifestContent []byte, modTime int64, checkTime time.Time) *manifestDataStruct {
|
||||
paths, names := parseManifest(manifestContent)
|
||||
data := &manifestDataStruct{
|
||||
paths: paths,
|
||||
names: names,
|
||||
modTime: modTime,
|
||||
checkTime: checkTime,
|
||||
}
|
||||
manifestData.Store(data)
|
||||
return data
|
||||
}
|
||||
|
||||
func getManifestData() *manifestDataStruct {
|
||||
data := manifestData.Load()
|
||||
|
||||
// In production the manifest is immutable (embedded in the binary).
|
||||
// In dev mode, check if it changed on disk (for watch-frontend).
|
||||
if data == nil || !setting.IsProd {
|
||||
data = reloadManifest(data)
|
||||
}
|
||||
if data == nil {
|
||||
data = &manifestDataStruct{}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// getHashedPath resolves an unhashed asset path (origin path) to its content-hashed path from the frontend manifest.
|
||||
// Example: getHashedPath("js/index.js") returns "js/index.C6Z2MRVQ.js"
|
||||
// Falls back to returning the input path unchanged if the manifest is unavailable.
|
||||
func getHashedPath(originPath string) string {
|
||||
data := getManifestData()
|
||||
if p, ok := data.paths[originPath]; ok {
|
||||
return p
|
||||
}
|
||||
return originPath
|
||||
}
|
||||
|
||||
// AssetURI returns the URI for a frontend asset.
|
||||
// It may return a relative path or a full URL depending on the StaticURLPrefix setting.
|
||||
// In Vite dev mode, known entry points are mapped to their source paths
|
||||
// so the reverse proxy serves them from the Vite dev server.
|
||||
// In production, it resolves the content-hashed path from the manifest.
|
||||
func AssetURI(originPath string) string {
|
||||
if src := viteDevSourceURL(originPath); src != "" {
|
||||
return src
|
||||
}
|
||||
return setting.StaticURLPrefix + "/assets/" + getHashedPath(originPath)
|
||||
}
|
||||
|
||||
// AssetNameFromHashedPath returns the asset entry name for a given hashed asset path.
|
||||
// Example: returns "theme-gitea-dark" for "css/theme-gitea-dark.CyAaQnn5.css".
|
||||
// Returns empty string if the path is not found in the manifest.
|
||||
func AssetNameFromHashedPath(hashedPath string) string {
|
||||
return getManifestData().names[hashedPath]
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package public
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestViteManifest(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.IsProd, true)()
|
||||
|
||||
const testManifest = `{
|
||||
"web_src/js/index.ts": {
|
||||
"file": "js/index.C6Z2MRVQ.js",
|
||||
"name": "index",
|
||||
"src": "web_src/js/index.ts",
|
||||
"isEntry": true,
|
||||
"css": ["css/index.B3zrQPqD.css"]
|
||||
},
|
||||
"web_src/js/standalone/swagger.ts": {
|
||||
"file": "js/swagger.SujiEmYM.js",
|
||||
"name": "swagger",
|
||||
"src": "web_src/js/standalone/swagger.ts",
|
||||
"isEntry": true,
|
||||
"css": ["css/swagger._-APWT_3.css"]
|
||||
},
|
||||
"web_src/css/themes/theme-gitea-dark.css": {
|
||||
"file": "css/theme-gitea-dark.CyAaQnn5.css",
|
||||
"name": "theme-gitea-dark",
|
||||
"src": "web_src/css/themes/theme-gitea-dark.css",
|
||||
"isEntry": true
|
||||
},
|
||||
"web_src/js/features/eventsource.sharedworker.ts": {
|
||||
"file": "js/eventsource.sharedworker.Dug1twio.js",
|
||||
"name": "eventsource.sharedworker",
|
||||
"src": "web_src/js/features/eventsource.sharedworker.ts",
|
||||
"isEntry": true
|
||||
},
|
||||
"_chunk.js": {
|
||||
"file": "js/chunk.abc123.js",
|
||||
"name": "chunk"
|
||||
}
|
||||
}`
|
||||
|
||||
t.Run("EmptyManifest", func(t *testing.T) {
|
||||
storeManifestFromBytes([]byte(``), 0, time.Now())
|
||||
assert.Equal(t, "/assets/js/index.js", AssetURI("js/index.js"))
|
||||
assert.Equal(t, "/assets/css/theme-gitea-dark.css", AssetURI("css/theme-gitea-dark.css"))
|
||||
assert.Equal(t, "", AssetNameFromHashedPath("css/no-such-file.css"))
|
||||
})
|
||||
|
||||
t.Run("ParseManifest", func(t *testing.T) {
|
||||
storeManifestFromBytes([]byte(testManifest), 0, time.Now())
|
||||
paths, names := manifestData.Load().paths, manifestData.Load().names
|
||||
|
||||
// JS entries
|
||||
assert.Equal(t, "js/index.C6Z2MRVQ.js", paths["js/index.js"])
|
||||
assert.Equal(t, "js/swagger.SujiEmYM.js", paths["js/swagger.js"])
|
||||
assert.Equal(t, "js/eventsource.sharedworker.Dug1twio.js", paths["js/eventsource.sharedworker.js"])
|
||||
|
||||
// Associated CSS from JS entries
|
||||
assert.Equal(t, "css/index.B3zrQPqD.css", paths["css/index.css"])
|
||||
assert.Equal(t, "css/swagger._-APWT_3.css", paths["css/swagger.css"])
|
||||
|
||||
// CSS-only entries
|
||||
assert.Equal(t, "css/theme-gitea-dark.CyAaQnn5.css", paths["css/theme-gitea-dark.css"])
|
||||
|
||||
// Non-entry chunks should not be included
|
||||
assert.Empty(t, paths["js/chunk.js"])
|
||||
|
||||
// Names: hashed path -> entry name
|
||||
assert.Equal(t, "index", names["js/index.C6Z2MRVQ.js"])
|
||||
assert.Equal(t, "index", names["css/index.B3zrQPqD.css"])
|
||||
assert.Equal(t, "swagger", names["js/swagger.SujiEmYM.js"])
|
||||
assert.Equal(t, "swagger", names["css/swagger._-APWT_3.css"])
|
||||
assert.Equal(t, "theme-gitea-dark", names["css/theme-gitea-dark.CyAaQnn5.css"])
|
||||
assert.Equal(t, "eventsource.sharedworker", names["js/eventsource.sharedworker.Dug1twio.js"])
|
||||
|
||||
// Test Asset related functions
|
||||
assert.Equal(t, "/assets/js/index.C6Z2MRVQ.js", AssetURI("js/index.js"))
|
||||
assert.Equal(t, "/assets/css/theme-gitea-dark.CyAaQnn5.css", AssetURI("css/theme-gitea-dark.css"))
|
||||
assert.Equal(t, "theme-gitea-dark", AssetNameFromHashedPath("css/theme-gitea-dark.CyAaQnn5.css"))
|
||||
})
|
||||
}
|
||||
@@ -4,31 +4,36 @@
|
||||
package public
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`, see the comment of DetectWellKnownMimeType
|
||||
var wellKnownMimeTypesLower = map[string]string{
|
||||
".avif": "image/avif",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".gif": "image/gif",
|
||||
".htm": "text/html; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".pdf": "application/pdf",
|
||||
".png": "image/png",
|
||||
".svg": "image/svg+xml",
|
||||
".wasm": "application/wasm",
|
||||
".webp": "image/webp",
|
||||
".xml": "text/xml; charset=utf-8",
|
||||
// wellKnownMimeTypesLower comes from Golang's builtin mime package: `builtinTypesLower`,
|
||||
// see the comment of DetectWellKnownMimeType
|
||||
var wellKnownMimeTypesLower = sync.OnceValue(func() map[string]string {
|
||||
return map[string]string{
|
||||
".avif": "image/avif",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".gif": "image/gif",
|
||||
".htm": "text/html; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".jpeg": "image/jpeg",
|
||||
".jpg": "image/jpeg",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".pdf": "application/pdf",
|
||||
".png": "image/png",
|
||||
".svg": "image/svg+xml",
|
||||
".wasm": "application/wasm",
|
||||
".webp": "image/webp",
|
||||
".xml": "text/xml; charset=utf-8",
|
||||
|
||||
// well, there are some types missing from the builtin list
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
}
|
||||
// well, there are some types missing from the builtin list
|
||||
".txt": "text/plain; charset=utf-8",
|
||||
}
|
||||
})
|
||||
|
||||
// DetectWellKnownMimeType will return the mime-type for a well-known file ext name
|
||||
// The purpose of this function is to bypass the unstable behavior of Golang's mime.TypeByExtension
|
||||
@@ -38,5 +43,8 @@ var wellKnownMimeTypesLower = map[string]string{
|
||||
// DetectWellKnownMimeType makes the Content-Type for well-known files stable.
|
||||
func DetectWellKnownMimeType(ext string) string {
|
||||
ext = strings.ToLower(ext)
|
||||
return wellKnownMimeTypesLower[ext]
|
||||
if s, ok := wellKnownMimeTypesLower()[ext]; ok {
|
||||
return s
|
||||
}
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package public
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/routing"
|
||||
)
|
||||
|
||||
const viteDevPortFile = "public/assets/.vite/dev-port"
|
||||
|
||||
var viteDevProxy atomic.Pointer[httputil.ReverseProxy]
|
||||
|
||||
func getViteDevProxy() *httputil.ReverseProxy {
|
||||
if proxy := viteDevProxy.Load(); proxy != nil {
|
||||
return proxy
|
||||
}
|
||||
|
||||
portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile)
|
||||
data, err := os.ReadFile(portFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
port := strings.TrimSpace(string(data))
|
||||
if port == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
target, err := url.Parse("http://localhost:" + port)
|
||||
if err != nil {
|
||||
log.Error("Failed to parse Vite dev server URL: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// there is a strange error log (from Golang's HTTP package)
|
||||
// 2026/03/28 19:50:13 modules/log/misc.go:72:(*loggerToWriter).Write() [I] Unsolicited response received on idle HTTP channel starting with "HTTP/1.1 400 Bad Request\r\n\r\n"; err=<nil>
|
||||
// maybe it is caused by that the Vite dev server doesn't support keep-alive connections? or different keep-alive timeouts?
|
||||
transport := &http.Transport{
|
||||
IdleConnTimeout: 5 * time.Second,
|
||||
ResponseHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
log.Info("Proxying Vite dev server requests to %s", target)
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Transport: transport,
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.SetURL(target)
|
||||
r.Out.Host = target.Host
|
||||
},
|
||||
ModifyResponse: func(resp *http.Response) error {
|
||||
// add a header to indicate the Vite dev server port,
|
||||
// make developers know that this request is proxied to Vite dev server and which port it is
|
||||
resp.Header.Add("X-Gitea-Vite-Port", port)
|
||||
return nil
|
||||
},
|
||||
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
log.Error("Error proxying to Vite dev server: %v", err)
|
||||
http.Error(w, "Error proxying to Vite dev server: "+err.Error(), http.StatusBadGateway)
|
||||
},
|
||||
}
|
||||
viteDevProxy.Store(proxy)
|
||||
return proxy
|
||||
}
|
||||
|
||||
// ViteDevMiddleware proxies matching requests to the Vite dev server.
|
||||
// It is registered as middleware in non-production mode and lazily discovers
|
||||
// the Vite dev server port from the port file written by the viteDevServerPortPlugin.
|
||||
// It is needed because there are container-based development, only Gitea web server's port is exposed.
|
||||
func ViteDevMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
if !isViteDevRequest(req) {
|
||||
next.ServeHTTP(resp, req)
|
||||
return
|
||||
}
|
||||
proxy := getViteDevProxy()
|
||||
if proxy == nil {
|
||||
next.ServeHTTP(resp, req)
|
||||
return
|
||||
}
|
||||
routing.MarkLongPolling(resp, req)
|
||||
proxy.ServeHTTP(resp, req)
|
||||
})
|
||||
}
|
||||
|
||||
// isViteDevMode returns true if the Vite dev server port file exists.
|
||||
// In production mode, the result is cached after the first check.
|
||||
func isViteDevMode() bool {
|
||||
if setting.IsProd {
|
||||
return false
|
||||
}
|
||||
portFile := filepath.Join(setting.StaticRootPath, viteDevPortFile)
|
||||
_, err := os.Stat(portFile)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func viteDevSourceURL(name string) string {
|
||||
if !isViteDevMode() {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(name, "css/theme-") {
|
||||
// Only redirect built-in themes to Vite source; custom themes are served from custom/public/assets/css/
|
||||
themeFile := strings.TrimPrefix(name, "css/")
|
||||
srcPath := filepath.Join(setting.StaticRootPath, "web_src/css/themes", themeFile)
|
||||
if _, err := os.Stat(srcPath); err == nil {
|
||||
return setting.AppSubURL + "/web_src/css/themes/" + themeFile
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(name, "css/") {
|
||||
return setting.AppSubURL + "/web_src/" + name
|
||||
}
|
||||
if name == "js/eventsource.sharedworker.js" {
|
||||
return setting.AppSubURL + "/web_src/js/features/eventsource.sharedworker.ts"
|
||||
}
|
||||
if name == "js/iife.js" {
|
||||
return setting.AppSubURL + "/web_src/js/__vite_iife.js"
|
||||
}
|
||||
if name == "js/index.js" {
|
||||
return setting.AppSubURL + "/web_src/js/index.ts"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isViteDevRequest returns true if the request should be proxied to the Vite dev server.
|
||||
// Ref: Vite source packages/vite/src/node/constants.ts and packages/vite/src/shared/constants.ts
|
||||
func isViteDevRequest(req *http.Request) bool {
|
||||
if req.Header.Get("Upgrade") == "websocket" {
|
||||
wsProtocol := req.Header.Get("Sec-WebSocket-Protocol")
|
||||
return wsProtocol == "vite-hmr" || wsProtocol == "vite-ping"
|
||||
}
|
||||
path := req.URL.Path
|
||||
|
||||
// vite internal requests
|
||||
if strings.HasPrefix(path, "/@vite/") /* HMR client */ ||
|
||||
strings.HasPrefix(path, "/@fs/") /* out-of-root file access, see vite.config.ts: fs.allow */ ||
|
||||
strings.HasPrefix(path, "/@id/") /* virtual modules */ {
|
||||
return true
|
||||
}
|
||||
|
||||
// local source requests (VITE-DEV-SERVER-SECURITY: don't serve sensitive files outside the allowed paths)
|
||||
if strings.HasPrefix(path, "/node_modules/") ||
|
||||
strings.HasPrefix(path, "/public/assets/") ||
|
||||
strings.HasPrefix(path, "/web_src/") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Vite uses a path relative to project root and adds "?import" to non-JS/CSS asset imports:
|
||||
// - {WebSite}/public/assets/... (e.g. SVG icons from "{RepoRoot}/public/assets/img/svg/")
|
||||
// - {WebSite}/assets/emoji.json: it is an exception for the frontend assets, it is imported by JS code, but:
|
||||
// - KEEP IN MIND: all static frontend assets are served from "{AssetFS}/assets" to "{WebSite}/assets" by Gitea Web Server
|
||||
// - "{AssetFS}" is a layered filesystem from "{RepoRoot}/public" or embedded assets, and user's custom files in "{CustomPath}/public"
|
||||
// - "{RepoRoot}/assets/emoji.json" just happens to have the dir name "assets", it is not related to frontend assets
|
||||
// - BAD DESIGN: indeed it is a "conflicted and polluted name" sample
|
||||
if path == "/assets/emoji.json" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -86,7 +86,6 @@ var (
|
||||
DefaultMergeMessageOfficialApproversOnly bool
|
||||
PopulateSquashCommentWithCommitMessages bool
|
||||
AddCoCommitterTrailers bool
|
||||
TestConflictingPatchesWithGitApply bool
|
||||
RetargetChildrenOnMerge bool
|
||||
DelayCheckForInactiveDays int
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
@@ -211,7 +210,6 @@ var (
|
||||
DefaultMergeMessageOfficialApproversOnly bool
|
||||
PopulateSquashCommentWithCommitMessages bool
|
||||
AddCoCommitterTrailers bool
|
||||
TestConflictingPatchesWithGitApply bool
|
||||
RetargetChildrenOnMerge bool
|
||||
DelayCheckForInactiveDays int
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
|
||||
@@ -72,9 +72,6 @@ var (
|
||||
// It maps to ini:"LOCAL_ROOT_URL" in [server]
|
||||
LocalURL string
|
||||
|
||||
// AssetVersion holds an opaque value that is used for cache-busting assets
|
||||
AssetVersion string
|
||||
|
||||
// appTempPathInternal is the temporary path for the app, it is only an internal variable
|
||||
// DO NOT use it directly, always use AppDataTempDir
|
||||
appTempPathInternal string
|
||||
@@ -286,7 +283,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
|
||||
defaultAppURL := string(Protocol) + "://" + Domain + ":" + HTTPPort
|
||||
AppURL = sec.Key("ROOT_URL").MustString(defaultAppURL)
|
||||
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLLegacy)
|
||||
PublicURLDetection = sec.Key("PUBLIC_URL_DETECTION").MustString(PublicURLAuto)
|
||||
if PublicURLDetection != PublicURLAuto && PublicURLDetection != PublicURLLegacy && PublicURLDetection != PublicURLNever {
|
||||
log.Fatal("Invalid PUBLIC_URL_DETECTION value: %s", PublicURLDetection)
|
||||
}
|
||||
@@ -317,8 +314,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
}
|
||||
|
||||
AbsoluteAssetURL = MakeAbsoluteAssetURL(appURL, StaticURLPrefix)
|
||||
AssetVersion = strings.ReplaceAll(AppVer, "+", "~") // make sure the version string is clear (no real escaping is needed)
|
||||
|
||||
manifestBytes := MakeManifestData(AppName, AppURL, AbsoluteAssetURL)
|
||||
ManifestData = `application/json;base64,` + base64.StdEncoding.EncodeToString(manifestBytes)
|
||||
|
||||
|
||||
@@ -28,8 +28,10 @@ var (
|
||||
CfgProvider ConfigProvider
|
||||
IsWindows bool
|
||||
|
||||
// IsInTesting indicates whether the testing is running. A lot of unreliable code causes a lot of nonsense error logs during testing
|
||||
// TODO: this is only a temporary solution, we should make the test code more reliable
|
||||
// IsInTesting indicates whether the testing is running (unit test or integration test). It can be used for:
|
||||
// * Skip nonsense error logs during testing caused by unreliable code (TODO: this is only a temporary solution, we should make the test code more reliable)
|
||||
// * Panic in dev or testing mode to make the problem more obvious and easier to debug
|
||||
// * Mock some functions or options to make testing easier (eg: session store, time, URL detection, etc.)
|
||||
IsInTesting = false
|
||||
)
|
||||
|
||||
@@ -57,6 +59,10 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) {
|
||||
return currentUser, runUser == currentUser
|
||||
}
|
||||
|
||||
func IsInE2eTesting() bool {
|
||||
return os.Getenv("GITEA_TEST_E2E") == "true"
|
||||
}
|
||||
|
||||
// PrepareAppDataPath creates app data directory if necessary
|
||||
func PrepareAppDataPath() error {
|
||||
// FIXME: There are too many calls to MkdirAll in old code. It is incorrect.
|
||||
|
||||
@@ -23,11 +23,7 @@ import (
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
var (
|
||||
_ ObjectStorage = &MinioStorage{}
|
||||
|
||||
quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||
)
|
||||
var _ ObjectStorage = &MinioStorage{}
|
||||
|
||||
type minioObject struct {
|
||||
*minio.Object
|
||||
|
||||
+12
-12
@@ -12,6 +12,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -62,31 +63,30 @@ type Object interface {
|
||||
type ServeDirectOptions struct {
|
||||
// Overrides the automatically detected MIME type.
|
||||
ContentType string
|
||||
// Overrides the default Content-Disposition header, which is `inline; filename="name"`.
|
||||
ContentDisposition string
|
||||
}
|
||||
|
||||
// Safe defaults are applied only when not explicitly overridden by the caller.
|
||||
func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret ServeDirectOptions) {
|
||||
func prepareServeDirectOptions(optsOptional *ServeDirectOptions, name string) (ret struct {
|
||||
ContentType string
|
||||
ContentDisposition string
|
||||
},
|
||||
) {
|
||||
// Here we might not know the real filename, and it's quite inefficient to detect the MIME type by pre-fetching the object head.
|
||||
// So we just do a quick detection by extension name, at least it works for the "View Raw File" for an LFS file on the Web UI.
|
||||
// TODO: OBJECT-STORAGE-CONTENT-TYPE: need a complete solution and refactor for Azure in the future
|
||||
|
||||
if optsOptional != nil {
|
||||
ret = *optsOptional
|
||||
ret.ContentType = optsOptional.ContentType
|
||||
}
|
||||
|
||||
// TODO: UNIFY-CONTENT-DISPOSITION-FROM-STORAGE
|
||||
name = path.Base(name)
|
||||
if ret.ContentType == "" {
|
||||
ext := path.Ext(name)
|
||||
ret.ContentType = public.DetectWellKnownMimeType(ext)
|
||||
}
|
||||
if ret.ContentDisposition == "" {
|
||||
// When using ServeDirect, the URL is from the object storage's web server,
|
||||
// it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly.
|
||||
// If a browser doesn't support the content type to be displayed inline, browser will download with the filename.
|
||||
ret.ContentDisposition = fmt.Sprintf(`inline; filename="%s"`, quoteEscaper.Replace(name))
|
||||
}
|
||||
// When using ServeDirect, the URL is from the object storage's web server,
|
||||
// it is not the same origin as Gitea server, so it should be safe enough to use "inline" to render the content directly.
|
||||
// If a browser doesn't support the content type to be displayed inline, browser will download with the filename.
|
||||
ret.ContentDisposition = httplib.EncodeContentDispositionInline(name)
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,12 @@ func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) {
|
||||
}
|
||||
}
|
||||
|
||||
func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected ServeDirectOptions, reqParams *ServeDirectOptions) {
|
||||
type expectedServeDirectHeaders struct {
|
||||
ContentType string
|
||||
ContentDisposition string
|
||||
}
|
||||
|
||||
func testSingleBlobStorageURLContentTypeAndDisposition(t *testing.T, s ObjectStorage, path, name string, expected expectedServeDirectHeaders, reqParams *ServeDirectOptions) {
|
||||
u, err := s.ServeDirectURL(path, name, http.MethodGet, reqParams)
|
||||
require.NoError(t, err)
|
||||
resp, err := http.Get(u.String())
|
||||
@@ -71,36 +76,29 @@ func testBlobStorageURLContentTypeAndDisposition(t *testing.T, typStr Type, cfg
|
||||
s, err := NewStorage(typStr, cfg)
|
||||
assert.NoError(t, err)
|
||||
|
||||
data := "Q2xTckt6Y1hDOWh0" // arbitrary test content; specific value is irrelevant to this test
|
||||
testfilename := "test.txt" // arbitrary file name; specific value is irrelevant to this test
|
||||
_, err = s.Save(testfilename, strings.NewReader(data), int64(len(data)))
|
||||
testFilename := "test.txt"
|
||||
_, err = s.Save(testFilename, strings.NewReader("dummy-content"), -1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{
|
||||
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.txt", expectedServeDirectHeaders{
|
||||
ContentType: "text/plain; charset=utf-8",
|
||||
ContentDisposition: `inline; filename="test.txt"`,
|
||||
ContentDisposition: `inline; filename=test.txt`,
|
||||
}, nil)
|
||||
|
||||
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.pdf", ServeDirectOptions{
|
||||
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.pdf", expectedServeDirectHeaders{
|
||||
ContentType: "application/pdf",
|
||||
ContentDisposition: `inline; filename="test.pdf"`,
|
||||
ContentDisposition: `inline; filename=test.pdf`,
|
||||
}, nil)
|
||||
|
||||
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{
|
||||
ContentDisposition: `inline; filename="test.wasm"`,
|
||||
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{
|
||||
ContentDisposition: `inline; filename=test.wasm`,
|
||||
}, nil)
|
||||
|
||||
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.wasm", ServeDirectOptions{
|
||||
ContentDisposition: `inline; filename="test.wasm"`,
|
||||
}, &ServeDirectOptions{})
|
||||
|
||||
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testfilename, "test.txt", ServeDirectOptions{
|
||||
ContentType: "application/octet-stream",
|
||||
ContentDisposition: `inline; filename="test.xml"`,
|
||||
testSingleBlobStorageURLContentTypeAndDisposition(t, s, testFilename, "test.wasm", expectedServeDirectHeaders{
|
||||
ContentType: "application/wasm",
|
||||
ContentDisposition: `inline; filename=test.wasm`,
|
||||
}, &ServeDirectOptions{
|
||||
ContentType: "application/octet-stream",
|
||||
ContentDisposition: `inline; filename="test.xml"`,
|
||||
ContentType: "application/wasm",
|
||||
})
|
||||
|
||||
assert.NoError(t, s.Delete(testfilename))
|
||||
assert.NoError(t, s.Delete(testFilename))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ type Activity struct {
|
||||
UserID int64 `json:"user_id"` // Receiver user
|
||||
// the type of action
|
||||
//
|
||||
// enum: create_repo,rename_repo,star_repo,watch_repo,commit_repo,create_issue,create_pull_request,transfer_repo,push_tag,comment_issue,merge_pull_request,close_issue,reopen_issue,close_pull_request,reopen_pull_request,delete_tag,delete_branch,mirror_sync_push,mirror_sync_create,mirror_sync_delete,approve_pull_request,reject_pull_request,comment_pull,publish_release,pull_review_dismissed,pull_request_ready_for_review,auto_merge_pull_request
|
||||
// enum: ["create_repo","rename_repo","star_repo","watch_repo","commit_repo","create_issue","create_pull_request","transfer_repo","push_tag","comment_issue","merge_pull_request","close_issue","reopen_issue","close_pull_request","reopen_pull_request","delete_tag","delete_branch","mirror_sync_push","mirror_sync_create","mirror_sync_delete","approve_pull_request","reject_pull_request","comment_pull","publish_release","pull_review_dismissed","pull_request_ready_for_review","auto_merge_pull_request"]
|
||||
OpType string `json:"op_type"`
|
||||
// The ID of the user who performed the action
|
||||
ActUserID int64 `json:"act_user_id"`
|
||||
|
||||
@@ -51,7 +51,7 @@ type CreateHookOptionConfig map[string]string
|
||||
// CreateHookOption options when create a hook
|
||||
type CreateHookOption struct {
|
||||
// required: true
|
||||
// enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist
|
||||
// enum: ["dingtalk","discord","gitea","gogs","msteams","slack","telegram","feishu","wechatwork","packagist"]
|
||||
// The type of the webhook to create
|
||||
Type string `json:"type" binding:"Required"`
|
||||
// required: true
|
||||
|
||||
+14
-12
@@ -14,6 +14,8 @@ import (
|
||||
)
|
||||
|
||||
// StateType issue state type
|
||||
//
|
||||
// swagger:enum StateType
|
||||
type StateType string
|
||||
|
||||
const (
|
||||
@@ -21,10 +23,11 @@ const (
|
||||
StateOpen StateType = "open"
|
||||
// StateClosed pr is closed
|
||||
StateClosed StateType = "closed"
|
||||
// StateAll is all
|
||||
StateAll StateType = "all"
|
||||
)
|
||||
|
||||
// StateAll is a query parameter filter value, not a valid object state.
|
||||
const StateAll = "all"
|
||||
|
||||
// PullRequestMeta PR info if an issue is a PR
|
||||
type PullRequestMeta struct {
|
||||
HasMerged bool `json:"merged"`
|
||||
@@ -58,15 +61,11 @@ type Issue struct {
|
||||
Labels []*Label `json:"labels"`
|
||||
Milestone *Milestone `json:"milestone"`
|
||||
// deprecated
|
||||
Assignee *User `json:"assignee"`
|
||||
Assignees []*User `json:"assignees"`
|
||||
// Whether the issue is open or closed
|
||||
//
|
||||
// type: string
|
||||
// enum: open,closed
|
||||
State StateType `json:"state"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
Comments int `json:"comments"`
|
||||
Assignee *User `json:"assignee"`
|
||||
Assignees []*User `json:"assignees"`
|
||||
State StateType `json:"state"`
|
||||
IsLocked bool `json:"is_locked"`
|
||||
Comments int `json:"comments"`
|
||||
// swagger:strfmt date-time
|
||||
Created time.Time `json:"created_at"`
|
||||
// swagger:strfmt date-time
|
||||
@@ -132,6 +131,8 @@ type IssueDeadline struct {
|
||||
}
|
||||
|
||||
// IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes"
|
||||
//
|
||||
// swagger:enum IssueFormFieldType
|
||||
type IssueFormFieldType string
|
||||
|
||||
const (
|
||||
@@ -168,7 +169,8 @@ func (iff IssueFormField) VisibleInContent() bool {
|
||||
}
|
||||
|
||||
// IssueFormFieldVisible defines issue form field visible
|
||||
// swagger:model
|
||||
//
|
||||
// swagger:enum IssueFormFieldVisible
|
||||
type IssueFormFieldVisible string
|
||||
|
||||
const (
|
||||
|
||||
@@ -40,7 +40,7 @@ type CreateMilestoneOption struct {
|
||||
// swagger:strfmt date-time
|
||||
// Deadline is the due date for the milestone
|
||||
Deadline *time.Time `json:"due_on"`
|
||||
// enum: open,closed
|
||||
// enum: ["open","closed"]
|
||||
// State indicates the initial state of the milestone
|
||||
State string `json:"state"`
|
||||
}
|
||||
@@ -52,6 +52,7 @@ type EditMilestoneOption struct {
|
||||
// Description provides updated details about the milestone
|
||||
Description *string `json:"description"`
|
||||
// State indicates the updated state of the milestone
|
||||
// enum: ["open","closed"]
|
||||
State *string `json:"state"`
|
||||
// Deadline is the updated due date for the milestone
|
||||
Deadline *time.Time `json:"due_on"`
|
||||
|
||||
@@ -40,7 +40,7 @@ type NotificationSubject struct {
|
||||
// Type indicates the type of the notification subject
|
||||
Type NotifySubjectType `json:"type" binding:"In(Issue,Pull,Commit,Repository)"`
|
||||
// State indicates the current state of the notification subject
|
||||
State StateType `json:"state"`
|
||||
State NotifySubjectStateType `json:"state"`
|
||||
}
|
||||
|
||||
// NotificationCount number of unread notifications
|
||||
@@ -49,7 +49,22 @@ type NotificationCount struct {
|
||||
New int64 `json:"new"`
|
||||
}
|
||||
|
||||
// NotifySubjectStateType represents the state of a notification subject
|
||||
// swagger:enum NotifySubjectStateType
|
||||
type NotifySubjectStateType string
|
||||
|
||||
const (
|
||||
// NotifySubjectStateOpen is an open subject
|
||||
NotifySubjectStateOpen NotifySubjectStateType = "open"
|
||||
// NotifySubjectStateClosed is a closed subject
|
||||
NotifySubjectStateClosed NotifySubjectStateType = "closed"
|
||||
// NotifySubjectStateMerged is a merged pull request
|
||||
NotifySubjectStateMerged NotifySubjectStateType = "merged"
|
||||
)
|
||||
|
||||
// NotifySubjectType represent type of notification subject
|
||||
//
|
||||
// swagger:enum NotifySubjectType
|
||||
type NotifySubjectType string
|
||||
|
||||
const (
|
||||
|
||||
+9
-11
@@ -60,29 +60,27 @@ type CreateOrgOption struct {
|
||||
// The location of the organization
|
||||
Location string `json:"location" binding:"MaxSize(50)"`
|
||||
// possible values are `public` (default), `limited` or `private`
|
||||
// enum: public,limited,private
|
||||
// enum: ["public","limited","private"]
|
||||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
// Whether repository administrators can change team access
|
||||
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
|
||||
}
|
||||
|
||||
// TODO: make EditOrgOption fields optional after https://gitea.com/go-chi/binding/pulls/5 got merged
|
||||
|
||||
// EditOrgOption options for editing an organization
|
||||
type EditOrgOption struct {
|
||||
// The full display name of the organization
|
||||
FullName string `json:"full_name" binding:"MaxSize(100)"`
|
||||
// The email address of the organization
|
||||
Email string `json:"email" binding:"MaxSize(255)"`
|
||||
FullName *string `json:"full_name" binding:"MaxSize(100)"`
|
||||
// The email address of the organization; use empty string to clear
|
||||
Email *string `json:"email" binding:"MaxSize(255)"`
|
||||
// The description of the organization
|
||||
Description string `json:"description" binding:"MaxSize(255)"`
|
||||
Description *string `json:"description" binding:"MaxSize(255)"`
|
||||
// The website URL of the organization
|
||||
Website string `json:"website" binding:"ValidUrl;MaxSize(255)"`
|
||||
Website *string `json:"website" binding:"ValidUrl;MaxSize(255)"`
|
||||
// The location of the organization
|
||||
Location string `json:"location" binding:"MaxSize(50)"`
|
||||
Location *string `json:"location" binding:"MaxSize(50)"`
|
||||
// possible values are `public`, `limited` or `private`
|
||||
// enum: public,limited,private
|
||||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
// enum: ["public","limited","private"]
|
||||
Visibility *string `json:"visibility" binding:"In(,public,limited,private)"`
|
||||
// Whether repository administrators can change team access
|
||||
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ type Team struct {
|
||||
Organization *Organization `json:"organization"`
|
||||
// Whether the team has access to all repositories in the organization
|
||||
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||
// enum: none,read,write,admin,owner
|
||||
// enum: ["none","read","write","admin","owner"]
|
||||
Permission string `json:"permission"`
|
||||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
|
||||
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
|
||||
@@ -35,7 +35,7 @@ type CreateTeamOption struct {
|
||||
Description string `json:"description" binding:"MaxSize(255)"`
|
||||
// Whether the team has access to all repositories in the organization
|
||||
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||
// enum: read,write,admin
|
||||
// enum: ["read","write","admin"]
|
||||
Permission string `json:"permission"`
|
||||
// example: ["repo.actions","repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.ext_wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
|
||||
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
|
||||
@@ -54,7 +54,7 @@ type EditTeamOption struct {
|
||||
Description *string `json:"description" binding:"MaxSize(255)"`
|
||||
// Whether the team has access to all repositories in the organization
|
||||
IncludesAllRepositories *bool `json:"includes_all_repositories"`
|
||||
// enum: read,write,admin
|
||||
// enum: ["read","write","admin"]
|
||||
Permission string `json:"permission"`
|
||||
// example: ["repo.code","repo.issues","repo.ext_issues","repo.wiki","repo.pulls","repo.releases","repo.projects","repo.ext_wiki"]
|
||||
// Deprecated: This variable should be replaced by UnitsMap and will be dropped in later versions.
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
)
|
||||
|
||||
// ReviewStateType review state type
|
||||
//
|
||||
// swagger:enum ReviewStateType
|
||||
type ReviewStateType string
|
||||
|
||||
const (
|
||||
@@ -21,10 +23,11 @@ const (
|
||||
ReviewStateRequestChanges ReviewStateType = "REQUEST_CHANGES"
|
||||
// ReviewStateRequestReview review is requested from user
|
||||
ReviewStateRequestReview ReviewStateType = "REQUEST_REVIEW"
|
||||
// ReviewStateUnknown state of pr is unknown
|
||||
ReviewStateUnknown ReviewStateType = ""
|
||||
)
|
||||
|
||||
// ReviewStateUnknown is an internal sentinel for unknown review state, not a valid API value.
|
||||
const ReviewStateUnknown = ""
|
||||
|
||||
// PullReview represents a pull request review
|
||||
type PullReview struct {
|
||||
ID int64 `json:"id"`
|
||||
|
||||
@@ -114,7 +114,7 @@ type Repository struct {
|
||||
Internal bool `json:"internal"`
|
||||
MirrorInterval string `json:"mirror_interval"`
|
||||
// ObjectFormatName of the underlying git repository
|
||||
// enum: sha1,sha256
|
||||
// enum: ["sha1","sha256"]
|
||||
ObjectFormatName string `json:"object_format_name"`
|
||||
// swagger:strfmt date-time
|
||||
MirrorUpdated time.Time `json:"mirror_updated"`
|
||||
@@ -150,10 +150,10 @@ type CreateRepoOption struct {
|
||||
// DefaultBranch of the repository (used when initializes and in template)
|
||||
DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"`
|
||||
// TrustModel of the repository
|
||||
// enum: default,collaborator,committer,collaboratorcommitter
|
||||
// enum: ["default","collaborator","committer","collaboratorcommitter"]
|
||||
TrustModel string `json:"trust_model"`
|
||||
// ObjectFormatName of the underlying git repository, empty string for default (sha1)
|
||||
// enum: sha1,sha256
|
||||
// enum: ["sha1","sha256"]
|
||||
ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"`
|
||||
}
|
||||
|
||||
@@ -378,7 +378,7 @@ type MigrateRepoOptions struct {
|
||||
// required: true
|
||||
RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"`
|
||||
|
||||
// enum: git,github,gitea,gitlab,gogs,onedev,gitbucket,codebase,codecommit
|
||||
// enum: ["git","github","gitea","gitlab","gogs","onedev","gitbucket","codebase","codecommit"]
|
||||
Service string `json:"service"`
|
||||
AuthUsername string `json:"auth_username"`
|
||||
AuthPassword string `json:"auth_password"`
|
||||
|
||||
@@ -5,7 +5,7 @@ package structs
|
||||
|
||||
// AddCollaboratorOption options when adding a user as a collaborator of a repository
|
||||
type AddCollaboratorOption struct {
|
||||
// enum: read,write,admin
|
||||
// enum: ["read","write","admin"]
|
||||
// Permission level to grant the collaborator
|
||||
Permission *string `json:"permission"`
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ type ChangeFileOperation struct {
|
||||
// indicates what to do with the file: "create" for creating a new file, "update" for updating an existing file,
|
||||
// "upload" for creating or updating a file, "rename" for renaming a file, and "delete" for deleting an existing file.
|
||||
// required: true
|
||||
// enum: create,update,upload,rename,delete
|
||||
// enum: ["create","update","upload","rename","delete"]
|
||||
Operation string `json:"operation" binding:"Required"`
|
||||
// path to the existing or new file
|
||||
// required: true
|
||||
|
||||
@@ -6,15 +6,18 @@ package templates
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/public"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
"code.gitea.io/gitea/modules/templates/eval"
|
||||
@@ -68,6 +71,8 @@ func NewFuncMap() template.FuncMap {
|
||||
return strconv.FormatInt(time.Since(startTime).Nanoseconds()/1e6, 10) + "ms"
|
||||
},
|
||||
|
||||
"AssetURI": public.AssetURI,
|
||||
"ScriptImport": scriptImport,
|
||||
// -----------------------------------------------------------------
|
||||
// setting
|
||||
"AppName": func() string {
|
||||
@@ -92,9 +97,6 @@ func NewFuncMap() template.FuncMap {
|
||||
"AppDomain": func() string { // documented in mail-templates.md
|
||||
return setting.Domain
|
||||
},
|
||||
"AssetVersion": func() string {
|
||||
return setting.AssetVersion
|
||||
},
|
||||
"ShowFooterTemplateLoadTime": func() bool {
|
||||
return setting.Other.ShowFooterTemplateLoadTime
|
||||
},
|
||||
@@ -303,3 +305,30 @@ func QueryBuild(a ...any) template.URL {
|
||||
}
|
||||
return template.URL(s)
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() (ret struct {
|
||||
scriptImportRemainingPart string
|
||||
},
|
||||
) {
|
||||
// add onerror handler to alert users when the script fails to load:
|
||||
// * for end users: there were many users reporting that "UI doesn't work", actually they made mistakes in their config
|
||||
// * for developers: help them to remember to run "make watch-frontend" to build frontend assets
|
||||
// the message will be directly put in the onerror JS code's string
|
||||
onScriptErrorPrompt := `Please make sure the asset files can be accessed.`
|
||||
if !setting.IsProd {
|
||||
onScriptErrorPrompt += `\n\nFor development, run: make watch-frontend.`
|
||||
}
|
||||
onScriptErrorJS := fmt.Sprintf(`alert('Failed to load asset file from ' + this.src + '. %s')`, onScriptErrorPrompt)
|
||||
ret.scriptImportRemainingPart = `onerror="` + html.EscapeString(onScriptErrorJS) + `"></script>`
|
||||
return ret
|
||||
})
|
||||
|
||||
func scriptImport(path string, typ ...string) template.HTML {
|
||||
if len(typ) > 0 {
|
||||
if typ[0] == "module" {
|
||||
return template.HTML(`<script type="module" src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
|
||||
}
|
||||
panic("unsupported script type: " + typ[0])
|
||||
}
|
||||
return template.HTML(`<script src="` + html.EscapeString(public.AssetURI(path)) + `" ` + globalVars().scriptImportRemainingPart)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user