Merge branch 'main' into add-issue-planned-time

This commit is contained in:
stuzer05 2023-06-18 17:27:33 +03:00
commit 015ad01513
No known key found for this signature in database
GPG Key ID: A6ABAAA9268F9F4F
332 changed files with 11764 additions and 8861 deletions

View File

@ -0,0 +1,34 @@
{
"name": "Gitea DevContainer",
"image": "mcr.microsoft.com/devcontainers/go:1.20",
"features": {
// installs nodejs into container
"ghcr.io/devcontainers/features/node:1": {
"version":"20"
}
},
"customizations": {
"vscode": {
"settings": {},
// same extensions as Gitpod, should match /.gitpod.yml
"extensions": [
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"golang.go",
"stylelint.vscode-stylelint",
"DavidAnson.vscode-markdownlint",
"Vue.volar",
"ms-azuretools.vscode-docker",
"zixuanchen.vitest-explorer",
"alexcvzz.vscode-sqlite"
]
}
},
"portsAttributes": {
"3000": {
"label": "Gitea Web",
"onAutoForward": "notify"
}
},
"postCreateCommand": "make deps"
}

View File

@ -1,138 +1,3 @@
---
kind: pipeline
type: docker
name: release-latest
platform:
os: linux
arch: amd64
workspace:
base: /source
path: /
trigger:
branch:
- main
- "release/*"
event:
- push
paths:
exclude:
- "docs/**"
volumes:
- name: deps
temp: {}
steps:
- name: fetch-tags
image: docker:git
pull: always
commands:
- git fetch --tags --force
- name: deps-frontend
image: node:20
pull: always
commands:
- make deps-frontend
- name: deps-backend
image: gitea/test_env:linux-1.20-amd64
pull: always
commands:
- make deps-backend
volumes:
- name: deps
path: /go
- name: static
image: techknowlogick/xgo:go-1.20.x
pull: always
commands:
# Upgrade to node 20 once https://github.com/techknowlogick/xgo/issues/163 is resolved
- curl -sL https://deb.nodesource.com/setup_16.x | bash - && apt-get -qqy install nodejs
- export PATH=$PATH:$GOPATH/bin
- make release
environment:
GOPROXY: https://goproxy.io # proxy.golang.org is blocked in China, this proxy is not
TAGS: bindata sqlite sqlite_unlock_notify
DEBIAN_FRONTEND: noninteractive
volumes:
- name: deps
path: /go
- name: gpg-sign
image: plugins/gpgsign:1
pull: always
settings:
detach_sign: true
excludes:
- "dist/release/*.sha256"
files:
- "dist/release/*"
environment:
GPGSIGN_KEY:
from_secret: gpgsign_key
GPGSIGN_PASSPHRASE:
from_secret: gpgsign_passphrase
- name: release-branch
image: woodpeckerci/plugin-s3:latest
pull: always
settings:
acl:
from_secret: aws_s3_acl
region:
from_secret: aws_s3_region
bucket:
from_secret: aws_s3_bucket
endpoint:
from_secret: aws_s3_endpoint
path_style:
from_secret: aws_s3_path_style
source: "dist/release/*"
strip_prefix: dist/release/
target: "/gitea/${DRONE_BRANCH##release/v}"
environment:
AWS_ACCESS_KEY_ID:
from_secret: aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: aws_secret_access_key
when:
branch:
- "release/*"
event:
- push
- name: release-main
image: woodpeckerci/plugin-s3:latest
settings:
acl:
from_secret: aws_s3_acl
region:
from_secret: aws_s3_region
bucket:
from_secret: aws_s3_bucket
endpoint:
from_secret: aws_s3_endpoint
path_style:
from_secret: aws_s3_path_style
source: "dist/release/*"
strip_prefix: dist/release/
target: /gitea/main
environment:
AWS_ACCESS_KEY_ID:
from_secret: aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: aws_secret_access_key
when:
branch:
- main
event:
- push
--- ---
kind: pipeline kind: pipeline
name: release-version name: release-version
@ -379,133 +244,6 @@ steps:
exclude: exclude:
- pull_request - pull_request
---
kind: pipeline
type: docker
name: docker-linux-amd64-release
platform:
os: linux
arch: amd64
trigger:
ref:
- refs/heads/main
steps:
- name: fetch-tags
image: docker:git
pull: always
commands:
- git fetch --tags --force
- name: publish
image: plugins/docker:latest
pull: always
settings:
auto_tag: false
tags: nightly-linux-amd64
repo: gitea/gitea
build_args:
- GOPROXY=https://goproxy.io
password:
from_secret: docker_password
username:
from_secret: docker_username
environment:
PLUGIN_MIRROR:
from_secret: plugin_mirror
DOCKER_BUILDKIT: 1
when:
event:
exclude:
- pull_request
- name: publish-rootless
image: plugins/docker:latest
settings:
dockerfile: Dockerfile.rootless
auto_tag: false
tags: nightly-linux-amd64-rootless
repo: gitea/gitea
build_args:
- GOPROXY=https://goproxy.io
password:
from_secret: docker_password
username:
from_secret: docker_username
environment:
PLUGIN_MIRROR:
from_secret: plugin_mirror
DOCKER_BUILDKIT: 1
when:
event:
exclude:
- pull_request
---
kind: pipeline
name: docker-linux-amd64-release-branch
platform:
os: linux
arch: amd64
trigger:
ref:
- "refs/heads/release/v*"
steps:
- name: fetch-tags
image: docker:git
pull: always
commands:
- git fetch --tags --force
- name: publish
image: plugins/docker:latest
pull: always
settings:
auto_tag: false
tags: ${DRONE_BRANCH##release/v}-nightly-linux-amd64
repo: gitea/gitea
build_args:
- GOPROXY=https://goproxy.io
password:
from_secret: docker_password
username:
from_secret: docker_username
environment:
PLUGIN_MIRROR:
from_secret: plugin_mirror
DOCKER_BUILDKIT: 1
when:
event:
exclude:
- pull_request
- name: publish-rootless
image: plugins/docker:latest
settings:
dockerfile: Dockerfile.rootless
auto_tag: false
tags: ${DRONE_BRANCH##release/v}-nightly-linux-amd64-rootless
repo: gitea/gitea
build_args:
- GOPROXY=https://goproxy.io
password:
from_secret: docker_password
username:
from_secret: docker_username
environment:
PLUGIN_MIRROR:
from_secret: plugin_mirror
DOCKER_BUILDKIT: 1
when:
event:
exclude:
- pull_request
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
@ -641,136 +379,6 @@ steps:
exclude: exclude:
- pull_request - pull_request
---
kind: pipeline
type: docker
name: docker-linux-arm64-release
platform:
os: linux
arch: arm64
trigger:
ref:
- refs/heads/main
paths:
exclude:
- "docs/**"
steps:
- name: fetch-tags
image: docker:git
pull: always
commands:
- git fetch --tags --force
- name: publish
image: plugins/docker:latest
pull: always
settings:
auto_tag: false
tags: nightly-linux-arm64
repo: gitea/gitea
build_args:
- GOPROXY=https://goproxy.io
password:
from_secret: docker_password
username:
from_secret: docker_username
environment:
PLUGIN_MIRROR:
from_secret: plugin_mirror
DOCKER_BUILDKIT: 1
when:
event:
exclude:
- pull_request
- name: publish-rootless
image: plugins/docker:latest
settings:
dockerfile: Dockerfile.rootless
auto_tag: false
tags: nightly-linux-arm64-rootless
repo: gitea/gitea
build_args:
- GOPROXY=https://goproxy.io
password:
from_secret: docker_password
username:
from_secret: docker_username
environment:
PLUGIN_MIRROR:
from_secret: plugin_mirror
DOCKER_BUILDKIT: 1
when:
event:
exclude:
- pull_request
---
kind: pipeline
name: docker-linux-arm64-release-branch
platform:
os: linux
arch: arm64
trigger:
ref:
- "refs/heads/release/v*"
steps:
- name: fetch-tags
image: docker:git
pull: always
commands:
- git fetch --tags --force
- name: publish
image: plugins/docker:latest
pull: always
settings:
auto_tag: false
tags: ${DRONE_BRANCH##release/v}-nightly-linux-arm64
repo: gitea/gitea
build_args:
- GOPROXY=https://goproxy.io
password:
from_secret: docker_password
username:
from_secret: docker_username
environment:
PLUGIN_MIRROR:
from_secret: plugin_mirror
DOCKER_BUILDKIT: 1
when:
event:
exclude:
- pull_request
- name: publish-rootless
image: plugins/docker:latest
settings:
dockerfile: Dockerfile.rootless
auto_tag: false
tags: ${DRONE_BRANCH##release/v}-nightly-linux-arm64-rootless
repo: gitea/gitea
build_args:
- GOPROXY=https://goproxy.io
password:
from_secret: docker_password
username:
from_secret: docker_username
environment:
PLUGIN_MIRROR:
from_secret: plugin_mirror
DOCKER_BUILDKIT: 1
when:
event:
exclude:
- pull_request
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
@ -816,50 +424,3 @@ depends_on:
- docker-linux-amd64-release-candidate-version - docker-linux-amd64-release-candidate-version
- docker-linux-arm64-release-version - docker-linux-arm64-release-version
- docker-linux-arm64-release-candidate-version - docker-linux-arm64-release-candidate-version
---
kind: pipeline
type: docker
name: docker-manifest
platform:
os: linux
arch: amd64
steps:
- name: manifest-rootless
image: plugins/manifest
pull: always
settings:
auto_tag: false
ignore_missing: true
spec: docker/manifest.rootless.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
- name: manifest
image: plugins/manifest
settings:
auto_tag: false
ignore_missing: true
spec: docker/manifest.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
trigger:
ref:
- refs/heads/main
- "refs/heads/release/v*"
paths:
exclude:
- "docs/**"
depends_on:
- docker-linux-amd64-release
- docker-linux-arm64-release
- docker-linux-amd64-release-branch
- docker-linux-arm64-release-branch

1
.gitattributes vendored
View File

@ -5,5 +5,6 @@
/templates/swagger/v1_json.tmpl linguist-generated /templates/swagger/v1_json.tmpl linguist-generated
/vendor/** -text -eol linguist-vendored /vendor/** -text -eol linguist-vendored
/web_src/fomantic/build/** linguist-generated /web_src/fomantic/build/** linguist-generated
/web_src/fomantic/_site/globals/site.variables linguist-language=Less
/web_src/js/vendor/** -text -eol linguist-vendored /web_src/js/vendor/** -text -eol linguist-vendored
Dockerfile.* linguist-language=Dockerfile Dockerfile.* linguist-language=Dockerfile

View File

@ -3,6 +3,7 @@ name: cron-licenses
on: on:
schedule: schedule:
- cron: "7 0 * * 1" # every Monday at 00:07 UTC - cron: "7 0 * * 1" # every Monday at 00:07 UTC
workflow_dispatch:
jobs: jobs:
cron-licenses: cron-licenses:

View File

@ -3,6 +3,7 @@ name: cron-translations
on: on:
schedule: schedule:
- cron: "7 0 * * *" # every day at 00:07 UTC - cron: "7 0 * * *" # every day at 00:07 UTC
workflow_dispatch:
jobs: jobs:
crowdin-pull: crowdin-pull:

View File

@ -15,6 +15,9 @@ on:
actions: actions:
description: "whether actions files changed" description: "whether actions files changed"
value: ${{ jobs.detect.outputs.actions }} value: ${{ jobs.detect.outputs.actions }}
templates:
description: "whether templates files changed"
value: ${{ jobs.detect.outputs.templates }}
jobs: jobs:
detect: detect:
@ -27,6 +30,7 @@ jobs:
frontend: ${{ steps.changes.outputs.frontend }} frontend: ${{ steps.changes.outputs.frontend }}
docs: ${{ steps.changes.outputs.docs }} docs: ${{ steps.changes.outputs.docs }}
actions: ${{ steps.changes.outputs.actions }} actions: ${{ steps.changes.outputs.actions }}
templates: ${{ steps.changes.outputs.templates }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: dorny/paths-filter@v2 - uses: dorny/paths-filter@v2
@ -35,7 +39,7 @@ jobs:
filters: | filters: |
backend: backend:
- "**/*.go" - "**/*.go"
- "**/*.tmpl" - "templates/**/*.tmpl"
- "go.mod" - "go.mod"
- "go.sum" - "go.sum"
@ -51,3 +55,6 @@ jobs:
actions: actions:
- ".github/workflows/*" - ".github/workflows/*"
templates:
- "templates/**/*.tmpl"

View File

@ -12,7 +12,7 @@ jobs:
uses: ./.github/workflows/files-changed.yml uses: ./.github/workflows/files-changed.yml
lint-backend: lint-backend:
if: needs.files-changed.outputs.backend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -26,8 +26,21 @@ jobs:
env: env:
TAGS: bindata sqlite sqlite_unlock_notify TAGS: bindata sqlite sqlite_unlock_notify
lint-templates:
if: needs.files-changed.outputs.templates == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- run: pip install poetry
- run: make deps-py
- run: make lint-templates
lint-go-windows: lint-go-windows:
if: needs.files-changed.outputs.backend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -44,7 +57,7 @@ jobs:
GOARCH: amd64 GOARCH: amd64
lint-go-gogit: lint-go-gogit:
if: needs.files-changed.outputs.backend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -59,7 +72,7 @@ jobs:
TAGS: bindata gogit sqlite sqlite_unlock_notify TAGS: bindata gogit sqlite sqlite_unlock_notify
checks-backend: checks-backend:
if: needs.files-changed.outputs.backend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -72,7 +85,7 @@ jobs:
- run: make --always-make checks-backend # ensure the "go-licenses" make target runs - run: make --always-make checks-backend # ensure the "go-licenses" make target runs
frontend: frontend:
if: needs.files-changed.outputs.frontend == 'true' if: needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -83,9 +96,10 @@ jobs:
- run: make deps-frontend - run: make deps-frontend
- run: make lint-frontend - run: make lint-frontend
- run: make checks-frontend - run: make checks-frontend
- run: make frontend
backend: backend:
if: needs.files-changed.outputs.backend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -94,12 +108,9 @@ jobs:
with: with:
go-version: ">=1.20" go-version: ">=1.20"
check-latest: true check-latest: true
- uses: actions/setup-node@v3 # no frontend build here as backend should be able to build
with: # even without any frontend files
node-version: 20
- run: make deps-backend deps-tools - run: make deps-backend deps-tools
- run: make deps-frontend
- run: make frontend
- run: go build -o gitea_no_gcc # test if build succeeds without the sqlite tag - run: go build -o gitea_no_gcc # test if build succeeds without the sqlite tag
- name: build-backend-arm64 - name: build-backend-arm64
run: make backend # test cross compile run: make backend # test cross compile
@ -120,7 +131,7 @@ jobs:
GOARCH: 386 GOARCH: 386
docs: docs:
if: needs.files-changed.outputs.docs == 'true' if: needs.files-changed.outputs.docs == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -133,7 +144,7 @@ jobs:
- run: make docs # test if build could succeed - run: make docs # test if build could succeed
actions: actions:
if: needs.files-changed.outputs.actions == 'true' if: needs.files-changed.outputs.actions == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@ -12,7 +12,7 @@ jobs:
uses: ./.github/workflows/files-changed.yml uses: ./.github/workflows/files-changed.yml
test-pgsql: test-pgsql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
@ -59,7 +59,7 @@ jobs:
USE_REPO_TEST_DIR: 1 USE_REPO_TEST_DIR: 1
test-sqlite: test-sqlite:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -81,7 +81,7 @@ jobs:
USE_REPO_TEST_DIR: 1 USE_REPO_TEST_DIR: 1
test-unit: test-unit:
if: needs.files-changed.outputs.backend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
@ -147,7 +147,7 @@ jobs:
GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }}
test-mysql5: test-mysql5:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
@ -192,7 +192,7 @@ jobs:
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
test-mysql8: test-mysql8:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
@ -222,7 +222,7 @@ jobs:
USE_REPO_TEST_DIR: 1 USE_REPO_TEST_DIR: 1
test-mssql: test-mssql:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:

View File

@ -12,7 +12,7 @@ jobs:
uses: ./.github/workflows/files-changed.yml uses: ./.github/workflows/files-changed.yml
docker-dryrun: docker-dryrun:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@ -12,7 +12,7 @@ jobs:
uses: ./.github/workflows/files-changed.yml uses: ./.github/workflows/files-changed.yml
test-e2e: test-e2e:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.frontend == 'true' || needs.files-changed.outputs.actions == 'true'
needs: files-changed needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

87
.github/workflows/release-nightly.yml vendored Normal file
View File

@ -0,0 +1,87 @@
name: release-nightly-assets
on:
push:
branches: [ main, release/v* ]
jobs:
nightly-binary:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# fetch all commits instead of only the last as some branches are long lived and could have many between versions
# fetch all tags to ensure that "git describe" reports expected Gitea version, eg. v1.21.0-dev-1-g1234567
- run: git fetch --unshallow --quiet --tags --force
- uses: actions/setup-go@v4
with:
go-version: ">=1.20"
check-latest: true
- uses: actions/setup-node@v3
with:
node-version: 20
- run: make deps-frontend deps-backend
# xgo build
- run: make release
env:
TAGS: bindata sqlite sqlite_unlock_notify
- name: import gpg key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v5
with:
gpg_private_key: ${{ secrets.GPGSIGN_KEY }}
passphrase: ${{ secrets.GPGSIGN_PASSPHRASE }}
- name: sign binaries
run: |
for f in dist/release/*; do
echo '${{ secrets.GPGSIGN_PASSPHRASE }}' | gpg --pinentry-mode loopback --passphrase-fd 0 --batch --yes --detach-sign -u ${{ steps.import_gpg.outputs.fingerprint }} --output "$f.asc" "$f"
done
# clean branch name to get the folder name in S3
- 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 "Cleaned name is ${REF_NAME}"
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
- name: upload binaries to s3
uses: jakejarvis/s3-sync-action@master
env:
AWS_S3_BUCKET: ${{ secrets.AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
SOURCE_DIR: dist/release
DEST_DIR: gitea/${{ steps.clean_name.outputs.branch }}
nightly-docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- name: Get cleaned branch name
id: clean_name
run: |
# if main then say nightly otherwise cleanup name
if [ "${{ github.ref }}" = "refs/heads/main" ]; then
echo "branch=nightly" >> "$GITHUB_OUTPUT"
exit 0
fi
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
echo "branch=${REF_NAME}-nightly" >> "$GITHUB_OUTPUT"
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: build rootful docker image
uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/arm64
push: true
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}
- name: build rootless docker image
uses: docker/build-push-action@v4
with:
platforms: linux/amd64,linux/arm64
push: true
file: Dockerfile.rootless
tags: gitea/gitea:${{ steps.clean_name.outputs.branch }}-rootless

1
.gitignore vendored
View File

@ -70,6 +70,7 @@ cpu.out
/tests/*.ini /tests/*.ini
/tests/**/*.git/**/*.sample /tests/**/*.git/**/*.sample
/node_modules /node_modules
/.venv
/yarn.lock /yarn.lock
/yarn-error.log /yarn-error.log
/npm-debug.log* /npm-debug.log*

View File

@ -1,5 +1,7 @@
plugins: plugins:
- stylelint-declaration-strict-value - stylelint-declaration-strict-value
- stylelint-declaration-block-no-ignored-properties
- stylelint-stylistic
ignoreFiles: ignoreFiles:
- "**/*.go" - "**/*.go"
@ -80,6 +82,7 @@ rules:
media-feature-name-no-vendor-prefix: true media-feature-name-no-vendor-prefix: true
media-feature-name-unit-allowed-list: null media-feature-name-unit-allowed-list: null
media-feature-name-value-allowed-list: null media-feature-name-value-allowed-list: null
media-feature-name-value-no-unknown: true
media-feature-range-notation: null media-feature-range-notation: null
named-grid-areas-no-invalid: true named-grid-areas-no-invalid: true
no-descending-specificity: null no-descending-specificity: null
@ -92,6 +95,7 @@ rules:
no-unknown-animations: null no-unknown-animations: null
no-unknown-custom-properties: null no-unknown-custom-properties: null
number-max-precision: null number-max-precision: null
plugin/declaration-block-no-ignored-properties: true
property-allowed-list: null property-allowed-list: null
property-disallowed-list: null property-disallowed-list: null
property-no-unknown: true property-no-unknown: true
@ -132,6 +136,82 @@ rules:
selector-type-no-unknown: [true, {ignore: [custom-elements]}] selector-type-no-unknown: [true, {ignore: [custom-elements]}]
shorthand-property-no-redundant-values: true shorthand-property-no-redundant-values: true
string-no-newline: true string-no-newline: true
stylistic/at-rule-name-case: null
stylistic/at-rule-name-newline-after: null
stylistic/at-rule-name-space-after: null
stylistic/at-rule-semicolon-newline-after: null
stylistic/at-rule-semicolon-space-before: null
stylistic/block-closing-brace-empty-line-before: null
stylistic/block-closing-brace-newline-after: null
stylistic/block-closing-brace-newline-before: null
stylistic/block-closing-brace-space-after: null
stylistic/block-closing-brace-space-before: null
stylistic/block-opening-brace-newline-after: null
stylistic/block-opening-brace-newline-before: null
stylistic/block-opening-brace-space-after: null
stylistic/block-opening-brace-space-before: null
stylistic/color-hex-case: lower
stylistic/declaration-bang-space-after: never
stylistic/declaration-bang-space-before: null
stylistic/declaration-block-semicolon-newline-after: null
stylistic/declaration-block-semicolon-newline-before: null
stylistic/declaration-block-semicolon-space-after: null
stylistic/declaration-block-semicolon-space-before: never
stylistic/declaration-block-trailing-semicolon: null
stylistic/declaration-colon-newline-after: null
stylistic/declaration-colon-space-after: null
stylistic/declaration-colon-space-before: never
stylistic/function-comma-newline-after: null
stylistic/function-comma-newline-before: null
stylistic/function-comma-space-after: null
stylistic/function-comma-space-before: null
stylistic/function-max-empty-lines: 0
stylistic/function-parentheses-newline-inside: never-multi-line
stylistic/function-parentheses-space-inside: null
stylistic/function-whitespace-after: null
stylistic/indentation: 2
stylistic/linebreaks: null
stylistic/max-empty-lines: 1
stylistic/max-line-length: null
stylistic/media-feature-colon-space-after: null
stylistic/media-feature-colon-space-before: never
stylistic/media-feature-name-case: null
stylistic/media-feature-parentheses-space-inside: null
stylistic/media-feature-range-operator-space-after: always
stylistic/media-feature-range-operator-space-before: always
stylistic/media-query-list-comma-newline-after: null
stylistic/media-query-list-comma-newline-before: null
stylistic/media-query-list-comma-space-after: null
stylistic/media-query-list-comma-space-before: null
stylistic/no-empty-first-line: null
stylistic/no-eol-whitespace: true
stylistic/no-extra-semicolons: true
stylistic/no-missing-end-of-source-newline: null
stylistic/number-leading-zero: null
stylistic/number-no-trailing-zeros: null
stylistic/property-case: lower
stylistic/selector-attribute-brackets-space-inside: null
stylistic/selector-attribute-operator-space-after: null
stylistic/selector-attribute-operator-space-before: null
stylistic/selector-combinator-space-after: null
stylistic/selector-combinator-space-before: null
stylistic/selector-descendant-combinator-no-non-space: null
stylistic/selector-list-comma-newline-after: null
stylistic/selector-list-comma-newline-before: null
stylistic/selector-list-comma-space-after: always-single-line
stylistic/selector-list-comma-space-before: never-single-line
stylistic/selector-max-empty-lines: 0
stylistic/selector-pseudo-class-case: lower
stylistic/selector-pseudo-class-parentheses-space-inside: never
stylistic/selector-pseudo-element-case: lower
stylistic/string-quotes: double
stylistic/unicode-bom: null
stylistic/unit-case: lower
stylistic/value-list-comma-newline-after: null
stylistic/value-list-comma-newline-before: null
stylistic/value-list-comma-space-after: null
stylistic/value-list-comma-space-before: null
stylistic/value-list-max-empty-lines: 0
time-min-milliseconds: null time-min-milliseconds: null
unit-allowed-list: null unit-allowed-list: null
unit-disallowed-list: null unit-disallowed-list: null

View File

@ -79,12 +79,12 @@ endif
STORED_VERSION_FILE := VERSION STORED_VERSION_FILE := VERSION
HUGO_VERSION ?= 0.111.3 HUGO_VERSION ?= 0.111.3
ifneq ($(DRONE_TAG),) ifneq ($(GITHUB_REF_TYPE),branch)
VERSION ?= $(subst v,,$(DRONE_TAG)) VERSION ?= $(subst v,,$(GITHUB_REF_NAME))
GITEA_VERSION ?= $(VERSION) GITEA_VERSION ?= $(GITHUB_REF_NAME)
else else
ifneq ($(DRONE_BRANCH),) ifneq ($(GITHUB_REF_NAME),)
VERSION ?= $(subst release/v,,$(DRONE_BRANCH)) VERSION ?= $(subst release/v,,$(GITHUB_REF_NAME))
else else
VERSION ?= main VERSION ?= main
endif endif
@ -198,6 +198,7 @@ help:
@echo " - deps-frontend install frontend dependencies" @echo " - deps-frontend install frontend dependencies"
@echo " - deps-backend install backend dependencies" @echo " - deps-backend install backend dependencies"
@echo " - deps-tools install tool dependencies" @echo " - deps-tools install tool dependencies"
@echo " - deps-py install python dependencies"
@echo " - lint lint everything" @echo " - lint lint everything"
@echo " - lint-fix lint everything and fix issues" @echo " - lint-fix lint everything and fix issues"
@echo " - lint-actions lint action workflow files" @echo " - lint-actions lint action workflow files"
@ -214,6 +215,7 @@ help:
@echo " - lint-css-fix lint css files and fix issues" @echo " - lint-css-fix lint css files and fix issues"
@echo " - lint-md lint markdown files" @echo " - lint-md lint markdown files"
@echo " - lint-swagger lint swagger files" @echo " - lint-swagger lint swagger files"
@echo " - lint-templates lint template files"
@echo " - checks run various consistency checks" @echo " - checks run various consistency checks"
@echo " - checks-frontend check frontend files" @echo " - checks-frontend check frontend files"
@echo " - checks-backend check backend files" @echo " - checks-backend check backend files"
@ -417,6 +419,10 @@ lint-editorconfig:
lint-actions: lint-actions:
$(GO) run $(ACTIONLINT_PACKAGE) $(GO) run $(ACTIONLINT_PACKAGE)
.PHONY: lint-templates
lint-templates: .venv
@poetry run djlint $(shell find templates -type f -iname '*.tmpl')
.PHONY: watch .PHONY: watch
watch: watch:
@bash build/watch.sh @bash build/watch.sh
@ -831,30 +837,18 @@ release-windows: | $(DIST_DIRS)
ifeq (,$(findstring gogit,$(TAGS))) ifeq (,$(findstring gogit,$(TAGS)))
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit . CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -buildmode exe -dest $(DIST)/binaries -tags 'osusergo gogit $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets 'windows/*' -out gitea-$(VERSION)-gogit .
endif endif
ifeq ($(CI),true)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-linux .PHONY: release-linux
release-linux: | $(DIST_DIRS) release-linux: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) . CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '-linkmode external -extldflags "-static" $(LDFLAGS)' -targets '$(LINUX_ARCHS)' -out gitea-$(VERSION) .
ifeq ($(CI),true)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-darwin .PHONY: release-darwin
release-darwin: | $(DIST_DIRS) release-darwin: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) . CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'darwin-10.12/amd64,darwin-10.12/arm64' -out gitea-$(VERSION) .
ifeq ($(CI),true)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-freebsd .PHONY: release-freebsd
release-freebsd: | $(DIST_DIRS) release-freebsd: | $(DIST_DIRS)
CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) . CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) run $(XGO_PACKAGE) -go $(XGO_VERSION) -dest $(DIST)/binaries -tags 'netgo osusergo $(TAGS)' -ldflags '$(LDFLAGS)' -targets 'freebsd/amd64' -out gitea-$(VERSION) .
ifeq ($(CI),true)
cp /build/* $(DIST)/binaries
endif
.PHONY: release-copy .PHONY: release-copy
release-copy: | $(DIST_DIRS) release-copy: | $(DIST_DIRS)
@ -893,7 +887,10 @@ deps-docs:
fi fi
.PHONY: deps .PHONY: deps
deps: deps-frontend deps-backend deps-tools deps-docs deps: deps-frontend deps-backend deps-tools deps-docs deps-py
.PHONY: deps-py
deps-py: .venv
.PHONY: deps-frontend .PHONY: deps-frontend
deps-frontend: node_modules deps-frontend: node_modules
@ -920,6 +917,10 @@ node_modules: package-lock.json
npm install --no-save npm install --no-save
@touch node_modules @touch node_modules
.venv: poetry.lock
poetry install
@touch .venv
.PHONY: npm-update .PHONY: npm-update
npm-update: node-check | node_modules npm-update: node-check | node_modules
npx updates -cu npx updates -cu

View File

@ -353,9 +353,9 @@ func runDump(ctx *cli.Context) error {
} }
excludes = append(excludes, setting.RepoRootPath) excludes = append(excludes, setting.RepoRootPath)
excludes = append(excludes, setting.LFS.Path) excludes = append(excludes, setting.LFS.Storage.Path)
excludes = append(excludes, setting.Attachment.Path) excludes = append(excludes, setting.Attachment.Storage.Path)
excludes = append(excludes, setting.Packages.Path) excludes = append(excludes, setting.Packages.Storage.Path)
excludes = append(excludes, setting.Log.RootPath) excludes = append(excludes, setting.Log.RootPath)
excludes = append(excludes, absFileName) excludes = append(excludes, absFileName)
if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil { if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil {

View File

@ -179,7 +179,7 @@ func runMigrateStorage(ctx *cli.Context) error {
switch strings.ToLower(ctx.String("storage")) { switch strings.ToLower(ctx.String("storage")) {
case "": case "":
fallthrough fallthrough
case string(storage.LocalStorageType): case string(setting.LocalStorageType):
p := ctx.String("path") p := ctx.String("path")
if p == "" { if p == "" {
log.Fatal("Path must be given when storage is loal") log.Fatal("Path must be given when storage is loal")
@ -187,22 +187,24 @@ func runMigrateStorage(ctx *cli.Context) error {
} }
dstStorage, err = storage.NewLocalStorage( dstStorage, err = storage.NewLocalStorage(
stdCtx, stdCtx,
storage.LocalStorageConfig{ &setting.Storage{
Path: p, Path: p,
}) })
case string(storage.MinioStorageType): case string(setting.MinioStorageType):
dstStorage, err = storage.NewMinioStorage( dstStorage, err = storage.NewMinioStorage(
stdCtx, stdCtx,
storage.MinioStorageConfig{ &setting.Storage{
Endpoint: ctx.String("minio-endpoint"), MinioConfig: setting.MinioStorageConfig{
AccessKeyID: ctx.String("minio-access-key-id"), Endpoint: ctx.String("minio-endpoint"),
SecretAccessKey: ctx.String("minio-secret-access-key"), AccessKeyID: ctx.String("minio-access-key-id"),
Bucket: ctx.String("minio-bucket"), SecretAccessKey: ctx.String("minio-secret-access-key"),
Location: ctx.String("minio-location"), Bucket: ctx.String("minio-bucket"),
BasePath: ctx.String("minio-base-path"), Location: ctx.String("minio-location"),
UseSSL: ctx.Bool("minio-use-ssl"), BasePath: ctx.String("minio-base-path"),
InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"), UseSSL: ctx.Bool("minio-use-ssl"),
ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"), InsecureSkipVerify: ctx.Bool("minio-insecure-skip-verify"),
ChecksumAlgorithm: ctx.String("minio-checksum-algorithm"),
},
}) })
default: default:
return fmt.Errorf("unsupported storage type: %s", ctx.String("storage")) return fmt.Errorf("unsupported storage type: %s", ctx.String("storage"))

View File

@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
packages_module "code.gitea.io/gitea/modules/packages" packages_module "code.gitea.io/gitea/modules/packages"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
@ -57,7 +58,7 @@ func TestMigratePackages(t *testing.T) {
dstStorage, err := storage.NewLocalStorage( dstStorage, err := storage.NewLocalStorage(
ctx, ctx,
storage.LocalStorageConfig{ &setting.Storage{
Path: p, Path: p,
}) })
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -191,7 +191,7 @@ func runWeb(ctx *cli.Context) error {
} }
// Set up Chi routes // Set up Chi routes
c := routers.NormalRoutes(graceful.GetManager().HammerContext()) c := routers.NormalRoutes()
err := listen(c, true) err := listen(c, true)
<-graceful.GetManager().Done() <-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid()) log.Info("PID: %d Gitea Web Finished", os.Getpid())

View File

@ -2392,6 +2392,10 @@ LEVEL = Info
;; Enable/Disable package registry capabilities ;; Enable/Disable package registry capabilities
;ENABLED = true ;ENABLED = true
;; ;;
;STORAGE_TYPE = local
;; override the minio base path if storage type is minio
;MINIO_BASE_PATH = packages/
;;
;; Path for chunked uploads. Defaults to APP_DATA_PATH + `tmp/package-upload` ;; Path for chunked uploads. Defaults to APP_DATA_PATH + `tmp/package-upload`
;CHUNKED_UPLOAD_PATH = tmp/package-upload ;CHUNKED_UPLOAD_PATH = tmp/package-upload
;; ;;
@ -2452,6 +2456,19 @@ LEVEL = Info
;; storage type ;; storage type
;STORAGE_TYPE = local ;STORAGE_TYPE = local
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; repo-archive storage will override storage
;;
;[repo-archive]
;STORAGE_TYPE = local
;;
;; Where your lfs files reside, default is data/lfs.
;PATH = data/repo-archive
;;
;; override the minio base path if storage type is minio
;MINIO_BASE_PATH = repo-archive/
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; settings for repository archives, will override storage setting ;; settings for repository archives, will override storage setting
@ -2471,6 +2488,9 @@ LEVEL = Info
;; ;;
;; Where your lfs files reside, default is data/lfs. ;; Where your lfs files reside, default is data/lfs.
;PATH = data/lfs ;PATH = data/lfs
;;
;; override the minio base path if storage type is minio
;MINIO_BASE_PATH = lfs/
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -2520,6 +2540,7 @@ LEVEL = Info
; [actions] ; [actions]
;; Enable/Disable actions capabilities ;; Enable/Disable actions capabilities
;ENABLED = false ;ENABLED = false
;;
;; Default address to get action plugins, e.g. the default value means downloading from "https://gitea.com/actions/checkout" for "uses: actions/checkout@v3" ;; Default address to get action plugins, e.g. the default value means downloading from "https://gitea.com/actions/checkout" for "uses: actions/checkout@v3"
;DEFAULT_ACTIONS_URL = https://gitea.com ;DEFAULT_ACTIONS_URL = https://gitea.com

View File

@ -1254,8 +1254,9 @@ is `data/lfs` and the default of `MINIO_BASE_PATH` is `lfs/`.
## Storage (`storage`) ## Storage (`storage`)
Default storage configuration for attachments, lfs, avatars and etc. Default storage configuration for attachments, lfs, avatars, repo-avatars, repo-archive, packages, actions_log, actions_artifact.
- `STORAGE_TYPE`: **local**: Storage type, `local` for local disk or `minio` for s3 compatible object storage service.
- `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing. - `SERVE_DIRECT`: **false**: Allows the storage driver to redirect to authenticated URLs to serve files directly. Currently, only Minio/S3 is supported via signed URLs, local does nothing.
- `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio` - `MINIO_ENDPOINT`: **localhost:9000**: Minio endpoint to connect only available when `STORAGE_TYPE` is `minio`
- `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio` - `MINIO_ACCESS_KEY_ID`: Minio accessKeyID to connect only available when `STORAGE_TYPE` is `minio`
@ -1265,9 +1266,56 @@ Default storage configuration for attachments, lfs, avatars and etc.
- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio` - `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio`
- `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio` - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio`
And you can also define a customize storage like below: The recommanded storage configuration for minio like below:
```ini ```ini
[storage]
STORAGE_TYPE = minio
; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
MINIO_ENDPOINT = localhost:9000
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
MINIO_ACCESS_KEY_ID =
; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
MINIO_SECRET_ACCESS_KEY =
; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
MINIO_BUCKET = gitea
; Minio location to create bucket only available when STORAGE_TYPE is `minio`
MINIO_LOCATION = us-east-1
; Minio enabled ssl only available when STORAGE_TYPE is `minio`
MINIO_USE_SSL = false
; Minio skip SSL verification available when STORAGE_TYPE is `minio`
MINIO_INSECURE_SKIP_VERIFY = false
SERVE_DIRECT = true
```
Defaultly every storage has their default base path like below
| storage | default base path |
| ----------------- | ------------------ |
| attachments | attachments/ |
| lfs | lfs/ |
| avatars | avatars/ |
| repo-avatars | repo-avatars/ |
| repo-archive | repo-archive/ |
| packages | packages/ |
| actions_log | actions_log/ |
| actions_artifacts | actions_artifacts/ |
And bucket, basepath or `SERVE_DIRECT` could be special or overrided, if you want to use a different you can:
```ini
[storage.actions_log]
MINIO_BUCKET = gitea_actions_log
SERVE_DIRECT = true
MINIO_BASE_PATH = my_actions_log/ ; default is actions_log/ if blank
```
If you want to customerize a different storage for `lfs` if above default storage defined
```ini
[lfs]
STORAGE_TYPE = my_minio
[storage.my_minio] [storage.my_minio]
STORAGE_TYPE = minio STORAGE_TYPE = minio
; Minio endpoint to connect only available when STORAGE_TYPE is `minio` ; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
@ -1286,8 +1334,6 @@ MINIO_USE_SSL = false
MINIO_INSECURE_SKIP_VERIFY = false MINIO_INSECURE_SKIP_VERIFY = false
``` ```
And used by `[attachment]`, `[lfs]` and etc. as `STORAGE_TYPE`.
## Repository Archive Storage (`storage.repo-archive`) ## Repository Archive Storage (`storage.repo-archive`)
Configuration for repository archive storage. It will inherit from default `[storage]` or Configuration for repository archive storage. It will inherit from default `[storage]` or
@ -1306,6 +1352,11 @@ is `data/repo-archive` and the default of `MINIO_BASE_PATH` is `repo-archive/`.
- `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio` - `MINIO_USE_SSL`: **false**: Minio enabled ssl only available when `STORAGE_TYPE` is `minio`
- `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio` - `MINIO_INSECURE_SKIP_VERIFY`: **false**: Minio skip SSL verification available when STORAGE_TYPE is `minio`
## Repository Archives (`repo-archive`)
- `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`
- `MINIO_BASE_PATH`: **repo-archive/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio`
## Proxy (`proxy`) ## Proxy (`proxy`)
- `PROXY_ENABLED`: **false**: Enable the proxy if true, all requests to external via HTTP will be affected, if false, no proxy will be used even environment http_proxy/https_proxy - `PROXY_ENABLED`: **false**: Enable the proxy if true, all requests to external via HTTP will be affected, if false, no proxy will be used even environment http_proxy/https_proxy
@ -1324,6 +1375,8 @@ PROXY_HOSTS = *.github.com
- `ENABLED`: **false**: Enable/Disable actions capabilities - `ENABLED`: **false**: Enable/Disable actions capabilities
- `DEFAULT_ACTIONS_URL`: **https://gitea.com**: Default address to get action plugins, e.g. the default value means downloading from "<https://gitea.com/actions/checkout>" for "uses: actions/checkout@v3" - `DEFAULT_ACTIONS_URL`: **https://gitea.com**: Default address to get action plugins, e.g. the default value means downloading from "<https://gitea.com/actions/checkout>" for "uses: actions/checkout@v3"
- `STORAGE_TYPE`: **local**: Storage type for actions logs, `local` for local disk or `minio` for s3 compatible object storage service, default is `local` or other name defined with `[storage.xxx]`
- `MINIO_BASE_PATH`: **actions_log/**: Minio base path on the bucket only available when STORAGE_TYPE is `minio`
`DEFAULT_ACTIONS_URL` indicates where should we find the relative path action plugin. i.e. when use an action in a workflow file like `DEFAULT_ACTIONS_URL` indicates where should we find the relative path action plugin. i.e. when use an action in a workflow file like

View File

@ -414,7 +414,7 @@ LFS 的存储配置。 如果 `STORAGE_TYPE` 为空,则此配置将从 `[stora
## Storage (`storage`) ## Storage (`storage`)
Attachments, lfs, avatars and etc 的默认存储配置。 Attachments, lfs, avatars, repo-avatars, repo-archive, packages, actions_log, actions_artifact 的默认存储配置。
- `STORAGE_TYPE`: **local**: 附件存储类型,`local` 将存储到本地文件夹, `minio` 将存储到 s3 兼容的对象存储服务中。 - `STORAGE_TYPE`: **local**: 附件存储类型,`local` 将存储到本地文件夹, `minio` 将存储到 s3 兼容的对象存储服务中。
- `SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。 - `SERVE_DIRECT`: **false**: 允许直接重定向到存储系统。当前,仅 Minio/S3 是支持的。
@ -425,9 +425,59 @@ Attachments, lfs, avatars and etc 的默认存储配置。
- `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket仅当 `STORAGE_TYPE``minio` 时有效。 - `MINIO_LOCATION`: **us-east-1**: Minio location to create bucket仅当 `STORAGE_TYPE``minio` 时有效。
- `MINIO_USE_SSL`: **false**: Minio enabled ssl仅当 `STORAGE_TYPE``minio` 时有效。 - `MINIO_USE_SSL`: **false**: Minio enabled ssl仅当 `STORAGE_TYPE``minio` 时有效。
你也可以自定义一个存储的名字如下: 以下为推荐的 recommanded storage configuration for minio like below:
```ini ```ini
[storage]
STORAGE_TYPE = minio
; uncomment when STORAGE_TYPE = local
; PATH = storage root path
; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
MINIO_ENDPOINT = localhost:9000
; Minio accessKeyID to connect only available when STORAGE_TYPE is `minio`
MINIO_ACCESS_KEY_ID =
; Minio secretAccessKey to connect only available when STORAGE_TYPE is `minio`
MINIO_SECRET_ACCESS_KEY =
; Minio bucket to store the attachments only available when STORAGE_TYPE is `minio`
MINIO_BUCKET = gitea
; Minio location to create bucket only available when STORAGE_TYPE is `minio`
MINIO_LOCATION = us-east-1
; Minio enabled ssl only available when STORAGE_TYPE is `minio`
MINIO_USE_SSL = false
; Minio skip SSL verification available when STORAGE_TYPE is `minio`
MINIO_INSECURE_SKIP_VERIFY = false
SERVE_DIRECT = true
```
默认的,每一个存储都会有各自默认的 BasePath 在同一个minio中默认值如下
| storage | default base path |
| ----------------- | ------------------ |
| attachments | attachments/ |
| lfs | lfs/ |
| avatars | avatars/ |
| repo-avatars | repo-avatars/ |
| repo-archive | repo-archive/ |
| packages | packages/ |
| actions_log | actions_log/ |
| actions_artifacts | actions_artifacts/ |
同时 bucket, basepath or `SERVE_DIRECT` 是可以被覆写的,像如下所示:
```ini
[storage.actions_log]
MINIO_BUCKET = gitea_actions_log
SERVE_DIRECT = true
MINIO_BASE_PATH = my_actions_log/ ; default is actions_log/ if blank
```
当然你也可以完全自定义,像如下
```ini
[lfs]
STORAGE_TYPE = my_minio
MINIO_BASE_PATH = my_lfs_basepath
[storage.my_minio] [storage.my_minio]
STORAGE_TYPE = minio STORAGE_TYPE = minio
; Minio endpoint to connect only available when STORAGE_TYPE is `minio` ; Minio endpoint to connect only available when STORAGE_TYPE is `minio`
@ -444,10 +494,9 @@ MINIO_LOCATION = us-east-1
MINIO_USE_SSL = false MINIO_USE_SSL = false
; Minio skip SSL verification available when STORAGE_TYPE is `minio` ; Minio skip SSL verification available when STORAGE_TYPE is `minio`
MINIO_INSECURE_SKIP_VERIFY = false MINIO_INSECURE_SKIP_VERIFY = false
SERVE_DIRECT = true
``` ```
然后你在 `[attachment]`, `[lfs]` 等中可以把这个名字用作 `STORAGE_TYPE` 的值。
## Repository Archive Storage (`storage.repo-archive`) ## Repository Archive Storage (`storage.repo-archive`)
Repository archive 的存储配置。 如果 `STORAGE_TYPE` 为空,则此配置将从 `[storage]` 继承。如果不为 `local` 或者 `minio` 而为 `xxx` 则从 `[storage.xxx]` 继承。当继承时, `PATH` 默认为 `data/repo-archive``MINIO_BASE_PATH` 默认为 `repo-archive/` Repository archive 的存储配置。 如果 `STORAGE_TYPE` 为空,则此配置将从 `[storage]` 继承。如果不为 `local` 或者 `minio` 而为 `xxx` 则从 `[storage.xxx]` 继承。当继承时, `PATH` 默认为 `data/repo-archive``MINIO_BASE_PATH` 默认为 `repo-archive/`

View File

@ -48,6 +48,9 @@ Gitea uses `gofmt` to format source code. However, the results of
recommended to install the version of Go that our continuous integration is recommended to install the version of Go that our continuous integration is
running. As of last update, the Go version should be {{< go-version >}}. running. As of last update, the Go version should be {{< go-version >}}.
To lint the template files, ensure [Python](https://www.python.org/) and
[Poetry](https://python-poetry.org/) are installed.
## Installing Make ## Installing Make
Gitea makes heavy use of Make to automate tasks and improve development. This Gitea makes heavy use of Make to automate tasks and improve development. This

View File

@ -25,21 +25,19 @@ For more help resources, check all [Support Options]({{< relref "doc/help/suppor
{{< toc >}} {{< toc >}}
## Difference between 1.x and 1.x.x downloads ## Difference between 1.x and 1.x.x downloads, how can I get latest stable release with bug fixes?
Version 1.7.x will be used for this example. Version 1.20.x will be used for this example.
**NOTE:** this example applies to Docker images as well! On our [downloads page](https://dl.gitea.com/gitea/) you will see a 1.20 directory, as well as directories for 1.20.0, 1.20.1.
On our [downloads page](https://dl.gitea.io/gitea/) you will see a 1.7 directory, as well as directories for 1.7.0, 1.7.1, 1.7.2, 1.7.3, 1.7.4, 1.7.5, and 1.7.6. The 1.20 directory is the nightly build, which is built on each merged commit to the [`release/v1.20`](https://github.com/go-gitea/gitea/tree/release/v1.20) branch.
The 1.7 and 1.7.0 directories are **not** the same. The 1.7 directory is built on each merged commit to the [`release/v1.7`](https://github.com/go-gitea/gitea/tree/release/v1.7) branch. The 1.20.0 directory is a release build that was created when the [`v1.20.0`](https://github.com/go-gitea/gitea/releases/tag/v1.20.0) tag was created.
The 1.7.0 directory, however, is a build that was created when the [`v1.7.0`](https://github.com/go-gitea/gitea/releases/tag/v1.7.0) tag was created. The nightly builds (1.x) downloads will change as commits are merged to their respective branch, they contain the latest changes/fixes before a tag release is built.
This means that 1.x downloads will change as commits are merged to their respective branch (think of it as a separate "main" branch for each release). If a bug fix is targeted on 1.20.1 but 1.20.1 is not released yet, you can get the "1.20-nightly" build to get the bug fix.
On the other hand, 1.x.x downloads should never change.
## How to migrate from Gogs/GitHub/etc. to Gitea ## How to migrate from Gogs/GitHub/etc. to Gitea
@ -404,14 +402,6 @@ You will also need to change the app.ini database charset to `CHARSET=utf8mb4`.
Gitea requires the system or browser to have one of the supported Emoji fonts installed, which are Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji and Twemoji Mozilla. Generally, the operating system should already provide one of these fonts, but especially on Linux, it may be necessary to install them manually. Gitea requires the system or browser to have one of the supported Emoji fonts installed, which are Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji and Twemoji Mozilla. Generally, the operating system should already provide one of these fonts, but especially on Linux, it may be necessary to install them manually.
## Stdout logging on SystemD and Docker
Stdout on systemd goes to the journal by default. Try using `journalctl`, `journalctl -u gitea`, or `journalctl <path-to-gitea-binary>`.
Similarly, stdout on docker can be viewed using `docker logs <container>`.
To collect logs for help and issue report, see [Support Options]({{< relref "doc/help/support.en-us.md" >}}).
## Initial logging ## Initial logging
Before Gitea has read the configuration file and set-up its logging it will log a number of things to stdout in order to help debug things if logging does not work. Before Gitea has read the configuration file and set-up its logging it will log a number of things to stdout in order to help debug things if logging does not work.
@ -454,12 +444,6 @@ gitea doctor recreate-table
It is highly recommended to back-up your database before running these commands. It is highly recommended to back-up your database before running these commands.
## Why are tabs/indents wrong when viewing files
If you are using Cloudflare, turn off the auto-minify option in the dashboard.
`Speed` -> `Optimization` -> Uncheck `HTML` within the `Auto-Minify` settings.
## How to adopt repositories from disk ## How to adopt repositories from disk
- Add your (bare) repositories to the correct spot for your configuration (`repository.ROOT`), ensuring they are in the correct layout `<REPO_ROOT>/[user]/[repo].git`. - Add your (bare) repositories to the correct spot for your configuration (`repository.ROOT`), ensuring they are in the correct layout `<REPO_ROOT>/[user]/[repo].git`.
@ -470,3 +454,17 @@ If you are using Cloudflare, turn off the auto-minify option in the dashboard.
- Users can also be given similar permissions via config [`ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES`]({{< relref "doc/administration/config-cheat-sheet.en-us.md#repository" >}}). - Users can also be given similar permissions via config [`ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES`]({{< relref "doc/administration/config-cheat-sheet.en-us.md#repository" >}}).
- If the above steps are done correctly, you should be able to select repositories to adopt. - If the above steps are done correctly, you should be able to select repositories to adopt.
- If no repositories are found, enable [debug logging]({{< relref "doc/administration/config-cheat-sheet.en-us.md#repository" >}}) to check for any specific errors. - If no repositories are found, enable [debug logging]({{< relref "doc/administration/config-cheat-sheet.en-us.md#repository" >}}) to check for any specific errors.
## Gitea can't start on NFS
In most cases, it's caused by broken NFS lock system. You can try to stop Gitea process and
run `flock -n /data-nfs/gitea/queues/LOCK echo 'lock acquired'` to see whether the lock can be acquired immediately.
If the lock can't be acquired, NFS might report some errors like `lockd: cannot monitor node-3, statd: server rpc.statd not responding, timed out` in its server logs.
Then the NFS lock could be reset by:
```bash
# /etc/init.d/nfs stop
# rm -rf /var/lib/nfs/sm/*
# /etc/init.d/nfs start
```

View File

@ -17,6 +17,7 @@ menu:
# Support Options # Support Options
- [Paid Commercial Support](https://about.gitea.com/)
- [Discord](https://discord.gg/Gitea) - [Discord](https://discord.gg/Gitea)
- [Discourse Forum](https://discourse.gitea.io/) - [Discourse Forum](https://discourse.gitea.io/)
@ -35,30 +36,13 @@ menu:
[log] [log]
LEVEL=debug LEVEL=debug
MODE=console,file MODE=console,file
ROUTER=console,file
XORM=console,file
ENABLE_XORM_LOG=true
FILE_NAME=gitea.log
[log.file.router]
FILE_NAME=router.log
[log.file.xorm]
FILE_NAME=xorm.log
``` ```
3. Any error messages you are seeing. 3. Any error messages you are seeing.
4. When possible, try to replicate the issue on [try.gitea.io](https://try.gitea.io) and include steps so that others can reproduce the issue. 4. When possible, try to replicate the issue on [try.gitea.io](https://try.gitea.io) and include steps so that others can reproduce the issue.
- This will greatly improve the chance that the root of the issue can be quickly discovered and resolved. - This will greatly improve the chance that the root of the issue can be quickly discovered and resolved.
5. If you meet slow/hanging/deadlock problems, please report the stack trace when the problem occurs: 5. If you encounter slow/hanging/deadlock problems, please report the stack trace when the problem occurs.
1. Enable pprof in `app.ini` and restart Gitea Go to the "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report".
```ini
[server]
ENABLE_PPROF = true
```
2. Trigger the bug, when Gitea gets stuck, use curl or browser to visit: `http://127.0.0.1:6060/debug/pprof/goroutine?debug=1` (IP must be `127.0.0.1` and port must be `6060`).
3. If you are using Docker, please use `docker exec -it <container-name> curl "http://127.0.0.1:6060/debug/pprof/goroutine?debug=1"`.
4. Report the output (the stack trace doesn't contain sensitive data)
## Bugs ## Bugs

View File

@ -206,3 +206,61 @@ Similarly, a script for zsh-completion can be found at [`contrib/autocompletion/
`.zshrc`. `.zshrc`.
YMMV and these scripts may need further improvement. YMMV and these scripts may need further improvement.
## Compile or cross-compile using Linux with Zig
Follow [Getting Started of Zig](https://ziglang.org/learn/getting-started/#installing-zig) to install zig.
- Compile (Linux ➝ Linux)
```sh
CC="zig cc -target x86_64-linux-gnu" \
CGO_ENABLED=1 \
CGO_CFLAGS="-O2 -g -pthread" \
CGO_LDFLAGS="-linkmode=external -v"
GOOS=linux \
GOARCH=amd64 \
TAGS="bindata sqlite sqlite_unlock_notify" \
make build
```
- Cross-compile (Linux ➝ Windows)
```sh
CC="zig cc -target x86_64-windows-gnu" \
CGO_ENABLED=1 \
CGO_CFLAGS="-O2 -g -pthread" \
GOOS=windows \
GOARCH=amd64 \
TAGS="bindata sqlite sqlite_unlock_notify" \
make build
```
## Compile or cross-compile with Zig using Windows
Compile with `GIT BASH`.
- Compile (Windows ➝ Windows)
```sh
CC="zig cc -target x86_64-windows-gnu" \
CGO_ENABLED=1 \
CGO_CFLAGS="-O2 -g -pthread" \
GOOS=windows \
GOARCH=amd64 \
TAGS="bindata sqlite sqlite_unlock_notify" \
make build
```
- Cross-compile (Windows ➝ Linux)
```sh
CC="zig cc -target x86_64-linux-gnu" \
CGO_ENABLED=1 \
CGO_CFLAGS="-O2 -g -pthread" \
CGO_LDFLAGS="-linkmode=external -v"
GOOS=linux \
GOARCH=amd64 \
TAGS="bindata sqlite sqlite_unlock_notify" \
make build
```

View File

@ -103,3 +103,61 @@ GOOS=linux GOARCH=arm64 make build
```bash ```bash
CC=aarch64-unknown-linux-gnu-gcc GOOS=linux GOARCH=arm64 TAGS="bindata sqlite sqlite_unlock_notify" make build CC=aarch64-unknown-linux-gnu-gcc GOOS=linux GOARCH=arm64 TAGS="bindata sqlite sqlite_unlock_notify" make build
``` ```
## 使用Linux与Zig编译或交叉编译
按照[Getting Started of Zig](https://ziglang.org/learn/getting-started/#installing-zig)来安装zig。
- 编译(Linux ➝ Linux)
```sh
CC="zig cc -target x86_64-linux-gnu" \
CGO_ENABLED=1 \
CGO_CFLAGS="-O2 -g -pthread" \
CGO_LDFLAGS="-linkmode=external -v"
GOOS=linux \
GOARCH=amd64 \
TAGS="bindata sqlite sqlite_unlock_notify" \
make build
```
- 交叉编译(Linux ➝ Windows)
```sh
CC="zig cc -target x86_64-windows-gnu" \
CGO_ENABLED=1 \
CGO_CFLAGS="-O2 -g -pthread" \
GOOS=windows \
GOARCH=amd64 \
TAGS="bindata sqlite sqlite_unlock_notify" \
make build
```
## 使用Windows与Zig编译或交叉编译
使用`GIT BASH`编译。
- 编译(Windows ➝ Windows)
```sh
CC="zig cc -target x86_64-windows-gnu" \
CGO_ENABLED=1 \
CGO_CFLAGS="-O2 -g -pthread" \
GOOS=windows \
GOARCH=amd64 \
TAGS="bindata sqlite sqlite_unlock_notify" \
make build
```
- 交叉编译(Windows ➝ Linux)
```sh
CC="zig cc -target x86_64-linux-gnu" \
CGO_ENABLED=1 \
CGO_CFLAGS="-O2 -g -pthread" \
CGO_LDFLAGS="-linkmode=external -v"
GOOS=linux \
GOARCH=amd64 \
TAGS="bindata sqlite sqlite_unlock_notify" \
make build
```

View File

@ -56,3 +56,13 @@ To deploy Gitea to Linode, have a look at the [Linode Marketplace](https://www.l
[alwaysdata](https://www.alwaysdata.com/) has Gitea as an app in their marketplace. [alwaysdata](https://www.alwaysdata.com/) has Gitea as an app in their marketplace.
To deploy Gitea to alwaysdata, have a look at the [alwaysdata Marketplace](https://www.alwaysdata.com/en/marketplace/gitea/). To deploy Gitea to alwaysdata, have a look at the [alwaysdata Marketplace](https://www.alwaysdata.com/en/marketplace/gitea/).
## Exoscale
[Exoscale](https://www.exoscale.com/) provides Gitea managed by [Glasskube](https://glasskube.eu/) in their marketplace.
Exoscale is a European cloud service provider.
The package is maintained and update via the open source [Glasskube Kubernetes Operator](https://github.com/glasskube/operator).
To deploy Gitea to Exoscale, have a look at the [Exoscale Marketplace](https://www.exoscale.com/marketplace/listing/glasskube-gitea/).

View File

@ -76,6 +76,12 @@ The default configuration is safe to use without any modification, so you can ju
./act_runner --config config.yaml [command] ./act_runner --config config.yaml [command]
``` ```
You could also generate config file with docker:
```bash
docker run --entrypoint="" --rm -it gitea/act_runner:latest act_runner generate-config > config.yaml
```
When you are using the docker image, you can specify the configuration file by using the `CONFIG_FILE` environment variable. Make sure that the file is mounted into the container as a volume: When you are using the docker image, you can specify the configuration file by using the `CONFIG_FILE` environment variable. Make sure that the file is mounted into the container as a volume:
```bash ```bash
@ -172,6 +178,27 @@ It is because the act runner will run jobs in docker containers, so it needs to
As mentioned, you can remove it if you want to run jobs in the host directly. As mentioned, you can remove it if you want to run jobs in the host directly.
To be clear, the "host" actually means the container which is running the act runner now, instead of the host machine. To be clear, the "host" actually means the container which is running the act runner now, instead of the host machine.
### Set up the runner using docker compose
You could also set up the runner using the following `docker-compose.yml`:
```yml
version: "3.8"
services:
runner:
image: gitea/act_runner:nightly
environment:
CONFIG_FILE: /config.yaml
GITEA_INSTANCE_URL: "${INSTANCE_URL}"
GITEA_RUNNER_REGISTRATION_TOKEN: "${REGISTRATION_TOKEN}"
GITEA_RUNNER_NAME: "${RUNNER_NAME}"
GITEA_RUNNER_LABELS: "${RUNNER_LABELS}"
volumes:
- ./config.yaml:/config.yaml
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock
```
### Configuring cache when starting a Runner using docker image ### Configuring cache when starting a Runner using docker image
If you do not intend to use `actions/cache` in workflow, you can ignore this section. If you do not intend to use `actions/cache` in workflow, you can ignore this section.

View File

@ -76,6 +76,12 @@ docker pull gitea/act_runner:nightly # for the latest nightly build
./act_runner --config config.yaml [command] ./act_runner --config config.yaml [command]
``` ```
您亦可以如下使用 docker 创建配置文件:
```bash
docker run --entrypoint="" --rm -it gitea/act_runner:latest act_runner generate-config > config.yaml
```
当使用Docker镜像时可以使用`CONFIG_FILE`环境变量指定配置文件。确保将文件作为卷挂载到容器中: 当使用Docker镜像时可以使用`CONFIG_FILE`环境变量指定配置文件。确保将文件作为卷挂载到容器中:
```bash ```bash
@ -169,6 +175,27 @@ docker run \
如前所述如果要在主机上直接运行Job可以将其移除。 如前所述如果要在主机上直接运行Job可以将其移除。
需要明确的是,这里的 "主机" 实际上指的是当前运行 Act Runner的容器而不是主机机器本身。 需要明确的是,这里的 "主机" 实际上指的是当前运行 Act Runner的容器而不是主机机器本身。
### 使用 Docker compose 运行 Runner
您亦可使用如下的 `docker-compose.yml`:
```yml
version: "3.8"
services:
runner:
image: gitea/act_runner:nightly
environment:
CONFIG_FILE: /config.yaml
GITEA_INSTANCE_URL: "${INSTANCE_URL}"
GITEA_RUNNER_REGISTRATION_TOKEN: "${REGISTRATION_TOKEN}"
GITEA_RUNNER_NAME: "${RUNNER_NAME}"
GITEA_RUNNER_LABELS: "${RUNNER_LABELS}"
volumes:
- ./config.yaml:/config.yaml
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock
```
### 当您使用 Docker 镜像启动 Runner如何配置 Cache ### 当您使用 Docker 镜像启动 Runner如何配置 Cache
如果你不打算在工作流中使用 `actions/cache`,你可以忽略本段。 如果你不打算在工作流中使用 `actions/cache`,你可以忽略本段。

2
go.mod
View File

@ -3,7 +3,7 @@ module code.gitea.io/gitea
go 1.20 go 1.20
require ( require (
code.gitea.io/actions-proto-go v0.2.1 code.gitea.io/actions-proto-go v0.3.0
code.gitea.io/gitea-vet v0.2.2 code.gitea.io/gitea-vet v0.2.2
code.gitea.io/sdk/gitea v0.15.1 code.gitea.io/sdk/gitea v0.15.1
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570

4
go.sum
View File

@ -40,8 +40,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
code.gitea.io/actions-proto-go v0.2.1 h1:ToMN/8thz2q10TuCq8dL2d8mI+/pWpJcHCvG+TELwa0= code.gitea.io/actions-proto-go v0.3.0 h1:9Tvg8+TaaCXPKi6EnWl9vVgs2VZsj1Cs5afnsHa4AmM=
code.gitea.io/actions-proto-go v0.2.1/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A= code.gitea.io/actions-proto-go v0.3.0/go.mod h1:00ys5QDo1iHN1tHNvvddAcy2W/g+425hQya1cCSvq9A=
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
code.gitea.io/gitea-vet v0.2.2 h1:TEOV/Glf38iGmKzKP0EB++Z5OSL4zGg3RrAvlwaMuvk= code.gitea.io/gitea-vet v0.2.2 h1:TEOV/Glf38iGmKzKP0EB++Z5OSL4zGg3RrAvlwaMuvk=
code.gitea.io/gitea-vet v0.2.2/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/gitea-vet v0.2.2/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=

View File

@ -43,10 +43,8 @@ type ActionRunner struct {
LastOnline timeutil.TimeStamp `xorm:"index"` LastOnline timeutil.TimeStamp `xorm:"index"`
LastActive timeutil.TimeStamp `xorm:"index"` LastActive timeutil.TimeStamp `xorm:"index"`
// Store OS and Artch. // Store labels defined in state file (default: .runner file) of `act_runner`
AgentLabels []string AgentLabels []string `xorm:"TEXT"`
// Store custom labes use defined.
CustomLabels []string
Created timeutil.TimeStamp `xorm:"created"` Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"` Updated timeutil.TimeStamp `xorm:"updated"`
@ -104,11 +102,6 @@ func (r *ActionRunner) IsOnline() bool {
return false return false
} }
// AllLabels returns agent and custom labels
func (r *ActionRunner) AllLabels() []string {
return append(r.AgentLabels, r.CustomLabels...)
}
// Editable checks if the runner is editable by the user // Editable checks if the runner is editable by the user
func (r *ActionRunner) Editable(ownerID, repoID int64) bool { func (r *ActionRunner) Editable(ownerID, repoID int64) bool {
if ownerID == 0 && repoID == 0 { if ownerID == 0 && repoID == 0 {

View File

@ -241,11 +241,9 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
// TODO: a more efficient way to filter labels // TODO: a more efficient way to filter labels
var job *ActionRunJob var job *ActionRunJob
labels := runner.AgentLabels log.Trace("runner labels: %v", runner.AgentLabels)
labels = append(labels, runner.CustomLabels...)
log.Trace("runner labels: %v", labels)
for _, v := range jobs { for _, v := range jobs {
if isSubset(labels, v.RunsOn) { if isSubset(runner.AgentLabels, v.RunsOn) {
job = v job = v
break break
} }

View File

@ -89,6 +89,33 @@ func mysqlGetNextResourceIndex(ctx context.Context, tableName string, groupID in
return idx, nil return idx, nil
} }
func mssqlGetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) {
if _, err := GetEngine(ctx).Exec(fmt.Sprintf(`
MERGE INTO %s WITH (HOLDLOCK) AS target
USING (SELECT %d AS group_id) AS source
(group_id)
ON target.group_id = source.group_id
WHEN MATCHED
THEN UPDATE
SET max_index = max_index + 1
WHEN NOT MATCHED
THEN INSERT (group_id, max_index)
VALUES (%d, 1);
`, tableName, groupID, groupID)); err != nil {
return 0, err
}
var idx int64
_, err := GetEngine(ctx).SQL(fmt.Sprintf("SELECT max_index FROM %s WHERE group_id = ?", tableName), groupID).Get(&idx)
if err != nil {
return 0, err
}
if idx == 0 {
return 0, errors.New("cannot get the correct index")
}
return idx, nil
}
// GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created // GetNextResourceIndex generates a resource index, it must run in the same transaction where the resource is created
func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) { func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64) (int64, error) {
switch { switch {
@ -96,6 +123,8 @@ func GetNextResourceIndex(ctx context.Context, tableName string, groupID int64)
return postgresGetNextResourceIndex(ctx, tableName, groupID) return postgresGetNextResourceIndex(ctx, tableName, groupID)
case setting.Database.Type.IsMySQL(): case setting.Database.Type.IsMySQL():
return mysqlGetNextResourceIndex(ctx, tableName, groupID) return mysqlGetNextResourceIndex(ctx, tableName, groupID)
case setting.Database.Type.IsMSSQL():
return mssqlGetNextResourceIndex(ctx, tableName, groupID)
} }
e := GetEngine(ctx) e := GetEngine(ctx)

View File

@ -83,13 +83,47 @@ func mysqlGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (i
return idx, nil return idx, nil
} }
func mssqlGetCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
if _, err := db.GetEngine(ctx).Exec(`
MERGE INTO commit_status_index WITH (HOLDLOCK) AS target
USING (SELECT ? AS repo_id, ? AS sha) AS source
(repo_id, sha)
ON target.repo_id = source.repo_id AND target.sha = source.sha
WHEN MATCHED
THEN UPDATE
SET max_index = max_index + 1
WHEN NOT MATCHED
THEN INSERT (repo_id, sha, max_index)
VALUES (?, ?, 1);
`, repoID, sha, repoID, sha); err != nil {
return 0, err
}
var idx int64
_, err := db.GetEngine(ctx).SQL("SELECT max_index FROM `commit_status_index` WHERE repo_id = ? AND sha = ?",
repoID, sha).Get(&idx)
if err != nil {
return 0, err
}
if idx == 0 {
return 0, errors.New("cannot get the correct index")
}
return idx, nil
}
// GetNextCommitStatusIndex retried 3 times to generate a resource index // GetNextCommitStatusIndex retried 3 times to generate a resource index
func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) { func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
if !git.IsValidSHAPattern(sha) {
return 0, git.ErrInvalidSHA{SHA: sha}
}
switch { switch {
case setting.Database.Type.IsPostgreSQL(): case setting.Database.Type.IsPostgreSQL():
return postgresGetCommitStatusIndex(ctx, repoID, sha) return postgresGetCommitStatusIndex(ctx, repoID, sha)
case setting.Database.Type.IsMySQL(): case setting.Database.Type.IsMySQL():
return mysqlGetCommitStatusIndex(ctx, repoID, sha) return mysqlGetCommitStatusIndex(ctx, repoID, sha)
case setting.Database.Type.IsMSSQL():
return mssqlGetCommitStatusIndex(ctx, repoID, sha)
} }
e := db.GetEngine(ctx) e := db.GetEngine(ctx)

View File

@ -920,7 +920,7 @@ func PullRequestCodeOwnersReview(ctx context.Context, pull *Issue, pr *PullReque
var data string var data string
for _, file := range files { for _, file := range files {
if blob, err := commit.GetBlobByPath(file); err == nil { if blob, err := commit.GetBlobByPath(file); err == nil {
data, err = blob.GetBlobContent() data, err = blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err == nil { if err == nil {
break break
} }

View File

@ -20,6 +20,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_18" "code.gitea.io/gitea/models/migrations/v1_18"
"code.gitea.io/gitea/models/migrations/v1_19" "code.gitea.io/gitea/models/migrations/v1_19"
"code.gitea.io/gitea/models/migrations/v1_20" "code.gitea.io/gitea/models/migrations/v1_20"
"code.gitea.io/gitea/models/migrations/v1_21"
"code.gitea.io/gitea/models/migrations/v1_6" "code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7" "code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8" "code.gitea.io/gitea/models/migrations/v1_8"
@ -493,13 +494,18 @@ var migrations = []Migration{
NewMigration("Add is_internal column to package", v1_20.AddIsInternalColumnToPackage), NewMigration("Add is_internal column to package", v1_20.AddIsInternalColumnToPackage),
// v257 -> v258 // v257 -> v258
NewMigration("Add Actions Artifact table", v1_20.CreateActionArtifactTable), NewMigration("Add Actions Artifact table", v1_20.CreateActionArtifactTable),
// v258 -> 259 // v258 -> v259
NewMigration("Add PinOrder Column", v1_20.AddPinOrderToIssue), NewMigration("Add PinOrder Column", v1_20.AddPinOrderToIssue),
// v259 -> 260 // v259 -> v260
NewMigration("Convert scoped access tokens", v1_20.ConvertScopedAccessTokens), NewMigration("Convert scoped access tokens", v1_20.ConvertScopedAccessTokens),
// Gitea 1.20.0 ends at 260
// v260 -> v261 // v260 -> v261
NewMigration("Add TimeEstimate to issue table", v1_20.AddTimeEstimateColumnToIssueTable), NewMigration("Drop custom_labels column of action_runner table", v1_21.DropCustomLabelsColumnOfActionRunner),
NewMigration("Add TimeTracked, TimeEstimate to comment table", v1_20.AddColumnsToCommentTable), // v261 -> v262
NewMigration("Add TimeEstimate to issue table", v1_21.AddTimeEstimateColumnToIssueTable),
NewMigration("Add TimeTracked, TimeEstimate to comment table", v1_21.AddColumnsToCommentTable),
} }
// GetCurrentDBVersion returns the current db version // GetCurrentDBVersion returns the current db version

View File

@ -53,7 +53,7 @@ func DeleteOrphanedAttachments(x *xorm.Engine) error {
for _, attachment := range attachments { for _, attachment := range attachments {
uuid := attachment.UUID uuid := attachment.UUID
if err := util.RemoveAll(filepath.Join(setting.Attachment.Path, uuid[0:1], uuid[1:2], uuid)); err != nil { if err := util.RemoveAll(filepath.Join(setting.Attachment.Storage.Path, uuid[0:1], uuid[1:2], uuid)); err != nil {
return err return err
} }
} }

View File

@ -30,7 +30,7 @@ func RemoveAttachmentMissedRepo(x *xorm.Engine) error {
for i := 0; i < len(attachments); i++ { for i := 0; i < len(attachments); i++ {
uuid := attachments[i].UUID uuid := attachments[i].UUID
if err = util.RemoveAll(filepath.Join(setting.Attachment.Path, uuid[0:1], uuid[1:2], uuid)); err != nil { if err = util.RemoveAll(filepath.Join(setting.Attachment.Storage.Path, uuid[0:1], uuid[1:2], uuid)); err != nil {
fmt.Printf("Error: %v", err) //nolint:forbidigo fmt.Printf("Error: %v", err) //nolint:forbidigo
} }
} }

View File

@ -61,7 +61,7 @@ func RenameExistingUserAvatarName(x *xorm.Engine) error {
for _, user := range users { for _, user := range users {
oldAvatar := user.Avatar oldAvatar := user.Avatar
if stat, err := os.Stat(filepath.Join(setting.Avatar.Path, oldAvatar)); err != nil || !stat.Mode().IsRegular() { if stat, err := os.Stat(filepath.Join(setting.Avatar.Storage.Path, oldAvatar)); err != nil || !stat.Mode().IsRegular() {
if err == nil { if err == nil {
err = fmt.Errorf("Error: \"%s\" is not a regular file", oldAvatar) err = fmt.Errorf("Error: \"%s\" is not a regular file", oldAvatar)
} }
@ -86,7 +86,7 @@ func RenameExistingUserAvatarName(x *xorm.Engine) error {
return fmt.Errorf("[user: %s] user table update: %w", user.LowerName, err) return fmt.Errorf("[user: %s] user table update: %w", user.LowerName, err)
} }
deleteList.Add(filepath.Join(setting.Avatar.Path, oldAvatar)) deleteList.Add(filepath.Join(setting.Avatar.Storage.Path, oldAvatar))
migrated++ migrated++
select { select {
case <-ticker.C: case <-ticker.C:
@ -135,7 +135,7 @@ func RenameExistingUserAvatarName(x *xorm.Engine) error {
// copyOldAvatarToNewLocation copies oldAvatar to newAvatarLocation // copyOldAvatarToNewLocation copies oldAvatar to newAvatarLocation
// and returns newAvatar location // and returns newAvatar location
func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) { func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) {
fr, err := os.Open(filepath.Join(setting.Avatar.Path, oldAvatar)) fr, err := os.Open(filepath.Join(setting.Avatar.Storage.Path, oldAvatar))
if err != nil { if err != nil {
return "", fmt.Errorf("os.Open: %w", err) return "", fmt.Errorf("os.Open: %w", err)
} }
@ -151,7 +151,7 @@ func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error)
return newAvatar, nil return newAvatar, nil
} }
if err := os.WriteFile(filepath.Join(setting.Avatar.Path, newAvatar), data, 0o666); err != nil { if err := os.WriteFile(filepath.Join(setting.Avatar.Storage.Path, newAvatar), data, 0o666); err != nil {
return "", fmt.Errorf("os.WriteFile: %w", err) return "", fmt.Errorf("os.WriteFile: %w", err)
} }

View File

@ -0,0 +1,14 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_21 //nolint
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
)
func TestMain(m *testing.M) {
base.MainTest(m)
}

View File

@ -0,0 +1,26 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_21 //nolint
import (
"code.gitea.io/gitea/models/migrations/base"
"xorm.io/xorm"
)
func DropCustomLabelsColumnOfActionRunner(x *xorm.Engine) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
// drop "custom_labels" cols
if err := base.DropTableColumns(sess, "action_runner", "custom_labels"); err != nil {
return err
}
return sess.Commit()
}

View File

@ -1,7 +1,7 @@
// Copyright 2023 The Gitea Authors. All rights reserved. // Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
package v1_20 //nolint package v1_21 //nolint
import ( import (
"xorm.io/xorm" "xorm.io/xorm"

View File

@ -262,6 +262,10 @@ func GetRepositories(ctx context.Context, actor *user_model.User, n int, last st
cond = cond.And(builder.Gt{"package_property.value": strings.ToLower(last)}) cond = cond.And(builder.Gt{"package_property.value": strings.ToLower(last)})
} }
if actor.IsGhost() {
actor = nil
}
cond = cond.And(user_model.BuildCanSeeUserCondition(actor)) cond = cond.And(user_model.BuildCanSeeUserCondition(actor))
sess := db.GetEngine(ctx). sess := db.GetEngine(ctx).

View File

@ -20,6 +20,8 @@ import (
"code.gitea.io/gitea/modules/httpcache" "code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web"
web_types "code.gitea.io/gitea/modules/web/types"
"gitea.com/go-chi/cache" "gitea.com/go-chi/cache"
) )
@ -41,6 +43,12 @@ type APIContext struct {
Package *Package Package *Package
} }
func init() {
web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(apiContextKey).(*APIContext)
})
}
// Currently, we have the following common fields in error response: // Currently, we have the following common fields in error response:
// * message: the message for end users (it shouldn't be used for error type detection) // * message: the message for end users (it shouldn't be used for error type detection)
// if we need to indicate some errors, we should introduce some new fields like ErrorCode or ErrorType // if we need to indicate some errors, we should introduce some new fields like ErrorCode or ErrorType

View File

@ -96,7 +96,11 @@ func (b *Base) SetTotalCountHeader(total int64) {
// Written returns true if there are something sent to web browser // Written returns true if there are something sent to web browser
func (b *Base) Written() bool { func (b *Base) Written() bool {
return b.Resp.Status() > 0 return b.Resp.WrittenStatus() != 0
}
func (b *Base) WrittenStatus() int {
return b.Resp.WrittenStatus()
} }
// Status writes status code // Status writes status code
@ -132,6 +136,14 @@ func (b *Base) JSON(status int, content interface{}) {
} }
} }
func (b *Base) JSONRedirect(redirect string) {
b.JSON(http.StatusOK, map[string]any{"redirect": redirect})
}
func (b *Base) JSONError(msg string) {
b.JSON(http.StatusBadRequest, map[string]any{"errorMessage": msg})
}
// RemoteAddr returns the client machine ip address // RemoteAddr returns the client machine ip address
func (b *Base) RemoteAddr() string { func (b *Base) RemoteAddr() string {
return b.Req.RemoteAddr return b.Req.RemoteAddr

View File

@ -21,7 +21,9 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
web_types "code.gitea.io/gitea/modules/web/types"
"gitea.com/go-chi/cache" "gitea.com/go-chi/cache"
"gitea.com/go-chi/session" "gitea.com/go-chi/session"
@ -58,6 +60,12 @@ type Context struct {
Package *Package Package *Package
} }
func init() {
web.RegisterResponseStatusProvider[*Context](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(WebContextKey).(*Context)
})
}
// TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString. // TrHTMLEscapeArgs runs ".Locale.Tr()" but pre-escapes all arguments with html.EscapeString.
// This is useful if the locale message is intended to only produce HTML content. // This is useful if the locale message is intended to only produce HTML content.
func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string { func (ctx *Context) TrHTMLEscapeArgs(msg string, args ...string) string {

View File

@ -16,6 +16,7 @@ import (
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
@ -49,14 +50,7 @@ func (ctx *Context) RedirectToFirst(location ...string) {
continue continue
} }
// Unfortunately browsers consider a redirect Location with preceding "//", "\\" and "/\" as meaning redirect to "http(s)://REST_OF_PATH" if httplib.IsRiskyRedirectURL(loc) {
// Therefore we should ignore these redirect locations to prevent open redirects
if len(loc) > 1 && (loc[0] == '/' || loc[0] == '\\') && (loc[1] == '/' || loc[1] == '\\') {
continue
}
u, err := url.Parse(loc)
if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(loc), strings.ToLower(setting.AppURL))) {
continue continue
} }

View File

@ -11,6 +11,8 @@ import (
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/web"
web_types "code.gitea.io/gitea/modules/web/types"
) )
// PrivateContext represents a context for private routes // PrivateContext represents a context for private routes
@ -21,6 +23,12 @@ type PrivateContext struct {
Repo *Repository Repo *Repository
} }
func init() {
web.RegisterResponseStatusProvider[*PrivateContext](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(privateContextKey).(*PrivateContext)
})
}
// Deadline is part of the interface for context.Context and we pass this to the request context // Deadline is part of the interface for context.Context and we pass this to the request context
func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) { func (ctx *PrivateContext) Deadline() (deadline time.Time, ok bool) {
if ctx.Override != nil { if ctx.Override != nil {

View File

@ -5,15 +5,20 @@ package context
import ( import (
"net/http" "net/http"
web_types "code.gitea.io/gitea/modules/web/types"
) )
// ResponseWriter represents a response writer for HTTP // ResponseWriter represents a response writer for HTTP
type ResponseWriter interface { type ResponseWriter interface {
http.ResponseWriter http.ResponseWriter
http.Flusher http.Flusher
Status() int web_types.ResponseStatusProvider
Before(func(ResponseWriter)) Before(func(ResponseWriter))
Size() int // used by access logger template
Status() int // used by access logger template
Size() int // used by access logger template
} }
var _ ResponseWriter = &Response{} var _ ResponseWriter = &Response{}
@ -46,6 +51,10 @@ func (r *Response) Write(bs []byte) (int, error) {
return size, nil return size, nil
} }
func (r *Response) Status() int {
return r.status
}
func (r *Response) Size() int { func (r *Response) Size() int {
return r.written return r.written
} }
@ -71,8 +80,8 @@ func (r *Response) Flush() {
} }
} }
// Status returned status code written // WrittenStatus returned status code written
func (r *Response) Status() int { func (r *Response) WrittenStatus() int {
return r.status return r.status
} }

View File

@ -20,17 +20,18 @@ func (b *Blob) Name() string {
return b.name return b.name
} }
// GetBlobContent Gets the content of the blob as raw text // GetBlobContent Gets the limited content of the blob as raw text
func (b *Blob) GetBlobContent() (string, error) { func (b *Blob) GetBlobContent(limit int64) (string, error) {
if limit <= 0 {
return "", nil
}
dataRc, err := b.DataAsync() dataRc, err := b.DataAsync()
if err != nil { if err != nil {
return "", err return "", err
} }
defer dataRc.Close() defer dataRc.Close()
buf := make([]byte, 1024) buf, err := util.ReadWithLimit(dataRc, int(limit))
n, _ := util.ReadAtMost(dataRc, buf) return string(buf), err
buf = buf[:n]
return string(buf), nil
} }
// GetBlobLineCount gets line count of the blob // GetBlobLineCount gets line count of the blob

View File

@ -163,6 +163,7 @@ func (ref RefName) ShortName() string {
} }
// RefGroup returns the group type of the reference // RefGroup returns the group type of the reference
// Using the name of the directory under .git/refs
func (ref RefName) RefGroup() string { func (ref RefName) RefGroup() string {
if ref.IsBranch() { if ref.IsBranch() {
return "heads" return "heads"
@ -182,6 +183,19 @@ func (ref RefName) RefGroup() string {
return "" return ""
} }
// RefType returns the simple ref type of the reference, e.g. branch, tag
// It's differrent from RefGroup, which is using the name of the directory under .git/refs
// Here we using branch but not heads, using tag but not tags
func (ref RefName) RefType() string {
var refType string
if ref.IsBranch() {
refType = "branch"
} else if ref.IsTag() {
refType = "tag"
}
return refType
}
// RefURL returns the absolute URL for a ref in a repository // RefURL returns the absolute URL for a ref in a repository
func RefURL(repoURL, ref string) string { func RefURL(repoURL, ref string) string {
refFullName := RefName(ref) refFullName := RefName(ref)

View File

@ -28,6 +28,14 @@ func IsValidSHAPattern(sha string) bool {
return shaPattern.MatchString(sha) return shaPattern.MatchString(sha)
} }
type ErrInvalidSHA struct {
SHA string
}
func (err ErrInvalidSHA) Error() string {
return fmt.Sprintf("invalid sha: %s", err.SHA)
}
// MustID always creates a new SHA1 from a [20]byte array with no validation of input. // MustID always creates a new SHA1 from a [20]byte array with no validation of input.
func MustID(b []byte) SHA1 { func MustID(b []byte) SHA1 {
var id SHA1 var id SHA1

27
modules/httplib/url.go Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package httplib
import (
"net/url"
"strings"
"code.gitea.io/gitea/modules/setting"
)
// IsRiskyRedirectURL returns true if the URL is considered risky for redirects
func IsRiskyRedirectURL(s string) bool {
// Unfortunately browsers consider a redirect Location with preceding "//", "\\", "/\" and "\/" as meaning redirect to "http(s)://REST_OF_PATH"
// Therefore we should ignore these redirect locations to prevent open redirects
if len(s) > 1 && (s[0] == '/' || s[0] == '\\') && (s[1] == '/' || s[1] == '\\') {
return true
}
u, err := url.Parse(s)
if err != nil || ((u.Scheme != "" || u.Host != "") && !strings.HasPrefix(strings.ToLower(s), strings.ToLower(setting.AppURL))) {
return true
}
return false
}

View File

@ -0,0 +1,38 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package httplib
import (
"testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
)
func TestIsRiskyRedirectURL(t *testing.T) {
setting.AppURL = "http://localhost:3000/"
tests := []struct {
input string
want bool
}{
{"", false},
{"foo", false},
{"/", false},
{"/foo?k=%20#abc", false},
{"//", true},
{"\\\\", true},
{"/\\", true},
{"\\/", true},
{"mail:a@b.com", true},
{"https://test.com", true},
{setting.AppURL + "/foo", false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.want, IsRiskyRedirectURL(tt.input))
})
}
}

View File

@ -76,7 +76,8 @@ func IsSummary(node ast.Node) bool {
// TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox // TaskCheckBoxListItem is a block that represents a list item of a markdown block with a checkbox
type TaskCheckBoxListItem struct { type TaskCheckBoxListItem struct {
*ast.ListItem *ast.ListItem
IsChecked bool IsChecked bool
SourcePosition int
} }
// KindTaskCheckBoxListItem is the NodeKind for TaskCheckBoxListItem // KindTaskCheckBoxListItem is the NodeKind for TaskCheckBoxListItem
@ -86,6 +87,7 @@ var KindTaskCheckBoxListItem = ast.NewNodeKind("TaskCheckBoxListItem")
func (n *TaskCheckBoxListItem) Dump(source []byte, level int) { func (n *TaskCheckBoxListItem) Dump(source []byte, level int) {
m := map[string]string{} m := map[string]string{}
m["IsChecked"] = strconv.FormatBool(n.IsChecked) m["IsChecked"] = strconv.FormatBool(n.IsChecked)
m["SourcePosition"] = strconv.FormatInt(int64(n.SourcePosition), 10)
ast.DumpHelper(n, source, level, m, nil) ast.DumpHelper(n, source, level, m, nil)
} }

View File

@ -177,6 +177,11 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
newChild := NewTaskCheckBoxListItem(listItem) newChild := NewTaskCheckBoxListItem(listItem)
newChild.IsChecked = taskCheckBox.IsChecked newChild.IsChecked = taskCheckBox.IsChecked
newChild.SetAttributeString("class", []byte("task-list-item")) newChild.SetAttributeString("class", []byte("task-list-item"))
segments := newChild.FirstChild().Lines()
if segments.Len() > 0 {
segment := segments.At(0)
newChild.SourcePosition = rc.metaLength + segment.Start
}
v.AppendChild(v, newChild) v.AppendChild(v, newChild)
} }
} }
@ -457,12 +462,7 @@ func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byt
} else { } else {
_, _ = w.WriteString("<li>") _, _ = w.WriteString("<li>")
} }
_, _ = w.WriteString(`<input type="checkbox" disabled=""`) fmt.Fprintf(w, `<input type="checkbox" disabled="" data-source-position="%d"`, n.SourcePosition)
segments := node.FirstChild().Lines()
if segments.Len() > 0 {
segment := segments.At(0)
_, _ = w.WriteString(fmt.Sprintf(` data-source-position="%d"`, segment.Start))
}
if n.IsChecked { if n.IsChecked {
_, _ = w.WriteString(` checked=""`) _, _ = w.WriteString(` checked=""`)
} }

View File

@ -178,6 +178,9 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
} }
buf = giteautil.NormalizeEOL(buf) buf = giteautil.NormalizeEOL(buf)
// Preserve original length.
bufWithMetadataLength := len(buf)
rc := &RenderConfig{ rc := &RenderConfig{
Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)), Meta: renderMetaModeFromString(string(ctx.RenderMetaAs)),
Icon: "table", Icon: "table",
@ -185,6 +188,12 @@ func actualRender(ctx *markup.RenderContext, input io.Reader, output io.Writer)
} }
buf, _ = ExtractMetadataBytes(buf, rc) buf, _ = ExtractMetadataBytes(buf, rc)
metaLength := bufWithMetadataLength - len(buf)
if metaLength < 0 {
metaLength = 0
}
rc.metaLength = metaLength
pc.Set(renderConfigKey, rc) pc.Set(renderConfigKey, rc)
if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil { if err := converter.Convert(buf, lw, parser.WithContext(pc)); err != nil {

View File

@ -520,3 +520,40 @@ func TestMathBlock(t *testing.T) {
} }
} }
func TestTaskList(t *testing.T) {
testcases := []struct {
testcase string
expected string
}{
{
// data-source-position should take into account YAML frontmatter.
`---
foo: bar
---
- [ ] task 1`,
`<details><summary><i class="icon table"></i></summary><table>
<thead>
<tr>
<th>foo</th>
</tr>
</thead>
<tbody>
<tr>
<td>bar</td>
</tr>
</tbody>
</table>
</details><ul>
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="19"/>task 1</li>
</ul>
`,
},
}
for _, test := range testcases {
res, err := RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
}
}

View File

@ -20,6 +20,9 @@ type RenderConfig struct {
TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view TOC string // "false": hide, "side"/empty: in sidebar, "main"/"true": in main view
Lang string Lang string
yamlNode *yaml.Node yamlNode *yaml.Node
// Used internally. Cannot be controlled by frontmatter.
metaLength int
} }
func renderMetaModeFromString(s string) markup.RenderMetaMode { func renderMetaModeFromString(s string) markup.RenderMetaMode {

View File

@ -4,14 +4,14 @@
package setting package setting
import ( import (
"code.gitea.io/gitea/modules/log" "fmt"
) )
// Actions settings // Actions settings
var ( var (
Actions = struct { Actions = struct {
LogStorage Storage // how the created logs should be stored LogStorage *Storage // how the created logs should be stored
ArtifactStorage Storage // how the created artifacts should be stored ArtifactStorage *Storage // how the created artifacts should be stored
Enabled bool Enabled bool
DefaultActionsURL string `ini:"DEFAULT_ACTIONS_URL"` DefaultActionsURL string `ini:"DEFAULT_ACTIONS_URL"`
}{ }{
@ -20,15 +20,22 @@ var (
} }
) )
func loadActionsFrom(rootCfg ConfigProvider) { func loadActionsFrom(rootCfg ConfigProvider) error {
sec := rootCfg.Section("actions") sec := rootCfg.Section("actions")
if err := sec.MapTo(&Actions); err != nil { err := sec.MapTo(&Actions)
log.Fatal("Failed to map Actions settings: %v", err) if err != nil {
return fmt.Errorf("failed to map Actions settings: %v", err)
} }
actionsSec := rootCfg.Section("actions.artifacts") // don't support to read configuration from [actions]
storageType := actionsSec.Key("STORAGE_TYPE").MustString("") Actions.LogStorage, err = getStorage(rootCfg, "actions_log", "", nil)
if err != nil {
return err
}
Actions.LogStorage = getStorage(rootCfg, "actions_log", "", nil) actionsSec, _ := rootCfg.GetSection("actions.artifacts")
Actions.ArtifactStorage = getStorage(rootCfg, "actions_artifacts", storageType, actionsSec)
Actions.ArtifactStorage, err = getStorage(rootCfg, "actions_artifacts", "", actionsSec)
return err
} }

View File

@ -0,0 +1,97 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_getStorageInheritNameSectionTypeForActions(t *testing.T) {
iniStr := `
[storage]
STORAGE_TYPE = minio
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadActionsFrom(cfg))
assert.EqualValues(t, "minio", Actions.LogStorage.Type)
assert.EqualValues(t, "actions_log/", Actions.LogStorage.MinioConfig.BasePath)
assert.EqualValues(t, "minio", Actions.ArtifactStorage.Type)
assert.EqualValues(t, "actions_artifacts/", Actions.ArtifactStorage.MinioConfig.BasePath)
iniStr = `
[storage.actions_log]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadActionsFrom(cfg))
assert.EqualValues(t, "minio", Actions.LogStorage.Type)
assert.EqualValues(t, "actions_log/", Actions.LogStorage.MinioConfig.BasePath)
assert.EqualValues(t, "local", Actions.ArtifactStorage.Type)
assert.EqualValues(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
iniStr = `
[storage.actions_log]
STORAGE_TYPE = my_storage
[storage.my_storage]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadActionsFrom(cfg))
assert.EqualValues(t, "minio", Actions.LogStorage.Type)
assert.EqualValues(t, "actions_log/", Actions.LogStorage.MinioConfig.BasePath)
assert.EqualValues(t, "local", Actions.ArtifactStorage.Type)
assert.EqualValues(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
iniStr = `
[storage.actions_artifacts]
STORAGE_TYPE = my_storage
[storage.my_storage]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadActionsFrom(cfg))
assert.EqualValues(t, "local", Actions.LogStorage.Type)
assert.EqualValues(t, "actions_log", filepath.Base(Actions.LogStorage.Path))
assert.EqualValues(t, "minio", Actions.ArtifactStorage.Type)
assert.EqualValues(t, "actions_artifacts/", Actions.ArtifactStorage.MinioConfig.BasePath)
iniStr = `
[storage.actions_artifacts]
STORAGE_TYPE = my_storage
[storage.my_storage]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadActionsFrom(cfg))
assert.EqualValues(t, "local", Actions.LogStorage.Type)
assert.EqualValues(t, "actions_log", filepath.Base(Actions.LogStorage.Path))
assert.EqualValues(t, "minio", Actions.ArtifactStorage.Type)
assert.EqualValues(t, "actions_artifacts/", Actions.ArtifactStorage.MinioConfig.BasePath)
iniStr = ``
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadActionsFrom(cfg))
assert.EqualValues(t, "local", Actions.LogStorage.Type)
assert.EqualValues(t, "actions_log", filepath.Base(Actions.LogStorage.Path))
assert.EqualValues(t, "local", Actions.ArtifactStorage.Type)
assert.EqualValues(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
}

View File

@ -5,29 +5,31 @@ package setting
// Attachment settings // Attachment settings
var Attachment = struct { var Attachment = struct {
Storage Storage *Storage
AllowedTypes string AllowedTypes string
MaxSize int64 MaxSize int64
MaxFiles int MaxFiles int
Enabled bool Enabled bool
}{ }{
Storage: Storage{ Storage: &Storage{},
ServeDirect: false, AllowedTypes: ".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip",
},
AllowedTypes: "image/jpeg,image/png,application/zip,application/gzip",
MaxSize: 4, MaxSize: 4,
MaxFiles: 5, MaxFiles: 5,
Enabled: true, Enabled: true,
} }
func loadAttachmentFrom(rootCfg ConfigProvider) { func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
sec := rootCfg.Section("attachment") sec, _ := rootCfg.GetSection("attachment")
storageType := sec.Key("STORAGE_TYPE").MustString("") if sec == nil {
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", nil)
Attachment.Storage = getStorage(rootCfg, "attachments", storageType, sec) return err
}
Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip") Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(4) Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(4)
Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5) Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
Attachment.Enabled = sec.Key("ENABLED").MustBool(true) Attachment.Enabled = sec.Key("ENABLED").MustBool(true)
Attachment.Storage, err = getStorage(rootCfg, "attachments", "", sec)
return err
} }

View File

@ -0,0 +1,133 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_getStorageCustomType(t *testing.T) {
iniStr := `
[attachment]
STORAGE_TYPE = my_minio
MINIO_BUCKET = gitea-attachment
[storage.my_minio]
STORAGE_TYPE = minio
MINIO_ENDPOINT = my_minio:9000
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadAttachmentFrom(cfg))
assert.EqualValues(t, "minio", Attachment.Storage.Type)
assert.EqualValues(t, "my_minio:9000", Attachment.Storage.MinioConfig.Endpoint)
assert.EqualValues(t, "gitea-attachment", Attachment.Storage.MinioConfig.Bucket)
assert.EqualValues(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
}
func Test_getStorageTypeSectionOverridesStorageSection(t *testing.T) {
iniStr := `
[attachment]
STORAGE_TYPE = minio
[storage.minio]
MINIO_BUCKET = gitea-minio
[storage]
MINIO_BUCKET = gitea
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadAttachmentFrom(cfg))
assert.EqualValues(t, "minio", Attachment.Storage.Type)
assert.EqualValues(t, "gitea-minio", Attachment.Storage.MinioConfig.Bucket)
assert.EqualValues(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
}
func Test_getStorageSpecificOverridesStorage(t *testing.T) {
iniStr := `
[attachment]
STORAGE_TYPE = minio
MINIO_BUCKET = gitea-attachment
[storage.attachments]
MINIO_BUCKET = gitea
[storage]
STORAGE_TYPE = local
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadAttachmentFrom(cfg))
assert.EqualValues(t, "minio", Attachment.Storage.Type)
assert.EqualValues(t, "gitea-attachment", Attachment.Storage.MinioConfig.Bucket)
assert.EqualValues(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
}
func Test_getStorageGetDefaults(t *testing.T) {
cfg, err := NewConfigProviderFromData("")
assert.NoError(t, err)
assert.NoError(t, loadAttachmentFrom(cfg))
// default storage is local, so bucket is empty
assert.EqualValues(t, "", Attachment.Storage.MinioConfig.Bucket)
}
func Test_getStorageInheritNameSectionType(t *testing.T) {
iniStr := `
[storage.attachments]
STORAGE_TYPE = minio
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadAttachmentFrom(cfg))
assert.EqualValues(t, "minio", Attachment.Storage.Type)
}
func Test_AttachmentStorage(t *testing.T) {
iniStr := `
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[storage]
STORAGE_TYPE = minio
MINIO_ENDPOINT = s3.my-domain.net
MINIO_BUCKET = gitea
MINIO_LOCATION = homenet
MINIO_USE_SSL = true
MINIO_ACCESS_KEY_ID = correct_key
MINIO_SECRET_ACCESS_KEY = correct_key
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadAttachmentFrom(cfg))
storage := Attachment.Storage
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea", storage.MinioConfig.Bucket)
}
func Test_AttachmentStorage1(t *testing.T) {
iniStr := `
[storage]
STORAGE_TYPE = minio
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadAttachmentFrom(cfg))
assert.EqualValues(t, "minio", Attachment.Storage.Type)
assert.EqualValues(t, "gitea", Attachment.Storage.MinioConfig.Bucket)
assert.EqualValues(t, "attachments/", Attachment.Storage.MinioConfig.BasePath)
}

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
@ -95,6 +96,18 @@ func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string
return "" return ""
} }
func ConfigSectionKeyBool(sec ConfigSection, key string, def ...bool) bool {
k := ConfigSectionKey(sec, key)
if k != nil && k.String() != "" {
b, _ := strconv.ParseBool(k.String())
return b
}
if len(def) > 0 {
return def[0]
}
return false
}
// ConfigInheritedKey works like ini.Section.Key(), but it always returns a new key instance, it is O(n) because NewKey is O(n) // ConfigInheritedKey works like ini.Section.Key(), but it always returns a new key instance, it is O(n) because NewKey is O(n)
// and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values. // and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values.
// Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys. // Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys.
@ -287,6 +300,12 @@ func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, n
} }
} }
func deprecatedSettingFatal(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
if rootCfg.Section(oldSection).HasKey(oldKey) {
log.Fatal("Deprecated fallback `[%s]` `%s` present. Use `[%s]` `%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
}
}
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini // deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) { func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
if rootCfg.Section(oldSection).HasKey(oldKey) { if rootCfg.Section(oldSection).HasKey(oldKey) {

View File

@ -5,10 +5,10 @@ package setting
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"time" "time"
"code.gitea.io/gitea/modules/generate" "code.gitea.io/gitea/modules/generate"
"code.gitea.io/gitea/modules/log"
) )
// LFS represents the configuration for Git LFS // LFS represents the configuration for Git LFS
@ -20,25 +20,27 @@ var LFS = struct {
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"` MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"` LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
Storage Storage *Storage
}{} }{}
func loadLFSFrom(rootCfg ConfigProvider) { func loadLFSFrom(rootCfg ConfigProvider) error {
sec := rootCfg.Section("server") sec := rootCfg.Section("server")
if err := sec.MapTo(&LFS); err != nil { if err := sec.MapTo(&LFS); err != nil {
log.Fatal("Failed to map LFS settings: %v", err) return fmt.Errorf("failed to map LFS settings: %v", err)
} }
lfsSec := rootCfg.Section("lfs") lfsSec, _ := rootCfg.GetSection("lfs")
storageType := lfsSec.Key("STORAGE_TYPE").MustString("")
// Specifically default PATH to LFS_CONTENT_PATH // Specifically default PATH to LFS_CONTENT_PATH
// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version // DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
// if these are removed, the warning will not be shown // if these are removed, the warning will not be shown
deprecatedSetting(rootCfg, "server", "LFS_CONTENT_PATH", "lfs", "PATH", "v1.19.0") deprecatedSettingFatal(rootCfg, "server", "LFS_CONTENT_PATH", "lfs", "PATH", "v1.19.0")
lfsSec.Key("PATH").MustString(sec.Key("LFS_CONTENT_PATH").String())
LFS.Storage = getStorage(rootCfg, "lfs", storageType, lfsSec) var err error
LFS.Storage, err = getStorage(rootCfg, "lfs", "", lfsSec)
if err != nil {
return err
}
// Rest of LFS service settings // Rest of LFS service settings
if LFS.LocksPagingNum == 0 { if LFS.LocksPagingNum == 0 {
@ -47,23 +49,25 @@ func loadLFSFrom(rootCfg ConfigProvider) {
LFS.HTTPAuthExpiry = sec.Key("LFS_HTTP_AUTH_EXPIRY").MustDuration(24 * time.Hour) LFS.HTTPAuthExpiry = sec.Key("LFS_HTTP_AUTH_EXPIRY").MustDuration(24 * time.Hour)
if LFS.StartServer { if !LFS.StartServer {
LFS.JWTSecretBytes = make([]byte, 32) return nil
n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64)) }
if err != nil || n != 32 { LFS.JWTSecretBytes = make([]byte, 32)
LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64() n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64))
if err != nil {
log.Fatal("Error generating JWT Secret for custom config: %v", err)
return
}
// Save secret if err != nil || n != 32 {
sec.Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64) LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64()
if err := rootCfg.Save(); err != nil { if err != nil {
log.Fatal("Error saving JWT Secret for custom config: %v", err) return fmt.Errorf("Error generating JWT Secret for custom config: %v", err)
return }
}
// Save secret
sec.Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
if err := rootCfg.Save(); err != nil {
return fmt.Errorf("Error saving JWT Secret for custom config: %v", err)
} }
} }
return nil
} }

View File

@ -0,0 +1,77 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_getStorageInheritNameSectionTypeForLFS(t *testing.T) {
iniStr := `
[storage]
STORAGE_TYPE = minio
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadLFSFrom(cfg))
assert.EqualValues(t, "minio", LFS.Storage.Type)
assert.EqualValues(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
iniStr = `
[storage.lfs]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadLFSFrom(cfg))
assert.EqualValues(t, "minio", LFS.Storage.Type)
assert.EqualValues(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
iniStr = `
[lfs]
STORAGE_TYPE = my_minio
[storage.my_minio]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadLFSFrom(cfg))
assert.EqualValues(t, "minio", LFS.Storage.Type)
assert.EqualValues(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
iniStr = `
[lfs]
STORAGE_TYPE = my_minio
MINIO_BASE_PATH = my_lfs/
[storage.my_minio]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadLFSFrom(cfg))
assert.EqualValues(t, "minio", LFS.Storage.Type)
assert.EqualValues(t, "my_lfs/", LFS.Storage.MinioConfig.BasePath)
}
func Test_LFSStorage1(t *testing.T) {
iniStr := `
[storage]
STORAGE_TYPE = minio
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadLFSFrom(cfg))
assert.EqualValues(t, "minio", LFS.Storage.Type)
assert.EqualValues(t, "gitea", LFS.Storage.MinioConfig.Bucket)
assert.EqualValues(t, "lfs/", LFS.Storage.MinioConfig.BasePath)
}

View File

@ -4,20 +4,19 @@
package setting package setting
import ( import (
"fmt"
"math" "math"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"code.gitea.io/gitea/modules/log"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
) )
// Package registry settings // Package registry settings
var ( var (
Packages = struct { Packages = struct {
Storage Storage *Storage
Enabled bool Enabled bool
ChunkedUploadPath string ChunkedUploadPath string
RegistryHost string RegistryHost string
@ -51,13 +50,21 @@ var (
} }
) )
func loadPackagesFrom(rootCfg ConfigProvider) { func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
sec := rootCfg.Section("packages") sec, _ := rootCfg.GetSection("packages")
if err := sec.MapTo(&Packages); err != nil { if sec == nil {
log.Fatal("Failed to map Packages settings: %v", err) Packages.Storage, err = getStorage(rootCfg, "packages", "", nil)
return err
} }
Packages.Storage = getStorage(rootCfg, "packages", "", nil) if err = sec.MapTo(&Packages); err != nil {
return fmt.Errorf("failed to map Packages settings: %v", err)
}
Packages.Storage, err = getStorage(rootCfg, "packages", "", sec)
if err != nil {
return err
}
appURL, _ := url.Parse(AppURL) appURL, _ := url.Parse(AppURL)
Packages.RegistryHost = appURL.Host Packages.RegistryHost = appURL.Host
@ -68,7 +75,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
} }
if err := os.MkdirAll(Packages.ChunkedUploadPath, os.ModePerm); err != nil { if err := os.MkdirAll(Packages.ChunkedUploadPath, os.ModePerm); err != nil {
log.Error("Unable to create chunked upload directory: %s (%v)", Packages.ChunkedUploadPath, err) return fmt.Errorf("unable to create chunked upload directory: %s (%v)", Packages.ChunkedUploadPath, err)
} }
Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE") Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
@ -93,6 +100,7 @@ func loadPackagesFrom(rootCfg ConfigProvider) {
Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS") Packages.LimitSizeRubyGems = mustBytes(sec, "LIMIT_SIZE_RUBYGEMS")
Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT") Packages.LimitSizeSwift = mustBytes(sec, "LIMIT_SIZE_SWIFT")
Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT") Packages.LimitSizeVagrant = mustBytes(sec, "LIMIT_SIZE_VAGRANT")
return nil
} }
func mustBytes(section ConfigSection, key string) int64 { func mustBytes(section ConfigSection, key string) int64 {

View File

@ -29,3 +29,170 @@ func TestMustBytes(t *testing.T) {
assert.EqualValues(t, 1782579, test("1.7mib")) assert.EqualValues(t, 1782579, test("1.7mib"))
assert.EqualValues(t, -1, test("1 yib")) // too large assert.EqualValues(t, -1, test("1 yib")) // too large
} }
func Test_getStorageInheritNameSectionTypeForPackages(t *testing.T) {
// packages storage inherits from storage if nothing configured
iniStr := `
[storage]
STORAGE_TYPE = minio
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadPackagesFrom(cfg))
assert.EqualValues(t, "minio", Packages.Storage.Type)
assert.EqualValues(t, "packages/", Packages.Storage.MinioConfig.BasePath)
// we can also configure packages storage directly
iniStr = `
[storage.packages]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadPackagesFrom(cfg))
assert.EqualValues(t, "minio", Packages.Storage.Type)
assert.EqualValues(t, "packages/", Packages.Storage.MinioConfig.BasePath)
// or we can indicate the storage type in the packages section
iniStr = `
[packages]
STORAGE_TYPE = my_minio
[storage.my_minio]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadPackagesFrom(cfg))
assert.EqualValues(t, "minio", Packages.Storage.Type)
assert.EqualValues(t, "packages/", Packages.Storage.MinioConfig.BasePath)
// or we can indicate the storage type and minio base path in the packages section
iniStr = `
[packages]
STORAGE_TYPE = my_minio
MINIO_BASE_PATH = my_packages/
[storage.my_minio]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadPackagesFrom(cfg))
assert.EqualValues(t, "minio", Packages.Storage.Type)
assert.EqualValues(t, "my_packages/", Packages.Storage.MinioConfig.BasePath)
}
func Test_PackageStorage1(t *testing.T) {
iniStr := `
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[packages]
MINIO_BASE_PATH = packages/
SERVE_DIRECT = true
[storage]
STORAGE_TYPE = minio
MINIO_ENDPOINT = s3.my-domain.net
MINIO_BUCKET = gitea
MINIO_LOCATION = homenet
MINIO_USE_SSL = true
MINIO_ACCESS_KEY_ID = correct_key
MINIO_SECRET_ACCESS_KEY = correct_key
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadPackagesFrom(cfg))
storage := Packages.Storage
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea", storage.MinioConfig.Bucket)
assert.EqualValues(t, "packages/", storage.MinioConfig.BasePath)
assert.True(t, storage.MinioConfig.ServeDirect)
}
func Test_PackageStorage2(t *testing.T) {
iniStr := `
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[storage.packages]
MINIO_BASE_PATH = packages/
SERVE_DIRECT = true
[storage]
STORAGE_TYPE = minio
MINIO_ENDPOINT = s3.my-domain.net
MINIO_BUCKET = gitea
MINIO_LOCATION = homenet
MINIO_USE_SSL = true
MINIO_ACCESS_KEY_ID = correct_key
MINIO_SECRET_ACCESS_KEY = correct_key
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadPackagesFrom(cfg))
storage := Packages.Storage
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea", storage.MinioConfig.Bucket)
assert.EqualValues(t, "packages/", storage.MinioConfig.BasePath)
assert.True(t, storage.MinioConfig.ServeDirect)
}
func Test_PackageStorage3(t *testing.T) {
iniStr := `
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[packages]
STORAGE_TYPE = my_cfg
MINIO_BASE_PATH = my_packages/
SERVE_DIRECT = true
[storage.my_cfg]
STORAGE_TYPE = minio
MINIO_ENDPOINT = s3.my-domain.net
MINIO_BUCKET = gitea
MINIO_LOCATION = homenet
MINIO_USE_SSL = true
MINIO_ACCESS_KEY_ID = correct_key
MINIO_SECRET_ACCESS_KEY = correct_key
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadPackagesFrom(cfg))
storage := Packages.Storage
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea", storage.MinioConfig.Bucket)
assert.EqualValues(t, "my_packages/", storage.MinioConfig.BasePath)
assert.True(t, storage.MinioConfig.ServeDirect)
}
func Test_PackageStorage4(t *testing.T) {
iniStr := `
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[storage.packages]
STORAGE_TYPE = my_cfg
MINIO_BASE_PATH = my_packages/
SERVE_DIRECT = true
[storage.my_cfg]
STORAGE_TYPE = minio
MINIO_ENDPOINT = s3.my-domain.net
MINIO_BUCKET = gitea
MINIO_LOCATION = homenet
MINIO_USE_SSL = true
MINIO_ACCESS_KEY_ID = correct_key
MINIO_SECRET_ACCESS_KEY = correct_key
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadPackagesFrom(cfg))
storage := Packages.Storage
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea", storage.MinioConfig.Bucket)
assert.EqualValues(t, "my_packages/", storage.MinioConfig.BasePath)
assert.True(t, storage.MinioConfig.ServeDirect)
}

View File

@ -7,7 +7,7 @@ package setting
var ( var (
Avatar = struct { Avatar = struct {
Storage Storage *Storage
MaxWidth int MaxWidth int
MaxHeight int MaxHeight int
@ -27,23 +27,26 @@ var (
EnableFederatedAvatar bool // Depreciated: migrated to database EnableFederatedAvatar bool // Depreciated: migrated to database
RepoAvatar = struct { RepoAvatar = struct {
Storage Storage *Storage
Fallback string Fallback string
FallbackImage string FallbackImage string
}{} }{}
) )
func loadPictureFrom(rootCfg ConfigProvider) { func loadAvatarsFrom(rootCfg ConfigProvider) error {
sec := rootCfg.Section("picture") sec := rootCfg.Section("picture")
avatarSec := rootCfg.Section("avatar") avatarSec := rootCfg.Section("avatar")
storageType := sec.Key("AVATAR_STORAGE_TYPE").MustString("") storageType := sec.Key("AVATAR_STORAGE_TYPE").MustString("")
// Specifically default PATH to AVATAR_UPLOAD_PATH // Specifically default PATH to AVATAR_UPLOAD_PATH
avatarSec.Key("PATH").MustString( avatarSec.Key("PATH").MustString(sec.Key("AVATAR_UPLOAD_PATH").String())
sec.Key("AVATAR_UPLOAD_PATH").String())
Avatar.Storage = getStorage(rootCfg, "avatars", storageType, avatarSec) var err error
Avatar.Storage, err = getStorage(rootCfg, "avatars", storageType, avatarSec)
if err != nil {
return err
}
Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096) Avatar.MaxWidth = sec.Key("AVATAR_MAX_WIDTH").MustInt(4096)
Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(4096) Avatar.MaxHeight = sec.Key("AVATAR_MAX_HEIGHT").MustInt(4096)
@ -67,7 +70,7 @@ func loadPictureFrom(rootCfg ConfigProvider) {
EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(GetDefaultEnableFederatedAvatar(DisableGravatar)) EnableFederatedAvatar = sec.Key("ENABLE_FEDERATED_AVATAR").MustBool(GetDefaultEnableFederatedAvatar(DisableGravatar))
deprecatedSettingDB(rootCfg, "", "ENABLE_FEDERATED_AVATAR") deprecatedSettingDB(rootCfg, "", "ENABLE_FEDERATED_AVATAR")
loadRepoAvatarFrom(rootCfg) return nil
} }
func GetDefaultDisableGravatar() bool { func GetDefaultDisableGravatar() bool {
@ -85,17 +88,22 @@ func GetDefaultEnableFederatedAvatar(disableGravatar bool) bool {
return v return v
} }
func loadRepoAvatarFrom(rootCfg ConfigProvider) { func loadRepoAvatarFrom(rootCfg ConfigProvider) error {
sec := rootCfg.Section("picture") sec := rootCfg.Section("picture")
repoAvatarSec := rootCfg.Section("repo-avatar") repoAvatarSec := rootCfg.Section("repo-avatar")
storageType := sec.Key("REPOSITORY_AVATAR_STORAGE_TYPE").MustString("") storageType := sec.Key("REPOSITORY_AVATAR_STORAGE_TYPE").MustString("")
// Specifically default PATH to AVATAR_UPLOAD_PATH // Specifically default PATH to AVATAR_UPLOAD_PATH
repoAvatarSec.Key("PATH").MustString( repoAvatarSec.Key("PATH").MustString(sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").String())
sec.Key("REPOSITORY_AVATAR_UPLOAD_PATH").String())
RepoAvatar.Storage = getStorage(rootCfg, "repo-avatars", storageType, repoAvatarSec) var err error
RepoAvatar.Storage, err = getStorage(rootCfg, "repo-avatars", storageType, repoAvatarSec)
if err != nil {
return err
}
RepoAvatar.Fallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none") RepoAvatar.Fallback = sec.Key("REPOSITORY_AVATAR_FALLBACK").MustString("none")
RepoAvatar.FallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString(AppSubURL + "/assets/img/repo_default.png") RepoAvatar.FallbackImage = sec.Key("REPOSITORY_AVATAR_FALLBACK_IMAGE").MustString(AppSubURL + "/assets/img/repo_default.png")
return nil
} }

View File

@ -265,10 +265,6 @@ var (
} }
RepoRootPath string RepoRootPath string
ScriptType = "bash" ScriptType = "bash"
RepoArchive = struct {
Storage
}{}
) )
func loadRepositoryFrom(rootCfg ConfigProvider) { func loadRepositoryFrom(rootCfg ConfigProvider) {
@ -359,5 +355,7 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
Repository.Upload.TempPath = path.Join(AppWorkPath, Repository.Upload.TempPath) Repository.Upload.TempPath = path.Join(AppWorkPath, Repository.Upload.TempPath)
} }
RepoArchive.Storage = getStorage(rootCfg, "repo-archive", "", nil) if err := loadRepoArchiveFrom(rootCfg); err != nil {
log.Fatal("loadRepoArchiveFrom: %v", err)
}
} }

View File

@ -0,0 +1,25 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import "fmt"
var RepoArchive = struct {
Storage *Storage
}{}
func loadRepoArchiveFrom(rootCfg ConfigProvider) (err error) {
sec, _ := rootCfg.GetSection("repo-archive")
if sec == nil {
RepoArchive.Storage, err = getStorage(rootCfg, "repo-archive", "", nil)
return err
}
if err := sec.MapTo(&RepoArchive); err != nil {
return fmt.Errorf("mapto repoarchive failed: %v", err)
}
RepoArchive.Storage, err = getStorage(rootCfg, "repo-archive", "", sec)
return err
}

View File

@ -0,0 +1,111 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_getStorageInheritNameSectionTypeForRepoArchive(t *testing.T) {
// packages storage inherits from storage if nothing configured
iniStr := `
[storage]
STORAGE_TYPE = minio
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadRepoArchiveFrom(cfg))
assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
assert.EqualValues(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
// we can also configure packages storage directly
iniStr = `
[storage.repo-archive]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadRepoArchiveFrom(cfg))
assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
assert.EqualValues(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
// or we can indicate the storage type in the packages section
iniStr = `
[repo-archive]
STORAGE_TYPE = my_minio
[storage.my_minio]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadRepoArchiveFrom(cfg))
assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
assert.EqualValues(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
// or we can indicate the storage type and minio base path in the packages section
iniStr = `
[repo-archive]
STORAGE_TYPE = my_minio
MINIO_BASE_PATH = my_archive/
[storage.my_minio]
STORAGE_TYPE = minio
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadRepoArchiveFrom(cfg))
assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
assert.EqualValues(t, "my_archive/", RepoArchive.Storage.MinioConfig.BasePath)
}
func Test_RepoArchiveStorage(t *testing.T) {
iniStr := `
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[storage]
STORAGE_TYPE = minio
MINIO_ENDPOINT = s3.my-domain.net
MINIO_BUCKET = gitea
MINIO_LOCATION = homenet
MINIO_USE_SSL = true
MINIO_ACCESS_KEY_ID = correct_key
MINIO_SECRET_ACCESS_KEY = correct_key
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadRepoArchiveFrom(cfg))
storage := RepoArchive.Storage
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea", storage.MinioConfig.Bucket)
iniStr = `
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[storage.repo-archive]
STORAGE_TYPE = s3
[storage.s3]
STORAGE_TYPE = minio
MINIO_ENDPOINT = s3.my-domain.net
MINIO_BUCKET = gitea
MINIO_LOCATION = homenet
MINIO_USE_SSL = true
MINIO_ACCESS_KEY_ID = correct_key
MINIO_SECRET_ACCESS_KEY = correct_key
`
cfg, err = NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
assert.NoError(t, loadRepoArchiveFrom(cfg))
storage = RepoArchive.Storage
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea", storage.MinioConfig.Bucket)
}

View File

@ -203,16 +203,18 @@ func Init(opts *Options) {
var err error var err error
CfgProvider, err = NewConfigProviderFromFile(opts) CfgProvider, err = NewConfigProviderFromFile(opts)
if err != nil { if err != nil {
log.Fatal("Init[%v]: %v", opts, err) log.Fatal("newConfigProviderFromFile[%v]: %v", opts, err)
} }
if !opts.DisableLoadCommonSettings { if !opts.DisableLoadCommonSettings {
loadCommonSettingsFrom(CfgProvider) if err := loadCommonSettingsFrom(CfgProvider); err != nil {
log.Fatal("loadCommonSettingsFrom[%v]: %v", opts, err)
}
} }
} }
// loadCommonSettingsFrom loads common configurations from a configuration provider. // loadCommonSettingsFrom loads common configurations from a configuration provider.
func loadCommonSettingsFrom(cfg ConfigProvider) { func loadCommonSettingsFrom(cfg ConfigProvider) error {
// WARNING: don't change the sequence except you know what you are doing. // WARNNING: don't change the sequence except you know what you are doing.
loadRunModeFrom(cfg) loadRunModeFrom(cfg)
loadLogGlobalFrom(cfg) loadLogGlobalFrom(cfg)
loadServerFrom(cfg) loadServerFrom(cfg)
@ -222,13 +224,26 @@ func loadCommonSettingsFrom(cfg ConfigProvider) {
loadOAuth2From(cfg) loadOAuth2From(cfg)
loadSecurityFrom(cfg) loadSecurityFrom(cfg)
loadAttachmentFrom(cfg) if err := loadAttachmentFrom(cfg); err != nil {
loadLFSFrom(cfg) return err
}
if err := loadLFSFrom(cfg); err != nil {
return err
}
loadTimeFrom(cfg) loadTimeFrom(cfg)
loadRepositoryFrom(cfg) loadRepositoryFrom(cfg)
loadPictureFrom(cfg) if err := loadAvatarsFrom(cfg); err != nil {
loadPackagesFrom(cfg) return err
loadActionsFrom(cfg) }
if err := loadRepoAvatarFrom(cfg); err != nil {
return err
}
if err := loadPackagesFrom(cfg); err != nil {
return err
}
if err := loadActionsFrom(cfg); err != nil {
return err
}
loadUIFrom(cfg) loadUIFrom(cfg)
loadAdminFrom(cfg) loadAdminFrom(cfg)
loadAPIFrom(cfg) loadAPIFrom(cfg)
@ -239,6 +254,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) {
loadMirrorFrom(cfg) loadMirrorFrom(cfg)
loadMarkupFrom(cfg) loadMarkupFrom(cfg)
loadOtherFrom(cfg) loadOtherFrom(cfg)
return nil
} }
func loadRunModeFrom(rootCfg ConfigProvider) { func loadRunModeFrom(rootCfg ConfigProvider) {

View File

@ -4,87 +4,182 @@
package setting package setting
import ( import (
"errors"
"fmt"
"path/filepath" "path/filepath"
"reflect"
) )
// StorageType is a type of Storage
type StorageType string
const (
// LocalStorageType is the type descriptor for local storage
LocalStorageType StorageType = "local"
// MinioStorageType is the type descriptor for minio storage
MinioStorageType StorageType = "minio"
)
var storageTypes = []StorageType{
LocalStorageType,
MinioStorageType,
}
// IsValidStorageType returns true if the given storage type is valid
func IsValidStorageType(storageType StorageType) bool {
for _, t := range storageTypes {
if t == storageType {
return true
}
}
return false
}
// MinioStorageConfig represents the configuration for a minio storage
type MinioStorageConfig struct {
Endpoint string `ini:"MINIO_ENDPOINT" json:",omitempty"`
AccessKeyID string `ini:"MINIO_ACCESS_KEY_ID" json:",omitempty"`
SecretAccessKey string `ini:"MINIO_SECRET_ACCESS_KEY" json:",omitempty"`
Bucket string `ini:"MINIO_BUCKET" json:",omitempty"`
Location string `ini:"MINIO_LOCATION" json:",omitempty"`
BasePath string `ini:"MINIO_BASE_PATH" json:",omitempty"`
UseSSL bool `ini:"MINIO_USE_SSL"`
InsecureSkipVerify bool `ini:"MINIO_INSECURE_SKIP_VERIFY"`
ChecksumAlgorithm string `ini:"MINIO_CHECKSUM_ALGORITHM" json:",omitempty"`
ServeDirect bool `ini:"SERVE_DIRECT"`
}
// Storage represents configuration of storages // Storage represents configuration of storages
type Storage struct { type Storage struct {
Type string Type StorageType // local or minio
Path string Path string `json:",omitempty"` // for local type
Section ConfigSection TemporaryPath string `json:",omitempty"`
ServeDirect bool MinioConfig MinioStorageConfig // for minio type
} }
// MapTo implements the Mappable interface func (storage *Storage) ToShadowCopy() Storage {
func (s *Storage) MapTo(v interface{}) error { shadowStorage := *storage
pathValue := reflect.ValueOf(v).Elem().FieldByName("Path") if shadowStorage.MinioConfig.AccessKeyID != "" {
if pathValue.IsValid() && pathValue.Kind() == reflect.String { shadowStorage.MinioConfig.AccessKeyID = "******"
pathValue.SetString(s.Path)
} }
if s.Section != nil { if shadowStorage.MinioConfig.SecretAccessKey != "" {
return s.Section.MapTo(v) shadowStorage.MinioConfig.SecretAccessKey = "******"
} }
return nil return shadowStorage
} }
func getStorage(rootCfg ConfigProvider, name, typ string, targetSec ConfigSection) Storage { const storageSectionName = "storage"
const sectionName = "storage"
sec := rootCfg.Section(sectionName)
func getDefaultStorageSection(rootCfg ConfigProvider) ConfigSection {
storageSec := rootCfg.Section(storageSectionName)
// Global Defaults // Global Defaults
sec.Key("MINIO_ENDPOINT").MustString("localhost:9000") storageSec.Key("STORAGE_TYPE").MustString("local")
sec.Key("MINIO_ACCESS_KEY_ID").MustString("") storageSec.Key("MINIO_ENDPOINT").MustString("localhost:9000")
sec.Key("MINIO_SECRET_ACCESS_KEY").MustString("") storageSec.Key("MINIO_ACCESS_KEY_ID").MustString("")
sec.Key("MINIO_BUCKET").MustString("gitea") storageSec.Key("MINIO_SECRET_ACCESS_KEY").MustString("")
sec.Key("MINIO_LOCATION").MustString("us-east-1") storageSec.Key("MINIO_BUCKET").MustString("gitea")
sec.Key("MINIO_USE_SSL").MustBool(false) storageSec.Key("MINIO_LOCATION").MustString("us-east-1")
sec.Key("MINIO_INSECURE_SKIP_VERIFY").MustBool(false) storageSec.Key("MINIO_USE_SSL").MustBool(false)
sec.Key("MINIO_CHECKSUM_ALGORITHM").MustString("default") storageSec.Key("MINIO_INSECURE_SKIP_VERIFY").MustBool(false)
storageSec.Key("MINIO_CHECKSUM_ALGORITHM").MustString("default")
return storageSec
}
func getStorage(rootCfg ConfigProvider, name, typ string, sec ConfigSection) (*Storage, error) {
if name == "" {
return nil, errors.New("no name for storage")
}
var targetSec ConfigSection
if typ != "" {
var err error
targetSec, err = rootCfg.GetSection(storageSectionName + "." + typ)
if err != nil {
if !IsValidStorageType(StorageType(typ)) {
return nil, fmt.Errorf("get section via storage type %q failed: %v", typ, err)
}
}
if targetSec != nil {
targetType := targetSec.Key("STORAGE_TYPE").String()
if targetType == "" {
if !IsValidStorageType(StorageType(typ)) {
return nil, fmt.Errorf("unknow storage type %q", typ)
}
targetSec.Key("STORAGE_TYPE").SetValue(typ)
} else if !IsValidStorageType(StorageType(targetType)) {
return nil, fmt.Errorf("unknow storage type %q for section storage.%v", targetType, typ)
}
}
}
storageNameSec, _ := rootCfg.GetSection(storageSectionName + "." + name)
if targetSec == nil { if targetSec == nil {
targetSec, _ = rootCfg.NewSection(name) targetSec = sec
}
if targetSec == nil {
targetSec = storageNameSec
}
if targetSec == nil {
targetSec = getDefaultStorageSection(rootCfg)
} else {
targetType := targetSec.Key("STORAGE_TYPE").String()
switch {
case targetType == "":
if targetSec.Key("PATH").String() == "" {
targetSec = getDefaultStorageSection(rootCfg)
} else {
targetSec.Key("STORAGE_TYPE").SetValue("local")
}
default:
newTargetSec, _ := rootCfg.GetSection(storageSectionName + "." + targetType)
if newTargetSec == nil {
if !IsValidStorageType(StorageType(targetType)) {
return nil, fmt.Errorf("invalid storage section %s.%q", storageSectionName, targetType)
}
} else {
targetSec = newTargetSec
if IsValidStorageType(StorageType(targetType)) {
tp := targetSec.Key("STORAGE_TYPE").String()
if tp == "" {
targetSec.Key("STORAGE_TYPE").SetValue(targetType)
}
}
}
}
}
targetType := targetSec.Key("STORAGE_TYPE").String()
if !IsValidStorageType(StorageType(targetType)) {
return nil, fmt.Errorf("invalid storage type %q", targetType)
} }
var storage Storage var storage Storage
storage.Section = targetSec storage.Type = StorageType(targetType)
storage.Type = typ
overrides := make([]ConfigSection, 0, 3) switch targetType {
nameSec, err := rootCfg.GetSection(sectionName + "." + name) case string(LocalStorageType):
if err == nil { storage.Path = ConfigSectionKeyString(targetSec, "PATH", filepath.Join(AppDataPath, name))
overrides = append(overrides, nameSec) if !filepath.IsAbs(storage.Path) {
} storage.Path = filepath.Join(AppWorkPath, storage.Path)
}
case string(MinioStorageType):
storage.MinioConfig.BasePath = name + "/"
typeSec, err := rootCfg.GetSection(sectionName + "." + typ) if err := targetSec.MapTo(&storage.MinioConfig); err != nil {
if err == nil { return nil, fmt.Errorf("map minio config failed: %v", err)
overrides = append(overrides, typeSec) }
nextType := typeSec.Key("STORAGE_TYPE").String() // extra config section will be read SERVE_DIRECT, PATH, MINIO_BASE_PATH to override the targetsec
if len(nextType) > 0 { extraConfigSec := sec
storage.Type = nextType // Support custom STORAGE_TYPE if extraConfigSec == nil {
extraConfigSec = storageNameSec
}
if extraConfigSec != nil {
storage.MinioConfig.ServeDirect = ConfigSectionKeyBool(extraConfigSec, "SERVE_DIRECT", storage.MinioConfig.ServeDirect)
storage.MinioConfig.BasePath = ConfigSectionKeyString(extraConfigSec, "MINIO_BASE_PATH", storage.MinioConfig.BasePath)
storage.MinioConfig.Bucket = ConfigSectionKeyString(extraConfigSec, "MINIO_BUCKET", storage.MinioConfig.Bucket)
} }
} }
overrides = append(overrides, sec)
for _, override := range overrides { return &storage, nil
for _, key := range override.Keys() {
if !targetSec.HasKey(key.Name()) {
_, _ = targetSec.NewKey(key.Name(), key.Value())
}
}
if len(storage.Type) == 0 {
storage.Type = override.Key("STORAGE_TYPE").String()
}
}
storage.ServeDirect = storage.Section.Key("SERVE_DIRECT").MustBool(false)
// Specific defaults
storage.Path = storage.Section.Key("PATH").MustString(filepath.Join(AppDataPath, name))
if !filepath.IsAbs(storage.Path) {
storage.Path = filepath.Join(AppWorkPath, storage.Path)
storage.Section.Key("PATH").SetValue(storage.Path)
}
storage.Section.Key("MINIO_BASE_PATH").MustString(name + "/")
return storage
} }

View File

@ -9,106 +9,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_getStorageCustomType(t *testing.T) {
iniStr := `
[attachment]
STORAGE_TYPE = my_minio
MINIO_BUCKET = gitea-attachment
[storage.my_minio]
STORAGE_TYPE = minio
MINIO_ENDPOINT = my_minio:9000
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
sec := cfg.Section("attachment")
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "attachments", storageType, sec)
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "my_minio:9000", storage.Section.Key("MINIO_ENDPOINT").String())
assert.EqualValues(t, "gitea-attachment", storage.Section.Key("MINIO_BUCKET").String())
}
func Test_getStorageNameSectionOverridesTypeSection(t *testing.T) {
iniStr := `
[attachment]
STORAGE_TYPE = minio
[storage.attachments]
MINIO_BUCKET = gitea-attachment
[storage.minio]
MINIO_BUCKET = gitea
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
sec := cfg.Section("attachment")
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "attachments", storageType, sec)
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea-attachment", storage.Section.Key("MINIO_BUCKET").String())
}
func Test_getStorageTypeSectionOverridesStorageSection(t *testing.T) {
iniStr := `
[attachment]
STORAGE_TYPE = minio
[storage.minio]
MINIO_BUCKET = gitea-minio
[storage]
MINIO_BUCKET = gitea
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
sec := cfg.Section("attachment")
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "attachments", storageType, sec)
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea-minio", storage.Section.Key("MINIO_BUCKET").String())
}
func Test_getStorageSpecificOverridesStorage(t *testing.T) {
iniStr := `
[attachment]
STORAGE_TYPE = minio
MINIO_BUCKET = gitea-attachment
[storage.attachments]
MINIO_BUCKET = gitea
[storage]
STORAGE_TYPE = local
`
cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err)
sec := cfg.Section("attachment")
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "attachments", storageType, sec)
assert.EqualValues(t, "minio", storage.Type)
assert.EqualValues(t, "gitea-attachment", storage.Section.Key("MINIO_BUCKET").String())
}
func Test_getStorageGetDefaults(t *testing.T) {
cfg, err := NewConfigProviderFromData("")
assert.NoError(t, err)
sec := cfg.Section("attachment")
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "attachments", storageType, sec)
assert.EqualValues(t, "gitea", storage.Section.Key("MINIO_BUCKET").String())
}
func Test_getStorageMultipleName(t *testing.T) { func Test_getStorageMultipleName(t *testing.T) {
iniStr := ` iniStr := `
[lfs] [lfs]
@ -118,32 +18,20 @@ MINIO_BUCKET = gitea-lfs
MINIO_BUCKET = gitea-attachment MINIO_BUCKET = gitea-attachment
[storage] [storage]
STORAGE_TYPE = minio
MINIO_BUCKET = gitea-storage MINIO_BUCKET = gitea-storage
` `
cfg, err := NewConfigProviderFromData(iniStr) cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err) assert.NoError(t, err)
{ assert.NoError(t, loadAttachmentFrom(cfg))
sec := cfg.Section("attachment") assert.EqualValues(t, "gitea-attachment", Attachment.Storage.MinioConfig.Bucket)
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "attachments", storageType, sec)
assert.EqualValues(t, "gitea-attachment", storage.Section.Key("MINIO_BUCKET").String()) assert.NoError(t, loadLFSFrom(cfg))
} assert.EqualValues(t, "gitea-lfs", LFS.Storage.MinioConfig.Bucket)
{
sec := cfg.Section("lfs")
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "lfs", storageType, sec)
assert.EqualValues(t, "gitea-lfs", storage.Section.Key("MINIO_BUCKET").String()) assert.NoError(t, loadAvatarsFrom(cfg))
} assert.EqualValues(t, "gitea-storage", Avatar.Storage.MinioConfig.Bucket)
{
sec := cfg.Section("avatar")
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "avatars", storageType, sec)
assert.EqualValues(t, "gitea-storage", storage.Section.Key("MINIO_BUCKET").String())
}
} }
func Test_getStorageUseOtherNameAsType(t *testing.T) { func Test_getStorageUseOtherNameAsType(t *testing.T) {
@ -152,25 +40,17 @@ func Test_getStorageUseOtherNameAsType(t *testing.T) {
STORAGE_TYPE = lfs STORAGE_TYPE = lfs
[storage.lfs] [storage.lfs]
STORAGE_TYPE = minio
MINIO_BUCKET = gitea-storage MINIO_BUCKET = gitea-storage
` `
cfg, err := NewConfigProviderFromData(iniStr) cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err) assert.NoError(t, err)
{ assert.NoError(t, loadAttachmentFrom(cfg))
sec := cfg.Section("attachment") assert.EqualValues(t, "gitea-storage", Attachment.Storage.MinioConfig.Bucket)
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "attachments", storageType, sec)
assert.EqualValues(t, "gitea-storage", storage.Section.Key("MINIO_BUCKET").String()) assert.NoError(t, loadLFSFrom(cfg))
} assert.EqualValues(t, "gitea-storage", LFS.Storage.MinioConfig.Bucket)
{
sec := cfg.Section("lfs")
storageType := sec.Key("STORAGE_TYPE").MustString("")
storage := getStorage(cfg, "lfs", storageType, sec)
assert.EqualValues(t, "gitea-storage", storage.Section.Key("MINIO_BUCKET").String())
}
} }
func Test_getStorageInheritStorageType(t *testing.T) { func Test_getStorageInheritStorageType(t *testing.T) {
@ -181,24 +61,32 @@ STORAGE_TYPE = minio
cfg, err := NewConfigProviderFromData(iniStr) cfg, err := NewConfigProviderFromData(iniStr)
assert.NoError(t, err) assert.NoError(t, err)
sec := cfg.Section("attachment") assert.NoError(t, loadPackagesFrom(cfg))
storageType := sec.Key("STORAGE_TYPE").MustString("") assert.EqualValues(t, "minio", Packages.Storage.Type)
storage := getStorage(cfg, "attachments", storageType, sec) assert.EqualValues(t, "gitea", Packages.Storage.MinioConfig.Bucket)
assert.EqualValues(t, "packages/", Packages.Storage.MinioConfig.BasePath)
assert.EqualValues(t, "minio", storage.Type) assert.NoError(t, loadRepoArchiveFrom(cfg))
} assert.EqualValues(t, "minio", RepoArchive.Storage.Type)
assert.EqualValues(t, "gitea", RepoArchive.Storage.MinioConfig.Bucket)
func Test_getStorageInheritNameSectionType(t *testing.T) { assert.EqualValues(t, "repo-archive/", RepoArchive.Storage.MinioConfig.BasePath)
iniStr := `
[storage.attachments] assert.NoError(t, loadActionsFrom(cfg))
STORAGE_TYPE = minio assert.EqualValues(t, "minio", Actions.LogStorage.Type)
` assert.EqualValues(t, "gitea", Actions.LogStorage.MinioConfig.Bucket)
cfg, err := NewConfigProviderFromData(iniStr) assert.EqualValues(t, "actions_log/", Actions.LogStorage.MinioConfig.BasePath)
assert.NoError(t, err)
assert.EqualValues(t, "minio", Actions.ArtifactStorage.Type)
sec := cfg.Section("attachment") assert.EqualValues(t, "gitea", Actions.ArtifactStorage.MinioConfig.Bucket)
storageType := sec.Key("STORAGE_TYPE").MustString("") assert.EqualValues(t, "actions_artifacts/", Actions.ArtifactStorage.MinioConfig.BasePath)
storage := getStorage(cfg, "attachments", storageType, sec)
assert.NoError(t, loadAvatarsFrom(cfg))
assert.EqualValues(t, "minio", storage.Type) assert.EqualValues(t, "minio", Avatar.Storage.Type)
assert.EqualValues(t, "gitea", Avatar.Storage.MinioConfig.Bucket)
assert.EqualValues(t, "avatars/", Avatar.Storage.MinioConfig.BasePath)
assert.NoError(t, loadRepoAvatarFrom(cfg))
assert.EqualValues(t, "minio", RepoAvatar.Storage.Type)
assert.EqualValues(t, "gitea", RepoAvatar.Storage.MinioConfig.Bucket)
assert.EqualValues(t, "repo-avatars/", RepoAvatar.Storage.MinioConfig.BasePath)
} }

View File

@ -8,64 +8,8 @@ import (
"io" "io"
"net/url" "net/url"
"os" "os"
"reflect"
"code.gitea.io/gitea/modules/json"
) )
// Mappable represents an interface that can MapTo another interface
type Mappable interface {
MapTo(v interface{}) error
}
// toConfig will attempt to convert a given configuration cfg into the provided exemplar type.
//
// It will tolerate the cfg being passed as a []byte or string of a json representation of the
// exemplar or the correct type of the exemplar itself
func toConfig(exemplar, cfg interface{}) (interface{}, error) {
// First of all check if we've got the same type as the exemplar - if so it's all fine.
if reflect.TypeOf(cfg).AssignableTo(reflect.TypeOf(exemplar)) {
return cfg, nil
}
// Now if not - does it provide a MapTo function we can try?
if mappable, ok := cfg.(Mappable); ok {
newVal := reflect.New(reflect.TypeOf(exemplar))
if err := mappable.MapTo(newVal.Interface()); err == nil {
return newVal.Elem().Interface(), nil
}
// MapTo has failed us ... let's try the json route ...
}
// OK we've been passed a byte array right?
configBytes, ok := cfg.([]byte)
if !ok {
// oh ... it's a string then?
var configStr string
configStr, ok = cfg.(string)
configBytes = []byte(configStr)
}
if !ok {
// hmm ... can we marshal it to json?
var err error
configBytes, err = json.Marshal(cfg)
ok = err == nil
}
if !ok {
// no ... we've tried hard enough at this point - throw an error!
return nil, ErrInvalidConfiguration{cfg: cfg}
}
// OK unmarshal the byte array into a new copy of the exemplar
newVal := reflect.New(reflect.TypeOf(exemplar))
if err := json.Unmarshal(configBytes, newVal.Interface()); err != nil {
// If we can't unmarshal it then return an error!
return nil, ErrInvalidConfiguration{cfg: cfg, err: err}
}
return newVal.Elem().Interface(), nil
}
var uninitializedStorage = discardStorage("uninitialized storage") var uninitializedStorage = discardStorage("uninitialized storage")
type discardStorage string type discardStorage string

View File

@ -12,20 +12,12 @@ import (
"path/filepath" "path/filepath"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
var _ ObjectStorage = &LocalStorage{} var _ ObjectStorage = &LocalStorage{}
// LocalStorageType is the type descriptor for local storage
const LocalStorageType Type = "local"
// LocalStorageConfig represents the configuration for a local storage
type LocalStorageConfig struct {
Path string `ini:"PATH"`
TemporaryPath string `ini:"TEMPORARY_PATH"`
}
// LocalStorage represents a local files storage // LocalStorage represents a local files storage
type LocalStorage struct { type LocalStorage struct {
ctx context.Context ctx context.Context
@ -34,13 +26,7 @@ type LocalStorage struct {
} }
// NewLocalStorage returns a local files // NewLocalStorage returns a local files
func NewLocalStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error) { func NewLocalStorage(ctx context.Context, config *setting.Storage) (ObjectStorage, error) {
configInterface, err := toConfig(LocalStorageConfig{}, cfg)
if err != nil {
return nil, err
}
config := configInterface.(LocalStorageConfig)
if !filepath.IsAbs(config.Path) { if !filepath.IsAbs(config.Path) {
return nil, fmt.Errorf("LocalStorageConfig.Path should have been prepared by setting/storage.go and should be an absolute path, but not: %q", config.Path) return nil, fmt.Errorf("LocalStorageConfig.Path should have been prepared by setting/storage.go and should be an absolute path, but not: %q", config.Path)
} }
@ -164,5 +150,5 @@ func (l *LocalStorage) IterateObjects(dirName string, fn func(path string, obj O
} }
func init() { func init() {
RegisterStorageType(LocalStorageType, NewLocalStorage) RegisterStorageType(setting.LocalStorageType, NewLocalStorage)
} }

View File

@ -8,6 +8,8 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -55,5 +57,5 @@ func TestBuildLocalPath(t *testing.T) {
func TestLocalStorageIterator(t *testing.T) { func TestLocalStorageIterator(t *testing.T) {
dir := filepath.Join(os.TempDir(), "TestLocalStorageIteratorTestDir") dir := filepath.Join(os.TempDir(), "TestLocalStorageIteratorTestDir")
testStorageIterator(t, string(LocalStorageType), LocalStorageConfig{Path: dir}) testStorageIterator(t, setting.LocalStorageType, &setting.Storage{Path: dir})
} }

View File

@ -16,6 +16,7 @@ import (
"time" "time"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
@ -41,25 +42,9 @@ func (m *minioObject) Stat() (os.FileInfo, error) {
return &minioFileInfo{oi}, nil return &minioFileInfo{oi}, nil
} }
// MinioStorageType is the type descriptor for minio storage
const MinioStorageType Type = "minio"
// MinioStorageConfig represents the configuration for a minio storage
type MinioStorageConfig struct {
Endpoint string `ini:"MINIO_ENDPOINT"`
AccessKeyID string `ini:"MINIO_ACCESS_KEY_ID"`
SecretAccessKey string `ini:"MINIO_SECRET_ACCESS_KEY"`
Bucket string `ini:"MINIO_BUCKET"`
Location string `ini:"MINIO_LOCATION"`
BasePath string `ini:"MINIO_BASE_PATH"`
UseSSL bool `ini:"MINIO_USE_SSL"`
InsecureSkipVerify bool `ini:"MINIO_INSECURE_SKIP_VERIFY"`
ChecksumAlgorithm string `ini:"MINIO_CHECKSUM_ALGORITHM"`
}
// MinioStorage returns a minio bucket storage // MinioStorage returns a minio bucket storage
type MinioStorage struct { type MinioStorage struct {
cfg *MinioStorageConfig cfg *setting.MinioStorageConfig
ctx context.Context ctx context.Context
client *minio.Client client *minio.Client
bucket string bucket string
@ -87,13 +72,8 @@ func convertMinioErr(err error) error {
} }
// NewMinioStorage returns a minio storage // NewMinioStorage returns a minio storage
func NewMinioStorage(ctx context.Context, cfg interface{}) (ObjectStorage, error) { func NewMinioStorage(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error) {
configInterface, err := toConfig(MinioStorageConfig{}, cfg) config := cfg.MinioConfig
if err != nil {
return nil, convertMinioErr(err)
}
config := configInterface.(MinioStorageConfig)
if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" { if config.ChecksumAlgorithm != "" && config.ChecksumAlgorithm != "default" && config.ChecksumAlgorithm != "md5" {
return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm) return nil, fmt.Errorf("invalid minio checksum algorithm: %s", config.ChecksumAlgorithm)
} }
@ -258,5 +238,5 @@ func (m *MinioStorage) IterateObjects(dirName string, fn func(path string, obj O
} }
func init() { func init() {
RegisterStorageType(MinioStorageType, NewMinioStorage) RegisterStorageType(setting.MinioStorageType, NewMinioStorage)
} }

View File

@ -6,6 +6,8 @@ package storage
import ( import (
"os" "os"
"testing" "testing"
"code.gitea.io/gitea/modules/setting"
) )
func TestMinioStorageIterator(t *testing.T) { func TestMinioStorageIterator(t *testing.T) {
@ -13,11 +15,13 @@ func TestMinioStorageIterator(t *testing.T) {
t.Skip("minioStorage not present outside of CI") t.Skip("minioStorage not present outside of CI")
return return
} }
testStorageIterator(t, string(MinioStorageType), MinioStorageConfig{ testStorageIterator(t, setting.MinioStorageType, &setting.Storage{
Endpoint: "127.0.0.1:9000", MinioConfig: setting.MinioStorageConfig{
AccessKeyID: "123456", Endpoint: "127.0.0.1:9000",
SecretAccessKey: "12345678", AccessKeyID: "123456",
Bucket: "gitea", SecretAccessKey: "12345678",
Location: "us-east-1", Bucket: "gitea",
Location: "us-east-1",
},
}) })
} }

View File

@ -37,16 +37,15 @@ func IsErrInvalidConfiguration(err error) bool {
return ok return ok
} }
// Type is a type of Storage type Type = setting.StorageType
type Type string
// NewStorageFunc is a function that creates a storage // NewStorageFunc is a function that creates a storage
type NewStorageFunc func(ctx context.Context, cfg interface{}) (ObjectStorage, error) type NewStorageFunc func(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error)
var storageMap = map[Type]NewStorageFunc{} var storageMap = map[Type]NewStorageFunc{}
// RegisterStorageType registers a provided storage type with a function to create it // RegisterStorageType registers a provided storage type with a function to create it
func RegisterStorageType(typ Type, fn func(ctx context.Context, cfg interface{}) (ObjectStorage, error)) { func RegisterStorageType(typ Type, fn func(ctx context.Context, cfg *setting.Storage) (ObjectStorage, error)) {
storageMap[typ] = fn storageMap[typ] = fn
} }
@ -151,11 +150,11 @@ func Init() error {
} }
// NewStorage takes a storage type and some config and returns an ObjectStorage or an error // NewStorage takes a storage type and some config and returns an ObjectStorage or an error
func NewStorage(typStr string, cfg interface{}) (ObjectStorage, error) { func NewStorage(typStr Type, cfg *setting.Storage) (ObjectStorage, error) {
if len(typStr) == 0 { if len(typStr) == 0 {
typStr = string(LocalStorageType) typStr = setting.LocalStorageType
} }
fn, ok := storageMap[Type(typStr)] fn, ok := storageMap[typStr]
if !ok { if !ok {
return nil, fmt.Errorf("Unsupported storage type: %s", typStr) return nil, fmt.Errorf("Unsupported storage type: %s", typStr)
} }
@ -165,7 +164,7 @@ func NewStorage(typStr string, cfg interface{}) (ObjectStorage, error) {
func initAvatars() (err error) { func initAvatars() (err error) {
log.Info("Initialising Avatar storage with type: %s", setting.Avatar.Storage.Type) log.Info("Initialising Avatar storage with type: %s", setting.Avatar.Storage.Type)
Avatars, err = NewStorage(setting.Avatar.Storage.Type, &setting.Avatar.Storage) Avatars, err = NewStorage(setting.Avatar.Storage.Type, setting.Avatar.Storage)
return err return err
} }
@ -175,7 +174,7 @@ func initAttachments() (err error) {
return nil return nil
} }
log.Info("Initialising Attachment storage with type: %s", setting.Attachment.Storage.Type) log.Info("Initialising Attachment storage with type: %s", setting.Attachment.Storage.Type)
Attachments, err = NewStorage(setting.Attachment.Storage.Type, &setting.Attachment.Storage) Attachments, err = NewStorage(setting.Attachment.Storage.Type, setting.Attachment.Storage)
return err return err
} }
@ -185,19 +184,19 @@ func initLFS() (err error) {
return nil return nil
} }
log.Info("Initialising LFS storage with type: %s", setting.LFS.Storage.Type) log.Info("Initialising LFS storage with type: %s", setting.LFS.Storage.Type)
LFS, err = NewStorage(setting.LFS.Storage.Type, &setting.LFS.Storage) LFS, err = NewStorage(setting.LFS.Storage.Type, setting.LFS.Storage)
return err return err
} }
func initRepoAvatars() (err error) { func initRepoAvatars() (err error) {
log.Info("Initialising Repository Avatar storage with type: %s", setting.RepoAvatar.Storage.Type) log.Info("Initialising Repository Avatar storage with type: %s", setting.RepoAvatar.Storage.Type)
RepoAvatars, err = NewStorage(setting.RepoAvatar.Storage.Type, &setting.RepoAvatar.Storage) RepoAvatars, err = NewStorage(setting.RepoAvatar.Storage.Type, setting.RepoAvatar.Storage)
return err return err
} }
func initRepoArchives() (err error) { func initRepoArchives() (err error) {
log.Info("Initialising Repository Archive storage with type: %s", setting.RepoArchive.Storage.Type) log.Info("Initialising Repository Archive storage with type: %s", setting.RepoArchive.Storage.Type)
RepoArchives, err = NewStorage(setting.RepoArchive.Storage.Type, &setting.RepoArchive.Storage) RepoArchives, err = NewStorage(setting.RepoArchive.Storage.Type, setting.RepoArchive.Storage)
return err return err
} }
@ -207,7 +206,7 @@ func initPackages() (err error) {
return nil return nil
} }
log.Info("Initialising Packages storage with type: %s", setting.Packages.Storage.Type) log.Info("Initialising Packages storage with type: %s", setting.Packages.Storage.Type)
Packages, err = NewStorage(setting.Packages.Storage.Type, &setting.Packages.Storage) Packages, err = NewStorage(setting.Packages.Storage.Type, setting.Packages.Storage)
return err return err
} }
@ -218,10 +217,10 @@ func initActions() (err error) {
return nil return nil
} }
log.Info("Initialising Actions storage with type: %s", setting.Actions.LogStorage.Type) log.Info("Initialising Actions storage with type: %s", setting.Actions.LogStorage.Type)
if Actions, err = NewStorage(setting.Actions.LogStorage.Type, &setting.Actions.LogStorage); err != nil { if Actions, err = NewStorage(setting.Actions.LogStorage.Type, setting.Actions.LogStorage); err != nil {
return err return err
} }
log.Info("Initialising ActionsArtifacts storage with type: %s", setting.Actions.ArtifactStorage.Type) log.Info("Initialising ActionsArtifacts storage with type: %s", setting.Actions.ArtifactStorage.Type)
ActionsArtifacts, err = NewStorage(setting.Actions.ArtifactStorage.Type, &setting.Actions.ArtifactStorage) ActionsArtifacts, err = NewStorage(setting.Actions.ArtifactStorage.Type, setting.Actions.ArtifactStorage)
return err return err
} }

View File

@ -7,10 +7,12 @@ import (
"bytes" "bytes"
"testing" "testing"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func testStorageIterator(t *testing.T, typStr string, cfg interface{}) { func testStorageIterator(t *testing.T, typStr Type, cfg *setting.Storage) {
l, err := NewStorage(typStr, cfg) l, err := NewStorage(typStr, cfg)
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -62,5 +62,5 @@ func RenderHTML(icon string, others ...interface{}) template.HTML {
} }
return template.HTML(svgStr) return template.HTML(svgStr)
} }
return template.HTML("") return ""
} }

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"strings"
"testing" "testing"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
@ -25,19 +26,26 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
// MockContext mock context for unit tests func mockRequest(t *testing.T, reqPath string) *http.Request {
// TODO: move this function to other packages, because it depends on "models" package method, path, found := strings.Cut(reqPath, " ")
func MockContext(t *testing.T, path string) *context.Context { if !found {
resp := httptest.NewRecorder() method = "GET"
path = reqPath
}
requestURL, err := url.Parse(path) requestURL, err := url.Parse(path)
assert.NoError(t, err) assert.NoError(t, err)
req := &http.Request{ req := &http.Request{Method: method, URL: requestURL, Form: url.Values{}}
URL: requestURL, req = req.WithContext(middleware.WithContextData(req.Context()))
Form: url.Values{}, return req
} }
// MockContext mock context for unit tests
// TODO: move this function to other packages, because it depends on "models" package
func MockContext(t *testing.T, reqPath string) (*context.Context, *httptest.ResponseRecorder) {
resp := httptest.NewRecorder()
req := mockRequest(t, reqPath)
base, baseCleanUp := context.NewBaseContext(resp, req) base, baseCleanUp := context.NewBaseContext(resp, req)
base.Data = middleware.ContextData{} base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{} base.Locale = &translation.MockLocale{}
ctx := &context.Context{ ctx := &context.Context{
Base: base, Base: base,
@ -48,29 +56,23 @@ func MockContext(t *testing.T, path string) *context.Context {
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
return ctx return ctx, resp
} }
// MockAPIContext mock context for unit tests // MockAPIContext mock context for unit tests
// TODO: move this function to other packages, because it depends on "models" package // TODO: move this function to other packages, because it depends on "models" package
func MockAPIContext(t *testing.T, path string) *context.APIContext { func MockAPIContext(t *testing.T, reqPath string) (*context.APIContext, *httptest.ResponseRecorder) {
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
requestURL, err := url.Parse(path) req := mockRequest(t, reqPath)
assert.NoError(t, err)
req := &http.Request{
URL: requestURL,
Form: url.Values{},
}
base, baseCleanUp := context.NewBaseContext(resp, req) base, baseCleanUp := context.NewBaseContext(resp, req)
base.Data = middleware.ContextData{} base.Data = middleware.GetContextData(req.Context())
base.Locale = &translation.MockLocale{} base.Locale = &translation.MockLocale{}
ctx := &context.APIContext{Base: base} ctx := &context.APIContext{Base: base}
_ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later _ = baseCleanUp // during test, it doesn't need to do clean up. TODO: this can be improved later
chiCtx := chi.NewRouteContext() chiCtx := chi.NewRouteContext()
ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx) ctx.Base.AppendContextValue(chi.RouteCtxKey, chiCtx)
return ctx return ctx, resp
} }
// LoadRepo load a repo into a test context. // LoadRepo load a repo into a test context.

View File

@ -5,12 +5,29 @@ package test
import ( import (
"net/http" "net/http"
"net/http/httptest"
"strings" "strings"
"code.gitea.io/gitea/modules/json"
) )
// RedirectURL returns the redirect URL of a http response. // RedirectURL returns the redirect URL of a http response.
// It also works for JSONRedirect: `{"redirect": "..."}`
func RedirectURL(resp http.ResponseWriter) string { func RedirectURL(resp http.ResponseWriter) string {
return resp.Header().Get("Location") loc := resp.Header().Get("Location")
if loc != "" {
return loc
}
if r, ok := resp.(*httptest.ResponseRecorder); ok {
m := map[string]any{}
err := json.Unmarshal(r.Body.Bytes(), &m)
if err == nil {
if loc, ok := m["redirect"].(string); ok {
return loc
}
}
}
return ""
} }
func IsNormalPageCompleted(s string) bool { func IsNormalPageCompleted(s string) bool {

View File

@ -4,13 +4,14 @@
package util package util
import ( import (
"bytes"
"errors" "errors"
"io" "io"
) )
// ReadAtMost reads at most len(buf) bytes from r into buf. // ReadAtMost reads at most len(buf) bytes from r into buf.
// It returns the number of bytes copied. n is only less than len(buf) if r provides fewer bytes. // It returns the number of bytes copied. n is only less than len(buf) if r provides fewer bytes.
// If EOF occurs while reading, err will be nil. // If EOF or ErrUnexpectedEOF occurs while reading, err will be nil.
func ReadAtMost(r io.Reader, buf []byte) (n int, err error) { func ReadAtMost(r io.Reader, buf []byte) (n int, err error) {
n, err = io.ReadFull(r, buf) n, err = io.ReadFull(r, buf)
if err == io.EOF || err == io.ErrUnexpectedEOF { if err == io.EOF || err == io.ErrUnexpectedEOF {
@ -19,6 +20,42 @@ func ReadAtMost(r io.Reader, buf []byte) (n int, err error) {
return n, err return n, err
} }
// ReadWithLimit reads at most "limit" bytes from r into buf.
// If EOF or ErrUnexpectedEOF occurs while reading, err will be nil.
func ReadWithLimit(r io.Reader, n int) (buf []byte, err error) {
return readWithLimit(r, 1024, n)
}
func readWithLimit(r io.Reader, batch, limit int) ([]byte, error) {
if limit <= batch {
buf := make([]byte, limit)
n, err := ReadAtMost(r, buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
res := bytes.NewBuffer(make([]byte, 0, batch))
bufFix := make([]byte, batch)
eof := false
for res.Len() < limit && !eof {
bufTmp := bufFix
if res.Len()+batch > limit {
bufTmp = bufFix[:limit-res.Len()]
}
n, err := io.ReadFull(r, bufTmp)
if err == io.EOF || err == io.ErrUnexpectedEOF {
eof = true
} else if err != nil {
return nil, err
}
if _, err = res.Write(bufTmp[:n]); err != nil {
return nil, err
}
}
return res.Bytes(), nil
}
// ErrNotEmpty is an error reported when there is a non-empty reader // ErrNotEmpty is an error reported when there is a non-empty reader
var ErrNotEmpty = errors.New("not-empty") var ErrNotEmpty = errors.New("not-empty")

66
modules/util/io_test.go Normal file
View File

@ -0,0 +1,66 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package util
import (
"bytes"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
type readerWithError struct {
buf *bytes.Buffer
}
func (r *readerWithError) Read(p []byte) (n int, err error) {
if r.buf.Len() < 2 {
return 0, errors.New("test error")
}
return r.buf.Read(p)
}
func TestReadWithLimit(t *testing.T) {
bs := []byte("0123456789abcdef")
// normal test
buf, err := readWithLimit(bytes.NewBuffer(bs), 5, 2)
assert.NoError(t, err)
assert.Equal(t, []byte("01"), buf)
buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 5)
assert.NoError(t, err)
assert.Equal(t, []byte("01234"), buf)
buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 6)
assert.NoError(t, err)
assert.Equal(t, []byte("012345"), buf)
buf, err = readWithLimit(bytes.NewBuffer(bs), 5, len(bs))
assert.NoError(t, err)
assert.Equal(t, []byte("0123456789abcdef"), buf)
buf, err = readWithLimit(bytes.NewBuffer(bs), 5, 100)
assert.NoError(t, err)
assert.Equal(t, []byte("0123456789abcdef"), buf)
// test with error
buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 10)
assert.NoError(t, err)
assert.Equal(t, []byte("0123456789"), buf)
buf, err = readWithLimit(&readerWithError{bytes.NewBuffer(bs)}, 5, 100)
assert.ErrorContains(t, err, "test error")
assert.Empty(t, buf)
// test public function
buf, err = ReadWithLimit(bytes.NewBuffer(bs), 2)
assert.NoError(t, err)
assert.Equal(t, []byte("01"), buf)
buf, err = ReadWithLimit(bytes.NewBuffer(bs), 9999999)
assert.NoError(t, err)
assert.Equal(t, []byte("0123456789abcdef"), buf)
}

View File

@ -9,25 +9,15 @@ import (
"net/http" "net/http"
"reflect" "reflect"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web/routing" "code.gitea.io/gitea/modules/web/routing"
"code.gitea.io/gitea/modules/web/types"
) )
// ResponseStatusProvider is an interface to check whether the response has been written by the handler var responseStatusProviders = map[reflect.Type]func(req *http.Request) types.ResponseStatusProvider{}
type ResponseStatusProvider interface {
Written() bool
}
// TODO: decouple this from the context package, let the context package register these providers func RegisterResponseStatusProvider[T any](fn func(req *http.Request) types.ResponseStatusProvider) {
var argTypeProvider = map[reflect.Type]func(req *http.Request) ResponseStatusProvider{ responseStatusProviders[reflect.TypeOf((*T)(nil)).Elem()] = fn
reflect.TypeOf(&context.APIContext{}): func(req *http.Request) ResponseStatusProvider { return context.GetAPIContext(req) },
reflect.TypeOf(&context.Context{}): func(req *http.Request) ResponseStatusProvider { return context.GetWebContext(req) },
reflect.TypeOf(&context.PrivateContext{}): func(req *http.Request) ResponseStatusProvider { return context.GetPrivateContext(req) },
}
func RegisterHandleTypeProvider[T any](fn func(req *http.Request) ResponseStatusProvider) {
argTypeProvider[reflect.TypeOf((*T)(nil)).Elem()] = fn
} }
// responseWriter is a wrapper of http.ResponseWriter, to check whether the response has been written // responseWriter is a wrapper of http.ResponseWriter, to check whether the response has been written
@ -36,10 +26,10 @@ type responseWriter struct {
status int status int
} }
var _ ResponseStatusProvider = (*responseWriter)(nil) var _ types.ResponseStatusProvider = (*responseWriter)(nil)
func (r *responseWriter) Written() bool { func (r *responseWriter) WrittenStatus() int {
return r.status > 0 return r.status
} }
func (r *responseWriter) Header() http.Header { func (r *responseWriter) Header() http.Header {
@ -68,7 +58,7 @@ var (
func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) { func preCheckHandler(fn reflect.Value, argsIn []reflect.Value) {
hasStatusProvider := false hasStatusProvider := false
for _, argIn := range argsIn { for _, argIn := range argsIn {
if _, hasStatusProvider = argIn.Interface().(ResponseStatusProvider); hasStatusProvider { if _, hasStatusProvider = argIn.Interface().(types.ResponseStatusProvider); hasStatusProvider {
break break
} }
} }
@ -101,7 +91,7 @@ func prepareHandleArgsIn(resp http.ResponseWriter, req *http.Request, fn reflect
case httpReqType: case httpReqType:
argsIn[i] = reflect.ValueOf(req) argsIn[i] = reflect.ValueOf(req)
default: default:
if argFn, ok := argTypeProvider[argTyp]; ok { if argFn, ok := responseStatusProviders[argTyp]; ok {
if isPreCheck { if isPreCheck {
argsIn[i] = reflect.ValueOf(&responseWriter{}) argsIn[i] = reflect.ValueOf(&responseWriter{})
} else { } else {
@ -129,8 +119,8 @@ func handleResponse(fn reflect.Value, ret []reflect.Value) goctx.CancelFunc {
func hasResponseBeenWritten(argsIn []reflect.Value) bool { func hasResponseBeenWritten(argsIn []reflect.Value) bool {
for _, argIn := range argsIn { for _, argIn := range argsIn {
if statusProvider, ok := argIn.Interface().(ResponseStatusProvider); ok { if statusProvider, ok := argIn.Interface().(types.ResponseStatusProvider); ok {
if statusProvider.Written() { if statusProvider.WrittenStatus() != 0 {
return true return true
} }
} }
@ -161,7 +151,7 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
// wrap the response writer to check whether the response has been written // wrap the response writer to check whether the response has been written
resp := respOrig resp := respOrig
if _, ok := resp.(ResponseStatusProvider); !ok { if _, ok := resp.(types.ResponseStatusProvider); !ok {
resp = &responseWriter{respWriter: resp} resp = &responseWriter{respWriter: resp}
} }

View File

@ -17,7 +17,7 @@ type ContextDataStore interface {
type ContextData map[string]any type ContextData map[string]any
func (ds ContextData) GetData() map[string]any { func (ds ContextData) GetData() ContextData {
return ds return ds
} }

View File

@ -7,31 +7,31 @@ import (
"net/http" "net/http"
"strings" "strings"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"gitea.com/go-chi/binding" "gitea.com/go-chi/binding"
chi "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
// Bind binding an obj to a handler // Bind binding an obj to a handler's context data
func Bind[T any](_ T) any { func Bind[T any](_ T) http.HandlerFunc {
return func(ctx *context.Context) { return func(resp http.ResponseWriter, req *http.Request) {
theObj := new(T) // create a new form obj for every request but not use obj directly theObj := new(T) // create a new form obj for every request but not use obj directly
binding.Bind(ctx.Req, theObj) data := middleware.GetContextData(req.Context())
SetForm(ctx, theObj) binding.Bind(req, theObj)
middleware.AssignForm(theObj, ctx.Data) SetForm(data, theObj)
middleware.AssignForm(theObj, data)
} }
} }
// SetForm set the form object // SetForm set the form object
func SetForm(data middleware.ContextDataStore, obj interface{}) { func SetForm(dataStore middleware.ContextDataStore, obj interface{}) {
data.GetData()["__form"] = obj dataStore.GetData()["__form"] = obj
} }
// GetForm returns the validate form information // GetForm returns the validate form information
func GetForm(data middleware.ContextDataStore) interface{} { func GetForm(dataStore middleware.ContextDataStore) interface{} {
return data.GetData()["__form"] return dataStore.GetData()["__form"]
} }
// Route defines a route based on chi's router // Route defines a route based on chi's router

View File

@ -8,8 +8,8 @@ import (
"strings" "strings"
"time" "time"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web/types"
) )
// NewLoggerHandler is a handler that will log routing to the router log taking account of // NewLoggerHandler is a handler that will log routing to the router log taking account of
@ -86,8 +86,8 @@ func logPrinter(logger log.Logger) func(trigger Event, record *requestRecord) {
} }
var status int var status int
if v, ok := record.responseWriter.(context.ResponseWriter); ok { if v, ok := record.responseWriter.(types.ResponseStatusProvider); ok {
status = v.Status() status = v.WrittenStatus()
} }
logf := log.Info logf := log.Info
if strings.HasPrefix(req.RequestURI, "/assets/") { if strings.HasPrefix(req.RequestURI, "/assets/") {

View File

@ -0,0 +1,10 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package types
// ResponseStatusProvider is an interface to get the written status in the response
// Many packages need this interface, so put it in the separate package to avoid import cycle
type ResponseStatusProvider interface {
WrittenStatus() int
}

View File

@ -0,0 +1,17 @@
ASWF Digital Assets License v1.1
License for <Asset Name> (the "Asset Name").
<Asset Name> Copyright <Year> <Asset Owner>. All rights reserved.
Redistribution and use of these digital assets, with or without modification, solely for education, training, research, software and hardware development, performance benchmarking (including publication of benchmark results and permitting reproducibility of the benchmark results by third parties), or software and hardware product demonstrations, are permitted provided that the following conditions are met:
1. Redistributions of these digital assets or any part of them must include the above copyright notice, this list of conditions and the disclaimer below, and if applicable, a description of how the redistributed versions of the digital assets differ from the originals.
2. Publications showing images derived from these digital assets must include the above copyright notice.
3. The names of copyright holder or the names of its contributors may NOT be used to promote or to imply endorsement, sponsorship, or affiliation with products developed or tested utilizing these digital assets or benchmarking results obtained from these digital assets, without prior written permission from copyright holder.
4. The assets and their output may only be referred to as the Asset Name listed above, and your use of the Asset Name shall be solely to identify the digital assets. Other than as expressly permitted by this License, you may NOT use any trade names, trademarks, service marks, or product names of the copyright holder for any purpose.
DISCLAIMER: THESE DIGITAL ASSETS ARE PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THESE DIGITAL ASSETS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. <http://fsf.org/> Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
@ -215,7 +215,7 @@ To do so, attach the following notices to the program. It is safest to attach th
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
@ -227,6 +227,6 @@ If the program does terminal interaction, make it output a short notice like thi
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>. You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007 Version 3, 29 June 2007
Copyright © 2007 Free Software Foundation, Inc. <http://fsf.org/> Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
@ -215,7 +215,7 @@ To do so, attach the following notices to the program. It is safest to attach th
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
@ -227,6 +227,6 @@ If the program does terminal interaction, make it output a short notice like thi
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”.
You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>. You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <https://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@ -811,7 +811,10 @@ repo_and_org_access = Repository and Organization Access
permissions_public_only = Public only permissions_public_only = Public only
permissions_access_all = All (public, private, and limited) permissions_access_all = All (public, private, and limited)
select_permissions = Select permissions select_permissions = Select permissions
scoped_token_desc = Selected token scopes limit authentication only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information. permission_no_access = No Access
permission_read = Read
permission_write = Read and Write
access_token_desc = Selected token permissions limit authorization only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information.
at_least_one_permission = You must select at least one permission to create a token at_least_one_permission = You must select at least one permission to create a token
permissions_list = Permissions: permissions_list = Permissions:
@ -1153,6 +1156,7 @@ video_not_supported_in_browser = Your browser does not support the HTML5 'video'
audio_not_supported_in_browser = Your browser does not support the HTML5 'audio' tag. audio_not_supported_in_browser = Your browser does not support the HTML5 'audio' tag.
stored_lfs = Stored with Git LFS stored_lfs = Stored with Git LFS
symbolic_link = Symbolic link symbolic_link = Symbolic link
executable_file = Executable File
commit_graph = Commit Graph commit_graph = Commit Graph
commit_graph.select = Select branches commit_graph.select = Select branches
commit_graph.hide_pr_refs = Hide Pull Requests commit_graph.hide_pr_refs = Hide Pull Requests
@ -2903,7 +2907,7 @@ auths.sspi_default_language = Default user language
auths.sspi_default_language_helper = Default language for users automatically created by SSPI auth method. Leave empty if you prefer language to be automatically detected. auths.sspi_default_language_helper = Default language for users automatically created by SSPI auth method. Leave empty if you prefer language to be automatically detected.
auths.tips = Tips auths.tips = Tips
auths.tips.oauth2.general = OAuth2 Authentication auths.tips.oauth2.general = OAuth2 Authentication
auths.tips.oauth2.general.tip = When registering a new OAuth2 authentication, the callback/redirect URL should be: <host>/user/oauth2/<Authentication Name>/callback auths.tips.oauth2.general.tip = When registering a new OAuth2 authentication, the callback/redirect URL should be:
auths.tip.oauth2_provider = OAuth2 Provider auths.tip.oauth2_provider = OAuth2 Provider
auths.tip.bitbucket = Register a new OAuth consumer on https://bitbucket.org/account/user/<your username>/oauth-consumers/new and add the permission 'Account' - 'Read' auths.tip.bitbucket = Register a new OAuth consumer on https://bitbucket.org/account/user/<your username>/oauth-consumers/new and add the permission 'Account' - 'Read'
auths.tip.nextcloud = Register a new OAuth consumer on your instance using the following menu "Settings -> Security -> OAuth 2.0 client" auths.tip.nextcloud = Register a new OAuth consumer on your instance using the following menu "Settings -> Security -> OAuth 2.0 client"
@ -3428,11 +3432,9 @@ runners.owner_type = Type
runners.description = Description runners.description = Description
runners.labels = Labels runners.labels = Labels
runners.last_online = Last Online Time runners.last_online = Last Online Time
runners.agent_labels = Agent Labels
runners.custom_labels = Custom Labels
runners.custom_labels_helper = Custom labels are labels that are added manually by an administrator. A comma separates labels, whitespace at the start and end of each label is ignored.
runners.runner_title = Runner runners.runner_title = Runner
runners.task_list = Recent tasks on this runner runners.task_list = Recent tasks on this runner
runners.task_list.no_tasks = There is no task yet.
runners.task_list.run = Run runners.task_list.run = Run
runners.task_list.status = Status runners.task_list.status = Status
runners.task_list.repository = Repository runners.task_list.repository = Repository

View File

@ -912,7 +912,7 @@ repo_desc=説明
repo_desc_helper=簡単な説明を入力してください (オプション) repo_desc_helper=簡単な説明を入力してください (オプション)
repo_lang=言語 repo_lang=言語
repo_gitignore_helper=.gitignoreテンプレートを選択してください。 repo_gitignore_helper=.gitignoreテンプレートを選択してください。
repo_gitignore_helper_desc=追跡しないファイルの設定を、一般的な言語について用意されたテンプレートから選択します。 .gitignoreには、それぞれの言語のビルドツールが生成するファイルで、よくあるものがデフォルトで含まれています。 repo_gitignore_helper_desc=一般的な言語のテンプレートリストから、追跡しないファイルの設定を選択します。 各言語のビルドツールが生成する典型的なファイルが、デフォルトで.gitignoreに含まれます。
issue_labels=イシューラベル issue_labels=イシューラベル
issue_labels_helper=イシューのラベルセットを選択 issue_labels_helper=イシューのラベルセットを選択
license=ライセンス license=ライセンス
@ -2138,13 +2138,12 @@ settings.protected_branch.delete_rule=ルールを削除
settings.protected_branch_can_push=プッシュを許可する settings.protected_branch_can_push=プッシュを許可する
settings.protected_branch_can_push_yes=プッシュできます settings.protected_branch_can_push_yes=プッシュできます
settings.protected_branch_can_push_no=プッシュできません settings.protected_branch_can_push_no=プッシュできません
settings.branch_protection=ブランチ '<b>%s</b>' の保護
settings.protect_this_branch=ブランチの保護を有効にする settings.protect_this_branch=ブランチの保護を有効にする
settings.protect_this_branch_desc=ブランチの削除を防ぎ、ブランチへのプッシュやマージを制限します。 settings.protect_this_branch_desc=ブランチの削除を防ぎ、ブランチへのプッシュやマージを制限します。
settings.protect_disable_push=プッシュ無効 settings.protect_disable_push=プッシュ無効
settings.protect_disable_push_desc=このブランチへのプッシュは許可されません。 settings.protect_disable_push_desc=このブランチへのプッシュは許可されません。
settings.protect_enable_push=プッシュ有効 settings.protect_enable_push=プッシュ有効
settings.protect_enable_push_desc=書き込み権限があれば誰にでも、このブランチへのプッシュが許可されます。(強制プッシュ以外) settings.protect_enable_push_desc=誰でも書き込み権限があれば、このブランチへのプッシュが許可されます。(強制プッシュ以外)
settings.protect_whitelist_committers=ホワイトリストでプッシュを制限 settings.protect_whitelist_committers=ホワイトリストでプッシュを制限
settings.protect_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームにのみ、このブランチへのプッシュが許可されます。(強制プッシュ以外) settings.protect_whitelist_committers_desc=ホワイトリストに登録したユーザーまたはチームにのみ、このブランチへのプッシュが許可されます。(強制プッシュ以外)
settings.protect_whitelist_deploy_keys=プッシュ可能な書き込み権限を持つデプロイキーをホワイトリストに含める。 settings.protect_whitelist_deploy_keys=プッシュ可能な書き込み権限を持つデプロイキーをホワイトリストに含める。
@ -2157,7 +2156,6 @@ settings.protect_merge_whitelist_committers_desc=ホワイトリストに登録
settings.protect_merge_whitelist_users=マージ・ホワイトリストに含むユーザー: settings.protect_merge_whitelist_users=マージ・ホワイトリストに含むユーザー:
settings.protect_merge_whitelist_teams=マージ・ホワイトリストに含むチーム: settings.protect_merge_whitelist_teams=マージ・ホワイトリストに含むチーム:
settings.protect_check_status_contexts=ステータスチェックを有効にする settings.protect_check_status_contexts=ステータスチェックを有効にする
settings.protect_check_status_contexts_desc=マージの前にステータスチェックがパスしていることを必須にします。 このルールの対象ブランチへマージ可能となる前に、どのステータスチェックがパスしていなければならないかを選んでください。 有効にした場合は、まずコミットを別のブランチにプッシュし、ステータスチェックがパスしたあと、このルールの対象ブランチにマージ、または直接プッシュするようにします。 コンテキストを選択していない場合は、コンテキストに関係なく最後のコミットが成功している必要があります。
settings.protect_check_status_contexts_list=この1週間に、このリポジトリに対して行われたステータスチェック settings.protect_check_status_contexts_list=この1週間に、このリポジトリに対して行われたステータスチェック
settings.protect_required_approvals=必要な承認数: settings.protect_required_approvals=必要な承認数:
settings.protect_required_approvals_desc=肯定的なレビューの数を満たしたプルリクエストしかマージできないようにします。 settings.protect_required_approvals_desc=肯定的なレビューの数を満たしたプルリクエストしかマージできないようにします。

View File

@ -357,7 +357,7 @@ hi_user_x=Cześć <b>%s</b>,
activate_account=Aktywuj swoje konto activate_account=Aktywuj swoje konto
activate_account.title=%s, proszę aktywuj swoje konto activate_account.title=%s, proszę aktywuj swoje konto
activate_account.text_1=Cześć <b>%[1]s</b>, dziękujemy za rejestrację na %[2]! activate_account.text_1=Cześć <b>%[1]s</b>, dziękujemy za rejestrację na %[2]s!
activate_account.text_2=Kliknij poniższy link, aby aktywować swoje konto w ciągu <b>%s</b>: activate_account.text_2=Kliknij poniższy link, aby aktywować swoje konto w ciągu <b>%s</b>:
activate_email=Potwierdź swój adres e-mail activate_email=Potwierdź swój adres e-mail

View File

@ -1288,7 +1288,7 @@ projects.column.edit_title=Nome
projects.column.new_title=Nome projects.column.new_title=Nome
projects.column.new_submit=Criar coluna projects.column.new_submit=Criar coluna
projects.column.new=Nova coluna projects.column.new=Nova coluna
projects.column.set_default=Definir como predefinida projects.column.set_default=Tornar predefinida
projects.column.set_default_desc=Definir esta coluna como a predefinida para questões e pedidos de integração não categorizados projects.column.set_default_desc=Definir esta coluna como a predefinida para questões e pedidos de integração não categorizados
projects.column.unset_default=Deixar de ser a predefinida projects.column.unset_default=Deixar de ser a predefinida
projects.column.unset_default_desc=Faz com que esta coluna deixe de ser a predefinida projects.column.unset_default_desc=Faz com que esta coluna deixe de ser a predefinida

Some files were not shown because too many files have changed in this diff Show More